From 010e6196288ceee0723c339f406f298c31ea833a Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 14 Aug 2026 21:32:58 -0700 Subject: [PATCH] FN-9046: fix scoped pnpm dependent selector Correct workspace merge verification to test changed packages with their dependents. - Use pnpm's valid leading dependents selector for scoped merge tests. - Cover single, multiple, and shell-metacharacter package selectors. - Add a patch changeset for the verification fix. Files changed: .changeset/fn-9046-pnpm-filter-selector.md | 7 +++++ .../src/__tests__/merger-verification.test.ts | 33 +++++++++++++++++++--- .../src/merge/merger-workspace-test-commands.ts | 11 ++++++-- 3 files changed, 45 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-9046 Fusion-Task-Lineage: 07186296-36b7-42bb-aaa8-739dbf5e00d5 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-9046-pnpm-filter-selector.md | 7 ++++ .../src/__tests__/merger-verification.test.ts | 33 ++++++++++++++++--- .../merge/merger-workspace-test-commands.ts | 11 +++++-- 3 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 .changeset/fn-9046-pnpm-filter-selector.md diff --git a/.changeset/fn-9046-pnpm-filter-selector.md b/.changeset/fn-9046-pnpm-filter-selector.md new file mode 100644 index 0000000000..7aa1baab4d --- /dev/null +++ b/.changeset/fn-9046-pnpm-filter-selector.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Merge verification now runs tests for packages depending on a changed package. +category: fix +dev: `deriveScopedPnpmTestCommand` now uses `...` instead of malformed `...^` selectors. diff --git a/packages/engine/src/__tests__/merger-verification.test.ts b/packages/engine/src/__tests__/merger-verification.test.ts index 9f33f41e5b..d3704ae3e9 100644 --- a/packages/engine/src/__tests__/merger-verification.test.ts +++ b/packages/engine/src/__tests__/merger-verification.test.ts @@ -3000,7 +3000,8 @@ describe("inferDefaultTestCommand — pnpm workspace scoping", () => { }); const result = inferDefaultTestCommand("/tmp/root", undefined, undefined, "main", "fusion/fn-123"); - expect(result?.command).toBe(`pnpm --filter '@fusion/dashboard...^' test`); + expect(result?.command).toBe(`pnpm --filter '...@fusion/dashboard' test`); + expect(result?.command).not.toMatch(/\.\.\.\^/); expect(result?.testSource).toBe("inferred-scoped"); expect(mockedExecSync).toHaveBeenCalledWith( "git diff --name-only 'main'...'fusion/fn-123'", @@ -3036,12 +3037,36 @@ describe("inferDefaultTestCommand — pnpm workspace scoping", () => { }); const result = inferDefaultTestCommand("/tmp/root", undefined, undefined, "main", "fusion/fn-123"); - expect(result?.command).toContain("--filter"); - expect(result?.command).toContain("@fusion/dashboard"); - expect(result?.command).toContain("@fusion/engine"); + expect(result?.command).toBe( + "pnpm --filter '...@fusion/dashboard' --filter '...@fusion/engine' test", + ); + expect(result?.command).not.toMatch(/\.\.\.\^/); expect(result?.testSource).toBe("inferred-scoped"); }); + it("keeps a metacharacter package name shell-quoted inside a dependents selector", () => { + mockedExistsSync.mockImplementation((p: any) => { + const path = String(p); + return path.includes("pnpm-lock.yaml") || path.includes("pnpm-workspace.yaml") || path.includes("package.json"); + }); + mockedReadFileSync.mockImplementation((p: any) => { + const path = String(p); + if (path.includes("pnpm-workspace.yaml")) return `packages:\n - "packages/*"\n`; + if (path.includes("dashboard/package.json")) return JSON.stringify({ name: "@evil/pkg'; rm -rf /" }); + return JSON.stringify({ name: "unknown" }); + }); + mockedReaddirSync.mockReturnValue([ + { name: "dashboard", isDirectory: () => true }, + ] as any); + mockedExecSync.mockImplementation((cmd: any) => String(cmd).includes("git diff --name-only") + ? "packages/dashboard/src/index.ts\n" + : ""); + + const result = inferDefaultTestCommand("/tmp/root", undefined, undefined, "main", "fusion/fn-123"); + expect(result?.command).toBe("pnpm --filter '...@evil/pkg'\\''; rm -rf /' test"); + expect(result?.command).not.toMatch(/\.\.\.\^/); + }); + it("falls back to pnpm test (inferred) when all changes are root-only files", () => { mockedExistsSync.mockImplementation((p: any) => { const path = String(p); diff --git a/packages/engine/src/merge/merger-workspace-test-commands.ts b/packages/engine/src/merge/merger-workspace-test-commands.ts index 6fb5183109..c9dc0ac288 100644 --- a/packages/engine/src/merge/merger-workspace-test-commands.ts +++ b/packages/engine/src/merge/merger-workspace-test-commands.ts @@ -228,11 +228,18 @@ export function deriveScopedPnpmTestCommand(rootDir: string, baseBranch: string, } // 5. Compose the scoped pnpm command - // `...^` includes dependents (packages that import the changed packages). + /* + * FNXC:Verification 2026-08-15-04:18: + * pnpm `name...` selects a package with dependencies, `...name` selects it + * with dependents, and `...^name` excludes the package from its dependents. + * Merge verification needs `...name` so changed-package tests run alongside + * dependent tests; the former malformed trailing `name...^` could produce a + * zero-package, zero-test green verification. + */ // Package names come from workspace package.json files (potentially // untrusted) so we quote each filter argument via `quoteArg` to prevent // shell interpolation if a name contains metacharacters. - const filters = packageNames.map((name) => `--filter ${quoteArg(`${name}...^`)}`).join(" "); + const filters = packageNames.map((name) => `--filter ${quoteArg(`...${name}`)}`).join(" "); return `pnpm ${filters} test`; }