test(tests): antagonistic tier — fast-check property tests + Stryker mutation testing (PR-gated) (#461)

* feat(#454): add antagonistic tier — fast-check property tests + Stryker mutation config

Adds property-based testing (fast-check v4) and mutation testing scaffolding
(Stryker v8) as the antagonistic validation tier for lib/*.cjs pure logic.

## Files added

### Shared setup
- tests/helpers/fast-check-setup.cjs — configureGlobal({ numRuns:200, seed:42 })
  for deterministic CI; override locally with GSD_FC_SEED

### Property test suites (node:test + fast-check)
- tests/context-utilization.property.test.cjs — boundary at 60%/70% thresholds
  (exact Math.ceil boundary, not Math.floor), TypeError on all invalid inputs,
  overflow clamping to 100%/critical, shape invariants (7 tests, all pass)
- tests/prompt-budget.property.test.cjs — estimateTokens monotonicity + ceil(len/4)
  exactness; applyBudget shape invariant, instructions/roadmap verbatim, budget
  envelope, omit tracking (11 tests, all pass)
- tests/frontmatter.property.test.cjs — extractFrontmatter/reconstructFrontmatter/
  spliceFrontmatter never-throw + type shape + splice→extract round-trip (9 tests)
- tests/adr-parser.property.test.cjs — shouldRejectAdrStatus boundary (3 statuses
  only), parseAdrMarkdown shape + title trim invariant (discovered: parser trims
  trailing whitespace) (9 tests, all pass)
- tests/config-schema.property.test.cjs — isValidConfigKey never throws, returns
  boolean, accepts all VALID/RUNTIME_STATE_KEYS, rejects empty/null/unknown (8 tests)

### Stryker mutation config
- stryker.config.mjs — testRunner:'command', mutate bin/lib/**/*.cjs minus 13
  generated files, coverageAnalysis:'off', thresholds {high:80,low:60,break:50},
  incremental:true, reporters html+clear-text+progress

### CI workflow
- .github/workflows/mutation.yml — PR-gating job (pull_request + workflow_dispatch),
  runs stryker --incremental --since origin/next (changed files only), uploads
  HTML artifact; SINCE_REF via env not interpolation (injection-safe)

### Package config
- package.json: +test:mutation, +test:mutation:since scripts
- .gitignore: +.stryker-tmp/, +.stryker-incremental.json, +reports/mutation/
- package-lock.json: fast-check@4.8.0, @stryker-mutator/core@9.6.1

## Verified
node --test on all 5 property test files: 44 tests, 0 failures.
Stryker NOT run (slow; reserved for CI).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#454): pin upload-artifact to v7.0.1 SHA used across repo (bad SHA ea165f8d)

The SHA ea165f8d65b6e75b540449d3ec4f5dde0c5a4e1 (labeled v4.6.2) does not
resolve on GitHub Actions. All other workflows in this repo pin
043fb46d1a93c77aae656e7c1c64a875d1fc6a0a (v7.0.1) — align mutation.yml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#454): mutation workflow — replace invalid --since flag with changed-core-files --mutate scoping

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: CI Rebase Check <ci@gsd-redux>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-29 11:07:07 -04:00
committed by GitHub
parent 33afb4f6eb
commit 4b908e6bcd
11 changed files with 3060 additions and 9 deletions

92
stryker.config.mjs Normal file
View File

@@ -0,0 +1,92 @@
/**
* stryker.config.mjs
*
* Mutation testing configuration for get-shit-done-redux.
*
* Test runner: 'command' (built into @stryker-mutator/core)
* Runs: node --test over the lib test files via the repo's run-tests invocation.
*
* Mutate scope: bin/lib/**\/*.cjs, excluding generated files and test files.
*
* coverageAnalysis: 'off' — command runner does not support per-mutant coverage
* thresholds: high=80, low=60, break=50
* incremental: true — caches results; PR-scoped runs pass --mutate <changed-files>
*
* Reports:
* - html: reports/mutation/mutation.html
* - clear-text (console)
* - progress (spinner)
*
* NOTE: This is incremental / changed-files-only in CI (--mutate <changed-files>)
* to stay bounded. Full runs are for local exploration only.
*/
// Generated files that must NEVER be mutated
const GENERATED_FILES = [
'!get-shit-done/bin/lib/configuration.cjs', // GENERATED — sdk/src/config/index.ts
'!get-shit-done/bin/lib/command-aliases.cjs', // GENERATED
'!get-shit-done/bin/lib/commands.cjs', // GENERATED
'!get-shit-done/bin/lib/core.cjs', // GENERATED
'!get-shit-done/bin/lib/install-profiles.cjs', // GENERATED
'!get-shit-done/bin/lib/installer-migrations.cjs', // GENERATED
'!get-shit-done/bin/lib/phase.cjs', // GENERATED
'!get-shit-done/bin/lib/profile-output.cjs', // GENERATED
'!get-shit-done/bin/lib/state.cjs', // GENERATED
'!get-shit-done/bin/lib/verify.cjs', // GENERATED
'!get-shit-done/bin/lib/init.cjs', // GENERATED
'!get-shit-done/bin/lib/audit.cjs', // GENERATED
'!get-shit-done/bin/lib/gsd2-import.cjs', // GENERATED
];
/** @type {import('@stryker-mutator/core').PartialStrykerOptions} */
export default {
// ── Test runner ──────────────────────────────────────────────────────────────
testRunner: 'command',
commandRunner: {
// Run property tests + unit tests over lib only.
// Deliberately avoids running the full integration suite (slow).
command: 'node --test tests/context-utilization.property.test.cjs tests/prompt-budget.property.test.cjs tests/frontmatter.property.test.cjs tests/adr-parser.property.test.cjs tests/config-schema.property.test.cjs tests/adr-parser.test.cjs tests/active-workstream-store.test.cjs',
},
// ── Files to mutate ──────────────────────────────────────────────────────────
mutate: [
'get-shit-done/bin/lib/**/*.cjs',
'!get-shit-done/bin/lib/**/*.test.cjs',
...GENERATED_FILES,
],
// ── Coverage ─────────────────────────────────────────────────────────────────
// 'off' is required for the command test runner — it cannot instrument per-mutant.
coverageAnalysis: 'off',
// ── Thresholds ───────────────────────────────────────────────────────────────
thresholds: {
high: 80,
low: 60,
break: 50,
},
// ── Incremental mode ─────────────────────────────────────────────────────────
// Cache mutation results; re-run only changed mutants on subsequent calls.
// In CI the workflow computes changed files and passes: stryker run --incremental --mutate <list>
incremental: true,
incrementalFile: '.stryker-incremental.json',
// ── Reporters ────────────────────────────────────────────────────────────────
reporters: ['html', 'clear-text', 'progress'],
htmlReporter: {
fileName: 'reports/mutation/mutation.html',
},
// ── Temp directory ───────────────────────────────────────────────────────────
tempDirName: '.stryker-tmp',
// ── Ignore patterns ──────────────────────────────────────────────────────────
ignorePatterns: [
'node_modules',
'reports',
'.stryker-tmp',
'coverage',
'hooks/dist',
],
};