From 0f5e5c5183781bcdd80bca912d3743ff5eb9687e Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Mon, 7 Sep 2026 15:29:29 -0700 Subject: [PATCH] test(cli): assert source on every resolvePlatformBinary tier, and add the missing match-mode refusal platform-binary.test.ts's candidate-order suite already pins PATH's tie-break ordering but never read .source, so a resolver that always reported "platform-package" (the pinned-binary claim callers gate on) would have passed. Add source assertions to the two PATH-tier tests and a new case for the node_modules/.bin tier; platform-package and undefined are already covered in engine-version-consistency.test.ts. engine-dispatch.test.ts's REFUSALS table covers every other reason a runtime capture is refused but had no case for an unimplemented match: value, even though "must not be coerced to anchor" is asMatchMode's central claim. Add that case. Coverage only, no production changes. --- packages/cli/test/engine-dispatch.test.ts | 13 +++++ packages/cli/test/platform-binary.test.ts | 59 +++++++++++++++++++++-- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/packages/cli/test/engine-dispatch.test.ts b/packages/cli/test/engine-dispatch.test.ts index 8a2b1db2..7e31d4c2 100644 --- a/packages/cli/test/engine-dispatch.test.ts +++ b/packages/cli/test/engine-dispatch.test.ts @@ -434,6 +434,19 @@ describe("engine dispatch by directory", () => { RUNTIME_CAPTURE.replace("id: logs-abc12345\n", ""), /no string `id`/, ], + [ + // asMatchMode's central claim: an unimplemented `match:` is refused, not + // coerced to `anchor`. `anchor` and `broad` scan differently — `anchor` + // is a syntactic narrow, `broad` a whole-language enumerator — so + // silently falling back would run the capture as a narrow, match a + // fraction of what it was written for, and report the shortfall as a + // clean pass. The `it.each` above already asserts the capture is + // dropped from discovery; this asserts `verify` explains why, which is + // what REFUSALS exists to cover for every other reason. + "unimplemented match mode", + RUNTIME_CAPTURE.replace("match: anchor", "match: whole-repo"), + /match: "whole-repo", which this build does not implement/, + ], ]; it.each(REFUSALS)( diff --git a/packages/cli/test/platform-binary.test.ts b/packages/cli/test/platform-binary.test.ts index 50abb6b9..dce92d48 100644 --- a/packages/cli/test/platform-binary.test.ts +++ b/packages/cli/test/platform-binary.test.ts @@ -1,5 +1,6 @@ import { chmodSync, + mkdirSync, mkdtempSync, readFileSync, rmSync, @@ -167,7 +168,14 @@ describe("candidate order", () => { const originalPath = process.env.PATH; process.env.PATH = directory; try { - expect(resolvePlatformBinary(FAKE).path).toBe(join(directory, "sg")); + const resolution = resolvePlatformBinary(FAKE); + expect(resolution.path).toBe(join(directory, "sg")); + // The tier that answered, not just the path: a resolver that always + // reported "platform-package" would still pass every assertion above, + // since FAKE's platform-package tier is deliberately unresolvable and + // the path itself only proves *a* file was found, not which search tier + // found it. + expect(resolution.source).toBe("PATH"); } finally { process.env.PATH = originalPath; } @@ -178,14 +186,57 @@ describe("candidate order", () => { const originalPath = process.env.PATH; process.env.PATH = directory; try { - expect(resolvePlatformBinary(FAKE).path).toBe( - join(directory, "ast-grep") - ); + const resolution = resolvePlatformBinary(FAKE); + expect(resolution.path).toBe(join(directory, "ast-grep")); + expect(resolution.source).toBe("PATH"); } finally { process.env.PATH = originalPath; } }); + /** + * The tier `resolvePlatformBinary` searches between the pinned platform + * package and PATH: a binary linked into the CLI's own `node_modules/.bin`. + * + * `platform-package` and `undefined` are already pinned in + * `engine-version-consistency.test.ts`, and `PATH` above. Every closed-set + * value `source` can take needs its own case — a resolver that collapsed + * this tier's result to `"PATH"` (they are searched by the same loop, over + * the same candidate list) would pass every other test in this file. + */ + onUnix( + "reports node_modules/.bin as the source when that tier answers", + () => { + // Mirrors the computation `resolvePlatformBinary` uses internally + // (relative to the compiled module's own directory) — this is the exact + // location that tier searches, not a stand-in for it. + const localBin = join( + import.meta.dirname, + "..", + "src", + "node_modules", + ".bin" + ); + mkdirSync(localBin, { recursive: true }); + workspaces.push(join(import.meta.dirname, "..", "src", "node_modules")); + const binaryPath = join(localBin, "sg"); + writeFileSync(binaryPath, "#!/bin/sh\necho 'fake-tool 1.0.0'\n"); + chmodSync(binaryPath, 0o755); + + // Keep PATH from also containing a match, so only the .bin tier can + // answer. + const originalPath = process.env.PATH; + process.env.PATH = ""; + try { + const resolution = resolvePlatformBinary(FAKE); + expect(resolution.path).toBe(binaryPath); + expect(resolution.source).toBe("node_modules/.bin"); + } finally { + process.env.PATH = originalPath; + } + } + ); + it("names each searched location once, however many spellings it tried", () => { // The platform package is probed under one name, and a tier searched under // two spellings is still one place to look; repeating a label makes