1import type { DesktopHostTelemetrySnapshot } from "@t3tools/contracts";
2import { describe, expect, it } from "@effect/vitest";
3import * as DateTime from "effect/DateTime";
4import * as Deferred from "effect/Deferred";
5import * as Effect from "effect/Effect";
6import * as Fiber from "effect/Fiber";
7import * as Layer from "effect/Layer";
8import * as Option from "effect/Option";
9import * as PubSub from "effect/PubSub";
10import * as Stream from "effect/Stream";
11
12import * as DesktopTelemetryReceiver from "../resourceTelemetry/DesktopTelemetryReceiver.ts";
13import * as HostPowerMonitor from "./HostPowerMonitor.ts";
14
15describe("HostPowerMonitor", () => {
16 it.effect("publishes semantic power changes without idle-time heartbeat churn", () =>
17 Effect.gen(function* () {
18 const monitor = yield* HostPowerMonitor.make();
19 const initial = {
20 source: "electron-main",
21 idle: "false",
22 idleSeconds: 0,
23 locked: "false",
24 suspended: false,
25 onBattery: "false",
26 lowPowerMode: "unknown",
27 thermalState: "nominal",
28 stale: false,
29 updatedAt: DateTime.makeUnsafe("2026-06-17T12:00:00.000Z"),
30 } as const;
31 yield* monitor.report(initial);
32
33 const nextChange = yield* Stream.runHead(monitor.streamChanges).pipe(Effect.forkChild);
34 yield* Effect.yieldNow;
35 yield* monitor.report({
36 ...initial,
37 idleSeconds: 1,
38 updatedAt: DateTime.makeUnsafe("2026-06-17T12:00:01.000Z"),
39 });
40 yield* monitor.report({
41 ...initial,
42 locked: "true",
43 updatedAt: DateTime.makeUnsafe("2026-06-17T12:00:02.000Z"),
44 });
45
46 expect(Option.getOrThrow(yield* Fiber.join(nextChange)).locked).toBe("true");
47 }),
48 );
49
50 it.effect("ignores host power reports older than the latest accepted snapshot", () =>
51 Effect.gen(function* () {
52 const initial = {
53 source: "electron-main",
54 idle: "false",
55 idleSeconds: 0,
56 locked: "false",
57 suspended: false,
58 onBattery: "false",
59 lowPowerMode: "false",
60 thermalState: "nominal",
61 stale: false,
62 updatedAt: DateTime.makeUnsafe("2026-06-17T12:00:00.000Z"),
63 } as const;
64 const monitor = yield* HostPowerMonitor.make(initial);
65 const latestAt = DateTime.makeUnsafe("2026-06-17T12:00:02.000Z");
66 yield* monitor.report({
67 ...initial,
68 locked: "true",
69 updatedAt: latestAt,
70 });
71 yield* monitor.report({
72 ...initial,
73 updatedAt: DateTime.makeUnsafe("2026-06-17T12:00:01.000Z"),
74 });
75
76 const snapshot = yield* monitor.snapshot;
77 expect(snapshot.locked).toBe("true");
78 expect(DateTime.toEpochMillis(snapshot.updatedAt)).toBe(DateTime.toEpochMillis(latestAt));
79 }),
80 );
81
82 it.effect(
83 "subscribes before reading the desktop snapshot so concurrent power updates survive",
84 () =>
85 Effect.gen(function* () {
86 const sampledAt = DateTime.makeUnsafe("2026-06-17T12:00:00.000Z");
87 const initial: DesktopHostTelemetrySnapshot = {
88 version: 1,
89 type: "desktopTelemetry",
90 sequence: 1,
91 sampledAtUnixMs: DateTime.toEpochMillis(sampledAt),
92 electronPid: 100,
93 power: {
94 source: "electron-main",
95 idle: "false",
96 idleSeconds: 0,
97 locked: "false",
98 suspended: false,
99 onBattery: "false",
100 lowPowerMode: "unknown",
101 thermalState: "nominal",
102 stale: false,
103 updatedAt: sampledAt,
104 },
105 speedLimitPercent: Option.none(),
106 electronProcesses: [],
107 };
108 const updated: DesktopHostTelemetrySnapshot = {
109 ...initial,
110 sequence: 2,
111 power: {
112 ...initial.power,
113 onBattery: "true",
114 updatedAt: DateTime.makeUnsafe("2026-06-17T12:00:01.000Z"),
115 },
116 };
117 const desktopChanges = yield* PubSub.sliding<DesktopHostTelemetrySnapshot>(1);
118 const changeApplied = yield* Deferred.make<void>();
119 const layerReceiver = DesktopTelemetryReceiver.layerTest({
120 latest: Effect.succeedSome(initial),
121 changes: Stream.empty,
122 subscribe: Effect.gen(function* () {
123 const subscription = yield* PubSub.subscribe(desktopChanges);
124 yield* PubSub.publish(desktopChanges, updated);
125 return {
126 latest: Option.some(initial),
127 changes: Stream.fromSubscription(subscription).pipe(
128 Stream.take(1),
129 Stream.ensuring(Deferred.succeed(changeApplied, undefined)),
130 ),
131 };
132 }),
133 });
134 const layer = HostPowerMonitor.layer.pipe(Layer.provide(layerReceiver));
135
136 yield* Effect.gen(function* () {
137 const monitor = yield* HostPowerMonitor.HostPowerMonitor;
138 yield* Deferred.await(changeApplied);
139 expect((yield* monitor.snapshot).onBattery).toBe("true");
140 }).pipe(Effect.provide(layer));
141 }),
142 );
143});