Skip to content
Source
Sign inSign up
Open History to see the last commit

AntigravityProvider.test.ts

851 lines32.8 KB

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