import type { DesktopHostTelemetrySnapshot, ResourceMonitorProcessSample, ResourceMonitorSnapshotEvent, } from "@t3tools/contracts"; import { describe, expect, it } from "@effect/vitest"; import * as DateTime from "effect/DateTime"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as PubSub from "effect/PubSub"; import * as Ref from "effect/Ref"; import * as Stream from "effect/Stream"; import * as TestClock from "effect/testing/TestClock"; import * as DesktopTelemetryReceiver from "./DesktopTelemetryReceiver.ts"; import * as NativeTelemetryClient from "./NativeTelemetryClient.ts"; import * as ResourceAttribution from "./ResourceAttribution.ts"; import * as ResourceTelemetry from "./ResourceTelemetry.ts"; function processSample( input: Partial & Pick, ): ResourceMonitorProcessSample { return { runTimeMs: 1_000, name: `process-${input.pid}`, command: `process-${input.pid}`, status: "Running", cpuPercent: 0, cpuTimeMs: 0, residentBytes: 1_024, virtualBytes: 2_048, ioReadBytes: 0, ioWriteBytes: 0, ioSemantics: "storage", ...input, }; } function nativeSnapshot(input: { readonly sequence: number; readonly sampledAtUnixMs: number; readonly childCpuTimeMs: number; readonly childWriteBytes: number; }): ResourceMonitorSnapshotEvent { const processes = [ processSample({ pid: process.pid, ppid: 1, startTimeMs: 100, cpuTimeMs: input.sequence * 10, }), processSample({ pid: 4_242, ppid: process.pid, startTimeMs: 200, name: "codex", command: "codex app-server", cpuTimeMs: input.childCpuTimeMs, ioWriteBytes: input.childWriteBytes, }), processSample({ pid: 5_000, ppid: 1, startTimeMs: 300, name: "electron", command: "electron", cpuTimeMs: input.sequence * 20, }), processSample({ pid: 9_000, ppid: process.pid, startTimeMs: 400, name: "t3-resource-monitor", command: "t3-resource-monitor", cpuTimeMs: input.sequence * 5, }), ]; return { version: 2, type: "snapshot", sequence: input.sequence, sampledAtUnixMs: input.sampledAtUnixMs, collectionDurationMicros: 300, scannedProcessCount: 80, retainedProcessCount: processes.length, inaccessibleProcessCount: 1, processes, }; } function desktopSnapshot(sampledAtUnixMs: number): DesktopHostTelemetrySnapshot { const sampledAt = DateTime.makeUnsafe(sampledAtUnixMs); return { version: 1, type: "desktopTelemetry", sequence: 1, sampledAtUnixMs, electronPid: 5_000, power: { source: "electron-main", idle: "false", idleSeconds: 2, locked: "false", suspended: false, onBattery: "true", lowPowerMode: "unknown", thermalState: "fair", stale: false, updatedAt: sampledAt, }, speedLimitPercent: Option.some(90), electronProcesses: [ { pid: 5_000, creationTimeMs: 300, type: "Browser", name: "electron", cpuPercent: 2, cumulativeCpuSeconds: 0.02, idleWakeupsPerSecond: 3, workingSetBytes: 4_096, peakWorkingSetBytes: 8_192, }, ], }; } describe("ResourceTelemetry", () => { it.effect("enables live native and Electron collection only while changes are retained", () => Effect.gen(function* () { const sampledAtUnixMs = DateTime.toEpochMillis(yield* DateTime.now); const sample = nativeSnapshot({ sequence: 1, sampledAtUnixMs, childCpuTimeMs: 100, childWriteBytes: 1_000, }); const demandChanges = yield* Ref.make>([]); const nativeLayer = NativeTelemetryClient.layerTest({ sampleNow: Effect.succeed(sample), health: Effect.succeed({ status: "healthy", hello: Option.none(), lastSampleAt: Option.none(), lastError: Option.none(), restartCount: 0, sampleIntervalMs: 1_000, }), }); const desktopLayer = DesktopTelemetryReceiver.layerTest({ latest: Effect.succeedSome(desktopSnapshot(sampledAtUnixMs)), setDiagnosticsDemand: (enabled) => Ref.update(demandChanges, (changes) => [...changes, enabled]), }); const telemetryLayer = ResourceTelemetry.layer.pipe( Layer.provide(Layer.mergeAll(nativeLayer, desktopLayer, ResourceAttribution.layer)), ); const live = yield* Stream.runHead( Effect.gen(function* () { const telemetry = yield* ResourceTelemetry.ResourceTelemetry; return telemetry.changes; }).pipe(Stream.unwrap), ).pipe(Effect.provide(telemetryLayer)); expect(Option.isSome(live)).toBe(true); expect(yield* Ref.get(demandChanges)).toEqual([true, false]); }), ); it.effect("combines native, Electron, attribution, retry, and history data", () => Effect.gen(function* () { const startedAt = DateTime.toEpochMillis(yield* DateTime.now); const samples = [ nativeSnapshot({ sequence: 1, sampledAtUnixMs: startedAt, childCpuTimeMs: 100, childWriteBytes: 1_000, }), nativeSnapshot({ sequence: 2, sampledAtUnixMs: startedAt + 1_000, childCpuTimeMs: 350, childWriteBytes: 5_000, }), nativeSnapshot({ sequence: 1, sampledAtUnixMs: startedAt + 2_000, childCpuTimeMs: 500, childWriteBytes: 7_000, }), ] as const; const sampleIndex = yield* Ref.make(0); const externalProcesses = yield* Ref.make< ReadonlyArray<{ readonly pid: number; readonly startTimeMs?: number }> >([]); const retryCount = yield* Ref.make(0); const nativeHealth = yield* Ref.make({ status: "healthy", hello: Option.some({ version: 2, type: "hello", sidecarVersion: "0.1.0", sidecarPid: 9_000, platform: "test", arch: "test", capabilities: { cumulativeCpuTime: true, currentCpuPercent: true, residentMemory: true, virtualMemory: true, ioBytes: true, processStartTime: true, processTree: true, }, }), lastSampleAt: Option.some(DateTime.makeUnsafe(startedAt)), lastError: Option.none(), restartCount: 2, sampleIntervalMs: 1_000, }); const nativeHealthChanges = yield* PubSub.sliding(4); const nativeLayer = NativeTelemetryClient.layerTest({ setExternalProcesses: (processes) => Ref.set(externalProcesses, processes), readHistory: () => Effect.succeed(samples.slice(0, 2)), sampleNow: Ref.modify(sampleIndex, (index) => [ samples[Math.min(index, samples.length - 1)]!, index + 1, ]), retry: Ref.updateAndGet(retryCount, (count) => count + 1).pipe(Effect.as(true)), health: Ref.get(nativeHealth), healthChanges: Stream.fromPubSub(nativeHealthChanges), }); const desktopLayer = DesktopTelemetryReceiver.layerTest({ latest: Effect.succeedSome(desktopSnapshot(startedAt)), health: Effect.succeed({ status: "healthy", lastSampleAt: Option.some(DateTime.makeUnsafe(startedAt)), lastError: Option.none(), }), }); const attributionLayer = ResourceAttribution.layer; const dependencies = Layer.mergeAll(nativeLayer, desktopLayer, attributionLayer); const telemetryLayer = ResourceTelemetry.layer.pipe(Layer.provide(dependencies)); const layer = Layer.mergeAll(dependencies, telemetryLayer); yield* Effect.gen(function* () { const telemetry = yield* ResourceTelemetry.ResourceTelemetry; const attribution = yield* ResourceAttribution.ResourceAttribution; expect(yield* Ref.get(externalProcesses)).toEqual([{ pid: 5_000 }]); yield* attribution.record({ component: "provider-event-log", operation: "append", logicalWriteBytes: 512, count: 2, durationMs: 4, }); const first = yield* telemetry.refresh; expect(first.groups.backend.processCount).toBe(2); expect(first.groups.electron.processCount).toBe(1); expect(first.groups.monitor.processCount).toBe(1); expect(first.power.onBattery).toBe("true"); expect(Option.getOrNull(first.speedLimitPercent)).toBe(90); expect(first.attribution.entries).toEqual([ { component: "provider-event-log", operation: "append", logicalReadBytes: 0, logicalWriteBytes: 512, count: 2, durationMs: 4, }, ]); yield* TestClock.adjust(Duration.seconds(1)); const second = yield* telemetry.refresh; const codex = second.processes.find((entry) => entry.identity.pid === 4_242); expect(codex?.cpuPercent).toBe(25); expect(codex?.ioWriteBytesPerSecond).toBe(4_000); expect(second.groups.backend.ioWriteBytes).toBe(4_000); expect(second.health.collectionDurationMicros).toBe(300); expect(second.health.scannedProcessCount).toBe(80); expect(second.health.inaccessibleProcessCount).toBe(1); const history = yield* telemetry.readHistory({ windowMs: 60_000, bucketMs: 10_000, }); expect(history.retainedSampleCount).toBeGreaterThan(0); expect( history.topProcesses.find((entry) => entry.identity.pid === 4_242)?.sampleCount, ).toBe(2); expect(history.topProcesses.find((entry) => entry.identity.pid === 4_242)?.cpuTimeMs).toBe( 250, ); expect( history.topProcesses.find((entry) => entry.identity.pid === 4_242)?.ioWriteBytes, ).toBe(4_000); expect(history.buckets.reduce((total, bucket) => total + bucket.ioWriteBytes, 0)).toBe( 4_000, ); const retry = yield* telemetry.retry; expect(retry.accepted).toBe(true); expect(yield* Ref.get(retryCount)).toBe(1); yield* Ref.update(nativeHealth, (current) => ({ ...current, hello: Option.map(current.hello, (hello) => ({ ...hello, sidecarPid: 9_001, })), restartCount: 3, })); yield* TestClock.adjust(Duration.seconds(1)); const restarted = yield* telemetry.refresh; expect(DateTime.toEpochMillis(restarted.readAt)).toBe(startedAt + 2_000); expect(Option.getOrNull(restarted.health.sidecarPid)).toBe(9_001); yield* Ref.update(nativeHealth, (current) => ({ ...current, status: "degraded" as const, lastError: Option.some("collector exited"), })); yield* PubSub.publish(nativeHealthChanges, yield* Ref.get(nativeHealth)); yield* Effect.yieldNow; const healthUpdate = yield* telemetry.latest; expect(healthUpdate.health.native.status).toBe("degraded"); expect(Option.getOrNull(healthUpdate.health.native.lastError)).toBe("collector exited"); const degradedHistory = yield* telemetry.readHistory({ windowMs: 60_000, bucketMs: 10_000, }); expect(degradedHistory.health.native.status).toBe("degraded"); }).pipe(Effect.provide(layer)); }), ); });