Six independent windows-only failure clusters identified from the windows-22 CI log on1be0e4e2: - 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 in1be0e4e2. Validated: holodeck (ubuntu docker) 11224/0 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
222 lines
7.2 KiB
JavaScript
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}`
|
|
);
|
|
}
|
|
});
|
|
});
|