Open History to see the last commit
1import { describe, expect, it } from "@effect/vitest";
2import type {
3 DesktopHostTelemetrySnapshot,
4 ResourceMonitorSnapshotEvent,
5} from "@t3tools/contracts";
6import * as DateTime from "effect/DateTime";
7import * as Effect from "effect/Effect";
8import * as Layer from "effect/Layer";
9import * as Option from "effect/Option";
10import * as Stream from "effect/Stream";
11
12import * as DesktopTelemetryReceiver from "../resourceTelemetry/DesktopTelemetryReceiver.ts";
13import * as NativeTelemetryClient from "../resourceTelemetry/NativeTelemetryClient.ts";
14import * as ResourceAttribution from "../resourceTelemetry/ResourceAttribution.ts";
15import * as ResourceTelemetry from "../resourceTelemetry/ResourceTelemetry.ts";
16import * as ProcessDiagnostics from "./ProcessDiagnostics.ts";
17
18// ProcessDiagnostics treats process.pid as the server, so the fixture PID is an
19// offset from it. A fixed PID collides whenever the test worker is assigned it.
20const SERVER_PID = process.pid;
21const CHILD_PID = SERVER_PID + 1;
22
23function makeNativeSnapshot(
24 processes: ResourceMonitorSnapshotEvent["processes"],
25): ResourceMonitorSnapshotEvent {
26 return {
27 version: 3,
28 type: "snapshot",
29 sequence: 1,
30 sampledAtUnixMs: DateTime.toEpochMillis(DateTime.makeUnsafe("2026-05-05T10:00:00.000Z")),
31 collectionDurationMicros: 250,
32 scannedProcessCount: processes.length,
33 retainedProcessCount: processes.length,
34 inaccessibleProcessCount: 0,
35 processes,
36 };
37}
38
39function layerTelemetryFor(
40 snapshot: ResourceMonitorSnapshotEvent,
41 desktopSnapshot?: DesktopHostTelemetrySnapshot,
42) {
43 const layerNative = NativeTelemetryClient.layerTest({
44 sampleNow: Effect.succeed({ generation: 0, snapshot }),
45 health: Effect.succeed({
46 status: "healthy",
47 hello: Option.none(),
48 lastSampleAt: Option.some(DateTime.makeUnsafe(snapshot.sampledAtUnixMs)),
49 lastError: Option.none(),
50 restartCount: 0,
51 sampleIntervalMs: 1_000,
52 }),
53 });
54 const layerDesktop = desktopSnapshot
55 ? DesktopTelemetryReceiver.layerTest({
56 latest: Effect.succeedSome(desktopSnapshot),
57 health: Effect.succeed({
58 status: "healthy",
59 lastSampleAt: Option.some(DateTime.makeUnsafe(desktopSnapshot.sampledAtUnixMs)),
60 lastError: Option.none(),
61 }),
62 })
63 : DesktopTelemetryReceiver.layerTest();
64 return ResourceTelemetry.layer.pipe(
65 Layer.provide(Layer.mergeAll(layerNative, layerDesktop, ResourceAttribution.layer)),
66 );
67}
68
69describe("ProcessDiagnostics", () => {
70 it.effect("rejects stale process identities before signaling", () =>
71 Effect.gen(function* () {
72 const snapshot = makeNativeSnapshot([]);
73 const layerTelemetry = layerTelemetryFor(snapshot);
74 const layer = ProcessDiagnostics.layer.pipe(Layer.provide(layerTelemetry));
75
76 const result = yield* Effect.service(ProcessDiagnostics.ProcessDiagnostics).pipe(
77 Effect.flatMap((processDiagnostics) =>
78 processDiagnostics.signal({
79 pid: CHILD_PID,
80 startTimeMs: 2_000,
81 signal: "SIGINT",
82 }),
83 ),
84 Effect.provide(layer),
85 );
86
87 expect(result).toEqual({
88 pid: CHILD_PID,
89 signal: "SIGINT",
90 signaled: false,
91 message: Option.some(
92 `Process ${CHILD_PID} no longer matches the selected process identity.`,
93 ),
94 });
95 }),
96 );
97
98 it.effect("refuses to signal when a fresh identity check cannot be collected", () =>
99 Effect.gen(function* () {
100 const snapshot = makeNativeSnapshot([
101 {
102 pid: CHILD_PID,
103 ppid: SERVER_PID,
104 startTimeMs: 2_000,
105 runTimeMs: 4_000,
106 name: "agent",
107 command: "codex app-server",
108 status: "Running",
109 cpuPercent: 1.5,
110 cpuTimeMs: 60,
111 residentBytes: 2_048,
112 virtualBytes: 4_096,
113 ioReadBytes: 300,
114 ioWriteBytes: 400,
115 ioSemantics: "storage",
116 },
117 ]);
118 const staleTelemetry = yield* Effect.service(ResourceTelemetry.ResourceTelemetry).pipe(
119 Effect.flatMap((telemetry) => telemetry.latest),
120 Effect.provide(layerTelemetryFor(snapshot)),
121 );
122 const layerTelemetry = Layer.succeed(
123 ResourceTelemetry.ResourceTelemetry,
124 ResourceTelemetry.ResourceTelemetry.of({
125 latest: Effect.succeed(staleTelemetry),
126 changes: Stream.empty,
127 subscribe: Effect.die("unused"),
128 readHistory: () => Effect.die("unused"),
129 refresh: Effect.fail(
130 new ResourceTelemetry.ResourceTelemetryRefreshFailed({
131 operation: "refresh",
132 cause: new Error("collector unavailable"),
133 }),
134 ),
135 retry: Effect.die("unused"),
136 }),
137 );
138 const layer = ProcessDiagnostics.layer.pipe(Layer.provide(layerTelemetry));
139
140 const result = yield* Effect.service(ProcessDiagnostics.ProcessDiagnostics).pipe(
141 Effect.flatMap((processDiagnostics) =>
142 processDiagnostics.signal({
143 pid: CHILD_PID,
144 startTimeMs: 2_000,
145 signal: "SIGINT",
146 }),
147 ),
148 Effect.provide(layer),
149 );
150
151 expect(result).toEqual({
152 pid: CHILD_PID,
153 signal: "SIGINT",
154 signaled: false,
155 message: Option.some(
156 `Could not refresh process ${CHILD_PID}; refusing to signal a stale identity.`,
157 ),
158 });
159 }),
160 );
161
162 it.effect("rejects Electron processes as signal targets", () =>
163 Effect.gen(function* () {
164 const sampledAtUnixMs = DateTime.toEpochMillis(
165 DateTime.makeUnsafe("2026-05-05T10:00:00.000Z"),
166 );
167 const snapshot = makeNativeSnapshot([
168 {
169 pid: CHILD_PID,
170 ppid: 1,
171 startTimeMs: 2_000,
172 runTimeMs: 4_000,
173 name: "electron",
174 command: "electron",
175 status: "Running",
176 cpuPercent: 1.5,
177 cpuTimeMs: 60,
178 residentBytes: 2_048,
179 virtualBytes: 4_096,
180 ioReadBytes: 300,
181 ioWriteBytes: 400,
182 ioSemantics: "storage",
183 },
184 ]);
185 const sampledAt = DateTime.makeUnsafe(sampledAtUnixMs);
186 const layerTelemetry = layerTelemetryFor(snapshot, {
187 version: 1,
188 type: "desktopTelemetry",
189 sequence: 1,
190 sampledAtUnixMs,
191 electronPid: CHILD_PID,
192 power: {
193 source: "electron-main",
194 idle: "false",
195 idleSeconds: 0,
196 locked: "false",
197 suspended: false,
198 onBattery: "false",
199 lowPowerMode: "unknown",
200 thermalState: "nominal",
201 stale: false,
202 updatedAt: sampledAt,
203 },
204 speedLimitPercent: Option.none(),
205 electronProcesses: [
206 {
207 pid: CHILD_PID,
208 creationTimeMs: 2_000,
209 type: "Browser",
210 name: "electron",
211 cpuPercent: 1.5,
212 idleWakeupsPerSecond: 0,
213 workingSetBytes: 2_048,
214 peakWorkingSetBytes: 2_048,
215 },
216 ],
217 });
218 const layer = ProcessDiagnostics.layer.pipe(Layer.provide(layerTelemetry));
219
220 const result = yield* Effect.service(ProcessDiagnostics.ProcessDiagnostics).pipe(
221 Effect.flatMap((processDiagnostics) =>
222 processDiagnostics.signal({
223 pid: CHILD_PID,
224 startTimeMs: 2_000,
225 signal: "SIGKILL",
226 }),
227 ),
228 Effect.provide(layer),
229 );
230
231 expect(result).toEqual({
232 pid: CHILD_PID,
233 signal: "SIGKILL",
234 signaled: false,
235 message: Option.some(`Process ${CHILD_PID} is not a signalable T3 backend descendant.`),
236 });
237
238 const diagnostics = yield* Effect.service(ProcessDiagnostics.ProcessDiagnostics).pipe(
239 Effect.flatMap((processDiagnostics) => processDiagnostics.read),
240 Effect.provide(layer),
241 );
242 expect(diagnostics.processes).toEqual([]);
243 expect(diagnostics.processCount).toBe(0);
244 expect(diagnostics.totalCpuPercent).toBe(0);
245 expect(diagnostics.totalRssBytes).toBe(0);
246 }),
247 );
248});