import { describe, expect, it } from "@effect/vitest"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import * as ProcessResourceMonitor from "./ProcessResourceMonitor.ts"; describe("ProcessResourceMonitor", () => { it.effect("samples the server root process and descendants", () => Effect.sync(() => { const sampledAt = DateTime.makeUnsafe("2026-05-05T10:00:00.000Z"); const samples = ProcessResourceMonitor.collectMonitoredSamples({ serverPid: 100, sampledAt, sampledAtMs: DateTime.toEpochMillis(sampledAt), rows: [ { pid: 100, ppid: 1, pgid: 100, status: "S", cpuPercent: 2, rssBytes: 1_000, elapsed: "01:00", command: "t3 server", }, { pid: 101, ppid: 100, pgid: 100, status: "S", cpuPercent: 10, rssBytes: 2_000, elapsed: "00:20", command: "codex app-server", }, { pid: 102, ppid: 101, pgid: 100, status: "R", cpuPercent: 50, rssBytes: 3_000, elapsed: "00:05", command: "rg needle", }, { pid: 200, ppid: 1, pgid: 200, status: "R", cpuPercent: 99, rssBytes: 9_000, elapsed: "00:05", command: "unrelated", }, ], }); expect(samples.map((sample) => sample.pid)).toEqual([100, 101, 102]); expect(samples.map((sample) => sample.depth)).toEqual([0, 1, 2]); expect(samples[0]?.isServerRoot).toBe(true); expect(samples[1]?.isServerRoot).toBe(false); }), ); it.effect("rolls samples up by process and CPU time", () => Effect.sync(() => { const firstAt = DateTime.makeUnsafe("2026-05-05T10:00:00.000Z"); const secondAt = DateTime.makeUnsafe("2026-05-05T10:00:05.000Z"); const samples = [ ...ProcessResourceMonitor.collectMonitoredSamples({ serverPid: 100, sampledAt: firstAt, sampledAtMs: DateTime.toEpochMillis(firstAt), rows: [ { pid: 100, ppid: 1, pgid: 100, status: "S", cpuPercent: 10, rssBytes: 1_000, elapsed: "01:00", command: "t3 server", }, ], }), ...ProcessResourceMonitor.collectMonitoredSamples({ serverPid: 100, sampledAt: secondAt, sampledAtMs: DateTime.toEpochMillis(secondAt), rows: [ { pid: 100, ppid: 1, pgid: 100, status: "S", cpuPercent: 30, rssBytes: 2_000, elapsed: "01:05", command: "t3 server", }, ], }), ]; const result = ProcessResourceMonitor.aggregateProcessResourceHistory({ samples, readAt: secondAt, readAtMs: DateTime.toEpochMillis(secondAt), windowMs: 60_000, bucketMs: 10_000, lastFailure: null, }); expect(Option.isNone(result.error)).toBe(true); expect(result.topProcesses).toHaveLength(1); expect(result.topProcesses[0]?.avgCpuPercent).toBe(20); expect(result.topProcesses[0]?.maxCpuPercent).toBe(30); expect(result.topProcesses[0]?.cpuSecondsApprox).toBe(2); expect(result.totalCpuSecondsApprox).toBe(2); expect(result.buckets.some((bucket) => bucket.maxCpuPercent === 30)).toBe(true); }), ); it.effect("keeps a process grouped when elapsed time drifts between samples", () => Effect.sync(() => { const firstAt = DateTime.makeUnsafe("2026-05-05T10:00:00.400Z"); const secondAt = DateTime.makeUnsafe("2026-05-05T10:00:05.900Z"); const samples = [ ...ProcessResourceMonitor.collectMonitoredSamples({ serverPid: 100, sampledAt: firstAt, sampledAtMs: DateTime.toEpochMillis(firstAt), rows: [ { pid: 100, ppid: 1, pgid: 100, status: "S", cpuPercent: 1, rssBytes: 1_000, elapsed: "01:00", command: "t3 server", }, ], }), ...ProcessResourceMonitor.collectMonitoredSamples({ serverPid: 100, sampledAt: secondAt, sampledAtMs: DateTime.toEpochMillis(secondAt), rows: [ { pid: 100, ppid: 1, pgid: 100, status: "S", cpuPercent: 2, rssBytes: 2_000, elapsed: "01:06", command: "t3 server", }, ], }), ]; const result = ProcessResourceMonitor.aggregateProcessResourceHistory({ samples, readAt: secondAt, readAtMs: DateTime.toEpochMillis(secondAt), windowMs: 60_000, bucketMs: 10_000, lastFailure: null, }); expect(result.topProcesses).toHaveLength(1); expect(result.topProcesses[0]?.isServerRoot).toBe(true); expect(result.topProcesses[0]?.sampleCount).toBe(2); expect(result.topProcesses[0]?.maxRssBytes).toBe(2_000); }), ); it.effect("returns all process summaries in the selected window", () => Effect.sync(() => { const sampledAt = DateTime.makeUnsafe("2026-05-05T10:00:00.000Z"); const samples = ProcessResourceMonitor.collectMonitoredSamples({ serverPid: 100, sampledAt, sampledAtMs: DateTime.toEpochMillis(sampledAt), rows: [ { pid: 100, ppid: 1, pgid: 100, status: "S", cpuPercent: 1, rssBytes: 1_000, elapsed: "01:00", command: "t3 server", }, ...Array.from({ length: 35 }, (_, index) => ({ pid: 200 + index, ppid: index === 0 ? 100 : 199 + index, pgid: 100, status: "S", cpuPercent: 35 - index, rssBytes: 2_000 + index, elapsed: "00:10", command: `worker ${index}`, })), ], }); const result = ProcessResourceMonitor.aggregateProcessResourceHistory({ samples, readAt: sampledAt, readAtMs: DateTime.toEpochMillis(sampledAt), windowMs: 60_000, bucketMs: 10_000, lastFailure: null, }); expect(result.topProcesses).toHaveLength(36); expect(result.topProcesses.some((process) => process.command === "worker 34")).toBe(true); }), ); it.effect("exposes bounded failure diagnostics while retaining the exact cause", () => Effect.sync(() => { const readAt = DateTime.makeUnsafe("2026-05-05T10:00:00.000Z"); const cause = new Error("stderr included credential=secret-value"); const failure = new ProcessResourceMonitor.ProcessResourceSamplingError({ failureTag: "ProcessDiagnosticsQueryFailedError", cause, }); const result = ProcessResourceMonitor.aggregateProcessResourceHistory({ samples: [], readAt, readAtMs: DateTime.toEpochMillis(readAt), windowMs: 60_000, bucketMs: 10_000, lastFailure: failure, }); expect(failure.cause).toBe(cause); expect(Option.getOrThrow(result.error)).toEqual({ failureTag: "ProcessDiagnosticsQueryFailedError", message: "Failed to sample process resources (ProcessDiagnosticsQueryFailedError).", }); expect(Option.getOrThrow(result.error).message).not.toContain("secret-value"); }), ); });