fix(3597): make windows test cleanup retry on EBUSY + drop CRLF-broken local parseFrontmatter

Two windows-only failure clusters surfaced when the chunking fix in
52f23ac0 made Windows tests actually run:

1) Cleanup EBUSY race. On Windows, fs.rmSync without {maxRetries,retryDelay}
   races against AV scanners / file-indexers / just-exited child processes
   that still hold handles when teardown fires. Surfaces as
       EBUSY: resource busy or locked, rmdir 'C:\Users\...\AppData\Local\Temp\gsd-...'
   in bug-1736, bug-2248, bug-2698, bug-2838, and every test routing through
   the shared tests/helpers.cjs cleanup() (≈170 consumers — bug-2774 et al).
   Fix: add {maxRetries: 10, retryDelay: 100} to the shared helper plus the
   four inline cleanup sites. On POSIX the retry loop is dead code (first
   try succeeds), so no impact on ubuntu/macos.

2) bug-2839 had a local parseFrontmatter that anchored on /^---\n/ — on a
   Windows checkout with autocrlf=true the file content is CRLF, the regex
   never matches, the function returns null, and the before() hook asserts
   "agent must have YAML frontmatter" → entire suite cancels. The shared
   tests/helpers.cjs parseFrontmatter is already CRLF-aware (split /\r?\n/);
   switch to it.

Validated: plex2 (ubuntu docker) 11224/0 pass post-patch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-16 11:49:26 -04:00
parent f8eda5bf16
commit 1be0e4e2ee
6 changed files with 10 additions and 17 deletions

View File

@@ -46,7 +46,7 @@ describe('#1736: local Claude install populates .claude/commands/gsd/', () => {
}); });
afterEach(() => { afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true }); fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
}); });
test('local install creates .claude/commands/gsd/ directory', (t) => { test('local install creates .claude/commands/gsd/ directory', (t) => {

View File

@@ -47,7 +47,7 @@ describe('#2248: local Claude install does not clobber profile-level statusLine'
}); });
afterEach(() => { afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true }); fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
}); });
test('local install does not write statusLine to .claude/settings.json', (t) => { test('local install does not write statusLine to .claude/settings.json', (t) => {

View File

@@ -60,7 +60,7 @@ describe('#2698: CRLF stale gsd-update-check block is removed on Codex reinstall
}); });
afterEach(() => { afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true }); fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
}); });
// Helper: pre-populate .codex/config.toml with a GSD marker + stale hooks block // Helper: pre-populate .codex/config.toml with a GSD marker + stale hooks block

View File

@@ -163,7 +163,7 @@ ${rescueBlock}
} }
function cleanup(tmp) { function cleanup(tmp) {
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (_) {} 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/', () => {

View File

@@ -30,19 +30,9 @@ const assert = require('node:assert/strict');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const SENTINEL_NAME = '.review-fix-recovery-pending.json'; const { parseFrontmatter } = require('./helpers.cjs');
function parseFrontmatter(content) { const SENTINEL_NAME = '.review-fix-recovery-pending.json';
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match) return null;
const body = match[1];
const out = {};
for (const line of body.split('\n')) {
const m = line.match(/^([a-zA-Z_]+):\s*(.*)$/);
if (m) out[m[1]] = m[2].trim();
}
return out;
}
function extractStep(content, stepName) { function extractStep(content, stepName) {
const re = new RegExp(`<step\\s+name="${stepName}">([\\s\\S]*?)</step>`); const re = new RegExp(`<step\\s+name="${stepName}">([\\s\\S]*?)</step>`);

View File

@@ -104,7 +104,10 @@ function createTempGitProject(prefix = 'gsd-test-') {
} }
function cleanup(tmpDir) { function cleanup(tmpDir) {
fs.rmSync(tmpDir, { recursive: true, force: true }); // 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 });
} }
/** /**