diff --git a/bin/smoke/mutation-reproof.smoke.ts b/bin/smoke/mutation-reproof.smoke.ts index 3bea965b9..3db779ce9 100644 --- a/bin/smoke/mutation-reproof.smoke.ts +++ b/bin/smoke/mutation-reproof.smoke.ts @@ -90,6 +90,7 @@ function makeRepo(mutate: (root: string) => void): { root: string; base: string; "", ].join("\n")); writeFileSync(join(root, "smoke", "mutations", `${name}.mutations.json`), JSON.stringify({ + suite: `${name}.suite.mjs`, command: `node ${name}.suite.mjs`, mutations: [{ name: `the ${name} cap is removed`, @@ -228,7 +229,45 @@ try { ); } - // 5. A selected fixture whose suite is ALREADY RED before any mutation. a.mjs changes (so a's + // 5. A suite-only change must select the fixture that names that suite, even though neither its + // config nor guarded source changed. The other suite is the negative arm: changing a file whose + // name merely looks like a suite must not select either fixture. + { + const { root, base, head } = build((r) => { + writeFileSync(join(r, "a.suite.mjs"), [ + "// suite-only change; a.mjs and the fixture config are untouched", + "import { capped_a } from './a.mjs';", + "if (capped_a(100) !== 32) { console.error('✗ FAIL: the a cap holds'); process.exit(1); }", + "console.log('✓ the a cap holds');", + "", + ].join("\n")); + git(r, ["add", "a.suite.mjs"]); + git(r, ["commit", "--quiet", "-m", "change only a's suite"]); + }); + const { status, out } = scan(root, base, head); + check( + "a suite-only change selects exactly the fixture that names that suite", + status === 0 + && eq(selectedPaths(out), ["smoke/mutations/a.mutations.json"]) + && JSON.stringify(okCounts(out)) === JSON.stringify({ discriminated: 1, preRed: 0, inconclusive: 0 }), + `status=${status} selected=${JSON.stringify(selectedPaths(out))} counts=${JSON.stringify(okCounts(out))}\n${out}`, + ); + } + { + const { root, base, head } = build((r) => { + writeFileSync(join(r, "unrelated.suite.mjs"), "// not named by any fixture\n"); + git(r, ["add", "unrelated.suite.mjs"]); + git(r, ["commit", "--quiet", "-m", "change an unrelated suite"]); + }); + const { status, out } = scan(root, base, head); + check( + "an unrelated suite selects no fixture", + status === 0 && selectedPaths(out).length === 0, + `status=${status} selected=${JSON.stringify(selectedPaths(out))}\n${out}`, + ); + } + + // 6. A selected fixture whose suite is ALREADY RED before any mutation. a.mjs changes (so a's // fixture is selected) but a's suite fails unconditionally, so mutation-proof refuses at its // baseline (exit 4). That is the suite's own defect, not this diff's — the #1279/sandbox-guard // shape the reviewer reproduced. The gate must report it as PRE-RED, name exactly that fixture, @@ -263,7 +302,7 @@ try { ); } - // 6. INCONCLUSIVE (not a timeout — deterministic): a mutation that leaves the suite exiting 0 but + // 7. INCONCLUSIVE (not a timeout — deterministic): a mutation that leaves the suite exiting 0 but // never printing its named assertion. mutation-proof grades that INCONCLUSIVE, "a green status // is not a pass". The gate must report it as INCONCLUSIVE, not fail, and NOT collapse it into // SURVIVED (false blocker) or KILLED (false clearance). This is the outcome the reviewer flagged @@ -303,7 +342,7 @@ try { ); } - // 7. A real all-clear that reaches the OK summary (a fixture whose mutant is genuinely killed). + // 8. A real all-clear that reaches the OK summary (a fixture whose mutant is genuinely killed). // Its counts must read `discriminated > 0, pre-red 0, inconclusive 0` — the positive contrast to // cases 5 and 6, so "the gate worked and everything was clean" is not the same output as "every // selected fixture was pre-red or inconclusive". diff --git a/bin/smoke/mutations/mutation-reproof.json b/bin/smoke/mutations/mutation-reproof.json index e65f1909c..0fac61021 100644 --- a/bin/smoke/mutations/mutation-reproof.json +++ b/bin/smoke/mutations/mutation-reproof.json @@ -1,6 +1,6 @@ { "suite": "bin/smoke/mutation-reproof.smoke.ts", - "guard": "changed guarded source re-proves its fixture; a deleted or renamed-away source is a loud dangling failure naming the exact fixture; both sides of a rename enter the changed set", + "guard": "changed fixture config, suite, or guarded source re-proves its fixture; a deleted or renamed-away source is a loud dangling failure naming the exact fixture; both sides of a rename enter the changed set", "command": "pnpm smoke:mutation-reproof", "grades": "tool", "proveWith": "pnpm mutation-proof --config bin/smoke/mutations/mutation-reproof.json", @@ -21,6 +21,14 @@ "expectRed": "the gate FAILS on a renamed-away guarded source and names exactly the dangling fixture", "cell": "the gate FAILS on a renamed-away guarded source and names exactly the dangling fixture" }, + { + "name": "a changed configured suite is ignored by changed-set selection", + "file": "scripts/mutation-reproof.mjs", + "find": " || (typeof suite === \"string\" && changed.has(suite))\n", + "replace": "", + "expectRed": "a suite-only change selects exactly the fixture that names that suite", + "cell": "a suite-only change selects exactly the fixture that names that suite" + }, { "name": "a dangling fixture is no longer refused, so a deleted source stops being a loud failure", "file": "scripts/mutation-reproof.mjs", diff --git a/extensions/connector-claude-code/smoke/mutations/boot-wake-race.json b/extensions/connector-claude-code/smoke/mutations/boot-wake-race.json index 90f5d9e81..900126fa4 100644 --- a/extensions/connector-claude-code/smoke/mutations/boot-wake-race.json +++ b/extensions/connector-claude-code/smoke/mutations/boot-wake-race.json @@ -1,6 +1,6 @@ { "suite": "extensions/connector-claude-code/smoke/boot-wake-race.smoke.ts", - "guard": "a wake dropped in the connector's boot window (message already pending before the claude/channel handshake activates) is re-fired without a human turn, so the session is never permanently deaf", + "guard": "a wake dropped in the connector's boot window (message already pending before the claude/channel handshake activates) is re-fired by activation itself, so the session is never permanently deaf", "command": "pnpm --filter @cotal-ai/connector-claude-code... build && pnpm smoke:claude-boot-wake", "proveWith": "node scripts/mutation-proof.mjs --config extensions/connector-claude-code/smoke/mutations/boot-wake-race.json", "why": [ @@ -9,34 +9,34 @@ "while proving nothing about the boot window, so the recovery has to be traced to the one line", "that performs it.", "", - "The connector itself has no reconcile and the cell asserts that too: activation only flips the", - "flag (hooks.ts setChannelActive), nudge() returns early while inactive, and the retry is", - "unreachable in that window because it is armed only from a REJECTED notify, which cannot happen", - "when nudge returns before notifying. So the whole recovery rests on ingest re-announcing the", - "still-pending item when JetStream redelivers it, and B1 deletes exactly that.", + "THAT LINE IS THE ACTIVATION RECONCILE, not the durable re-announce. This fixture used to mutate", + "the re-announce in `connector-core/src/agent.ts`, from a time when the suite's recovery ran", + "through JetStream redelivery. It does not any more: the suite settles each scenario in a 500ms", + "window chosen so that no redelivery can have happened, and its header says the redelivery guard", + "lives in `smoke:cross-path-dedup` and `smoke:claude-wake`. So the old mutation changed nothing", + "this suite observes, the suite passed with it applied, and it graded UNGRADABLE rather than", + "either verdict. The re-announce is now graded where it is driven, in", + "`extensions/connector-core/smoke/mutations/durable-redelivery.json`.", "", - "REBUILD IS PART OF THE EXPERIMENT, NOT AN OPTIMIZATION. The suite loads the connector's own", - "wake policy by relative source path (../src/hooks.js) but reaches MeshAgent through the package", - "name, which resolves to dist/. Editing src/agent.ts alone therefore mutates bytes the run never", - "reads: measured here, the first attempt at B1 SURVIVED with the source visibly mutated, purely", - "because dist/ still held the pre-mutation build. Hence the build in `command` and the rebuild in", - "`afterRestore` — dist/ is gitignored, so putting the source back is not enough to leave the tree", - "honest.", + "What is left here is the recovery this suite really does drive: `setChannelActive(false → true)`", + "reconciling one buffered wake, with nothing else in the window able to produce a nudge. The", + "mention branch above it is left in place, so the #917 scenario still passes and this mutation", + "reddens only the buffered-wake cell it names.", "", - "One mutation, deliberately: the guard is a single line, and B1 is its exact removal — the state", - "of this branch before the re-announce landed. A second contrived mutant would grade nothing the", - "first does not." + "REBUILD IS PART OF THE EXPERIMENT, NOT AN OPTIMIZATION. `command` builds the mutated source into", + "dist before each run, so `afterRestore` has to rebuild from the restored source; dist is", + "gitignored, and putting the source back is not enough to leave the tree honest." ], "mutations": [ { - "name": "B1 a durable redelivery of a still-pending item refreshes the ack handle without re-announcing it", - "file": "extensions/connector-core/src/agent.ts", - "find": " existing.ack = delivery.ack;\n this.emit(\"incoming\", existing.item);", - "replace": " existing.ack = delivery.ack;", - "expectRed": "a wake lost in the boot window is re-fired without a human turn", - "cell": "a wake lost in the boot window is re-fired without a human turn (JetStream redelivery re-announces the still-pending item)", + "name": "B1 activation flips the flag without reconciling the wake that was already buffered", + "file": "extensions/connector-claude-code/src/hooks.ts", + "find": " else if (agent.pendingWake() > 0) nudge();", + "replace": "", + "expectRed": "the claude/channel handshake reconciles a buffered wake that was already pending", + "cell": "the claude/channel handshake reconciles a buffered wake that was already pending", "afterRestore": "pnpm --filter @cotal-ai/connector-claude-code... build", - "note": "Restores the swallow this branch had before the re-announce: the redelivery arrives, the ack handle is refreshed, and nothing tells the wake policy the message is still there. The session then sits idle with the DM buffered and un-acked for as long as the cell waits." + "note": "Restores the #226 defect exactly: activation sets `channelActive` and stops there. `nudge()` returns early while inactive, and the retry timer is armed only from a REJECTED notify, which cannot happen when nudge returns before notifying. With this line gone nothing in the boot window wakes the session at all." } ] } diff --git a/extensions/connector-core/smoke/fixtures/connection-status.mutations.json b/extensions/connector-core/smoke/fixtures/connection-status.mutations.json index 99fe8acbe..08597e2ca 100644 --- a/extensions/connector-core/smoke/fixtures/connection-status.mutations.json +++ b/extensions/connector-core/smoke/fixtures/connection-status.mutations.json @@ -3,13 +3,14 @@ "guard": "cotal_connection_status reports MeshAgent's live state, and its five states stay distinct", "command": "pnpm smoke:connection-status", "progressPattern": " ✓ ", - "completionMarker": "SUITE COMPLETE: 15 cells", + "completionMarker": "SUITE COMPLETE:", "proveWith": "node scripts/mutation-proof.mjs --config extensions/connector-core/smoke/fixtures/connection-status.mutations.json", "why": [ "The state a caller acts on is derived from three facts, so each collapse of that derivation gets its own mutation rather than one mutation standing in for the whole getter. A single mutation on connectionState would be killed by whichever cell ran first and would leave the other states ungraded.", "M1 and M6 mutate the two liveness getters, which proves the tool reports live MeshAgent state rather than deriving or assuming it. M2 to M4 collapse one state into another, which proves each state is separately observable. M5 mutates the issue scoping, which is the only thing keeping a stopped session's post-mortem from being read as a current fault.", "The full predicted kill set, including every exclusion, is recorded in the suite header before this config is run.", - "The suite imports connector-core source relatively, so mutation-proof executes the changed source directly and needs no build or afterRestore command." + "The suite imports connector-core source relatively, so mutation-proof executes the changed source directly and needs no build or afterRestore command.", + "The completion marker carries no cell count on purpose. Each of these suites prints `SUITE COMPLETE: ${ran} cells` and separately refuses a run whose `ran` is not its EXPECTED_CELLS, so a count in the marker duplicates a check the suite already makes and rots the moment a cell is added. It did: a cell landed in the suite, the marker still said 15, and every mutant here graded INCONCLUSIVE for a week because a finished run printed a line the marker could never match." ], "mutations": [ { diff --git a/extensions/connector-core/smoke/fixtures/transport-liveness-broker.mutations.json b/extensions/connector-core/smoke/fixtures/transport-liveness-broker.mutations.json index 9230fad30..8449fad3a 100644 --- a/extensions/connector-core/smoke/fixtures/transport-liveness-broker.mutations.json +++ b/extensions/connector-core/smoke/fixtures/transport-liveness-broker.mutations.json @@ -3,7 +3,7 @@ "guard": "real NATS transport edges reach CotalEndpoint and MeshAgent without flapping readiness", "command": "pnpm --filter @cotal-ai/core build && pnpm smoke:transport-liveness:broker", "progressPattern": " \u2713 ", - "completionMarker": "SUITE COMPLETE: 13 cells", + "completionMarker": "SUITE COMPLETE:", "proveWith": "node scripts/mutation-proof.mjs --config extensions/connector-core/smoke/fixtures/transport-liveness-broker.mutations.json", "why": [ "The unit transport-liveness suite proves the stop-versus-bind race by replacing connectAndBind", @@ -23,7 +23,8 @@ "doRebuild's tearDownIfStopped. That helper is what prevents a rebuild that already emitted", "connection:true (the shared tail ran before stop completed) from leaving nc+heartbeat+supervisor", "live and from supervising a stopped endpoint. A shared-tail mutation is deliberately NOT used to", - "grade the rebuild cell." + "grade the rebuild cell.", + "The completion marker carries no cell count on purpose. Each of these suites prints `SUITE COMPLETE: ${ran} cells` and separately refuses a run whose `ran` is not its EXPECTED_CELLS, so a count in the marker duplicates a check the suite already makes and rots the moment a cell is added. It did: a cell landed in the suite, the marker still said 15, and every mutant here graded INCONCLUSIVE for a week because a finished run printed a line the marker could never match." ], "mutations": [ { diff --git a/extensions/connector-core/smoke/fixtures/transport-liveness.mutations.json b/extensions/connector-core/smoke/fixtures/transport-liveness.mutations.json index bbfaecd4a..2b7aedb66 100644 --- a/extensions/connector-core/smoke/fixtures/transport-liveness.mutations.json +++ b/extensions/connector-core/smoke/fixtures/transport-liveness.mutations.json @@ -3,13 +3,14 @@ "guard": "raw NATS transport liveness is epoch-safe and separate from full endpoint readiness", "command": "pnpm --filter @cotal-ai/core build && pnpm smoke:transport-liveness", "progressPattern": " \u2713 ", - "completionMarker": "SUITE COMPLETE: 20 cells", + "completionMarker": "SUITE COMPLETE:", "proveWith": "node scripts/mutation-proof.mjs --config extensions/connector-core/smoke/fixtures/transport-liveness.mutations.json", "why": [ "The deterministic status queues grade the exact nats.js lifecycle contract without timing a broker outage. The companion broker smoke separately proves real public disconnect and reconnect events reach these paths.", "Core is rebuilt before each run and after every restore because connector-core resolves @cotal-ai/core through dist. Connector-core's agent source is imported relatively by the suite.", "The full predicted kill sets, including every exclusion, are recorded in the suite header before this config is run.", - "M16 is defensive-only, not a real-entry claim. Temporary instrumentation around the real watchStatus catch fired zero times across five complete broker-companion runs on pinned nats.js 3.4.0, each covering loss, reconnect, manual epoch replacement, and terminal close. The client's status iterator closes normally on those paths. The controlled rejection cell and mutation keep catch behavior epoch-consistent if a runtime or future client version can reject, but no real cell is claimed because no reachable rejection was measured." + "M16 is defensive-only, not a real-entry claim. Temporary instrumentation around the real watchStatus catch fired zero times across five complete broker-companion runs on pinned nats.js 3.4.0, each covering loss, reconnect, manual epoch replacement, and terminal close. The client's status iterator closes normally on those paths. The controlled rejection cell and mutation keep catch behavior epoch-consistent if a runtime or future client version can reject, but no real cell is claimed because no reachable rejection was measured.", + "The completion marker carries no cell count on purpose. Each of these suites prints `SUITE COMPLETE: ${ran} cells` and separately refuses a run whose `ran` is not its EXPECTED_CELLS, so a count in the marker duplicates a check the suite already makes and rots the moment a cell is added. It did: a cell landed in the suite, the marker still said 15, and every mutant here graded INCONCLUSIVE for a week because a finished run printed a line the marker could never match." ], "mutations": [ { diff --git a/extensions/connector-core/smoke/mutations/durable-redelivery.json b/extensions/connector-core/smoke/mutations/durable-redelivery.json new file mode 100644 index 000000000..c7c37ae3a --- /dev/null +++ b/extensions/connector-core/smoke/mutations/durable-redelivery.json @@ -0,0 +1,51 @@ +{ + "suite": "extensions/connector-core/smoke/cross-path-dedup.smoke.ts", + "guard": "a JetStream redelivery of an item that is still pending re-announces it through the ordinary incoming path and commits the freshest ack handle, so a wake the host dropped comes back on its own", + "command": "pnpm smoke:cross-path-dedup", + "progressPattern": " ✓ ", + "completionMarker": "CROSS-PATH DEDUP SMOKE", + "proveWith": "node scripts/mutation-proof.mjs --config extensions/connector-core/smoke/mutations/durable-redelivery.json", + "why": [ + "The re-announce is what turns JetStream redelivery into a timer-free retry for a wake the host", + "dropped, and it is two lines doing two jobs: refresh the ack handle so the commit lands on the", + "live one, and tell the policy the message is still there. A mutation on either alone leaves the", + "other in place, which is why they are graded separately rather than as one removal.", + "", + "THE MUTANTS LIVE HERE BECAUSE THIS IS THE SUITE THAT DRIVES THEM. They were filed against the", + "claude boot-wake suite, whose own header says the durable redelivery guard moved out to this one", + "and to `smoke:claude-wake`. That suite settles every scenario inside a 500ms window and never", + "waits for a redelivery, so removing the re-announce changed nothing it observes and the mutation", + "graded UNGRADABLE for months: the suite passed, and no other mutation in the same file was", + "killed in that run to show whether the suite reached the file at all.", + "", + "This suite drives both deliveries by hand through `agent.ep.emit`, so the redelivery case is a", + "deterministic two-emit sequence rather than a wait on ack_wait, and it reads MeshAgent from", + "source (`../src/agent.js`), so no build sits between the mutation and the run.", + "", + "EACH MUTATION NAMES THE FIRST CELL IT REDDENS, which is in the live-first/durable-second case", + "rather than the redelivery case below it. The two lines serve both: a durable delivery that", + "finds a pending entry upgrades it the same way whether the first copy arrived live or durable.", + "This suite asserts through `assert.ok` and so stops at its first red, and naming a later cell", + "would grade a line the run never reaches." + ], + "mutations": [ + { + "name": "R1 a durable redelivery of a still-pending item refreshes the ack handle without re-announcing it", + "file": "extensions/connector-core/src/agent.ts", + "find": " existing.ack = delivery.ack;\n this.emit(\"incoming\", existing.item);", + "replace": " existing.ack = delivery.ack;", + "expectRed": "live-first/durable-second: the durable pending duplicate re-announces through incoming", + "cell": "live-first/durable-second: the durable pending duplicate re-announces through incoming", + "note": "Restores the swallow this branch had before the re-announce landed: the redelivery arrives, the ack handle is refreshed, and nothing tells the wake policy the message is still there. A session in that state sits idle with the message buffered and un-acked for as long as anything waits on it." + }, + { + "name": "R2 a durable redelivery re-announces but keeps the stale ack handle, so the commit lands on a handle the server has already replaced", + "file": "extensions/connector-core/src/agent.ts", + "find": " existing.ack = delivery.ack;\n this.emit(\"incoming\", existing.item);", + "replace": " this.emit(\"incoming\", existing.item);", + "expectRed": "live-first/durable-second: the DURABLE ack committed (upgraded from the live no-op)", + "cell": "live-first/durable-second: the DURABLE ack committed (upgraded from the live no-op)", + "note": "The other half of the same two lines, and the positive control for R1: a run that kills this one has demonstrably reached this file, so an R1 that survived would be a reportable coverage gap rather than an ungradable one." + } + ] +} diff --git a/implementations/auth/smoke/user-spawn.smoke.ts b/implementations/auth/smoke/user-spawn.smoke.ts index bfa60b0f6..85390e3ee 100644 --- a/implementations/auth/smoke/user-spawn.smoke.ts +++ b/implementations/auth/smoke/user-spawn.smoke.ts @@ -1515,6 +1515,12 @@ try { } catch (e) { fail++; console.error(" ✗ scenario threw:", (e as Error).stack ?? (e as Error).message); + // THE SUMMARY BELONGS ON BOTH PATHS. A cell here can red and the cascade behind it throw: the + // refusal text that the next lines parse is missing, so the flags they extract are empty and the + // real ledger writer refuses them. Printing the verdict only on the success path meant such a run + // ended silently, and a reader (or a mutation gate keyed on this line) saw an unfinished run + // rather than a failed one. + console.log(`\nUSER-SPAWN SMOKE FAILED ❌ (${pass} passed, ${fail} failed)`); process.exitCode = 1; } finally { try { await observer?.stop(); } catch { /* */ } diff --git a/implementations/manager/smoke/boot-self-heal-gate.smoke.ts b/implementations/manager/smoke/boot-self-heal-gate.smoke.ts index 4a5ecd453..35c0c13f9 100644 --- a/implementations/manager/smoke/boot-self-heal-gate.smoke.ts +++ b/implementations/manager/smoke/boot-self-heal-gate.smoke.ts @@ -267,6 +267,11 @@ try { const familyBefore = await endpointRegistrationBarrier(kv, SPACE, { endpoint: MANAGER_ENDPOINT, instanceId: iid, opId: mintLifecycleUid() }).enumerate(); const r = await startSuccessor(); + // A SUCCESSOR THAT STARTED WHEN IT SHOULD HAVE REFUSED MUST STILL BE STOPPED. It is a live + // manager with timers and a broker connection, so leaving it running holds the event loop open + // and the whole suite hangs after its last cell. A hang prints no verdict and grades as no + // evidence, which is the opposite of what a cell that just went red is for. + if (r.ok) await r.mgr.stop().catch(() => {}); check("LIVE HOLDER: successor start REFUSES", r.ok === false, r.ok ? "started" : undefined); check("LIVE HOLDER: the refusal is named `holder-alive`", r.ok === false && conditionOf(r.error) === "holder-alive", @@ -292,6 +297,7 @@ try { const { kv, nc } = await execKv(iid); const before = await readGate(kv, iid); const r = await startSuccessor(); + if (r.ok) await r.mgr.stop().catch(() => {}); // same reason as CELL 2: a started successor keeps the process alive check("NO ORACLE: successor start REFUSES", r.ok === false, r.ok ? "started" : undefined); check("NO ORACLE: the refusal is named `liveness-unestablishable` (silence is not death)", r.ok === false && conditionOf(r.error) === "liveness-unestablishable", diff --git a/implementations/manager/smoke/events-grant-acl.smoke.ts b/implementations/manager/smoke/events-grant-acl.smoke.ts index 1349da0ae..66cc59a52 100644 --- a/implementations/manager/smoke/events-grant-acl.smoke.ts +++ b/implementations/manager/smoke/events-grant-acl.smoke.ts @@ -590,6 +590,14 @@ try { ); } +} catch (e) { + // A THROW IS A FAILING RUN, NOT AN ABSENT ONE. Several cells here feed the next: a rule that + // stops refusing lets a spawn through, and the section that parses the refusal then mints on an + // empty profile and dies inside `mintCreds`. Without this arm the process ended before the + // summary below, so a run that had already reddened the right cell printed no verdict at all and + // graded INCONCLUSIVE rather than as the failure it was. + failures++; + console.log(`✗ the scenario threw before it finished — ${(e as Error).stack ?? (e as Error).message}`); } finally { await stopBroker(); rmSync(workspaceRoot, { recursive: true, force: true }); diff --git a/scripts/mutation-reproof.mjs b/scripts/mutation-reproof.mjs index 4ba0a56c1..89f2f1e97 100644 --- a/scripts/mutation-reproof.mjs +++ b/scripts/mutation-reproof.mjs @@ -81,7 +81,7 @@ function loadCorpus(root, paths) { errors.push(`${path}: no top-level "mutations" array`); continue; } - fixtures.push({ path, mutations: config.mutations }); + fixtures.push({ path, suite: config.suite, mutations: config.mutations }); } return { fixtures, errors }; } @@ -158,7 +158,8 @@ if (errors.length) { const { changed, diffSize } = a.all ? { changed: new Set(), diffSize: 0 } : changedSet(root, a.base, head); -let selected = fixtures.filter(({ path, mutations }) => a.all || changed.has(path) +let selected = fixtures.filter(({ path, suite, mutations }) => a.all || changed.has(path) + || (typeof suite === "string" && changed.has(suite)) || mutations.some((mutation) => typeof mutation?.file === "string" && changed.has(mutation.file))); if (shard) selected = selected.filter(({ path }) => shardOf(path, Number(shard[2])) === Number(shard[1])); @@ -186,7 +187,7 @@ console.log(`mutation reproof: ${selected.length} fixture(s) selected from ${fix ? " for a full sweep" : ` (diff ${diffSize} record(s), ${changed.size} changed path(s), corpus ${fixtures.length})`)); if (selected.length === 0) { - console.log("No mutation fixtures to re-prove: no fixture config or guarded source intersects the diff."); + console.log("No mutation fixtures to re-prove: no fixture config, suite, or guarded source intersects the diff."); process.exit(0); } console.log(`selected fixture paths:\n${selected.map(({ path }) => ` ${path}`).join("\n")}`);