Skip to content

test(mutation): add frontend activation smoke guard - #2931

Open
Chris0Jeky wants to merge 9 commits into
mainfrom
codex/maintenance-health
Open

Chris0Jeky wants to merge 9 commits into
mainfrom
codex/maintenance-health

Conversation

@Chris0Jeky

@Chris0Jeky Chris0Jeky commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Summary

Add a contained mutation:smoke activation guard for the frontend mutation lane and run it after
npm ci and before the non-blocking full Stryker report, so an incompatible Stryker/Vitest pairing
fails loudly instead of producing an apparently valid zero-execution report. Reconcile the mutation
policy and testing guide with the manual-dispatch workflow and the smoke preflight.

The Vitest 4.1.x downgrade this PR originally carried has been dropped. See "Re-resolution
against main" below. frontend/taskdeck-web/package-lock.json is now byte-identical to main, and
package.json differs from main by the single added mutation:smoke script line.

Changed surface

File What
frontend/taskdeck-web/stryker.smoke.config.mjs new: one mutated expression, four mutants, break: 100
frontend/taskdeck-web/scripts/check-mutation-smoke.mjs new: fail-closed assertion on the smoke report
frontend/taskdeck-web/package.json one added script
.github/workflows/mutation-testing.yml one added step in the manual-dispatch frontend job
.gitignore ignore stryker-smoke-tmp/
docs/TESTING_GUIDE.md, docs/testing/MUTATION_TESTING_POLICY.md reconcile

Re-resolution against main (2026-09-12)

main moved under this branch, and the merge exposed two defects that a textual merge does not catch.

1. The mutated range went dead, and the guard stayed green anyway. The smoke pinned
src/store/board/boardCrudStore.ts:587:28-587:78. main moved the board-list deletion expression
to line 591. Stryker then instruments zero mutants, reports a mutation score of NaN, logs
Final mutation score of NaN is greater than or equal to break threshold 100 and exits 0.
Measured on the merged tree before the fix: exit code 0, # killed 0, "Ran NaN tests per mutant".
thresholds.break cannot catch an empty probe, so the activation guard would have gone green in CI
while proving nothing — precisely the failure mode it exists to prevent.

Fixed two ways: the range is re-pointed at line 591, and scripts/check-mutation-smoke.mjs now
reads the smoke run's JSON report and requires exactly four mutants, all Killed. Proven both
directions on this head — 4/4 killed exits 0 with mutation:smoke guard OK: 4/4 mutants killed;
the stale 587 range exits 1 with a message naming the moved seam.

2. The Vitest 4.1.x downgrade is no longer needed. The original rationale reproduces exactly:
with @stryker-mutator/vitest-runner 10.0.0 on this repository's Vitest 5.0.0 line, the smoke
reports Ran 0.00 tests per mutant and all four mutants survive. 10.0.0 is still the latest
published runner and its peer range (vitest: >=2.0.0) does not express the incompatibility, so
there is no newer runner to upgrade to.

But the downgrade is not the only fix. Driving the same probe through Stryker's command test
runner (npx vitest --run --maxWorkers=2 <spec>, coverageAnalysis: 'off') kills 4/4 in ~13 s on
Vitest 5.0.0
. The smoke therefore no longer needs a runner-compatible Vitest pin, and main's
Vitest 5 line from #2803 / #2815 stays intact rather than being reverted by a test-infrastructure PR.
That removes package-lock.json from the diff entirely.

Trade-off, stated plainly: the smoke now exercises Vitest through the CLI rather than through
@stryker-mutator/vitest-runner, which is the component the full npx stryker run step that
follows it still depends on. So the smoke proves "Stryker can mutate this seam and the suite kills
the mutants", and it no longer proves "the vitest-runner integration is healthy". The known
vitest-runner/Vitest-5 zero-execution behaviour is recorded in
docs/testing/MUTATION_TESTING_POLICY.md with the measurement, and the full run remains advisory.

Verification (on head 9b41866d5, Windows, local)

Ran and passed:

  • npm ci --ignore-scripts
  • npm run mutation:smoke — 4/4 mutants killed, 100%, ~13 s, guard prints 4/4 mutants killed, exit 0
  • negative control: the stale 587 range — Stryker exits 0 at score NaN, the guard exits 1
  • @stryker-mutator/vitest-runner on Vitest 5.0.0 — reproduces Ran 0.00 tests per mutant, 4 survived (the measurement behind the config choice)
  • npx vitest --run --maxWorkers=2 src/tests/store/board/boardCrudStore.spec.ts — 70/70 passed on Vitest 5.0.0
  • npm run typecheck — clean
  • npx eslint scripts/check-mutation-smoke.mjs stryker.smoke.config.mjs — clean
  • node scripts/check-docs-governance.mjs — passed
  • node scripts/check-doc-links.mjs — 695 Markdown files, 0 broken links
  • powershell -File scripts/ci/Test-StrykerConfig.ps1 -SelfTest — 35/35 passed

