* docs(#3469): amend ADR-3408 section 8.3 — the pipeline has sanctioned exceptions Section 8.3 read 'Every STATE.md write applies the pipeline.' That is false by design for two commands, and acting on it would have inverted a shipped feature. Preservation makes curated frontmatter win over a re-derived body value. state sync exists to do the opposite — #905's 'body annotation beats existing frontmatter when both are present'; it re-derives frontmatter FROM the body. REGENERATE_STATE is a factory reset that rebuilds STATE.md from scratch. Applying the pipeline to either would re-lock exactly what the command was invoked to replace. This issue's own scope line, inherited from the epic, said to route the direct writeStateMd callers through the pipeline. For cmdStateSync that would have shipped silently, with every gate green, because no test asserts that sync LETS the body win. Caught by reading the helper's docstring and then verifying the claim against the code — a stale comment had already misdirected this epic once. Both commands are now named in a closed exception list and are permanent ratchet entries. Consequence recorded rather than left to bite Phase 4: the 'drive the ratchet to 0 and delete the file' target in this ADR and in #3471 is wrong. Two entries are permanent, so the correct end state is 2, and the honest report is '0 removable bypasses, 2 sanctioned'. A guard reaching 0 here would only do so by having stopped looking at two real writers. * refactor(#3469): one composition for the write seam, not one per caller Implements ADR-3408 section 8.3 as amended. syncAndPreserveStateMd is now the single composition of syncStateFrontmatter and applyPostSyncPreservation. readModifyWriteStateMd and cmdPhaseComplete both CALL it instead of each assembling the two steps themselves. cmdPhaseComplete keeps its own writePlanningFileSet envelope — the composition returns content, it does not take over the write, so STATE.md still commits atomically with ROADMAP and REQUIREMENTS. Assembling the stages at a call site is a re-derivation even when every step calls an owner. Upstream's fix(#3374) routed cmdPhaseComplete through applyPostSyncPreservation but left it calling syncStateFrontmatter directly first, so the composition was duplicated and free to diverge with both guards green. That is ADR-3180 Amendment 2's finding repeating on the write side. cmdMilestoneComplete gains preservation. It wrote through writeStateMd, so it got sync and no preservation — the identical shape #3374 reported for phase.complete, and flagged upstream as a follow-up in the helper's own docstring. This is that follow-up. Divergence is now visible: preservation_warnings names each field restored over a disagreeing derived value. Deliberately NOT named warnings — cmdPhaseComplete already exposes warnings as a prose string array, and two sibling commands carrying that name with different element types is Generative Fix Divergence, the class this epic exists to remove. patchCore stops running stateReplaceField over the whole document. One observable consequence, intended per design row 9: a frontmatter-shaped patch key with no body counterpart now reports failed instead of silently succeeding, because the old whole-document match was literally hitting the YAML line case-insensitively. The guard closes Phase 1's DECLARED KNOWN GAP as promised rather than re-deferring it: section 8.3(b) detection is tractable now the composition exists. Scoped by two factors to avoid Phase 1's measured 29-to-1 false positive rate — a variable field-name argument AND a content argument whose nearest preceding assignment is not stripFrontmatter. Verified 0 findings and 0 false positives across all 33 call sites, plus 5 synthetic shapes. It also detects the re-assembly shape above. Ratchet: 4 entries to 2, both sanctioned-permanent. cmdStateSync's owner changes from #3471 to sanctioned-permanent per Amendment 2 — routing it through preservation would invert the #905 contract. Also fixed inline rather than deferred: cmdMilestoneComplete's STATE.md read now happens inside withStateLock. It previously read outside any lock before writeStateMd took its own, leaving a TOCTOU window under concurrent writers. * test(#3469): characterization coverage for the single write seam Matrix sections A-E. Criterion 6 was amended by maintainer decision — all five instances closed by point fixes while Phase 1 was in flight — so these are characterization tests at the consumer's output per ADR-3180 Decision 4(b)/(c), paired with the drift guard's count, never either alone. Section C is the one that earns its keep. cmdStateSync is a sanctioned permanent exception: state sync exists to re-derive frontmatter FROM the body, so preservation there re-locks exactly what the command was invoked to replace. C1 pins that the body wins; C4 pins that this phase left the command byte-identical. Nothing else in the suite would notice if a future change made sync start preserving, and the natural reading of 'one write seam' is to make precisely that change. Section E pins the guard's false-positive scoping. E4 (updateCore's strip-then-replace) and E5 (sectionBody-scoped calls) must NOT be reported — the naive detector measured 29 false positives to 1 true positive in Phase 1. E7 is the inverse: a sanctioned-permanent entry disappearing must FAIL, because a guard reaching zero here would only do so by having stopped looking at two real writers. Also corrects a stale test that asserted patchCore's old whole-document behavior, which this phase deliberately changes. One honest limitation, flagged rather than papered over: A1's 'byte-identical to pre-refactor' cannot be diffed against real pre-refactor bytes from inside the suite. It is implemented as the seeded fast-check property that cmdPhaseComplete's composed output equals readModifyWriteStateMd's for the same inputs — the strongest available proxy, not the literal claim. * docs(#3469): refresh the seam glossary entry and add the changeset Two spec-review gaps, both real. CONTEXT.md's STATE.md Transition Module entry named three direct writeStateMd callers including cmdMilestoneComplete. This phase routed that one through the composition, so the line was false the moment the refactor landed. Worth recording plainly: I wrote that sentence in Phase 0, correcting an older stale pointer in it, and my own Phase 2 change invalidated it again within the same epic. That is the exact drift this epic exists to remove, demonstrated on the epic's own documentation — and it is why the entry now ends by saying the whole-repo drift guard, not this line, is the authoritative count. The entry now records the composition (syncAndPreserveStateMd) and states that exactly two direct callers remain, both SANCTIONED PERMANENT rather than debt. Changeset: type Changed, because milestone complete's observable output moves. Tier-2 per ADR-3180 Decision 3 — a stale body line no longer wins over fresher frontmatter, and the command gains preservation_warnings. Docs requirement is met by the ADR amendment already in this diff. * test(#3469): register property-test temp-dir cleanup at creation time Standards review, minor but real: the new fast-check property cleaned up its temp dirs in a loop AFTER fc.assert returned. A genuine property failure throws, so that line never ran and every dir from the failing run — including all of fast-check's shrinking iterations — leaked. The failure path is exactly when a littered machine hurts most, and a failing property test is the case the test exists for. Cleanup is now registered with t.after() at dir-creation time, so teardown happens however the test exits. Not try/finally — CONTRIBUTING.md:356 bans it inside test bodies, which is why the after-the-assertion shape existed in the first place. Swept the rest of the branch's test diff for the same shape; phase.test.cjs already uses registered teardown and nothing else matched. * fix(#3469): patchCore routes frontmatter writes instead of dropping them Checkpoint returned 10 failures of 33880. One implementation defect, three test defects, one stale test — all fixed, and the implementation defect is the one that matters. patchCore stripped frontmatter and then reconstructed it VERBATIM, applying no patches to it. An arbitrary custom frontmatter key with no body counterpart and no FIELD_CLASSIFICATION row — risk_level in the upstream fix(#3351) test — therefore always reported failed and silently never wrote. It worked before, via the old whole-document match on the raw YAML line. That is a regression against this phase's own design row 9, which requires frontmatter changes to ROUTE THROUGH the seam — still work, policy-governed — not to stop working. Removing a capability is not routing it. An upstream test caught it, which is the argument for running the checkpoint before believing the refactor. patchCore now partitions by frontmatter shape, decided structurally from the parsed frontmatter's own keys rather than a naming heuristic: - classified keys still report failed — policy owns them and a raw patch may not bypass it; - unclassified keys apply to the frontmatter object and report updated — Phase 1's behavior-table row 19, a field with no row is not this contract's business; - body-shaped keys are unchanged. The property 'failure' was my own test breaking the repo's Clock Seams rule. The two paths agree byte-for-byte; the only difference was last_updated, stamped from the wall clock on two invocations milliseconds apart, so it could never pass. Time is now frozen with mock.timers across both — not by excluding last_updated from the comparison, which would have silently stopped comparing a field the composition writes. B4's fixture could not discriminate: normalizeStateStatus maps any text containing 'complete' to 'completed', and milestone complete's own new body value derives to exactly that — which was also the fixture's stale value. The stale value is now 'executing' so the assertion can tell 'body correctly won' from 'stale survived'. B5's fixture tripped a pre-existing unstarted-phase guard before reaching any write-seam code; it now has the matching phase directory. D9 asserted the old exempt set. readModifyWriteStateMd now calls one symbol rather than assembling two, so it needs no exemption; syncAndPreserveStateMd is the sole legitimate composition site. * fix(#3469): patchCore resolves body-first, so the body wins a name collision Re-verification returned 2 failures of 33880, both D4 — the hostile row for a key that exists as BOTH a frontmatter key and a body field. The partition checked frontmatter first, so 'status' — classified in FIELD_CLASSIFICATION and also present as a body 'Status:' line — routed to the frontmatter branch, was rejected as classified, and reported failed. Wrong order. Patching 'status' means the body field, and upstream fix(#3351) says so in its own comment: 'the legitimate working case for state.patch is display-cased BODY fields — Status, Current Plan, Phase.' The body is authoritative in this model; frontmatter is the projection. D4 asserted exactly that and was right. Resolution order is now body, then frontmatter: 1. resolves to a body field -> apply to body, updated 2. else an own key of the frontmatter: classified -> failed (policy owns it) unclassified -> apply to frontmatter, updated 3. else -> failed Verified by probe against the compiled lib for all four cases rather than asserted: risk_level (frontmatter-only, unclassified) still lands; current_phase still fails; display-cased Status unchanged; D4's lower-cased status now lands via the body with the frontmatter untouched. The current_phase case was the one that could have regressed silently, so its fixture was read rather than assumed — D1's body carries 'Phase: 3 (alpha)' and no 'Current Phase:' line, so body-first cannot reach it. * chore(#3469): backfill pr number in changeset fragment --------- Co-authored-by: sim <sim@local>
761 lines
34 KiB
JavaScript
761 lines
34 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Tests for the STATE.md write-path anti-divergence drift guard
|
|
* (epic #3408, issue #3468, ADR-3408 Decision 5) —
|
|
* `scripts/lint-state-write-path-drift.cjs`.
|
|
*
|
|
* Design contract: docs/adr/3408-state-write-path-preservation.md (§8.1/§8.2/§8.3)
|
|
* Test matrix: .gsd/phase/refactor-3468-table-driven-preservation/50-test-matrix.md
|
|
* (section D, rows D1-D13 — this file covers section D only)
|
|
*
|
|
* Every row except D1 and D13 drives the guard's exported PURE functions
|
|
* (`findSeamBypasses`, `findPromptSeamUses`, `applyRatchet`, `loadBaseline`)
|
|
* directly with in-memory fixtures — no temp tree is needed, mirroring
|
|
* tests/state-field-drift.test.cjs's own house pattern for this class of
|
|
* guard. `REPO_ROOT` inside the guard module is a constant resolved from
|
|
* `__dirname` at require time, so it cannot be pointed at a synthetic tree
|
|
* without changing the guard's own interface — D1 (the real-tree contract)
|
|
* is therefore driven through the CLI's `--json` output instead, and D13
|
|
* (an unreadable file) through an `fs.readFileSync` monkeypatch rather than
|
|
* a real synthetic tree.
|
|
*
|
|
* Fixtures use array `.join('\n')`, never an indented template literal —
|
|
* indentation bleed would shift every asserted line number. Per D2/D5/D7's
|
|
* matrix note ("guard fixtures come from outside the guard's own writer"),
|
|
* the write-seam call lines reused below are copied VERBATIM from real,
|
|
* pre-existing production call sites (the guard's own current baseline
|
|
* entries) rather than invented by this test file:
|
|
* - `writeStateMd(statePath, modified, cwd);` src/state.cts:3682
|
|
* - `writeStateMd(statePath, result.content, cwd);` src/milestone.cts:865
|
|
* - `writeStateMd(statePath, stateContent, cwd);` src/health-diagnostic.cts:337
|
|
*
|
|
* 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 guard = require('../scripts/lint-state-write-path-drift.cjs');
|
|
const {
|
|
REASON,
|
|
findSeamBypasses,
|
|
findPromptSeamUses,
|
|
findPolicyDispatchDrift,
|
|
findUnstrippedContentWrites,
|
|
applyRatchet,
|
|
loadBaseline,
|
|
buildBaselineEntries,
|
|
collect,
|
|
SEAM_OWNER_FILE,
|
|
SEAM_OWNER_EXEMPT_FUNCTIONS,
|
|
EXECUTOR_FILE,
|
|
REPO_ROOT,
|
|
BASELINE_PATH,
|
|
} = guard;
|
|
|
|
const GUARD_PATH = path.join(REPO_ROOT, 'scripts', 'lint-state-write-path-drift.cjs');
|
|
|
|
// A synthetic, non-owner, non-executor consumer file — never a real repo
|
|
// path — used as the `rel` argument wherever a row does not specifically
|
|
// need EXECUTOR_FILE or SEAM_OWNER_FILE behavior.
|
|
const OTHER_FILE = 'src/example-consumer.cts';
|
|
const OTHER_FILE_2 = 'src/example-consumer-2.cts';
|
|
const OTHER_FILE_3 = 'src/example-consumer-3.cts';
|
|
|
|
// ─── D1: the real tree, through the CLI's --json contract ─────────────────
|
|
|
|
describe('D1 — the real tree', () => {
|
|
test('guard: clean tree passes', () => {
|
|
// Expected RED until the sibling refactor of src/state-transition.cts
|
|
// (issue #3468 Phase 1, concurrent with this test file's own authorship)
|
|
// lands: at write time the executor still dispatches five fields by
|
|
// literal name and leaves `derive`/`clear` unimplemented, which this
|
|
// guard's policy-dispatch axis correctly reports as 7 findings. Mirrors
|
|
// tests/milestone-window-drift-guard.test.cjs's own precedent of an
|
|
// explicitly-documented real-tree row that is red until its companion
|
|
// consolidation lands.
|
|
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);
|
|
});
|
|
});
|
|
|
|
// ─── D2: the guard MUST be able to fail ────────────────────────────────────
|
|
|
|
describe('D2 — an unrecorded bypass fails', () => {
|
|
test('guard: an unrecorded bypass fails', () => {
|
|
const text = [
|
|
'function cmdSomethingElse(cwd) {',
|
|
' const modified = deriveModifiedContent();',
|
|
' writeStateMd(statePath, modified, cwd);',
|
|
'}',
|
|
].join('\n');
|
|
|
|
const observed = findSeamBypasses(OTHER_FILE, text);
|
|
assert.strictEqual(observed.length, 1);
|
|
assert.strictEqual(observed[0].line, 3);
|
|
|
|
const findings = applyRatchet(observed, { entries: [] });
|
|
// A guard that cannot fail is worse than no guard: an unacknowledged
|
|
// bypass against an empty baseline MUST produce exactly one finding,
|
|
// reasoned, at the exact file and line — not merely "an array".
|
|
assert.strictEqual(findings.length, 1);
|
|
assert.strictEqual(findings[0].reason, REASON.SEAM_BYPASS_UNRECORDED);
|
|
assert.strictEqual(findings[0].file, OTHER_FILE);
|
|
assert.strictEqual(findings[0].line, 3);
|
|
assert.strictEqual(findings[0].source, 'writeStateMd(statePath, modified, cwd);');
|
|
});
|
|
});
|
|
|
|
// ─── D3: a recorded bypass is acknowledged ─────────────────────────────────
|
|
|
|
describe('D3 — a recorded bypass is acknowledged', () => {
|
|
test('guard: a recorded bypass is acknowledged', () => {
|
|
const text = [
|
|
'function cmdSomethingElse(cwd) {',
|
|
' const modified = deriveModifiedContent();',
|
|
' writeStateMd(statePath, modified, cwd);',
|
|
'}',
|
|
].join('\n');
|
|
|
|
const observed = findSeamBypasses(OTHER_FILE, text);
|
|
const baseline = {
|
|
entries: [{ file: OTHER_FILE, source: 'writeStateMd(statePath, modified, cwd);', symbol: 'writeStateMd', count: 1, owner: null }],
|
|
};
|
|
assert.deepStrictEqual(applyRatchet(observed, baseline), []);
|
|
});
|
|
});
|
|
|
|
// ─── D4: a stale acknowledgment fails ──────────────────────────────────────
|
|
|
|
describe('D4 — a stale acknowledgment fails', () => {
|
|
test('guard: a stale acknowledgment fails', () => {
|
|
// The call site the baseline acknowledges no longer fires at all this
|
|
// scan — the acknowledgment has outlived what it describes.
|
|
const baseline = {
|
|
entries: [{ file: OTHER_FILE, source: 'writeStateMd(statePath, modified, cwd);', symbol: 'writeStateMd', count: 1, owner: null }],
|
|
};
|
|
const findings = applyRatchet([], baseline);
|
|
assert.strictEqual(findings.length, 1);
|
|
assert.strictEqual(findings[0].reason, REASON.BASELINE_ENTRY_STALE);
|
|
assert.strictEqual(findings[0].file, OTHER_FILE);
|
|
assert.strictEqual(findings[0].observed, 0);
|
|
assert.strictEqual(findings[0].acknowledged, 1);
|
|
});
|
|
});
|
|
|
|
// ─── D5/D6/D7: the occurrence-count boundary triple (limit-1/limit/limit+1) ─
|
|
// The baseline acknowledges 2 occurrences throughout ("the limit"); only the
|
|
// OBSERVED count in the fixture source varies. This is why the ratchet keys
|
|
// entries on (file, trimmed source) instead of line number: two
|
|
// byte-identical call sites in one file are otherwise indistinguishable.
|
|
|
|
describe('D5 — occurrence count catches partial migration (limit-1)', () => {
|
|
test('guard: occurrence count catches partial migration', () => {
|
|
const text = [
|
|
'function cmdSomethingElse(cwd) {',
|
|
' writeStateMd(statePath, result.content, cwd);',
|
|
'}',
|
|
].join('\n');
|
|
|
|
const observed = findSeamBypasses(OTHER_FILE_2, text);
|
|
assert.strictEqual(observed.length, 1);
|
|
|
|
const baseline = {
|
|
entries: [{ file: OTHER_FILE_2, source: 'writeStateMd(statePath, result.content, cwd);', symbol: 'writeStateMd', count: 2, owner: null }],
|
|
};
|
|
const findings = applyRatchet(observed, baseline);
|
|
// Only 1 of the 2 acknowledged call sites still fires — a genuine
|
|
// partial migration, not a clean removal — must fail, not silently pass.
|
|
assert.strictEqual(findings.length, 1);
|
|
assert.strictEqual(findings[0].reason, REASON.SEAM_BYPASS_COUNT_SHRANK);
|
|
assert.strictEqual(findings[0].observed, 1);
|
|
assert.strictEqual(findings[0].acknowledged, 2);
|
|
});
|
|
});
|
|
|
|
describe('D6 — matching occurrence count passes (limit)', () => {
|
|
test('guard: matching occurrence count passes', () => {
|
|
const text = [
|
|
'function cmdSomethingElse(cwd) {',
|
|
' writeStateMd(statePath, result.content, cwd);',
|
|
' writeStateMd(statePath, result.content, cwd);',
|
|
'}',
|
|
].join('\n');
|
|
|
|
const observed = findSeamBypasses(OTHER_FILE_2, text);
|
|
assert.strictEqual(observed.length, 2);
|
|
|
|
const baseline = {
|
|
entries: [{ file: OTHER_FILE_2, source: 'writeStateMd(statePath, result.content, cwd);', symbol: 'writeStateMd', count: 2, owner: null }],
|
|
};
|
|
assert.deepStrictEqual(applyRatchet(observed, baseline), []);
|
|
});
|
|
});
|
|
|
|
describe('D7 — a new copy beside an acknowledged one fails (limit+1)', () => {
|
|
test('guard: a new copy beside an acknowledged one fails', () => {
|
|
const text = [
|
|
'function cmdA(cwd) {',
|
|
' writeStateMd(statePath, stateContent, cwd);',
|
|
'}',
|
|
'function cmdB(cwd) {',
|
|
' writeStateMd(statePath, stateContent, cwd);',
|
|
'}',
|
|
'function cmdC(cwd) {',
|
|
' writeStateMd(statePath, stateContent, cwd);',
|
|
'}',
|
|
].join('\n');
|
|
|
|
const observed = findSeamBypasses(OTHER_FILE_3, text);
|
|
assert.strictEqual(observed.length, 3);
|
|
|
|
const baseline = {
|
|
entries: [{ file: OTHER_FILE_3, source: 'writeStateMd(statePath, stateContent, cwd);', symbol: 'writeStateMd', count: 2, owner: null }],
|
|
};
|
|
const findings = applyRatchet(observed, baseline);
|
|
assert.strictEqual(findings.length, 1);
|
|
assert.strictEqual(findings[0].reason, REASON.SEAM_BYPASS_COUNT_GREW);
|
|
assert.strictEqual(findings[0].observed, 3);
|
|
assert.strictEqual(findings[0].acknowledged, 2);
|
|
});
|
|
});
|
|
|
|
// ─── D8: comments are not drift ────────────────────────────────────────────
|
|
|
|
describe('D8 — comments are not drift', () => {
|
|
test('guard: comments are not drift', () => {
|
|
const text = [
|
|
'// writeStateMd(statePath, modified, cwd);',
|
|
'/**',
|
|
' * writeStateMd(statePath, modified, cwd);',
|
|
' */',
|
|
'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(findSeamBypasses(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', () => {
|
|
// #3469: `readModifyWriteStateMd` now calls the single
|
|
// `syncAndPreserveStateMd` symbol rather than assembling the two seam
|
|
// calls itself, so it needs no exemption — `syncAndPreserveStateMd` is
|
|
// the sole legitimate place `syncStateFrontmatter(` and
|
|
// `applyPostSyncPreservation(` appear together (the composition every
|
|
// OTHER caller, including `readModifyWriteStateMd`, now routes through).
|
|
assert.ok(SEAM_OWNER_EXEMPT_FUNCTIONS.includes('syncAndPreserveStateMd'));
|
|
|
|
const text = [
|
|
'function syncAndPreserveStateMd(originalContent, transformedContent, statePath, cwd, resync) {',
|
|
' const synced = syncStateFrontmatter(transformedContent, cwd);',
|
|
' return applyPostSyncPreservation(originalContent, transformedContent, synced, statePath, resync);',
|
|
'}',
|
|
].join('\n');
|
|
|
|
// The seam's own internal plumbing (the one owned composition —
|
|
// sync then post-sync preservation) is not a bypass.
|
|
assert.deepStrictEqual(findSeamBypasses(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();',
|
|
' writeStateMd(statePath, modified, cwd);',
|
|
' return modified;',
|
|
'}',
|
|
].join('\n');
|
|
|
|
const out = findSeamBypasses(SEAM_OWNER_FILE, text);
|
|
assert.strictEqual(out.length, 1);
|
|
assert.strictEqual(out[0].line, 3);
|
|
});
|
|
});
|
|
|
|
// ─── 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].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), []);
|
|
});
|
|
});
|
|
|
|
// ─── D12: CRLF is scanned identically to LF ────────────────────────────────
|
|
|
|
describe('D12 — CRLF is scanned identically', () => {
|
|
test('guard: CRLF is scanned identically', () => {
|
|
const lfText = [
|
|
'function cmdSomethingElse(cwd) {',
|
|
' writeStateMd(statePath, modified, cwd);',
|
|
'}',
|
|
].join('\n');
|
|
const crlfText = lfText.split('\n').join('\r\n');
|
|
|
|
const lfOut = findSeamBypasses(OTHER_FILE, lfText);
|
|
const crlfOut = findSeamBypasses(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, 'writeStateMd(statePath, modified, cwd);');
|
|
});
|
|
});
|
|
|
|
// ─── D13: an unreadable file degrades, never crashes ───────────────────────
|
|
|
|
describe('D13 — an unreadable file is reported, not fatal', () => {
|
|
test('guard: an unreadable file is reported, not fatal', (t) => {
|
|
const originalReadFileSync = fs.readFileSync;
|
|
t.after(() => {
|
|
fs.readFileSync = originalReadFileSync;
|
|
});
|
|
|
|
// Monkeypatch (never chmod 0o000, which root bypasses under Docker/CI
|
|
// and would leave this assertion covering nothing). Scoped to
|
|
// BASELINE_PATH only, so no other read in this process is disturbed.
|
|
fs.readFileSync = function patchedReadFileSync(target, ...rest) {
|
|
if (target === BASELINE_PATH) {
|
|
const err = new Error('simulated unreadable baseline file');
|
|
err.code = 'EACCES';
|
|
throw err;
|
|
}
|
|
return originalReadFileSync.call(fs, target, ...rest);
|
|
};
|
|
|
|
// loadBaseline() must not throw — it degrades to a returned value.
|
|
assert.doesNotThrow(() => loadBaseline());
|
|
const result = loadBaseline();
|
|
// An unreadable file (EACCES) is NOT the same state as an absent one
|
|
// (ENOENT) and must not degrade to the same "no baseline yet" shape —
|
|
// collapsing the two is the exact ADR-3180/ADR-3408 failure mode this
|
|
// guard exists to catch. loadBaseline() must surface a distinguishable
|
|
// `entries: null` result carrying the underlying fs error code.
|
|
assert.deepStrictEqual(result, { entries: null, code: 'EACCES' });
|
|
});
|
|
|
|
test('CLI: an unreadable baseline reaches REASON.BASELINE_UNREADABLE with its error code, not the first-run shape', (t) => {
|
|
const originalReadFileSync = fs.readFileSync;
|
|
t.after(() => {
|
|
fs.readFileSync = originalReadFileSync;
|
|
});
|
|
|
|
fs.readFileSync = function patchedReadFileSync(target, ...rest) {
|
|
if (target === BASELINE_PATH) {
|
|
const err = new Error('simulated unreadable baseline file');
|
|
err.code = 'EACCES';
|
|
throw err;
|
|
}
|
|
return originalReadFileSync.call(fs, target, ...rest);
|
|
};
|
|
|
|
// Drive main() in-process (not via the CLI subprocess helper) so the
|
|
// monkeypatched fs.readFileSync is actually in effect for the call.
|
|
const originalArgv = process.argv;
|
|
const originalWrite = process.stdout.write;
|
|
t.after(() => {
|
|
process.stdout.write = originalWrite;
|
|
process.argv = originalArgv;
|
|
process.exitCode = 0;
|
|
});
|
|
let captured = '';
|
|
process.stdout.write = function patchedWrite(chunk) {
|
|
captured += chunk;
|
|
return true;
|
|
};
|
|
process.argv = [originalArgv[0], GUARD_PATH, '--json'];
|
|
guard.main(['--json']);
|
|
const exitCode = process.exitCode;
|
|
|
|
assert.strictEqual(exitCode, 1);
|
|
const parsed = JSON.parse(captured);
|
|
assert.strictEqual(parsed.ok, false);
|
|
assert.strictEqual(parsed.findings.length, 1);
|
|
assert.strictEqual(parsed.findings[0].reason, REASON.BASELINE_UNREADABLE);
|
|
assert.strictEqual(parsed.findings[0].code, 'EACCES');
|
|
});
|
|
});
|
|
|
|
// ─── 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 and the committed
|
|
// baseline (`scripts/state-write-path-drift-baseline.json`) 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`, baseline) 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('findSeamBypasses: an attacker-controlled filename comes back escaped', () => {
|
|
const text = ['function cmdSomethingElse(cwd) {', ' writeStateMd(statePath, modified, cwd);', '}'].join('\n');
|
|
|
|
const out = findSeamBypasses(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));
|
|
|
|
// The SAME escaped value is what a regenerated baseline entry persists —
|
|
// proving the fix reaches the committed
|
|
// scripts/state-write-path-drift-baseline.json, not just the finding.
|
|
const entries = buildBaselineEntries(out, null);
|
|
assert.strictEqual(entries.length, 1);
|
|
assert.strictEqual(entries[0].file, ESCAPED_FILE);
|
|
assert.ok(!entries[0].file.includes(RLO));
|
|
assert.ok(!entries[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));
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Phase 2 (#3469) — ADR-3408 §8.3 Matrix section E: guard rows closing
|
|
// Phase 1's declared known gap (Axis 3, §8.3(b)) and pinning the ratchet's
|
|
// new 2-permanent-entry shape (Amendment 2). Test matrix:
|
|
// .gsd/phase/refactor-3469-one-write-seam/50-test-matrix.md
|
|
//
|
|
// E4/E5 are the false-positive guards — the exact shape that measured 29
|
|
// false positives to 1 true positive in Phase 1's naive co-occurrence
|
|
// approximation (see this guard's own header, Axis 3). E7 is the inverse: a
|
|
// sanctioned-permanent entry vanishing from the observed tree must FAIL, not
|
|
// silently reach zero — a guard reaching zero here would only do so by
|
|
// having stopped looking at a real writer.
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
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 this same phase.
|
|
const text = [
|
|
'function cmdReassembledAdapter(cwd, statePath, stateContent) {',
|
|
' let synced = syncStateFrontmatter(stateContent, cwd, authoritativeFm);',
|
|
' synced = applyPostSyncPreservation(originalStateContent, stateContent, synced, statePath, true, authoritativeFm);',
|
|
' return synced;',
|
|
'}',
|
|
].join('\n');
|
|
|
|
const observed = findSeamBypasses(OTHER_FILE, text);
|
|
assert.strictEqual(observed.length, 2, 'both re-assembled stages must be caught, not just one');
|
|
assert.deepStrictEqual(observed.map((f) => f.symbol).sort(), ['applyPostSyncPreservation', 'syncStateFrontmatter']);
|
|
|
|
const findings = applyRatchet(observed, { entries: [] });
|
|
assert.strictEqual(findings.length, 2);
|
|
assert.ok(findings.every((f) => f.reason === REASON.SEAM_BYPASS_UNRECORDED));
|
|
});
|
|
});
|
|
|
|
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 from src/milestone.cts's real cmdMilestoneComplete call site
|
|
// (ADR-3408 Amendment 2's third caller).
|
|
const text = [
|
|
' const finalContent = syncAndPreserveStateMd(',
|
|
' originalStateContent,',
|
|
' result.content,',
|
|
' statePath,',
|
|
' cwd,',
|
|
' true,',
|
|
' undefined,',
|
|
' undefined,',
|
|
' divergedFields,',
|
|
' );',
|
|
].join('\n');
|
|
|
|
assert.deepStrictEqual(findSeamBypasses(OTHER_FILE, text), []);
|
|
});
|
|
});
|
|
|
|
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), []);
|
|
});
|
|
});
|
|
|
|
describe('E6 — ratchet: exactly 2 sanctioned-permanent entries remain (limit)', () => {
|
|
test('guard: the real baseline has exactly 2 permanent entries, and the real tree matches it with zero findings', () => {
|
|
const baseline = loadBaseline();
|
|
assert.strictEqual(
|
|
baseline.entries.length,
|
|
2,
|
|
'ADR-3408 Amendment 2: the ratchet holds exactly 2 sanctioned-permanent entries, not 0 — ' +
|
|
'Phase 4 does not drive this baseline to empty',
|
|
);
|
|
for (const entry of baseline.entries) {
|
|
assert.strictEqual(entry.owner, 'sanctioned-permanent');
|
|
}
|
|
const { seamFindings } = collect();
|
|
const findings = applyRatchet(seamFindings, baseline);
|
|
assert.deepStrictEqual(findings, [], 'the real tree must match the 2-entry baseline exactly');
|
|
});
|
|
});
|
|
|
|
describe('E7 — ratchet: a sanctioned-permanent entry disappearing fails (limit-1)', () => {
|
|
test('guard: removing one of the two permanent entries from the observed tree is reported STALE, not silently accepted', () => {
|
|
const baseline = loadBaseline();
|
|
assert.strictEqual(baseline.entries.length, 2);
|
|
// Simulate one sanctioned entry (cmdStateSync's writeStateMd call)
|
|
// vanishing from the observed tree — exactly the shape §8.3's closed
|
|
// exception list forbids: a sanctioned exception may not silently
|
|
// disappear (a guard reaching zero here would only do so by having
|
|
// stopped looking at a real writer).
|
|
const vanished = baseline.entries[0];
|
|
const stillPresent = baseline.entries[1];
|
|
const observed = [{ file: stillPresent.file, source: stillPresent.source, symbol: stillPresent.symbol, line: 1 }];
|
|
|
|
const findings = applyRatchet(observed, baseline);
|
|
assert.strictEqual(findings.length, 1);
|
|
assert.strictEqual(findings[0].reason, REASON.BASELINE_ENTRY_STALE);
|
|
assert.strictEqual(findings[0].file, vanished.file);
|
|
assert.strictEqual(findings[0].observed, 0);
|
|
assert.strictEqual(findings[0].acknowledged, 1);
|
|
});
|
|
});
|
|
|
|
describe('E8 — ratchet: a 3rd bypass beside the 2 sanctioned entries fails as unrecorded (limit+1)', () => {
|
|
test('guard: a new, unacknowledged writeStateMd call alongside the 2 sanctioned entries fails', () => {
|
|
const baseline = loadBaseline();
|
|
assert.strictEqual(baseline.entries.length, 2);
|
|
const matchingObserved = baseline.entries.map((e) => ({ file: e.file, source: e.source, symbol: e.symbol, line: 1 }));
|
|
const newBypass = { file: OTHER_FILE, source: 'writeStateMd(statePath, modified, cwd);', symbol: 'writeStateMd', line: 42 };
|
|
const observed = [...matchingObserved, newBypass];
|
|
|
|
const findings = applyRatchet(observed, baseline);
|
|
assert.strictEqual(findings.length, 1);
|
|
assert.strictEqual(findings[0].reason, REASON.SEAM_BYPASS_UNRECORDED);
|
|
assert.strictEqual(findings[0].file, OTHER_FILE);
|
|
});
|
|
});
|