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 Effect from "effect/Effect";
10import * as Layer from "effect/Layer";
11import * as PubSub from "effect/PubSub";
12import * as Stream from "effect/Stream";
13
14import * as ServerSettings from "../serverSettings.ts";
15import * as BackgroundPolicy from "./BackgroundPolicy.ts";
16import * as HostPowerMonitor from "./HostPowerMonitor.ts";
17
18const TEST_NOW = DateTime.makeUnsafe("2026-05-13T00:00:00.000Z");
19
20const nominalHostPower: HostPowerSnapshot = {
21 source: "unknown",
22 idle: "unknown",
23 idleSeconds: null,
24 locked: "unknown",
25 suspended: false,
26 onBattery: "unknown",
27 lowPowerMode: "unknown",
28 thermalState: "unknown",
29 stale: true,
30 updatedAt: TEST_NOW,
31};
32
33function makeReport(overrides: Partial<ClientActivityReportInput> = {}): ClientActivityReportInput {
34 return {
35 clientId: "client-1",
36 clientKind: "web",
37 visible: true,
38 focused: true,
39 recentlyInteracted: true,
40 scopes: [{ type: "vcs-status", cwd: "/repo" }],
41 ttlMs: 45_000,
42 observedAt: TEST_NOW,
43 ...overrides,
44 };
45}
46
47function layerFor(
48 hostPower: HostPowerSnapshot,
49 settingsOverrides: Parameters<typeof ServerSettings.layerTest>[0] = {},
50) {
51 const layerHost = Layer.effect(
52 HostPowerMonitor.HostPowerMonitor,
53 Effect.gen(function* () {
54 const changes = yield* PubSub.sliding<HostPowerSnapshot>(1);
55 let snapshot = hostPower;
56 return HostPowerMonitor.HostPowerMonitor.of({
57 snapshot: Effect.sync(() => snapshot),
58 report: (next) =>
59 Effect.sync(() => {
60 snapshot = next;
61 }).pipe(Effect.andThen(PubSub.publish(changes, next)), Effect.asVoid),
62 streamChanges: Stream.fromPubSub(changes),
63 });
64 }),
65 );
66 return BackgroundPolicy.layer.pipe(
67 Layer.provide(Layer.merge(layerHost, ServerSettings.layerTest(settingsOverrides))),
68 );
69}
70
71describe("BackgroundPolicy", () => {
72 it.effect("removes all leases for a disconnected websocket connection", () =>
73 Effect.gen(function* () {
74 const policy = yield* BackgroundPolicy.BackgroundPolicy;
75 yield* policy.reportClientActivity(
76 AuthSessionId.make("session-1"),
77 RpcClientId.make(1),
78 makeReport(),
79 );
80 yield* policy.removeRpcClient(AuthSessionId.make("session-1"), RpcClientId.make(1));
81
82 const snapshot = yield* policy.snapshot;
83 assert.equal(snapshot.activeForegroundLeaseCount, 0);
84 assert.deepStrictEqual(snapshot.activeScopeKeys, []);
85 assert.equal(snapshot.shouldRunOpportunisticWork, false);
86 }).pipe(Effect.provide(layerFor(nominalHostPower))),
87 );
88
89 it.effect("keeps leases from another session when rpc client ids are reused", () =>
90 Effect.gen(function* () {
91 const policy = yield* BackgroundPolicy.BackgroundPolicy;
92 const rpcClientId = RpcClientId.make(1);
93 yield* policy.reportClientActivity(
94 AuthSessionId.make("session-1"),
95 rpcClientId,
96 makeReport({ clientId: "client-1" }),
97 );
98 yield* policy.reportClientActivity(
99 AuthSessionId.make("session-2"),
100 rpcClientId,
101 makeReport({ clientId: "client-2" }),
102 );
103
104 yield* policy.removeRpcClient(AuthSessionId.make("session-1"), rpcClientId);
105
106 const snapshot = yield* policy.snapshot;
107 assert.equal(snapshot.activeForegroundLeaseCount, 1);
108 assert.equal(snapshot.leases[0]?.sessionId, AuthSessionId.make("session-2"));
109 assert.equal(snapshot.leases[0]?.clientId, "client-2");
110 }).pipe(Effect.provide(layerFor(nominalHostPower))),
111 );
112
113 it.effect("bounds client-id churn for one websocket connection", () =>
114 Effect.gen(function* () {
115 const policy = yield* BackgroundPolicy.BackgroundPolicy;
116 const sessionId = AuthSessionId.make("session-1");
117 const rpcClientId = RpcClientId.make(1);
118 for (
119 let index = 0;
120 index <= BackgroundPolicy.MAX_CLIENT_ACTIVITY_LEASES_PER_RPC_CLIENT;
121 index += 1
122 ) {
123 yield* policy.reportClientActivity(
124 sessionId,
125 rpcClientId,
126 makeReport({ clientId: `client-${index}` }),
127 );
128 }
129
130 const snapshot = yield* policy.snapshot;
131 assert.equal(
132 snapshot.leases.length,
133 BackgroundPolicy.MAX_CLIENT_ACTIVITY_LEASES_PER_RPC_CLIENT,
134 );
135 assert.isTrue(
136 snapshot.leases.some(
137 (lease) =>
138 lease.clientId ===
139 `client-${BackgroundPolicy.MAX_CLIENT_ACTIVITY_LEASES_PER_RPC_CLIENT}`,
140 ),
141 );
142 }).pipe(Effect.provide(layerFor(nominalHostPower))),
143 );
144
145 it.effect("host suspension disables scoped and opportunistic work", () =>
146 Effect.gen(function* () {
147 const policy = yield* BackgroundPolicy.BackgroundPolicy;
148 yield* policy.reportClientActivity(
149 AuthSessionId.make("session-1"),
150 RpcClientId.make(1),
151 makeReport(),
152 );
153
154 const snapshot = yield* policy.snapshot;
155 assert.equal(snapshot.activeForegroundLeaseCount, 1);
156 assert.equal(snapshot.shouldRunOpportunisticWork, false);
157 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), false);
158 }).pipe(
159 Effect.provide(
160 layerFor({
161 ...nominalHostPower,
162 suspended: true,
163 stale: false,
164 }),
165 ),
166 ),
167 );
168
169 it.effect("keeps background demand visible while preventing scoped work", () =>
170 Effect.gen(function* () {
171 const policy = yield* BackgroundPolicy.BackgroundPolicy;
172 yield* policy.reportClientActivity(
173 AuthSessionId.make("session-1"),
174 RpcClientId.make(1),
175 makeReport({ focused: false, visible: false }),
176 );
177
178 const snapshot = yield* policy.snapshot;
179 assert.equal(snapshot.activeForegroundLeaseCount, 0);
180 assert.deepStrictEqual(snapshot.activeScopeKeys, ["vcs-status:/repo"]);
181 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), false);
182 }).pipe(Effect.provide(layerFor(nominalHostPower))),
183 );
184
185 it.effect("keeps scoped work active after a recently used visible window loses focus", () =>
186 Effect.gen(function* () {
187 const policy = yield* BackgroundPolicy.BackgroundPolicy;
188 yield* policy.reportClientActivity(
189 AuthSessionId.make("session-1"),
190 RpcClientId.make(1),
191 makeReport({ focused: false, recentlyInteracted: true }),
192 );
193
194 const snapshot = yield* policy.snapshot;
195 assert.equal(snapshot.activeForegroundLeaseCount, 1);
196 assert.equal(snapshot.shouldRunOpportunisticWork, true);
197 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), true);
198 }).pipe(Effect.provide(layerFor(nominalHostPower))),
199 );
200
201 it.effect("battery saver profile pauses scoped work on battery", () =>
202 Effect.gen(function* () {
203 const policy = yield* BackgroundPolicy.BackgroundPolicy;
204 yield* policy.reportClientActivity(
205 AuthSessionId.make("session-1"),
206 RpcClientId.make(1),
207 makeReport(),
208 );
209
210 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), false);
211 }).pipe(
212 Effect.provide(
213 layerFor(
214 {
215 ...nominalHostPower,
216 onBattery: "true",
217 stale: false,
218 },
219 { backgroundActivityProfile: "battery-saver" },
220 ),
221 ),
222 ),
223 );
224
225 it.effect("does not gate work on stale host power values", () =>
226 Effect.gen(function* () {
227 const policy = yield* BackgroundPolicy.BackgroundPolicy;
228 yield* policy.reportClientActivity(
229 AuthSessionId.make("session-1"),
230 RpcClientId.make(1),
231 makeReport(),
232 );
233
234 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), true);
235 }).pipe(
236 Effect.provide(
237 layerFor(
238 {
239 ...nominalHostPower,
240 locked: "true",
241 onBattery: "true",
242 lowPowerMode: "true",
243 thermalState: "critical",
244 stale: true,
245 },
246 { backgroundActivityProfile: "battery-saver" },
247 ),
248 ),
249 ),
250 );
251});