NOT run / not claimed here: the full frontend Vitest suite, npm run build, the full frontend
mutation report, any backend test, browser or device proof, and a manual Mutation Testing
workflow dispatch at this head (the previous dispatch at 0e367fb3a,
run 34518952589, predates this
re-resolution). Hosted required CI at this head is the authority for everything above.

Merge gate

This is a control-plane PR under ci/policy.v1.json: it changes
.github/workflows/mutation-testing.yml and frontend/taskdeck-web/package.json, both declared
control paths, so it is R4. It is recorded as parked on OUTSTANDING_TASKS.md §J.3 pending the
maintainer's answer to J.3 (b) — whether the ADR-0066 per-PR maintainer review still binds new
control-plane PRs. No agent merges this while (b) is unanswered.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-10T18:21:27.000432Z 0e367fb PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Chris0Jeky

Chris0Jeky commented Sep 10, 2026

Copy link
Copy Markdown
Owner Author

Fresh-context control-path review completed at 0e367fb against base b3edd1e.

Result: no CRITICAL or HIGH findings.

Verified:

  • activation smoke is ordered after npm ci and before full Stryker
  • the smoke range targets the intended deletion expression and the selected test asserts deletion
  • break: 100 prevents a surviving smoke mutant from producing a green step
  • package and lockfile versions are coherent
  • workflow permissions remain contents: read
  • exact-head required CI is green

Remaining gates:

  • exact-head manual Mutation Testing dispatch with frontend_only=true
  • maintainer review required by ADR-0066 for the workflow control path
  • exact-head confirmation before merge

@Chris0Jeky

Chris0Jeky commented Sep 10, 2026

Copy link
Copy Markdown
Owner Author

Manual mutation dispatch triage for exact head 0e367fb:

  • run: https://github.com/Chris0Jeky/Taskdeck/actions/runs/34518952589
  • new Run mutation activation smoke test step: passed
  • later advisory Run Stryker step: failed during its initial Vitest dry run on the unrelated PaperHomeView UTC+14 assertion
  • exact-head ordinary frontend unit jobs remain green on Ubuntu and Windows

The requested activation proof is satisfied. The broader mutation-calibration failure is tracked separately in #2943 and is not being fixed in this PR. The remaining merge gate is the ADR-0066 maintainer review plus exact-head confirmation.

Copy link
Copy Markdown
Owner Author

CLAUDE_SYNC_PACKET
canonical truth changes: PR #2986 documents the September 11 product-trust reconciliation and preserves this PR's TESTING_GUIDE lease. Please link docs/analysis/2026-09-11-product-trust-reconciliation.md when integrating the guide. Exact additional proving obligation: delayed assignment PUT followed by modal/inspector Escape, backdrop, header close, discard confirmation or navigation must never promise discard while the server can still commit. Cover delayed success and failure, independent drafts, card/account transitions and Paper/Legacy with the real API. This is HIGH #2981 on parked PR #2977, not a tested outcome.

Historical evidence to retain as scoped: hierarchy Required34540544226 and WIP Required34542612225 passed; assignment's current-base 238 Application/53 API and full Windows frontend coverage6530pass/3skip did not exercise this defect. Its earlier broad backend run was not fully green; superseding focused/project passes do not relabel that invocation. Linux Vitest SIGSEGV on old08cdf is not established flaky; Windows two-worker non-reproduction is not equivalent proof.
issue/project changes: #2240/#2977 stay Blocked; prepared v0.4 QA cases stay NOT RUN. No mutation policy or dependency pin changes in the reconciliation.
architecture/decision impact: none to this PR's runtime compatibility decision or remaining maintainer gate
manual validation still needed: the delayed-write journey and combined work-model candidate QA
human actions: all 41 existing OUTSTANDING_TASKS items unchanged.

# Conflicts:
#	docs/testing/MUTATION_TESTING_POLICY.md
Merging main surfaced two defects in the smoke guard.

