Просмотр исходного кода

fix: cross-platform tests and restore workspace protocol in manifests

Fixes two classes of issues that prevented the test suite from passing
on Windows and caused a latent publish bug:

1. Windows path-separator bugs in unit tests. studio.test.ts,
   studio-runtime.test.ts, and daemon.test.ts hard-coded POSIX paths
   (e.g. "/project/inkos.pid") while the production code uses path.join,
   which produces backslashes on Windows. Tests now build expected
   paths with path.join so they match whichever separator the current
   OS uses. No production code changes.

2. execFileSync("npm", ...) in publish-package.test.ts failed with
   ENOENT on Windows because npm ships as npm.cmd. Switched to npm.cmd
   plus shell: true on win32.

3. packages/cli/package.json and packages/studio/package.json had been
   committed in their prepack-normalized form, pinning
   @actalk/inkos-core and @actalk/inkos-studio to "1.1.1" instead of
   "workspace:*". This was correctly flagged by the
   "keeps source CLI dependencies linked through the workspace
   protocol" test, which was failing on every platform. Restored the
   workspace protocol so future publishes (via prepack rewrite) cut
   clean releases instead of locking siblings to the previous version.

Before: 43 failed / 32 passed on Windows.
After: 79 passed / 0 failed on Windows, typecheck clean across all
three packages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tahshsa 7 месяцев назад
Родитель
Сommit
49a41d86da

+ 2 - 2
packages/cli/package.json

@@ -43,8 +43,8 @@
     "typecheck": "tsc --noEmit"
   },
   "dependencies": {
-    "@actalk/inkos-core": "1.1.1",
-    "@actalk/inkos-studio": "1.1.1",
+    "@actalk/inkos-core": "workspace:*",
+    "@actalk/inkos-studio": "workspace:*",
     "commander": "^13.0.0",
     "dotenv": "^16.4.0",
     "epub-gen-memory": "^1.0.10",

+ 4 - 2
packages/cli/src/__tests__/daemon.test.ts

@@ -1,4 +1,5 @@
 import { beforeEach, describe, expect, it, vi } from "vitest";
+import { join } from "node:path";
 
 const readFileMock = vi.fn();
 const writeFileMock = vi.fn();
@@ -83,8 +84,9 @@ describe("daemon command", () => {
       upCommand.parseAsync(["node", "up", "--quiet"]),
     ).rejects.toMatchObject({ code: 1 });
 
-    expect(writeFileMock).toHaveBeenCalledWith("/project/inkos.pid", expect.any(String), "utf-8");
-    expect(unlinkMock).toHaveBeenCalledWith("/project/inkos.pid");
+    const pidPath = join("/project", "inkos.pid");
+    expect(writeFileMock).toHaveBeenCalledWith(pidPath, expect.any(String), "utf-8");
+    expect(unlinkMock).toHaveBeenCalledWith(pidPath);
 
     exitSpy.mockRestore();
   });

+ 3 - 1
packages/cli/src/__tests__/publish-package.test.ts

