Files
msd-core/tests/state-write-path-drift-guard.test.cjs
Tom Boucher 1863f5569c enhance(#3871): the state transaction — mandatory snapshot, open()/rebuild() (#3874)
* test(#3871): failing-first regressions for the dropped curated progress block

Pins ADR-3473 §8.6 / #3756 at the consumer's output: state record-session and
state add-decision on an archived-milestone project drop the curated progress
frontmatter entirely, exit 0, and report nothing. Reproduced against the real
CLI before writing the tests, not inferred from the issue text.

Also adds the unit-level probe that applyStatePreservation's preserve-always
row is inert on a resyncing write, and an over-preservation guard that an
empty project is never inflated.

Refs #3871

* feat(#3871): make the STATE.md pre-write snapshot mandatory via open()/rebuild()

ADR-3473 §8.6. StatePreservationInput's nullable preFm and the always-present
preFmSnapshot were the same extractFrontmatter call, one of them nulled on
resync — a policy flag baked into a snapshot. Both collapse into a single
StateTransaction whose snapshot cannot be absent: openStateTransaction()
applies preservation, rebuildStateTransaction() does not, and both carry the
snapshot because the reporting phase needs it either way. An absent snapshot
is now a construction failure; an empty one stays legal, because that is what
a document with no parseable frontmatter honestly has.

writeStateMd requires a rebuild transaction, which types ADR-3408 §8.3's
closed exception list at both call sites (state sync, health --repair) instead
of matching them as strings in a ratcheted baseline.

Fixes the dropped curated progress block: an all-zero or absent derived total
set is an unmeasured scan, not a measurement, so the curated block stands.
Also fixes two defects surfaced while building — preserve-always reported a
mutation even when it restored an identical value, and it re-entered the
curated object by reference, which would alias the snapshot the next phase
diffs against.

Refs #3871

* fix(#3871): close the three remaining subsumed defects and restore the arm the type does not replace

Review of the first two commits found four things.

The guard shrink deleted the seam-bypass axis whole, but only its
writeStateMd( arm became redundant. Its other arm catches a call site
re-assembling syncStateFrontmatter + applyPostSyncPreservation instead of the
owned composition, which the transaction type does not make unrepresentable
and which #3469 found live. Restored as findCompositionBypasses, terminal
rather than ratcheted.

Three of the four issues this phase claims were untouched. All three are the
epic's own shape and are fixed at the seam: current_phase_name is reasserted
from the curated value when the caller names none, and cmdStateJson stops
carrying a hand-maintained list parallel to FIELD_CLASSIFICATION and projects
it instead.

The construction failure that is the point of this phase had no test. Every
enumerated matrix row now has one, including the measured-versus-unmeasured
coercion boundary and a seeded property that no curated key is ever dropped.

ADR-3473 §8.6 said the guard 'keeps only its raw-write check'. Verified
against next: there was no raw-write check, and four other checks it does not
name. Amended in place with the evidence. ARCHITECTURE.md separately
advertised a preservation policy the code had deleted.

Refs #3871

* fix(#3871): do not let the unmeasured-scan rule block an explicitly-requested resync

The remote matrix caught over-preservation, the failure this phase's own
negative space says must not happen. state update Progress re-derives the
block from the body the caller just rewrote; on a project with no phase dirs
the derivation yields zero totals, the unmeasured rule read that as 'the scan
measured nothing', and the stale curated percent was restored over the resync
the user asked for.

preserve-always already said what the missing condition was: never overwrite
unless the caller explicitly names this field. explicitProgressField carries
it and is derived from shouldResyncStateProgress, not set by hand at a call
site, so it cannot drift from what the caller asked for.

Two defects found in the same mechanism and fixed with it. readModifyWriteStateMd
enumerates its option keys, so a new option was silently dropped rather than
rejected. And the raw-write axis captured its first argument up to the first
comma, which lands inside a nested path.join, so a write to a STATE.md literal
was invisible to it — the prove-it-can-fail test caught that one immediately.

No test assertion was weakened; all three frontmatter rows encode #3242, #1969
B3 and #1972 and stand unchanged.

Refs #3871

* docs(#3871): record why the raw-write check is kept, not why it was named

The amendment justified findRawStateWrites as 'written because §8.6 requires
it to exist', which is cargo-culting the contract and would have been the
wrong reason to keep anything. The real reason is that writeStateMd acquires
the STATE.md lockfile and a raw fs.writeFileSync acquires nothing, so this is
a lock bypass and lost-update is the #500/#905/#1230 family — and after this
phase it is the one reachable path into the file that nothing else covers.

Also records why ADR-3408 §8.6's deletion of the 'clear' policy is not the
precedent it looks like: 'clear' was dead vocabulary in a closed enum, this is
coverage of a reachable path.

Refs #3871

* chore(#3871): backfill changeset PR number

---------

Co-authored-by: sim <sim@local>
2026-08-25 20:38:12 -04:00

637 lines
30 KiB
JavaScript

'use strict';
/**
* Tests for the STATE.md write-path anti-divergence drift guard
* (epic #3408, issue #3468, ADR-3408 Decision 5; SHRUNK per ADR-3473 §8.6,
* issue #3871) — `scripts/lint-state-write-path-drift.cjs`.
*
* Design contract: docs/adr/3408-state-write-path-preservation.md (§8.1/§8.2/§8.3)
* docs/adr/3473-enforcement-by-construction.md (§8.6)
*
* The guard covers five axes, each backed by its own exported pure function
* and each terminal (every finding carries its own `reason` — there is no
* ratchet and nothing here reads a baseline file): `findPolicyDispatchDrift`/
* `findUnimplementedPolicies` (Axis 1, policy dispatch), `findRawStateWrites`
* (Axis 2, a raw `fs.writeFileSync` against the state path),
* `findUnstrippedContentWrites` (Axis 3, a frontmatter-shaped body write),
* `findPromptSeamUses` (Axis 4, prompt-layer prose shelling out to a
* write-side command), and `findCompositionBypasses` (Axis 5, a direct
* `syncStateFrontmatter`/`applyPostSyncPreservation` call outside their
* owner). ADR-3473 §8.6 retired one prior axis's `writeStateMd(` arm and the
* ratchet/retired-baseline machinery that backed it, once `writeStateMd`'s
* third parameter started requiring a `StateTransaction` the type system
* names; issue #3871 review kept that axis's OTHER arm (now
* `findCompositionBypasses`) because the type system gates only
* `writeStateMd`'s parameter, not a call site that never goes through
* `writeStateMd` at all — that arm was made terminal too.
*
* Sections D and E drive each pure function directly with in-memory
* fixtures — no temp tree needed, mirroring
* tests/state-field-drift.test.cjs's own house pattern. Section F instead
* drives the real CLI entry point, via the guard's `--root <dir>` flag
* (`collect(root)` takes a matching parameter, default `REPO_ROOT`, so every
* existing invocation — `npm run lint:ci` included — is unaffected): this is
* what lets a real-tree fixture run inside a disposable `createTempDir()`
* tree instead of ever mutating this repository's own `src/`, where a
* planted fixture would be visible to any concurrent `tsc`/`npm run
* build:lib`/`npm run lint`/another guard invocation, and would survive as
* build-breaking debris if the process were killed before `t.after` ran. D1
* is the one exception among the fixture-driven rows: it asserts the real
* repo's own `src/` and prompt layer are clean, so it runs through the CLI's
* `--json` output against the real `REPO_ROOT` with no `--root` override.
*
* Fixtures use array `.join('\n')`, never an indented template literal —
* indentation bleed would shift every asserted line number.
*
* Assertions compare the frozen `REASON` enum values and the `--json`/pure
* function return shapes only — never a substring/regex match on the human
* formatter's prose (CONTRIBUTING.md, "Prohibited: Raw Text Matching on Test
* Outputs").
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { runNode } = require('./helpers/process-seam.cjs');
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const { createTempDir, cleanup } = require('./helpers.cjs');
const guard = require('../scripts/lint-state-write-path-drift.cjs');
const {
REASON,
findPromptSeamUses,
findPolicyDispatchDrift,
findUnstrippedContentWrites,
findRawStateWrites,
findCompositionBypasses,
targetsStatePath,
EXECUTOR_FILE,
SEAM_OWNER_FILE,
SEAM_OWNER_EXEMPT_FUNCTIONS,
REPO_ROOT,
} = guard;
const GUARD_PATH = path.join(REPO_ROOT, 'scripts', 'lint-state-write-path-drift.cjs');
// A synthetic, non-executor consumer file — never a real repo path — used
// wherever a row does not specifically need EXECUTOR_FILE behavior.
const OTHER_FILE = 'src/example-consumer.cts';
// ─── D1: the real tree, through the CLI's --json contract ─────────────────
describe('D1 — the real tree', () => {
test('guard: clean tree passes', () => {
const result = runNode([GUARD_PATH, '--json'], { cwd: REPO_ROOT, timeoutMs: PROBE_TIMEOUT_MS });
assert.strictEqual(result.outcome, 'exited');
const body = JSON.parse(result.stdout);
assert.strictEqual(body.ok, true);
assert.deepStrictEqual(body.findings, []);
assert.strictEqual(result.exitCode, 0);
});
});
// ─── D8: comments are not drift (composition-bypass shape) ────────────────
describe('D8 — comments are not drift', () => {
test('guard: comments are not drift', () => {
const text = [
'// syncStateFrontmatter(content, cwd);',
'/**',
' * applyPostSyncPreservation(originalContent, content, synced, statePath, options);',
' */',
'function noop() {}',
].join('\n');
// ADR-3180 Amendment 3's recorded false positive: a `//` line comment
// and a `/* */` block comment both carrying the exact call text must
// stay silent — both are blanked by `stripComments` before the seam-call
// regex ever runs.
assert.deepStrictEqual(findCompositionBypasses(OTHER_FILE, text), []);
});
});
// ─── D9/D10: the owner-file exemption is function-scoped, not file-scoped ──
describe('D9 — owner functions are exempt', () => {
test('guard: owner functions are exempt', () => {
// `syncAndPreserveStateMd` is the sole legitimate place
// `syncStateFrontmatter(` and `applyPostSyncPreservation(` appear
// together (the composition every OTHER caller routes through).
assert.ok(SEAM_OWNER_EXEMPT_FUNCTIONS.includes('syncAndPreserveStateMd'));
const text = [
'function syncAndPreserveStateMd(originalContent, transformedContent, statePath, cwd, options) {',
' const synced = syncStateFrontmatter(transformedContent, cwd, options.authoritativeFm);',
' return applyPostSyncPreservation(originalContent, transformedContent, synced, statePath, options);',
'}',
].join('\n');
// The seam's own internal plumbing (the one owned composition —
// sync then post-sync preservation) is not a bypass.
assert.deepStrictEqual(findCompositionBypasses(SEAM_OWNER_FILE, text), []);
});
test('guard: writeStateMd is also exempt — its own sanctioned direct syncStateFrontmatter call is not a bypass', () => {
assert.ok(SEAM_OWNER_EXEMPT_FUNCTIONS.includes('writeStateMd'));
const text = [
'function writeStateMd(statePath, content, transaction, cwd, clock) {',
" const synced = syncStateFrontmatter(content, cwd, undefined, transaction.kind === 'rebuild');",
' platformWriteSync(statePath, synced);',
'}',
].join('\n');
assert.deepStrictEqual(findCompositionBypasses(SEAM_OWNER_FILE, text), []);
});
});
describe('D10 — the owner file is not exempt', () => {
test('guard: the owner file is not exempt', () => {
// ADR-3408 Decision 5's named gaming route: a whole-FILE exemption on
// the owner is exactly how getMilestoneInfo stayed invisible to an
// earlier drift guard. A call inside any OTHER function in state.cts —
// not one of SEAM_OWNER_EXEMPT_FUNCTIONS — must still be caught.
const text = [
'function patchCore(cwd) {',
' const modified = compute();',
' const synced = syncStateFrontmatter(modified, cwd);',
' return synced;',
'}',
].join('\n');
const out = findCompositionBypasses(SEAM_OWNER_FILE, text);
assert.strictEqual(out.length, 1);
assert.strictEqual(out[0].reason, REASON.COMPOSITION_BYPASS);
assert.strictEqual(out[0].line, 3);
});
});
// ─── D12: CRLF is scanned identically to LF ────────────────────────────────
describe('D12 — CRLF is scanned identically', () => {
test('guard: CRLF is scanned identically', () => {
const lfText = [
'function cmdSomethingElse(cwd) {',
' const synced = syncStateFrontmatter(modified, cwd);',
'}',
].join('\n');
const crlfText = lfText.split('\n').join('\r\n');
const lfOut = findCompositionBypasses(OTHER_FILE, lfText);
const crlfOut = findCompositionBypasses(OTHER_FILE, crlfText);
assert.strictEqual(crlfOut.length, 1);
const strip = (arr) => arr.map(({ line, symbol, source }) => ({ line, symbol, source }));
assert.deepStrictEqual(strip(crlfOut), strip(lfOut));
// A stray trailing \r surviving into the reported source (the repo's
// documented \n-only-regex bug class) would show up here as a
// sanitized `\x0d` escape — it must not.
assert.strictEqual(crlfOut[0].source, 'const synced = syncStateFrontmatter(modified, cwd);');
});
});
// ─── E1/E2 (Phase 2 / #3469, RETAINED per issue #3871): the composition-pair
// re-assembly shape itself, and the legitimate single-call composition ─────
describe('E1 — a re-assembled composition at a new call site is detected', () => {
test('guard: a call site invoking syncStateFrontmatter and applyPostSyncPreservation directly (bypassing syncAndPreserveStateMd) is caught on BOTH calls', () => {
// Finding 3's exact shape (ADR-3408 Amendment 2): every step calls an
// owner, so neither call alone is undeclared — but assembling the PAIR
// at a call site outside the seam composition is the re-derivation §8.3
// forbids by name. Synthetic: the real instance of this shape
// (cmdPhaseComplete's pre-#3469 adapter) was fixed by that phase.
const text = [
'function cmdReassembledAdapter(cwd, statePath, stateContent) {',
' let synced = syncStateFrontmatter(stateContent, cwd, authoritativeFm);',
' synced = applyPostSyncPreservation(originalStateContent, stateContent, synced, statePath, options);',
' return synced;',
'}',
].join('\n');
const out = findCompositionBypasses(OTHER_FILE, text);
assert.strictEqual(out.length, 2, 'both re-assembled stages must be caught, not just one');
assert.deepStrictEqual(out.map((f) => f.symbol).sort(), ['applyPostSyncPreservation', 'syncStateFrontmatter']);
assert.ok(out.every((f) => f.reason === REASON.COMPOSITION_BYPASS));
});
});
describe('E2 — a legitimate single call to the composition is not detected', () => {
test('guard: calling syncAndPreserveStateMd (the ONE write-seam composition) is not a bypass', () => {
// Verbatim shape from a real caller of the composition (e.g.
// milestone.cts's cmdMilestoneComplete) — a single call to the owned
// composition function, never to its two internal stages directly.
const text = [
' const finalContent = syncAndPreserveStateMd(',
' originalStateContent,',
' result.content,',
' statePath,',
' cwd,',
' {',
' resync: true,',
' authoritativeFm: Object.keys(authoritativeFm).length > 0 ? authoritativeFm : undefined,',
' divergedFields,',
' },',
' );',
].join('\n');
assert.deepStrictEqual(findCompositionBypasses(OTHER_FILE, text), []);
});
});
// ─── D11: the prompt layer is in the scan surface ──────────────────────────
describe('D11 — the prompt layer is in the scan surface', () => {
const PROMPT_FILE = 'gsd-core/workflows/example-workflow.md';
test('guard: the prompt layer is in the scan surface', () => {
const text = [
'# Example workflow',
'',
'Run gsd-tools state.patch --field status --value done to record completion directly.',
].join('\n');
const out = findPromptSeamUses(PROMPT_FILE, text);
assert.strictEqual(out.length, 1);
assert.strictEqual(out[0].line, 3);
assert.strictEqual(out[0].reason, REASON.PROMPT_LAYER_STATE_WRITE);
assert.strictEqual(out[0].symbol, 'prompt-layer-state-write');
});
test('control: the same candidate wrapped in backticks is a mention, not an invocation, and is not reported', () => {
// Proves the detection above is genuinely exercising the code-span
// exclusion, not merely that the fixture happens to score zero.
const text = 'Documentation only: `gsd-tools state.patch --field status --value done`.';
assert.deepStrictEqual(findPromptSeamUses(PROMPT_FILE, text), []);
});
});
// ─── D14: field-name-keyed BRANCH comparisons (not just CALLS) ────────────
// #3468: `applyPreserveIfPlaceholder` shipped a `field !== 'milestone_name'`
// branch — a field-name-keyed dispatch that routed around
// `getFieldClassification` entirely, so `FIELD_NAME_DISPATCH_RE` (which only
// matches the CALL shape) reported zero violations while the branch shape
// sat in the executor undetected. This section drives the widened detector
// directly against in-memory fixtures, mirroring D2's own "guard: does the
// pure function itself report" pattern.
describe('D14 — field-name-keyed branch comparisons are caught', () => {
test('guard: field !== literal is reported as FIELD_NAME_DISPATCH', () => {
const text = [
'function applyPreserveIfPlaceholder(field, cls, ctx) {',
" if (field !== 'milestone_name') return;",
'}',
].join('\n');
const out = findPolicyDispatchDrift(EXECUTOR_FILE, text);
assert.strictEqual(out.length, 1);
assert.strictEqual(out[0].reason, REASON.FIELD_NAME_DISPATCH);
assert.strictEqual(out[0].line, 2);
assert.strictEqual(out[0].field, 'milestone_name');
});
test('guard: field === literal is reported as FIELD_NAME_DISPATCH', () => {
const text = [
'function applyPreserveWhenUnchanged(field, cls, ctx) {',
" if (field === 'status') return;",
'}',
].join('\n');
const out = findPolicyDispatchDrift(EXECUTOR_FILE, text);
assert.strictEqual(out.length, 1);
assert.strictEqual(out[0].reason, REASON.FIELD_NAME_DISPATCH);
assert.strictEqual(out[0].line, 2);
assert.strictEqual(out[0].field, 'status');
});
test('guard: the reversed literal === field is reported as FIELD_NAME_DISPATCH', () => {
const text = [
'function applyPreserveIfPlaceholder(field, cls, ctx) {',
" if ('milestone' === field) return;",
'}',
].join('\n');
const out = findPolicyDispatchDrift(EXECUTOR_FILE, text);
assert.strictEqual(out.length, 1);
assert.strictEqual(out[0].reason, REASON.FIELD_NAME_DISPATCH);
assert.strictEqual(out[0].line, 2);
assert.strictEqual(out[0].field, 'milestone');
});
test('control: preservation === literal (the CORRECT policy-dispatch shape) is NOT reported', () => {
const text = [
'function applyPreserveAlways(field, cls, ctx) {',
" if (cls.preservation === 'preserve-always') return;",
'}',
].join('\n');
assert.deepStrictEqual(findPolicyDispatchDrift(EXECUTOR_FILE, text), []);
});
test('control: an unrelated variable compared to a literal is NOT reported', () => {
const text = [
'function helper(status, cls, ctx) {',
" if (status !== 'unknown') return;",
'}',
].join('\n');
assert.deepStrictEqual(findPolicyDispatchDrift(EXECUTOR_FILE, text), []);
});
test('control: bracket-indexed field access (ctx.postFm[field]) compared to a non-literal is NOT reported', () => {
const text = [
'function applyPreserveWhenUnchanged(field, cls, ctx) {',
' if (ctx.postFm[field] === snapshot) return;',
'}',
].join('\n');
assert.deepStrictEqual(findPolicyDispatchDrift(EXECUTOR_FILE, text), []);
});
});
// ─── D15: `file` (and other attacker-derived fields) are sanitized AT
// CONSTRUCTION, not just by the human formatter ─────────────────────────────
// Security review finding: a repo can legally track a filename containing C1
// control bytes or bidi-override codepoints — exactly as attacker-controlled
// on a fork PR as the `source` fragment this guard already sanitized before
// this fix. Before this fix `file` reached `--json` stdout unsanitized —
// only the human formatter wrapped it. A finding's `file` (and any other
// attacker-derived field, like `field`) must come back escaped from the
// FINDER itself, so every consumer (human, `--json`) inherits the
// sanitization uniformly.
//
// The two attack codepoints are built via `String.fromCharCode` rather than
// embedded as literal bytes, so this test file's own source never carries a
// live control/bidi codepoint on disk.
describe('D15 — file (and field) values are sanitized at construction', () => {
const RLO = String.fromCharCode(0x202e); // RIGHT-TO-LEFT OVERRIDE (bidi)
const C1_CSI = String.fromCharCode(0x9b); // C1 CONTROL: CSI
const ATTACK_FILE = `src/evil${RLO}${C1_CSI}name.cts`;
const ESCAPED_FILE = 'src/evil\\u202e\\x9bname.cts';
test('findRawStateWrites: an attacker-controlled filename comes back escaped', () => {
const text = ['function cmdSomethingElse(cwd) {', ' fs.writeFileSync(statePath, modified);', '}'].join('\n');
const out = findRawStateWrites(ATTACK_FILE, text);
assert.strictEqual(out.length, 1);
assert.strictEqual(out[0].file, ESCAPED_FILE);
// Neither raw attack codepoint survives in the finding at all — this is
// exactly what reaches `--json` stdout verbatim (JSON.stringify
// neutralizes C0 but NOT C1 or bidi codepoints, which is why
// construction-time escaping — not JSON.stringify — is load-bearing).
assert.ok(!out[0].file.includes(RLO));
assert.ok(!out[0].file.includes(C1_CSI));
});
test('findPromptSeamUses: an attacker-controlled filename comes back escaped', () => {
const text = 'Run gsd-tools state.patch --field status --value done to record completion directly.';
const out = findPromptSeamUses(ATTACK_FILE, text);
assert.strictEqual(out.length, 1);
assert.strictEqual(out[0].file, ESCAPED_FILE);
assert.ok(!out[0].file.includes(RLO));
assert.ok(!out[0].file.includes(C1_CSI));
});
test('findPolicyDispatchDrift: filename AND the field literal are both escaped', () => {
const text = [" if (field === 'status" + RLO + C1_CSI + "') return;"].join('\n');
const out = findPolicyDispatchDrift(ATTACK_FILE, text);
assert.strictEqual(out.length, 1);
assert.strictEqual(out[0].file, ESCAPED_FILE);
assert.strictEqual(out[0].field, 'status\\u202e\\x9b');
assert.ok(!out[0].field.includes(RLO));
assert.ok(!out[0].field.includes(C1_CSI));
});
});
// ─────────────────────────────────────────────────────────────────────────
// Section E (Phase 2 / #3469) — ADR-3408 §8.3 Matrix section E: guard rows
// closing Phase 1's declared known gap (Axis 3, §8.3(b)). E1/E2/E6/E7/E8
// (all of which drove the retired `findSeamBypasses`/ratchet machinery) are
// REMOVED along with it, per ADR-3473 §8.6. E3/E4/E5 (the frontmatter-write
// axis ADR-3473 §8.6 did NOT name for removal) are retained.
// ─────────────────────────────────────────────────────────────────────────
describe('E3 — a patchCore-style frontmatter write is detected (closes the Phase 1 declared gap)', () => {
test('guard: stateReplaceField over unstripped content with a variable field name is caught', () => {
// The pre-Phase-2 shape #3469 fixed: patchCore ran stateReplaceField
// over content that was never stripped of frontmatter, letting a
// lowercase/frontmatter-shaped patch key rewrite the YAML block
// directly, outside FIELD_CLASSIFICATION.
const text = [
'function patchCoreOld(content, intent) {',
' let modified = content;',
' for (const [field, value] of Object.entries(intent.patches)) {',
' const replaced = stateReplaceField(modified, field, value);',
' if (replaced !== null) modified = replaced;',
' }',
' return { content: modified };',
'}',
].join('\n');
const out = findUnstrippedContentWrites(EXECUTOR_FILE, text);
assert.strictEqual(out.length, 1);
assert.strictEqual(out[0].reason, REASON.UNSTRIPPED_CONTENT_WRITE);
assert.strictEqual(out[0].line, 4);
});
});
describe('E4 — updateCore\'s strip-then-replace is NOT detected', () => {
test('guard: the real updateCore call site (content stripped first) is not flagged', () => {
// Verbatim from src/state-transition.cts's real updateCore — the shape
// Phase 1 measured a naive co-occurrence detector at 29 false positives
// to 1 true positive against; this is one of the 29.
const text = [
'function updateCore(content, intent) {',
' const existingFm = extractFrontmatter(content) as Record<string, unknown>;',
' const hasFrontmatter = Object.keys(existingFm).length > 0;',
' const body = stripFrontmatter(content);',
' const result = stateReplaceField(body, intent.field, intent.value);',
' if (result === null) {',
' return { content, updated: [], data: { updated: false } };',
' }',
'}',
].join('\n');
assert.deepStrictEqual(findUnstrippedContentWrites(EXECUTOR_FILE, text), []);
});
});
describe('E5 — sectionBody-scoped stateReplaceField calls are NOT detected', () => {
test('guard: a literal field name against a non-stripFrontmatter-derived section slice is not flagged', () => {
// Verbatim from src/state-transition.cts's real mutateCurrentPositionFirstTime:
// sectionBody is a Current-Position section slice (frontmatter-free by
// construction — it comes from body.slice(...), never from raw content),
// and the field name is a fixed Title-Case literal that can never
// collide with a lowercase/snake_case YAML key. One of the ~20 calls
// Phase 1's naive detector over-reported.
const text = [
'function mutateCurrentPositionFirstTime(body, intent, today, updated) {',
' const span = locateCurrentPosition(body);',
' if (span === null) return body;',
' let sectionBody = body.slice(span.start, span.end);',
' const phaseLabel = `${intent.phaseNumber} — EXECUTING`;',
' if (/^Phase:/m.test(sectionBody)) {',
' sectionBody = sectionBody.replace(/^Phase:.*$/m, `Phase: ${phaseLabel}`);',
' } else {',
" const replaced = stateReplaceField(sectionBody, 'Phase', phaseLabel);",
' if (replaced !== null) sectionBody = replaced;',
' }',
'}',
].join('\n');
assert.deepStrictEqual(findUnstrippedContentWrites(EXECUTOR_FILE, text), []);
});
});
// ─────────────────────────────────────────────────────────────────────────
// F — ADR-3473 §8.6: the raw-write axis (`findRawStateWrites`), and the
// guard's own real CLI entry point proving the shrunk guard can still fail.
// ─────────────────────────────────────────────────────────────────────────
describe('F1 — the raw-write axis: pure function coverage', () => {
test('guard: fs.writeFileSync against statePath is reported', () => {
const text = [
'function bogusRawWrite(statePath, content) {',
' fs.writeFileSync(statePath, content);',
'}',
].join('\n');
const out = findRawStateWrites(OTHER_FILE, text);
assert.strictEqual(out.length, 1);
assert.strictEqual(out[0].reason, REASON.RAW_STATE_WRITE);
assert.strictEqual(out[0].line, 2);
assert.strictEqual(out[0].source, 'fs.writeFileSync(statePath, content);');
});
test('guard: fs.writeFileSync against a STATE.md literal is reported', () => {
const text = [
'function bogusRawWrite(cwd, content) {',
" fs.writeFileSync(path.join(cwd, 'STATE.md'), content);",
'}',
].join('\n');
const out = findRawStateWrites(OTHER_FILE, text);
assert.strictEqual(out.length, 1);
assert.strictEqual(out[0].reason, REASON.RAW_STATE_WRITE);
});
test('control: fs.writeFileSync against an unrelated target is NOT reported', () => {
const text = ['function writeSomethingElse(otherPath, content) {', ' fs.writeFileSync(otherPath, content);', '}'].join(
'\n',
);
assert.deepStrictEqual(findRawStateWrites(OTHER_FILE, text), []);
});
test('control: platformWriteSync (the sanctioned seam) against statePath is NOT reported — a different call, by name', () => {
// The type/seam this axis exists BESIDE, not instead of: every real
// STATE.md writer in this codebase calls `platformWriteSync`, never raw
// `fs.writeFileSync`, against `statePath`. This axis only matches the
// literal `fs.writeFileSync` call shape.
const text = ['function realWriter(statePath, content) {', ' platformWriteSync(statePath, content);', '}'].join(
'\n',
);
assert.deepStrictEqual(findRawStateWrites(OTHER_FILE, text), []);
});
test('guard: comments are not drift', () => {
const text = [
'// fs.writeFileSync(statePath, modified);',
'/**',
' * fs.writeFileSync(statePath, modified);',
' */',
'function noop() {}',
].join('\n');
assert.deepStrictEqual(findRawStateWrites(OTHER_FILE, text), []);
});
test('targetsStatePath: bare identifier and STATE.md-literal both match; an unrelated identifier does not', () => {
assert.ok(targetsStatePath('statePath'));
assert.ok(targetsStatePath("path.join(cwd, 'STATE.md')"));
assert.ok(!targetsStatePath('otherPath'));
});
});
describe('F2 — a guard that cannot fail is not a guard: the real CLI entry point catches a raw write', () => {
test('CLI: --root <synthetic tree> with fs.writeFileSync(statePath, ...) is reported, without touching the real src/ tree', (t) => {
// A throwaway tree in an OS temp dir, never inside this repository — the
// guard's own `--root` flag (default REPO_ROOT, so every OTHER caller of
// this CLI is unaffected) is what makes this possible without mutating
// the repo under test. See this file's header for why a real-src/
// fixture was rejected.
const tmpRoot = createTempDir('state-write-path-drift-guard-');
t.after(() => cleanup(tmpRoot));
const srcDir = path.join(tmpRoot, 'src');
fs.mkdirSync(srcDir, { recursive: true });
const fixtureContent = [
"import * as fs from 'node:fs';",
'',
'function bogusRawWrite(statePath: string, content: string): void {',
' fs.writeFileSync(statePath, content);',
'}',
].join('\n');
fs.writeFileSync(path.join(srcDir, 'bogus.cts'), fixtureContent, 'utf8');
const result = runNode([GUARD_PATH, '--root', tmpRoot, '--json'], { cwd: REPO_ROOT, timeoutMs: PROBE_TIMEOUT_MS });
assert.strictEqual(result.outcome, 'exited');
assert.strictEqual(result.exitCode, 1, 'a real raw write against statePath must fail the CLI, not pass it');
const body = JSON.parse(result.stdout);
assert.strictEqual(body.ok, false);
const finding = body.findings.find((f) => f.file === 'src/bogus.cts');
assert.ok(finding, 'the planted fixture must appear in --json findings');
assert.strictEqual(finding.reason, REASON.RAW_STATE_WRITE);
assert.strictEqual(finding.line, 4);
});
test('CLI: --root <a clean synthetic tree> passes, proving --root does not silently widen scope back to REPO_ROOT', (t) => {
const tmpRoot = createTempDir('state-write-path-drift-guard-clean-');
t.after(() => cleanup(tmpRoot));
const srcDir = path.join(tmpRoot, 'src');
fs.mkdirSync(srcDir, { recursive: true });
fs.writeFileSync(path.join(srcDir, 'clean.cts'), "export const noop = () => 'noop';\n", 'utf8');
const result = runNode([GUARD_PATH, '--root', tmpRoot, '--json'], { cwd: REPO_ROOT, timeoutMs: PROBE_TIMEOUT_MS });
assert.strictEqual(result.outcome, 'exited');
assert.strictEqual(result.exitCode, 0);
const body = JSON.parse(result.stdout);
assert.strictEqual(body.ok, true);
assert.deepStrictEqual(body.findings, []);
});
});
describe('F3 — a guard that cannot fail is not a guard: the real CLI entry point catches a composition bypass', () => {
test('CLI: --root <synthetic tree> with a re-assembled syncStateFrontmatter + applyPostSyncPreservation pair is reported, without touching the real src/ tree', (t) => {
const tmpRoot = createTempDir('state-write-path-drift-guard-composition-');
t.after(() => cleanup(tmpRoot));
const srcDir = path.join(tmpRoot, 'src');
fs.mkdirSync(srcDir, { recursive: true });
const fixtureContent = [
'function cmdReassembledAdapter(cwd: string, statePath: string, stateContent: string): string {',
' let synced = syncStateFrontmatter(stateContent, cwd, authoritativeFm);',
' synced = applyPostSyncPreservation(originalStateContent, stateContent, synced, statePath, options);',
' return synced;',
'}',
].join('\n');
fs.writeFileSync(path.join(srcDir, 'bogus-composition.cts'), fixtureContent, 'utf8');
const result = runNode([GUARD_PATH, '--root', tmpRoot, '--json'], { cwd: REPO_ROOT, timeoutMs: PROBE_TIMEOUT_MS });
assert.strictEqual(result.outcome, 'exited');
assert.strictEqual(result.exitCode, 1, 'a re-assembled write-seam composition must fail the CLI, not pass it');
const body = JSON.parse(result.stdout);
assert.strictEqual(body.ok, false);
const findings = body.findings.filter((f) => f.file === 'src/bogus-composition.cts');
assert.strictEqual(findings.length, 2, 'both re-assembled stages must appear in --json findings');
assert.ok(findings.every((f) => f.reason === REASON.COMPOSITION_BYPASS));
});
});