test(#2986): mutation-killer suite for config-schema.cjs (95 typed assertions) (#3005)

* test(#2986): mutation-killer suite for config-schema.cjs (95 typed assertions)

Stryker measured 4.62% mutation score on config-schema.cjs (6 killed,
124 survived). Surviving mutants documented that existing tests were
exercising paths without verifying outputs.

Adds tests/bug-2986-config-schema-mutation-killers.test.cjs (95 tests,
4 suites) targeting each surviving mutant class:

- M1/M4: parameterized isValidConfigKey(key) === true for every member
  of VALID_CONFIG_KEYS. Kills static-key-fast-path mutations
  (if (VALID_CONFIG_KEYS.has(...)) return true; -> if (false) return true;)
  because no static key matches any DYNAMIC_KEY_PATTERN by design.

- M2: representative dynamic-pattern keys (one per pattern). Each matches
  exactly one pattern. Kills .some -> .every mutation: with .every, no
  single key matches all patterns -> all dynamic keys would be rejected.

- M3: strictEqual against the literal boolean true/false (not assert.ok
  truthy checks). Kills polarity-flip mutations.

- Anchor-tightening: keys that differ from valid by one char beyond the
  documented shape (trailing dot-segment, empty agent name, non-enum tier,
  etc.). Kills regex-loosening mutations on ^, $, charset boundaries.

Tests assert on typed boolean return values from the lib's public surface.
Zero source-grep, zero raw-text matching.

* chore(#2986): add changeset fragment for PR #3005

* test(#2986): use dynamic-only rep key for features pattern (CR feedback)

CodeRabbit on PR #3005: features.thinking_partner is in the static
VALID_CONFIG_KEYS set, so the static fast-path returns true before
DYNAMIC_KEY_PATTERNS.some() is ever called. A Stryker mutant that
removed only the features entry from DYNAMIC_KEY_PATTERNS would
survive because the test only ever exercised the static path for
that key.

Replaced features.thinking_partner with features.some_dynamic_feature
which is NOT in static keys, so isValidConfigKey must reach the
dynamic path to return true. Added a per-rep invariant that asserts
each representative key is NOT a member of VALID_CONFIG_KEYS,
catching this class of mistake at test time on any future
representative-key change.
This commit is contained in:
Tom Boucher
2026-05-01 21:13:25 -04:00
committed by GitHub
parent 3657c4ea9e
commit 55ae8e42d2
3 changed files with 154 additions and 0 deletions

View File

@@ -6,6 +6,10 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased](https://github.com/gsd-build/get-shit-done/compare/v1.39.1...HEAD)
### Changed
- **Test suite for `config-schema.cjs` is now mutation-resistant** — Stryker measured a 4.62% mutation score on `get-shit-done/bin/lib/config-schema.cjs` (6 killed, 124 survived out of 130). Surviving mutants flagged that existing tests were exercising paths but not verifying outputs: a polarity flip (`return true` → `return false`), a predicate swap (`.some` → `.every`), or a guard removal (`if (VALID_CONFIG_KEYS.has(...)) return true;` → unguarded fallthrough) all passed every test. New `tests/bug-2986-config-schema-mutation-killers.test.cjs` adds 95 tests across four suites that target each surviving mutant class: (1) parameterized `isValidConfigKey('${key}') === true` for every member of `VALID_CONFIG_KEYS` (kills the static-key-fast-path mutation), (2) representative dynamic-pattern keys that match exactly one pattern (kills the `.some` → `.every` mutation, with an inline mutual-exclusivity invariant check), (3) `strictEqual` against the literal boolean `true`/`false` instead of `assert.ok` truthy checks (kills polarity-flip mutations), (4) anchor-tightening cases that differ from valid keys by one character beyond the documented shape (kills regex-loosening mutations on `^`, `$`, and character-class boundaries). Tests use the lib's public surface (typed boolean assertions on `isValidConfigKey` return values), no source-grep. (#2986)
### Fixed
- **`release-sdk` hotfix re-run no longer fails at `Dry-run publish validation` when the version is already on npm** — the `Detect prior publish (reconciliation mode)` step sets `skip_publish=true` when the package version is already on the registry, and the actual publish step honors that gate. The `Dry-run publish validation` step was missing the same guard, so any operator re-run of an already-published hotfix (the typical recovery path when later steps fail mid-flight) hit `npm publish --dry-run` first and got `npm error You cannot publish over the previously published versions: X.Y.Z` — `npm publish --dry-run` contacts the registry and rejects existing-version targets even though it doesn't actually publish. The dry-run validation step is now gated on the same `steps.prior_publish.outputs.skip_publish != 'true'` condition as the publish step. The rehearsal still runs on first publishes (where it has value); it skips only in the specific reconciliation case where the publish itself would be skipped. Trigger run: [25233855236](https://github.com/gsd-build/get-shit-done/actions/runs/25233855236/job/73995605643). Regression covered by `tests/bug-2987-dry-run-validation-skip-on-reconciliation.test.cjs`. (#2987)