| import { describe, expect, it } from "vitest"; |
| import { createRunRegistry } from "./registry.js"; |
|
|
| type RunRegistry = ReturnType<typeof createRunRegistry>; |
|
|
| function addRunningRecord( |
| registry: RunRegistry, |
| params: { |
| runId: string; |
| sessionId: string; |
| startedAtMs: number; |
| scopeKey?: string; |
| backendId?: string; |
| }, |
| ) { |
| registry.add({ |
| runId: params.runId, |
| sessionId: params.sessionId, |
| backendId: params.backendId ?? "b1", |
| scopeKey: params.scopeKey, |
| state: "running", |
| startedAtMs: params.startedAtMs, |
| lastOutputAtMs: params.startedAtMs, |
| createdAtMs: params.startedAtMs, |
| updatedAtMs: params.startedAtMs, |
| }); |
| } |
|
|
| describe("process supervisor run registry", () => { |
| it("finalize is idempotent and preserves first terminal metadata", () => { |
| const registry = createRunRegistry(); |
| addRunningRecord(registry, { runId: "r1", sessionId: "s1", startedAtMs: 1 }); |
|
|
| const first = registry.finalize("r1", { |
| reason: "overall-timeout", |
| exitCode: null, |
| exitSignal: "SIGKILL", |
| }); |
| const second = registry.finalize("r1", { |
| reason: "manual-cancel", |
| exitCode: 0, |
| exitSignal: null, |
| }); |
|
|
| expect(first).not.toBeNull(); |
| expect(first?.firstFinalize).toBe(true); |
| expect(first?.record.terminationReason).toBe("overall-timeout"); |
| expect(first?.record.exitCode).toBeNull(); |
| expect(first?.record.exitSignal).toBe("SIGKILL"); |
|
|
| expect(second).not.toBeNull(); |
| expect(second?.firstFinalize).toBe(false); |
| expect(second?.record.terminationReason).toBe("overall-timeout"); |
| expect(second?.record.exitCode).toBeNull(); |
| expect(second?.record.exitSignal).toBe("SIGKILL"); |
| }); |
|
|
| it("prunes oldest exited records once retention cap is exceeded", () => { |
| const registry = createRunRegistry({ maxExitedRecords: 2 }); |
| addRunningRecord(registry, { runId: "r1", sessionId: "s1", startedAtMs: 1 }); |
| addRunningRecord(registry, { runId: "r2", sessionId: "s2", startedAtMs: 2 }); |
| addRunningRecord(registry, { runId: "r3", sessionId: "s3", startedAtMs: 3 }); |
|
|
| registry.finalize("r1", { reason: "exit", exitCode: 0, exitSignal: null }); |
| registry.finalize("r2", { reason: "exit", exitCode: 0, exitSignal: null }); |
| registry.finalize("r3", { reason: "exit", exitCode: 0, exitSignal: null }); |
|
|
| expect(registry.get("r1")).toBeUndefined(); |
| expect(registry.get("r2")?.state).toBe("exited"); |
| expect(registry.get("r3")?.state).toBe("exited"); |
| }); |
|
|
| it("filters listByScope and returns detached copies", () => { |
| const registry = createRunRegistry(); |
| addRunningRecord(registry, { |
| runId: "r1", |
| sessionId: "s1", |
| scopeKey: "scope:a", |
| startedAtMs: 1, |
| }); |
| addRunningRecord(registry, { |
| runId: "r2", |
| sessionId: "s2", |
| scopeKey: "scope:b", |
| startedAtMs: 2, |
| }); |
|
|
| expect(registry.listByScope(" ")).toEqual([]); |
| const scoped = registry.listByScope("scope:a"); |
| expect(scoped).toHaveLength(1); |
| const [firstScoped] = scoped; |
| expect(firstScoped?.runId).toBe("r1"); |
|
|
| if (!firstScoped) { |
| throw new Error("missing scoped record"); |
| } |
| firstScoped.state = "exited"; |
| expect(registry.get("r1")?.state).toBe("running"); |
| }); |
| }); |
|
|