Files
msd-core/tests/spec-section.test.cjs
Tom Boucher 7d6d788b51 fix(#4020): bound the test run's temp footprint with a swept run-scoped root (#4207)
* test(#4020): the runner must bound and sweep a run-scoped temp root

* fix(#4020): bound the run's temp footprint with a swept run-scoped root

* test(#4020): isolate the env-mutating rows in child processes

* fix(#4020): gate root removal on ownership so nested runners spare the outer root

* test(#4020): pass the probe file via --files, the runner's explicit-file flag

* test(#4020): resolve the probe by basename, as --files matching requires

* test(#4020): assert root survival, not content survival, in the nested-row

* chore(#4020): changeset for the run-scoped temp root

* chore(#4020): backfill changeset pr number

* fix(#4020): the sweep spares ancestors of the runner's own selected files

* fix(#4020): TMPDIR precedence — an operator redirect beats inherited TEMP/TMP

* fix(#4020): only the root's owner sweeps — a nested runner spares live sibling fixtures

---------

Co-authored-by: sim <sim@local>
2026-09-02 20:47:34 -04:00

162 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* SPEC section-status helper tests.
*
* Asserts the LOCKED behaviour of `spec-section` against the BUILT artifact
* (`gsd-core/bin/lib/spec-section.cjs`), which `npm run build:lib` (run by pretest) emits from
* `src/spec-section.cts`.
*
* This is the contract that pins the spec-less probe fallback's per-section absence detection
* (plan-phase step 7.95). It exists because the prior ad-hoc awk produced two regressions:
* - an exact `^## Prohibitions$` anchor that missed the canonical `## Prohibitions (must-NOT)`
* heading -> a supplied section read as absent -> human SPEC prohibitions overridden;
* - single-table row counting that could miscount multi-table sections.
* Both are regression-tested below.
*/
'use strict';
process.env.GSD_TEST_MODE = '1';
const { test, describe, after } = require('node:test');
const { cleanup } = require('./helpers.cjs'); // #4020: fixture-tree removal
// #4020: remove every writeTmp tree once the suite ends (writeTmp callers hold
// file paths, not dirs, so this is the only owner that can).
after(() => {
for (const dir of SPEC_SECTION_TMP_DIRS.splice(0)) {
try { cleanup(dir); } catch { /* best-effort */ }
}
});
const assert = require('node:assert/strict');
const path = require('node:path');
const fs = require('node:fs');
const os = require('node:os');
const { runNode } = require('./helpers/process-seam.cjs');
const { toLegacyResult } = require('./helpers/git-fixture.cjs');
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const BUILT_SCRIPT = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'spec-section.cjs');
const ss = require(BUILT_SCRIPT);
// #4020: every writeTmp tree is tracked and removed after the suite — writeTmp
// returns a FILE path, so callers had no dir handle to clean up themselves.
const SPEC_SECTION_TMP_DIRS = [];
function writeTmp(name, content) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'spec-section-'));
SPEC_SECTION_TMP_DIRS.push(dir);
const p = path.join(dir, name);
fs.writeFileSync(p, content);
return p;
}
const PROHIB_SUFFIX_SPEC = [
'## Prohibitions (must-NOT)',
'',
'**Coverage:** 2/2 applicable prohibitions resolved · 0 unresolved',
'',
'| Prohibition | Requirement | Status | Verification |',
'|-------------|-------------|--------|--------------|',
'| MUST NOT do X | R1 | resolved | test |',
'| MUST NOT do Y | R2 | resolved | judgment |',
'',
'## Ambiguity Report',
'| Dim | Score |',
'|-----|-------|',
'| Goal | 0.9 |',
].join('\n');
describe('spec-section: countSectionDataRows / specSectionStatus', () => {
test('suffix-tolerant header: `## Prohibitions (must-NOT)` is detected for key=prohibitions (the header-bug regression)', () => {
const status = ss.countSectionDataRows(PROHIB_SUFFIX_SPEC, 'prohibitions');
assert.equal(status.present, true);
assert.equal(status.dataRows, 2);
});
test('does not bleed into the next `##` section (Ambiguity Report rows are not counted as prohibitions)', () => {
const status = ss.countSectionDataRows(PROHIB_SUFFIX_SPEC, 'prohibitions');
assert.equal(status.dataRows, 2); // not 3 — the Ambiguity Report data row is outside the section
});
test('`## Edge Coverage` is detected and data rows counted', () => {
const spec = [
'## Edge Coverage',
'',
'| Category | Requirement | Status | Resolution |',
'|----------|-------------|--------|------------|',
'| empty | R1 | covered | x |',
'| ordering | R1 | dismissed | y |',
'| adjacency | R2 | backstop | z |',
].join('\n');
const status = ss.countSectionDataRows(spec, 'edges');
assert.equal(status.present, true);
assert.equal(status.dataRows, 3);
});
test('present-but-empty section (header + table header + separator, no data rows) is NOT supplied', () => {
const spec = [
'## Prohibitions (must-NOT)',
'',
'**Coverage:** 0/0 applicable',
'',
'| Prohibition | Requirement | Status | Verification |',
'|-------------|-------------|--------|--------------|',
].join('\n');
const p = writeTmp('empty.md', spec);
const status = ss.specSectionStatus(p, 'prohibitions');
assert.equal(status.present, true);
assert.equal(status.dataRows, 0);
assert.equal(status.supplied, false);
});
test('absent section -> not present, not supplied', () => {
const p = writeTmp('noproh.md', '## Edge Coverage\n\n| a | b |\n|---|---|\n| 1 | 2 |\n');
const status = ss.specSectionStatus(p, 'prohibitions');
assert.equal(status.present, false);
assert.equal(status.supplied, false);
});
test('missing file -> not present, not supplied (no throw)', () => {
const status = ss.specSectionStatus('/no/such/file-xyz.md', 'edges');
assert.deepEqual(status, { key: 'edges', present: false, dataRows: 0, supplied: false });
});
test('multi-table section counts every table’s data rows (the awk single-table miscount regression)', () => {
const spec = [
'## Edge Coverage',
'',
'| Category | Status |',
'|----------|--------|',
'| empty | covered |',
'',
'Some prose between two tables.',
'',
'| Category | Status |',
'|----------|--------|',
'| ordering | covered |',
'| adjacency | backstop |',
].join('\n');
const status = ss.countSectionDataRows(spec, 'edges');
assert.equal(status.dataRows, 3); // 1 + 2 across both tables
});
test('supplied = present AND dataRows > 0', () => {
const p = writeTmp('full.md', PROHIB_SUFFIX_SPEC);
assert.equal(ss.specSectionStatus(p, 'prohibitions').supplied, true);
});
});
describe('spec-section: CLI', () => {
test('prints JSON status and exits 0 for a valid key', () => {
const p = writeTmp('cli.md', PROHIB_SUFFIX_SPEC);
const r = toLegacyResult(runNode([BUILT_SCRIPT, p, 'prohibitions'], { timeoutMs: PROBE_TIMEOUT_MS }));
assert.equal(r.status, 0);
const out = JSON.parse(r.stdout);
assert.equal(out.supplied, true);
assert.equal(out.dataRows, 2);
});
test('exits 2 on a bad key', () => {
const p = writeTmp('cli2.md', PROHIB_SUFFIX_SPEC);
const r = toLegacyResult(runNode([BUILT_SCRIPT, p, 'bogus'], { timeoutMs: PROBE_TIMEOUT_MS }));
assert.equal(r.status, 2);
});
});