Files
msd-core/tests/enh-3271-sdk-adr-structure.test.cjs
Tom Boucher 23b52f1a14 fix(3597): windows test parity batch — CRLF parsers, ESM file URLs, posix-tmp, bash-only skips, retry bump
Six independent windows-only failure clusters identified from the
windows-22 CI log on 1be0e4e2:

- scripts/command-contract-helpers.cjs: parseFrontmatter split on `\n`
  → on Windows checkout (autocrlf=true) every line carries trailing \r,
  lines.indexOf('---', 1) returns -1, all fields read as missing. Drove
  the bulk of three "67 subtests failed" suite-level errors covering
  workstreams.md, workspace.md, verify-work.md, add-tests.md, etc.
  Fix: split(/\r?\n/).

- tests/enh-3271-sdk-adr-structure.test.cjs: same CRLF pattern — H2
  captures pulled '\r' into headings like "decision\r", breaking
  equality checks on "## Decision" / "## Consequences". Fix: same.

- tests/runtime-bridge-sync-smoke.test.cjs: await import(BRIDGE_PATH)
  passed a Windows absolute path to Node's ESM loader, which rejects
  with "Only URLs with a scheme in: file, data, and node are
  supported." Fix: wrap once at module scope with pathToFileURL.
  pathToFileURL is a no-op for POSIX absolute paths.

- tests/bug-2957-claude-global-postinstall-message.test.cjs: hardcoded
  '/tmp/gsd-test-settings.json' resolved to D:\tmp\... on Windows
  where the parent dir doesn't exist → ENOENT on fs.writeFileSync
  inside finishInstall. Fix: os.tmpdir() + pid suffix.

- tests/bug-2774-worktree-cleanup-workspace-safety.test.cjs: the
  "while/read loop" subtest and the "end-to-end against real git
  worktrees" describe both assert POSIX shell behavior (process
  substitution `< <(...)`, RUNNER~1 8.3-shortname mismatch). Skip on
  win32 with explicit reasons (satisfies the
  no-unconditional-win32-skip guard).

- tests/bug-2838-summary-rescue-gitignored-planning.test.cjs: entire
  describe extracts bash rescue blocks from workflow .md files and
  runs them; the shell contract itself is the test's point. Skip on
  win32 with reason.

- tests/helpers.cjs cleanup(): bumped rmSync retry budget from
  10×100ms to 20×250ms — 1s wasn't enough for Windows Defender's
  deferred handle release; bumping to 5s should absorb the residual
  EBUSY failures observed in bug-1736 / bug-2248 / bug-2698 after the
  first retry bump landed in 1be0e4e2.

Validated: holodeck (ubuntu docker) 11224/0 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 12:03:44 -04:00

222 lines
7.2 KiB
JavaScript

