fix(engine-tests): plug fusion-test-ref-* leaks on SIGTERM fork recycle

Same class of failure as the kb-db-test-* leak: vitest's forks pool
SIGTERMs a fork on test timeout and skips the in-test `finally { rmSync }`,
leaking `fusion-test-ref-project-*`, `fusion-test-ref-concurrent-project-*`,
and `fusion-test-ref-advance-*` dirs that scripts/check-test-isolation.mjs
flags during deterministic merge verification.

Track every minted dir in a per-file set and sweep it from
SIGTERM/SIGINT/SIGHUP/beforeExit/exit handlers (signals re-raised after
cleanup) plus an `afterAll` for the happy path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-22 23:30:19 -07:00
parent 380f8b8e5e
commit 7345ab85d0
2 changed files with 104 additions and 10 deletions

View File

@@ -1,16 +1,60 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, afterAll } from "vitest";
import { mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { execSync } from "node:child_process";
import { advanceIntegrationBranchRef } from "../merger-ref-update-advance.js";
// Signal-safe sweep for fusion-test-ref-advance-* tmp dirs. Vitest's forks pool
// SIGTERMs a fork when a test times out, which skips `finally { rmSync(...) }`
// and leaks dirs that scripts/check-test-isolation.mjs then fails merge
// verification on.
const TMP_DIR_RM_OPTIONS = { recursive: true, force: true, maxRetries: 5, retryDelay: 50 } as const;
const TMP_DIR_CLEANUP_HOOK_KEY = Symbol.for(
"fusion.engine.merger-ref-update-advance-test.tmp-cleanup-hooks-installed",
);
const trackedTmpDirs = new Set<string>();
function removeTmpDirSync(dir: string): void {
try {
rmSync(dir, TMP_DIR_RM_OPTIONS);
} catch {
// best-effort fallback during teardown
} finally {
trackedTmpDirs.delete(dir);
}
}
function cleanupTmpDirsSync(): void {
for (const dir of Array.from(trackedTmpDirs)) removeTmpDirSync(dir);
}
const processWithCleanupFlag = process as typeof process & {
[TMP_DIR_CLEANUP_HOOK_KEY]?: boolean;
};
if (!processWithCleanupFlag[TMP_DIR_CLEANUP_HOOK_KEY]) {
process.once("beforeExit", cleanupTmpDirsSync);
process.once("exit", cleanupTmpDirsSync);
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"] as const) {
process.once(signal, () => {
cleanupTmpDirsSync();
process.kill(process.pid, signal);
});
}
processWithCleanupFlag[TMP_DIR_CLEANUP_HOOK_KEY] = true;
}
afterAll(() => {
cleanupTmpDirsSync();
});
function git(cwd: string, cmd: string): string {
return execSync(cmd, { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
}
function setupRepo(defaultBranch: "main" | "master" = "main") {
const dir = mkdtempSync(join(tmpdir(), "fusion-test-ref-advance-"));
trackedTmpDirs.add(dir);
git(dir, `git init -b ${defaultBranch}`);
git(dir, "git config user.name tester");
git(dir, "git config user.email tester@example.com");
@@ -52,7 +96,7 @@ describe("advanceIntegrationBranchRef", () => {
expect(events[0]?.metadata?.refName).toBe(`refs/heads/${integrationBranch}`);
expect(events[0]?.target).toBe(integrationBranch);
} finally {
rmSync(dir, { recursive: true, force: true });
removeTmpDirSync(dir);
}
});
@@ -94,7 +138,7 @@ describe("advanceIntegrationBranchRef", () => {
expect(events[0]?.type).toBe("merge:integration-ref-advance");
expect(events[0]?.metadata?.succeeded).toBe(false);
} finally {
rmSync(dir, { recursive: true, force: true });
removeTmpDirSync(dir);
}
});
@@ -132,7 +176,7 @@ describe("advanceIntegrationBranchRef", () => {
expect(status).toContain("tracked.txt");
expect(status).toContain("untracked.txt");
} finally {
rmSync(dir, { recursive: true, force: true });
removeTmpDirSync(dir);
}
});
@@ -159,7 +203,7 @@ describe("advanceIntegrationBranchRef", () => {
audit: { git: async () => undefined } as any,
})).rejects.toThrow("expectedCurrentSha");
} finally {
rmSync(dir, { recursive: true, force: true });
removeTmpDirSync(dir);
}
});
});