@@ -17,10 +17,12 @@ const sourceStudioPackageJsonPromise = readFile(resolve(studioDir, "package.json
 );
 
 async function packPackage(packageDir: string, packDir: string) {
-  execFileSync("npm", ["pack", "--pack-destination", packDir], {
+  const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
+  execFileSync(npmCmd, ["pack", "--pack-destination", packDir], {
     cwd: packageDir,
     env: process.env,
     encoding: "utf-8",
+    shell: process.platform === "win32",
   });
 
   const tgzFiles = (await readdir(packDir)).filter((name) => name.endsWith(".tgz"));

+ 35 - 21
packages/cli/src/__tests__/studio-runtime.test.ts

@@ -1,5 +1,5 @@
 import { beforeEach, describe, expect, it, vi } from "vitest";
-import { dirname, resolve } from "node:path";
+import { dirname, join, resolve } from "node:path";
 import { fileURLToPath } from "node:url";
 
 const accessMock = vi.fn();
@@ -16,12 +16,31 @@ describe("studio runtime resolution", () => {
     vi.resetModules();
   });
 
+  const tsSourceEntry = join("/repo", "packages", "studio", "src", "api", "index.ts");
+  const tsxLoader = join("/repo", "packages", "studio", "node_modules", "tsx", "dist", "loader.mjs");
+  const projectBuiltEntry = join(
+    "/repo",
+    "test-project",
+    "node_modules",
+    "@actalk",
+    "inkos-studio",
+    "dist",
+    "api",
+    "index.js",
+  );
+  const cliBuiltEntry = join(
+    cliPackageRoot,
+    "node_modules",
+    "@actalk",
+    "inkos-studio",
+    "dist",
+    "api",
+    "index.js",
+  );
+
   it("prefers the repository-local tsx loader for monorepo sources", async () => {
     accessMock.mockImplementation(async (path: string) => {
-      if (
-        path === "/repo/packages/studio/src/api/index.ts" ||
-        path === "/repo/packages/studio/node_modules/tsx/dist/loader.mjs"
-      ) {
+      if (path === tsSourceEntry || path === tsxLoader) {
         return;
       }
       throw new Error(`missing: ${path}`);
@@ -31,20 +50,15 @@ describe("studio runtime resolution", () => {
     const launch = await resolveStudioLaunch("/repo/test-project");
 
     expect(launch).toEqual({
-      studioEntry: "/repo/packages/studio/src/api/index.ts",
+      studioEntry: tsSourceEntry,
       command: "node",
-      args: [
-        "--import",
-        "/repo/packages/studio/node_modules/tsx/dist/loader.mjs",
-        "/repo/packages/studio/src/api/index.ts",
-        "/repo/test-project",
-      ],
+      args: ["--import", tsxLoader, tsSourceEntry, "/repo/test-project"],
     });
   });
 
   it("finds monorepo packages/studio sources from a project directory", async () => {
     accessMock.mockImplementation(async (path: string) => {
-      if (path === "/repo/packages/studio/src/api/index.ts") {
+      if (path === tsSourceEntry) {
         return;
       }
       throw new Error(`missing: ${path}`);
@@ -54,15 +68,15 @@ describe("studio runtime resolution", () => {
     const launch = await resolveStudioLaunch("/repo/test-project");
 
     expect(launch).toEqual({
-      studioEntry: "/repo/packages/studio/src/api/index.ts",
+      studioEntry: tsSourceEntry,
       command: "npx",
-      args: ["tsx", "/repo/packages/studio/src/api/index.ts", "/repo/test-project"],
+      args: ["tsx", tsSourceEntry, "/repo/test-project"],
     });
   });
 
   it("uses node for built JavaScript entries", async () => {
     accessMock.mockImplementation(async (path: string) => {
-      if (path === "/repo/test-project/node_modules/@actalk/inkos-studio/dist/api/index.js") {
+      if (path === projectBuiltEntry) {
         return;
       }
       throw new Error(`missing: ${path}`);
@@ -72,15 +86,15 @@ describe("studio runtime resolution", () => {
     const launch = await resolveStudioLaunch("/repo/test-project");
 
     expect(launch).toEqual({
-      studioEntry: "/repo/test-project/node_modules/@actalk/inkos-studio/dist/api/index.js",
+      studioEntry: projectBuiltEntry,
       command: "node",
-      args: ["/repo/test-project/node_modules/@actalk/inkos-studio/dist/api/index.js", "/repo/test-project"],
+      args: [projectBuiltEntry, "/repo/test-project"],
     });
   });
 
   it("falls back to the CLI installation's bundled studio runtime", async () => {
     accessMock.mockImplementation(async (path: string) => {
-      if (path === `${cliPackageRoot}/node_modules/@actalk/inkos-studio/dist/api/index.js`) {
+      if (path === cliBuiltEntry) {
         return;
       }
       throw new Error(`missing: ${path}`);
@@ -90,9 +104,9 @@ describe("studio runtime resolution", () => {
     const launch = await resolveStudioLaunch("/repo/test-project");
 
     expect(launch).toEqual({
-      studioEntry: `${cliPackageRoot}/node_modules/@actalk/inkos-studio/dist/api/index.js`,
+      studioEntry: cliBuiltEntry,
       command: "node",
-      args: [`${cliPackageRoot}/node_modules/@actalk/inkos-studio/dist/api/index.js`, "/repo/test-project"],
+      args: [cliBuiltEntry, "/repo/test-project"],
     });
   });
 });

+ 15 - 4
packages/cli/src/__tests__/studio.test.ts

@@ -1,4 +1,5 @@
 import { beforeEach, describe, expect, it, vi } from "vitest";
+import { join } from "node:path";
 
 const accessMock = vi.fn();
 const spawnMock = vi.fn(() => ({
@@ -28,8 +29,9 @@ describe("studio command", () => {
   });
 
   it("launches TypeScript sources through tsx in monorepo mode", async () => {
+    const tsEntry = join("/project", "packages", "studio", "src", "api", "index.ts");
     accessMock.mockImplementation(async (path: string) => {
-      if (path === "/project/packages/studio/src/api/index.ts") {
+      if (path === tsEntry) {
         return;
       }
       throw new Error(`missing: ${path}`);
@@ -40,7 +42,7 @@ describe("studio command", () => {
 
     expect(spawnMock).toHaveBeenCalledWith(
       "npx",
-      ["tsx", "/project/packages/studio/src/api/index.ts", "/project"],
+      ["tsx", tsEntry, "/project"],
       expect.objectContaining({
         cwd: "/project",
         stdio: "inherit",
@@ -50,8 +52,17 @@ describe("studio command", () => {
   });
 
   it("launches built JavaScript entries through node", async () => {
+    const jsEntry = join(
+      "/project",
+      "node_modules",
+      "@actalk",
+      "inkos-studio",
+      "dist",
+      "api",
+      "index.js",
+    );
     accessMock.mockImplementation(async (path: string) => {
-      if (path === "/project/node_modules/@actalk/inkos-studio/dist/api/index.js") {
+      if (path === jsEntry) {
         return;
       }
       throw new Error(`missing: ${path}`);
@@ -62,7 +73,7 @@ describe("studio command", () => {
 
     expect(spawnMock).toHaveBeenCalledWith(
       "node",
-      ["/project/node_modules/@actalk/inkos-studio/dist/api/index.js", "/project"],
+      [jsEntry, "/project"],
       expect.objectContaining({
         cwd: "/project",
         stdio: "inherit",

+ 1 - 1
packages/studio/package.json

@@ -28,7 +28,7 @@
     "typecheck": "tsc --noEmit && tsc -p tsconfig.server.json --noEmit"
   },
   "dependencies": {
-    "@actalk/inkos-core": "1.1.1",
+    "@actalk/inkos-core": "workspace:*",
     "@base-ui/react": "^1.3.0",
     "@fontsource-variable/geist": "^5.2.8",
     "@hono/node-server": "^1.13.0",