/**
* Structural tests for ADR 0005 (SDK architecture seam-map) and
* ADR 0006 (planning-path projection module), per issue #3271.
*
* Assertions parse the markdown by splitting on heading lines and inspect
* typed records. The docs/adr/README.md index must exist and reference
* both ADRs by filename.
*/
// allow-test-rule: heading-split structural parser for ADR markdown documents.
// Assertions target typed records (heading sets, status strings, filename refs),
// not raw .includes()/.match() on prose.
'use strict';
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ADR_DIR = path.join(__dirname, '..', 'docs', 'adr');
const README_PATH = path.join(ADR_DIR, 'README.md');
// --- Helpers -----------------------------------------------------------------
function parseAdr(filePath) {
let raw;
try {
raw = fs.readFileSync(filePath, 'utf8');
} catch (err) {
throw new Error(`Cannot read ADR file: ${filePath} — ${err.message}`);
}
// CRLF-tolerant split: Windows checkouts (autocrlf=true) include \r\n.
// Capture groups like /^##\s+(.+)$/ would otherwise pull a trailing \r
// into headings (e.g. "decision\r"), breaking heading equality checks.
const lines = raw.split(/\r?\n/);
let title = null;
const headings = [];
let status = null;
let date = null;
for (const line of lines) {
const h1 = line.match(/^#\s+(.+)$/);
if (h1 && title === null) { title = h1[1].trim(); continue; }
const h2 = line.match(/^##\s+(.+)$/);
if (h2) { headings.push(h2[1].trim().toLowerCase()); continue; }
const statusMatch = line.match(/\*\*Status:\*\*\s*(.+)/);
if (statusMatch && status === null) { status = statusMatch[1].trim(); continue; }
const dateMatch = line.match(/\*\*Date:\*\*\s*(.+)/);
if (dateMatch && date === null) { date = dateMatch[1].trim(); }
}
return { title, headings, status, date };
}
function parseReadmeIndex(filePath) {
let raw;
try {
raw = fs.readFileSync(filePath, 'utf8');
} catch (err) {
throw new Error(`Cannot read ADR README: ${filePath} — ${err.message}`);
}
// CRLF-tolerant split: Windows checkouts (autocrlf=true) include \r\n.
// Capture groups like /^##\s+(.+)$/ would otherwise pull a trailing \r
// into headings (e.g. "decision\r"), breaking heading equality checks.
const lines = raw.split(/\r?\n/);
const linkedFiles = [];
for (const line of lines) {
const linkRe = /\[.*?\]\(\.?\/?([^)]+\.md)\)/g;
let m;
while ((m = linkRe.exec(line)) !== null) {
linkedFiles.push(path.basename(m[1]));
}
}
return { linkedFiles };
}
// --- ADR 0005: SDK Architecture seam-map -------------------------------------
describe('ADR 0005 — SDK architecture seam-map', () => {
const adrPath = path.join(ADR_DIR, '0005-sdk-architecture-seam-map.md');
test('file exists', () => {
assert.ok(fs.existsSync(adrPath), `Expected ADR file to exist: ${adrPath}`);
});
test('has a title (H1 heading)', () => {
const { title } = parseAdr(adrPath);
assert.ok(title && title.length > 0, 'ADR must have a non-empty H1 title');
});
test('has Status metadata', () => {
const { status } = parseAdr(adrPath);
assert.ok(status && status.length > 0, 'ADR must have a **Status:** line');
});
test('has Date metadata', () => {
const { date } = parseAdr(adrPath);
assert.ok(date && date.length > 0, 'ADR must have a **Date:** line');
});
test('has Decision section', () => {
const { headings } = parseAdr(adrPath);
assert.ok(
headings.some(h => h === 'decision'),
`ADR must have a ## Decision section. Found headings: ${headings.join(', ')}`
);
});
test('has Consequences section', () => {
const { headings } = parseAdr(adrPath);
assert.ok(
headings.some(h => h === 'consequences'),
`ADR must have a ## Consequences section. Found headings: ${headings.join(', ')}`
);
});
test('cross-references at least two other ADR files', () => {
// allow-test-rule: reading markdown link targets and backtick code spans
// from ADR to build a typed set of referenced filenames.
const raw = fs.readFileSync(adrPath, 'utf8');
const refs = new Set();
const linkRe = /\((\d{4}-[^)]*\.md)\)/g;
let m;
while ((m = linkRe.exec(raw)) !== null) { refs.add(path.basename(m[1])); }
const codeRe = /`(\d{4}-[^`]*\.md)`/g;
while ((m = codeRe.exec(raw)) !== null) { refs.add(path.basename(m[1])); }
refs.delete('0005-sdk-architecture-seam-map.md');
assert.ok(
refs.size >= 2,
`Seam-map ADR must cross-reference at least 2 other ADR files. Found: ${[...refs].join(', ')}`
);
});
});
// --- ADR 0006: Planning Path Projection Module --------------------------------
describe('ADR 0006 — planning-path projection module', () => {
const adrPath = path.join(ADR_DIR, '0006-planning-path-projection-module.md');
test('file exists', () => {
assert.ok(fs.existsSync(adrPath), `Expected ADR file to exist: ${adrPath}`);
});
test('has a title (H1 heading)', () => {
const { title } = parseAdr(adrPath);
assert.ok(title && title.length > 0, 'ADR must have a non-empty H1 title');
});
test('has Status metadata', () => {
const { status } = parseAdr(adrPath);
assert.ok(status && status.length > 0, 'ADR must have a **Status:** line');
});
test('has Date metadata', () => {
const { date } = parseAdr(adrPath);
assert.ok(date && date.length > 0, 'ADR must have a **Date:** line');
});
test('has Decision section', () => {
const { headings } = parseAdr(adrPath);
assert.ok(
headings.some(h => h === 'decision'),
`ADR must have a ## Decision section. Found headings: ${headings.join(', ')}`
);
});
test('has Consequences section', () => {
const { headings } = parseAdr(adrPath);
assert.ok(
headings.some(h => h === 'consequences'),
`ADR must have a ## Consequences section. Found headings: ${headings.join(', ')}`
);
});
});
// --- docs/adr/README.md index ------------------------------------------------
describe('docs/adr/README.md index', () => {
test('README file exists', () => {
assert.ok(
fs.existsSync(README_PATH),
`Expected docs/adr/README.md to exist: ${README_PATH}`
);
});
test('links to ADR 0005', () => {
const { linkedFiles } = parseReadmeIndex(README_PATH);
assert.ok(
linkedFiles.some(f => f === '0005-sdk-architecture-seam-map.md'),
`README must link to 0005-sdk-architecture-seam-map.md. Found: ${linkedFiles.join(', ')}`
);
});
test('links to ADR 0006', () => {
const { linkedFiles } = parseReadmeIndex(README_PATH);
assert.ok(
linkedFiles.some(f => f === '0006-planning-path-projection-module.md'),
`README must link to 0006-planning-path-projection-module.md. Found: ${linkedFiles.join(', ')}`
);
});
test('links to all existing ADR files', () => {
const existingAdrs = fs.readdirSync(ADR_DIR)
.filter(f => /^\d{4}-.*\.md$/.test(f))
.sort();
const { linkedFiles } = parseReadmeIndex(README_PATH);
for (const adrFile of existingAdrs) {
assert.ok(
linkedFiles.includes(adrFile),
`README must link to every ADR. Missing: ${adrFile}`
);
}
});
});