Files
msd-core/tests/spec-section.test.cjs

147 lines
5.5 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 } = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const fs = require('node:fs');
const os = require('node:os');
const { spawnSync } = require('node:child_process');
const BUILT_SCRIPT = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'spec-section.cjs');
const ss = require(BUILT_SCRIPT);
function writeTmp(name, content) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'spec-section-'));
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 = spawnSync(process.execPath, [BUILT_SCRIPT, p, 'prohibitions'], { encoding: 'utf8' });
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 = spawnSync(process.execPath, [BUILT_SCRIPT, p, 'bogus'], { encoding: 'utf8' });
assert.equal(r.status, 2);
});
});