1import { describe, expect, it } from "@effect/vitest";
2import * as DateTime from "effect/DateTime";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6import * as Stream from "effect/Stream";
7
8import * as ResourceTelemetry from "../resourceTelemetry/ResourceTelemetry.ts";
9import type { ResourceTelemetryHistoryWithLegacyBuckets } from "../resourceTelemetry/ResourceTelemetryHistory.ts";
10import * as ProcessResourceMonitor from "./ProcessResourceMonitor.ts";
11
12describe("ProcessResourceMonitor", () => {
13 it.effect("projects resource telemetry history into the legacy diagnostics contract", () =>
14 Effect.gen(function* () {
15 const readAt = DateTime.makeUnsafe("2026-05-05T10:00:00.000Z");
16 const history: ResourceTelemetryHistoryWithLegacyBuckets = {
17 readAt,
18 windowMs: 60_000,
19 bucketMs: 10_000,
20 sampleIntervalMs: 1_000,
21 retainedSampleCount: 2,
22 buckets: [
23 {
24 startedAt: DateTime.makeUnsafe("2026-05-05T09:59:50.000Z"),
25 endedAt: readAt,
26 avgCpuPercent: 15,
27 maxCpuPercent: 25,
28 maxRssBytes: 4_096,
29 ioReadBytes: 1_024,
30 ioWriteBytes: 2_048,
31 maxProcessCount: 2,
32 },
33 ],
34 legacyBackendBuckets: [
35 {
36 startedAt: DateTime.makeUnsafe("2026-05-05T09:59:50.000Z"),
37 endedAt: readAt,
38 avgCpuPercent: 5,
39 maxCpuPercent: 8,
40 maxRssBytes: 4_096,
41 ioReadBytes: 1_024,
42 ioWriteBytes: 2_048,
43 maxProcessCount: 1,
44 },
45 ],
46 topProcesses: [
47 {
48 identity: { pid: process.pid, startTimeMs: 100 },
49 ppid: 1,
50 depth: 0,
51 name: "node",
52 command: "t3 server",
53 category: "server",
54 firstSeenAt: DateTime.makeUnsafe("2026-05-05T09:59:55.000Z"),
55 lastSeenAt: readAt,
56 currentCpuPercent: 5,
57 avgCpuPercent: 4,
58 maxCpuPercent: 8,
59 cpuTimeMs: 1_500,
60 currentRssBytes: 2_048,
61 peakRssBytes: 4_096,
62 ioReadBytes: 1_024,
63 ioWriteBytes: 2_048,
64 ioSemantics: "storage",
65 sampleCount: 2,
66 },
67 {
68 identity: { pid: 5_000, startTimeMs: 200 },
69 ppid: 1,
70 depth: 0,
71 name: "electron",
72 command: "electron",
73 category: "electron-main",
74 firstSeenAt: DateTime.makeUnsafe("2026-05-05T09:59:55.000Z"),
75 lastSeenAt: readAt,
76 currentCpuPercent: 50,
77 avgCpuPercent: 40,
78 maxCpuPercent: 80,
79 cpuTimeMs: 15_000,
80 currentRssBytes: 20_480,
81 peakRssBytes: 40_960,
82 ioReadBytes: 10_240,
83 ioWriteBytes: 20_480,
84 ioSemantics: "storage",
85 sampleCount: 2,
86 },
87 ],
88 health: {
89 native: {
90 status: "degraded",
91 lastSampleAt: Option.some(readAt),
92 lastError: Option.some("collector stalled"),
93 },
94 desktop: {
95 status: "healthy",
96 lastSampleAt: Option.some(readAt),
97 lastError: Option.none(),
98 },
99 sidecarVersion: Option.some("0.1.0"),
100 sidecarPid: Option.some(9_000),
101 restartCount: 1,
102 collectionDurationMicros: 250,
103 scannedProcessCount: 80,
104 retainedProcessCount: 2,
105 inaccessibleProcessCount: 0,
106 },
107 };
108 const telemetry: ResourceTelemetry.ResourceTelemetry["Service"] = {
109 latest: Effect.die("unused"),
110 changes: Stream.empty,
111 subscribe: Effect.die("unused"),
112 readHistory: () => Effect.succeed(history),
113 refresh: Effect.die("unused"),
114 validateProcessIdentity: () => Effect.die("unused"),
115 retry: Effect.die("unused"),
116 };
117 const layer = ProcessResourceMonitor.layer.pipe(
118 Layer.provide(
119 Layer.succeed(
120 ResourceTelemetry.ResourceTelemetry,
121 ResourceTelemetry.ResourceTelemetry.of(telemetry),
122 ),
123 ),
124 );
125
126 const result = yield* Effect.service(ProcessResourceMonitor.ProcessResourceMonitor).pipe(
127 Effect.flatMap((monitor) =>
128 monitor.readHistory({
129 windowMs: 60_000,
130 bucketMs: 10_000,
131 }),
132 ),
133 Effect.provide(layer),
134 );
135
136 expect(result.totalCpuSecondsApprox).toBe(1.5);
137 expect(result.topProcesses).toEqual([
138 {
139 processKey: `${process.pid}:100`,
140 pid: process.pid,
141 ppid: 1,
142 command: "t3 server",
143 depth: 0,
144 isServerRoot: true,
145 firstSeenAt: DateTime.makeUnsafe("2026-05-05T09:59:55.000Z"),
146 lastSeenAt: readAt,
147 currentCpuPercent: 5,
148 avgCpuPercent: 4,
149 maxCpuPercent: 8,
150 cpuSecondsApprox: 1.5,
151 currentRssBytes: 2_048,
152 maxRssBytes: 4_096,
153 sampleCount: 2,
154 },
155 ]);
156 expect(result.buckets[0]).toMatchObject({
157 avgCpuPercent: 5,
158 maxCpuPercent: 8,
159 maxRssBytes: 4_096,
160 maxProcessCount: 1,
161 });
162 expect(result.error).toEqual(
163 Option.some({
164 failureTag: "ProcessDiagnosticsQueryFailedError",
165 message: "collector stalled",
166 }),
167 );
168 }),
169 );
170});