File size: 3,262 Bytes
fc93158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
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");
  });
});