317 lines · 9.5 KB · TypeScript
RawDownload
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 * as ProcessDiagnostics from "./ProcessDiagnostics.ts"; const encoder = new TextEncoder();const encodeJsonString = 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.deepEqual(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.deepEqual(        diagnostics.processes.map((process) => process.pid),        [101, 102],      );      assert.deepEqual(        diagnostics.processes.map((process) => process.depth),        [0, 1],      );      assert.equal(Option.getOrNull(diagnostics.processes[0]!.pgid), 100);      assert.deepEqual(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.deepEqual(        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<string> }> =        [];      const spawnerLayer = Layer.mock(ChildProcessSpawner.ChildProcessSpawner, {        spawn: (command) => {          const childProcess = command as unknown as {            readonly command: string;            readonly args: ReadonlyArray<string>;          };          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.deepEqual(        diagnostics.processes.map((process) => process.pid),        [4242],      );      assert.deepEqual(commands, [        {          command: "ps",          args: ["-axo", "pid=,ppid=,pgid=,stat=,pcpu=,rss=,etime=,command="],        },      ]);    }),  );   it.effect("decodes Windows process rows through Schema JSON parsing", () =>    Effect.gen(function* () {      const commands: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }> =        [];      const spawnerLayer = Layer.mock(ChildProcessSpawner.ChildProcessSpawner, {        spawn: (command) => {          const childProcess = command as unknown as {            readonly command: string;            readonly args: ReadonlyArray<string>;          };          commands.push({ command: childProcess.command, args: childProcess.args });          return Effect.succeed(            mockHandle({              stdout: encodeJsonString([                {                  ProcessId: 4242,                  ParentProcessId: 100,                  Name: "node.exe",                  CommandLine: "node server.js",                  Status: "Running",                  WorkingSetSize: 2048,                  PercentProcessorTime: 12.5,                },                {                  ProcessId: 0,                  ParentProcessId: 100,                  Name: "invalid.exe",                },              ]),            }),          );        },      });       const rows = yield* ProcessDiagnostics.readProcessRows("win32").pipe(        Effect.provide(spawnerLayer),      );       assert.deepEqual(rows, [        {          pid: 4242,          ppid: 100,          pgid: null,          status: "Running",          cpuPercent: 12.5,          rssBytes: 2048,          elapsed: "",          command: "node server.js",        },      ]);      assert.equal(commands[0]?.command, "powershell.exe");    }),  );   it.effect("does not allow signaling the diagnostics query process", () =>    Effect.gen(function* () {      const spawnerLayer = Layer.mock(ChildProcessSpawner.ChildProcessSpawner, {        spawn: () =>          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.deepEqual(result, {        pid: 4242,        signal: "SIGINT",        signaled: false,        message: Option.some("Process 4242 is not a live descendant of the T3 server."),      });    }),  );});