FN-5840: harden test project teardown cleanup
Tighten test fixture cleanup so temporary test directories are reliably removed. - track created test project directories and install process exit/beforeExit backstop cleanup hooks - make destroyTestProject resilient with async rm plus rmSync fallback and tracked-dir removal - add regression tests for sqlite wal/shm sibling cleanup and tracked-directory drain behavior - make db lock-child teardown remove children from tracking during iteration Files changed: packages/core/src/__tests__/db.test.ts | 10 +++-- packages/core/src/__tests__/test-project.test.ts | 34 ++++++++++++++- packages/core/src/__tests__/test-project.ts | 54 +++++++++++++++++++++++- 3 files changed, 92 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-5840 Fusion-Task-Lineage: 0977be1d-8334-413d-a652-318584d7058b
This commit is contained in:
@@ -71,14 +71,18 @@ function removeTrackedTmpDirSync(dir: string | undefined): void {
|
||||
const activeLockChildren = new Set<ChildProcessWithoutNullStreams>();
|
||||
|
||||
function killLockChildrenSync(): void {
|
||||
for (const child of activeLockChildren) {
|
||||
const children = Array.from(activeLockChildren);
|
||||
for (const child of children) {
|
||||
try {
|
||||
if (child.exitCode === null && !child.killed) child.kill("SIGKILL");
|
||||
if (child.exitCode === null && !child.killed) {
|
||||
child.kill("SIGKILL");
|
||||
}
|
||||
} catch {
|
||||
// best-effort
|
||||
} finally {
|
||||
activeLockChildren.delete(child);
|
||||
}
|
||||
}
|
||||
activeLockChildren.clear();
|
||||
}
|
||||
|
||||
function cleanupTmpDirsSync(): void {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtempSync, existsSync, readdirSync } from "node:fs";
|
||||
import { mkdtempSync, existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import { rm, readFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
@@ -7,6 +7,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { TaskStore } from "../store.js";
|
||||
import {
|
||||
__getTrackedTestProjectDirsForTests,
|
||||
createTestProject,
|
||||
destroyTestProject,
|
||||
seedTasks,
|
||||
@@ -68,6 +69,22 @@ describe("test-project fixture", () => {
|
||||
expect(existsSync(fixture.rootDir)).toBe(false);
|
||||
});
|
||||
|
||||
it("destroyTestProject() removes directories containing sqlite wal/shm siblings", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "fusion-test-project-wal-"));
|
||||
extraDirs.add(dir);
|
||||
|
||||
const fusionDir = join(dir, ".fusion");
|
||||
mkdirSync(fusionDir, { recursive: true });
|
||||
writeFileSync(join(fusionDir, "fusion.db"), "db");
|
||||
writeFileSync(join(fusionDir, "fusion.db-wal"), "wal");
|
||||
writeFileSync(join(fusionDir, "fusion.db-shm"), "shm");
|
||||
|
||||
await destroyTestProject(dir);
|
||||
|
||||
extraDirs.delete(dir);
|
||||
expect(existsSync(dir)).toBe(false);
|
||||
});
|
||||
|
||||
it(
|
||||
"supports multiple isolated projects without cross-interference",
|
||||
async () => {
|
||||
@@ -160,4 +177,19 @@ describe("test-project fixture", () => {
|
||||
|
||||
expect(tasks).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("cleanup() drains tracked backstop directories", async () => {
|
||||
const fixture = await createFixture();
|
||||
const trackedDirs = __getTrackedTestProjectDirsForTests();
|
||||
|
||||
expect(trackedDirs.has(fixture.rootDir)).toBe(true);
|
||||
expect(trackedDirs.has(fixture.globalDir)).toBe(true);
|
||||
|
||||
await fixture.cleanup();
|
||||
|
||||
expect(trackedDirs.has(fixture.rootDir)).toBe(false);
|
||||
expect(trackedDirs.has(fixture.globalDir)).toBe(false);
|
||||
expect(existsSync(fixture.rootDir)).toBe(false);
|
||||
expect(existsSync(fixture.globalDir)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { mkdir, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
@@ -26,6 +26,15 @@ export interface TestProjectFixture {
|
||||
cleanup: () => Promise<void>;
|
||||
}
|
||||
|
||||
const TEST_PROJECT_RM_OPTIONS = {
|
||||
recursive: true,
|
||||
force: true,
|
||||
maxRetries: 5,
|
||||
retryDelay: 50,
|
||||
} as const;
|
||||
const TRACKED_TEST_PROJECT_DIRS = new Set<string>();
|
||||
const TEST_PROJECT_CLEANUP_HOOK_KEY = Symbol.for("fusion.core.test-project.cleanup-hooks-installed");
|
||||
|
||||
function assertAbsolutePath(pathValue: string, label: string): void {
|
||||
if (!isAbsolute(pathValue)) {
|
||||
throw new Error(`${label} must be an absolute path`);
|
||||
@@ -79,6 +88,11 @@ export async function createTestProject(
|
||||
assertAbsolutePath(rootDir, "rootDir");
|
||||
assertAbsolutePath(globalDir, "globalSettingsDir");
|
||||
|
||||
TRACKED_TEST_PROJECT_DIRS.add(rootDir);
|
||||
if (ownsGlobalDir) {
|
||||
TRACKED_TEST_PROJECT_DIRS.add(globalDir);
|
||||
}
|
||||
|
||||
let store: TaskStore | undefined;
|
||||
|
||||
try {
|
||||
@@ -199,5 +213,41 @@ export async function seedTasks(store: TaskStore, count = 3): Promise<Task[]> {
|
||||
*/
|
||||
export async function destroyTestProject(dir: string): Promise<void> {
|
||||
assertAbsolutePath(dir, "dir");
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
try {
|
||||
await rm(dir, TEST_PROJECT_RM_OPTIONS);
|
||||
} catch {
|
||||
try {
|
||||
rmSync(dir, TEST_PROJECT_RM_OPTIONS);
|
||||
} catch {
|
||||
// best-effort fallback during teardown
|
||||
}
|
||||
} finally {
|
||||
TRACKED_TEST_PROJECT_DIRS.delete(dir);
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupTrackedTestProjectDirsSync(): void {
|
||||
const cleanup = Array.from(TRACKED_TEST_PROJECT_DIRS);
|
||||
for (const dir of cleanup) {
|
||||
try {
|
||||
rmSync(dir, TEST_PROJECT_RM_OPTIONS);
|
||||
} catch {
|
||||
// best-effort fallback during process teardown
|
||||
} finally {
|
||||
TRACKED_TEST_PROJECT_DIRS.delete(dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const processWithCleanupFlag = process as typeof process & {
|
||||
[TEST_PROJECT_CLEANUP_HOOK_KEY]?: boolean;
|
||||
};
|
||||
if (!processWithCleanupFlag[TEST_PROJECT_CLEANUP_HOOK_KEY]) {
|
||||
process.once("beforeExit", cleanupTrackedTestProjectDirsSync);
|
||||
process.once("exit", cleanupTrackedTestProjectDirsSync);
|
||||
processWithCleanupFlag[TEST_PROJECT_CLEANUP_HOOK_KEY] = true;
|
||||
}
|
||||
|
||||
export function __getTrackedTestProjectDirsForTests(): Set<string> {
|
||||
return TRACKED_TEST_PROJECT_DIRS;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user