import { assert, describe, it } from "@effect/vitest"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import { ChildProcessSpawner } from "effect/unstable/process"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as ProcessDiagnostics from "./ProcessDiagnostics.ts"; const encoder = new TextEncoder(); const encodeUnknownJson = Schema.encodeUnknownSync(Schema.UnknownFromJsonString); function mockHandle(result: { readonly stdout?: string; readonly stderr?: string; readonly code?: number; }) { return ChildProcessSpawner.makeHandle({ pid: ChildProcessSpawner.ProcessId(1), exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(result.code ?? 0)), isRunning: Effect.succeed(false), kill: () => Effect.void, unref: Effect.succeed(Effect.void), stdin: Sink.drain, stdout: Stream.make(encoder.encode(result.stdout ?? "")), stderr: Stream.make(encoder.encode(result.stderr ?? "")), all: Stream.empty, getInputFd: () => Sink.drain, getOutputFd: () => Stream.empty, }); } describe("ProcessDiagnostics", () => { it.effect("parses POSIX ps rows with full commands", () => Effect.sync(() => { const rows = ProcessDiagnostics.parsePosixProcessRows( [ " 10 1 10 Ss 0.0 1024 01:02.03 /usr/bin/node server.js", " 11 10 10 S+ 12.5 20480 00:04 codex app-server --config /tmp/one two", ].join("\n"), ); assert.deepStrictEqual(rows, [ { pid: 10, ppid: 1, pgid: 10, status: "Ss", cpuPercent: 0, rssBytes: 1024 * 1024, elapsed: "01:02.03", command: "/usr/bin/node server.js", }, { pid: 11, ppid: 10, pgid: 10, status: "S+", cpuPercent: 12.5, rssBytes: 20480 * 1024, elapsed: "00:04", command: "codex app-server --config /tmp/one two", }, ]); }), ); it.effect("aggregates only descendants of the server process", () => Effect.sync(() => { const diagnostics = ProcessDiagnostics.aggregateProcessDiagnostics({ serverPid: 100, readAt: DateTime.makeUnsafe("2026-05-05T10:00:00.000Z"), rows: [ { pid: 100, ppid: 1, pgid: 100, status: "S", cpuPercent: 0, rssBytes: 1_000, elapsed: "01:00", command: "t3 server", }, { pid: 101, ppid: 100, pgid: 100, status: "S", cpuPercent: 1.5, rssBytes: 2_000, elapsed: "00:20", command: "codex app-server", }, { pid: 102, ppid: 101, pgid: 100, status: "R", cpuPercent: 3.25, rssBytes: 4_000, elapsed: "00:05", command: "git status", }, { pid: 200, ppid: 1, pgid: 200, status: "S", cpuPercent: 99, rssBytes: 8_000, elapsed: "00:01", command: "unrelated", }, { pid: 201, ppid: 100, pgid: 100, status: "R", cpuPercent: 9, rssBytes: 9_000, elapsed: "00:00", command: "ps -axo pid=,ppid=,pgid=,stat=,pcpu=,rss=,etime=,command=", }, ], }); assert.equal(diagnostics.serverPid, 100); assert.equal(DateTime.formatIso(diagnostics.readAt), "2026-05-05T10:00:00.000Z"); assert.equal(diagnostics.processCount, 2); assert.equal(diagnostics.totalRssBytes, 6_000); assert.equal(diagnostics.totalCpuPercent, 4.75); assert.deepStrictEqual( diagnostics.processes.map((process) => process.pid), [101, 102], ); assert.deepStrictEqual( diagnostics.processes.map((process) => process.depth), [0, 1], ); assert.equal(Option.getOrNull(diagnostics.processes[0]!.pgid), 100); assert.deepStrictEqual(diagnostics.processes[0]?.childPids, [102]); }), ); it.effect("preserves ascending sibling order for nested descendants", () => Effect.sync(() => { const diagnostics = ProcessDiagnostics.aggregateProcessDiagnostics({ serverPid: 100, readAt: DateTime.makeUnsafe("2026-05-05T10:00:00.000Z"), rows: [ { pid: 101, ppid: 100, pgid: 100, status: "S", cpuPercent: 0, rssBytes: 100, elapsed: "00:10", command: "agent", }, { pid: 103, ppid: 101, pgid: 100, status: "S", cpuPercent: 0, rssBytes: 100, elapsed: "00:10", command: "child-b", }, { pid: 102, ppid: 101, pgid: 100, status: "S", cpuPercent: 0, rssBytes: 100, elapsed: "00:10", command: "child-a", }, ], }); assert.deepStrictEqual( diagnostics.processes.map((process) => process.pid), [101, 102, 103], ); }), ); it.effect("queries processes through the ChildProcessSpawner service", () => Effect.gen(function* () { const commands: Array<{ readonly command: string; readonly args: ReadonlyArray }> = []; const spawnerLayer = Layer.succeed( ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make((command) => { const childProcess = command as unknown as { readonly command: string; readonly args: ReadonlyArray; }; commands.push({ command: childProcess.command, args: childProcess.args }); return Effect.succeed( mockHandle({ stdout: [ ` ${process.pid} 1 ${process.pid} Ss 0.0 1024 01:02.03 t3 server`, ` 4242 ${process.pid} ${process.pid} S 1.5 2048 00:04 agent`, ].join("\n"), }), ); }), ); const layer = ProcessDiagnostics.layer.pipe(Layer.provide(spawnerLayer)); const diagnostics = yield* Effect.service(ProcessDiagnostics.ProcessDiagnostics).pipe( Effect.flatMap((pd) => pd.read), Effect.provide(layer), ); assert.deepStrictEqual( diagnostics.processes.map((process) => process.pid), [4242], ); assert.deepStrictEqual(commands, [ { command: "ps", args: ["-axo", "pid=,ppid=,pgid=,stat=,pcpu=,rss=,etime=,command="], }, ]); }), ); it.effect("keeps bounded command diagnostics when the process query exits unsuccessfully", () => Effect.gen(function* () { const spawnerLayer = Layer.succeed( ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make(() => Effect.succeed( mockHandle({ code: 17, stdout: "partial process output", stderr: "process access denied", }), ), ), ); const error = yield* ProcessDiagnostics.readProcessRows.pipe( Effect.provide(spawnerLayer), Effect.provideService(HostProcessPlatform, "linux"), Effect.flip, ); if (error._tag !== "ProcessDiagnosticsQueryFailedError") { assert.fail(`Expected ProcessDiagnosticsQueryFailedError, got ${error._tag}`); } assert.deepStrictEqual( { command: error.command, argCount: error.argCount, cwd: error.cwd, exitCode: error.exitCode, stdoutBytes: error.stdoutBytes, stderrBytes: error.stderrBytes, stdoutTruncated: error.stdoutTruncated, stderrTruncated: error.stderrTruncated, }, { command: "ps", argCount: 2, cwd: process.cwd(), exitCode: 17, stdoutBytes: 22, stderrBytes: 21, stdoutTruncated: false, stderrTruncated: false, }, ); assert.equal( error.message, `Process diagnostics query 'ps' failed with exit code 17 in '${process.cwd()}'.`, ); }), ); it.effect("decodes Windows process JSON through Schema and skips invalid records", () => Effect.gen(function* () { const commands: Array<{ readonly command: string; readonly args: ReadonlyArray }> = []; const spawnerLayer = Layer.succeed( ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make((command) => { const childProcess = command as unknown as { readonly command: string; readonly args: ReadonlyArray; }; commands.push({ command: childProcess.command, args: childProcess.args }); return Effect.succeed( mockHandle({ stdout: encodeUnknownJson([ { ProcessId: process.pid, ParentProcessId: 1, Name: "node.exe", CommandLine: "t3 server", Status: "", WorkingSetSize: 1024, PercentProcessorTime: 0, }, { ProcessId: 4242, ParentProcessId: process.pid, Name: "agent.exe", CommandLine: null, Status: "Running", WorkingSetSize: 2048.6, PercentProcessorTime: 1.25, }, { ProcessId: "invalid", ParentProcessId: process.pid, Name: "bad.exe", }, ]), }), ); }), ); const rows = yield* ProcessDiagnostics.readProcessRows.pipe( Effect.provide(spawnerLayer), Effect.provideService(HostProcessPlatform, "win32"), ); assert.deepStrictEqual(commands, [ { command: "powershell.exe", args: [ "-NoProfile", "-NonInteractive", "-Command", [ "$processes = Get-CimInstance Win32_Process | ForEach-Object {", '$perf = Get-CimInstance Win32_PerfFormattedData_PerfProc_Process -Filter "IDProcess = $($_.ProcessId)" -ErrorAction SilentlyContinue;', "[pscustomobject]@{ ProcessId = $_.ProcessId; ParentProcessId = $_.ParentProcessId; Name = $_.Name; CommandLine = $_.CommandLine; Status = $_.Status; WorkingSetSize = $_.WorkingSetSize; PercentProcessorTime = if ($perf) { $perf.PercentProcessorTime } else { 0 } }", "};", "$processes | ConvertTo-Json -Compress -Depth 3", ].join(" "), ], }, ]); assert.deepStrictEqual(rows, [ { pid: process.pid, ppid: 1, pgid: null, status: "Live", cpuPercent: 0, rssBytes: 1024, elapsed: "", command: "t3 server", }, { pid: 4242, ppid: process.pid, pgid: null, status: "Running", cpuPercent: 1.25, rssBytes: 2049, elapsed: "", command: "agent.exe", }, ]); }), ); it.effect("does not allow signaling the diagnostics query process", () => Effect.gen(function* () { const spawnerLayer = Layer.succeed( ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make(() => Effect.succeed( mockHandle({ stdout: [ ` ${process.pid} 1 ${process.pid} Ss 0.0 1024 01:02.03 t3 server`, ` 4242 ${process.pid} ${process.pid} R 1.5 2048 00:00 ps -axo pid=,ppid=,pgid=,stat=,pcpu=,rss=,etime=,command=`, ].join("\n"), }), ), ), ); const layer = ProcessDiagnostics.layer.pipe(Layer.provide(spawnerLayer)); const result = yield* Effect.service(ProcessDiagnostics.ProcessDiagnostics).pipe( Effect.flatMap((pd) => pd.signal({ pid: 4242, signal: "SIGINT" })), Effect.provide(layer), ); assert.deepStrictEqual(result, { pid: 4242, signal: "SIGINT", signaled: false, message: Option.some("Process 4242 is not a live descendant of the T3 server."), }); }), ); });