1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { describe, expect, it } from "@effect/vitest";
3import {
4 ANTIGRAVITY_DEFAULT_MODEL,
5 AntigravitySettings,
6 ProviderDriverKind,
7 ProviderInstanceId,
8 ProviderSetupError,
9 type ServerProviderUsageLimits,
10} from "@t3tools/contracts";
11import * as Deferred from "effect/Deferred";
12import * as Effect from "effect/Effect";
13import * as Fiber from "effect/Fiber";
14import * as Layer from "effect/Layer";
15import * as Ref from "effect/Ref";
16import * as Schema from "effect/Schema";
17import * as Stream from "effect/Stream";
18import * as TestClock from "effect/testing/TestClock";
19import * as EffectAcpErrors from "effect-acp/errors";
20import type * as EffectAcpSchema from "effect-acp/compat";
21
22import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";
23import * as ServerConfig from "../config.ts";
24import * as ServerSettings from "../serverSettings.ts";
25import * as ProviderHostLive from "./ProviderHostLive.ts";
26import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
27import type * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
28import {
29 buildAntigravityModelsFromSession,
30 makeAntigravityProvider,
31} from "./AntigravityProvider.ts";
32
33const decodeSettings = Schema.decodeSync(AntigravitySettings);
34const instanceId = ProviderInstanceId.make("antigravity-test");
35const driver = ProviderDriverKind.make("antigravity");
36
37const initializeResult = {
38 protocolVersion: 1,
39 agentCapabilities: {
40 loadSession: true,
41 promptCapabilities: { image: true, audio: true, embeddedContext: true },
42 sessionCapabilities: { list: {}, resume: {} },
43 },
44 authMethods: [{ id: "oauth-personal", name: "Log in with Google" }],
45 agentInfo: {
46 name: "antigravity-acp",
47 title: "Google Antigravity",
48 version: "agy_acp_server_20260818_01_RC01",
49 },
50} satisfies EffectAcpSchema.InitializeResponse;
51
52const modelOptions = [
53 { value: "gemini-3.8-flash-high", name: "Gemini 3.8 Flash (High)" },
54 { value: "gemini-3.8-flash-medium", name: "Gemini 3.8 Flash (Medium)" },
55 { value: "gemini-3.8-flash-low", name: "Gemini 3.8 Flash (Low)" },
56 { value: "gemini-3.7-flash-high", name: "Gemini 3.7 Flash (High)" },
57 { value: "gemini-3.7-flash-medium", name: "Gemini 3.7 Flash (Medium)" },
58 { value: "gemini-3.7-flash-low", name: "Gemini 3.7 Flash (Low)" },
59 { value: "gemini-3.6-flash-high", name: "Gemini 3.6 Flash (High)" },
60 { value: "gemini-3.6-flash-medium", name: "Gemini 3.6 Flash (Medium)" },
61 { value: "gemini-3.6-flash-low", name: "Gemini 3.6 Flash (Low)" },
62 { value: "gemini-pro-agent", name: "Gemini 3.1 Pro (High)" },
63 { value: "gemini-3.1-pro-low", name: "Gemini 3.1 Pro (Low)" },
64];
65
66const modelConfig = {
67 id: "model",
68 name: "Model",
69 category: "model",
70 type: "select",
71 currentValue: "gemini-3.7-flash-high",
72 options: modelOptions,
73} satisfies EffectAcpSchema.SessionConfigOption;
74
75const sessionSetupResult = {
76 sessionId: "session-1",
77 models: {
78 currentModelId: "gemini-3.7-flash-high",
79 availableModels: modelOptions.map((option) => ({ modelId: option.value, name: option.name })),
80 },
81 configOptions: [
82 modelConfig,
83 {
84 id: "mode",
85 name: "Session Mode",
86 category: "mode",
87 type: "select",
88 currentValue: "default",
89 options: [
90 { value: "default", name: "Default" },
91 { value: "yolo", name: "YOLO" },
92 ],
93 },
94 ],
95} satisfies EffectAcpSchema.NewSessionResponse;
96
97const started = {
98 sessionId: "session-1",
99 initializeResult,
100 sessionSetupResult,
101 modelConfigId: "model",
102} satisfies AcpSessionRuntime.AcpSessionRuntimeStartResult;
103
104const commands = [
105 { name: "plan", description: "Create a plan", input: { type: "text", hint: "What to plan" } },
106 { name: "logout", description: "Sign out of Google" },
107] satisfies ReadonlyArray<EffectAcpSchema.AvailableCommand>;
108
109const expectedCommands = [
110 { name: "plan", description: "Create a plan", input: { hint: "What to plan" } },
111 { name: "logout", description: "Sign out of Google" },
112];
113
114const layerTest = ProviderHostLive.layer.pipe(
115 Layer.provideMerge(
116 Layer.mock(BackgroundPolicy.BackgroundPolicy)({
117 shouldRunScopeWork: () => Effect.succeed(false),
118 }),
119 ),
120 Layer.provideMerge(ServerSettings.layerTest()),
121 Layer.provideMerge(ServerSecretStore.layer),
122 Layer.provide(
123 ServerConfig.layerTest(process.cwd(), { prefix: "t3-antigravity-provider-test-" }).pipe(
124 Layer.provideMerge(NodeServices.layer),
125 ),
126 ),
127);
128
129type ProbeError = EffectAcpErrors.AcpError | ProviderSetupError;
130
131const makeHarness = Effect.fn("makeAntigravityProviderHarness")(function* (
132 options: {
133 readonly enabled?: boolean;
134 readonly safe?: boolean;
135 readonly probeUsage?: Effect.Effect<ServerProviderUsageLimits>;
136 } = {},
137) {
138 const initialProbe = yield* Deferred.make<EffectAcpSchema.InitializeResponse, ProbeError>();
139 const probeCalls = yield* Ref.make(0);
140 const safetyCalls = yield* Ref.make(0);
141 const probe = yield* Ref.make<Effect.Effect<EffectAcpSchema.InitializeResponse, ProbeError>>(
142 Deferred.await(initialProbe),
143 );
144 const safety = yield* Ref.make<Effect.Effect<boolean>>(Effect.succeed(options.safe ?? true));
145 const provider = yield* makeAntigravityProvider(
146 decodeSettings({ enabled: options.enabled ?? true, customModels: ["do-not-seed-me"] }),
147 {
148 stampIdentity: (snapshot) => Effect.succeed({ ...snapshot, instanceId, driver }),
149 ...(options.probeUsage ? { probeUsage: options.probeUsage } : {}),
150 probe: Ref.update(probeCalls, (count) => count + 1).pipe(
151 Effect.andThen(Ref.get(probe)),
152 Effect.flatten,
153 ),
154 supportsTextGeneration: Ref.update(safetyCalls, (count) => count + 1).pipe(
155 Effect.andThen(Ref.get(safety)),
156 Effect.flatten,
157 ),
158 },
159 );
160 const initialUpdate = yield* Stream.toPull(
161 provider.snapshot.streamChanges.pipe(
162 Stream.filter((snapshot) => snapshot.installed || snapshot.status === "error"),
163 ),
164 );
165 const initialize = Deferred.succeed(initialProbe, initializeResult).pipe(
166 Effect.andThen(initialUpdate),
167 Effect.asVoid,
168 );
169 return {
170 provider,
171 probe,
172 probeCalls,
173 safety,
174 safetyCalls,
175 initialProbe,
176 initialUpdate,
177 initialize,
178 };
179});
180
181describe("Antigravity model catalog", () => {
182 it("keeps the captured personal catalog's IDs, labels, order, and selected default", () => {
183 const models = buildAntigravityModelsFromSession(sessionSetupResult);
184 expect(models.map((model) => [model.slug, model.name])).toEqual(
185 modelOptions.map((option) => [option.value, option.name]),
186 );
187 expect(models.filter((model) => model.isDefault).map((model) => model.slug)).toEqual([
188 "gemini-3.7-flash-high",
189 ]);
190 expect(
191 models
192 .filter((model) => model.aliases?.includes(ANTIGRAVITY_DEFAULT_MODEL))
193 .map((model) => model.slug),
194 ).toEqual(["gemini-3.7-flash-high"]);
195 expect(models.every((model) => model.capabilities?.optionDescriptors?.length === 0)).toBe(true);
196 expect(models.every((model) => !model.isCustom)).toBe(true);
197 });
198
199 it("uses legacy session models only when model config is absent", () => {
200 const fromLegacy = buildAntigravityModelsFromSession({
201 models: sessionSetupResult.models,
202 });
203 expect(fromLegacy).toEqual(buildAntigravityModelsFromSession(sessionSetupResult));
204 expect(
205 buildAntigravityModelsFromSession({
206 ...sessionSetupResult,
207 configOptions: [{ ...modelConfig, options: [] }],
208 }),
209 ).toEqual([]);
210 });
211
212 it("flattens native option groups without combining distinct model IDs", () => {
213 const models = buildAntigravityModelsFromSession({
214 configOptions: [
215 {
216 ...modelConfig,
217 currentValue: "gemini-pro-agent",
218 options: [
219 { groupId: "Flash", name: "Flash", options: [modelOptions[3]!, modelOptions[4]!] },
220 { groupId: "Pro", name: "Pro", options: [modelOptions[9]!, modelOptions[3]!] },
221 ],
222 },
223 ],
224 });
225 expect(models.map((model) => model.slug)).toEqual([
226 "gemini-3.7-flash-high",
227 "gemini-3.7-flash-medium",
228 "gemini-pro-agent",
229 ]);
230 expect(models.find((model) => model.isDefault)?.slug).toBe("gemini-pro-agent");
231 expect(models.find((model) => model.aliases?.includes(ANTIGRAVITY_DEFAULT_MODEL))?.slug).toBe(
232 "gemini-pro-agent",
233 );
234 });
235});
236
237it.layer(layerTest)("Antigravity provider snapshots", (it) => {
238 it.effect("does not probe or run helper safety checks while disabled", () =>
239 Effect.scoped(
240 Effect.gen(function* () {
241 const harness = yield* makeHarness({ enabled: false });
242 const snapshot = yield* harness.provider.snapshot.refresh;
243 expect(snapshot).toMatchObject({
244 enabled: false,
245 installed: false,
246 status: "disabled",
247 auth: { status: "unknown" },
248 models: [],
249 setup: { canAuthenticate: true, canInstall: true },
250 showInteractionModeToggle: false,
251 supportsConversationRollback: false,
252 supportsTextGeneration: false,
253 });
254 expect(yield* Ref.get(harness.probeCalls)).toBe(0);
255 expect(yield* Ref.get(harness.safetyCalls)).toBe(0);
256 }),
257 ),
258 );
259
260 it.effect("ignores account updates while disabled", () =>
261 Effect.scoped(
262 Effect.gen(function* () {
263 const harness = yield* makeHarness({ enabled: false });
264 yield* harness.provider.onSessionStarted(started);
265 yield* harness.provider.onAvailableCommands(commands);
266 yield* harness.provider.onSignedOut;
267 const snapshot = yield* harness.provider.snapshot.refresh;
268 expect(snapshot).toMatchObject({
269 enabled: false,
270 installed: false,
271 status: "disabled",
272 auth: { status: "unknown" },
273 models: [],
274 slashCommands: [],
275 workspaceSnapshots: [],
276 });
277 expect(yield* Ref.get(harness.probeCalls)).toBe(0);
278 }),
279 ),
280 );
281
282 it.effect("publishes the configured sign-in method before any account is checked", () =>
283 Effect.scoped(
284 Effect.gen(function* () {
285 const provider = yield* makeAntigravityProvider(decodeSettings({ enabled: true }), {
286 stampIdentity: (snapshot) => Effect.succeed({ ...snapshot, instanceId, driver }),
287 probe: Effect.succeed(initializeResult),
288 supportsTextGeneration: Effect.succeed(true),
289 auth: { type: "gemini-api-key", label: "Gemini API key" },
290 });
291 expect((yield* provider.snapshot.getSnapshot).auth).toEqual({
292 status: "unknown",
293 type: "gemini-api-key",
294 });
295 }),
296 ),
297 );
298
299 it.effect("treats initialize as installation proof, not account or model discovery", () =>
300 Effect.scoped(
301 Effect.gen(function* () {
302 const harness = yield* makeHarness();
303 expect(yield* harness.provider.snapshot.getSnapshot).toMatchObject({
304 installed: false,
305 status: "warning",
306 auth: { status: "unknown" },
307 models: [],
308 });
309 yield* harness.initialize;
310 const snapshot = yield* harness.provider.snapshot.getSnapshot;
311 expect(snapshot).toMatchObject({
312 installed: true,
313 status: "warning",
314 version: "agy_acp_server_20260818_01_RC01",
315 auth: { status: "unknown" },
316 models: [],
317 });
318 expect(yield* Ref.get(harness.probeCalls)).toBe(1);
319 }),
320 ),
321 );
322
323 it.effect("publishes session metadata and native commands without another health probe", () =>
324 Effect.scoped(
325 Effect.gen(function* () {
326 const harness = yield* makeHarness();
327 yield* harness.initialize;
328 const nextReady = yield* Stream.toPull(
329 harness.provider.snapshot.streamChanges.pipe(
330 Stream.filter(
331 (snapshot) =>
332 snapshot.auth.status === "authenticated" && snapshot.slashCommands.length > 0,
333 ),
334 ),
335 );
336 yield* harness.provider.onSessionStarted(started, "/workspace");
337 yield* harness.provider.onAvailableCommands(commands, "/workspace");
338 const [snapshot] = yield* nextReady;
339 expect(snapshot).toMatchObject({
340 status: "ready",
341 auth: { status: "authenticated", type: "oauth-personal" },
342 supportsTextGeneration: true,
343 });
344 expect(snapshot.models).toEqual(buildAntigravityModelsFromSession(sessionSetupResult));
345 expect(snapshot.slashCommands).toEqual(expectedCommands);
346 expect((yield* harness.provider.snapshotForCwd("/workspace")).slashCommands).toEqual(
347 expectedCommands,
348 );
349 expect(yield* Ref.get(harness.probeCalls)).toBe(1);
350 }),
351 ),
352 );
353
354 it.effect("does not retain a disposable sign-in workspace", () =>
355 Effect.scoped(
356 Effect.gen(function* () {
357 const harness = yield* makeHarness();
358 yield* harness.initialize;
359 yield* harness.provider.onSessionStarted(started);
360 yield* harness.provider.onAvailableCommands(commands);
361 const snapshot = yield* harness.provider.snapshot.getSnapshot;
362 expect(snapshot.models).toHaveLength(11);
363 expect(snapshot.slashCommands).toEqual(expectedCommands);
364 expect(snapshot.workspaceSnapshots).toEqual([]);
365 }),
366 ),
367 );
368
369 it.effect("clears all account metadata on sign-out and authentication failure", () =>
370 Effect.scoped(
371 Effect.gen(function* () {
372 const harness = yield* makeHarness();
373 yield* harness.initialize;
374 for (const clear of [harness.provider.onSignedOut, harness.provider.onAuthRequired]) {
375 yield* harness.provider.onSessionStarted(started, "/workspace");
376 yield* harness.provider.onAvailableCommands(commands, "/workspace");
377 yield* clear;
378 expect(yield* harness.provider.snapshot.getSnapshot).toMatchObject({
379 installed: true,
380 status: "warning",
381 auth: { status: "unauthenticated" },
382 models: [],
383 slashCommands: [],
384 skills: [],
385 workspaceSnapshots: [],
386 supportsTextGeneration: false,
387 });
388 yield* harness.provider.onAvailableCommands(commands, "/workspace");
389 yield* harness.provider.onConfigOptionsUpdated([modelConfig]);
390 expect((yield* harness.provider.snapshotForCwd("/workspace")).slashCommands).toEqual([]);
391 expect((yield* harness.provider.snapshot.getSnapshot).models).toEqual([]);
392 }
393 const refreshed = yield* harness.provider.snapshot.refresh;
394 expect(refreshed.auth.status).toBe("unauthenticated");
395 expect(refreshed.supportsTextGeneration).toBe(false);
396 }),
397 ),
398 );
399
400 it.effect("replaces live model choices and accepts an empty catalog", () =>
401 Effect.scoped(
402 Effect.gen(function* () {
403 const harness = yield* makeHarness();
404 yield* harness.initialize;
405 yield* harness.provider.onSessionStarted(started, "/workspace");
406 yield* harness.provider.onAvailableCommands(commands, "/workspace");
407 const before = yield* harness.provider.snapshot.getSnapshot;
408 const configOptions = [
409 {
410 ...modelConfig,
411 currentValue: "gemini-3.8-flash-high",
412 options: modelOptions.slice(0, 3),
413 },
414 ];
415 const nextSnapshot = yield* Stream.toPull(
416 harness.provider.snapshot.streamChanges.pipe(
417 Stream.filter((snapshot) => snapshot.models.length === 3),
418 ),
419 );
420 yield* harness.provider.onConfigOptionsUpdated(configOptions);
421 expect((yield* nextSnapshot)[0]).toMatchObject({
422 models: buildAntigravityModelsFromSession({ configOptions }),
423 auth: before.auth,
424 workspaceSnapshots: before.workspaceSnapshots,
425 slashCommands: expectedCommands,
426 });
427 yield* harness.provider.onConfigOptionsUpdated([]);
428 expect((yield* harness.provider.snapshot.getSnapshot).models).toEqual([]);
429 }),
430 ),
431 );
432
433 it.effect("replaces one account's catalog instead of combining accounts", () =>
434 Effect.scoped(
435 Effect.gen(function* () {
436 const harness = yield* makeHarness();
437 yield* harness.initialize;
438 yield* harness.provider.onSessionStarted(started);
439 yield* harness.provider.onSignedOut;
440 yield* harness.provider.onSessionStarted({
441 ...started,
442 sessionSetupResult: {
443 sessionId: "new-account-session",
444 configOptions: [
445 { ...modelConfig, currentValue: "gemini-pro-agent", options: [modelOptions[9]!] },
446 ],
447 },
448 });
449 expect(
450 (yield* harness.provider.snapshot.getSnapshot).models.map((model) => model.slug),
451 ).toEqual(["gemini-pro-agent"]);
452 }),
453 ),
454 );
455
456 it.effect("retains known account metadata when a local health check fails", () =>
457 Effect.scoped(
458 Effect.gen(function* () {
459 const harness = yield* makeHarness();
460 yield* harness.initialize;
461 yield* harness.provider.onSessionStarted(started, "/workspace");
462 yield* harness.provider.onAvailableCommands(commands, "/workspace");
463 yield* Ref.set(
464 harness.probe,
465 Effect.fail(EffectAcpErrors.AcpRequestError.internalError("probe failed")),
466 );
467 const snapshot = yield* harness.provider.snapshot.refresh;
468 expect(snapshot).toMatchObject({
469 installed: true,
470 status: "error",
471 auth: { status: "authenticated" },
472 });
473 expect(snapshot.models).toEqual(buildAntigravityModelsFromSession(sessionSetupResult));
474 expect(snapshot.slashCommands).toEqual(expectedCommands);
475 expect(snapshot.workspaceSnapshots?.[0]?.cwd).toBe("/workspace");
476 }),
477 ),
478 );
479
480 it.effect("allows a slow packaged runtime health check to finish", () =>
481 Effect.scoped(
482 Effect.gen(function* () {
483 const harness = yield* makeHarness();
484 yield* harness.initialize;
485 const entered = yield* Deferred.make<void>();
486 const initialized = yield* Deferred.make<EffectAcpSchema.InitializeResponse>();
487 yield* Ref.set(
488 harness.probe,
489 Deferred.succeed(entered, undefined).pipe(Effect.andThen(Deferred.await(initialized))),
490 );
491
492 const refresh = yield* harness.provider.snapshot.refresh.pipe(Effect.forkChild);
493 yield* Deferred.await(entered);
494 yield* TestClock.adjust("47 seconds");
495 yield* Deferred.succeed(initialized, initializeResult);
496 const snapshot = yield* Fiber.join(refresh);
497
498 expect(snapshot).toMatchObject({
499 installed: true,
500 status: "warning",
501 auth: { status: "unknown" },
502 });
503 }),
504 ),
505 );
506
507 it.effect("closes a stalled health probe at its deadline", () =>
508 Effect.scoped(
509 Effect.gen(function* () {
510 const harness = yield* makeHarness();
511 yield* harness.initialize;
512 const entered = yield* Deferred.make<void>();
513 const closed = yield* Deferred.make<void>();
514 yield* Ref.set(
515 harness.probe,
516 Deferred.succeed(entered, undefined).pipe(
517 Effect.andThen(Effect.never),
518 Effect.ensuring(Deferred.succeed(closed, undefined)),
519 ),
520 );
521 const refresh = yield* harness.provider.snapshot.refresh.pipe(Effect.forkChild);
522 yield* Deferred.await(entered);
523 yield* TestClock.adjust("90 seconds");
524 const snapshot = yield* Fiber.join(refresh);
525 yield* Deferred.await(closed);
526 expect(snapshot.status).toBe("error");
527 expect(snapshot.message).toContain("90 seconds");
528 }),
529 ),
530 );
531
532 it.effect("distinguishes missing executables from a failed installed executable", () =>
533 Effect.scoped(
534 Effect.gen(function* () {
535 const harness = yield* makeHarness();
536 yield* harness.initialize;
537 const failures = [
538 {
539 error: new EffectAcpErrors.AcpSpawnError({ cause: { code: "ENOENT" } }),
540 installed: false,
541 },
542 {
543 error: new EffectAcpErrors.AcpSpawnError({ cause: { code: "EACCES" } }),
544 installed: true,
545 },
546 {
547 error: new ProviderSetupError({
548 instanceId,
549 operation: "resolve",
550 detail: "Antigravity is not installed.",
551 }),
552 installed: false,
553 },
554 ];
555 for (const { error, installed } of failures) {
556 yield* harness.provider.onSessionStarted(started, "/workspace");
557 yield* harness.provider.onAvailableCommands(commands, "/workspace");
558 yield* Ref.set(harness.probe, Effect.fail(error));
559 const snapshot = yield* harness.provider.snapshot.refresh;
560 expect(snapshot).toMatchObject({
561 installed,
562 status: "error",
563 auth: { status: "authenticated" },
564 });
565 expect(snapshot.models).toHaveLength(installed ? 11 : 0);
566 expect(snapshot.slashCommands).toHaveLength(installed ? 2 : 0);
567 expect(snapshot.workspaceSnapshots).toHaveLength(installed ? 1 : 0);
568 expect(snapshot.supportsTextGeneration).toBe(installed);
569 }
570 }),
571 ),
572 );
573
574 it.effect("does not let an old health result restore a signed-out account", () =>
575 Effect.scoped(
576 Effect.gen(function* () {
577 const harness = yield* makeHarness();
578 yield* harness.initialize;
579 yield* harness.provider.onSessionStarted(started);
580 const releaseProbe = yield* Deferred.make<EffectAcpSchema.InitializeResponse>();
581 const probeEntered = yield* Deferred.make<void>();
582 yield* Ref.set(
583 harness.probe,
584 Deferred.succeed(probeEntered, undefined).pipe(
585 Effect.andThen(Deferred.await(releaseProbe)),
586 ),
587 );
588 const refresh = yield* harness.provider.snapshot.refresh.pipe(Effect.forkChild);
589 yield* Deferred.await(probeEntered);
590 yield* harness.provider.onSignedOut;
591 yield* Deferred.succeed(releaseProbe, initializeResult);
592 const snapshot = yield* Fiber.join(refresh);
593 expect(snapshot).toMatchObject({
594 auth: { status: "unauthenticated" },
595 models: [],
596 supportsTextGeneration: false,
597 });
598 }),
599 ),
600 );
601
602 it.effect("exposes helper support only when the supplied safety check allows it", () =>
603 Effect.scoped(
604 Effect.gen(function* () {
605 const harness = yield* makeHarness({ safe: false });
606 yield* harness.initialize;
607 yield* harness.provider.onSessionStarted(started);
608 expect((yield* harness.provider.snapshot.getSnapshot).supportsTextGeneration).toBe(false);
609 yield* Ref.set(harness.safety, Effect.succeed(true));
610 yield* harness.provider.snapshot.refresh;
611 expect((yield* harness.provider.snapshot.getSnapshot).supportsTextGeneration).toBe(true);
612 }),
613 ),
614 );
615
616 it.effect("keeps discovered workspace skills through session and command updates", () =>
617 Effect.scoped(
618 Effect.gen(function* () {
619 const harness = yield* makeHarness();
620 yield* harness.initialize;
621 const skills = [
622 {
623 name: "deploy",
624 description: "Ship it",
625 path: "/workspace/.agent/skills/deploy",
626 enabled: true,
627 },
628 ];
629 const discovered = yield* harness.provider.snapshotForCwd("/workspace", skills);
630 expect(discovered.skills).toEqual(skills);
631 yield* harness.provider.onSessionStarted(started, "/workspace");
632 yield* harness.provider.onAvailableCommands(commands, "/workspace");
633 const after = yield* harness.provider.snapshot.getSnapshot;
634 expect(
635 after.workspaceSnapshots?.find((entry) => entry.cwd === "/workspace")?.skills,
636 ).toEqual(skills);
637 expect((yield* harness.provider.snapshotForCwd("/workspace")).skills).toEqual(skills);
638
639 const rescanned = [
640 ...skills,
641 { name: "review", path: "/workspace/.agent/skills/review", enabled: true },
642 ];
643 yield* harness.provider.snapshotForCwd("/workspace", rescanned);
644 yield* harness.provider.onSessionStarted(started, "/workspace");
645 expect(
646 (yield* harness.provider.snapshot.getSnapshot).workspaceSnapshots?.find(
647 (entry) => entry.cwd === "/workspace",
648 )?.skills,
649 ).toEqual(rescanned);
650 }),
651 ),
652 );
653
654 it.effect("bounds workspace metadata without starting sessions for workspace lookup", () =>
655 Effect.scoped(
656 Effect.gen(function* () {
657 const harness = yield* makeHarness();
658 yield* harness.initialize;
659 yield* harness.provider.onSessionStarted(started);
660 for (let index = 0; index < 35; index++) {
661 yield* harness.provider.onAvailableCommands(commands, `/workspace-${index}`);
662 }
663 const snapshot = yield* harness.provider.snapshotForCwd("/workspace-34");
664 expect(snapshot.workspaceSnapshots).toHaveLength(32);
665 expect(snapshot.workspaceSnapshots?.[0]?.cwd).toBe("/workspace-3");
666 expect(snapshot.slashCommands).toEqual(expectedCommands);
667 expect(yield* Ref.get(harness.probeCalls)).toBe(1);
668 }),
669 ),
670 );
671
672 it.effect(
673 "publishes limits, retains a successful read on failure, and clears them on sign-out",
674 () =>
675 Effect.scoped(
676 Effect.gen(function* () {
677 const limits: ServerProviderUsageLimits = {
678 checkedAt: "2026-10-09T12:00:00.000Z",
679 windows: [
680 {
681 id: "3p-weekly",
682 kind: "weekly",
683 label: "Claude & GPT (shared) · Weekly",
684 usedPercent: 75,
685 },
686 ],
687 };
688 const usage = yield* Ref.make(limits);
689 const harness = yield* makeHarness({ probeUsage: Ref.get(usage) });
690 yield* harness.initialize;
691 expect((yield* harness.provider.snapshot.getSnapshot).usageLimits).toEqual(limits);
692 yield* Ref.set(harness.probe, Effect.succeed(initializeResult));
693 yield* Ref.set(usage, {
694 checkedAt: "2026-10-09T12:01:00.000Z",
695 windows: [],
696 unavailable: { reason: "probeFailed" },
697 });
698 expect((yield* harness.provider.snapshot.refresh).usageLimits).toEqual(limits);
699 yield* harness.provider.onSignedOut;
700 expect((yield* harness.provider.snapshot.getSnapshot).usageLimits).toBeUndefined();
701 yield* Ref.set(usage, {
702 checkedAt: "2026-10-09T12:02:00.000Z",
703 windows: [],
704 unavailable: { reason: "unsupported" },
705 });
706 expect((yield* harness.provider.snapshot.refresh).usageLimits?.windows).toEqual([]);
707 }),
708 ),
709 );
710
711 it.effect("a quota read finishing after sign-out cannot restore account limits", () =>
712 Effect.scoped(
713 Effect.gen(function* () {
714 const readStarted = yield* Deferred.make<void>();
715 const readResult = yield* Deferred.make<ServerProviderUsageLimits>();
716 const usage = yield* Ref.make<Effect.Effect<ServerProviderUsageLimits>>(
717 Effect.succeed({ checkedAt: "2026-10-09T12:00:00.000Z", windows: [] }),
718 );
719 const harness = yield* makeHarness({ probeUsage: Ref.get(usage).pipe(Effect.flatten) });
720 yield* harness.initialize;
721 yield* Ref.set(harness.probe, Effect.succeed(initializeResult));
722 yield* Ref.set(
723 usage,
724 Deferred.succeed(readStarted, undefined).pipe(Effect.andThen(Deferred.await(readResult))),
725 );
726 const refresh = yield* harness.provider.snapshot.refresh.pipe(Effect.forkChild);
727 yield* Deferred.await(readStarted);
728 yield* harness.provider.onSignedOut;
729 yield* Deferred.succeed(readResult, {
730 checkedAt: "2026-10-09T12:01:00.000Z",
731 windows: [
732 { id: "gemini-5h", kind: "session", label: "Gemini · 5-hour", usedPercent: 10 },
733 ],
734 });
735 yield* Fiber.join(refresh);
736 expect((yield* harness.provider.snapshot.getSnapshot).usageLimits).toBeUndefined();
737 }),
738 ),
739 );
740});