Files
fusion/packages/engine/src/__tests__/engine-singleton-lock.test.ts
gsxdsm cb57093d03 refactor: domain folder layout (types, API, core, engine) (#2398)
## Summary

Wave 17 organizes Fusion into **domain folders** (stacks on #2397).

### Layout
- **core/types/** — board, task, agents, settings, merge, workflow,
mesh, …
- **core/src/** — agents, ai, async-stores, workflows, tasks, config,
db, …
- **dashboard/app/api/** — client, tasks, agents, git, missions,
planning, …
- **engine/src/** — agents, auth, execution, merge, missions, overseer,
worktree, …

Root keepers retained for large entrypoints (`store.ts`, `executor.ts`,
`merger.ts`, …).

Public barrels (`@fusion/core`, `@fusion/engine`, `app/api.ts` → legacy)
stay stable.

## Test plan
- [x] `@fusion/core` typecheck
- [x] `@fusion/engine` typecheck (pre-existing playwright-core noise
only)
- [ ] CI merge gate

**Stack:** #2394 → #2397 → **this PR**
2026-08-03 00:20:53 -07:00

119 lines
4.0 KiB
TypeScript

import { mkdtempSync, rmSync, existsSync } from "node:fs";
import { tmpdir, platform } from "node:os";
import { join } from "node:path";
import net from "node:net";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
acquireEngineSingleton,
computeEngineLockFilePath,
computeEngineSocketPath,
EngineAlreadyRunningError,
type EngineSingletonLock,
} from "../project/engine-singleton-lock.js";
function uniqueProjectId(label: string): string {
return `proj_${label}_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
}
describe("engine-singleton-lock", () => {
let workDir: string;
let acquired: EngineSingletonLock[] = [];
beforeEach(() => {
workDir = mkdtempSync(join(tmpdir(), "fusion-engine-lock-test-"));
acquired = [];
});
afterEach(async () => {
for (const lock of acquired) {
await lock.release().catch(() => {});
}
rmSync(workDir, { recursive: true, force: true });
});
it("computes a deterministic socket path per projectId", () => {
const id = "proj_deterministic";
expect(computeEngineSocketPath(id)).toBe(computeEngineSocketPath(id));
expect(computeEngineSocketPath(id)).not.toBe(
computeEngineSocketPath("proj_other"),
);
});
it("places the lockfile under <workingDir>/.fusion/engine.lock", () => {
expect(computeEngineLockFilePath(workDir)).toBe(
join(workDir, ".fusion", "engine.lock"),
);
});
it("acquires successfully on first call and creates .fusion/engine.lock", async () => {
const id = uniqueProjectId("first");
const lock = await acquireEngineSingleton(id, workDir);
acquired.push(lock);
expect(existsSync(lock.lockFilePath)).toBe(true);
});
it("rejects a second acquisition for the same project + workingDir", async () => {
const id = uniqueProjectId("double");
const first = await acquireEngineSingleton(id, workDir);
acquired.push(first);
await expect(acquireEngineSingleton(id, workDir)).rejects.toBeInstanceOf(
EngineAlreadyRunningError,
);
});
it("allows re-acquisition after release", async () => {
const id = uniqueProjectId("cycle");
const first = await acquireEngineSingleton(id, workDir);
await first.release();
// Double-release must be a no-op.
await first.release();
const second = await acquireEngineSingleton(id, workDir);
acquired.push(second);
expect(existsSync(second.lockFilePath)).toBe(true);
});
it("recovers from a stale socket file (POSIX only)", async () => {
if (platform() === "win32") return;
const id = uniqueProjectId("stale");
const socketPath = computeEngineSocketPath(id);
// Simulate a stale socket file from a crashed engine.
const stale = net.createServer();
stale.unref();
await new Promise<void>((resolve, reject) => {
stale.once("error", reject);
stale.once("listening", () => resolve());
stale.listen(socketPath);
});
await new Promise<void>((resolve) => stale.close(() => resolve()));
const lock = await acquireEngineSingleton(id, workDir);
acquired.push(lock);
expect(existsSync(lock.lockFilePath)).toBe(true);
});
it("releases the lockfile so a fresh process could re-acquire", async () => {
const id = uniqueProjectId("release-lockfile");
const lock = await acquireEngineSingleton(id, workDir);
await lock.release();
// proper-lockfile uses `<path>.lock` as the actual mutex dir.
expect(existsSync(`${lock.lockFilePath}.lock`)).toBe(false);
});
it("different projects don't block each other", async () => {
const a = await acquireEngineSingleton(uniqueProjectId("a"), workDir);
acquired.push(a);
const otherWork = mkdtempSync(join(tmpdir(), "fusion-engine-lock-test-b-"));
try {
const b = await acquireEngineSingleton(uniqueProjectId("b"), otherWork);
acquired.push(b);
expect(existsSync(a.lockFilePath)).toBe(true);
expect(existsSync(b.lockFilePath)).toBe(true);
} finally {
rmSync(otherWork, { recursive: true, force: true });
}
});
});