View File

@@ -1,13 +1,63 @@
import { describe, it, expect, vi } from "vitest";
import { describe, it, expect, vi, afterAll } from "vitest";
import { mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { git, hasGit } from "./_helpers.js";
import { advanceIntegrationBranchRef, __test__ } from "../../merger-ref-update-advance.js";
// Vitest's forks pool SIGTERMs a fork when a test times out, which skips any
// in-test `finally { rmSync(...) }` and leaves `fusion-test-ref-*-project-*`
// dirs behind. scripts/check-test-isolation.mjs then fails deterministic merge
// verification with these as leaks. Track every minted dir and sweep them in
// signal/exit handlers as a backstop.
const TMP_DIR_RM_OPTIONS = { recursive: true, force: true, maxRetries: 5, retryDelay: 50 } as const;
const TMP_DIR_CLEANUP_HOOK_KEY = Symbol.for(
"fusion.engine.dirty-integration-worktree-test.tmp-cleanup-hooks-installed",
);
const trackedTmpDirs = new Set<string>();
function mintTmpDir(prefix: string): string {
const dir = mkdtempSync(join(tmpdir(), prefix));
trackedTmpDirs.add(dir);
return dir;
}
function removeTmpDirSync(dir: string): void {
try {
rmSync(dir, TMP_DIR_RM_OPTIONS);
} catch {
// best-effort fallback during teardown
} finally {
trackedTmpDirs.delete(dir);
}
}
function cleanupTmpDirsSync(): void {
for (const dir of Array.from(trackedTmpDirs)) removeTmpDirSync(dir);
}
const processWithCleanupFlag = process as typeof process & {
[TMP_DIR_CLEANUP_HOOK_KEY]?: boolean;
};
if (!processWithCleanupFlag[TMP_DIR_CLEANUP_HOOK_KEY]) {
process.once("beforeExit", cleanupTmpDirsSync);
process.once("exit", cleanupTmpDirsSync);
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"] as const) {
process.once(signal, () => {
cleanupTmpDirsSync();
process.kill(process.pid, signal);
});
}
processWithCleanupFlag[TMP_DIR_CLEANUP_HOOK_KEY] = true;
}
afterAll(() => {
cleanupTmpDirsSync();
});
describe.skipIf(!hasGit)("reliability interaction: dirty integration worktree with ref-only advance", () => {
it.each(["main", "master"] as const)("advances %s without touching dirty/untracked files", async (integrationBranch) => {
const projectRootDir = mkdtempSync(join(tmpdir(), "fusion-test-ref-project-"));
const projectRootDir = mintTmpDir("fusion-test-ref-project-");
const rootDir = join(projectRootDir, "task-wt");
const events: any[] = [];
const runGitSpy = vi.spyOn(__test__, "runGit");
@@ -68,12 +118,12 @@ describe.skipIf(!hasGit)("reliability interaction: dirty integration worktree wi
}
} finally {
runGitSpy.mockRestore();
rmSync(projectRootDir, { recursive: true, force: true });
removeTmpDirSync(projectRootDir);
}
});
it("returns concurrent-advance and preserves concurrent ref", async () => {
const projectRootDir = mkdtempSync(join(tmpdir(), "fusion-test-ref-concurrent-project-"));
const projectRootDir = mintTmpDir("fusion-test-ref-concurrent-project-");
const rootDir = join(projectRootDir, "task-wt");
const events: any[] = [];
try {
@@ -119,7 +169,7 @@ describe.skipIf(!hasGit)("reliability interaction: dirty integration worktree wi
expect(failureEvent?.metadata?.succeeded).toBe(false);
expect(String(failureEvent?.metadata?.error ?? "")).toContain("concurrent-advance");
} finally {
rmSync(projectRootDir, { recursive: true, force: true });
removeTmpDirSync(projectRootDir);
}
});
});