From 844cc158f314ec70b586dbd4dadce457009fdb8c Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Sat, 25 Jul 2026 22:07:05 +0100 Subject: [PATCH] =?UTF-8?q?test:=20retry=20recursive=20teardown=20removals?= =?UTF-8?q?=20=E2=80=94=20kill=20a=20class=20of=20phantom=20failures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A full-gate run went red on ENOTEMPTY inside an afterEach rmSync, in a suite unrelated to what was being changed, while four background agents churned temp directories. It passed 3/3 in isolation: the engine spawns git subprocesses, git drops lock and temp files a moment after the command returns, and a recursive remove that has just emptied a directory then fails to remove it. Every recursive teardown removal in tests/scripts now passes maxRetries/retryDelay, as the prose-test world builder already did. 64 call sites across 25 suites; no behavioural change, and a false red on the gate costs more than the churn. Co-Authored-By: Claude Opus 5 --- tests/scripts/kb-smoke.cjs | 2 +- tests/scripts/test-conventions-lint.cjs | 2 +- tests/scripts/test-engine-agent-state.cjs | 2 +- tests/scripts/test-engine-boot.cjs | 8 ++++---- tests/scripts/test-engine-discovery-session.cjs | 8 ++++---- tests/scripts/test-engine-manifest-fields.cjs | 16 ++++++++-------- tests/scripts/test-engine-manifest-io.cjs | 10 +++++----- tests/scripts/test-engine-render-surfaces.cjs | 2 +- tests/scripts/test-engine-transactions.cjs | 4 ++-- tests/scripts/test-engine-workunit-absorb.cjs | 8 ++++---- tests/scripts/test-engine-workunit-create.cjs | 16 ++++++++-------- tests/scripts/test-engine-workunit-pivot.cjs | 4 ++-- tests/scripts/test-engine-workunit-promote.cjs | 16 ++++++++-------- tests/scripts/test-ensure-discovery-item.cjs | 2 +- tests/scripts/test-knowledge-config.cjs | 2 +- tests/scripts/test-knowledge-discovery.cjs | 2 +- tests/scripts/test-knowledge-integration.cjs | 2 +- .../scripts/test-knowledge-openai-compatible.cjs | 2 +- tests/scripts/test-knowledge-setup-forms.cjs | 6 +++--- tests/scripts/test-knowledge-store.cjs | 4 ++-- tests/scripts/test-migration-047.cjs | 2 +- tests/scripts/test-migration-048.cjs | 2 +- tests/scripts/test-migration-049.cjs | 2 +- tests/scripts/test-migration-050.cjs | 2 +- tests/scripts/test-migration-052.cjs | 2 +- 25 files changed, 64 insertions(+), 64 deletions(-) diff --git a/tests/scripts/kb-smoke.cjs b/tests/scripts/kb-smoke.cjs index bb27c66b5..602520005 100644 --- a/tests/scripts/kb-smoke.cjs +++ b/tests/scripts/kb-smoke.cjs @@ -137,7 +137,7 @@ async function main() { check('loaded store serves hybrid', afterLoadHyb.length > 0); console.log('\n' + (failures === 0 ? 'ALL PASSED' : failures + ' FAILURES')); - fs.rmSync(dir, { recursive: true, force: true }); + fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); process.exit(failures === 0 ? 0 : 1); } diff --git a/tests/scripts/test-conventions-lint.cjs b/tests/scripts/test-conventions-lint.cjs index 070c4d229..a3a561123 100644 --- a/tests/scripts/test-conventions-lint.cjs +++ b/tests/scripts/test-conventions-lint.cjs @@ -756,7 +756,7 @@ function withTemp(fn) { try { return fn(dir); } finally { - fs.rmSync(dir, { recursive: true, force: true }); + fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); } } diff --git a/tests/scripts/test-engine-agent-state.cjs b/tests/scripts/test-engine-agent-state.cjs index 20a16567a..3620d10c9 100644 --- a/tests/scripts/test-engine-agent-state.cjs +++ b/tests/scripts/test-engine-agent-state.cjs @@ -228,7 +228,7 @@ describe('engine agent — lifecycle store', () => { it('deleting the topic cache dir is a complete cleanse — state is colocated', () => { runJson(dir, ['dispatch', 'pay', 'research', 'alpha', '--kind', 'review']); runJson(dir, ['dispatch', 'pay', 'research', 'beta', '--kind', 'review']); - fs.rmSync(path.join(dir, '.workflows', '.cache', 'pay', 'research', 'alpha'), { recursive: true, force: true }); + fs.rmSync(path.join(dir, '.workflows', '.cache', 'pay', 'research', 'alpha'), { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); assert.deepStrictEqual(runJson(dir, ['scan', 'pay', 'research', 'alpha']).in_flight, [], 'the restart rm -rf removes rows with the content'); assert.deepStrictEqual(runJson(dir, ['scan', 'pay', 'research', 'beta']).in_flight.map((r) => r.id), ['review-001'], diff --git a/tests/scripts/test-engine-boot.cjs b/tests/scripts/test-engine-boot.cjs index d360fb15b..0121194d5 100644 --- a/tests/scripts/test-engine-boot.cjs +++ b/tests/scripts/test-engine-boot.cjs @@ -153,7 +153,7 @@ function knowledgeCalls(project) { describe('engine boot', () => { let fix; beforeEach(() => { fix = setupSkillsFixture(); }); - afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('happy path: no pending migrations, knowledge ready — compact runs', () => { const res = runEngine(fix.engine, fix.project, ['boot'], { STUB_CHECK: 'ready' }); @@ -297,7 +297,7 @@ describe('engine boot system-config detection', () => { home = path.join(fix.root, 'home'); fs.mkdirSync(home, { recursive: true }); }); - afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); function writeSystemConfig(content) { writeFile(home, '.config/workflows/config.json', content); @@ -368,7 +368,7 @@ describe('engine boot (real scripts)', () => { root = fs.mkdtempSync(path.join(os.tmpdir(), 'engine-boot-real-')); project = setupProject(root); }); - afterEach(() => { fs.rmSync(root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('runs the real migrate.cjs and knowledge CLI against an isolated project', async () => { const first = runEngine(REAL_ENGINE, project, ['boot']); @@ -418,7 +418,7 @@ describe('engine commit --workflows', () => { root = fs.mkdtempSync(path.join(os.tmpdir(), 'engine-boot-commit-')); project = setupProject(root); }); - afterEach(() => { fs.rmSync(root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('stages the whole .workflows tree — many work units plus .state — and commits', () => { writeFile(project, '.workflows/payments/manifest.json', '{"name":"payments","v":2}\n'); diff --git a/tests/scripts/test-engine-discovery-session.cjs b/tests/scripts/test-engine-discovery-session.cjs index 1e923b48b..6758272b4 100644 --- a/tests/scripts/test-engine-discovery-session.cjs +++ b/tests/scripts/test-engine-discovery-session.cjs @@ -153,7 +153,7 @@ const CLOSE = ['discovery-session', 'close', 'payments', '-m', 'discovery(paymen describe('engine discovery-session close — happy path', () => { let fix; - afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('clears the marker, indexes the closed log, and commits the session dirt with the caller message', () => { fix = setupFixture(); @@ -225,7 +225,7 @@ describe('engine discovery-session close — happy path', () => { describe('engine discovery-session close — guards refuse loudly, everything pristine', () => { let fix; - afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); /** Assert the refusal leaves every `.workflows/` byte identical, no commit, no KB call. */ function refusedPristine(args, pattern) { @@ -279,7 +279,7 @@ function closedEpicManifest() { describe('engine discovery-session open — happy path', () => { let fix; - afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('allocates past the highest on-disk log, moves the draft into place, sets the marker, and does NOT commit', () => { fix = setupFixture({ epic: closedEpicManifest() }); @@ -356,7 +356,7 @@ describe('engine discovery-session open — happy path', () => { describe('engine discovery-session open — guards refuse loudly, everything pristine', () => { let fix; - afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); /** Assert the refusal leaves every `.workflows/` byte identical (the draft included), no commit, no KB call. */ function refusedPristine(args, pattern) { diff --git a/tests/scripts/test-engine-manifest-fields.cjs b/tests/scripts/test-engine-manifest-fields.cjs index 61a546c5f..0142024d2 100644 --- a/tests/scripts/test-engine-manifest-fields.cjs +++ b/tests/scripts/test-engine-manifest-fields.cjs @@ -76,7 +76,7 @@ function readWorkUnit(dir, name) { describe('engine manifest — reads keep the CLI stdout contract', () => { let dir; beforeEach(() => { dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'engine-fields-'))); }); - afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('get: scalar raw, subtree pretty JSON, missing path empty exit 0', () => { writeWorkUnit(dir, 'auth', 'feature'); @@ -164,7 +164,7 @@ describe('engine manifest — reads keep the CLI stdout contract', () => { describe('engine manifest — mutations answer with the engine JSON contract', () => { let dir; beforeEach(() => { dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'engine-fields-'))); }); - afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('set: one-line JSON listing what was written', () => { writeWorkUnit(dir, 'auth', 'feature'); @@ -323,7 +323,7 @@ describe('engine manifest — mutations answer with the engine JSON contract', ( describe('engine manifest — the batched set holds the work-unit lock', () => { let dir; beforeEach(() => { dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'engine-fields-'))); }); - afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); @@ -367,7 +367,7 @@ describe('engine manifest apply — the batch form of set/delete (D7)', () => { }, }); }); - afterEach(() => fs.rmSync(dir, { recursive: true, force: true })); + afterEach(() => fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })); function payload(ops) { const p = path.join(dir, 'ops.json'); @@ -443,7 +443,7 @@ describe('engine manifest set — two grammars, never mixed', () => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'manifest-grammar-')); writeWorkUnit(dir, 'auth', 'feature'); }); - afterEach(() => fs.rmSync(dir, { recursive: true, force: true })); + afterEach(() => fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })); it('uniform batch: every pair assigned, first pair included', () => { const res = runJson(dir, ['set', 'auth', 'status=completed', 'note=hello']); @@ -481,7 +481,7 @@ describe('staging, candidate, and tracking state is vocabulary-guarded', () => { discovery: { items: {} }, } }); }); - afterEach(() => fs.rmSync(dir, { recursive: true, force: true })); + afterEach(() => fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })); it('staging task decisions take only the gate vocabulary', () => { runJson(dir, ['set', 'pay.review.pay', 'staging.c1.gate_mode=gated', 'staging.c1.tasks.1=pending', 'staging.c1.tasks.2=pending']); @@ -536,7 +536,7 @@ describe('work-unit-level fields never shadow the phases tree', () => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shadow-guard-')); writeWorkUnit(dir, 'pay', 'feature'); }); - afterEach(() => fs.rmSync(dir, { recursive: true, force: true })); + afterEach(() => fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })); it('set refuses a phase-name-headed field in both grammars', () => { const err = runFails(dir, ['set', 'pay', 'specification.foo', 'bar']); @@ -582,7 +582,7 @@ describe('storage_paths is guarded at write time — set and apply', () => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'storage-paths-')); writeWorkUnit(dir, 'pay', 'feature', { phases: { planning: { items: { pay: { status: 'in-progress' } } } } }); }); - afterEach(() => fs.rmSync(dir, { recursive: true, force: true })); + afterEach(() => fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })); it('set refuses traversal, absolute, dot, and non-array values; a legal array lands', () => { for (const bad of ['\'["../evil"]\'', '\'["/abs"]\'', '\'["."]\'', '\'[""]\'', '"nope"']) { diff --git a/tests/scripts/test-engine-manifest-io.cjs b/tests/scripts/test-engine-manifest-io.cjs index f0f8a06e8..79a2cda32 100644 --- a/tests/scripts/test-engine-manifest-io.cjs +++ b/tests/scripts/test-engine-manifest-io.cjs @@ -85,7 +85,7 @@ function sleep(ms) { describe('manifest-io — shared lock constants and IO contract', () => { let dir; beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'manifest-io-')); }); - afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('carries the shared lock discipline: 30s stale, 50ms retry, 10s timeout, 60s tmp orphan', () => { assert.strictEqual(io.LOCK_STALE_MS, 30000); @@ -205,7 +205,7 @@ describe('stale-lock break is atomic — one winner, mutual exclusion holds', () const IO_PATH = path.join(__dirname, '../../skills/workflow-engine/scripts/kernel/manifest-io.cjs'); let dir; beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'lock-break-')); }); - afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); /** Spawn a node child running `script` with argv, resolving with its exit code. */ function child(script, args) { @@ -289,7 +289,7 @@ describe('structurally corrupt manifests refuse writes', () => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'engine-root-')); fs.mkdirSync(path.join(dir, '.workflows/unit'), { recursive: true }); }); - afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); /** Run the engine expecting `{ok:false}` exit 1; returns the parsed stderr JSON. */ function engineFail(args) { @@ -376,7 +376,7 @@ describe('structurally corrupt manifests refuse writes', () => { describe('engine writes take the work-unit lock', () => { let dir; beforeEach(() => { dir = setupGitFixture(); writeEpicFixture(dir); }); - afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('a stale .lock is broken per the constants: the write proceeds, the lock is cleaned up', () => { const lock = path.join(dir, '.workflows/payments/.lock'); @@ -436,7 +436,7 @@ describe('engine project-manifest writes take the project lock', () => { git(dir, ['add', '-A']); git(dir, ['commit', '-q', '-m', 'init']); }); - afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('a stale .project-lock is broken: create registers and cleans up', () => { const lock = path.join(dir, '.workflows/.project-lock'); diff --git a/tests/scripts/test-engine-render-surfaces.cjs b/tests/scripts/test-engine-render-surfaces.cjs index f44203166..cee4f1d02 100644 --- a/tests/scripts/test-engine-render-surfaces.cjs +++ b/tests/scripts/test-engine-render-surfaces.cjs @@ -14,7 +14,7 @@ function setup() { return fs.mkdtempSync(path.join(os.tmpdir(), 'render-surfaces-')); } function teardown(dir) { - fs.rmSync(dir, { recursive: true, force: true }); + fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); } function writeManifest(dir, name, data) { const mdir = path.join(dir, '.workflows', name); diff --git a/tests/scripts/test-engine-transactions.cjs b/tests/scripts/test-engine-transactions.cjs index 561263db6..696c22ea9 100644 --- a/tests/scripts/test-engine-transactions.cjs +++ b/tests/scripts/test-engine-transactions.cjs @@ -1167,7 +1167,7 @@ describe('engine commit', () => { fs.writeFileSync(path.join(dir, '.workflows/payments/manifest.json'), JSON.stringify(m, null, 2) + '\n'); writeFile(dir, '.tick/tasks-auth.jsonl', '{"id":"auth-1-1"}\n'); commitAll(dir, 'seed tick storage'); - fs.rmSync(path.join(dir, '.tick'), { recursive: true, force: true }); + fs.rmSync(path.join(dir, '.tick'), { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); const res = engine(dir, ['commit', 'payments', '-m', 'planning(payments): restart plan', '--plan', 'auth']); assert.strictEqual(res.committed, shortHead(dir)); @@ -1231,7 +1231,7 @@ describe('knowledge store rides along on every engine commit', () => { }); it('exists-guarded: no .knowledge directory, no pathspec, no git error', () => { - fs.rmSync(path.join(dir, '.workflows/.knowledge'), { recursive: true, force: true }); + fs.rmSync(path.join(dir, '.workflows/.knowledge'), { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); git(dir, ['add', '-A']); git(dir, ['commit', '-q', '-m', 'drop store']); writeFile(dir, '.workflows/payments/discussion/auth.md', '# Auth\n'); diff --git a/tests/scripts/test-engine-workunit-absorb.cjs b/tests/scripts/test-engine-workunit-absorb.cjs index 7017b01c2..195eaeb3d 100644 --- a/tests/scripts/test-engine-workunit-absorb.cjs +++ b/tests/scripts/test-engine-workunit-absorb.cjs @@ -175,7 +175,7 @@ const ABSORB = ['workunit', 'absorb', 'auth-flow', '--into', 'payments', '--topi describe('engine workunit absorb — happy path', () => { let fix; - afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('moves everything, mirrors statuses, deletes the feature, commits all three pathspecs once', () => { fix = setupFixture(); @@ -305,7 +305,7 @@ describe('engine workunit absorb — happy path', () => { describe('engine workunit absorb — guards refuse loudly, both work units pristine', () => { let fix; - afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); /** Assert the refusal leaves every `.workflows/` byte identical, no commit. */ function refusedPristine(args, pattern) { @@ -335,7 +335,7 @@ describe('engine workunit absorb — guards refuse loudly, both work units prist const feature = featureManifest({ phases: { research: { items: { exploration: { status: 'completed' } } } } }); fix = setupFixture({ feature }); refusedPristine(ABSORB, /has no discussion — absorb moves the discussion in/); - fs.rmSync(fix.root, { recursive: true, force: true }); + fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); fix = setupFixture(); fs.rmSync(path.join(fix.project, '.workflows/auth-flow/discussion/auth-flow.md')); @@ -349,7 +349,7 @@ describe('engine workunit absorb — guards refuse loudly, both work units prist withSpec.phases.specification = { items: { 'auth-flow': { status: 'in-progress' } } }; fix = setupFixture({ feature: withSpec }); refusedPristine(ABSORB, /has specification work — absorb is only for features before specification/); - fs.rmSync(fix.root, { recursive: true, force: true }); + fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); const withPlan = featureManifest(); withPlan.phases.planning = { items: {} }; diff --git a/tests/scripts/test-engine-workunit-create.cjs b/tests/scripts/test-engine-workunit-create.cjs index 21affc7a2..82f7bbc04 100644 --- a/tests/scripts/test-engine-workunit-create.cjs +++ b/tests/scripts/test-engine-workunit-create.cjs @@ -141,7 +141,7 @@ const ISO_SECONDS = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/; describe('engine workunit create — happy path', () => { let fix; beforeEach(() => { fix = setupFixture(); stageLog(fix); }); - afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('epic with imports and seeds: one transaction — manifest, files, log, marker, commit', () => { writeFile(fix.project, 'notes/My Design DOC.txt', 'design notes\n'); @@ -251,7 +251,7 @@ describe('engine workunit create — happy path', () => { describe('engine workunit create — the canonical on-disk documents', () => { let fix; beforeEach(() => { fix = setupFixture(); stageLog(fix); }); - afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('writes byte-exact manifest and project-manifest documents', () => { engine(fix, createArgs('payments', 'feature')); @@ -276,7 +276,7 @@ describe('engine workunit create — the canonical on-disk documents', () => { describe('engine workunit create — existing manifest reuse', () => { let fix; beforeEach(() => { fix = setupFixture(); stageLog(fix); }); - afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('reuses an existing manifest as-is: created false, fields preserved, landing still runs', () => { writeFile(fix.project, '.workflows/payments/manifest.json', JSON.stringify({ @@ -313,7 +313,7 @@ describe('engine workunit create — existing manifest reuse', () => { describe('engine workunit create — import filename normalisation', () => { let fix; beforeEach(() => { fix = setupFixture(); stageLog(fix); }); - afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); /** @param {string[]} sources */ function landImports(sources) { @@ -388,7 +388,7 @@ describe('engine workunit create — import filename normalisation', () => { describe('engine workunit create — seeds', () => { let fix; beforeEach(() => { fix = setupFixture(); stageLog(fix); }); - afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('derives the source tag from each inbox folder', () => { const res = engine(fix, createArgs('payments', 'epic', [ @@ -429,7 +429,7 @@ describe('engine workunit create — seeds', () => { describe('engine workunit create — missing imports fail fast', () => { let fix; beforeEach(() => { fix = setupFixture(); stageLog(fix); }); - afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('reports every missing path in missing_imports — nothing touched', () => { writeFile(fix.project, 'notes/good.md', 'fine\n'); @@ -453,7 +453,7 @@ describe('engine workunit create — missing imports fail fast', () => { describe('engine workunit create — knowledge base is warn-don\'t-block', () => { let fix; beforeEach(() => { fix = setupFixture(); stageLog(fix); }); - afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('per-file indexing failures land in warnings; the transaction still commits', () => { writeFile(fix.project, 'notes/design.md', 'notes\n'); @@ -475,7 +475,7 @@ describe('engine workunit create — knowledge base is warn-don\'t-block', () => describe('engine workunit create — validation', () => { let fix; beforeEach(() => { fix = setupFixture(); stageLog(fix); }); - afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('rejects an illegal work type via the shared schema vocabulary', () => { const err = engineFails(fix, createArgs('payments', 'saga')); diff --git a/tests/scripts/test-engine-workunit-pivot.cjs b/tests/scripts/test-engine-workunit-pivot.cjs index f3ce1f2f9..907c840cd 100644 --- a/tests/scripts/test-engine-workunit-pivot.cjs +++ b/tests/scripts/test-engine-workunit-pivot.cjs @@ -146,7 +146,7 @@ function snapshot(fix) { describe('engine workunit pivot — happy path', () => { let fix; - afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('flips work_type in BOTH manifests, registers the backfill map item, re-indexes, commits', () => { fix = setupFixture(); @@ -233,7 +233,7 @@ describe('engine workunit pivot — happy path', () => { describe('engine workunit pivot — guards refuse loudly, nothing touched', () => { let fix; - afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('refuses a non-feature work unit', () => { fix = setupFixture(featureManifest({ work_type: 'epic' })); diff --git a/tests/scripts/test-engine-workunit-promote.cjs b/tests/scripts/test-engine-workunit-promote.cjs index 0d56fbe06..48951f64e 100644 --- a/tests/scripts/test-engine-workunit-promote.cjs +++ b/tests/scripts/test-engine-workunit-promote.cjs @@ -177,7 +177,7 @@ const PROMOTE = ['workunit', 'promote', 'payments', 'caching-strategy', '--to', describe('engine workunit promote — happy path', () => { let fix; - afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('moves the spec and source discussions, completes the cc unit, marks promoted, commits all three pathspecs once', () => { fix = setupFixture(); @@ -339,7 +339,7 @@ describe('engine workunit promote — happy path', () => { describe('engine workunit promote — guards refuse loudly, everything pristine', () => { let fix; - afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true }); }); + afterEach(() => { fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); /** Assert the refusal leaves every `.workflows/` byte identical, no commit, no cc unit. */ function refusedPristine(args, pattern) { @@ -354,11 +354,11 @@ describe('engine workunit promote — guards refuse loudly, everything pristine' it('refuses unknown work units, non-epics, and closed epics', () => { fix = setupFixture(); refusedPristine(['workunit', 'promote', 'ghost', 'caching-strategy', '--to', 'caching', '--description', 'd'], /manifest not found/); - fs.rmSync(fix.root, { recursive: true, force: true }); + fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); fix = setupFixture({ epic: epicManifest({ work_type: 'feature' }) }); refusedPristine(PROMOTE, /not an epic \(work_type: feature\) — only epic specifications promote/); - fs.rmSync(fix.root, { recursive: true, force: true }); + fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); fix = setupFixture({ epic: epicManifest({ status: 'completed' }) }); refusedPristine(PROMOTE, /epic "payments" is not in-progress \(status: completed\)/); @@ -367,13 +367,13 @@ describe('engine workunit promote — guards refuse loudly, everything pristine' it('refuses a missing, incomplete, or already-promoted specification item', () => { fix = setupFixture(); refusedPristine(['workunit', 'promote', 'payments', 'ghost-topic', '--to', 'caching', '--description', 'd'], /no specification item "ghost-topic"/); - fs.rmSync(fix.root, { recursive: true, force: true }); + fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); const inProgress = epicManifest(); inProgress.phases.specification.items['caching-strategy'].status = 'in-progress'; fix = setupFixture({ epic: inProgress }); refusedPristine(PROMOTE, /not completed \(status: in-progress\) — only a completed specification promotes/); - fs.rmSync(fix.root, { recursive: true, force: true }); + fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); const promoted = epicManifest(); promoted.phases.specification.items['caching-strategy'].status = 'promoted'; @@ -403,7 +403,7 @@ describe('engine workunit promote — guards refuse loudly, everything pristine' git(fix.project, ['add', '-A']); git(fix.project, ['commit', '-q', '-m', 'existing unit']); refusedPristine(PROMOTE, /work unit "caching" already exists \(\.workflows\/caching\)/); - fs.rmSync(fix.root, { recursive: true, force: true }); + fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); fix = setupFixture(); writeFile(fix.project, '.workflows/manifest.json', JSON.stringify({ @@ -450,7 +450,7 @@ describe('engine workunit promote — source shape guard', () => { assert.match(err.error, /array-shaped sources/); assert.ok(!fs.existsSync(path.join(fix.project, '.workflows/caching-policy')), 'nothing created on refusal'); } finally { - fs.rmSync(fix.root, { recursive: true, force: true }); + fs.rmSync(fix.root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); } }); }); diff --git a/tests/scripts/test-ensure-discovery-item.cjs b/tests/scripts/test-ensure-discovery-item.cjs index e3062a192..13168328d 100644 --- a/tests/scripts/test-ensure-discovery-item.cjs +++ b/tests/scripts/test-ensure-discovery-item.cjs @@ -35,7 +35,7 @@ function setup() { } function cleanup() { - if (dir) fs.rmSync(dir, { recursive: true, force: true }); + if (dir) fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); dir = null; } diff --git a/tests/scripts/test-knowledge-config.cjs b/tests/scripts/test-knowledge-config.cjs index 49a8fc670..56de15c60 100644 --- a/tests/scripts/test-knowledge-config.cjs +++ b/tests/scripts/test-knowledge-config.cjs @@ -34,7 +34,7 @@ function setup() { } function teardown() { - fs.rmSync(tmpDir, { recursive: true, force: true }); + fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); } function writeJSON(filePath, data) { diff --git a/tests/scripts/test-knowledge-discovery.cjs b/tests/scripts/test-knowledge-discovery.cjs index 217cbbb70..9d8ffc473 100644 --- a/tests/scripts/test-knowledge-discovery.cjs +++ b/tests/scripts/test-knowledge-discovery.cjs @@ -178,7 +178,7 @@ describe('knowledge bulk discovery — artifact-set equivalence', () => { after(() => { process.chdir(cwd0); - fs.rmSync(root, { recursive: true, force: true }); + fs.rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('discovers exactly the expected identity + target set', () => { diff --git a/tests/scripts/test-knowledge-integration.cjs b/tests/scripts/test-knowledge-integration.cjs index b10af84b4..32a54ce9f 100644 --- a/tests/scripts/test-knowledge-integration.cjs +++ b/tests/scripts/test-knowledge-integration.cjs @@ -147,7 +147,7 @@ describe('knowledge store — end-to-end integration (via built bundle)', () => }); after(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('exposes the expected surface on the built bundle', () => { diff --git a/tests/scripts/test-knowledge-openai-compatible.cjs b/tests/scripts/test-knowledge-openai-compatible.cjs index f8eb534d3..3a1781845 100644 --- a/tests/scripts/test-knowledge-openai-compatible.cjs +++ b/tests/scripts/test-knowledge-openai-compatible.cjs @@ -286,7 +286,7 @@ describe('config merge carries base_url', () => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-compat-')); }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('merges base_url from the system config through to the merged config', () => { diff --git a/tests/scripts/test-knowledge-setup-forms.cjs b/tests/scripts/test-knowledge-setup-forms.cjs index 674eea484..88bad57b2 100644 --- a/tests/scripts/test-knowledge-setup-forms.cjs +++ b/tests/scripts/test-knowledge-setup-forms.cjs @@ -103,7 +103,7 @@ describe('runKeyOnly', () => { afterEach(() => { process.env.HOME = savedHome; - fs.rmSync(home, { recursive: true, force: true }); + fs.rmSync(home, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); const deps = (answers) => ({ @@ -172,8 +172,8 @@ describe('runFromSystem refusals', () => { process.chdir(savedCwd); process.env.HOME = savedHome; if (savedEnvKey !== undefined) process.env.OPENAI_API_KEY = savedEnvKey; - fs.rmSync(home, { recursive: true, force: true }); - fs.rmSync(project, { recursive: true, force: true }); + fs.rmSync(home, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + fs.rmSync(project, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); const noBulk = async () => { throw new Error('bulk index must not run on a refusal'); }; diff --git a/tests/scripts/test-knowledge-store.cjs b/tests/scripts/test-knowledge-store.cjs index e94889d3a..fbbbb9c23 100644 --- a/tests/scripts/test-knowledge-store.cjs +++ b/tests/scripts/test-knowledge-store.cjs @@ -537,7 +537,7 @@ describe('knowledge store — persistence and locking', () => { }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('saves the store to disk as a non-empty file', async () => { @@ -678,7 +678,7 @@ describe('knowledge store — metadata', () => { }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); it('writes metadata.json with all 5 fields', () => { diff --git a/tests/scripts/test-migration-047.cjs b/tests/scripts/test-migration-047.cjs index d13325199..a2be100c4 100644 --- a/tests/scripts/test-migration-047.cjs +++ b/tests/scripts/test-migration-047.cjs @@ -20,7 +20,7 @@ function setup() { return fs.mkdtempSync(path.join(os.tmpdir(), 'migration-047-')); } function teardown(dir) { - fs.rmSync(dir, { recursive: true, force: true }); + fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); } function runMigration(dir) { let updates = 0; diff --git a/tests/scripts/test-migration-048.cjs b/tests/scripts/test-migration-048.cjs index 2b47d05b6..ac55690bc 100644 --- a/tests/scripts/test-migration-048.cjs +++ b/tests/scripts/test-migration-048.cjs @@ -21,7 +21,7 @@ function setup() { return dir; } function teardown(dir) { - fs.rmSync(dir, { recursive: true, force: true }); + fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); } function runMigration(dir) { let updates = 0; diff --git a/tests/scripts/test-migration-049.cjs b/tests/scripts/test-migration-049.cjs index 1dc71a528..d1908edfa 100644 --- a/tests/scripts/test-migration-049.cjs +++ b/tests/scripts/test-migration-049.cjs @@ -22,7 +22,7 @@ function setup() { return dir; } function teardown(dir) { - fs.rmSync(dir, { recursive: true, force: true }); + fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); } function runMigration(dir) { MIGRATION.run({ projectDir: dir, reportUpdate: () => {}, reportSkip: () => {} }); diff --git a/tests/scripts/test-migration-050.cjs b/tests/scripts/test-migration-050.cjs index d5407b16b..65a444083 100644 --- a/tests/scripts/test-migration-050.cjs +++ b/tests/scripts/test-migration-050.cjs @@ -44,7 +44,7 @@ describe('migration 050: purge closed work-unit caches', () => { updates = 0; skips = 0; }); - afterEach(() => fs.rmSync(dir, { recursive: true, force: true })); + afterEach(() => fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })); it('purges completed and cancelled caches, keeps in-progress, one update per purged unit', () => { unit('done-feature', 'completed'); cache('done-feature'); diff --git a/tests/scripts/test-migration-052.cjs b/tests/scripts/test-migration-052.cjs index dfe9e2f95..5b8f82a71 100644 --- a/tests/scripts/test-migration-052.cjs +++ b/tests/scripts/test-migration-052.cjs @@ -71,7 +71,7 @@ function setup() { return dir; } function teardown(dir) { - fs.rmSync(dir, { recursive: true, force: true }); + fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); } function runMigration(dir) { let updates = 0;