@selesai/code 0.13.39 → 0.13.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/core/intercom-rpc.d.ts +135 -0
  2. package/dist/core/intercom-rpc.js +64 -0
  3. package/dist/core/release-rpc.d.ts +137 -0
  4. package/dist/core/release-rpc.js +77 -0
  5. package/dist/core/resource-loader.d.ts +3 -0
  6. package/dist/core/resource-loader.js +3 -0
  7. package/dist/core/subagent-rpc.d.ts +110 -0
  8. package/dist/core/subagent-rpc.js +60 -0
  9. package/dist/extensions/agent-browser.ts +9 -0
  10. package/dist/extensions/pi-intercom/README.md +15 -0
  11. package/dist/extensions/pi-intercom/broker/broker.ts +277 -13
  12. package/dist/extensions/pi-intercom/broker/client.ts +6 -2
  13. package/dist/extensions/pi-intercom/broker/mailbox-persistence.test.ts +523 -0
  14. package/dist/extensions/pi-intercom/broker/paths.test.ts +4 -4
  15. package/dist/extensions/pi-intercom/broker/paths.ts +4 -2
  16. package/dist/extensions/pi-intercom/broker/spawn.test.ts +1 -3
  17. package/dist/extensions/pi-intercom/extension-api.ts +10 -0
  18. package/dist/extensions/pi-intercom/index.ts +188 -8
  19. package/dist/extensions/pi-intercom/intercom.integration.test.ts +280 -0
  20. package/dist/extensions/pi-intercom/package.json +1 -1
  21. package/dist/extensions/pi-intercom/release-readiness.test.ts +116 -0
  22. package/dist/extensions/pi-intercom/release-readiness.ts +93 -0
  23. package/dist/extensions/pi-subagents/docs/extension-api.md +8 -0
  24. package/dist/extensions/pi-subagents/docs/observability.md +8 -0
  25. package/dist/extensions/pi-subagents/src/extension/index.ts +51 -1
  26. package/dist/extensions/pi-subagents/src/extension/observability.ts +164 -0
  27. package/dist/extensions/pi-subagents/src/extension/release-readiness.ts +367 -0
  28. package/dist/extensions/pi-subagents/src/extension/rpc.ts +8 -1
  29. package/dist/extensions/pi-subagents/src/integrations/pi-web-session-liveness.ts +7 -1
  30. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +1 -44
  31. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +6 -0
  32. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +13 -3
  33. package/dist/extensions/pi-subagents/src/runs/background/result-delivery-ownership.ts +269 -7
  34. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +11 -10
  35. package/dist/extensions/pi-subagents/src/runs/background/retained-nested-route-tracker.ts +15 -3
  36. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +64 -0
  37. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +69 -6
  38. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -1
  39. package/dist/extensions/pi-subagents/src/shared/owner-record.ts +107 -0
  40. package/dist/extensions/pi-subagents/src/shared/process-identity.ts +87 -0
  41. package/dist/extensions/pi-subagents/src/shared/types.ts +9 -0
  42. package/dist/extensions/pi-subagents/test/integration/async-attention-clearing.test.ts +191 -0
  43. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +63 -0
  44. package/dist/extensions/pi-subagents/test/integration/stop-all-background.test.ts +142 -0
  45. package/dist/extensions/pi-subagents/test/support/fake-child-session.ts +8 -0
  46. package/dist/extensions/pi-subagents/test/unit/observability.test.ts +198 -0
  47. package/dist/extensions/pi-subagents/test/unit/process-identity.test.ts +75 -0
  48. package/dist/extensions/pi-subagents/test/unit/release-readiness.test.ts +715 -0
  49. package/dist/extensions/pi-subagents/test/unit/result-takeover.test.ts +332 -0
  50. package/dist/extensions/tokenin-onboarding.ts +4 -2
  51. package/dist/index.d.ts +6 -1
  52. package/dist/index.js +7 -0
  53. package/dist/main.d.ts +6 -0
  54. package/dist/main.js +1 -1
  55. package/dist/modes/index.d.ts +1 -1
  56. package/dist/modes/rpc/intercom-bridge.d.ts +14 -0
  57. package/dist/modes/rpc/intercom-bridge.js +75 -0
  58. package/dist/modes/rpc/intercom-bridge.test.d.ts +1 -0
  59. package/dist/modes/rpc/intercom-bridge.test.js +65 -0
  60. package/dist/modes/rpc/release-bridge.d.ts +35 -0
  61. package/dist/modes/rpc/release-bridge.js +159 -0
  62. package/dist/modes/rpc/release-bridge.test.d.ts +1 -0
  63. package/dist/modes/rpc/release-bridge.test.js +347 -0
  64. package/dist/modes/rpc/rpc-client.d.ts +34 -10
  65. package/dist/modes/rpc/rpc-client.js +45 -2
  66. package/dist/modes/rpc/rpc-intercom-transport.test.d.ts +1 -0
  67. package/dist/modes/rpc/rpc-intercom-transport.test.js +85 -0
  68. package/dist/modes/rpc/rpc-mode.d.ts +1 -13
  69. package/dist/modes/rpc/rpc-mode.js +239 -8
  70. package/dist/modes/rpc/rpc-release-transport.test.d.ts +1 -0
  71. package/dist/modes/rpc/rpc-release-transport.test.js +329 -0
  72. package/dist/modes/rpc/rpc-subagent-transport.test.d.ts +1 -0
  73. package/dist/modes/rpc/rpc-subagent-transport.test.js +60 -0
  74. package/dist/modes/rpc/rpc-types.d.ts +12 -2
  75. package/dist/modes/rpc/rpc-types.js +0 -6
  76. package/dist/modes/rpc/subagent-bridge.d.ts +23 -0
  77. package/dist/modes/rpc/subagent-bridge.js +93 -0
  78. package/dist/modes/rpc/test-fixtures/intercom-host.d.ts +1 -0
  79. package/dist/modes/rpc/test-fixtures/intercom-host.js +13 -0
  80. package/docs/rpc-subagent.schema.json +491 -0
  81. package/docs/rpc.md +210 -1
  82. package/package.json +2 -1
