From 1802bafc74cc554381c097d287bbb717ead010cf Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Sun, 11 Oct 2026 13:05:31 +0800 Subject: [PATCH] fix(test): keep removing the temp HOME until a straggling CLI goes quiet maxRetries alone was not enough: on a machine with a real agent CLI installed, a test's code path probes or spawns `claude`, which flushes its own ~/.claude.json and ~/.claude/backups/ into the temp HOME a moment after the test ends, so rmSync raced it and failed the whole file with ENOTEMPTY after every test had passed (about one run in three for session-routes-claude-model; CI has no CLI). Retry the removal for up to ~5s, and if it never goes quiet leave the folder to the exit backstop instead of failing a file that passed. 12 consecutive runs of that file: no failure. --- test/setup.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/test/setup.ts b/test/setup.ts index 7a546103..e4f67be7 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -121,9 +121,20 @@ afterAll(async () => { if (originalPlaywrightBrowsersPath === undefined) delete process.env.PLAYWRIGHT_BROWSERS_PATH; else process.env.PLAYWRIGHT_BROWSERS_PATH = originalPlaywrightBrowsersPath; - // maxRetries: a straggling writer (a server persisting on shutdown) can recreate a file between - // rmSync's readdir and rmdir, which fails the whole file with ENOTEMPTY after every test passed. - rmSync(testHome, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + // A straggling writer can recreate files between rmSync's readdir and rmdir and fail the whole file + // with ENOTEMPTY after every test has passed. The usual culprit is a real agent CLI on a developer + // machine (the `claude` a test's code path probes or spawns) flushing its own `~/.claude.json` and + // `~/.claude/backups/` into the temp HOME a moment after the test ends; CI has no such CLI. Keep + // removing until it goes quiet, and if it never does, leave the temp dir to the `exit` backstop + // below: a leaked temp folder must not fail a test file that passed. + for (let attempt = 0; attempt < 20; attempt++) { + try { + rmSync(testHome, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 }); + break; + } catch { + await new Promise((resolve) => setTimeout(resolve, 250)); + } + } }); // afterAll never fires for a fully-skipped test file (no tests execute), which