The pinned mutant range `boardCrudStore.ts:587:28-587:78` no longer holds the
board-list deletion expression: main moved that seam to line 591. Stryker then
instruments zero mutants, reports a score of NaN, logs "NaN is greater than or
equal to break threshold 100" and exits 0, so the activation guard goes green
while proving nothing. `thresholds.break` cannot catch an empty probe.

Re-point the range at line 591 and add `scripts/check-mutation-smoke.mjs`,
which reads the smoke run's JSON report and requires exactly four mutants, all
Killed. Verified both ways: 4/4 killed exits 0; the stale 587 range now exits 1
with a message naming the moved seam.

Drop the Vitest 4.1.x downgrade. Driving the probe through Stryker's `command`
runner (`npx vitest --run <spec>`) instead of `@stryker-mutator/vitest-runner`
kills 4/4 on the repository's Vitest 5.0.0 in ~13 s, so the smoke no longer
needs a runner-compatible Vitest pin and main's Vitest 5 line (#2803/#2815)
stays intact. Measured on this head: vitest-runner on Vitest 5.0.0 reports
"Ran 0.00 tests per mutant" with all four mutants surviving, which reproduces
the incompatibility this PR originally worked around by downgrading.

frontend/taskdeck-web/package-lock.json is now byte-identical to main and
package.json differs from main by the single added mutation:smoke script.
…sertion

Replace the Vitest 4.1.x pin rationale with what is now true: the activation
smoke drives Vitest through Stryker's command runner, so it is independent of
the @stryker-mutator/vitest-runner / Vitest pairing, and a separate assertion
step fails the command when the probe yields no mutants.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

[Claude lane claim v2]

  • lane: beta-platform-integrity (CI / mutation lane / dependency manifests)
  • base SHA: edf654ef9-era origin/main at fetch time; merged origin/main into this branch as 1c8889ef4
  • previous head: 0e367fb3a (was mergeStateStatus: DIRTY)
  • owned paths: frontend/taskdeck-web/stryker.smoke.config.mjs, frontend/taskdeck-web/scripts/check-mutation-smoke.mjs, frontend/taskdeck-web/package.json, frontend/taskdeck-web/package-lock.json, .github/workflows/mutation-testing.yml, .gitignore, docs/TESTING_GUIDE.md, docs/testing/MUTATION_TESTING_POLICY.md
  • status: conflict resolution + re-verification only. No merge. This PR is parked on OUTSTANDING_TASKS.md §J.3 (b).

No issue is linked to this PR, so the claim is recorded here.

Independent review found the policy doc asserting a property the configuration
cannot deliver. It said the smoke makes "an incompatible test-runner upgrade
fail early instead of producing an apparently valid zero-execution report" --
but the smoke deliberately bypasses @stryker-mutator/vitest-runner, which is
the runner the full report in the very next workflow step still uses, and
stryker.config.mjs sets break: 0, so a full run in which every mutant survives
scores 0.00 and still exits 0. A green frontend-mutation job is therefore not
evidence that the full step executed any test. Both docs now say what the smoke
covers and what it does not, and the policy's "deliberately not an npm script"
paragraph names mutation:smoke as the deliberate R4 exception.

Also pin the guarded source file in check-mutation-smoke.mjs. The count check
alone would accept any report carrying four killed mutants, including a stale
one: reports/mutation/ is gitignored and survives branch switches, so a future
change to jsonReporter.fileName would leave the guard reading an old run. The
count-mismatch message now names the Stryker-upgrade cause as well as the
moved-seam cause, since it previously misdiagnosed the former as the latter.

Replace process.exit(1) with process.exitCode = 1 so the diagnostic is not
discarded when stderr is a pipe, as it is on a CI runner.

Verified all four failure branches exit 1 with the right message: moved seam
end to end, report from the wrong file, a Survived mutant, and a missing
report. Positive path still 4/4 killed, exit 0.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Independent review triage — head 9b41866d5, fixes in 6cd824c57

One fresh-context adversarial review ran against 9b41866d5 (base d7c2a4e58). The Codex
connector did not review this head: it replied "You have reached your Codex usage limits for
code reviews" at 2026-09-11T23:37:35Z. Under global law 2g the fresh-context independent review is
the review gate in the connector's absence. Every finding is triaged once below.

Fixed (confirmed HIGH)

H1 — the policy doc asserted a safety property the configuration cannot deliver.
docs/testing/MUTATION_TESTING_POLICY.md claimed the smoke makes "an incompatible test-runner
upgrade fail early instead of producing an apparently valid zero-execution report". It cannot. The
smoke deliberately bypasses @stryker-mutator/vitest-runner, which is the runner the full
report in the very next workflow step still uses, and stryker.config.mjs sets break: 0, so a
full run in which every mutant survives scores 0.00 and exits 0. A green frontend-mutation job
is therefore not evidence that the full step executed anything. This is law 2a's "records a false
safety property used operationally", so it was fixed rather than tracked: both docs now state what
the smoke covers and what it does not, and docs/TESTING_GUIDE.md carries the same warning.

