Files
fusion/scripts/__tests__/boot-smoke.test.mjs
gsxdsm aae6a6eca2 FN-7662: harden boot-smoke temp dir cleanup against macOS ENOTEMPTY races
Boot-smoke's post-verdict temp-dir cleanup could throw ENOTEMPTY on macOS when async writers (fsevents/Spotlight, the just-killed child) still touched the throwaway HOME/project dirs, turning an already-decided PASS into a pnpm verify:fast failure.

- Add removeTempDir() helper in scripts/boot-smoke.mjs: wraps rmSync with maxRetries/retryDelay and swallows any residual error (never throws) since cleanup always runs after the smoke verdict is decided/printed
- Replace both isolatedHome/isolatedProject rmSync call sites (exit handler and EADDRINUSE retry-port branch) with removeTempDir()
- Guard main() behind an import.meta.url === pathToFileURL(process.argv[1]) check so requiring boot-smoke.mjs from tests doesn't spawn a real server
- Add scripts/__tests__/boot-smoke.test.mjs covering ENOTEMPTY tolerance, always-failing rm, retry/backoff params, and the no-boot-on-import guard

Files changed:
 scripts/__tests__/boot-smoke.test.mjs | 77 +++++++++++++++++++++++++++++++++++
 scripts/boot-smoke.mjs                | 44 +++++++++++++++++---
 2 files changed, 115 insertions(+), 6 deletions(-)

Fusion-Task-Id: FN-7662

Fusion-Task-Lineage: 3288d055-4f70-4487-8462-933563214ce5

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-08 00:37:07 -07:00

78 lines
2.5 KiB
JavaScript

// FNXC:BootSmoke 2026-07-07-00:00: Regression for the macOS ENOTEMPTY temp-dir
// cleanup race (FN-7662). Drives an injected `rm` to throw ENOTEMPTY and
// asserts `removeTempDir` tolerates it (retries/succeeds, or swallows a
// persistently-throwing remover) so a post-PASS cleanup can never fail the gate.
import test from "node:test";
import assert from "node:assert/strict";
import { removeTempDir } from "../boot-smoke.mjs";
function enotempty() {
const err = new Error("ENOTEMPTY: directory not empty");
err.code = "ENOTEMPTY";
return err;
}
test("removeTempDir tolerates ENOTEMPTY on the first call then succeeds", () => {
let calls = 0;
const rm = (dir, opts) => {
calls += 1;
assert.equal(dir, "/tmp/fusion-boot-smoke-home-xyz");
assert.equal(opts.recursive, true);
assert.equal(opts.force, true);
if (calls === 1) throw enotempty();
// second call (simulating rmSync's own internal retry succeeding): no throw
};
assert.doesNotThrow(() => {
removeTempDir("/tmp/fusion-boot-smoke-home-xyz", { rm });
});
assert.equal(calls, 1);
});
test("removeTempDir never throws even when rm always fails with ENOTEMPTY", () => {
let calls = 0;
const rm = () => {
calls += 1;
throw enotempty();
};
assert.doesNotThrow(() => {
removeTempDir("/tmp/fusion-boot-smoke-project-abc", { rm });
});
assert.equal(calls, 1);
});
test("removeTempDir calls rm with recursive/force and a positive maxRetries", () => {
let seenOpts;
const rm = (dir, opts) => {
seenOpts = opts;
};
removeTempDir("/tmp/fusion-boot-smoke-home-happy", { rm });
assert.equal(seenOpts.recursive, true);
assert.equal(seenOpts.force, true);
assert.ok(seenOpts.maxRetries > 0, "maxRetries should be positive");
assert.ok(typeof seenOpts.retryDelay === "number");
});
test("removeTempDir uses the provided maxRetries/retryDelayMs overrides", () => {
let seenOpts;
const rm = (dir, opts) => {
seenOpts = opts;
};
removeTempDir("/tmp/fusion-boot-smoke-project-custom", { rm, maxRetries: 9, retryDelayMs: 250 });
assert.equal(seenOpts.maxRetries, 9);
assert.equal(seenOpts.retryDelay, 250);
});
test("importing boot-smoke.mjs does not boot a server (main() guard holds)", async () => {
// The module was already imported at the top of this file for `removeTempDir`.
// If the main()/bootAndVerify() top-level invocation ran unguarded, this test
// file would hang waiting on a spawned `fn serve` child. Reaching this
// assertion at all proves the import completed without side effects.
assert.ok(true);
});