From aad631e58e8201c9aac3b28f8965051eddd037f4 Mon Sep 17 00:00:00 2001 From: andriypolanski Date: Fri, 31 Jul 2026 14:50:01 +0000 Subject: [PATCH 1/2] fix(miner): stop pickScript selecting watch/fix validation scripts (#10006) Exclude watch/write script-name segments from the pattern fallback and pick the lexicographically smallest survivor so verification never runs :watch or :fix. --- .../loopover-miner/lib/stack-detection.ts | 17 +++++- test/unit/miner-stack-detection.test.ts | 54 ++++++++++++++++++- .../miner-target-repo-verification.test.ts | 37 +++++++++++++ 3 files changed, 104 insertions(+), 4 deletions(-) diff --git a/packages/loopover-miner/lib/stack-detection.ts b/packages/loopover-miner/lib/stack-detection.ts index f749f1c82c..999353ab48 100644 --- a/packages/loopover-miner/lib/stack-detection.ts +++ b/packages/loopover-miner/lib/stack-detection.ts @@ -100,11 +100,24 @@ function parseJson(text: any) { } } -/** Pick a package.json script by exact name first, then by pattern, considering only string-valued scripts. */ +/** Script-name segments that denote a watch/non-terminating or write-mode variant (#10006). Matched + * case-insensitively against whole `:`-delimited segments only, so `test:fixtures` is kept while + * `test:watch` / `lint:fix` / `test:u` are not. */ +const EXCLUDED_SCRIPT_SEGMENTS = new Set(["watch", "dev", "serve", "fix", "write", "update", "u"]); + +function isExcludedScriptName(name: string): boolean { + return name.split(":").some((segment) => EXCLUDED_SCRIPT_SEGMENTS.has(segment.toLowerCase())); +} + +/** Pick a package.json script by exact name first, then by pattern, considering only string-valued scripts. + * Pattern fallback skips watch/write variants and picks the lexicographically smallest survivor (#10006). */ function pickScript(scripts: any, exactName: any, pattern: any) { const names = Object.keys(scripts).filter((name) => typeof scripts[name] === "string"); if (names.includes(exactName)) return exactName; - return names.find((name) => pattern.test(name)) ?? null; + const candidates = names + .filter((name) => pattern.test(name) && !isExcludedScriptName(name)) + .sort(); + return candidates[0] ?? null; } function nodeLockfile(exists: any) { diff --git a/test/unit/miner-stack-detection.test.ts b/test/unit/miner-stack-detection.test.ts index 94f8980341..09fe90ed50 100644 --- a/test/unit/miner-stack-detection.test.ts +++ b/test/unit/miner-stack-detection.test.ts @@ -99,13 +99,14 @@ describe("detectRepoStack — Node (#4785)", () => { it("matches script name variants and ignores non-string script values", () => { const result = detect({ "package.json": pkg({ - scripts: { build: 123, "compile:prod": "tsc -p .", "test:ci": "vitest run", "lint:fix": "eslint --fix", fmt: "biome format" }, + // #10006: use lint:ci (not lint:fix) — write-mode :fix segments are excluded from pattern fallback. + scripts: { build: 123, "compile:prod": "tsc -p .", "test:ci": "vitest run", "lint:ci": "eslint .", fmt: "biome format" }, }), }); expect(result).toMatchObject({ buildCommand: "npm run compile:prod", testCommand: "npm run test:ci", - lintCommand: "npm run lint:fix", + lintCommand: "npm run lint:ci", formatCommand: "npm run fmt", }); }); @@ -146,6 +147,55 @@ describe("detectRepoStack — Node (#4785)", () => { it("ignores a non-object scripts field", () => { expect(detect({ "package.json": pkg({ scripts: ["build"] }) })).toMatchObject({ buildCommand: null, testCommand: null }); }); + + it("REGRESSION: a watch-only or fix-only script is never selected as a validation command (#10006)", () => { + expect(detect({ "package.json": pkg({ scripts: { "test:watch": "vitest" } }) })).toMatchObject({ + testCommand: null, + }); + expect(detect({ "package.json": pkg({ scripts: { "build:watch": "tsc -w" } }) })).toMatchObject({ + buildCommand: null, + }); + expect(detect({ "package.json": pkg({ scripts: { "lint:fix": "eslint --fix ." } }) })).toMatchObject({ + lintCommand: null, + }); + expect(detect({ "package.json": pkg({ scripts: { "format:write": "prettier -w ." } }) })).toMatchObject({ + formatCommand: null, + }); + // Other excluded segments (dev/serve/update/u) — whole-segment match only. + expect(detect({ "package.json": pkg({ scripts: { "test:dev": "vitest" } }) })).toMatchObject({ testCommand: null }); + expect(detect({ "package.json": pkg({ scripts: { "test:serve": "vitest" } }) })).toMatchObject({ testCommand: null }); + expect(detect({ "package.json": pkg({ scripts: { "test:update": "vitest -u" } }) })).toMatchObject({ testCommand: null }); + expect(detect({ "package.json": pkg({ scripts: { "test:u": "vitest -u" } }) })).toMatchObject({ testCommand: null }); + // Near-miss: fixtures is not an excluded segment. + expect(detect({ "package.json": pkg({ scripts: { "test:fixtures": "node gen.js" } }) })).toMatchObject({ + testCommand: "npm run test:fixtures", + }); + }); + + it("exact-name scripts still win over watch/fix siblings (#10006)", () => { + expect( + detect({ + "package.json": pkg({ scripts: { test: "vitest run", "test:watch": "vitest" } }), + }), + ).toMatchObject({ testCommand: "npm test" }); + expect( + detect({ + "package.json": pkg({ scripts: { lint: "eslint .", "lint:fix": "eslint --fix ." } }), + }), + ).toMatchObject({ lintCommand: "npm run lint" }); + }); + + it("picks the lexicographically smallest non-excluded pattern candidate regardless of key order (#10006)", () => { + const unitFirst = detect({ + "package.json": pkg({ scripts: { "test:unit": "a", "test:e2e": "b" } }), + }); + const e2eFirst = detect({ + "package.json": pkg({ scripts: { "test:e2e": "b", "test:unit": "a" } }), + }); + expect(unitFirst).toMatchObject({ testCommand: "npm run test:e2e" }); + expect(e2eFirst).toMatchObject({ testCommand: "npm run test:e2e" }); + expect(unitFirst.testCommand).toBe(e2eFirst.testCommand); + }); }); describe("detectRepoStack — Python (#4785)", () => { diff --git a/test/unit/miner-target-repo-verification.test.ts b/test/unit/miner-target-repo-verification.test.ts index 2eee8b9f48..7f4485ba0c 100644 --- a/test/unit/miner-target-repo-verification.test.ts +++ b/test/unit/miner-target-repo-verification.test.ts @@ -82,6 +82,43 @@ describe("runTargetRepoVerification (#8807)", () => { expect(spawn).not.toHaveBeenCalled(); }); + it("SKIPS when detectRepoStack excluded every watch/fix-only script (#10006)", async () => { + const { detectRepoStack } = await import("../../packages/loopover-miner/lib/stack-detection"); + const { mkdtempSync, writeFileSync, rmSync } = await import("node:fs"); + const { tmpdir } = await import("node:os"); + const { join } = await import("node:path"); + const dir = mkdtempSync(join(tmpdir(), "miner-verify-excluded-scripts-")); + try { + writeFileSync( + join(dir, "package.json"), + JSON.stringify({ + scripts: { + "test:watch": "vitest", + "lint:fix": "eslint --fix .", + "build:watch": "tsc -w", + }, + }), + ); + const stack = detectRepoStack(dir); + expect(stack).toMatchObject({ + detected: true, + testCommand: null, + lintCommand: null, + buildCommand: null, + }); + const spawn = vi.fn(); + const result = await runTargetRepoVerification({ + worktreeDir: dir, + stack, + spawn: spawn as never, + }); + expect(result).toEqual({ status: "skipped", reason: "no_commands_detected" }); + expect(spawn).not.toHaveBeenCalled(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + it("the default spawn runs shell commands from the cwd, merges output, and enforces the timeout", async () => { const ok = await defaultVerificationSpawn("echo hello && echo err 1>&2", { cwd: process.cwd(), timeoutMs: DEFAULT_VERIFICATION_TIMEOUT_MS }); expect(ok.code).toBe(0); From de71145f2f1eddcd824f627bb9cfe34f405d5e6a Mon Sep 17 00:00:00 2001 From: andriypolanski Date: Fri, 31 Jul 2026 14:51:01 +0000 Subject: [PATCH 2/2] test(miner): drop union-unsafe testCommand access in stack-detection (#10006) Keep the lex-order assertion on toMatchObject only so root tsc stays green. Co-authored-by: Cursor --- test/unit/miner-stack-detection.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/unit/miner-stack-detection.test.ts b/test/unit/miner-stack-detection.test.ts index 09fe90ed50..91de9fc7d3 100644 --- a/test/unit/miner-stack-detection.test.ts +++ b/test/unit/miner-stack-detection.test.ts @@ -194,7 +194,6 @@ describe("detectRepoStack — Node (#4785)", () => { }); expect(unitFirst).toMatchObject({ testCommand: "npm run test:e2e" }); expect(e2eFirst).toMatchObject({ testCommand: "npm run test:e2e" }); - expect(unitFirst.testCommand).toBe(e2eFirst.testCommand); }); });