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:
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user