Skip to content
Source
Sign inSign up
Open History to see the last commit

ProcessGroupMetrics.test.ts

120 lines4.4 KB

1import { assert, describe, it } from "@effect/vitest";
2import type { ResourceTelemetryProcess, ResourceTelemetrySnapshot } from "@t3tools/contracts";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Logger from "effect/Logger";
6import * as References from "effect/References";
7import * as Ref from "effect/Ref";
8import * as TestClock from "effect/testing/TestClock";
9​
10import * as ServerConfig from "../config.ts";
11import * as ProcessGroupMetrics from "./ProcessGroupMetrics.ts";
12import * as ResourceTelemetry from "./ResourceTelemetry.ts";
13​
14const { groupProcesses } = ProcessGroupMetrics;
15​
16const process = (
17 pid: number,
18 ppid: number,
19 name: string,
20 category: ResourceTelemetryProcess["category"] = "server-child",
21 command = name,
22) =>
23 ({
24 identity: { pid, startTimeMs: 1 },
25 ppid,
26 name,
27 command,
28 category,
29 residentBytes: pid * 1_000,
30 cpuPercent: pid,
31 }) as ResourceTelemetryProcess;
32​
33describe("groupProcesses", () => {
34 it("sums each direct child of the server with its whole subtree", () => {
35 const totals = groupProcesses([
36 process(10, 1, "node", "server"),
37 process(11, 10, "t3-resource-mon", "resource-monitor"),
38 process(20, 10, "claude"),
39 process(21, 20, "bash"),
40 process(22, 21, "cargo"),
41 process(30, 10, "codex"),
42 process(40, 10, "zsh"),
43 process(50, 10, "chrome-headless"),
44 process(60, 10, "git", "server-child", "git checkout claude/fix"),
45 process(70, 99, "orphan"),
46 // An older Claude package runs as a script under node.
47 process(
48 80,
49 10,
50 "node",
51 "server-child",
52 "node /opt/node_modules/@anthropic-ai/claude-code/cli.js",
53 ),
54 process(90, 10, "node", "server-child", "node /srv/tools/report.js"),
55 ]);
56​
57 assert.deepStrictEqual(Object.fromEntries(totals), {
58 server: { residentBytes: 10_000, cpuPercent: 10, count: 1 },
59 "resource-monitor": { residentBytes: 11_000, cpuPercent: 11, count: 1 },
60 // The agent's shell and the build it ran belong to the agent, not the terminal group.
61 claude: { residentBytes: 143_000, cpuPercent: 143, count: 4 },
62 codex: { residentBytes: 30_000, cpuPercent: 30, count: 1 },
63 terminal: { residentBytes: 40_000, cpuPercent: 40, count: 1 },
64 browser: { residentBytes: 50_000, cpuPercent: 50, count: 1 },
65 // `git` whose arguments mention claude, a parentless process, and a node script.
66 other: { residentBytes: 220_000, cpuPercent: 220, count: 3 },
67 });
68 });
69});
70​
71describe("ProcessGroupMetrics.layer", () => {
72 it.effect("logs the first failed sample and the recovery, not every repeat", () => {
73 const logs: Array<string> = [];
74 const logger = Logger.make(({ logLevel, message }) => {
75 logs.push(`${logLevel.toUpperCase()} ${String([message].flat()[0])}`);
76 });
77 return Effect.gen(function* () {
78 const outcomes = yield* Ref.make<ReadonlyArray<"fail" | "ok">>(["fail", "fail", "ok"]);
79 const refresh = Ref.modify(outcomes, ([next = "ok", ...rest]) => [next, rest] as const).pipe(
80 Effect.flatMap((next) =>
81 next === "fail"
82 ? Effect.fail(
83 new ResourceTelemetry.ResourceTelemetryRefreshFailed({
84 operation: "refresh",
85 cause: new Error("Resource monitor binary was not found for darwin/arm64."),
86 }),
87 )
88 : Effect.succeed({ processes: [] } as unknown as ResourceTelemetrySnapshot),
89 ),
90 );
91 const telemetry = { refresh } as unknown as ResourceTelemetry.ResourceTelemetry["Service"];
92​
93 yield* Layer.build(
94 ProcessGroupMetrics.layer.pipe(
95 Layer.provide(Layer.succeed(ResourceTelemetry.ResourceTelemetry, telemetry)),
96 Layer.provide(
97 ServerConfig.layer({
98 otlpMetricsUrl: "http://localhost:4318/v1/metrics",
99 } as ServerConfig.ServerConfig["Service"]),
100 ),
101 ),
102 );
103 yield* TestClock.adjust("3 minutes");
104​
105 assert.deepStrictEqual(logs, [
106 "WARN process group metrics are not exported: sample failed",
107 "DEBUG process group metrics sample failed",
108 "INFO process group metrics sample recovered",
109 ]);
110 }).pipe(
111 Effect.scoped,
112 Effect.provide(
113 Layer.mergeAll(
114 Logger.layer([logger], { mergeWithExisting: false }),
115 Layer.succeed(References.MinimumLogLevel, "Debug"),
116 ),
117 ),
118 );
119 });
120});