1import * as ScriptProgress from "@t3tools/provider-core/server/ScriptProgress";
2import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
3import * as CodexInstallation from "./CodexInstallation.ts";
4import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
5import * as NodeServices from "@effect/platform-node/NodeServices";
6import { describe, it, assert, expect } from "@effect/vitest";
7import * as DateTime from "effect/DateTime";
8import * as Deferred from "effect/Deferred";
9import * as Effect from "effect/Effect";
10import * as Path from "effect/Path";
11import * as Exit from "effect/Exit";
12import * as Fiber from "effect/Fiber";
13import * as Layer from "effect/Layer";
14import * as PubSub from "effect/PubSub";
15import * as Ref from "effect/Ref";
16import * as Schema from "effect/Schema";
17import * as Scope from "effect/Scope";
18import * as Sink from "effect/Sink";
19import * as Stream from "effect/Stream";
20import * as TestClock from "effect/testing/TestClock";
21import * as CodexErrors from "effect-codex-app-server/errors";
22import {
23 EnvironmentId,
24 ClaudeSettings,
25 CodexSettings,
26 DEFAULT_SERVER_SETTINGS,
27 PROVIDER_WORKSPACE_SNAPSHOT_TTL_MS,
28 ProviderDriverKind,
29 ProviderInstanceId,
30 ServerSettings,
31 type ServerProvider,
32 type ServerProviderSlashCommand,
33 type ServerSettings as ContractServerSettings,
34} from "@t3tools/contracts";
35import * as PlatformError from "effect/PlatformError";
36import { HttpClient, HttpClientResponse } from "effect/http";
37import { ChildProcessSpawner } from "effect/process";
38import { deepMerge } from "@t3tools/shared/Struct";
39import { createModelCapabilities } from "@t3tools/shared/model";
40import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";
41
42import { checkCodexProviderStatus, type CodexAppServerProviderSnapshot } from "./CodexProvider.ts";
43import { checkClaudeProviderStatus } from "./ClaudeProvider.ts";
44import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";
45import * as AntigravityInstallation from "./AntigravityInstallation.ts";
46import * as ModelManifest from "./ModelManifest.ts";
47import { applyProviderCompatibility } from "./providerCompatibility.ts";
48import * as ResetCreditCoordinator from "./resetCreditCoordinator.ts";
49import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderLatestVersions";
50import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
51import * as OpenCodeRuntime from "@t3tools/provider-opencode/server/OpenCodeRuntime";
52import * as OpenCodeServerLedger from "@t3tools/provider-opencode/server/OpenCodeServerLedger";
53import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
54import * as ProviderInstanceRegistryHydration from "./ProviderInstanceRegistryHydration.ts";
55import * as ServerConfig from "../config.ts";
56import * as ServerSettingsModule from "../serverSettings.ts";
57import {
58 readProviderStatusCache,
59 resolveProviderStatusCachePath,
60 writeProviderStatusCache,
61} from "./providerStatusCache.ts";
62import { COMPACT_SLASH_COMMAND } from "@t3tools/provider-core/server/snapshotProbe";
63import type {
64 ProviderInstance,
65 ProviderWorkspaceSnapshot,
66} from "@t3tools/provider-core/server/driver";
67import * as ProviderInstanceRegistry from "./ProviderInstanceRegistry.ts";
68import * as ProviderRegistry from "./ProviderRegistry.ts";
69import { makeManualOnlyProviderMaintenanceCapabilities } from "@t3tools/provider-core/server/maintenanceResolver";
70const decodeServerSettings = Schema.decodeSync(ServerSettings);
71const encodeServerSettings = Schema.encodeSync(ServerSettings);
72const encodedDefaultServerSettings = encodeServerSettings(DEFAULT_SERVER_SETTINGS);
73
74/** Default slots that settings mark disabled, so their probes never spawn. */
75const disabledDefaultSlots = (
76 ...drivers: ReadonlyArray<string>
77): ContractServerSettings["providerInstances"] =>
78 Object.fromEntries(
79 drivers.map((driver) => [
80 ProviderInstanceId.make(driver),
81 { driver: ProviderDriverKind.make(driver), enabled: false },
82 ]),
83 );
84
85const defaultClaudeSettings: ClaudeSettings = Schema.decodeSync(ClaudeSettings)({});
86const defaultCodexSettings: CodexSettings = Schema.decodeSync(CodexSettings)({});
87const decodeCodexSettings = Schema.decodeSync(CodexSettings);
88const disabledCodexSettings: CodexSettings = Schema.decodeSync(CodexSettings)({
89 enabled: false,
90});
91
92process.env.T3CODE_CURSOR_ENABLED = "1";
93
94// ── Test helpers ────────────────────────────────────────────────────
95
96const encoder = new TextEncoder();
97const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z");
98const withBundledCompatibility = (snapshot: ServerProvider) =>
99 applyProviderCompatibility(
100 snapshot,
101 undefined,
102 ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility,
103 );
104
105// Provider metadata checks use a bundled manifest and stubbed HTTP.
106const layerTestHttpClient = Layer.succeed(
107 HttpClient.HttpClient,
108 HttpClient.make((request) =>
109 Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ version: "0.0.0" }))),
110 ),
111).pipe(Layer.provideMerge(ModelManifest.layerTest));
112
113const layerBackgroundPolicyAlwaysRun = Layer.mock(BackgroundPolicy.BackgroundPolicy)({
114 reportClientActivity: () => Effect.void,
115 removeRpcClient: () => Effect.void,
116 reportHostPowerState: () => Effect.void,
117 snapshot: Effect.succeed({
118 hostPower: {
119 source: "unknown",
120 idle: "unknown",
121 idleSeconds: null,
122 locked: "unknown",
123 suspended: false,
124 onBattery: "unknown",
125 lowPowerMode: "unknown",
126 thermalState: "unknown",
127 stale: true,
128 updatedAt: TEST_EPOCH,
129 },
130 leases: [],
131 activeForegroundLeaseCount: 0,
132 activeScopeKeys: [],
133 shouldRunOpportunisticWork: true,
134 updatedAt: TEST_EPOCH,
135 }),
136 streamChanges: Stream.empty,
137 hasDemand: () => Effect.succeed(true),
138 shouldRunScopeWork: () => Effect.succeed(true),
139 shouldRunOpportunisticWork: Effect.succeed(true),
140});
141
142function selectDescriptor(
143 id: string,
144 label: string,
145 options: ReadonlyArray<{ id: string; label: string; isDefault?: boolean }>,
146) {
147 return {
148 id,
149 label,
150 type: "select" as const,
151 options: [...options],
152 ...(options.find((option) => option.isDefault)?.id
153 ? { currentValue: options.find((option) => option.isDefault)?.id }
154 : {}),
155 };
156}
157
158function booleanDescriptor(id: string, label: string) {
159 return {
160 id,
161 label,
162 type: "boolean" as const,
163 };
164}
165
166type TestClaudeCapabilities = {
167 readonly email: string | undefined;
168 readonly subscriptionType: string | undefined;
169 readonly tokenSource: string | undefined;
170 readonly apiKeySource: string | undefined;
171 readonly apiProvider: string | undefined;
172 readonly slashCommands: ReadonlyArray<ServerProviderSlashCommand>;
173};
174
175function claudeCapabilities(overrides: Partial<TestClaudeCapabilities> = {}) {
176 return () =>
177 Effect.succeed({
178 email: undefined,
179 subscriptionType: undefined,
180 tokenSource: undefined,
181 apiKeySource: undefined,
182 apiProvider: undefined,
183 slashCommands: [],
184 ...overrides,
185 });
186}
187
188const noClaudeCapabilities = () =>
189 Effect.sync(() => undefined as TestClaudeCapabilities | undefined);
190
191function mockHandle(result: { stdout: string; stderr: string; code: number }) {
192 return ChildProcessSpawner.makeHandle({
193 pid: ChildProcessSpawner.ProcessId(1),
194 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(result.code)),
195 isRunning: Effect.succeed(false),
196 kill: () => Effect.void,
197 unref: Effect.succeed(Effect.void),
198 stdin: Sink.drain,
199 stdout: Stream.make(encoder.encode(result.stdout)),
200 stderr: Stream.make(encoder.encode(result.stderr)),
201 all: Stream.empty,
202 getInputFd: () => Sink.drain,
203 getOutputFd: () => Stream.empty,
204 });
205}
206
207function layerMockSpawner(
208 handler: (args: ReadonlyArray<string>) => {
209 stdout: string;
210 stderr: string;
211 code: number;
212 },
213) {
214 return Layer.succeed(
215 ChildProcessSpawner.ChildProcessSpawner,
216 ChildProcessSpawner.make((command) => {
217 const cmd = command as unknown as { args: ReadonlyArray<string> };
218 return Effect.succeed(mockHandle(handler(cmd.args)));
219 }),
220 );
221}
222
223function recordingMockSpawnerLayer(
224 handler: (args: ReadonlyArray<string>) => {
225 stdout: string;
226 stderr: string;
227 code: number;
228 },
229) {
230 const commands: Array<{
231 readonly args: ReadonlyArray<string>;
232 readonly env: NodeJS.ProcessEnv | undefined;
233 }> = [];
234 const layer = Layer.succeed(
235 ChildProcessSpawner.ChildProcessSpawner,
236 ChildProcessSpawner.make((command) => {
237 const cmd = command as unknown as {
238 args: ReadonlyArray<string>;
239 options?: {
240 readonly env?: NodeJS.ProcessEnv;
241 };
242 };
243 commands.push({ args: cmd.args, env: cmd.options?.env });
244 return Effect.succeed(mockHandle(handler(cmd.args)));
245 }),
246 );
247 return { layer, commands };
248}
249
250function layerMockCommandSpawner(
251 handler: (
252 command: string,
253 args: ReadonlyArray<string>,
254 ) => { stdout: string; stderr: string; code: number },
255) {
256 return Layer.succeed(
257 ChildProcessSpawner.ChildProcessSpawner,
258 ChildProcessSpawner.make((command) => {
259 const cmd = command as unknown as {
260 command: string;
261 args: ReadonlyArray<string>;
262 };
263 return Effect.succeed(mockHandle(handler(cmd.command, cmd.args)));
264 }),
265 );
266}
267
268function layerFailingSpawner(description: string) {
269 return Layer.succeed(
270 ChildProcessSpawner.ChildProcessSpawner,
271 ChildProcessSpawner.make(() =>
272 Effect.fail(
273 PlatformError.systemError({
274 _tag: "NotFound",
275 module: "ChildProcess",
276 method: "spawn",
277 description,
278 }),
279 ),
280 ),
281 );
282}
283
284function layerHangingScopedSpawner(killCalls: Ref.Ref<number>) {
285 return Layer.succeed(
286 ChildProcessSpawner.ChildProcessSpawner,
287 ChildProcessSpawner.make(() =>
288 Effect.gen(function* () {
289 const handle = ChildProcessSpawner.makeHandle({
290 pid: ChildProcessSpawner.ProcessId(1),
291 exitCode: Effect.never,
292 isRunning: Effect.succeed(true),
293 kill: () => Ref.update(killCalls, (current) => current + 1),
294 unref: Effect.succeed(Effect.void),
295 stdin: Sink.drain,
296 stdout: Stream.never,
297 stderr: Stream.never,
298 all: Stream.never,
299 getInputFd: () => Sink.drain,
300 getOutputFd: () => Stream.empty,
301 });
302 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
303 return handle;
304 }),
305 ),
306 );
307}
308
309const codexModelCapabilities = createModelCapabilities({
310 optionDescriptors: [
311 selectDescriptor("reasoningEffort", "Reasoning", [
312 { id: "high", label: "High", isDefault: true },
313 { id: "low", label: "Low" },
314 ]),
315 booleanDescriptor("fastMode", "Fast Mode"),
316 ],
317}) satisfies NonNullable<ServerProvider["models"][number]["capabilities"]>;
318
319function makeCodexProbeSnapshot(
320 input: Partial<CodexAppServerProviderSnapshot> = {},
321): CodexAppServerProviderSnapshot {
322 return {
323 version: "1.0.0",
324 account: {
325 account: {
326 type: "chatgpt",
327 email: "test@example.com",
328 planType: "pro",
329 },
330 requiresOpenaiAuth: false,
331 },
332 models: [
333 {
334 slug: "gpt-live-codex",
335 name: "GPT Live Codex",
336 isCustom: false,
337 capabilities: codexModelCapabilities,
338 },
339 ],
340 skills: [],
341 ...input,
342 };
343}
344
345function makeMutableServerSettingsService(
346 initial: ContractServerSettings = DEFAULT_SERVER_SETTINGS,
347) {
348 return Effect.gen(function* () {
349 const settingsRef = yield* Ref.make(initial);
350 const changes = yield* PubSub.unbounded<ContractServerSettings>();
351
352 return {
353 start: Effect.void,
354 ready: Effect.void,
355 getSettings: Ref.get(settingsRef),
356 updateSettings: (patch) =>
357 Effect.gen(function* () {
358 const current = yield* Ref.get(settingsRef);
359 const next = applyServerSettingsPatch(current, patch);
360 encodeServerSettings(next);
361 yield* Ref.set(settingsRef, next);
362 yield* PubSub.publish(changes, next);
363 return next;
364 }),
365 updateProviderInstance: (mutation, patch = {}) =>
366 Effect.gen(function* () {
367 const current = yield* Ref.get(settingsRef);
368 const next = ServerSettingsModule.applyProviderInstanceMutation(
369 applyServerSettingsPatch(current, patch),
370 mutation,
371 );
372 encodeServerSettings(next);
373 yield* Ref.set(settingsRef, next);
374 yield* PubSub.publish(changes, next);
375 return next;
376 }),
377 withSettingsSnapshot: (use) => Ref.get(settingsRef).pipe(Effect.flatMap(use)),
378 get streamChanges() {
379 return Stream.fromPubSub(changes);
380 },
381 get subscribeChanges() {
382 return PubSub.subscribe(changes).pipe(
383 Effect.map((subscription) => Stream.fromSubscription(subscription)),
384 );
385 },
386 } satisfies ServerSettingsModule.ServerSettingsService["Service"];
387 });
388}
389
390// The registry writes the status cache and only then publishes the change, so
391// a subscriber that sees `checkedAt` on the stream knows the file is on disk.
392// Subscribed before the publish that triggers it; a spin on the file would
393// race the write and lose on a slow host.
394const awaitPersistedProvider = (
395 registry: ProviderRegistry.ProviderRegistry["Service"],
396 checkedAt: string,
397) =>
398 registry.streamChanges.pipe(
399 Stream.filter((providers) => providers.some((provider) => provider.checkedAt === checkedAt)),
400 Stream.take(1),
401 Stream.runDrain,
402 Effect.forkScoped,
403 );
404
405const layerTestNodeServices = Layer.mergeAll(
406 NodeServices.layer,
407 Layer.mock(CodexInstallation.CodexInstallation)({
408 managedDirectory: "unused-managed-installation",
409 }),
410 Layer.mock(ServerSecretStore.ServerSecretStore)({}),
411 Layer.succeed(ServerEnvironment.ServerEnvironmentIdentity, {
412 getEnvironmentId: Effect.succeed(EnvironmentId.make("00000000-0000-4000-8000-000000000001")),
413 }),
414);
415
416it.layer(
417 Layer.mergeAll(layerTestNodeServices, ServerSettingsModule.layerTest(), layerTestHttpClient),
418)("ProviderRegistry", (it) => {
419 describe("checkCodexProviderStatus", () => {
420 it.effect("uses the app-server account and model list for provider status", () =>
421 Effect.gen(function* () {
422 const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>
423 Effect.succeed(
424 makeCodexProbeSnapshot({
425 skills: [
426 {
427 name: "github:gh-fix-ci",
428 path: "/Users/test/.codex/skills/gh-fix-ci/SKILL.md",
429 enabled: true,
430 displayName: "CI Debug",
431 shortDescription: "Debug failing GitHub Actions checks",
432 },
433 ],
434 }),
435 ),
436 );
437 assert.strictEqual(status.status, "ready");
438 assert.strictEqual(status.installed, true);
439 assert.strictEqual(status.version, "1.0.0");
440 assert.strictEqual(status.auth.status, "authenticated");
441 assert.strictEqual(status.auth.type, "chatgpt");
442 assert.strictEqual(status.auth.label, "ChatGPT Pro 20x Subscription");
443 assert.strictEqual(status.auth.email, "test@example.com");
444 assert.deepStrictEqual(status.models, [
445 {
446 slug: "gpt-live-codex",
447 name: "GPT Live Codex",
448 isCustom: false,
449 capabilities: codexModelCapabilities,
450 },
451 ]);
452 assert.deepStrictEqual(status.skills, [
453 {
454 name: "github:gh-fix-ci",
455 path: "/Users/test/.codex/skills/gh-fix-ci/SKILL.md",
456 enabled: true,
457 displayName: "CI Debug",
458 shortDescription: "Debug failing GitHub Actions checks",
459 },
460 ]);
461 assert.deepStrictEqual(status.slashCommands.slice(1), [
462 {
463 name: "goal",
464 description: "Set a goal Codex keeps working toward until it is done",
465 input: { hint: "Objective, or pause, resume, clear" },
466 },
467 {
468 name: "feedback",
469 description: "Send this thread and Codex logs to OpenAI",
470 input: { hint: "Describe the issue (optional)" },
471 },
472 ]);
473 }),
474 );
475
476 it.effect("passes configured launch args to the Codex provider probe", () =>
477 Effect.gen(function* () {
478 let observedLaunchArgs: string | undefined;
479 const settings = decodeCodexSettings({ launchArgs: "--strict-config --enable foo" });
480
481 const status = yield* checkCodexProviderStatus(settings, (input) => {
482 observedLaunchArgs = input.launchArgs;
483 return Effect.succeed(makeCodexProbeSnapshot());
484 });
485
486 assert.strictEqual(status.status, "ready");
487 assert.strictEqual(observedLaunchArgs, "--strict-config --enable foo");
488 }),
489 );
490
491 it.effect("returns unauthenticated when app-server requires OpenAI auth", () =>
492 Effect.gen(function* () {
493 const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>
494 Effect.succeed(
495 makeCodexProbeSnapshot({
496 account: {
497 account: null,
498 requiresOpenaiAuth: true,
499 },
500 }),
501 ),
502 );
503
504 assert.strictEqual(status.status, "error");
505 assert.strictEqual(status.auth.status, "unauthenticated");
506 assert.strictEqual(
507 status.message,
508 "Codex CLI is not authenticated. Run `codex login` and try again.",
509 );
510 }),
511 );
512
513 it.effect("returns ready with unknown auth when app-server does not require OpenAI auth", () =>
514 Effect.gen(function* () {
515 const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>
516 Effect.succeed(
517 makeCodexProbeSnapshot({
518 account: {
519 account: null,
520 requiresOpenaiAuth: false,
521 },
522 }),
523 ),
524 );
525
526 assert.strictEqual(status.status, "ready");
527 assert.strictEqual(status.auth.status, "unknown");
528 }),
529 );
530
531 it.effect("returns an api key label for codex api key auth", () =>
532 Effect.gen(function* () {
533 const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>
534 Effect.succeed(
535 makeCodexProbeSnapshot({
536 account: {
537 account: { type: "apiKey" },
538 requiresOpenaiAuth: false,
539 },
540 }),
541 ),
542 );
543
544 assert.strictEqual(status.status, "ready");
545 assert.strictEqual(status.auth.status, "authenticated");
546 assert.strictEqual(status.auth.type, "apiKey");
547 assert.strictEqual(status.auth.label, "OpenAI API Key");
548 }),
549 );
550
551 it.effect("returns an Amazon Bedrock label for codex Bedrock auth", () =>
552 Effect.gen(function* () {
553 const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>
554 Effect.succeed(
555 makeCodexProbeSnapshot({
556 account: {
557 account: { type: "amazonBedrock" },
558 requiresOpenaiAuth: false,
559 },
560 }),
561 ),
562 );
563
564 assert.strictEqual(status.status, "ready");
565 assert.strictEqual(status.auth.status, "authenticated");
566 assert.strictEqual(status.auth.type, "amazonBedrock");
567 assert.strictEqual(status.auth.label, "Amazon Bedrock");
568 }),
569 );
570
571 it.effect.each([
572 "codex",
573 "/Applications/Custom App.app/Contents/Resources/codex",
574 "C:\\Tools\\codex.exe",
575 ])("explains how to configure a Codex executable that cannot start: %s", (binaryPath) =>
576 Effect.gen(function* () {
577 const settings = { ...defaultCodexSettings, binaryPath };
578 const status = yield* checkCodexProviderStatus(settings, (input) => {
579 assert.strictEqual(input.binaryPath, binaryPath);
580 return Effect.fail(
581 new CodexErrors.CodexAppServerSpawnError({
582 command: `${binaryPath} app-server`,
583 cause: new Error("spawn ENOENT"),
584 }),
585 );
586 });
587 assert.strictEqual(status.status, "error");
588 assert.strictEqual(status.installed, false);
589 assert.strictEqual(status.auth.status, "unknown");
590 assert.include(status.message, binaryPath);
591 assert.include(status.message, "Settings → Providers → Codex → Binary path on the server");
592 assert.strictEqual(
593 status.message?.includes("Installing ChatGPT or Codex desktop"),
594 binaryPath === "codex",
595 );
596 assert.strictEqual(settings.binaryPath, binaryPath);
597 }),
598 );
599
600 it.effect("closes the app-server probe scope when provider status times out", () =>
601 Effect.gen(function* () {
602 const killCalls = yield* Ref.make(0);
603 const statusFiber = yield* checkCodexProviderStatus(defaultCodexSettings).pipe(
604 Effect.provide(layerHangingScopedSpawner(killCalls)),
605 Effect.forkChild,
606 );
607
608 yield* Effect.yieldNow;
609 yield* TestClock.adjust("11 seconds");
610 yield* Effect.yieldNow;
611
612 const status = yield* Fiber.join(statusFiber);
613 assert.strictEqual(status.status, "error");
614 assert.strictEqual(
615 status.message,
616 "Timed out while checking Codex app-server provider status.",
617 );
618 assert.strictEqual(yield* Ref.get(killCalls), 1);
619 }),
620 );
621 });
622
623 describe("ProviderRegistry.layer", () => {
624 it("stores workspace skills and commands without changing machine metadata", () => {
625 const provider = {
626 instanceId: ProviderInstanceId.make("codex"),
627 driver: ProviderDriverKind.make("codex"),
628 status: "ready",
629 enabled: true,
630 installed: true,
631 auth: { status: "authenticated" },
632 checkedAt: "2026-03-25T00:00:00.000Z",
633 version: "1.0.0",
634 models: [],
635 slashCommands: [{ name: "global" }],
636 skills: [{ name: "global", path: "/global/SKILL.md", enabled: true }],
637 } satisfies ServerProvider;
638 const scopedSnapshot = {
639 ...provider,
640 checkedAt: "2026-03-25T00:01:00.000Z",
641 slashCommands: [{ name: "project" }],
642 skills: [{ name: "project", path: "/project/SKILL.md", enabled: true }],
643 } satisfies ServerProvider;
644
645 const result = ProviderRegistry.upsertProviderWorkspaceSnapshot(
646 provider,
647 "/project",
648 scopedSnapshot,
649 );
650
651 assert.deepStrictEqual(result.slashCommands, provider.slashCommands);
652 assert.deepStrictEqual(result.skills, provider.skills);
653 assert.deepStrictEqual(result.workspaceSnapshots, [
654 {
655 cwd: "/project",
656 checkedAt: scopedSnapshot.checkedAt,
657 slashCommands: scopedSnapshot.slashCommands,
658 skills: scopedSnapshot.skills,
659 },
660 ]);
661
662 const pendingSnapshot = {
663 ...scopedSnapshot,
664 slashCommands: [COMPACT_SLASH_COMMAND],
665 slashCommandsPending: true,
666 } satisfies ProviderWorkspaceSnapshot;
667 const partial = ProviderRegistry.upsertProviderWorkspaceSnapshot(
668 result,
669 "/project",
670 pendingSnapshot,
671 );
672 assert.deepStrictEqual(partial.workspaceSnapshots?.[0]?.slashCommands, [{ name: "project" }]);
673 assert.strictEqual(partial.workspaceSnapshots?.[0]?.slashCommandsPending, true);
674 const otherProject = ProviderRegistry.upsertProviderWorkspaceSnapshot(
675 partial,
676 "/other-project",
677 pendingSnapshot,
678 );
679 assert.deepStrictEqual(otherProject.workspaceSnapshots?.[1]?.slashCommands, [
680 COMPACT_SLASH_COMMAND,
681 ]);
682 const recovered = ProviderRegistry.upsertProviderWorkspaceSnapshot(partial, "/project", {
683 ...scopedSnapshot,
684 slashCommands: [COMPACT_SLASH_COMMAND, { name: "replacement" }],
685 });
686 assert.deepStrictEqual(recovered.workspaceSnapshots?.[0]?.slashCommands, [
687 COMPACT_SLASH_COMMAND,
688 { name: "replacement" },
689 ]);
690 assert.strictEqual(recovered.workspaceSnapshots?.[0]?.slashCommandsPending, undefined);
691 });
692
693 it("preserves previously discovered provider models when a refresh returns none", () => {
694 const previousProvider = {
695 instanceId: ProviderInstanceId.make("cursor"),
696 driver: ProviderDriverKind.make("cursor"),
697 status: "ready",
698 enabled: true,
699 installed: true,
700 auth: { status: "authenticated" },
701 checkedAt: "2026-04-14T00:00:00.000Z",
702 version: "2026.04.09-f2b0fcd",
703 models: [
704 {
705 slug: "claude-opus-4-6",
706 name: "Opus 4.6",
707 isCustom: false,
708 capabilities: createModelCapabilities({
709 optionDescriptors: [
710 selectDescriptor("reasoning", "Reasoning", [
711 { id: "high", label: "High", isDefault: true },
712 ]),
713 booleanDescriptor("fastMode", "Fast Mode"),
714 booleanDescriptor("thinking", "Thinking"),
715 ],
716 }),
717 },
718 ],
719 slashCommands: [{ name: "review", description: "Review changes" }],
720 skills: [
721 {
722 name: "typescript",
723 description: "TypeScript help",
724 path: "/skills/typescript/SKILL.md",
725 enabled: true,
726 },
727 ],
728 } as const satisfies ServerProvider;
729 const refreshedProvider = {
730 ...previousProvider,
731 checkedAt: "2026-04-14T00:01:00.000Z",
732 models: [],
733 slashCommands: [],
734 skills: [],
735 } satisfies ServerProvider;
736
737 assert.deepStrictEqual(
738 ProviderRegistry.mergeProviderSnapshot(previousProvider, refreshedProvider).models,
739 [...previousProvider.models],
740 );
741 assert.deepStrictEqual(
742 ProviderRegistry.mergeProviderSnapshot(previousProvider, refreshedProvider).slashCommands,
743 [],
744 );
745 assert.deepStrictEqual(
746 ProviderRegistry.mergeProviderSnapshot(previousProvider, refreshedProvider).skills,
747 [],
748 );
749 });
750
751 it("drops custom models the refreshed snapshot no longer carries", () => {
752 const previousProvider = {
753 instanceId: ProviderInstanceId.make("claudeAgent"),
754 driver: ProviderDriverKind.make("claudeAgent"),
755 status: "ready",
756 enabled: true,
757 installed: true,
758 auth: { status: "authenticated" },
759 checkedAt: "2026-04-14T00:00:00.000Z",
760 version: "2.1.0",
761 models: [
762 {
763 slug: "claude-sonnet-4-6",
764 name: "Sonnet 4.6",
765 isCustom: false,
766 capabilities: null,
767 },
768 {
769 slug: "removed-custom",
770 name: "removed-custom",
771 isCustom: true,
772 capabilities: null,
773 },
774 ],
775 slashCommands: [],
776 skills: [],
777 } as const satisfies ServerProvider;
778 const refreshedProvider = {
779 ...previousProvider,
780 checkedAt: "2026-04-14T00:01:00.000Z",
781 models: [previousProvider.models[0]],
782 } satisfies ServerProvider;
783
784 assert.deepStrictEqual(
785 ProviderRegistry.mergeProviderSnapshot(previousProvider, refreshedProvider).models,
786 [...refreshedProvider.models],
787 );
788 });
789
790 it("does not bring back models the installed CLI is too old to run", () => {
791 // The pending snapshot lists the whole catalog before the version is known.
792 const pendingProvider = {
793 instanceId: ProviderInstanceId.make("claudeAgent"),
794 driver: ProviderDriverKind.make("claudeAgent"),
795 status: "warning",
796 enabled: true,
797 installed: false,
798 auth: { status: "unknown" },
799 checkedAt: "2026-04-14T00:00:00.000Z",
800 version: null,
801 models: [
802 { slug: "claude-old", name: "Old", isCustom: false, capabilities: null },
803 { slug: "claude-next", name: "Next", isCustom: false, capabilities: null },
804 ],
805 slashCommands: [],
806 skills: [],
807 } as const satisfies ServerProvider;
808 const probedProvider = {
809 ...pendingProvider,
810 status: "ready",
811 installed: true,
812 auth: { status: "authenticated" },
813 version: "1.0.0",
814 models: [pendingProvider.models[0]],
815 updateRequiredModels: [{ slug: "claude-next", name: "Next", minVersion: "1.1.0" }],
816 } satisfies ServerProvider;
817
818 assert.deepStrictEqual(
819 ProviderRegistry.mergeProviderSnapshot(pendingProvider, probedProvider).models,
820 [...probedProvider.models],
821 );
822 });
823
824 it("drops stale ACP Registry models missing from a completed discovery probe", () => {
825 const previousProvider = {
826 instanceId: ProviderInstanceId.make("acpRegistry_codex"),
827 driver: ProviderDriverKind.make("acpRegistry"),
828 status: "ready",
829 enabled: true,
830 installed: true,
831 auth: { status: "authenticated" },
832 checkedAt: "2026-08-13T00:00:00.000Z",
833 version: "1.2.0",
834 models: [
835 { slug: "gpt-5.6-sol", name: "GPT-5.6-Sol", isCustom: false, capabilities: null },
836 {
837 slug: "gpt-5.6-sol[low]",
838 name: "GPT-5.6-Sol (low)",
839 isCustom: false,
840 capabilities: null,
841 },
842 { slug: "default", name: "Default", isCustom: false, capabilities: null },
843 ],
844 slashCommands: [],
845 skills: [],
846 } as const satisfies ServerProvider;
847 const refreshedProvider = {
848 ...previousProvider,
849 checkedAt: "2026-08-13T00:01:00.000Z",
850 models: [{ slug: "gpt-5.6-sol", name: "GPT-5.6-Sol", isCustom: false, capabilities: null }],
851 } satisfies ServerProvider;
852
853 assert.deepStrictEqual(
854 ProviderRegistry.mergeProviderSnapshot(previousProvider, refreshedProvider).models,
855 [...refreshedProvider.models],
856 );
857 });
858
859 it("retains ACP Registry models while discovery has not completed", () => {
860 const previousProvider = {
861 instanceId: ProviderInstanceId.make("acpRegistry_codex"),
862 driver: ProviderDriverKind.make("acpRegistry"),
863 status: "ready",
864 enabled: true,
865 installed: true,
866 auth: { status: "authenticated" },
867 checkedAt: "2026-08-13T00:00:00.000Z",
868 version: "1.2.0",
869 models: [{ slug: "gpt-5.6-sol", name: "GPT-5.6-Sol", isCustom: false, capabilities: null }],
870 slashCommands: [],
871 skills: [],
872 } as const satisfies ServerProvider;
873 const checkingProvider = {
874 ...previousProvider,
875 checkedAt: "2026-08-13T00:01:00.000Z",
876 auth: { status: "unknown" },
877 models: [{ slug: "default", name: "Default", isCustom: false, capabilities: null }],
878 } satisfies ServerProvider;
879 const failedProbeProvider = {
880 ...checkingProvider,
881 status: "warning",
882 } satisfies ServerProvider;
883
884 assert.deepStrictEqual(
885 ProviderRegistry.mergeProviderSnapshot(previousProvider, checkingProvider).models,
886 [
887 { slug: "default", name: "Default", isCustom: false, capabilities: null },
888 { slug: "gpt-5.6-sol", name: "GPT-5.6-Sol", isCustom: false, capabilities: null },
889 ],
890 );
891 assert.deepStrictEqual(
892 ProviderRegistry.mergeProviderSnapshot(previousProvider, failedProbeProvider).models,
893 [
894 { slug: "default", name: "Default", isCustom: false, capabilities: null },
895 { slug: "gpt-5.6-sol", name: "GPT-5.6-Sol", isCustom: false, capabilities: null },
896 ],
897 );
898 });
899
900 it("drops stale OpenCode models missing from a successful refresh", () => {
901 const previousProvider = {
902 instanceId: ProviderInstanceId.make("opencode"),
903 driver: ProviderDriverKind.make("opencode"),
904 status: "ready",
905 enabled: true,
906 installed: true,
907 auth: { status: "authenticated" },
908 checkedAt: "2026-07-17T00:00:00.000Z",
909 version: "1.0.0",
910 models: [
911 {
912 slug: "github/gpt-5",
913 name: "GPT-5",
914 subProvider: "GitHub",
915 isCustom: false,
916 capabilities: null,
917 },
918 {
919 slug: "removed-plugin/model",
920 name: "Removed Plugin Model",
921 subProvider: "Removed Plugin",
922 isCustom: false,
923 capabilities: null,
924 },
925 ],
926 slashCommands: [],
927 skills: [],
928 } as const satisfies ServerProvider;
929 const refreshedProvider = {
930 ...previousProvider,
931 checkedAt: "2026-07-17T00:01:00.000Z",
932 models: [
933 {
934 slug: "github/gpt-5",
935 name: "GPT-5",
936 subProvider: "GitHub",
937 isCustom: false,
938 capabilities: null,
939 },
940 ],
941 } satisfies ServerProvider;
942
943 assert.deepStrictEqual(
944 ProviderRegistry.mergeProviderSnapshot(previousProvider, refreshedProvider).models,
945 [...refreshedProvider.models],
946 );
947 });
948
949 it("retains stale OpenCode models when a refresh fails", () => {
950 const previousProvider = {
951 instanceId: ProviderInstanceId.make("opencode"),
952 driver: ProviderDriverKind.make("opencode"),
953 status: "ready",
954 enabled: true,
955 installed: true,
956 auth: { status: "authenticated" },
957 checkedAt: "2026-07-17T00:00:00.000Z",
958 version: "1.0.0",
959 models: [
960 {
961 slug: "github/gpt-5",
962 name: "GPT-5",
963 subProvider: "GitHub",
964 isCustom: false,
965 capabilities: null,
966 },
967 ],
968 slashCommands: [{ name: "review", description: "Review changes" }],
969 skills: [
970 {
971 name: "typescript",
972 description: "TypeScript help",
973 path: "/skills/typescript/SKILL.md",
974 enabled: true,
975 },
976 ],
977 } as const satisfies ServerProvider;
978 const refreshedProvider = {
979 ...previousProvider,
980 status: "error",
981 auth: { status: "unknown" },
982 checkedAt: "2026-07-17T00:01:00.000Z",
983 models: [],
984 message: "Failed to refresh OpenCode models.",
985 } satisfies ServerProvider;
986
987 assert.deepStrictEqual(
988 ProviderRegistry.mergeProviderSnapshot(previousProvider, refreshedProvider).models,
989 [...previousProvider.models],
990 );
991 assert.deepStrictEqual(
992 ProviderRegistry.mergeProviderSnapshot(previousProvider, refreshedProvider).slashCommands,
993 previousProvider.slashCommands,
994 );
995 assert.deepStrictEqual(
996 ProviderRegistry.mergeProviderSnapshot(previousProvider, refreshedProvider).skills,
997 previousProvider.skills,
998 );
999 });
1000
1001 it("classifies pending, logout, uninstall, and reconnect OpenCode inventories", () => {
1002 const previousProvider = {
1003 instanceId: ProviderInstanceId.make("opencode"),
1004 driver: ProviderDriverKind.make("opencode"),
1005 status: "ready",
1006 enabled: true,
1007 installed: true,
1008 auth: { status: "authenticated" },
1009 checkedAt: "2026-07-17T00:00:00.000Z",
1010 version: "1.0.0",
1011 models: [
1012 {
1013 slug: "github/gpt-5",
1014 name: "GPT-5",
1015 subProvider: "GitHub",
1016 isCustom: false,
1017 capabilities: null,
1018 },
1019 {
1020 slug: "removed-plugin/model",
1021 name: "Removed Plugin Model",
1022 subProvider: "Removed Plugin",
1023 isCustom: false,
1024 capabilities: null,
1025 },
1026 ],
1027 slashCommands: [{ name: "review", description: "Review changes" }],
1028 skills: [
1029 {
1030 name: "typescript",
1031 description: "TypeScript help",
1032 path: "/skills/typescript/SKILL.md",
1033 enabled: true,
1034 },
1035 ],
1036 } as const satisfies ServerProvider;
1037 const pendingProvider = {
1038 ...previousProvider,
1039 status: "warning",
1040 installed: false,
1041 auth: { status: "unknown" },
1042 checkedAt: "2026-07-17T00:01:00.000Z",
1043 version: null,
1044 models: [],
1045 message: "OpenCode provider status has not been checked in this session yet.",
1046 } satisfies ServerProvider;
1047 const loggedOutProvider = {
1048 ...previousProvider,
1049 status: "warning",
1050 auth: { status: "unknown" },
1051 checkedAt: "2026-07-17T00:02:00.000Z",
1052 models: [],
1053 slashCommands: [],
1054 skills: [],
1055 message: "OpenCode is available, but it did not report any connected upstream providers.",
1056 } satisfies ServerProvider;
1057 const missingProvider = {
1058 ...previousProvider,
1059 status: "error",
1060 installed: false,
1061 auth: { status: "unknown" },
1062 checkedAt: "2026-07-17T00:03:00.000Z",
1063 version: null,
1064 models: [],
1065 message: "OpenCode CLI (`opencode`) is not installed or not on PATH.",
1066 } satisfies ServerProvider;
1067 const authoritativeProvider = {
1068 ...previousProvider,
1069 checkedAt: "2026-07-17T00:04:00.000Z",
1070 models: [previousProvider.models[0]!],
1071 } satisfies ServerProvider;
1072 const failedProvider = {
1073 ...authoritativeProvider,
1074 status: "error",
1075 auth: { status: "unknown" },
1076 checkedAt: "2026-07-17T00:05:00.000Z",
1077 models: [],
1078 message: "Failed to refresh OpenCode models.",
1079 } satisfies ServerProvider;
1080
1081 assert.deepStrictEqual(
1082 ProviderRegistry.mergeProviderSnapshot(previousProvider, pendingProvider).models,
1083 [...previousProvider.models],
1084 );
1085 assert.deepStrictEqual(
1086 ProviderRegistry.mergeProviderSnapshot(previousProvider, loggedOutProvider).models,
1087 [],
1088 );
1089 assert.deepStrictEqual(
1090 ProviderRegistry.mergeProviderSnapshot(previousProvider, loggedOutProvider).slashCommands,
1091 [],
1092 );
1093 assert.deepStrictEqual(
1094 ProviderRegistry.mergeProviderSnapshot(previousProvider, loggedOutProvider).skills,
1095 [],
1096 );
1097 assert.deepStrictEqual(
1098 ProviderRegistry.mergeProviderSnapshot(previousProvider, missingProvider).models,
1099 [],
1100 );
1101
1102 const afterRemoval = ProviderRegistry.mergeProviderSnapshot(
1103 previousProvider,
1104 authoritativeProvider,
1105 );
1106 const afterFailure = ProviderRegistry.mergeProviderSnapshot(afterRemoval, failedProvider);
1107
1108 assert.deepStrictEqual(afterFailure.models, [authoritativeProvider.models[0]!]);
1109 });
1110
1111 describe("Codex model inventories", () => {
1112 const cachedProvider = {
1113 instanceId: ProviderInstanceId.make("codex-personal"),
1114 driver: ProviderDriverKind.make("codex"),
1115 status: "ready",
1116 enabled: true,
1117 installed: true,
1118 auth: { status: "authenticated" },
1119 checkedAt: "2026-09-04T19:00:00.000Z",
1120 version: "0.153.3",
1121 models: ["vega-alpha", "joule-alpha", "kindle-alpha", "ultima-alpha", "solstice-alpha"].map(
1122 (slug) => ({ slug, name: slug, isCustom: false, capabilities: null }),
1123 ),
1124 slashCommands: [],
1125 skills: [],
1126 } satisfies ServerProvider;
1127 const customModel = {
1128 slug: "custom-model",
1129 name: "Custom model",
1130 isCustom: true,
1131 capabilities: null,
1132 } as const;
1133 const refreshedProvider = {
1134 ...cachedProvider,
1135 checkedAt: "2026-09-04T19:01:00.000Z",
1136 models: [
1137 { slug: "gpt-6-astra", name: "GPT 6 Astra", isCustom: false, capabilities: null },
1138 cachedProvider.models[0]!,
1139 customModel,
1140 ],
1141 } satisfies ServerProvider;
1142 const pendingProvider = {
1143 ...cachedProvider,
1144 status: "warning",
1145 installed: false,
1146 auth: { status: "unknown" },
1147 models: [customModel],
1148 } satisfies ServerProvider;
1149 const failedProvider = {
1150 ...pendingProvider,
1151 checkedAt: "2026-09-04T19:02:00.000Z",
1152 status: "error",
1153 installed: true,
1154 } satisfies ServerProvider;
1155
1156 it("drops retired alpha models after discovery, including without OpenAI authentication", () => {
1157 for (const authStatus of ["authenticated", "unknown"] as const) {
1158 assert.deepStrictEqual(
1159 ProviderRegistry.mergeProviderSnapshot(cachedProvider, {
1160 ...refreshedProvider,
1161 auth: { status: authStatus },
1162 }).models,
1163 refreshedProvider.models,
1164 );
1165 }
1166 });
1167
1168 it("keeps discovered models during startup and failed probes without restoring removed custom models", () => {
1169 for (const provider of [pendingProvider, failedProvider]) {
1170 assert.deepStrictEqual(
1171 ProviderRegistry.mergeProviderSnapshot(
1172 {
1173 ...cachedProvider,
1174 models: [...cachedProvider.models, { ...customModel, slug: "removed-custom" }],
1175 },
1176 provider,
1177 ).models,
1178 [customModel, ...cachedProvider.models],
1179 );
1180 }
1181 });
1182
1183 it("clears discovered models after sign-out, disable, uninstall, or empty discovery", () => {
1184 const emptyProvider = { ...refreshedProvider, models: [customModel] };
1185 const clearedProviders = [
1186 { ...emptyProvider, status: "error", auth: { status: "unauthenticated" } },
1187 { ...emptyProvider, status: "disabled", enabled: false },
1188 { ...emptyProvider, status: "error", installed: false, auth: { status: "unknown" } },
1189 emptyProvider,
1190 { ...emptyProvider, models: [] },
1191 ] satisfies ReadonlyArray<ServerProvider>;
1192
1193 for (const provider of clearedProviders) {
1194 assert.deepStrictEqual(
1195 ProviderRegistry.mergeProviderSnapshot(cachedProvider, provider).models,
1196 provider.models,
1197 );
1198 }
1199 });
1200
1201 it.effect("persists removals across failed refreshes and registry restarts", () =>
1202 Effect.gen(function* () {
1203 const config = yield* ServerConfig.ServerConfig;
1204 const filePath = yield* resolveProviderStatusCachePath({
1205 cacheDir: config.providerStatusCacheDir,
1206 instanceId: cachedProvider.instanceId,
1207 });
1208 yield* writeProviderStatusCache({ filePath, provider: cachedProvider });
1209 const nextProvider = yield* Ref.make<ServerProvider>(refreshedProvider);
1210 const instance = {
1211 instanceId: cachedProvider.instanceId,
1212 driverKind: cachedProvider.driver,
1213 continuationIdentity: {
1214 driverKind: cachedProvider.driver,
1215 continuationKey: "codex:instance:codex-personal",
1216 },
1217 displayName: undefined,
1218 enabled: true,
1219 snapshot: {
1220 resolveMaintenance: () =>
1221 Effect.succeed(
1222 makeManualOnlyProviderMaintenanceCapabilities({
1223 provider: cachedProvider.driver,
1224 packageName: null,
1225 }),
1226 ),
1227 getSnapshot: Effect.succeed(pendingProvider),
1228 refresh: Ref.get(nextProvider),
1229 streamChanges: Stream.empty,
1230 applyUsageLimits: () => Effect.void,
1231 },
1232 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
1233 textGeneration: {} as ProviderInstance["textGeneration"],
1234 } satisfies ProviderInstance;
1235 const layerInstanceRegistry = Layer.succeed(
1236 ProviderInstanceRegistry.ProviderInstanceRegistry,
1237 {
1238 getInstance: (id) =>
1239 Effect.succeed(id === instance.instanceId ? instance : undefined),
1240 listInstances: Effect.succeed([instance]),
1241 listUnavailable: Effect.succeed([]),
1242 streamChanges: Stream.empty,
1243 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), PubSub.subscribe),
1244 },
1245 );
1246 const retainedModels = [
1247 customModel,
1248 ...refreshedProvider.models.filter((model) => !model.isCustom),
1249 ];
1250
1251 for (const restarted of [false, true]) {
1252 yield* Effect.gen(function* () {
1253 const registry = yield* ProviderRegistry.ProviderRegistry;
1254 const expectedModels = restarted
1255 ? retainedModels
1256 : [customModel, ...cachedProvider.models];
1257 assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, expectedModels);
1258
1259 yield* registry.refreshInstance(instance.instanceId);
1260 assert.deepStrictEqual(
1261 (yield* readProviderStatusCache(filePath))?.models,
1262 restarted ? retainedModels : refreshedProvider.models,
1263 );
1264
1265 yield* Ref.set(nextProvider, failedProvider);
1266 const afterFailure = yield* registry.refreshInstance(instance.instanceId);
1267 assert.deepStrictEqual(afterFailure[0]?.models, retainedModels);
1268 assert.deepStrictEqual(
1269 (yield* readProviderStatusCache(filePath))?.models,
1270 retainedModels,
1271 );
1272 }).pipe(
1273 Effect.provide(ProviderRegistry.layer.pipe(Layer.provide(layerInstanceRegistry))),
1274 Effect.scoped,
1275 );
1276 }
1277 }).pipe(
1278 Effect.provide(
1279 ServerConfig.layerTest(process.cwd(), {
1280 prefix: "t3-codex-retired-model-cache-",
1281 }).pipe(Layer.provideMerge(NodeServices.layer)),
1282 ),
1283 ),
1284 );
1285 });
1286
1287 describe("Antigravity model inventories", () => {
1288 const previousProvider = {
1289 instanceId: ProviderInstanceId.make("antigravity-personal"),
1290 driver: ProviderDriverKind.make("antigravity"),
1291 status: "ready",
1292 enabled: true,
1293 installed: true,
1294 auth: { status: "authenticated" },
1295 checkedAt: "2026-09-02T00:00:00.000Z",
1296 version: "0.1.3",
1297 models: [
1298 {
1299 slug: "gemini-3.1-pro-high",
1300 name: "Gemini 3.1 Pro High",
1301 isCustom: false,
1302 capabilities: null,
1303 },
1304 {
1305 slug: "gemini-3-flash",
1306 name: "Gemini 3 Flash",
1307 isCustom: false,
1308 capabilities: null,
1309 },
1310 ],
1311 slashCommands: [],
1312 skills: [],
1313 } as const satisfies ServerProvider;
1314
1315 it("removes unavailable models after a successful refresh", () => {
1316 for (const status of ["ready", "warning"] as const) {
1317 const refreshedProvider = {
1318 ...previousProvider,
1319 status,
1320 checkedAt: "2026-09-02T00:01:00.000Z",
1321 models: [previousProvider.models[1]],
1322 } satisfies ServerProvider;
1323 const afterRefresh = ProviderRegistry.mergeProviderSnapshot(
1324 previousProvider,
1325 refreshedProvider,
1326 );
1327
1328 assert.deepStrictEqual(afterRefresh.models, refreshedProvider.models);
1329
1330 const afterFailure = ProviderRegistry.mergeProviderSnapshot(afterRefresh, {
1331 ...refreshedProvider,
1332 status: "error",
1333 auth: { status: "unknown" },
1334 models: [],
1335 });
1336 assert.deepStrictEqual(afterFailure.models, refreshedProvider.models);
1337 }
1338 });
1339
1340 it("keeps cached models during health checks and temporary failures", () => {
1341 for (const installed of [false, true]) {
1342 const pendingProvider = {
1343 ...previousProvider,
1344 status: "warning",
1345 installed,
1346 auth: { status: "unknown" },
1347 checkedAt: "2026-09-02T00:01:00.000Z",
1348 version: installed ? previousProvider.version : null,
1349 models: [],
1350 } satisfies ServerProvider;
1351
1352 assert.deepStrictEqual(
1353 ProviderRegistry.mergeProviderSnapshot(previousProvider, pendingProvider).models,
1354 previousProvider.models,
1355 );
1356 }
1357
1358 for (const authStatus of ["unknown", "authenticated"] as const) {
1359 const failedProvider = {
1360 ...previousProvider,
1361 status: "error",
1362 auth: { status: authStatus },
1363 checkedAt: "2026-09-02T00:02:00.000Z",
1364 models: [],
1365 } satisfies ServerProvider;
1366
1367 assert.deepStrictEqual(
1368 ProviderRegistry.mergeProviderSnapshot(previousProvider, failedProvider).models,
1369 previousProvider.models,
1370 );
1371 }
1372 });
1373
1374 it("clears models after sign-out, disable, uninstall, or an empty successful refresh", () => {
1375 const emptyProvider = {
1376 ...previousProvider,
1377 checkedAt: "2026-09-02T00:01:00.000Z",
1378 models: [],
1379 } satisfies ServerProvider;
1380 const clearedProviders = [
1381 { ...emptyProvider, status: "warning", auth: { status: "unauthenticated" } },
1382 { ...emptyProvider, status: "error", auth: { status: "unauthenticated" } },
1383 { ...emptyProvider, status: "disabled", enabled: false },
1384 { ...emptyProvider, status: "error", enabled: false },
1385 { ...emptyProvider, status: "error", installed: false, auth: { status: "unknown" } },
1386 emptyProvider,
1387 ] satisfies ReadonlyArray<ServerProvider>;
1388
1389 for (const provider of clearedProviders) {
1390 const afterRemoval = ProviderRegistry.mergeProviderSnapshot(previousProvider, provider);
1391 assert.deepStrictEqual(afterRemoval.models, []);
1392
1393 const afterFailure = ProviderRegistry.mergeProviderSnapshot(afterRemoval, {
1394 ...emptyProvider,
1395 status: "error",
1396 auth: { status: "unknown" },
1397 });
1398 assert.deepStrictEqual(afterFailure.models, []);
1399 }
1400 });
1401 });
1402
1403 describe("Antigravity saved account", () => {
1404 const signedIn = {
1405 instanceId: ProviderInstanceId.make("antigravity-personal"),
1406 driver: ProviderDriverKind.make("antigravity"),
1407 status: "ready",
1408 enabled: true,
1409 installed: true,
1410 auth: { status: "authenticated", type: "oauth-personal", label: "Google account" },
1411 checkedAt: "2026-09-05T00:00:00.000Z",
1412 version: "agy_acp_server_1.1.1",
1413 models: [
1414 {
1415 slug: "gemini-3.7-flash-high",
1416 name: "Gemini 3.7 Flash",
1417 isCustom: false,
1418 capabilities: null,
1419 },
1420 ],
1421 slashCommands: [{ name: "plan" }],
1422 skills: [],
1423 } as const satisfies ServerProvider;
1424 const uncheckedMessage =
1425 "Antigravity is installed. Google account access is not checked yet.";
1426 const restartProbe = {
1427 ...signedIn,
1428 status: "warning",
1429 auth: { status: "unknown" },
1430 checkedAt: "2026-09-05T00:01:00.000Z",
1431 message: uncheckedMessage,
1432 models: [],
1433 } as const satisfies ServerProvider;
1434
1435 it("keeps the saved Google account through restart health checks", () => {
1436 const merged = ProviderRegistry.mergeProviderSnapshot(signedIn, restartProbe);
1437 const { message: _uncheckedMessage, ...probeWithoutMessage } = restartProbe;
1438 assert.deepStrictEqual(merged, {
1439 ...probeWithoutMessage,
1440 status: "ready",
1441 auth: signedIn.auth,
1442 models: signedIn.models,
1443 });
1444 assert.equal("message" in merged, false);
1445 // The next periodic probe reads the merged snapshot as its previous state.
1446 assert.deepStrictEqual(
1447 ProviderRegistry.mergeProviderSnapshot(merged, restartProbe),
1448 merged,
1449 );
1450 });
1451
1452 it("carries the account through the boot probe and a failed probe without hiding them", () => {
1453 const booting = {
1454 ...restartProbe,
1455 installed: false,
1456 version: null,
1457 message: "Checking Antigravity availability.",
1458 } satisfies ServerProvider;
1459 assert.deepStrictEqual(ProviderRegistry.mergeProviderSnapshot(signedIn, booting), {
1460 ...booting,
1461 auth: signedIn.auth,
1462 models: signedIn.models,
1463 });
1464
1465 const failed = {
1466 ...restartProbe,
1467 status: "error",
1468 message: "Antigravity did not respond to its local health check within 90 seconds.",
1469 } satisfies ServerProvider;
1470 assert.deepStrictEqual(ProviderRegistry.mergeProviderSnapshot(signedIn, failed), {
1471 ...failed,
1472 auth: signedIn.auth,
1473 models: signedIn.models,
1474 });
1475 });
1476
1477 it("does not invent an account after sign-out, disable, uninstall, or for other providers", () => {
1478 const untouched = [
1479 { ...restartProbe, auth: { status: "unauthenticated" } },
1480 { ...restartProbe, status: "disabled", enabled: false },
1481 { ...restartProbe, status: "error", installed: false },
1482 { ...restartProbe, driver: ProviderDriverKind.make("codex") },
1483 // The instance was rebuilt with another sign-in method.
1484 { ...restartProbe, auth: { status: "unknown", type: "gemini-api-key" } },
1485 ] satisfies ReadonlyArray<ServerProvider>;
1486 for (const next of untouched) {
1487 const merged = ProviderRegistry.mergeProviderSnapshot(signedIn, next);
1488 assert.deepStrictEqual(merged.auth, next.auth);
1489 assert.equal(merged.status, next.status);
1490 assert.equal(merged.message, next.message);
1491 }
1492 assert.deepStrictEqual(
1493 ProviderRegistry.mergeProviderSnapshot(
1494 { ...signedIn, auth: { status: "unknown" } },
1495 restartProbe,
1496 ).auth,
1497 { status: "unknown" },
1498 );
1499 assert.equal(
1500 ProviderRegistry.mergeProviderSnapshot(
1501 { ...signedIn, driver: ProviderDriverKind.make("codex") },
1502 restartProbe,
1503 ).auth.status,
1504 "unknown",
1505 );
1506 assert.deepStrictEqual(
1507 ProviderRegistry.mergeProviderSnapshot(signedIn, {
1508 ...restartProbe,
1509 auth: { status: "unknown", type: "oauth-personal" },
1510 }).auth,
1511 signedIn.auth,
1512 );
1513 });
1514 });
1515
1516 it("fills missing capabilities from the previous provider snapshot", () => {
1517 const previousProvider = {
1518 instanceId: ProviderInstanceId.make("cursor"),
1519 driver: ProviderDriverKind.make("cursor"),
1520 status: "ready",
1521 enabled: true,
1522 installed: true,
1523 auth: { status: "authenticated" },
1524 checkedAt: "2026-04-14T00:00:00.000Z",
1525 version: "2026.04.09-f2b0fcd",
1526 models: [
1527 {
1528 slug: "claude-opus-4-6",
1529 name: "Opus 4.6",
1530 isCustom: false,
1531 capabilities: createModelCapabilities({
1532 optionDescriptors: [
1533 selectDescriptor("reasoning", "Reasoning", [
1534 { id: "high", label: "High", isDefault: true },
1535 ]),
1536 booleanDescriptor("fastMode", "Fast Mode"),
1537 booleanDescriptor("thinking", "Thinking"),
1538 ],
1539 }),
1540 },
1541 ],
1542 slashCommands: [],
1543 skills: [],
1544 } as const satisfies ServerProvider;
1545 const refreshedProvider = {
1546 ...previousProvider,
1547 checkedAt: "2026-04-14T00:01:00.000Z",
1548 models: [
1549 {
1550 slug: "claude-opus-4-6",
1551 name: "Opus 4.6",
1552 isCustom: false,
1553 capabilities: createModelCapabilities({
1554 optionDescriptors: [],
1555 }),
1556 },
1557 ],
1558 } satisfies ServerProvider;
1559
1560 assert.deepStrictEqual(
1561 ProviderRegistry.mergeProviderSnapshot(previousProvider, refreshedProvider).models,
1562 [...previousProvider.models],
1563 );
1564 });
1565
1566 it.effect("does not run provider probes during layer construction", () =>
1567 Effect.gen(function* () {
1568 const codexDriver = ProviderDriverKind.make("codex");
1569 const codexInstanceId = ProviderInstanceId.make("codex");
1570 const initialProvider = {
1571 instanceId: codexInstanceId,
1572 driver: codexDriver,
1573 status: "warning",
1574 enabled: true,
1575 installed: false,
1576 auth: { status: "unknown" },
1577 checkedAt: "2026-06-10T00:00:00.000Z",
1578 version: null,
1579 message: "Checking Codex provider status.",
1580 models: [],
1581 slashCommands: [],
1582 skills: [],
1583 } as const satisfies ServerProvider;
1584 const refreshCalls = yield* Ref.make(0);
1585 const instance = {
1586 instanceId: codexInstanceId,
1587 driverKind: codexDriver,
1588 continuationIdentity: {
1589 driverKind: codexDriver,
1590 continuationKey: "codex:instance:codex",
1591 },
1592 displayName: undefined,
1593 enabled: true,
1594 snapshot: {
1595 resolveMaintenance: () =>
1596 Effect.succeed(
1597 makeManualOnlyProviderMaintenanceCapabilities({
1598 provider: codexDriver,
1599 packageName: null,
1600 }),
1601 ),
1602 getSnapshot: Effect.succeed(initialProvider),
1603 refresh: Ref.update(refreshCalls, (count) => count + 1).pipe(
1604 Effect.andThen(Effect.never),
1605 ),
1606 streamChanges: Stream.empty,
1607 applyUsageLimits: () => Effect.void,
1608 },
1609 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
1610 textGeneration: {} as ProviderInstance["textGeneration"],
1611 } satisfies ProviderInstance;
1612 const layerInstanceRegistry = Layer.succeed(
1613 ProviderInstanceRegistry.ProviderInstanceRegistry,
1614 {
1615 getInstance: (instanceId) =>
1616 Effect.succeed(instanceId === codexInstanceId ? instance : undefined),
1617 listInstances: Effect.succeed([instance]),
1618 listUnavailable: Effect.succeed([]),
1619 streamChanges: Stream.empty,
1620 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), PubSub.subscribe),
1621 },
1622 );
1623 const scope = yield* Scope.make();
1624 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
1625 const runtimeServices = yield* Layer.build(
1626 ProviderRegistry.layer.pipe(
1627 Layer.provideMerge(layerInstanceRegistry),
1628 Layer.provideMerge(
1629 ServerConfig.layerTest(process.cwd(), {
1630 prefix: "t3-provider-registry-background-refresh-",
1631 }),
1632 ),
1633 Layer.provideMerge(NodeServices.layer),
1634 ),
1635 ).pipe(Scope.provide(scope));
1636 yield* Effect.gen(function* () {
1637 const registry = yield* ProviderRegistry.ProviderRegistry;
1638 assert.deepStrictEqual(yield* registry.getProviders, [initialProvider]);
1639 assert.strictEqual(yield* Ref.get(refreshCalls), 0);
1640 }).pipe(Effect.provide(runtimeServices));
1641 }),
1642 );
1643
1644 it.effect("deduplicates cwd probes and clears snapshots when an instance rebuilds", () =>
1645 Effect.gen(function* () {
1646 const driver = ProviderDriverKind.make("codex");
1647 const instanceId = ProviderInstanceId.make("codex");
1648 const machineProvider = {
1649 instanceId,
1650 driver,
1651 status: "ready",
1652 enabled: true,
1653 installed: true,
1654 auth: { status: "authenticated" },
1655 checkedAt: "2026-06-10T00:00:00.000Z",
1656 version: "1.0.0",
1657 models: [],
1658 slashCommands: [{ name: "global" }],
1659 skills: [{ name: "global", path: "/global/SKILL.md", enabled: true }],
1660 } as const satisfies ServerProvider;
1661 const scopedProvider = {
1662 ...machineProvider,
1663 checkedAt: "2026-06-10T00:01:00.000Z",
1664 slashCommands: [{ name: "project" }],
1665 skills: [{ name: "project", path: "/workspace/SKILL.md", enabled: true }],
1666 } as const satisfies ServerProvider;
1667 const pendingScopedProvider = {
1668 ...scopedProvider,
1669 status: "error",
1670 installed: false,
1671 slashCommands: [],
1672 } as const satisfies ServerProvider;
1673 const snapshotCalls = yield* Ref.make(0);
1674 const scopedResult = yield* Ref.make<ProviderWorkspaceSnapshot>({
1675 ...scopedProvider,
1676 status: "error",
1677 slashCommands: [COMPACT_SLASH_COMMAND],
1678 slashCommandsPending: true,
1679 });
1680 const cacheInvalidations = yield* Ref.make(0);
1681 const scanGate = yield* Ref.make<{
1682 readonly started: Deferred.Deferred<void>;
1683 readonly release: Deferred.Deferred<void>;
1684 } | null>(null);
1685 const returnPendingSnapshot = yield* Ref.make(true);
1686 const probeStarted = yield* Deferred.make<void>();
1687 const releaseProbe = yield* Deferred.make<void>();
1688 const makeInstance = (
1689 provider: ServerProvider,
1690 snapshotForCwd: NonNullable<ProviderInstance["snapshotForCwd"]>,
1691 ): ProviderInstance => ({
1692 instanceId,
1693 driverKind: driver,
1694 continuationIdentity: {
1695 driverKind: driver,
1696 continuationKey: "codex:instance:codex",
1697 },
1698 displayName: undefined,
1699 enabled: true,
1700 snapshot: {
1701 resolveMaintenance: () =>
1702 Effect.succeed(
1703 makeManualOnlyProviderMaintenanceCapabilities({
1704 provider: driver,
1705 packageName: null,
1706 }),
1707 ),
1708 getSnapshot: Effect.succeed(provider),
1709 refresh: Effect.succeed(provider),
1710 streamChanges: Stream.empty,
1711 applyUsageLimits: () => Effect.void,
1712 },
1713 snapshotForCwd,
1714 invalidateCaches: Ref.update(cacheInvalidations, (count) => count + 1),
1715 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
1716 textGeneration: {} as ProviderInstance["textGeneration"],
1717 });
1718 const firstInstance = makeInstance(machineProvider, () =>
1719 Effect.gen(function* () {
1720 yield* Ref.update(snapshotCalls, (count) => count + 1);
1721 if (yield* Ref.get(returnPendingSnapshot)) return pendingScopedProvider;
1722 yield* Deferred.succeed(probeStarted, undefined);
1723 yield* Deferred.await(releaseProbe);
1724 const result = yield* Ref.get(scopedResult);
1725 const gate = yield* Ref.getAndSet(scanGate, null);
1726 if (gate) {
1727 yield* Deferred.succeed(gate.started, undefined);
1728 yield* Deferred.await(gate.release);
1729 }
1730 return result;
1731 }),
1732 );
1733 const rebuiltProvider = {
1734 ...machineProvider,
1735 checkedAt: "2026-06-10T00:02:00.000Z",
1736 status: "warning",
1737 installed: false,
1738 auth: { status: "unknown" },
1739 } satisfies ServerProvider;
1740 const rebuiltInstance = makeInstance(rebuiltProvider, () =>
1741 Ref.update(snapshotCalls, (count) => count + 1).pipe(Effect.as(scopedProvider)),
1742 );
1743 const registryChanges = yield* PubSub.unbounded<void>();
1744 const instancesRef = yield* Ref.make<ReadonlyArray<ProviderInstance>>([firstInstance]);
1745 const layerInstanceRegistry = Layer.succeed(
1746 ProviderInstanceRegistry.ProviderInstanceRegistry,
1747 {
1748 getInstance: (requestedId) =>
1749 Ref.get(instancesRef).pipe(
1750 Effect.map((instances) =>
1751 instances.find((instance) => instance.instanceId === requestedId),
1752 ),
1753 ),
1754 listInstances: Ref.get(instancesRef),
1755 listUnavailable: Effect.succeed([]),
1756 streamChanges: Stream.fromPubSub(registryChanges),
1757 subscribeChanges: PubSub.subscribe(registryChanges),
1758 },
1759 );
1760 const scope = yield* Scope.make();
1761 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
1762 const runtimeServices = yield* Layer.build(
1763 ProviderRegistry.layer.pipe(
1764 Layer.provideMerge(layerInstanceRegistry),
1765 Layer.provideMerge(
1766 ServerConfig.layerTest(process.cwd(), {
1767 prefix: "t3-provider-registry-workspace-snapshot-",
1768 }),
1769 ),
1770 Layer.provideMerge(NodeServices.layer),
1771 ),
1772 ).pipe(Scope.provide(scope));
1773
1774 yield* Effect.gen(function* () {
1775 const registry = yield* ProviderRegistry.ProviderRegistry;
1776 yield* registry.refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace" });
1777 assert.strictEqual((yield* registry.getProviders)[0]?.workspaceSnapshots, undefined);
1778 yield* Ref.set(returnPendingSnapshot, false);
1779 const workspaceUpdate = yield* registry.streamChanges.pipe(
1780 Stream.runHead,
1781 Effect.forkChild,
1782 );
1783 yield* Effect.yieldNow;
1784 const firstRefresh = yield* registry
1785 .refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace" })
1786 .pipe(Effect.forkChild);
1787 yield* Deferred.await(probeStarted);
1788 const duplicateRefresh = yield* registry
1789 .refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace" })
1790 .pipe(Effect.forkChild);
1791 yield* Effect.yieldNow;
1792 assert.strictEqual(yield* Ref.get(snapshotCalls), 2);
1793 yield* Deferred.succeed(releaseProbe, undefined);
1794 yield* Fiber.join(firstRefresh);
1795 yield* Fiber.join(duplicateRefresh);
1796 const published = yield* Fiber.join(workspaceUpdate);
1797 assert.strictEqual(published._tag, "Some");
1798 const providers = yield* registry.getProviders;
1799 assert.deepStrictEqual(providers[0]?.skills, machineProvider.skills);
1800 assert.deepStrictEqual(
1801 providers[0]?.workspaceSnapshots?.[0]?.skills,
1802 scopedProvider.skills,
1803 );
1804 assert.deepStrictEqual(providers[0]?.workspaceSnapshots?.[0]?.slashCommands, [
1805 COMPACT_SLASH_COMMAND,
1806 ]);
1807 assert.strictEqual(providers[0]?.workspaceSnapshots?.[0]?.slashCommandsPending, true);
1808 yield* Ref.set(scopedResult, {
1809 ...scopedProvider,
1810 status: "error",
1811 slashCommandsPending: false,
1812 });
1813 yield* registry.refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace" });
1814 assert.strictEqual(yield* Ref.get(snapshotCalls), 3);
1815 assert.deepStrictEqual(
1816 (yield* registry.getProviders)[0]?.workspaceSnapshots?.[0]?.slashCommands,
1817 scopedProvider.slashCommands,
1818 );
1819 assert.strictEqual(
1820 (yield* registry.getProviders)[0]?.workspaceSnapshots?.[0]?.slashCommandsPending,
1821 undefined,
1822 );
1823 yield* registry.refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace" });
1824 assert.strictEqual(yield* Ref.get(snapshotCalls), 3);
1825 // Nothing watches skill directories, so an expired scan is redone on use.
1826 yield* TestClock.adjust(PROVIDER_WORKSPACE_SNAPSHOT_TTL_MS - 1);
1827 yield* registry.refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace" });
1828 assert.strictEqual(yield* Ref.get(snapshotCalls), 3);
1829 yield* TestClock.adjust(1);
1830 yield* registry.refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace" });
1831 assert.strictEqual(yield* Ref.get(snapshotCalls), 4);
1832 const newSkills = [
1833 ...scopedProvider.skills,
1834 { name: "added", path: "/workspace/added/SKILL.md", enabled: true },
1835 ];
1836 yield* Ref.set(scopedResult, { ...scopedProvider, skills: newSkills });
1837 yield* registry.refreshWorkspaceSnapshot({
1838 instanceId,
1839 cwd: "/workspace",
1840 fresh: true,
1841 });
1842 assert.strictEqual(yield* Ref.get(snapshotCalls), 5);
1843 assert.strictEqual(yield* Ref.get(cacheInvalidations), 1);
1844 assert.deepStrictEqual(
1845 (yield* registry.getProviders)[0]?.workspaceSnapshots?.map((s) => s.skills),
1846 [newSkills],
1847 );
1848
1849 // A slow fresh scan that read older files must not overwrite a
1850 // newer scan that finished first.
1851 const slowStarted = yield* Deferred.make<void>();
1852 const releaseSlow = yield* Deferred.make<void>();
1853 yield* Ref.set(scanGate, { started: slowStarted, release: releaseSlow });
1854 yield* Ref.set(scopedResult, scopedProvider);
1855 const slowScan = yield* registry
1856 .refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace", fresh: true })
1857 .pipe(Effect.forkChild);
1858 yield* Deferred.await(slowStarted);
1859 const latestSkills = [
1860 ...newSkills,
1861 { name: "latest", path: "/workspace/latest/SKILL.md", enabled: true },
1862 ];
1863 yield* Ref.set(scopedResult, { ...scopedProvider, skills: latestSkills });
1864 yield* registry.refreshWorkspaceSnapshot({
1865 instanceId,
1866 cwd: "/workspace",
1867 fresh: true,
1868 });
1869 yield* Deferred.succeed(releaseSlow, undefined);
1870 yield* Fiber.join(slowScan);
1871 assert.deepStrictEqual(
1872 (yield* registry.getProviders)[0]?.workspaceSnapshots?.map((s) => s.skills),
1873 [latestSkills],
1874 );
1875
1876 yield* Ref.set(instancesRef, [rebuiltInstance]);
1877 yield* PubSub.publish(registryChanges, undefined);
1878 let rebuilt = yield* registry.getProviders;
1879 for (
1880 let attempt = 0;
1881 attempt < 50 && rebuilt[0]?.checkedAt !== rebuiltProvider.checkedAt;
1882 attempt += 1
1883 ) {
1884 yield* Effect.yieldNow;
1885 rebuilt = yield* registry.getProviders;
1886 }
1887 assert.strictEqual(rebuilt[0]?.checkedAt, rebuiltProvider.checkedAt);
1888 assert.strictEqual(rebuilt[0]?.workspaceSnapshots, undefined);
1889 }).pipe(Effect.provide(runtimeServices));
1890 }),
1891 );
1892
1893 it.effect("refreshes OpenCode catalogs and preserves other providers", () =>
1894 Effect.gen(function* () {
1895 const codexDriver = ProviderDriverKind.make("codex");
1896 const openCodeDriver = ProviderDriverKind.make("opencode");
1897 const codexInstanceId = ProviderInstanceId.make("codex");
1898 const openCodeInstanceId = ProviderInstanceId.make("opencode");
1899 const codexRefreshCalls = yield* Ref.make(0);
1900 const openCodeRefreshCalls = yield* Ref.make(0);
1901 const codexProvider = {
1902 instanceId: codexInstanceId,
1903 driver: codexDriver,
1904 status: "ready",
1905 enabled: true,
1906 installed: true,
1907 auth: { status: "authenticated" },
1908 checkedAt: "2026-06-10T00:00:00.000Z",
1909 version: "1.0.0",
1910 models: [],
1911 slashCommands: [],
1912 skills: [],
1913 } as const satisfies ServerProvider;
1914 const failedOpenCodeProvider = {
1915 instanceId: openCodeInstanceId,
1916 driver: openCodeDriver,
1917 status: "error",
1918 enabled: true,
1919 installed: true,
1920 auth: { status: "unknown" },
1921 checkedAt: "2026-06-10T00:00:00.000Z",
1922 version: "1.0.0",
1923 message: "Failed to refresh OpenCode models.",
1924 models: [],
1925 slashCommands: [],
1926 skills: [],
1927 } as const satisfies ServerProvider;
1928 const recoveredOpenCodeProvider = {
1929 ...failedOpenCodeProvider,
1930 status: "ready",
1931 auth: { status: "authenticated" },
1932 checkedAt: "2026-06-10T00:01:00.000Z",
1933 message: "One upstream provider connected through OpenCode.",
1934 models: [
1935 {
1936 slug: "github/gpt-5",
1937 name: "GPT-5",
1938 subProvider: "GitHub",
1939 isCustom: false,
1940 capabilities: null,
1941 },
1942 ],
1943 } as const satisfies ServerProvider;
1944 const changedCatalogProvider = {
1945 ...recoveredOpenCodeProvider,
1946 checkedAt: "2026-06-10T00:02:00.000Z",
1947 models: [
1948 {
1949 slug: "anthropic/claude-sonnet-4",
1950 name: "Claude Sonnet 4",
1951 subProvider: "Anthropic",
1952 isCustom: false,
1953 capabilities: null,
1954 },
1955 ],
1956 } as const satisfies ServerProvider;
1957 const catalogSnapshot = yield* Ref.make<ServerProvider>(recoveredOpenCodeProvider);
1958 const instances = [
1959 {
1960 instanceId: codexInstanceId,
1961 driverKind: codexDriver,
1962 continuationIdentity: {
1963 driverKind: codexDriver,
1964 continuationKey: "codex:instance:codex",
1965 },
1966 displayName: undefined,
1967 enabled: true,
1968 snapshot: {
1969 resolveMaintenance: () =>
1970 Effect.succeed(
1971 makeManualOnlyProviderMaintenanceCapabilities({
1972 provider: codexDriver,
1973 packageName: null,
1974 }),
1975 ),
1976 getSnapshot: Effect.succeed(codexProvider),
1977 refresh: Ref.update(codexRefreshCalls, (count) => count + 1).pipe(
1978 Effect.as(codexProvider),
1979 ),
1980 streamChanges: Stream.empty,
1981 applyUsageLimits: () => Effect.void,
1982 },
1983 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
1984 textGeneration: {} as ProviderInstance["textGeneration"],
1985 },
1986 {
1987 instanceId: openCodeInstanceId,
1988 driverKind: openCodeDriver,
1989 continuationIdentity: {
1990 driverKind: openCodeDriver,
1991 continuationKey: "opencode:instance:opencode",
1992 },
1993 displayName: undefined,
1994 enabled: true,
1995 snapshot: {
1996 resolveMaintenance: () =>
1997 Effect.succeed(
1998 makeManualOnlyProviderMaintenanceCapabilities({
1999 provider: openCodeDriver,
2000 packageName: null,
2001 }),
2002 ),
2003 getSnapshot: Effect.succeed(failedOpenCodeProvider),
2004 refresh: Ref.update(openCodeRefreshCalls, (count) => count + 1).pipe(
2005 Effect.andThen(Ref.get(catalogSnapshot)),
2006 ),
2007 streamChanges: Stream.empty,
2008 applyUsageLimits: () => Effect.void,
2009 },
2010 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
2011 textGeneration: {} as ProviderInstance["textGeneration"],
2012 },
2013 ] satisfies ReadonlyArray<ProviderInstance>;
2014 const layerInstanceRegistry = Layer.succeed(
2015 ProviderInstanceRegistry.ProviderInstanceRegistry,
2016 {
2017 getInstance: (instanceId) =>
2018 Effect.succeed(instances.find((instance) => instance.instanceId === instanceId)),
2019 listInstances: Effect.succeed(instances),
2020 listUnavailable: Effect.succeed([]),
2021 streamChanges: Stream.empty,
2022 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), PubSub.subscribe),
2023 },
2024 );
2025 const scope = yield* Scope.make();
2026 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
2027 const runtimeServices = yield* Layer.build(
2028 ProviderRegistry.layer.pipe(
2029 Layer.provideMerge(layerInstanceRegistry),
2030 Layer.provideMerge(
2031 ServerConfig.layerTest(process.cwd(), {
2032 prefix: "t3-provider-registry-reconnect-refresh-",
2033 }),
2034 ),
2035 Layer.provideMerge(NodeServices.layer),
2036 ),
2037 ).pipe(Scope.provide(scope));
2038
2039 yield* Effect.gen(function* () {
2040 const registry = yield* ProviderRegistry.ProviderRegistry;
2041 const initialProviders = yield* registry.getProviders;
2042 assert.strictEqual(
2043 initialProviders.find((provider) => provider.instanceId === openCodeInstanceId)?.status,
2044 "error",
2045 );
2046
2047 const recoveredProviders = yield* registry.refresh();
2048 assert.deepStrictEqual(
2049 recoveredProviders.find((provider) => provider.instanceId === openCodeInstanceId)
2050 ?.models,
2051 recoveredOpenCodeProvider.models,
2052 );
2053 assert.deepStrictEqual(
2054 recoveredProviders.find((provider) => provider.instanceId === codexInstanceId),
2055 withBundledCompatibility(codexProvider),
2056 );
2057
2058 yield* Ref.set(catalogSnapshot, changedCatalogProvider);
2059 const changedProviders = yield* registry.refresh();
2060 assert.deepStrictEqual(
2061 changedProviders.find((provider) => provider.instanceId === openCodeInstanceId)?.models,
2062 changedCatalogProvider.models,
2063 );
2064 assert.deepStrictEqual(
2065 changedProviders.find((provider) => provider.instanceId === codexInstanceId),
2066 withBundledCompatibility(codexProvider),
2067 );
2068 }).pipe(Effect.provide(runtimeServices));
2069
2070 assert.strictEqual(yield* Ref.get(codexRefreshCalls), 2);
2071 assert.strictEqual(yield* Ref.get(openCodeRefreshCalls), 2);
2072 }),
2073 );
2074
2075 it("persists merged provider snapshots for the providers that were refreshed", () => {
2076 const previousProviders = [
2077 {
2078 instanceId: ProviderInstanceId.make("cursor"),
2079 driver: ProviderDriverKind.make("cursor"),
2080 status: "ready",
2081 enabled: true,
2082 installed: true,
2083 auth: { status: "authenticated" },
2084 checkedAt: "2026-04-14T00:00:00.000Z",
2085 version: "2026.04.09-f2b0fcd",
2086 models: [
2087 {
2088 slug: "claude-opus-4-6",
2089 name: "Opus 4.6",
2090 isCustom: false,
2091 capabilities: createModelCapabilities({
2092 optionDescriptors: [
2093 selectDescriptor("reasoning", "Reasoning", [
2094 { id: "high", label: "High", isDefault: true },
2095 ]),
2096 booleanDescriptor("fastMode", "Fast Mode"),
2097 booleanDescriptor("thinking", "Thinking"),
2098 ],
2099 }),
2100 },
2101 ],
2102 slashCommands: [],
2103 skills: [],
2104 },
2105 {
2106 instanceId: ProviderInstanceId.make("codex"),
2107 driver: ProviderDriverKind.make("codex"),
2108 status: "ready",
2109 enabled: true,
2110 installed: true,
2111 auth: { status: "authenticated" },
2112 checkedAt: "2026-04-14T00:00:00.000Z",
2113 version: "1.0.0",
2114 models: [],
2115 slashCommands: [],
2116 skills: [],
2117 },
2118 ] as const satisfies ReadonlyArray<ServerProvider>;
2119 const refreshedCursor = {
2120 ...previousProviders[0],
2121 checkedAt: "2026-04-14T00:01:00.000Z",
2122 models: [],
2123 } satisfies ServerProvider;
2124
2125 const mergedProviders = ProviderRegistry.mergeProviderSnapshots(previousProviders, [
2126 refreshedCursor,
2127 ]);
2128 const persistedProviders = ProviderRegistry.selectProvidersByKind(
2129 mergedProviders,
2130 new Set([ProviderDriverKind.make("cursor")]),
2131 );
2132
2133 assert.deepStrictEqual(persistedProviders, [
2134 {
2135 ...refreshedCursor,
2136 models: [...previousProviders[0].models],
2137 },
2138 ]);
2139 });
2140
2141 it.effect("shares one in-flight pass between concurrent untargeted refreshes", () =>
2142 Effect.gen(function* () {
2143 const driver = ProviderDriverKind.make("codex");
2144 const instanceId = ProviderInstanceId.make("codex");
2145 const provider = {
2146 instanceId,
2147 driver,
2148 status: "ready",
2149 enabled: true,
2150 installed: true,
2151 auth: { status: "authenticated" },
2152 checkedAt: "2026-06-10T00:00:00.000Z",
2153 version: "1.0.0",
2154 models: [],
2155 slashCommands: [],
2156 skills: [],
2157 } as const satisfies ServerProvider;
2158 const refreshCalls = yield* Ref.make(0);
2159 const probeStarted = yield* Deferred.make<void>();
2160 const releaseProbe = yield* Deferred.make<void>();
2161 const instances = [
2162 {
2163 instanceId,
2164 driverKind: driver,
2165 continuationIdentity: { driverKind: driver, continuationKey: "codex:instance:codex" },
2166 displayName: undefined,
2167 enabled: true,
2168 snapshot: {
2169 resolveMaintenance: () =>
2170 Effect.succeed(
2171 makeManualOnlyProviderMaintenanceCapabilities({
2172 provider: driver,
2173 packageName: null,
2174 }),
2175 ),
2176 getSnapshot: Effect.succeed(provider),
2177 refresh: Effect.gen(function* () {
2178 yield* Ref.update(refreshCalls, (count) => count + 1);
2179 yield* Deferred.succeed(probeStarted, undefined);
2180 yield* Deferred.await(releaseProbe);
2181 return provider;
2182 }),
2183 streamChanges: Stream.empty,
2184 applyUsageLimits: () => Effect.void,
2185 },
2186 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
2187 textGeneration: {} as ProviderInstance["textGeneration"],
2188 },
2189 ] satisfies ReadonlyArray<ProviderInstance>;
2190 const instanceRegistryLayer = Layer.succeed(
2191 ProviderInstanceRegistry.ProviderInstanceRegistry,
2192 {
2193 getInstance: (requestedId) =>
2194 Effect.succeed(instances.find((instance) => instance.instanceId === requestedId)),
2195 listInstances: Effect.succeed(instances),
2196 listUnavailable: Effect.succeed([]),
2197 streamChanges: Stream.empty,
2198 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), PubSub.subscribe),
2199 },
2200 );
2201 const scope = yield* Scope.make();
2202 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
2203 const runtimeServices = yield* Layer.build(
2204 ProviderRegistry.layer.pipe(
2205 Layer.provideMerge(instanceRegistryLayer),
2206 Layer.provideMerge(
2207 ServerConfig.layerTest(process.cwd(), {
2208 prefix: "t3-provider-registry-shared-refresh-",
2209 }),
2210 ),
2211 Layer.provideMerge(NodeServices.layer),
2212 ),
2213 ).pipe(Scope.provide(scope));
2214
2215 yield* Effect.gen(function* () {
2216 const registry = yield* ProviderRegistry.ProviderRegistry;
2217 const first = yield* registry.refresh().pipe(Effect.forkChild);
2218 yield* Deferred.await(probeStarted);
2219 const second = yield* registry.refresh().pipe(Effect.forkChild);
2220 yield* Effect.yieldNow;
2221 assert.strictEqual(yield* Ref.get(refreshCalls), 1);
2222 yield* Deferred.succeed(releaseProbe, undefined);
2223 expect(yield* Fiber.join(first)).toEqual([expect.objectContaining(provider)]);
2224 expect(yield* Fiber.join(second)).toEqual([expect.objectContaining(provider)]);
2225 assert.strictEqual(yield* Ref.get(refreshCalls), 1);
2226
2227 yield* registry.refresh();
2228 assert.strictEqual(yield* Ref.get(refreshCalls), 2);
2229 }).pipe(Effect.provide(runtimeServices));
2230 }),
2231 );
2232
2233 it.effect("persists the merged snapshot when a live update has empty models", () =>
2234 Effect.gen(function* () {
2235 const cursorDriver = ProviderDriverKind.make("cursor");
2236 const cursorInstanceId = ProviderInstanceId.make("cursor");
2237 const initialProvider = {
2238 instanceId: cursorInstanceId,
2239 driver: cursorDriver,
2240 status: "ready",
2241 enabled: true,
2242 installed: true,
2243 auth: { status: "authenticated" },
2244 checkedAt: "2026-04-14T00:00:00.000Z",
2245 version: "2026.04.09-f2b0fcd",
2246 models: [
2247 {
2248 slug: "claude-opus-4-6",
2249 name: "Opus 4.6",
2250 isCustom: false,
2251 capabilities: createModelCapabilities({
2252 optionDescriptors: [
2253 selectDescriptor("reasoning", "Reasoning", [
2254 { id: "high", label: "High", isDefault: true },
2255 ]),
2256 ],
2257 }),
2258 },
2259 ],
2260 slashCommands: [],
2261 skills: [],
2262 } as const satisfies ServerProvider;
2263 const refreshedProvider = {
2264 ...initialProvider,
2265 checkedAt: "2026-04-14T00:01:00.000Z",
2266 models: [],
2267 } satisfies ServerProvider;
2268 const changes = yield* PubSub.unbounded<ServerProvider>();
2269 const instance = {
2270 instanceId: cursorInstanceId,
2271 driverKind: cursorDriver,
2272 continuationIdentity: {
2273 driverKind: cursorDriver,
2274 continuationKey: "cursor:instance:cursor",
2275 },
2276 displayName: undefined,
2277 enabled: true,
2278 snapshot: {
2279 resolveMaintenance: () =>
2280 Effect.succeed(
2281 makeManualOnlyProviderMaintenanceCapabilities({
2282 provider: cursorDriver,
2283 packageName: null,
2284 }),
2285 ),
2286 getSnapshot: Effect.succeed(initialProvider),
2287 refresh: Effect.succeed(refreshedProvider),
2288 streamChanges: Stream.fromPubSub(changes),
2289 applyUsageLimits: () => Effect.void,
2290 },
2291 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
2292 textGeneration: {} as ProviderInstance["textGeneration"],
2293 } satisfies ProviderInstance;
2294 const layerInstanceRegistry = Layer.succeed(
2295 ProviderInstanceRegistry.ProviderInstanceRegistry,
2296 {
2297 getInstance: (instanceId) =>
2298 Effect.succeed(instanceId === cursorInstanceId ? instance : undefined),
2299 listInstances: Effect.succeed([instance]),
2300 listUnavailable: Effect.succeed([]),
2301 streamChanges: Stream.empty,
2302 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>
2303 PubSub.subscribe(pubsub),
2304 ),
2305 },
2306 );
2307 const scope = yield* Scope.make();
2308 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
2309 const runtimeServices = yield* Layer.build(
2310 ProviderRegistry.layer.pipe(
2311 Layer.provideMerge(layerInstanceRegistry),
2312 Layer.provideMerge(
2313 ServerConfig.layerTest(process.cwd(), {
2314 prefix: "t3-provider-registry-merged-persist-",
2315 }),
2316 ),
2317 Layer.provideMerge(layerBackgroundPolicyAlwaysRun),
2318 Layer.provideMerge(NodeServices.layer),
2319 ),
2320 ).pipe(Scope.provide(scope));
2321
2322 yield* Effect.gen(function* () {
2323 const registry = yield* ProviderRegistry.ProviderRegistry;
2324 const config = yield* ServerConfig.ServerConfig;
2325 const filePath = yield* resolveProviderStatusCachePath({
2326 cacheDir: config.providerStatusCacheDir,
2327 instanceId: cursorInstanceId,
2328 });
2329
2330 assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, [
2331 ...initialProvider.models,
2332 ]);
2333 const persisted = yield* awaitPersistedProvider(registry, refreshedProvider.checkedAt);
2334 yield* PubSub.publish(changes, refreshedProvider);
2335 yield* Fiber.join(persisted);
2336 const cachedProvider = yield* readProviderStatusCache(filePath);
2337
2338 assert.deepStrictEqual(
2339 cachedProvider,
2340 withBundledCompatibility({
2341 ...refreshedProvider,
2342 models: [...initialProvider.models],
2343 }),
2344 );
2345 }).pipe(Effect.provide(runtimeServices));
2346 }),
2347 );
2348
2349 it.effect(
2350 "persists authoritative OpenCode removals without resurrecting them on a failed live refresh",
2351 () =>
2352 Effect.gen(function* () {
2353 const openCodeDriver = ProviderDriverKind.make("opencode");
2354 const openCodeInstanceId = ProviderInstanceId.make("opencode");
2355 const initialProvider = {
2356 instanceId: openCodeInstanceId,
2357 driver: openCodeDriver,
2358 status: "ready",
2359 enabled: true,
2360 installed: true,
2361 auth: { status: "authenticated" },
2362 checkedAt: "2026-07-17T00:00:00.000Z",
2363 version: "1.0.0",
2364 models: [
2365 {
2366 slug: "github/gpt-5",
2367 name: "GPT-5",
2368 subProvider: "GitHub",
2369 isCustom: false,
2370 capabilities: null,
2371 },
2372 {
2373 slug: "removed-plugin/model",
2374 name: "Removed Plugin Model",
2375 subProvider: "Removed Plugin",
2376 isCustom: false,
2377 capabilities: null,
2378 },
2379 ],
2380 slashCommands: [],
2381 skills: [],
2382 } as const satisfies ServerProvider;
2383 const authoritativeProvider = {
2384 ...initialProvider,
2385 checkedAt: "2026-07-17T00:01:00.000Z",
2386 models: [initialProvider.models[0]!],
2387 } satisfies ServerProvider;
2388 const failedProvider = {
2389 ...authoritativeProvider,
2390 status: "error",
2391 auth: { status: "unknown" },
2392 checkedAt: "2026-07-17T00:02:00.000Z",
2393 models: [],
2394 message: "Failed to refresh OpenCode models.",
2395 } satisfies ServerProvider;
2396 const changes = yield* PubSub.unbounded<ServerProvider>();
2397 const instance = {
2398 instanceId: openCodeInstanceId,
2399 driverKind: openCodeDriver,
2400 continuationIdentity: {
2401 driverKind: openCodeDriver,
2402 continuationKey: "opencode:instance:opencode",
2403 },
2404 displayName: undefined,
2405 enabled: true,
2406 snapshot: {
2407 resolveMaintenance: () =>
2408 Effect.succeed(
2409 makeManualOnlyProviderMaintenanceCapabilities({
2410 provider: openCodeDriver,
2411 packageName: null,
2412 }),
2413 ),
2414 getSnapshot: Effect.succeed(initialProvider),
2415 refresh: Effect.succeed(authoritativeProvider),
2416 streamChanges: Stream.fromPubSub(changes),
2417 applyUsageLimits: () => Effect.void,
2418 },
2419 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
2420 textGeneration: {} as ProviderInstance["textGeneration"],
2421 } satisfies ProviderInstance;
2422 const layerInstanceRegistry = Layer.succeed(
2423 ProviderInstanceRegistry.ProviderInstanceRegistry,
2424 {
2425 getInstance: (instanceId) =>
2426 Effect.succeed(instanceId === openCodeInstanceId ? instance : undefined),
2427 listInstances: Effect.succeed([instance]),
2428 listUnavailable: Effect.succeed([]),
2429 streamChanges: Stream.empty,
2430 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>
2431 PubSub.subscribe(pubsub),
2432 ),
2433 },
2434 );
2435 const scope = yield* Scope.make();
2436 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
2437 const runtimeServices = yield* Layer.build(
2438 ProviderRegistry.layer.pipe(
2439 Layer.provideMerge(layerInstanceRegistry),
2440 Layer.provideMerge(
2441 ServerConfig.layerTest(process.cwd(), {
2442 prefix: "t3-provider-registry-opencode-authoritative-persist-",
2443 }),
2444 ),
2445 Layer.provideMerge(NodeServices.layer),
2446 ),
2447 ).pipe(Scope.provide(scope));
2448
2449 yield* Effect.gen(function* () {
2450 const registry = yield* ProviderRegistry.ProviderRegistry;
2451 const config = yield* ServerConfig.ServerConfig;
2452 const filePath = yield* resolveProviderStatusCachePath({
2453 cacheDir: config.providerStatusCacheDir,
2454 instanceId: openCodeInstanceId,
2455 });
2456
2457 const authoritativePersisted = yield* awaitPersistedProvider(
2458 registry,
2459 authoritativeProvider.checkedAt,
2460 );
2461 yield* PubSub.publish(changes, authoritativeProvider);
2462 yield* Fiber.join(authoritativePersisted);
2463 let cachedProvider = yield* readProviderStatusCache(filePath);
2464
2465 assert.deepStrictEqual(cachedProvider?.models, [authoritativeProvider.models[0]!]);
2466
2467 const failedPersisted = yield* awaitPersistedProvider(
2468 registry,
2469 failedProvider.checkedAt,
2470 );
2471 yield* PubSub.publish(changes, failedProvider);
2472 yield* Fiber.join(failedPersisted);
2473 cachedProvider = yield* readProviderStatusCache(filePath);
2474
2475 assert.deepStrictEqual(cachedProvider?.models, [authoritativeProvider.models[0]!]);
2476 assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, [
2477 authoritativeProvider.models[0]!,
2478 ]);
2479 }).pipe(Effect.provide(runtimeServices));
2480 }),
2481 );
2482
2483 it.effect("returns the cached provider list when a manual refresh fails", () =>
2484 Effect.gen(function* () {
2485 const codexDriver = ProviderDriverKind.make("codex");
2486 const codexInstanceId = ProviderInstanceId.make("codex");
2487 const cachedProvider = {
2488 instanceId: codexInstanceId,
2489 driver: codexDriver,
2490 status: "ready",
2491 enabled: true,
2492 installed: true,
2493 auth: { status: "authenticated" },
2494 checkedAt: "2026-04-29T10:00:00.000Z",
2495 version: "1.0.0",
2496 models: [],
2497 slashCommands: [],
2498 skills: [],
2499 } as const satisfies ServerProvider;
2500 const instance = {
2501 instanceId: codexInstanceId,
2502 driverKind: codexDriver,
2503 continuationIdentity: {
2504 driverKind: codexDriver,
2505 continuationKey: "codex:instance:codex",
2506 },
2507 displayName: undefined,
2508 enabled: true,
2509 snapshot: {
2510 resolveMaintenance: () =>
2511 Effect.succeed(
2512 makeManualOnlyProviderMaintenanceCapabilities({
2513 provider: codexDriver,
2514 packageName: null,
2515 }),
2516 ),
2517 getSnapshot: Effect.succeed(cachedProvider),
2518 refresh: Effect.die(new Error("simulated refresh failure")),
2519 streamChanges: Stream.empty,
2520 applyUsageLimits: () => Effect.void,
2521 },
2522 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
2523 textGeneration: {} as ProviderInstance["textGeneration"],
2524 } satisfies ProviderInstance;
2525 const layerInstanceRegistry = Layer.succeed(
2526 ProviderInstanceRegistry.ProviderInstanceRegistry,
2527 {
2528 getInstance: (instanceId) =>
2529 Effect.succeed(instanceId === codexInstanceId ? instance : undefined),
2530 listInstances: Effect.succeed([instance]),
2531 listUnavailable: Effect.succeed([]),
2532 streamChanges: Stream.empty,
2533 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>
2534 PubSub.subscribe(pubsub),
2535 ),
2536 },
2537 );
2538 const scope = yield* Scope.make();
2539 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
2540 const runtimeServices = yield* Layer.build(
2541 ProviderRegistry.layer.pipe(
2542 Layer.provideMerge(layerInstanceRegistry),
2543 Layer.provideMerge(
2544 ServerConfig.layerTest(process.cwd(), {
2545 prefix: "t3-provider-registry-refresh-failure-",
2546 }),
2547 ),
2548 Layer.provideMerge(layerBackgroundPolicyAlwaysRun),
2549 Layer.provideMerge(NodeServices.layer),
2550 ),
2551 ).pipe(Scope.provide(scope));
2552
2553 yield* Effect.gen(function* () {
2554 const registry = yield* ProviderRegistry.ProviderRegistry;
2555
2556 assert.deepStrictEqual(yield* registry.getProviders, [
2557 withBundledCompatibility(cachedProvider),
2558 ]);
2559 assert.deepStrictEqual(yield* registry.refresh(codexDriver), [
2560 withBundledCompatibility(cachedProvider),
2561 ]);
2562 assert.deepStrictEqual(yield* registry.refreshInstance(codexInstanceId), [
2563 withBundledCompatibility(cachedProvider),
2564 ]);
2565 }).pipe(Effect.provide(runtimeServices));
2566 }),
2567 );
2568
2569 it.effect("keeps consuming registry changes after one sync fails", () =>
2570 Effect.gen(function* () {
2571 const codexDriver = ProviderDriverKind.make("codex");
2572 const codexInstanceId = ProviderInstanceId.make("codex");
2573 const claudeDriver = ProviderDriverKind.make("claudeAgent");
2574 const claudeInstanceId = ProviderInstanceId.make("claudeAgent");
2575 const codexProvider = {
2576 instanceId: codexInstanceId,
2577 driver: codexDriver,
2578 status: "ready",
2579 enabled: true,
2580 installed: true,
2581 auth: { status: "authenticated" },
2582 checkedAt: "2026-04-29T10:00:00.000Z",
2583 version: "1.0.0",
2584 models: [],
2585 slashCommands: [],
2586 skills: [],
2587 } as const satisfies ServerProvider;
2588 const claudeProvider = {
2589 instanceId: claudeInstanceId,
2590 driver: claudeDriver,
2591 status: "ready",
2592 enabled: true,
2593 installed: true,
2594 auth: { status: "authenticated" },
2595 checkedAt: "2026-04-29T10:01:00.000Z",
2596 version: "1.0.0",
2597 models: [],
2598 slashCommands: [],
2599 skills: [],
2600 } as const satisfies ServerProvider;
2601 const makeInstance = (provider: ServerProvider): ProviderInstance => ({
2602 instanceId: provider.instanceId,
2603 driverKind: provider.driver,
2604 continuationIdentity: {
2605 driverKind: provider.driver,
2606 continuationKey: `${provider.driver}:instance:${provider.instanceId}`,
2607 },
2608 displayName: undefined,
2609 enabled: true,
2610 snapshot: {
2611 resolveMaintenance: () =>
2612 Effect.succeed(
2613 makeManualOnlyProviderMaintenanceCapabilities({
2614 provider: provider.driver,
2615 packageName: null,
2616 }),
2617 ),
2618 getSnapshot: Effect.succeed(provider),
2619 refresh: Effect.succeed(provider),
2620 streamChanges: Stream.empty,
2621 applyUsageLimits: () => Effect.void,
2622 },
2623 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
2624 textGeneration: {} as ProviderInstance["textGeneration"],
2625 });
2626 const codexInstance = makeInstance(codexProvider);
2627 const claudeInstance = makeInstance(claudeProvider);
2628 const changes = yield* PubSub.unbounded<void>();
2629 const instancesRef = yield* Ref.make<ReadonlyArray<ProviderInstance>>([codexInstance]);
2630 const failNextList = yield* Ref.make(false);
2631 const wait = () => Effect.yieldNow;
2632 const layerInstanceRegistry = Layer.succeed(
2633 ProviderInstanceRegistry.ProviderInstanceRegistry,
2634 {
2635 getInstance: (instanceId) =>
2636 Ref.get(instancesRef).pipe(
2637 Effect.map((instances) =>
2638 instances.find((instance) => instance.instanceId === instanceId),
2639 ),
2640 ),
2641 listInstances: Effect.gen(function* () {
2642 const shouldFail = yield* Ref.get(failNextList);
2643 if (shouldFail) {
2644 yield* Ref.set(failNextList, false);
2645 return yield* Effect.die(new Error("simulated registry list failure"));
2646 }
2647 return yield* Ref.get(instancesRef);
2648 }),
2649 listUnavailable: Effect.succeed([]),
2650 streamChanges: Stream.fromPubSub(changes),
2651 subscribeChanges: PubSub.subscribe(changes),
2652 },
2653 );
2654 const scope = yield* Scope.make();
2655 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
2656 const runtimeServices = yield* Layer.build(
2657 ProviderRegistry.layer.pipe(
2658 Layer.provideMerge(layerInstanceRegistry),
2659 Layer.provideMerge(
2660 ServerConfig.layerTest(process.cwd(), {
2661 prefix: "t3-provider-registry-sync-failure-",
2662 }),
2663 ),
2664 Layer.provideMerge(layerBackgroundPolicyAlwaysRun),
2665 Layer.provideMerge(NodeServices.layer),
2666 ),
2667 ).pipe(Scope.provide(scope));
2668
2669 yield* Effect.gen(function* () {
2670 const registry = yield* ProviderRegistry.ProviderRegistry;
2671 assert.deepStrictEqual(yield* registry.getProviders, [
2672 withBundledCompatibility(codexProvider),
2673 ]);
2674
2675 yield* Ref.set(failNextList, true);
2676 yield* PubSub.publish(changes, undefined);
2677
2678 yield* Ref.set(instancesRef, [codexInstance, claudeInstance]);
2679 yield* PubSub.publish(changes, undefined);
2680
2681 let providers = yield* registry.getProviders;
2682 for (
2683 let attempt = 0;
2684 attempt < 50 && !providers.some((provider) => provider.instanceId === claudeInstanceId);
2685 attempt += 1
2686 ) {
2687 yield* wait();
2688 providers = yield* registry.getProviders;
2689 }
2690
2691 assert.deepStrictEqual(
2692 providers.map((provider) => provider.instanceId).toSorted(),
2693 [codexInstanceId, claudeInstanceId].toSorted(),
2694 );
2695 }).pipe(Effect.provide(runtimeServices));
2696 }),
2697 );
2698
2699 // This test intentionally avoids `mockCommandSpawnerLayer` so the real
2700 // `probeCodexAppServerProvider` path runs — including the full
2701 // `codex app-server` RPC handshake via `CodexClient.layerChildProcess`.
2702 // We point `binaryPath` at a name that cannot exist on any machine so
2703 // the real `ChildProcessSpawner` deterministically returns ENOENT; the
2704 // probe wraps that as `CodexAppServerSpawnError` and
2705 // `checkCodexProviderStatus` turns it into the user-visible "not
2706 // installed" error snapshot. If the aggregator's `syncLiveSources`
2707 // breaks — the `codex_personal`-never-probes bug we are guarding
2708 // against — that snapshot never lands in `getProviders` and the
2709 // assertions below fail.
2710 it.effect("propagates real Codex probe failures to the aggregator at boot", () =>
2711 Effect.gen(function* () {
2712 const missingBinary = `t3code_codex_missing_`;
2713 const serverSettings = yield* makeMutableServerSettingsService(
2714 decodeServerSettings(
2715 deepMerge(encodedDefaultServerSettings, {
2716 // `providerInstances` keys are branded `ProviderInstanceId`;
2717 // the branded index signature rejects plain string literals
2718 // at the TS level even though the runtime schema happily
2719 // accepts + decodes them. Cast the patch to `unknown` so
2720 // the `Schema.decodeSync` below does the real validation.
2721 providerInstances: {
2722 // Disable every built-in probe that would otherwise spawn
2723 // on the CI host. `enabled: false` short-circuits each
2724 // driver's probe *before* it touches the spawner, so the
2725 // test environment stays isolated from the dev
2726 // machine's PATH.
2727 ...disabledDefaultSlots("codex", "claudeAgent", "cursor", "grok", "opencode"),
2728 // Matches the shape the user had in `.t3/dev/settings.json`
2729 // when the bug was reported: a custom enabled Codex instance
2730 // pointing at a binary the server has to actually spawn.
2731 codex_personal: {
2732 driver: "codex",
2733 displayName: "Codex Personal",
2734 enabled: true,
2735 config: {
2736 binaryPath: missingBinary,
2737 homePath: `/tmp/${missingBinary}_home`,
2738 },
2739 },
2740 } as unknown as ContractServerSettings["providerInstances"],
2741 }),
2742 ),
2743 );
2744 const scope = yield* Scope.make();
2745 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
2746 const layerProviderRegistry = ProviderRegistry.layer.pipe(
2747 Layer.provideMerge(ProviderInstanceRegistryHydration.layer),
2748 Layer.provideMerge(AntigravityInstallation.AntigravityInstallation.layer),
2749 Layer.provideMerge(
2750 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
2751 ),
2752 Layer.provideMerge(ServerSecretStore.layer),
2753 Layer.provideMerge(
2754 ServerConfig.layerTest(process.cwd(), {
2755 prefix: "t3-provider-registry-",
2756 }),
2757 ),
2758 Layer.provideMerge(layerTestHttpClient),
2759 Layer.provideMerge(
2760 Layer.succeed(
2761 ProviderEventLoggers.ProviderEventLoggers,
2762 ProviderEventLoggers.NoOpProviderEventLoggers,
2763 ),
2764 ),
2765 Layer.provideMerge(ProviderLatestVersions.layer),
2766 Layer.provideMerge(
2767 Layer.merge(McpProviderSessions.layer, ScriptProgress.layerUnavailable),
2768 ),
2769 Layer.provideMerge(ModelManifest.layerTest),
2770 Layer.provideMerge(ResetCreditCoordinator.layerTest),
2771 Layer.provideMerge(
2772 OpenCodeRuntime.layer.pipe(Layer.provide(OpenCodeServerLedger.layerTest)),
2773 ),
2774 Layer.provideMerge(layerBackgroundPolicyAlwaysRun),
2775 // NO spawner mock — `ChildProcessSpawner` is supplied by the
2776 // outer `NodeServices.layer` on `it.layer(...)` and will
2777 // genuinely spawn a subprocess. The missing-binary ENOENT is
2778 // what exercises the same failure mode as a misconfigured
2779 // production `binaryPath`.
2780 );
2781 const runtimeServices = yield* Layer.build(layerProviderRegistry).pipe(
2782 Scope.provide(scope),
2783 );
2784
2785 yield* Effect.gen(function* () {
2786 const registry = yield* ProviderRegistry.ProviderRegistry;
2787 let providers = yield* registry.getProviders;
2788 for (
2789 let attempts = 0;
2790 attempts < 50 &&
2791 providers.find((provider) => provider.instanceId === "codex_personal")?.status !==
2792 "error";
2793 attempts += 1
2794 ) {
2795 yield* Effect.yieldNow;
2796 providers = yield* registry.getProviders;
2797 }
2798 const codexPersonal = providers.find(
2799 (provider) => provider.instanceId === "codex_personal",
2800 );
2801 assert.notStrictEqual(
2802 codexPersonal,
2803 undefined,
2804 `Expected the aggregator to know about codex_personal; instead saw: ${providers
2805 .map((provider) => provider.instanceId)
2806 .join(", ")}`,
2807 );
2808 assert.strictEqual(
2809 codexPersonal?.status,
2810 "error",
2811 "Real Codex probe against a missing binary should surface as 'error' in the aggregator",
2812 );
2813 assert.strictEqual(codexPersonal?.installed, false);
2814 assert.include(codexPersonal?.message, missingBinary);
2815 assert.include(codexPersonal?.message, "Settings → Providers → Codex → Binary path");
2816 }).pipe(Effect.provide(runtimeServices));
2817 }),
2818 );
2819
2820 // A binary path change must rebuild Codex and publish its new probe result.
2821 it.effect("re-probes when settings change the codex binaryPath", () =>
2822 Effect.gen(function* () {
2823 const firstMissing = `t3code_codex_first_`;
2824 const secondMissing = `t3code_codex_second_`;
2825 const spawnedCommands: Array<string> = [];
2826 const secondProbeStarted = yield* Deferred.make<void>();
2827 const releaseSecondProbe = yield* Deferred.make<void>();
2828 const allowLazySettingsStream = yield* Deferred.make<void>();
2829 const mutableServerSettings = yield* makeMutableServerSettingsService(
2830 decodeServerSettings(
2831 deepMerge(encodedDefaultServerSettings, {
2832 providerInstances: {
2833 ...disabledDefaultSlots("claudeAgent", "cursor", "grok", "opencode"),
2834 [ProviderInstanceId.make("codex")]: {
2835 driver: ProviderDriverKind.make("codex"),
2836 enabled: true,
2837 config: { binaryPath: firstMissing },
2838 },
2839 },
2840 }),
2841 ),
2842 );
2843 const serverSettings = {
2844 ...mutableServerSettings,
2845 streamChanges: Stream.unwrap(
2846 Deferred.await(allowLazySettingsStream).pipe(
2847 Effect.as(mutableServerSettings.streamChanges),
2848 ),
2849 ),
2850 } satisfies ServerSettingsModule.ServerSettingsService["Service"];
2851 const scope = yield* Scope.make();
2852 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
2853 const layerProviderRegistry = ProviderRegistry.layer.pipe(
2854 Layer.provideMerge(ProviderInstanceRegistryHydration.layer),
2855 Layer.provideMerge(AntigravityInstallation.AntigravityInstallation.layer),
2856 Layer.provideMerge(
2857 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
2858 ),
2859 Layer.provideMerge(ServerSecretStore.layer),
2860 Layer.provideMerge(
2861 ServerConfig.layerTest(process.cwd(), {
2862 prefix: "t3-provider-registry-",
2863 }),
2864 ),
2865 Layer.provideMerge(layerTestHttpClient),
2866 Layer.provideMerge(
2867 Layer.succeed(
2868 ProviderEventLoggers.ProviderEventLoggers,
2869 ProviderEventLoggers.NoOpProviderEventLoggers,
2870 ),
2871 ),
2872 Layer.provideMerge(ProviderLatestVersions.layer),
2873 Layer.provideMerge(
2874 Layer.merge(McpProviderSessions.layer, ScriptProgress.layerUnavailable),
2875 ),
2876 Layer.provideMerge(ModelManifest.layerTest),
2877 Layer.provideMerge(ResetCreditCoordinator.layerTest),
2878 Layer.provideMerge(
2879 OpenCodeRuntime.layer.pipe(Layer.provide(OpenCodeServerLedger.layerTest)),
2880 ),
2881 Layer.updateService(ChildProcessSpawner.ChildProcessSpawner, (spawner) =>
2882 ChildProcessSpawner.make((command) => {
2883 if (command._tag !== "StandardCommand") return spawner.spawn(command);
2884 spawnedCommands.push(command.command);
2885 const beforeSpawn =
2886 command.command === secondMissing
2887 ? Deferred.succeed(secondProbeStarted, undefined).pipe(
2888 Effect.andThen(Deferred.await(releaseSecondProbe)),
2889 )
2890 : Effect.void;
2891 return beforeSpawn.pipe(Effect.andThen(spawner.spawn(command)));
2892 }),
2893 ),
2894 Layer.provideMerge(NodeServices.layer),
2895 Layer.provideMerge(layerBackgroundPolicyAlwaysRun),
2896 );
2897 const runtimeServices = yield* Layer.build(layerProviderRegistry).pipe(
2898 Scope.provide(scope),
2899 );
2900
2901 yield* Effect.gen(function* () {
2902 const registry = yield* ProviderRegistry.ProviderRegistry;
2903 const codexSnapshots = registry.streamChanges.pipe(
2904 Stream.map((providers) =>
2905 providers.find((provider) => provider.instanceId === "codex"),
2906 ),
2907 Stream.filter((provider): provider is ServerProvider => provider !== undefined),
2908 );
2909 const firstError = yield* Stream.toPull(
2910 codexSnapshots.pipe(Stream.filter((provider) => provider.status === "error")),
2911 );
2912 const currentCodex = (yield* registry.getProviders).find(
2913 (provider) => provider.instanceId === "codex",
2914 );
2915 const initialCodex =
2916 currentCodex?.status === "error" ? currentCodex : (yield* firstError)[0];
2917 assert.strictEqual(initialCodex?.status, "error");
2918 assert.strictEqual(initialCodex?.installed, false);
2919 assert.deepStrictEqual(spawnedCommands, [firstMissing]);
2920
2921 const pendingRebuild = yield* Stream.toPull(
2922 codexSnapshots.pipe(
2923 Stream.filter((provider) => provider.status === "warning" && !provider.installed),
2924 ),
2925 );
2926 yield* serverSettings.updateSettings({
2927 providerInstances: {
2928 ...disabledDefaultSlots("claudeAgent", "cursor", "grok", "opencode"),
2929 [ProviderInstanceId.make("codex")]: {
2930 driver: ProviderDriverKind.make("codex"),
2931 enabled: true,
2932 config: { binaryPath: secondMissing },
2933 },
2934 },
2935 });
2936 // Start the lazy stream only after publishing. A watcher that did
2937 // not subscribe before forking has already lost this update.
2938 yield* Deferred.succeed(allowLazySettingsStream, undefined);
2939
2940 // Hold the second probe until the aggregator sees the rebuilt
2941 // instance. Its next error must come from the new executable.
2942 yield* Deferred.await(secondProbeStarted);
2943 yield* pendingRebuild;
2944 const rebuiltError = yield* Stream.toPull(
2945 codexSnapshots.pipe(Stream.filter((provider) => provider.status === "error")),
2946 );
2947 yield* Deferred.succeed(releaseSecondProbe, undefined);
2948 const [reprobedCodex] = yield* rebuiltError;
2949 assert.deepStrictEqual(spawnedCommands, [firstMissing, secondMissing]);
2950 assert.strictEqual(reprobedCodex?.status, "error");
2951 assert.strictEqual(reprobedCodex?.installed, false);
2952 }).pipe(Effect.provide(runtimeServices));
2953 }),
2954 );
2955
2956 it.effect("includes unavailable instance snapshots in getProviders", () =>
2957 Effect.gen(function* () {
2958 const serverSettings = yield* makeMutableServerSettingsService(
2959 decodeServerSettings(
2960 deepMerge(encodedDefaultServerSettings, {
2961 providerInstances: {
2962 ...disabledDefaultSlots("codex", "claudeAgent", "cursor", "grok", "opencode"),
2963 ghost_main: {
2964 driver: "ghostDriver",
2965 displayName: "A fork-only driver we don't ship",
2966 enabled: false,
2967 config: { arbitrary: "payload" },
2968 },
2969 } as unknown as ContractServerSettings["providerInstances"],
2970 }),
2971 ),
2972 );
2973 const scope = yield* Scope.make();
2974 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
2975 const layerProviderRegistry = ProviderRegistry.layer.pipe(
2976 Layer.provideMerge(ProviderInstanceRegistryHydration.layer),
2977 Layer.provideMerge(AntigravityInstallation.AntigravityInstallation.layer),
2978 Layer.provideMerge(
2979 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
2980 ),
2981 Layer.provideMerge(ServerSecretStore.layer),
2982 Layer.provideMerge(
2983 ServerConfig.layerTest(process.cwd(), {
2984 prefix: "t3-provider-registry-",
2985 }),
2986 ),
2987 Layer.provideMerge(layerTestHttpClient),
2988 Layer.provideMerge(
2989 Layer.succeed(
2990 ProviderEventLoggers.ProviderEventLoggers,
2991 ProviderEventLoggers.NoOpProviderEventLoggers,
2992 ),
2993 ),
2994 Layer.provideMerge(ProviderLatestVersions.layer),
2995 Layer.provideMerge(
2996 Layer.merge(McpProviderSessions.layer, ScriptProgress.layerUnavailable),
2997 ),
2998 Layer.provideMerge(ModelManifest.layerTest),
2999 Layer.provideMerge(ResetCreditCoordinator.layerTest),
3000 Layer.provideMerge(
3001 OpenCodeRuntime.layer.pipe(Layer.provide(OpenCodeServerLedger.layerTest)),
3002 ),
3003 Layer.provideMerge(NodeServices.layer),
3004 Layer.provideMerge(layerBackgroundPolicyAlwaysRun),
3005 );
3006 const runtimeServices = yield* Layer.build(layerProviderRegistry).pipe(
3007 Scope.provide(scope),
3008 );
3009
3010 yield* Effect.gen(function* () {
3011 const registry = yield* ProviderRegistry.ProviderRegistry;
3012 const providers = yield* registry.getProviders;
3013 const ghost = providers.find((provider) => provider.instanceId === "ghost_main");
3014
3015 assert.notStrictEqual(ghost, undefined);
3016 assert.strictEqual(ghost?.driver, "ghostDriver");
3017 assert.strictEqual(ghost?.availability, "unavailable");
3018 assert.match(ghost?.unavailableReason ?? "", /ghostDriver/);
3019 }).pipe(Effect.provide(runtimeServices));
3020 }),
3021 );
3022
3023 it.effect(
3024 "keeps Cursor disabled and skips provider probing when settings use their defaults",
3025 () =>
3026 Effect.gen(function* () {
3027 const serverSettings = yield* makeMutableServerSettingsService(
3028 decodeServerSettings(
3029 deepMerge(encodedDefaultServerSettings, {
3030 providerInstances: disabledDefaultSlots("codex", "grok"),
3031 }),
3032 ),
3033 );
3034 let cursorSpawned = false;
3035 const scope = yield* Scope.make();
3036 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
3037 const layerProviderRegistry = ProviderRegistry.layer.pipe(
3038 Layer.provideMerge(ProviderInstanceRegistryHydration.layer),
3039 Layer.provideMerge(AntigravityInstallation.AntigravityInstallation.layer),
3040 Layer.provideMerge(
3041 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
3042 ),
3043 Layer.provideMerge(ServerSecretStore.layer),
3044 Layer.provideMerge(
3045 ServerConfig.layerTest(process.cwd(), {
3046 prefix: "t3-provider-registry-",
3047 }),
3048 ),
3049 Layer.provideMerge(layerTestHttpClient),
3050 Layer.provideMerge(
3051 Layer.succeed(
3052 ProviderEventLoggers.ProviderEventLoggers,
3053 ProviderEventLoggers.NoOpProviderEventLoggers,
3054 ),
3055 ),
3056 Layer.provideMerge(ProviderLatestVersions.layer),
3057 Layer.provideMerge(
3058 Layer.merge(McpProviderSessions.layer, ScriptProgress.layerUnavailable),
3059 ),
3060 Layer.provideMerge(ModelManifest.layerTest),
3061 Layer.provideMerge(ResetCreditCoordinator.layerTest),
3062 Layer.provideMerge(
3063 OpenCodeRuntime.layer.pipe(Layer.provide(OpenCodeServerLedger.layerTest)),
3064 ),
3065 Layer.provideMerge(layerBackgroundPolicyAlwaysRun),
3066 Layer.provideMerge(
3067 layerMockCommandSpawner((command, args) => {
3068 if (command === "cursor-agent") {
3069 cursorSpawned = true;
3070 }
3071 const joined = args.join(" ");
3072 if (joined === "--version") {
3073 return {
3074 stdout: `${command} 1.0.0\n`,
3075 stderr: "",
3076 code: 0,
3077 };
3078 }
3079 if (joined === "auth status") {
3080 return {
3081 stdout: '{"authenticated":true}\n',
3082 stderr: "",
3083 code: 0,
3084 };
3085 }
3086 throw new Error(`Unexpected args: ${command} ${joined}`);
3087 }),
3088 ),
3089 );
3090 const runtimeServices = yield* Layer.build(
3091 Layer.mergeAll(
3092 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
3093 layerProviderRegistry,
3094 ),
3095 ).pipe(Scope.provide(scope));
3096
3097 yield* Effect.gen(function* () {
3098 const registry = yield* ProviderRegistry.ProviderRegistry;
3099 const providers = yield* registry.getProviders;
3100 const cursorProvider = providers.find(
3101 (provider) => provider.instanceId === ProviderInstanceId.make("cursor"),
3102 );
3103
3104 assert.deepStrictEqual(providers.map((provider) => provider.instanceId).toSorted(), [
3105 "antigravity",
3106 "claudeAgent",
3107 "codex",
3108 "cursor",
3109 "grok",
3110 "muse",
3111 "opencode",
3112 "pi",
3113 ]);
3114 assert.strictEqual(cursorProvider?.enabled, false);
3115 assert.strictEqual(cursorProvider?.status, "disabled");
3116 assert.strictEqual(cursorProvider?.message, "Cursor is disabled in T3 Code settings.");
3117 const museProvider = providers.find((provider) => provider.driver === "muse");
3118 assert.strictEqual(museProvider?.enabled, false);
3119 assert.strictEqual(museProvider?.status, "disabled");
3120 assert.strictEqual(cursorSpawned, false);
3121 }).pipe(Effect.provide(runtimeServices));
3122 }),
3123 );
3124
3125 it.effect("skips codex probes entirely when the provider is disabled", () =>
3126 Effect.gen(function* () {
3127 const status = yield* checkCodexProviderStatus(disabledCodexSettings).pipe(
3128 Effect.provide(layerFailingSpawner("spawn codex ENOENT")),
3129 );
3130 assert.strictEqual(status.enabled, false);
3131 assert.strictEqual(status.status, "disabled");
3132 assert.strictEqual(status.installed, false);
3133 assert.strictEqual(status.message, "Codex is disabled in T3 Code settings.");
3134 }),
3135 );
3136 });
3137
3138 // ── checkClaudeProviderStatus tests ──────────────────────────
3139
3140 describe("checkClaudeProviderStatus", () => {
3141 it.effect("returns ready when claude is installed and authenticated", () =>
3142 Effect.gen(function* () {
3143 const status = yield* checkClaudeProviderStatus(
3144 defaultClaudeSettings,
3145 claudeCapabilities(),
3146 );
3147 assert.strictEqual(status.status, "ready");
3148 assert.strictEqual(status.installed, true);
3149 assert.strictEqual(status.auth.status, "authenticated");
3150 }).pipe(
3151 Effect.provide(
3152 layerMockSpawner((args) => {
3153 const joined = args.join(" ");
3154 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3155 if (joined === "auth status")
3156 return {
3157 stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
3158 stderr: "",
3159 code: 0,
3160 };
3161 throw new Error(`Unexpected args: ${joined}`);
3162 }),
3163 ),
3164 ),
3165 );
3166
3167 it.effect("returns ready and labels Bedrock-backed Claude as authenticated", () =>
3168 Effect.gen(function* () {
3169 // Bedrock authenticates via external AWS credentials, so the SDK init
3170 // reports only `apiProvider` with no subscription or token.
3171 const status = yield* checkClaudeProviderStatus(
3172 defaultClaudeSettings,
3173 claudeCapabilities({ apiProvider: "bedrock" }),
3174 );
3175 assert.strictEqual(status.status, "ready");
3176 assert.strictEqual(status.installed, true);
3177 assert.strictEqual(status.auth.status, "authenticated");
3178 assert.strictEqual(status.auth.type, "bedrock");
3179 assert.strictEqual(status.auth.label, "Amazon Bedrock");
3180 }).pipe(
3181 Effect.provide(
3182 layerMockSpawner((args) => {
3183 const joined = args.join(" ");
3184 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3185 throw new Error(`Unexpected args: ${joined}`);
3186 }),
3187 ),
3188 ),
3189 );
3190
3191 it.effect("reports a logged-out CLI as unauthenticated", () =>
3192 Effect.gen(function* () {
3193 // The capability probe resolves for a logged-out CLI, so `tokenSource:
3194 // "none"` is the only thing separating it from an authenticated one.
3195 const status = yield* checkClaudeProviderStatus(
3196 defaultClaudeSettings,
3197 claudeCapabilities({
3198 tokenSource: "none",
3199 apiKeySource: "none",
3200 apiProvider: "firstParty",
3201 }),
3202 );
3203 assert.strictEqual(status.status, "error");
3204 assert.strictEqual(status.auth.status, "unauthenticated");
3205 }).pipe(
3206 Effect.provide(
3207 layerMockSpawner((args) => {
3208 const joined = args.join(" ");
3209 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3210 throw new Error(`Unexpected args: ${joined}`);
3211 }),
3212 ),
3213 ),
3214 );
3215
3216 it.effect("keeps an API key install authenticated when it reports no token source", () =>
3217 Effect.gen(function* () {
3218 // `ANTHROPIC_API_KEY` never populates `tokenSource`, so reading that
3219 // field alone would log the install out.
3220 const status = yield* checkClaudeProviderStatus(
3221 defaultClaudeSettings,
3222 claudeCapabilities({
3223 tokenSource: "none",
3224 apiKeySource: "ANTHROPIC_API_KEY",
3225 apiProvider: "firstParty",
3226 }),
3227 );
3228 assert.strictEqual(status.status, "ready");
3229 assert.strictEqual(status.auth.status, "authenticated");
3230 }).pipe(
3231 Effect.provide(
3232 layerMockSpawner((args) => {
3233 const joined = args.join(" ");
3234 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3235 throw new Error(`Unexpected args: ${joined}`);
3236 }),
3237 ),
3238 ),
3239 );
3240
3241 it.effect("keeps a third-party backend authenticated without any token source", () =>
3242 Effect.gen(function* () {
3243 // Bedrock and Vertex authenticate outside the CLI, so the account
3244 // payload is empty by design rather than because nobody logged in.
3245 const status = yield* checkClaudeProviderStatus(
3246 defaultClaudeSettings,
3247 claudeCapabilities({ tokenSource: "none", apiProvider: "bedrock" }),
3248 );
3249 assert.strictEqual(status.status, "ready");
3250 assert.strictEqual(status.auth.status, "authenticated");
3251 }).pipe(
3252 Effect.provide(
3253 layerMockSpawner((args) => {
3254 const joined = args.join(" ");
3255 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3256 throw new Error(`Unexpected args: ${joined}`);
3257 }),
3258 ),
3259 ),
3260 );
3261
3262 it.effect("keeps a CLI that says nothing about its account authenticated", () =>
3263 Effect.gen(function* () {
3264 // Profile-authenticated installs report no token source at all, and a
3265 // CLI too old to send an account payload reports nothing whatsoever.
3266 // Only `tokenSource: "none"` disproves authentication; saying nothing
3267 // is not the same as saying no.
3268 const status = yield* checkClaudeProviderStatus(
3269 defaultClaudeSettings,
3270 claudeCapabilities(),
3271 );
3272 assert.strictEqual(status.status, "ready");
3273 assert.strictEqual(status.auth.status, "authenticated");
3274 }).pipe(
3275 Effect.provide(
3276 layerMockSpawner((args) => {
3277 const joined = args.join(" ");
3278 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3279 throw new Error(`Unexpected args: ${joined}`);
3280 }),
3281 ),
3282 ),
3283 );
3284
3285 it.effect("returns a display label for claude subscription types", () =>
3286 Effect.gen(function* () {
3287 const status = yield* checkClaudeProviderStatus(
3288 defaultClaudeSettings,
3289 claudeCapabilities({ subscriptionType: "maxplan" }),
3290 );
3291 assert.strictEqual(status.status, "ready");
3292 assert.strictEqual(status.auth.status, "authenticated");
3293 assert.strictEqual(status.auth.type, "maxplan");
3294 assert.strictEqual(status.auth.label, "Claude Max Subscription");
3295 }).pipe(
3296 Effect.provide(
3297 layerMockSpawner((args) => {
3298 const joined = args.join(" ");
3299 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3300 if (joined === "auth status")
3301 return {
3302 stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
3303 stderr: "",
3304 code: 0,
3305 };
3306 throw new Error(`Unexpected args: ${joined}`);
3307 }),
3308 ),
3309 ),
3310 );
3311
3312 it.effect("reads banked resets only for subscription logins", () =>
3313 Effect.gen(function* () {
3314 const check = (overrides: Partial<TestClaudeCapabilities>) =>
3315 checkClaudeProviderStatus(
3316 defaultClaudeSettings,
3317 () =>
3318 Effect.succeed({
3319 email: undefined,
3320 subscriptionType: undefined,
3321 tokenSource: undefined,
3322 apiKeySource: undefined,
3323 apiProvider: undefined,
3324 slashCommands: [],
3325 usage: { rate_limits_available: true, rate_limits: {} },
3326 ...overrides,
3327 }),
3328 undefined,
3329 undefined,
3330 undefined,
3331 undefined,
3332 () => Effect.succeed({ availableCount: 2 }),
3333 );
3334 const subscription = yield* check({ subscriptionType: "max" });
3335 const bedrock = yield* check({ apiProvider: "bedrock" });
3336 assert.deepStrictEqual(subscription.usageLimits?.resetCredits, { availableCount: 2 });
3337 assert.strictEqual(bedrock.usageLimits?.resetCredits, undefined);
3338 }).pipe(
3339 Effect.provide(
3340 layerMockSpawner((args) => {
3341 const joined = args.join(" ");
3342 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3343 throw new Error(`Unexpected args: ${joined}`);
3344 }),
3345 ),
3346 ),
3347 );
3348
3349 it.effect("does not duplicate Claude in full subscription labels", () =>
3350 Effect.gen(function* () {
3351 const status = yield* checkClaudeProviderStatus(
3352 defaultClaudeSettings,
3353 claudeCapabilities({
3354 subscriptionType: "Claude Max Subscription",
3355 }),
3356 );
3357 assert.strictEqual(status.auth.status, "authenticated");
3358 assert.strictEqual(status.auth.type, "Claude Max Subscription");
3359 assert.strictEqual(status.auth.label, "Claude Max Subscription");
3360 }).pipe(
3361 Effect.provide(
3362 layerMockSpawner((args) => {
3363 const joined = args.join(" ");
3364 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3365 throw new Error(`Unexpected args: ${joined}`);
3366 }),
3367 ),
3368 ),
3369 );
3370
3371 it.effect("does not duplicate Claude in provider-prefixed subscription names", () =>
3372 Effect.gen(function* () {
3373 const status = yield* checkClaudeProviderStatus(
3374 defaultClaudeSettings,
3375 claudeCapabilities({
3376 subscriptionType: "Claude Max",
3377 }),
3378 );
3379 assert.strictEqual(status.auth.status, "authenticated");
3380 assert.strictEqual(status.auth.type, "Claude Max");
3381 assert.strictEqual(status.auth.label, "Claude Max Subscription");
3382 }).pipe(
3383 Effect.provide(
3384 layerMockSpawner((args) => {
3385 const joined = args.join(" ");
3386 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3387 throw new Error(`Unexpected args: ${joined}`);
3388 }),
3389 ),
3390 ),
3391 );
3392
3393 it.effect("returns claude auth email from initialization result", () =>
3394 Effect.gen(function* () {
3395 const status = yield* checkClaudeProviderStatus(
3396 defaultClaudeSettings,
3397 claudeCapabilities({ email: "claude@example.com" }),
3398 );
3399 assert.strictEqual(status.auth.status, "authenticated");
3400 assert.strictEqual(status.auth.email, "claude@example.com");
3401 }).pipe(
3402 Effect.provide(
3403 layerMockSpawner((args) => {
3404 const joined = args.join(" ");
3405 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3406 if (joined === "auth status")
3407 return {
3408 stdout:
3409 '{"loggedIn":true,"authMethod":"claude.ai","account":{"email":"claude@example.com"}}\n',
3410 stderr: "",
3411 code: 0,
3412 };
3413 throw new Error(`Unexpected args: ${joined}`);
3414 }),
3415 ),
3416 ),
3417 );
3418
3419 it.effect("runs Claude status probes with the configured CLAUDE_CONFIG_DIR", () => {
3420 const claudeConfigDir = "/tmp/t3code-claude-home";
3421 const recorded = recordingMockSpawnerLayer((args) => {
3422 const joined = args.join(" ");
3423 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3424 if (joined === "auth status")
3425 return {
3426 stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
3427 stderr: "",
3428 code: 0,
3429 };
3430 throw new Error(`Unexpected args: ${joined}`);
3431 });
3432
3433 return Effect.gen(function* () {
3434 const status = yield* checkClaudeProviderStatus(
3435 {
3436 ...defaultClaudeSettings,
3437 homePath: claudeConfigDir,
3438 },
3439 claudeCapabilities(),
3440 );
3441 assert.strictEqual(status.status, "ready");
3442 // The home is resolved through the host Path before it reaches the env.
3443 assert.deepStrictEqual(
3444 recorded.commands.map((command) => command.env?.CLAUDE_CONFIG_DIR),
3445 [(yield* Path.Path).resolve(claudeConfigDir)],
3446 );
3447 }).pipe(Effect.provide(recorded.layer));
3448 });
3449
3450 it.effect("includes probed claude slash commands in the provider snapshot", () =>
3451 Effect.gen(function* () {
3452 const status = yield* checkClaudeProviderStatus(
3453 defaultClaudeSettings,
3454 claudeCapabilities({
3455 subscriptionType: "maxplan",
3456 slashCommands: [
3457 {
3458 name: "review",
3459 description: "Review a pull request",
3460 input: { hint: "pr-or-branch" },
3461 },
3462 ],
3463 }),
3464 );
3465
3466 assert.deepStrictEqual(status.slashCommands.slice(1), [
3467 {
3468 name: "review",
3469 description: "Review a pull request",
3470 input: { hint: "pr-or-branch" },
3471 },
3472 ]);
3473 }).pipe(
3474 Effect.provide(
3475 layerMockSpawner((args) => {
3476 const joined = args.join(" ");
3477 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3478 if (joined === "auth status")
3479 return {
3480 stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
3481 stderr: "",
3482 code: 0,
3483 };
3484 throw new Error(`Unexpected args: ${joined}`);
3485 }),
3486 ),
3487 ),
3488 );
3489
3490 it.effect("deduplicates probed claude slash commands by name", () =>
3491 Effect.gen(function* () {
3492 const status = yield* checkClaudeProviderStatus(
3493 defaultClaudeSettings,
3494 claudeCapabilities({
3495 subscriptionType: "maxplan",
3496 slashCommands: [
3497 {
3498 name: "ui",
3499 description: "Explore and refine UI",
3500 },
3501 {
3502 name: "ui",
3503 input: { hint: "component-or-screen" },
3504 },
3505 ],
3506 }),
3507 );
3508
3509 assert.deepStrictEqual(status.slashCommands, [
3510 COMPACT_SLASH_COMMAND,
3511 {
3512 name: "ui",
3513 description: "Explore and refine UI",
3514 input: { hint: "component-or-screen" },
3515 },
3516 ]);
3517 }).pipe(
3518 Effect.provide(
3519 layerMockSpawner((args) => {
3520 const joined = args.join(" ");
3521 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3522 if (joined === "auth status")
3523 return {
3524 stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
3525 stderr: "",
3526 code: 0,
3527 };
3528 throw new Error(`Unexpected args: ${joined}`);
3529 }),
3530 ),
3531 ),
3532 );
3533
3534 it.effect("returns an api key label for claude api key auth", () =>
3535 Effect.gen(function* () {
3536 const status = yield* checkClaudeProviderStatus(
3537 defaultClaudeSettings,
3538 claudeCapabilities({ tokenSource: "ANTHROPIC_AUTH_TOKEN" }),
3539 );
3540 assert.strictEqual(status.status, "ready");
3541 assert.strictEqual(status.auth.status, "authenticated");
3542 assert.strictEqual(status.auth.type, "apiKey");
3543 assert.strictEqual(status.auth.label, "Claude API Key");
3544 }).pipe(
3545 Effect.provide(
3546 layerMockSpawner((args) => {
3547 const joined = args.join(" ");
3548 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3549 if (joined === "auth status")
3550 return {
3551 stdout: '{"loggedIn":true,"authMethod":"api-key"}\n',
3552 stderr: "",
3553 code: 0,
3554 };
3555 throw new Error(`Unexpected args: ${joined}`);
3556 }),
3557 ),
3558 ),
3559 );
3560
3561 it.effect("returns unavailable when claude is missing", () =>
3562 Effect.gen(function* () {
3563 const status = yield* checkClaudeProviderStatus(
3564 defaultClaudeSettings,
3565 claudeCapabilities(),
3566 );
3567 assert.strictEqual(status.status, "error");
3568 assert.strictEqual(status.installed, false);
3569 assert.strictEqual(status.auth.status, "unknown");
3570 assert.strictEqual(status.message, "Claude Agent CLI (`claude`) was not found on PATH.");
3571 }).pipe(Effect.provide(layerFailingSpawner("spawn claude ENOENT"))),
3572 );
3573
3574 it.effect("returns error when version check fails with non-zero exit code", () => {
3575 const secretStderr = "Something went wrong: secret-token-value";
3576 return Effect.gen(function* () {
3577 const status = yield* checkClaudeProviderStatus(
3578 defaultClaudeSettings,
3579 claudeCapabilities(),
3580 );
3581 assert.strictEqual(status.status, "error");
3582 assert.strictEqual(status.installed, true);
3583 assert.strictEqual(status.message, "Claude Agent CLI is installed but failed to run.");
3584 assert.ok(!(status.message ?? "").includes(secretStderr));
3585 }).pipe(
3586 Effect.provide(
3587 layerMockSpawner((args) => {
3588 const joined = args.join(" ");
3589 if (joined === "--version")
3590 return {
3591 stdout: "",
3592 stderr: secretStderr,
3593 code: 1,
3594 };
3595 throw new Error(`Unexpected args: ${joined}`);
3596 }),
3597 ),
3598 );
3599 });
3600
3601 it.effect("returns warning when the Claude initialization result is unavailable", () =>
3602 Effect.gen(function* () {
3603 const status = yield* checkClaudeProviderStatus(
3604 defaultClaudeSettings,
3605 noClaudeCapabilities,
3606 );
3607 assert.strictEqual(status.status, "warning");
3608 assert.strictEqual(status.installed, true);
3609 assert.strictEqual(status.auth.status, "unknown");
3610 assert.strictEqual(
3611 status.message,
3612 "Could not verify Claude authentication status from initialization result.",
3613 );
3614 }).pipe(
3615 Effect.provide(
3616 layerMockSpawner((args) => {
3617 const joined = args.join(" ");
3618 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3619 if (joined === "auth status")
3620 return {
3621 stdout: '{"loggedIn":false}\n',
3622 stderr: "",
3623 code: 1,
3624 };
3625 throw new Error(`Unexpected args: ${joined}`);
3626 }),
3627 ),
3628 ),
3629 );
3630 });
3631});