diff --git a/scripts/command-contract-helpers.cjs b/scripts/command-contract-helpers.cjs index f7080cac9..0bbf84b0a 100644 --- a/scripts/command-contract-helpers.cjs +++ b/scripts/command-contract-helpers.cjs @@ -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 {}; diff --git a/tests/bug-2774-worktree-cleanup-workspace-safety.test.cjs b/tests/bug-2774-worktree-cleanup-workspace-safety.test.cjs index 648936c30..ecb03dab2 100644 --- a/tests/bug-2774-worktree-cleanup-workspace-safety.test.cjs +++ b/tests/bug-2774-worktree-cleanup-workspace-safety.test.cjs @@ -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 < <() // 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; diff --git a/tests/bug-2838-summary-rescue-gitignored-planning.test.cjs b/tests/bug-2838-summary-rescue-gitignored-planning.test.cjs index 1a90ed3eb..733435814 100644 --- a/tests/bug-2838-summary-rescue-gitignored-planning.test.cjs +++ b/tests/bug-2838-summary-rescue-gitignored-planning.test.cjs @@ -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); diff --git a/tests/bug-2957-claude-global-postinstall-message.test.cjs b/tests/bug-2957-claude-global-postinstall-message.test.cjs index 15489b8cf..571f20a45 100644 --- a/tests/bug-2957-claude-global-postinstall-message.test.cjs +++ b/tests/bug-2957-claude-global-postinstall-message.test.cjs @@ -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, diff --git a/tests/enh-3271-sdk-adr-structure.test.cjs b/tests/enh-3271-sdk-adr-structure.test.cjs index ea069b2a4..dd3af2bed 100644 --- a/tests/enh-3271-sdk-adr-structure.test.cjs +++ b/tests/enh-3271-sdk-adr-structure.test.cjs @@ -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) { diff --git a/tests/helpers.cjs b/tests/helpers.cjs index 2e9843fb4..b2ee8ec8d 100644 --- a/tests/helpers.cjs +++ b/tests/helpers.cjs @@ -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 }); } /** diff --git a/tests/runtime-bridge-sync-smoke.test.cjs b/tests/runtime-bridge-sync-smoke.test.cjs index 63b77dbce..c6dc12532 100644 --- a/tests/runtime-bridge-sync-smoke.test.cjs +++ b/tests/runtime-bridge-sync-smoke.test.cjs @@ -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({