@@ -42,6 +42,8 @@ export interface FakeChildResponse {
42
42
  createError?: string;
43
43
  /** Keeps the run open until the parent aborts it. */
44
44
  hangUntilAbort?: boolean;
45
+ /** Rejects the first N `steer()` calls (a child that cannot take steering yet), then accepts them. */
46
+ failSteers?: number;
45
47
  }
46
48
 
47
49
  export interface FakeChildSessionRecord {
@@ -262,7 +264,9 @@ export function createFakeChildSessions(queueDir: () => string): FakeChildSessio
262
264
  await sleep(20, abortedPromise);
263
265
  }
264
266
  };
267
+ let failingSteers = 0;
265
268
  const runScript = async (response: FakeChildResponse, task: string): Promise<void> => {
269
+ failingSteers = typeof response.failSteers === "number" ? response.failSteers : 0;
266
270
  if (typeof response.delay === "number" && response.delay > 0) await sleep(response.delay, abortedPromise);
267
271
  await waitForReleasePath(response.waitForPath);
268
272
  if (record.aborted) return;
@@ -376,6 +380,10 @@ export function createFakeChildSessions(queueDir: () => string): FakeChildSessio
376
380
  }
377
381
  },
378
382
  async steer(text) {
383
+ if (failingSteers > 0) {
384
+ failingSteers--;
385
+ throw new Error("mock child cannot accept steering");
386
+ }
379
387
  record.steers.push({ text, mode: "steer" });
380
388
  recordSteer(text, "steer");
381
389
  },
