1import { assert, describe, it } from "@effect/vitest";
2import {
3 AuthSessionId,
4 RpcClientId,
5 type HostPowerSnapshot,
6 type ClientActivityReportInput,
7} from "@t3tools/contracts";
8import * as DateTime from "effect/DateTime";
9import * as Deferred from "effect/Deferred";
10import * as Effect from "effect/Effect";
11import * as Fiber from "effect/Fiber";
12import * as Layer from "effect/Layer";
13import * as PubSub from "effect/PubSub";
14import * as Ref from "effect/Ref";
15import * as Stream from "effect/Stream";
16
17import * as ServerSettings from "../serverSettings.ts";
18import * as BackgroundPolicy from "./BackgroundPolicy.ts";
19import * as HostPowerMonitor from "./HostPowerMonitor.ts";
20
21const TEST_NOW = DateTime.makeUnsafe("2026-05-13T00:00:00.000Z");
22
23const nominalHostPower: HostPowerSnapshot = {
24 source: "unknown",
25 idle: "unknown",
26 idleSeconds: null,
27 locked: "unknown",
28 suspended: false,
29 onBattery: "unknown",
30 lowPowerMode: "unknown",
31 thermalState: "unknown",
32 stale: true,
33 updatedAt: TEST_NOW,
34};
35
36const constrainedHostPower: HostPowerSnapshot = {
37 ...nominalHostPower,
38 lowPowerMode: "true",
39 stale: false,
40};
41
42function makeReport(overrides: Partial<ClientActivityReportInput> = {}): ClientActivityReportInput {
43 return {
44 clientId: "client-1",
45 clientKind: "web",
46 visible: true,
47 focused: true,
48 recentlyInteracted: true,
49 scopes: [{ type: "vcs-status", cwd: "/repo" }],
50 ttlMs: 45_000,
51 observedAt: TEST_NOW,
52 ...overrides,
53 };
54}
55
56function layerFor(
57 hostPower: HostPowerSnapshot,
58 settingsOverrides: Parameters<typeof ServerSettings.layerTest>[0] = {},
59) {
60 const layerHost = Layer.effect(
61 HostPowerMonitor.HostPowerMonitor,
62 Effect.gen(function* () {
63 const changes = yield* PubSub.sliding<HostPowerSnapshot>(1);
64 let snapshot = hostPower;
65 return HostPowerMonitor.HostPowerMonitor.of({
66 snapshot: Effect.sync(() => snapshot),
67 report: (next) =>
68 Effect.sync(() => {
69 snapshot = next;
70 }).pipe(Effect.andThen(PubSub.publish(changes, next)), Effect.asVoid),
71 streamChanges: Stream.fromPubSub(changes),
72 });
73 }),
74 );
75 return BackgroundPolicy.layer.pipe(
76 Layer.provide(Layer.merge(layerHost, ServerSettings.layerTest(settingsOverrides))),
77 );
78}
79
80describe("BackgroundPolicy", () => {
81 it.effect("records foreground scoped client demand", () =>
82 Effect.gen(function* () {
83 const policy = yield* BackgroundPolicy.BackgroundPolicy;
84 yield* policy.reportClientActivity(
85 AuthSessionId.make("session-1"),
86 RpcClientId.make(1),
87 makeReport(),
88 );
89
90 const snapshot = yield* policy.snapshot;
91 assert.equal(snapshot.activeForegroundLeaseCount, 1);
92 assert.deepStrictEqual(snapshot.activeScopeKeys, ["vcs-status:/repo"]);
93 assert.equal(snapshot.shouldRunOpportunisticWork, true);
94 assert.equal(yield* policy.hasDemand({ type: "vcs-status", cwd: "/repo" }), true);
95 assert.equal(yield* policy.hasDemand({ type: "vcs-status", cwd: "/other" }), false);
96 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), true);
97 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/other" }), false);
98 }).pipe(Effect.provide(layerFor(nominalHostPower))),
99 );
100
101 it.effect("removes all leases for a disconnected websocket connection", () =>
102 Effect.gen(function* () {
103 const policy = yield* BackgroundPolicy.BackgroundPolicy;
104 yield* policy.reportClientActivity(
105 AuthSessionId.make("session-1"),
106 RpcClientId.make(1),
107 makeReport(),
108 );
109 yield* policy.removeRpcClient(AuthSessionId.make("session-1"), RpcClientId.make(1));
110
111 const snapshot = yield* policy.snapshot;
112 assert.equal(snapshot.activeForegroundLeaseCount, 0);
113 assert.deepStrictEqual(snapshot.activeScopeKeys, []);
114 assert.equal(snapshot.shouldRunOpportunisticWork, false);
115 }).pipe(Effect.provide(layerFor(nominalHostPower))),
116 );
117
118 it.effect("keeps leases from another session when rpc client ids are reused", () =>
119 Effect.gen(function* () {
120 const policy = yield* BackgroundPolicy.BackgroundPolicy;
121 const rpcClientId = RpcClientId.make(1);
122 yield* policy.reportClientActivity(
123 AuthSessionId.make("session-1"),
124 rpcClientId,
125 makeReport({ clientId: "client-1" }),
126 );
127 yield* policy.reportClientActivity(
128 AuthSessionId.make("session-2"),
129 rpcClientId,
130 makeReport({ clientId: "client-2" }),
131 );
132
133 yield* policy.removeRpcClient(AuthSessionId.make("session-1"), rpcClientId);
134
135 const snapshot = yield* policy.snapshot;
136 assert.equal(snapshot.activeForegroundLeaseCount, 1);
137 assert.equal(snapshot.leases[0]?.sessionId, AuthSessionId.make("session-2"));
138 assert.equal(snapshot.leases[0]?.clientId, "client-2");
139 }).pipe(Effect.provide(layerFor(nominalHostPower))),
140 );
141
142 it.effect("serializes lease mutation publications", () =>
143 Effect.gen(function* () {
144 const firstSnapshotStarted = yield* Deferred.make<void>();
145 const releaseFirstSnapshot = yield* Deferred.make<void>();
146 const snapshotReads = yield* Ref.make(0);
147 const layerHost = Layer.succeed(
148 HostPowerMonitor.HostPowerMonitor,
149 HostPowerMonitor.HostPowerMonitor.of({
150 snapshot: Ref.updateAndGet(snapshotReads, (count) => count + 1).pipe(
151 Effect.flatMap((count) =>
152 count === 1
153 ? Deferred.succeed(firstSnapshotStarted, undefined).pipe(
154 Effect.andThen(Deferred.await(releaseFirstSnapshot)),
155 Effect.as(nominalHostPower),
156 )
157 : Effect.succeed(nominalHostPower),
158 ),
159 ),
160 report: () => Effect.void,
161 streamChanges: Stream.empty,
162 }),
163 );
164 const layer = BackgroundPolicy.layer.pipe(
165 Layer.provide(Layer.merge(layerHost, ServerSettings.layerTest())),
166 );
167
168 yield* Effect.gen(function* () {
169 const policy = yield* BackgroundPolicy.BackgroundPolicy;
170 const updatesFiber = yield* policy.streamChanges.pipe(
171 Stream.take(2),
172 Stream.runCollect,
173 Effect.forkChild,
174 );
175 yield* Effect.yieldNow;
176 const reportFiber = yield* policy
177 .reportClientActivity(AuthSessionId.make("session-1"), RpcClientId.make(1), makeReport())
178 .pipe(Effect.forkChild);
179 yield* Deferred.await(firstSnapshotStarted);
180 const removeFiber = yield* policy
181 .removeRpcClient(AuthSessionId.make("session-1"), RpcClientId.make(1))
182 .pipe(Effect.forkChild);
183 yield* Effect.yieldNow;
184 yield* Deferred.succeed(releaseFirstSnapshot, undefined);
185 yield* Fiber.join(reportFiber);
186 yield* Fiber.join(removeFiber);
187 const updates = Array.from(yield* Fiber.join(updatesFiber));
188
189 assert.equal(updates.at(-1)?.activeForegroundLeaseCount, 0);
190 assert.deepStrictEqual(updates.at(-1)?.activeScopeKeys, []);
191 }).pipe(Effect.provide(layer));
192 }),
193 );
194
195 it.effect("bounds client-id churn for one websocket connection", () =>
196 Effect.gen(function* () {
197 const policy = yield* BackgroundPolicy.BackgroundPolicy;
198 const sessionId = AuthSessionId.make("session-1");
199 const rpcClientId = RpcClientId.make(1);
200 for (
201 let index = 0;
202 index <= BackgroundPolicy.MAX_CLIENT_ACTIVITY_LEASES_PER_RPC_CLIENT;
203 index += 1
204 ) {
205 yield* policy.reportClientActivity(
206 sessionId,
207 rpcClientId,
208 makeReport({ clientId: `client-${index}` }),
209 );
210 }
211
212 const snapshot = yield* policy.snapshot;
213 assert.equal(
214 snapshot.leases.length,
215 BackgroundPolicy.MAX_CLIENT_ACTIVITY_LEASES_PER_RPC_CLIENT,
216 );
217 assert.isTrue(
218 snapshot.leases.some(
219 (lease) =>
220 lease.clientId ===
221 `client-${BackgroundPolicy.MAX_CLIENT_ACTIVITY_LEASES_PER_RPC_CLIENT}`,
222 ),
223 );
224 }).pipe(Effect.provide(layerFor(nominalHostPower))),
225 );
226
227 it.effect("host low power mode disables opportunistic work without dropping scoped demand", () =>
228 Effect.gen(function* () {
229 const policy = yield* BackgroundPolicy.BackgroundPolicy;
230 yield* policy.reportClientActivity(
231 AuthSessionId.make("session-1"),
232 RpcClientId.make(1),
233 makeReport(),
234 );
235
236 const snapshot = yield* policy.snapshot;
237 assert.equal(snapshot.activeForegroundLeaseCount, 1);
238 assert.deepStrictEqual(snapshot.activeScopeKeys, ["vcs-status:/repo"]);
239 assert.equal(snapshot.shouldRunOpportunisticWork, false);
240 assert.equal(yield* policy.hasDemand({ type: "vcs-status", cwd: "/repo" }), true);
241 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), false);
242 }).pipe(Effect.provide(layerFor(constrainedHostPower))),
243 );
244
245 it.effect("host suspension disables scoped and opportunistic work", () =>
246 Effect.gen(function* () {
247 const policy = yield* BackgroundPolicy.BackgroundPolicy;
248 yield* policy.reportClientActivity(
249 AuthSessionId.make("session-1"),
250 RpcClientId.make(1),
251 makeReport(),
252 );
253
254 const snapshot = yield* policy.snapshot;
255 assert.equal(snapshot.activeForegroundLeaseCount, 1);
256 assert.equal(snapshot.shouldRunOpportunisticWork, false);
257 assert.equal(yield* policy.hasDemand({ type: "vcs-status", cwd: "/repo" }), true);
258 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), false);
259 }).pipe(
260 Effect.provide(
261 layerFor({
262 ...nominalHostPower,
263 suspended: true,
264 stale: false,
265 }),
266 ),
267 ),
268 );
269
270 it.effect("keeps background demand visible while preventing scoped work", () =>
271 Effect.gen(function* () {
272 const policy = yield* BackgroundPolicy.BackgroundPolicy;
273 yield* policy.reportClientActivity(
274 AuthSessionId.make("session-1"),
275 RpcClientId.make(1),
276 makeReport({ focused: false, visible: false }),
277 );
278
279 const snapshot = yield* policy.snapshot;
280 assert.equal(snapshot.activeForegroundLeaseCount, 0);
281 assert.deepStrictEqual(snapshot.activeScopeKeys, ["vcs-status:/repo"]);
282 assert.equal(yield* policy.hasDemand({ type: "vcs-status", cwd: "/repo" }), true);
283 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), false);
284 }).pipe(Effect.provide(layerFor(nominalHostPower))),
285 );
286
287 it.effect("keeps scoped work active after a recently used visible window loses focus", () =>
288 Effect.gen(function* () {
289 const policy = yield* BackgroundPolicy.BackgroundPolicy;
290 yield* policy.reportClientActivity(
291 AuthSessionId.make("session-1"),
292 RpcClientId.make(1),
293 makeReport({ focused: false, recentlyInteracted: true }),
294 );
295
296 const snapshot = yield* policy.snapshot;
297 assert.equal(snapshot.activeForegroundLeaseCount, 1);
298 assert.equal(snapshot.shouldRunOpportunisticWork, true);
299 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), true);
300 }).pipe(Effect.provide(layerFor(nominalHostPower))),
301 );
302
303 it.effect("pauses scoped work for a visible unfocused client without recent interaction", () =>
304 Effect.gen(function* () {
305 const policy = yield* BackgroundPolicy.BackgroundPolicy;
306 yield* policy.reportClientActivity(
307 AuthSessionId.make("session-1"),
308 RpcClientId.make(1),
309 makeReport({ focused: false, recentlyInteracted: false }),
310 );
311
312 const snapshot = yield* policy.snapshot;
313 assert.equal(snapshot.activeForegroundLeaseCount, 0);
314 assert.equal(snapshot.shouldRunOpportunisticWork, false);
315 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), false);
316 }).pipe(Effect.provide(layerFor(nominalHostPower))),
317 );
318
319 it.effect(
320 "performance profile allows background scoped work while a scoped lease is active",
321 () =>
322 Effect.gen(function* () {
323 const policy = yield* BackgroundPolicy.BackgroundPolicy;
324 yield* policy.reportClientActivity(
325 AuthSessionId.make("session-1"),
326 RpcClientId.make(1),
327 makeReport({ focused: false, visible: false }),
328 );
329
330 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), true);
331 }).pipe(
332 Effect.provide(layerFor(nominalHostPower, { backgroundActivityProfile: "performance" })),
333 ),
334 );
335
336 it.effect("battery saver profile pauses scoped work on battery", () =>
337 Effect.gen(function* () {
338 const policy = yield* BackgroundPolicy.BackgroundPolicy;
339 yield* policy.reportClientActivity(
340 AuthSessionId.make("session-1"),
341 RpcClientId.make(1),
342 makeReport(),
343 );
344
345 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), false);
346 }).pipe(
347 Effect.provide(
348 layerFor(
349 {
350 ...nominalHostPower,
351 onBattery: "true",
352 stale: false,
353 },
354 { backgroundActivityProfile: "battery-saver" },
355 ),
356 ),
357 ),
358 );
359
360 it.effect("does not gate work on stale host power values", () =>
361 Effect.gen(function* () {
362 const policy = yield* BackgroundPolicy.BackgroundPolicy;
363 yield* policy.reportClientActivity(
364 AuthSessionId.make("session-1"),
365 RpcClientId.make(1),
366 makeReport(),
367 );
368
369 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), true);
370 }).pipe(
371 Effect.provide(
372 layerFor(
373 {
374 ...nominalHostPower,
375 locked: "true",
376 onBattery: "true",
377 lowPowerMode: "true",
378 thermalState: "critical",
379 stale: true,
380 },
381 { backgroundActivityProfile: "battery-saver" },
382 ),
383 ),
384 ),
385 );
386});