The same commit also reconciles the policy's existing "deliberately not an npm script" paragraph,
which after this PR read as self-contradicting — mutation:smoke is now named as the deliberate R4
exception, because the workflow has to invoke it by name.

Also fixed, from MEDIUM/LOW, because both sit in the guard this PR adds and both weaken the
fail-closed property it exists for
(one commit, no cascade):

  • The guard now pins the expected source file, not just the mutant count. reports/mutation/ is
    gitignored and survives branch switches, so a future change to jsonReporter.fileName would have
    left the guard happily reading a stale report. Not reachable on today's && path — closed as a
    latent hole.
  • The count-mismatch message now names the Stryker-upgrade cause alongside the moved-seam cause; it
    previously misdiagnosed the former as the latter.
  • process.exit(1) replaced with process.exitCode = 1. On a CI runner stderr is a pipe, and
    process.exit() can discard the pending write — a guard that fails without saying why.

Re-verified after the fix, all four failure branches exiting 1 with the intended message: moved seam
end to end, report from the wrong file, a Survived mutant, and a missing report. Positive path
still 4/4 killed, exit 0. eslint, check-docs-governance and check-doc-links re-run clean.

Evidence gap closed

H3 — no hosted evidence for the one new CI step. mutation-testing.yml is
workflow_dispatch-only, so ci-required never exercises it, and .claude/rules/ci-control.md
requires CI-control changes to qualify hosted-only. Dispatched against this branch with
frontend_only=true: https://github.com/Chris0Jeky/Taskdeck/actions/runs/34659915617 — result to be
recorded on this thread. The concrete risk it tests is real: if npx vitest --run behaves
differently inside Stryker's Linux sandbox, the new step fails and the full advisory report is never
produced, which would be the opposite of the guard's purpose.

Tracked, not fixed (law 2c)

Declined / noted, no change

  • M4 — the §J.3 record for this PR is stale. It is: OUTSTANDING_TASKS.md records head
    0e367fb3a and a changed surface of "package.json and lock", whereas the current head is
    6cd824c57 and package-lock.json is byte-identical to main. That matters, because a
    maintainer answering J.3(b) would otherwise weigh a Vitest-family downgrade that no longer exists.
    Not edited here: OUTSTANDING_TASKS.md is the human-action file and this agent does not rewrite
    rows it is parked under. Flagged for the coordinator instead.

Merge disposition

Parked. Not merged, and not mergeable on agent authority.

