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>
65 lines
2.2 KiB
JavaScript
65 lines
2.2 KiB
JavaScript
'use strict';
|
|
/**
|
|
* command-contract-helpers.cjs (ADR-0002)
|
|
*
|
|
* Single source of truth for the commands/gsd/*.md contract constants and
|
|
* parsers shared by scripts/lint-command-contract.cjs and
|
|
* tests/command-contract.test.cjs.
|
|
*
|
|
* Keeping these in one place ensures the lint script and the test suite
|
|
* always agree on what constitutes a valid tool, a valid @-ref, and a valid
|
|
* frontmatter structure. A new canonical tool added here is automatically
|
|
* enforced by both consumers.
|
|
*/
|
|
|
|
const CANONICAL_TOOLS = new Set([
|
|
'Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep',
|
|
'Task', 'Agent', 'Skill', 'SlashCommand',
|
|
'AskUserQuestion', 'WebFetch', 'WebSearch', 'TodoWrite',
|
|
'mcp__context7__resolve-library-id',
|
|
'mcp__context7__query-docs',
|
|
'mcp__context7__*',
|
|
]);
|
|
|
|
function parseFrontmatter(content) {
|
|
// CRLF-tolerant split: Windows checkouts (autocrlf=true) leave a trailing
|
|
// \r on every line, making lines.indexOf('---', 1) return -1 (the value
|
|
// would be '---\r', not '---') → returns {} → every field appears missing.
|
|
const lines = content.split(/\r?\n/);
|
|
if (lines[0].trim() !== '---') return {};
|
|
const end = lines.indexOf('---', 1);
|
|
if (end === -1) return {};
|
|
const fm = {};
|
|
let key = null;
|
|
for (const line of lines.slice(1, end)) {
|
|
const kv = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)/);
|
|
if (kv) { key = kv[1]; fm[key] = kv[2].trim(); }
|
|
else if (key && line.match(/^\s+-\s+/)) {
|
|
const val = line.replace(/^\s+-\s+/, '').trim();
|
|
fm[key] = fm[key] ? fm[key] + '\n' + val : val;
|
|
}
|
|
}
|
|
return fm;
|
|
}
|
|
|
|
function executionContextRefs(content) {
|
|
const refs = [];
|
|
const re = /<execution_context(?:_extended)?>([\s\S]*?)<\/execution_context(?:_extended)?>/g;
|
|
let m;
|
|
while ((m = re.exec(content)) !== null) {
|
|
for (const rawLine of m[1].split('\n')) {
|
|
const line = rawLine.trim();
|
|
if (!line.startsWith('@')) continue;
|
|
const token = line.split(/\s+/)[0];
|
|
const trailingProse = line.length > token.length;
|
|
const normalized = token
|
|
.replace(/^@(?:~|\$HOME)\//, '')
|
|
.replace(/^(?:\.claude\/)?(?:get-shit-done\/)?/, '');
|
|
refs.push({ token, normalized, trailingProse });
|
|
}
|
|
}
|
|
return refs;
|
|
}
|
|
|
|
module.exports = { CANONICAL_TOOLS, parseFrontmatter, executionContextRefs };
|