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 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>
This commit is contained in:
@@ -22,7 +22,10 @@ const CANONICAL_TOOLS = new Set([
|
||||
]);
|
||||
|
||||
function parseFrontmatter(content) {
|
||||
const lines = content.split('\n');
|
||||
// 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 {};
|
||||
|
||||
@@ -33,6 +33,8 @@ const os = require('os');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// The exact discovery pipeline from get-shit-done/workflows/quick.md and
|
||||
// get-shit-done/workflows/execute-phase.md (line: `WORKTREES=$(git worktree
|
||||
// list --porcelain | grep "^worktree " | grep "\.claude/worktrees/agent-" |
|
||||
@@ -176,7 +178,9 @@ describe('bug #2774 — worktree cleanup pipeline must not target the parent wor
|
||||
);
|
||||
});
|
||||
|
||||
test('while/read loop iterates each whitespace-bearing path exactly once', () => {
|
||||
test('while/read loop iterates each whitespace-bearing path exactly once',
|
||||
{ skip: isWindows ? 'POSIX bash process-substitution `< <(...)` under test; not portable to cmd.exe / git-bash variance' : false },
|
||||
() => {
|
||||
// Verify the actual consumer pattern from quick.md / execute-phase.md:
|
||||
// while IFS= read -r WT; do ...; done < <(<pipeline>)
|
||||
// Counts the lines yielded to the loop body. With the previous
|
||||
@@ -222,7 +226,9 @@ done < <(${DISCOVERY_PIPELINE})
|
||||
});
|
||||
});
|
||||
|
||||
describe('end-to-end against real git worktrees', () => {
|
||||
describe('end-to-end against real git worktrees',
|
||||
{ skip: isWindows ? 'POSIX shell discovery pipeline under test + Windows 8.3 short-name (RUNNER~1) vs long-name path mismatch in temp dirs' : false },
|
||||
() => {
|
||||
let upstream;
|
||||
let workspace;
|
||||
let agentWorktree;
|
||||
|
||||
@@ -50,6 +50,8 @@ function parseRescueFooter(content) {
|
||||
}
|
||||
|
||||
const { describe, test, before, after } = require('node:test');
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
@@ -166,7 +168,9 @@ function cleanup(tmp) {
|
||||
try { fs.rmSync(tmp, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }); } catch (_) {}
|
||||
}
|
||||
|
||||
describe('bug-2838: SUMMARY rescue handles gitignored .planning/', () => {
|
||||
describe('bug-2838: SUMMARY rescue handles gitignored .planning/',
|
||||
{ skip: isWindows ? 'extracts and executes bash rescue blocks from quick.md/execute-phase.md (find | while read, `done < <(find ...)`); the POSIX shell contract itself is what is under test' : false },
|
||||
() => {
|
||||
test('execute-phase.md rescue block recovers SUMMARY when .planning/ is gitignored', () => {
|
||||
const block = extractRescueBlock(EXECUTE_PHASE_PATH);
|
||||
const { tmp, summaryFinalPath, rescueOut } = runRescueScenario(block);
|
||||
|
||||
@@ -15,8 +15,10 @@ process.env.GSD_TEST_MODE = '1';
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const SETTINGS_PATH = path.join(os.tmpdir(), `gsd-test-settings-${process.pid}.json`);
|
||||
const installModule = require(path.join(ROOT, 'bin', 'install.js'));
|
||||
|
||||
function captureFinishInstallOutput(runtime, isGlobal) {
|
||||
@@ -25,7 +27,7 @@ function captureFinishInstallOutput(runtime, isGlobal) {
|
||||
console.log = (...args) => { lines.push(args.join(' ')); };
|
||||
try {
|
||||
installModule.finishInstall(
|
||||
'/tmp/gsd-test-settings.json',
|
||||
SETTINGS_PATH,
|
||||
{},
|
||||
null,
|
||||
false,
|
||||
|
||||
@@ -31,7 +31,10 @@ function parseAdr(filePath) {
|
||||
throw new Error(`Cannot read ADR file: ${filePath} — ${err.message}`);
|
||||
}
|
||||
|
||||
const lines = raw.split('\n');
|
||||
// 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;
|
||||
@@ -59,7 +62,10 @@ function parseReadmeIndex(filePath) {
|
||||
throw new Error(`Cannot read ADR README: ${filePath} — ${err.message}`);
|
||||
}
|
||||
|
||||
const lines = raw.split('\n');
|
||||
// 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) {
|
||||
|
||||
@@ -107,7 +107,9 @@ function cleanup(tmpDir) {
|
||||
// maxRetries/retryDelay absorbs transient Windows EBUSY where AV scanners,
|
||||
// file-indexers, or just-exited child processes still hold handles when
|
||||
// teardown runs. On POSIX the retry loop is a no-op (rmSync succeeds first try).
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
||||
// Budget: 20 × 250ms = 5s total — Windows Defender's deferred scan can hold
|
||||
// newly-written files for several seconds on cold runners.
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 20, retryDelay: 250 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,20 +13,24 @@
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const { pathToFileURL } = require('node:url');
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, '..');
|
||||
const BRIDGE_PATH = path.join(REPO_ROOT, 'sdk', 'dist', 'runtime-bridge-sync', 'index.js');
|
||||
// Node's ESM loader rejects Windows absolute paths with `import()` — must be a
|
||||
// file:// URL. pathToFileURL is a no-op for POSIX paths (produces file:///abs/...).
|
||||
const BRIDGE_URL = pathToFileURL(BRIDGE_PATH).href;
|
||||
|
||||
describe('runtime-bridge-sync CJS smoke test', () => {
|
||||
test('executeForCjs is exported and is a function', async () => {
|
||||
// Use dynamic import because Node 24 supports require() of ESM but
|
||||
// the module is ESM (NodeNext output). Dynamic import works in all contexts.
|
||||
const mod = await import(BRIDGE_PATH);
|
||||
const mod = await import(BRIDGE_URL);
|
||||
assert.strictEqual(typeof mod.executeForCjs, 'function', 'executeForCjs must be a function');
|
||||
});
|
||||
|
||||
test('executeForCjs returns ok:true for generate-slug (success path)', async () => {
|
||||
const { executeForCjs } = await import(BRIDGE_PATH);
|
||||
const { executeForCjs } = await import(BRIDGE_URL);
|
||||
|
||||
const result = executeForCjs({
|
||||
registryCommand: 'generate-slug',
|
||||
@@ -52,7 +56,7 @@ describe('runtime-bridge-sync CJS smoke test', () => {
|
||||
});
|
||||
|
||||
test('executeForCjs returns ok:false for unknown command', async () => {
|
||||
const { executeForCjs } = await import(BRIDGE_PATH);
|
||||
const { executeForCjs } = await import(BRIDGE_URL);
|
||||
|
||||
const result = executeForCjs({
|
||||
registryCommand: '__smoke_test_unknown_command__',
|
||||
@@ -73,7 +77,7 @@ describe('runtime-bridge-sync CJS smoke test', () => {
|
||||
});
|
||||
|
||||
test('executeForCjs result shape matches RuntimeBridgeSyncResult discriminated union', async () => {
|
||||
const { executeForCjs } = await import(BRIDGE_PATH);
|
||||
const { executeForCjs } = await import(BRIDGE_URL);
|
||||
|
||||
// Success shape
|
||||
const success = executeForCjs({
|
||||
|
||||
Reference in New Issue
Block a user