ci/policy.v1.json declares both .github/workflows/** and frontend/taskdeck-web/package.json as
control paths, so this is R4 regardless of the lockfile revert being dropped.
OUTSTANDING_TASKS.md §J.3 already names this PR and states plainly: "While (b) is unanswered none
of the three merges on agent authority."
J.3(b) — whether the ADR-0066 per-PR maintainer review
still binds new control-plane PRs — remains unanswered.

The PR is now up to date with main (no conflicts), CI-qualified at the head, and its review round
is complete. It is waiting only on the maintainer.

Dispatched Mutation Testing (frontend_only=true) against this branch at
6cd824c to get hosted evidence for the new workflow step, which ci-required
never exercises. Run 34659915617:

- "Run mutation activation smoke test" passed on hosted Linux: 4/4 mutants
  killed, 1.00 tests per mutant, 11 seconds, guard printed OK. The command
  runner works inside Stryker's Linux sandbox.
- "Run Stryker" then failed in its initial dry run, before any mutant executed.

The dry-run failure is pre-existing and structural, not caused by this PR:
boardMutationCapabilityParity.spec.ts parses the raw source text of
store/boardStore.ts, and stryker.config.mjs mutates that same file, so the dry
run reads Stryker's instrumented copy, the facade regex matches nothing and
`expect(groups.size).toBeGreaterThan(15)` fails with "expected 0 to be greater
than 15". Filed as #3040.

Record it in the policy next to the smoke's scope statement, and note that it
currently masks the separate vitest-runner zero-execution defect (#3038) --
the lane never reaches mutant execution. The earlier claim that an
all-survived full run would exit green under `break: 0` stays as the
conditional it is, rather than as a description of today's lane.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Hosted evidence for the new workflow step — H3 closed

mutation-testing.yml is workflow_dispatch-only, so ci-required never exercises the step this
PR adds, and .claude/rules/ci-control.md requires CI-control changes to qualify hosted-only.
Dispatched against this branch at 6cd824c57:
run 34659915617, frontend_only=true.

Run mutation activation smoke test — passed on hosted Linux.

Instrumented 1 source file(s) with 4 mutant(s)
Initial test run succeeded. Ran 1 tests in 1 second
  ✓ All tests (killed 4)
Ran 1.00 tests per mutant on average.
All files | 100.00 | 100.00 | 4 killed | 0 timeout | 0 survived | 0 no cov | 0 errors
Final mutation score of 100.00 is greater than or equal to break threshold 100
mutation:smoke guard OK: 4/4 mutants killed in src/store/board/boardCrudStore.ts.

11 seconds. That refutes the reviewer's concrete H3 risk: npx vitest --run behaves the same inside
Stryker's Linux sandbox as locally, so the command runner does not break the lane on hosted Linux.

Run Stryker — failed, and the cause is pre-existing and unrelated to this PR.

ERROR DryRunExecutor One or more tests failed in the initial test run:
	board-mutation capability parity reads the facade return block, so a restructure cannot mute the guard
		expected 0 to be greater than 15
ConfigError: There were failed tests in the initial test run.

The full lane dies in its dry run, before any mutant executes.
src/tests/views/paper/boardMutationCapabilityParity.spec.ts parses the raw source text of
src/store/boardStore.ts, and stryker.config.mjs mutates that same file — so the dry run reads
Stryker's instrumented copy, the facade regex matches nothing, groups empties, and the assertion
fails. Structural, independent of Vitest version, runner and OS, and true on main today. Filed as
#3040 with the three repair options.

This is the expected combination, not a contradiction: the smoke runs a narrow testFiles set by
design, so a dry-run failure in an unrelated spec is outside its scope. 51dec50a6 records both
results in docs/testing/MUTATION_TESTING_POLICY.md.

One correction to my own earlier note. #3040 masks #3038: because the lane never reaches mutant
execution, the "every mutant survives, score 0.00, break: 0, job exits green with a valid-looking
report" scenario is not reachable today. It stays a real hazard for whenever #3040 is fixed. The
policy wording now states it as the conditional it is rather than as a description of today's lane,
and #3038 has been corrected on its thread.

Head is now 51dec50a6. Required CI re-running; disposition unchanged — parked on §J.3(b).

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

[Claude lane release v2]

State at this head

What changed versus the head this PR was parked at (0e367fb3a)

  1. The Vitest 4.1.x downgrade is gone. The incompatibility it worked around is real and
    reproduced (@stryker-mutator/vitest-runner 10 on Vitest 5 runs 0.00 tests per mutant), but
    Stryker's command runner kills 4/4 on Vitest 5, so main's Vitest line from deps(npm): update Vitest packages to v5 in lockstep #2803/Integrate reviewed product, dependency and CI PRs #2815 is no
    longer reverted. package-lock.json is byte-identical to main; package.json differs by one
    added script line.
  2. Two defects fixed in the guard itself. The pinned mutant range had gone dead when main
    moved the seam, and Stryker exits 0 on an empty mutant set (score NaN vs break: 100) — so
    the activation guard was fail-open. Re-pointed, plus scripts/check-mutation-smoke.mjs, which
    requires four Killed mutants in the expected file. All four failure branches proven to exit 1.
  3. A doc claiming a safety property the config cannot deliver was corrected, and the hosted
    result — including the pre-existing Frontend mutation lane fails its dry run: boardMutationCapabilityParity parses boardStore.ts source, which Stryker instruments #3040 dry-run blocker that kills the full lane — recorded.

Blocking gate (unchanged, and the reason this is a release rather than a merge)

ci/policy.v1.json declares .github/workflows/** and frontend/taskdeck-web/package.json as
control paths, so this is R4. OUTSTANDING_TASKS.md §J.3 names this PR and states: "While (b) is
unanswered none of the three merges on agent authority."
J.3(b) is unanswered. Waiting on the
maintainer only.

For the coordinator

The §J.3 record for #2931 is now factually stale: it records head 0e367fb3a and a changed surface
of "package.json and lock". The current head is 51dec50a6 and the lockfile is unchanged from
main — materially less risk than the Vitest-family downgrade the row describes. Not edited here,
since §J.3 is a human-action row this PR is parked under.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Pending

Development

Successfully merging this pull request may close these issues.

1 participant