@@ -0,0 +1,198 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { createEventBus } from "../../../../core/event-bus.ts";
3
+ import { SUBAGENT_OBSERVE_EVENT, isSubagentRpcProjection, type SubagentObserveRequest, type SubagentRpcEvent, type SubagentRpcProjection } from "../../../../core/subagent-rpc.ts";
4
+ import { RpcSubagentBridge } from "../../../../modes/rpc/subagent-bridge.ts";
5
+ import { registerSubagentObservability } from "../../src/extension/observability.ts";
6
+ import { buildFleetStatus } from "../../src/extension/rpc.ts";
7
+ import { createAsyncJobTracker } from "../../src/runs/background/async-job-tracker.ts";
8
+ import type { SubagentState } from "../../src/shared/types.ts";
9
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { dirname, join } from "node:path";
12
+ import { randomUUID } from "node:crypto";
13
+ import { retainLiveForegroundNestedRoute } from "../../src/integrations/pi-web-session-liveness.ts";
14
+ import { createRetainedNestedRouteTracker } from "../../src/runs/background/retained-nested-route-tracker.ts";
15
+ import { createNestedRoute, writeNestedEvent } from "../../src/runs/shared/nested-events.ts";
16
+
17
+ function native() {
18
+ const bus = createEventBus();
19
+ const state = { currentSessionId: "/managed/session", supervisorOwnerSessionId: "host", foregroundControls: new Map(), foregroundRuns: new Map(),
20
+ asyncJobs: new Map(), fleetJobs: new Map(), cleanupTimers: new Map(), resultFileCoalescer: new Map(), lastUiContext: null,
21
+ activeAsyncCapacity: { used: 1, limit: 3 }, poller: null } as unknown as SubagentState;
22
+ const waiting: Array<{ runId: string; childIndex: number; expectsReply: boolean }> = [];
23
+ const fleetKeys = { sessionId: null, next: 0, keys: new Map() };
24
+ const adapter = registerSubagentObservability({ events: bus, state, getHostSessionId: () => state.supervisorOwnerSessionId!,
25
+ getWaitingChildren: () => waiting, getFleet: () => buildFleetStatus(state, fleetKeys, state.currentSessionId) });
26
+ const events: SubagentRpcEvent[] = [];
27
+ const bridge = new RpcSubagentBridge(bus, event => events.push(event), { version: 1, sessionId: "host", epoch: "bind-1" });
28
+ bridge.initialize();
29
+ const status = () => {
30
+ const reply = bridge.request({ type: "subagent_status", version: 1 });
31
+ if (!reply.success || reply.command !== "subagent_status") throw new Error(JSON.stringify(reply));
32
+ return reply.data;
33
+ };
34
+ return { bus, state, waiting, adapter, bridge, events, status, dispose: () => { bridge.dispose(); adapter.dispose(); } };
35
+ }
36
+ afterEach(() => vi.useRealTimers());
37
+
38
+ describe("RpcSubagentBridge / native observability", () => {
39
+ it("explicitly reports missing backend and rejects requested versions", () => {
40
+ const output = vi.fn();
41
+ const bridge = new RpcSubagentBridge(undefined, output, { version: 1, sessionId: "host", epoch: "epoch" });
42
+ bridge.initialize();
43
+ expect(bridge.request({ id: "s", type: "subagent_status", version: 1 })).toMatchObject({ id: "s", success: true, data: { available: false, capabilities: { snapshot: true, events: false } } });
44
+ expect(bridge.request({ type: "subagent_status", version: 2 as 1 })).toMatchObject({ success: false, error: expect.stringContaining("version") });
45
+ expect(bridge.request({ type: "subagent_status" } as never)).toMatchObject({ success: false });
46
+ expect(bridge.request({ type: "subagent_status", version: 1, id: "x".repeat(129) })).toMatchObject({ success: false });
47
+ expect(bridge.request({ type: "subagent_status", version: 1, method: "spawn" } as never)).toMatchObject({ success: false });
48
+ expect(output).toHaveBeenCalledOnce();
49
+ bridge.dispose();
50
+ });
51
+
52
+ it("observes direct native status after parent agent_end, coalesces progress and clears actual supervisor waits", async () => {
53
+ vi.useFakeTimers();
54
+ const n = native();
55
+ try {
56
+ n.state.asyncJobs.set("run-a", { asyncId: "run-a", asyncDir: "/managed/run", sessionId: n.state.currentSessionId!, status: "running", startedAt: 10,
57
+ steps: [{ agent: "worker", status: "running", index: 0, tokens: { input: 10, output: 5, total: 15 }, currentTool: "read" }] });
58
+ n.bus.emit("agent_end", { messages: [] });
59
+ const snapshot = n.status();
60
+ expect(snapshot).toMatchObject({ available: true, sessionId: "host", epoch: "bind-1", fleet: { totalActive: 1 }, runs: [{ runId: "run-a", status: "running" }] });
61
+ expect(snapshot.runs[0].children[0].outcome).toBeUndefined();
62
+ expect(snapshot.runs[0].processTerminal).toBeUndefined();
63
+ const revision = snapshot.revision;
64
+ n.waiting.push({ runId: "run-a", childIndex: 0, expectsReply: true });
65
+ const child = n.state.asyncJobs.get("run-a")!.steps![0];
66
+ child.activityState = "needs_attention";
67
+ child.currentTool = "contact_supervisor";
68
+ child.recentOutput = ["first", "latest"];
69
+ await vi.advanceTimersByTimeAsync(250);
70
+ expect(n.events.at(-1)!.snapshot.runs[0].children[0]).toMatchObject({ activity: "waiting_supervisor", progress: "latest" });
71
+ expect(n.events.at(-1)!.revision).toBe(revision + 1);
72
+ n.waiting.length = 0;
73
+ expect(n.status().runs[0].children[0].activity).toBe("needs_attention");
74
+ child.activityState = undefined;
75
+ child.currentTool = "bash";
76
+ expect(n.status().runs[0].children[0].activity).toBeUndefined();
77
+ // Run level follows the same projection: once the runner clears `activityState` (D12) no attention is reported.
78
+ const job = n.state.asyncJobs.get("run-a")!;
79
+ job.activityState = "needs_attention";
80
+ expect(n.status().runs[0].activity).toBe("needs_attention");
81
+ job.activityState = undefined;
82
+ expect(n.status().runs[0].activity).toBeUndefined();
83
+ const eventCount = n.events.length;
84
+ await vi.advanceTimersByTimeAsync(500);
85
+ expect(n.events).toHaveLength(eventCount);
86
+ } finally { n.dispose(); }
87
+ });
88
+
89
+ it("keeps explicit task outcome separate from lifecycle and process terminal evidence", () => {
90
+ const n = native();
91
+ try {
92
+ n.state.asyncJobs.set("a", { asyncId: "a", asyncDir: "/private/path", sessionId: n.state.currentSessionId!, status: "complete", endedAt: 30,
93
+ processTerminal: { version: 1, state: "unknown", reason: "observer-unavailable", runId: "a", runnerProcessInstanceId: "instance" },
94
+ steps: [{ agent: "worker", status: "completed", execution: { status: "failed", success: false, exitCode: 1 }, error: "acceptance rejected" }] });
95
+ const node = n.status().runs[0];
96
+ expect(node.status).toBe("complete");
97
+ expect(node.outcome).toBeUndefined();
98
+ expect(node.processTerminal).toEqual({ state: "unknown", reason: "observer-unavailable" });
99
+ expect(node.children[0].outcome).toEqual({ status: "failed", success: false, exitCode: 1 });
100
+ n.state.asyncJobs.get("a")!.steps![0].execution = { status: "completed", success: true, exitCode: 0 };
101
+ expect(n.status().runs[0].children[0].outcome?.success).toBe(true);
102
+ expect(n.status().runs[0].processTerminal?.state).toBe("unknown");
103
+ expect(JSON.stringify(n.status())).not.toContain("/private/path");
104
+ } finally { n.dispose(); }
105
+ });
106
+
107
+ it("bounds runs, child trees, malformed fields and path identity with explicit omissions", () => {
108
+ const n = native();
109
+ try {
110
+ for (let i = 0; i < 50; i++) n.state.asyncJobs.set(`r${i}`, { asyncId: `r${i}`, asyncDir: "/private", status: "running", sessionId: n.state.currentSessionId!, startedAt: 1,
111
+ steps: Array.from({ length: 30 }, (_, index) => ({ agent: "worker", index, status: "running", runId: "/not-an-id", recentOutput: ["x".repeat(4000)] })) });
112
+ const snapshot = n.status();
113
+ expect(isSubagentRpcProjection({ fleet: snapshot.fleet, runs: snapshot.runs, omitted: snapshot.omitted })).toBe(true);
114
+ expect(snapshot.runs.length).toBeLessThanOrEqual(32);
115
+ expect(snapshot.runs[0].children).toHaveLength(16);
116
+ expect(snapshot.omitted.runs).toBeGreaterThan(0);
117
+ expect(snapshot.omitted.children).toBeGreaterThan(0);
118
+ expect(snapshot.fleet.totalActive).toBe(1500);
119
+ expect(snapshot.fleet.omitted).toBe(1484);
120
+ expect(snapshot.runs[0].children[0].runId).toBeUndefined();
121
+ expect(snapshot.runs[0].children[0].progress!.length).toBeLessThanOrEqual(256);
122
+ expect(Buffer.byteLength(JSON.stringify(snapshot))).toBeLessThan(65536);
123
+ expect(isSubagentRpcProjection({ fleet: snapshot.fleet, omitted: snapshot.omitted, runs: [{ ...snapshot.runs[0], status: "fake" }] })).toBe(false);
124
+ } finally { n.dispose(); }
125
+ });
126
+
127
+ it("rejects malformed backend projections and stale generation callbacks", () => {
128
+ const bus = createEventBus();
129
+ const projection: SubagentRpcProjection = { fleet: { version: 1, entries: [], totalActive: 0, omitted: 0, topLevelAsyncCapacity: { used: 0, limit: 0 } }, runs: [], omitted: { runs: 0, children: 0 } };
130
+ let callback: ((p: SubagentRpcProjection | null) => void) | undefined;
131
+ const dispose = vi.fn();
132
+ const off = bus.on(SUBAGENT_OBSERVE_EVENT, raw => (raw as SubagentObserveRequest).accept({ snapshot: () => projection, subscribe: listener => { callback = listener; return dispose; } }));
133
+ const output = vi.fn();
134
+ const old = new RpcSubagentBridge(bus, output, { version: 1, sessionId: "host", epoch: "old" });
135
+ old.initialize();
136
+ const stale = callback!;
137
+ old.dispose();
138
+ const next = new RpcSubagentBridge(bus, output, { version: 1, sessionId: "new", epoch: "new" });
139
+ next.initialize();
140
+ stale({ ...projection, omitted: { runs: 1, children: 0 } });
141
+ expect(output).toHaveBeenCalledTimes(2);
142
+ expect(dispose).toHaveBeenCalledOnce();
143
+ callback!({ ...projection, runs: [null] } as never);
144
+ expect(next.request({ type: "subagent_status", version: 1 })).toMatchObject({ success: false, error: expect.stringContaining("Malformed") });
145
+ next.dispose(); off();
146
+ });
147
+
148
+ it("keeps tracker-cached descendants visible after foreground settlement, fences ownership and clears terminal routes", async () => {
149
+ const n = native();
150
+ const route = createNestedRoute(`observe-${randomUUID()}`);
151
+ const write = (state: "running" | "complete", ts: number, id = "nested-observer-child") => writeNestedEvent(route, {
152
+ type: state === "running" ? "subagent.nested.updated" : "subagent.nested.completed", ts, parentRunId: route.rootRunId,
153
+ child: { id, parentRunId: route.rootRunId, depth: 1, path: [{ runId: route.rootRunId }], state, agent: "worker", lastUpdate: ts },
154
+ });
155
+ const tracker = createRetainedNestedRouteTracker(n.state, { platform: "win32", pollIntervalMs: 10 });
156
+ try {
157
+ n.state.foregroundControls.set(route.rootRunId, { runId: route.rootRunId, sessionId: n.state.currentSessionId!, parentWorkflowRunId: "workflow-owner", mode: "single", startedAt: 10, updatedAt: 10 });
158
+ write("running", 100);
159
+ expect(retainLiveForegroundNestedRoute(n.state, route)).toBe(true);
160
+ n.state.foregroundControls.delete(route.rootRunId); // scheduling owner settled; descendants remain live
161
+ expect(n.status().runs).toMatchObject([{ runId: "nested-observer-child", workflowId: "workflow-owner", status: "running" }]);
162
+ expect(n.status().runs[0].outcome).toBeUndefined();
163
+ tracker.track(route.rootRunId);
164
+ n.state.asyncJobs.set("nested-observer-child", { asyncId: "nested-observer-child", asyncDir: "/native", sessionId: n.state.currentSessionId!, status: "running" });
165
+ expect(n.status().runs.filter(r => r.runId === "nested-observer-child")).toHaveLength(1);
166
+ n.state.asyncJobs.delete("nested-observer-child");
167
+ write("complete", 200);
168
+ await vi.waitFor(() => expect(n.state.retainedForegroundNestedChildren?.size).toBe(0));
169
+ expect(n.status().runs).toEqual([]);
170
+ write("running", 300, "nested-new-child");
171
+ expect(retainLiveForegroundNestedRoute(n.state, route)).toBe(true);
172
+ tracker.track(route.rootRunId);
173
+ n.state.currentSessionId = "/new/session";
174
+ expect(n.status().runs).toEqual([]);
175
+ await vi.waitFor(() => expect(n.state.retainedForegroundNestedRoutes?.size).toBe(0));
176
+ tracker.clear();
177
+ expect(n.state.retainedForegroundNestedChildren?.size).toBe(0);
178
+ } finally { tracker.clear(); n.dispose(); rmSync(dirname(route.eventSink), { recursive: true, force: true }); }
179
+ });
180
+
181
+ it("receives background progress from the existing artifact tracker without observer I/O", async () => {
182
+ const root = mkdtempSync(join(tmpdir(), "subagent-observe-"));
183
+ const n = native();
184
+ const runDir = join(root, "run-a"); mkdirSync(runDir);
185
+ const status = { runId: "run-a", sessionId: n.state.currentSessionId, mode: "single", state: "running", startedAt: Date.now(), pid: process.pid,
186
+ steps: [{ agent: "worker", status: "running", currentTool: "read", tokens: { input: 12, output: 3, total: 15 } }] };
187
+ writeFileSync(join(runDir, "status.json"), JSON.stringify(status));
188
+ const tracker = createAsyncJobTracker({ events: n.bus }, n.state, root, { pollIntervalMs: 20, platform: "win32", resultsDir: join(root, "results"), widgetEnabled: false, kill: () => true });
189
+ try {
190
+ tracker.handleStarted({ id: "run-a", asyncDir: runDir, sessionId: n.state.currentSessionId, agent: "worker", pid: process.pid });
191
+ await vi.waitFor(() => expect(n.status().runs[0].children[0].currentTool).toBe("read"));
192
+ n.bus.emit("agent_end", { messages: [] });
193
+ writeFileSync(join(runDir, "status.json"), JSON.stringify({ ...status, steps: [{ ...status.steps[0], currentTool: "bash" }] }));
194
+ await vi.waitFor(() => expect(n.status().runs[0]).toMatchObject({ status: "running", children: [{ currentTool: "bash" }] }));
195
+ expect(n.status().fleet.totalActive).toBe(1);
196
+ } finally { tracker.dispose(); for (const timer of n.state.cleanupTimers.values()) clearTimeout(timer); n.dispose(); rmSync(root, { recursive: true, force: true }); }
197
+ });
198
+ });
@@ -0,0 +1,75 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { computeProcessStartKey, processLiveness, processStartKey, psProcessStartKey, startKeyScheme } from "../../src/shared/process-identity.ts";
4
+
5
+ function resolvers() {
6
+ const calls: string[] = [];
7
+ return {
8
+ calls,
9
+ linux: (pid: number) => { calls.push(`linux:${pid}`); return undefined as string | undefined; },
10
+ ps: (pid: number) => { calls.push(`ps:${pid}`); return `ps:started-${pid}` as string | undefined; },
11
+ windows: (pid: number) => { calls.push(`win:${pid}`); return `win:started-${pid}` as string | undefined; },
12
+ };
13
+ }
14
+
15
+ describe("process start key", () => {
16
+ it("uses ps on darwin and other non-linux, non-windows unixes", () => {
17
+ for (const platform of ["darwin", "freebsd", "openbsd", "sunos"] as const) {
18
+ const fakes = resolvers();
19
+ assert.equal(computeProcessStartKey(42, platform, fakes), "ps:started-42", platform);
20
+ assert.deepEqual(fakes.calls, ["ps:42"], platform);
21
+ }
22
+ });
23
+
24
+ it("keeps Linux on /proc with a ps fallback and Windows on CIM", () => {
25
+ const fakes = resolvers();
26
+ assert.equal(computeProcessStartKey(7, "linux", { ...fakes, linux: () => "linux:123" }), "linux:123");
27
+ assert.equal(computeProcessStartKey(7, "linux", fakes), "ps:started-7");
28
+ assert.deepEqual(fakes.calls, ["linux:7", "ps:7"]);
29
+ const win = resolvers();
30
+ assert.equal(computeProcessStartKey(9, "win32", win), "win:started-9");
31
+ assert.deepEqual(win.calls, ["win:9"]);
32
+ });
33
+
34
+ it("returns undefined when the identity cannot be established", () => {
35
+ assert.equal(computeProcessStartKey(1, "darwin", { ps: () => undefined }), undefined);
36
+ assert.equal(computeProcessStartKey(1, "win32", { windows: () => undefined }), undefined);
37
+ });
38
+
39
+ it("caches only this process's own key", () => {
40
+ const own = processStartKey(process.pid);
41
+ assert.equal(processStartKey(process.pid), own);
42
+ if (process.platform === "darwin") assert.match(own ?? "", /^ps:\S/);
43
+ });
44
+
45
+ it("reads a real ps start key on darwin", { skip: process.platform !== "darwin" }, () => {
46
+ const key = psProcessStartKey(process.pid);
47
+ assert.match(key ?? "", /^ps:\S/);
48
+ assert.equal(startKeyScheme(key!), "ps");
49
+ assert.equal(psProcessStartKey(2 ** 22 + 12345), undefined);
50
+ });
51
+
52
+ it("names key schemes so unlike keys are never compared", () => {
53
+ assert.equal(startKeyScheme("linux:123"), "linux");
54
+ assert.equal(startKeyScheme("ps:Mon Oct 4 10:00:00 2026"), "ps");
55
+ assert.equal(startKeyScheme("win:2026"), "win");
56
+ assert.equal(startKeyScheme("opaque"), "");
57
+ });
58
+ });
59
+
60
+ describe("process liveness", () => {
61
+ const killing = (code: string | undefined) => () => { throw Object.assign(new Error(code ?? "boom"), code ? { code } : {}); };
62
+
63
+ it("maps kill(pid, 0) outcomes to alive, dead, or unknown", () => {
64
+ assert.equal(processLiveness(10, () => true), "alive");
65
+ assert.equal(processLiveness(10, killing("ESRCH")), "dead");
66
+ assert.equal(processLiveness(10, killing("EPERM")), "alive");
67
+ assert.equal(processLiveness(10, killing("EINVAL")), "unknown");
68
+ assert.equal(processLiveness(10, killing(undefined)), "unknown");
69
+ });
70
+
71
+ it("never probes non-positive or fractional pids (kill(0) would signal the process group)", () => {
72
+ const probe = () => { throw new Error("must not be called"); };
73
+ for (const pid of [0, -1, 1.5, Number.NaN]) assert.equal(processLiveness(pid, probe), "unknown");
74
+ });
75
+ });