1import * as ScriptProgress from "@t3tools/provider-core/server/ScriptProgress";
2import * as ManagedProviderHome from "./ManagedProviderHome.ts";
3import * as SharedAccountClient from "./SharedAccountClient.ts";
4import * as SharedAccounts from "./SharedAccounts.ts";
5import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
6import * as CodexInstallation from "./CodexInstallation.ts";
7import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
8/**
9 * Multi-instance validation slices for the live `ProviderInstanceRegistry`.
10 *
11 * Two axes of the driver/registry refactor are exercised here:
12 *
13 * 1. **Same driver, many instances** — the "multi-instance codex slice"
14 * describe block below configures two independent `codex` instances and
15 * asserts each gets its own closures and identity. This is the
16 * multi-codex capability the refactor exists to unlock.
17 *
18 * 2. **Many drivers, one registry** — the "all drivers slice" describe
19 * block below configures one instance of every shipped driver
20 * (`codex`, `claudeAgent`, `cursor`, `grok`, `opencode`) in a single
21 * `ProviderInstanceConfigMap` and asserts the registry boots them all
22 * without cross-contamination. This proves the driver SPI is uniform
23 * across every provider — any driver plugs into the registry through
24 * the same `ProviderDriver` value contract.
25 *
26 * Every instance in these tests is configured with `enabled: false` so the
27 * provider-status checks short-circuit to pending/disabled snapshots
28 * without trying to spawn real `codex` / `claude` / `agent` / `grok` / `opencode`
29 * binaries. That keeps the assertions focused on registry routing
30 * behaviour rather than the runtime details of each provider.
31 */
32import { describe, expect, it } from "@effect/vitest";
33import * as NodeServices from "@effect/platform-node/NodeServices";
34import {
35 EnvironmentId,
36 type ClaudeSettings,
37 type CodexSettings,
38 ProviderDriverKind,
39 type ProviderInstanceConfigMap,
40 ProviderInstanceId,
41} from "@t3tools/contracts";
42import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderLatestVersions";
43import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
44import type { GrokSettings } from "@t3tools/provider-grok/settings";
45import type { CursorSettings } from "@t3tools/provider-cursor/settings";
46import * as HostProcess from "@t3tools/shared/HostProcess";
47import * as DateTime from "effect/DateTime";
48import * as Effect from "effect/Effect";
49import * as FileSystem from "effect/FileSystem";
50import * as Layer from "effect/Layer";
51import * as Path from "effect/Path";
52import * as Stream from "effect/Stream";
53import { HttpClient, HttpClientResponse } from "effect/http";
54
55import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";
56import * as AntigravityInstallation from "./AntigravityInstallation.ts";
57import * as ServerConfig from "../config.ts";
58import * as ServerSettings from "../serverSettings.ts";
59import { ClaudeDriver, type ClaudeDriverEnv } from "./Drivers/ClaudeDriver.ts";
60import { CodexDriver, type CodexDriverEnv } from "./Drivers/CodexDriver.ts";
61import { CursorDriver, type CursorDriverEnv } from "@t3tools/provider-cursor/server";
62import { GrokDriver, type GrokDriverEnv } from "@t3tools/provider-grok/server";
63import { OpenCodeDriver, type OpenCodeDriverEnv } from "@t3tools/provider-opencode/server";
64import type { OpenCodeSettings } from "@t3tools/provider-opencode/settings";
65import * as ModelManifest from "./ModelManifest.ts";
66import * as OpenCodeRuntime from "@t3tools/provider-opencode/server/OpenCodeRuntime";
67import * as OpenCodeServerLedger from "@t3tools/provider-opencode/server/OpenCodeServerLedger";
68import * as ResetCreditCoordinator from "./resetCreditCoordinator.ts";
69import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
70import { makeProviderInstanceRegistry } from "./ProviderInstanceRegistry.ts";
71import * as ProviderOrchestrationAdapterInfrastructure from "./ProviderOrchestrationAdapterInfrastructure.ts";
72import * as ProviderHostLive from "./ProviderHostLive.ts";
73
74const layerSharedAccounts = ManagedProviderHome.layer.pipe(
75 Layer.provideMerge(SharedAccountClient.layer),
76 Layer.provideMerge(SharedAccounts.layer),
77 Layer.provideMerge(ServerSecretStore.layer),
78);
79
80const layerTestHttpClient = Layer.succeed(
81 HttpClient.HttpClient,
82 HttpClient.make((request) =>
83 Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ version: "0.0.0" }))),
84 ),
85);
86
87// The mock CLIs from makeTildeProviderFixtures are scripts Windows cannot execute.
88const windowsHost = HostProcess.Platform.defaultValue() === "win32";
89
90const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z");
91
92const layerBackgroundPolicyAlwaysRun = Layer.mock(BackgroundPolicy.BackgroundPolicy)({
93 reportClientActivity: () => Effect.void,
94 removeRpcClient: () => Effect.void,
95 reportHostPowerState: () => Effect.void,
96 snapshot: Effect.succeed({
97 hostPower: {
98 source: "unknown",
99 idle: "unknown",
100 idleSeconds: null,
101 locked: "unknown",
102 suspended: false,
103 onBattery: "unknown",
104 lowPowerMode: "unknown",
105 thermalState: "unknown",
106 stale: true,
107 updatedAt: TEST_EPOCH,
108 },
109 leases: [],
110 activeForegroundLeaseCount: 0,
111 activeScopeKeys: [],
112 shouldRunOpportunisticWork: true,
113 updatedAt: TEST_EPOCH,
114 }),
115 streamChanges: Stream.empty,
116 hasDemand: () => Effect.succeed(true),
117 shouldRunScopeWork: () => Effect.succeed(true),
118 shouldRunOpportunisticWork: Effect.succeed(true),
119});
120
121const makeCodexConfig = (overrides: Partial<CodexSettings>): CodexSettings => ({
122 enabled: false,
123 binaryPath: "codex",
124 homePath: "",
125 shadowHomePath: "",
126 launchArgs: "",
127 customModels: [],
128 ...overrides,
129});
130
131const makeClaudeConfig = (overrides: Partial<ClaudeSettings>): ClaudeSettings => ({
132 enabled: false,
133 binaryPath: "claude",
134 homePath: "",
135 customModels: [],
136 launchArgs: "",
137 autoCompactWindow: "",
138 ...overrides,
139});
140
141const makeCursorConfig = (overrides: Partial<CursorSettings>): CursorSettings => ({
142 enabled: false,
143 customModels: [],
144 ...overrides,
145});
146
147const makeGrokConfig = (overrides: Partial<GrokSettings>): GrokSettings => ({
148 enabled: false,
149 binaryPath: "grok",
150 customModels: [],
151 ...overrides,
152});
153
154const makeOpenCodeConfig = (overrides: Partial<OpenCodeSettings>): OpenCodeSettings => ({
155 enabled: false,
156 binaryPath: "opencode",
157 serverUrl: "",
158 serverPassword: "",
159 customModels: [],
160 ...overrides,
161});
162
163const makeTildeProviderFixtures = Effect.fn(
164 "ProviderInstanceRegistry.test.makeTildeProviderFixtures",
165)(function* () {
166 const fileSystem = yield* FileSystem.FileSystem;
167 const path = yield* Path.Path;
168 const homePath = yield* HostProcess.HomeDirectory;
169 const fixtureDir = yield* fileSystem.makeTempDirectoryScoped({
170 directory: homePath,
171 prefix: ".t3-provider-path-test-",
172 });
173 const codexPath = path.join(fixtureDir, "codex");
174 const claudePath = path.join(fixtureDir, "claude");
175 const claudeHomePath = path.join(fixtureDir, "claude-home");
176 const codexScriptPath = path.join(fixtureDir, "codex-script.json");
177 const codexFixtureDir = path.join(import.meta.dirname, "testFixtures");
178
179 yield* fileSystem.copyFile(path.join(codexFixtureDir, "codexCollabMockPeer.sh"), codexPath);
180 yield* fileSystem.copyFile(
181 path.join(codexFixtureDir, "codexCollabMockPeer.mjs"),
182 path.join(fixtureDir, "codexCollabMockPeer.mjs"),
183 );
184 yield* fileSystem.copyFile(
185 path.join(codexFixtureDir, "codexMultiAgentWire.json"),
186 path.join(fixtureDir, "codexMultiAgentWire.json"),
187 );
188 yield* fileSystem.writeFileString(
189 codexScriptPath,
190 JSON.stringify({ rootThreadId: "probe-thread", notifications: [] }),
191 );
192 yield* fileSystem.chmod(codexPath, 0o755);
193
194 yield* fileSystem.copyFile(
195 yield* path.fromFileUrl(
196 new URL("./testing/ProviderInstanceRegistryLive.fixture.mjs", import.meta.url),
197 ),
198 claudePath,
199 );
200 yield* fileSystem.chmod(claudePath, 0o755);
201 yield* fileSystem.makeDirectory(claudeHomePath);
202
203 const asTildePath = (filePath: string) => `~/${path.relative(homePath, filePath)}`;
204 return {
205 codexBinaryPath: asTildePath(codexPath),
206 claudeBinaryPath: asTildePath(claudePath),
207 claudeHomePath,
208 codexScriptPath,
209 };
210});
211
212describe("ProviderInstanceRegistry — multi-instance codex slice", () => {
213 // `ServerConfig.layerTest` needs `FileSystem` to materialize its scratch
214 // directory. `Layer.merge` just unions requirements, so we have to push
215 // `NodeServices.layer` through `Layer.provideMerge` to satisfy that
216 // dependency while still surfacing NodeServices to the test body (the
217 // codex driver's `create` yields `ChildProcessSpawner` directly).
218 const layerBaseDeps = ServerConfig.layerTest(process.cwd(), {
219 prefix: "provider-instance-registry-test",
220 }).pipe(
221 Layer.provideMerge(NodeServices.layer),
222 Layer.provideMerge(
223 Layer.mock(CodexInstallation.CodexInstallation)({
224 managedDirectory: "unused-managed-installation",
225 }),
226 ),
227 Layer.provideMerge(
228 Layer.succeed(ServerEnvironment.ServerEnvironmentIdentity, {
229 getEnvironmentId: Effect.succeed(
230 EnvironmentId.make("00000000-0000-4000-8000-000000000001"),
231 ),
232 }),
233 ),
234 Layer.provideMerge(layerBackgroundPolicyAlwaysRun),
235 Layer.provideMerge(ServerSettings.layerTest()),
236 Layer.provideMerge(layerTestHttpClient),
237 Layer.provideMerge(ServerSettings.layerTest()),
238 Layer.provideMerge(
239 Layer.succeed(
240 ProviderEventLoggers.ProviderEventLoggers,
241 ProviderEventLoggers.NoOpProviderEventLoggers,
242 ),
243 ),
244 Layer.provideMerge(ProviderLatestVersions.layer),
245 Layer.provideMerge(Layer.merge(McpProviderSessions.layer, ScriptProgress.layerUnavailable)),
246 Layer.provideMerge(ModelManifest.layerTest),
247 Layer.provideMerge(ResetCreditCoordinator.layerTest),
248 );
249 const layerBase = ProviderHostLive.layer.pipe(
250 Layer.provideMerge(layerSharedAccounts),
251 Layer.provideMerge(layerBaseDeps),
252 );
253 const layerTest = ProviderOrchestrationAdapterInfrastructure.layer.pipe(
254 Layer.provideMerge(layerBase),
255 );
256
257 it.live("boots two independent codex instances from a ProviderInstanceConfigMap", () =>
258 Effect.gen(function* () {
259 const personalId = ProviderInstanceId.make("codex_personal");
260 const workId = ProviderInstanceId.make("codex_work");
261 const codexDriverKind = ProviderDriverKind.make("codex");
262
263 const configMap: ProviderInstanceConfigMap = {
264 [personalId]: {
265 driver: codexDriverKind,
266 displayName: "Codex (personal)",
267 enabled: false,
268 config: makeCodexConfig({
269 binaryPath: "/opt/codex-personal/bin/codex",
270 homePath: "/home/julius/.codex_personal",
271 customModels: ["personal-preview"],
272 }),
273 },
274 [workId]: {
275 driver: codexDriverKind,
276 displayName: "Codex (work)",
277 enabled: false,
278 config: makeCodexConfig({
279 binaryPath: "/opt/codex-work/bin/codex",
280 homePath: "/home/julius/.codex",
281 customModels: ["work-preview"],
282 }),
283 },
284 };
285
286 const { registry } = yield* makeProviderInstanceRegistry<CodexDriverEnv>({
287 drivers: [CodexDriver],
288 configMap,
289 });
290
291 const instances = yield* registry.listInstances;
292 expect(instances.map((instance) => instance.instanceId).toSorted()).toEqual(
293 [personalId, workId].toSorted(),
294 );
295 expect(instances.every((instance) => instance.driverKind === codexDriverKind)).toBe(true);
296 expect(instances.map((instance) => instance.displayName).toSorted()).toEqual(
297 ["Codex (personal)", "Codex (work)"].toSorted(),
298 );
299
300 // Each instance must be retrievable by id and carry its *own* closures.
301 const personal = yield* registry.getInstance(personalId);
302 const work = yield* registry.getInstance(workId);
303 expect(personal).toBeDefined();
304 expect(work).toBeDefined();
305 expect(personal!.orchestrationAdapter).not.toBe(work!.orchestrationAdapter);
306 expect(personal!.textGeneration).not.toBe(work!.textGeneration);
307 expect(personal!.snapshot).not.toBe(work!.snapshot);
308
309 // Snapshots identify themselves by instanceId + driver — this is
310 // what makes per-instance routing distinguishable downstream.
311 const personalSnapshot = yield* personal!.snapshot.getSnapshot;
312 expect(personalSnapshot.instanceId).toBe(personalId);
313 expect(personalSnapshot.driver).toBe(codexDriverKind);
314 expect(personalSnapshot.enabled).toBe(false);
315 // The layout resolves the configured home through the host Path.
316 const path = yield* Path.Path;
317 expect(personalSnapshot.continuation?.groupKey).toBe(
318 `codex:home:${path.resolve("/home/julius/.codex_personal")}`,
319 );
320
321 const workSnapshot = yield* work!.snapshot.getSnapshot;
322 expect(workSnapshot.instanceId).toBe(workId);
323 expect(workSnapshot.driver).toBe(codexDriverKind);
324 expect(workSnapshot.enabled).toBe(false);
325 expect(workSnapshot.continuation?.groupKey).toBe(
326 `codex:home:${path.resolve("/home/julius/.codex")}`,
327 );
328
329 // Nothing goes to the unavailable bucket — both drivers are registered.
330 const unavailable = yield* registry.listUnavailable;
331 expect(unavailable).toEqual([]);
332 }).pipe(Effect.provide(layerTest)),
333 );
334
335 it.live("treats an explicit in-config enabled:false as disabling despite the envelope", () =>
336 Effect.gen(function* () {
337 // Old settings files can carry both flags with conflicting values.
338 // The explicit false must win so a user's disable is never undone.
339 const staleId = ProviderInstanceId.make("codex_stale");
340 const configMap: ProviderInstanceConfigMap = {
341 [staleId]: {
342 driver: ProviderDriverKind.make("codex"),
343 enabled: true,
344 config: makeCodexConfig({ enabled: false }),
345 },
346 };
347
348 const { registry } = yield* makeProviderInstanceRegistry({
349 drivers: [CodexDriver],
350 configMap,
351 });
352
353 const instance = yield* registry.getInstance(staleId);
354 expect(instance).toBeDefined();
355 expect(instance!.enabled).toBe(false);
356 const snapshot = yield* instance!.snapshot.getSnapshot;
357 expect(snapshot.enabled).toBe(false);
358 }).pipe(Effect.provide(layerTest)),
359 );
360
361 it.live("reports Codex's answer when a redemption changed nothing", () =>
362 Effect.gen(function* () {
363 if (yield* HostProcess.isWindows) return;
364 const fileSystem = yield* FileSystem.FileSystem;
365 const fixtures = yield* makeTildeProviderFixtures();
366 yield* fileSystem.writeFileString(
367 fixtures.codexScriptPath,
368 JSON.stringify({
369 rootThreadId: "probe-thread",
370 notifications: [],
371 account: { type: "chatgpt", email: "test@example.com", planType: "plus" },
372 failRateLimitsRead: true,
373 resetCreditOutcome: "alreadyRedeemed",
374 }),
375 );
376 const codexId = ProviderInstanceId.make("codex_reset");
377 const { registry } = yield* makeProviderInstanceRegistry({
378 drivers: [CodexDriver],
379 configMap: {
380 [codexId]: {
381 driver: ProviderDriverKind.make("codex"),
382 enabled: true,
383 environment: [
384 { name: "T3_CODEX_COLLAB_SCRIPT", value: fixtures.codexScriptPath, sensitive: false },
385 ],
386 config: makeCodexConfig({ enabled: true, binaryPath: fixtures.codexBinaryPath }),
387 },
388 },
389 });
390 const codex = yield* registry.getInstance(codexId);
391 expect(codex).toBeDefined();
392 // The usage read fails, so the re-probe cannot confirm new limits.
393 yield* codex!.snapshot.refresh;
394 expect(yield* codex!.consumeResetCredit!()).toBe("alreadyRedeemed");
395 }).pipe(Effect.provide(layerTest)),
396 );
397
398 it.live("runs Codex and Claude readiness probes from configured tilde paths", () =>
399 Effect.gen(function* () {
400 if (yield* HostProcess.isWindows) return;
401
402 const fixtures = yield* makeTildeProviderFixtures();
403
404 const codexId = ProviderInstanceId.make("codex_tilde");
405 const claudeId = ProviderInstanceId.make("claude_tilde");
406 const configMap: ProviderInstanceConfigMap = {
407 [codexId]: {
408 driver: ProviderDriverKind.make("codex"),
409 enabled: true,
410 environment: [
411 {
412 name: "T3_CODEX_COLLAB_SCRIPT",
413 value: fixtures.codexScriptPath,
414 sensitive: false,
415 },
416 ],
417 config: makeCodexConfig({ enabled: true, binaryPath: fixtures.codexBinaryPath }),
418 },
419 [claudeId]: {
420 driver: ProviderDriverKind.make("claudeAgent"),
421 enabled: true,
422 config: makeClaudeConfig({
423 enabled: true,
424 binaryPath: fixtures.claudeBinaryPath,
425 homePath: fixtures.claudeHomePath,
426 }),
427 },
428 };
429
430 const { registry } = yield* makeProviderInstanceRegistry<CodexDriverEnv | ClaudeDriverEnv>({
431 drivers: [CodexDriver, ClaudeDriver],
432 configMap,
433 });
434 const codex = yield* registry.getInstance(codexId);
435 const claude = yield* registry.getInstance(claudeId);
436 expect(codex).toBeDefined();
437 expect(claude).toBeDefined();
438
439 const [codexSnapshot, claudeSnapshot] = yield* Effect.all(
440 [codex!.snapshot.refresh, claude!.snapshot.refresh],
441 { concurrency: "unbounded" },
442 );
443 expect(codexSnapshot).toMatchObject({ status: "ready", installed: true, version: "0.0.0" });
444 expect(claudeSnapshot).toMatchObject({
445 status: "ready",
446 installed: true,
447 version: "2.1.219",
448 });
449 }).pipe(Effect.provide(layerTest)),
450 );
451
452 it.live("runs Claude instance probes in the server state dir, not the server cwd", () =>
453 Effect.gen(function* () {
454 if (yield* HostProcess.isWindows) return;
455
456 const fixtures = yield* makeTildeProviderFixtures();
457 const { stateDir } = yield* ServerConfig.ServerConfig;
458 const claudeId = ProviderInstanceId.make("claude_state_dir");
459 const { registry } = yield* makeProviderInstanceRegistry<ClaudeDriverEnv>({
460 drivers: [ClaudeDriver],
461 configMap: {
462 [claudeId]: {
463 driver: ProviderDriverKind.make("claudeAgent"),
464 enabled: true,
465 // Packaged desktop builds start the server in $HOME, where some
466 // `claude` wrappers refuse to run.
467 environment: [{ name: "T3_CLAUDE_REQUIRED_CWD", value: stateDir, sensitive: false }],
468 config: makeClaudeConfig({
469 enabled: true,
470 binaryPath: fixtures.claudeBinaryPath,
471 homePath: fixtures.claudeHomePath,
472 }),
473 },
474 },
475 });
476 const claude = yield* registry.getInstance(claudeId);
477
478 expect(yield* claude!.snapshot.refresh).toMatchObject({
479 status: "ready",
480 version: "2.1.219",
481 auth: { status: "authenticated" },
482 });
483 }).pipe(Effect.provide(layerTest)),
484 );
485
486 it.live(
487 "launches shared Claude accounts with their managed token and a common continuation home",
488 () =>
489 Effect.gen(function* () {
490 if (yield* HostProcess.isWindows) return;
491 const fixtures = yield* makeTildeProviderFixtures();
492 const accounts = yield* SharedAccounts.SharedAccounts;
493 const a = yield* accounts.execute({
494 type: "import",
495 label: "First",
496 credentials: { driver: "claudeAgent", accessToken: "first-managed-token" },
497 });
498 const b = yield* accounts.execute({
499 type: "import",
500 label: "Second",
501 credentials: { driver: "claudeAgent", accessToken: "second-managed-token" },
502 });
503 if (a.type !== "imported" || b.type !== "imported")
504 return yield* Effect.die("Expected two shared accounts");
505 const firstId = ProviderInstanceId.make("first_shared_claude");
506 const secondId = ProviderInstanceId.make("second_shared_claude");
507 const { registry } = yield* makeProviderInstanceRegistry<ClaudeDriverEnv>({
508 drivers: [ClaudeDriver],
509 configMap: {
510 [firstId]: {
511 driver: ProviderDriverKind.make("claudeAgent"),
512 enabled: true,
513 config: makeClaudeConfig({
514 enabled: true,
515 sharedAccountId: a.account.id,
516 binaryPath: fixtures.claudeBinaryPath,
517 }),
518 environment: [
519 {
520 name: "T3_CLAUDE_REQUIRED_TOKEN",
521 value: "first-managed-token",
522 sensitive: false,
523 },
524 { name: "CLAUDE_CODE_OAUTH_TOKEN", value: "wrong-token", sensitive: true },
525 { name: "ANTHROPIC_API_KEY", value: "wrong-key", sensitive: true },
526 ],
527 },
528 [secondId]: {
529 driver: ProviderDriverKind.make("claudeAgent"),
530 enabled: true,
531 config: makeClaudeConfig({
532 enabled: true,
533 sharedAccountId: b.account.id,
534 binaryPath: fixtures.claudeBinaryPath,
535 }),
536 environment: [
537 {
538 name: "T3_CLAUDE_REQUIRED_TOKEN",
539 value: "second-managed-token",
540 sensitive: false,
541 },
542 { name: "ANTHROPIC_AUTH_TOKEN", value: "wrong-key", sensitive: true },
543 ],
544 },
545 },
546 });
547 const first = yield* registry.getInstance(firstId);
548 const second = yield* registry.getInstance(secondId);
549 expect(first).toBeDefined();
550 expect(second).toBeDefined();
551 expect(yield* first!.snapshot.refresh).toMatchObject({
552 status: "ready",
553 auth: { status: "authenticated" },
554 });
555 expect(yield* second!.snapshot.refresh).toMatchObject({
556 status: "ready",
557 auth: { status: "authenticated" },
558 });
559 expect(first!.continuationIdentity).toEqual(second!.continuationIdentity);
560 }).pipe(Effect.provide(layerTest)),
561 );
562
563 const redeemClaudeReset = (claim: { result: string; usageFailsAfterClaim: boolean }) =>
564 Effect.gen(function* () {
565 const fs = yield* FileSystem.FileSystem;
566 const path = yield* Path.Path;
567 const fixtures = yield* makeTildeProviderFixtures();
568 const marker = path.join(fixtures.claudeHomePath, "redeemed");
569 yield* fs.writeFileString(
570 path.join(fixtures.claudeHomePath, ".credentials.json"),
571 '{"claudeAiOauth":{"accessToken":"fake-token"}}',
572 );
573 yield* fs.writeFileString(
574 path.join(fixtures.claudeHomePath, ".claude.json"),
575 '{"oauthAccount":{"organizationUuid":"fake-org"}}',
576 );
577 const client = HttpClient.make((request) =>
578 Effect.gen(function* () {
579 if (request.url.endsWith("/api/oauth/usage")) {
580 return HttpClientResponse.fromWeb(
581 request,
582 Response.json({
583 cedar_ember: {
584 eligible: true,
585 next_grant_id: "grant_a",
586 grants: [{ id: "grant_a", resets_left: 1, usable_now: true }],
587 },
588 }),
589 );
590 }
591 if (request.url.endsWith("/reset_rate_limits")) {
592 yield* fs.writeFileString(marker, "redeemed").pipe(Effect.orDie);
593 return HttpClientResponse.fromWeb(request, Response.json({ result: claim.result }));
594 }
595 return HttpClientResponse.fromWeb(request, Response.json({ version: "0.0.0" }));
596 }),
597 );
598 const instanceId = ProviderInstanceId.make("claude_reset");
599 const { registry } = yield* makeProviderInstanceRegistry({
600 drivers: [ClaudeDriver],
601 configMap: {
602 [instanceId]: {
603 driver: ProviderDriverKind.make("claudeAgent"),
604 enabled: true,
605 environment: [
606 { name: "T3_CLAUDE_RESET_MARKER", value: marker, sensitive: false },
607 ...(claim.usageFailsAfterClaim
608 ? [{ name: "T3_CLAUDE_USAGE_FAILS_AFTER_CLAIM", value: "1", sensitive: false }]
609 : []),
610 ],
611 config: makeClaudeConfig({
612 enabled: true,
613 binaryPath: fixtures.claudeBinaryPath,
614 homePath: fixtures.claudeHomePath,
615 }),
616 },
617 },
618 }).pipe(Effect.provideService(HttpClient.HttpClient, client));
619 const instance = yield* registry.getInstance(instanceId);
620 expect(instance).toBeDefined();
621 const before = yield* instance!.snapshot.refresh;
622 expect(before.usageLimits?.windows[0]?.usedPercent).toBe(100);
623 expect(before.usageLimits?.resetCredits?.nextCreditId).toBe("grant_a");
624 const outcome = yield* instance!.consumeResetCredit!().pipe(Effect.result);
625 return { outcome, after: yield* instance!.snapshot.getSnapshot };
626 }).pipe(
627 // macOS logins live in the Keychain, where resets are never read.
628 Effect.provideService(HostProcess.Platform, "linux"),
629 Effect.provide(layerTest),
630 );
631
632 it.live.skipIf(windowsHost)("refreshes Claude usage after redeeming a reset", () =>
633 Effect.gen(function* () {
634 const { outcome, after } = yield* redeemClaudeReset({
635 result: "reset",
636 usageFailsAfterClaim: false,
637 });
638 expect(outcome).toMatchObject({ _tag: "Success", success: "reset" });
639 expect(after.usageLimits?.windows[0]?.usedPercent).toBe(0);
640 }),
641 );
642
643 it.live.skipIf(windowsHost)(
644 "reports Claude's answer when a claim changed nothing and the re-probe fails",
645 () =>
646 Effect.gen(function* () {
647 const { outcome } = yield* redeemClaudeReset({
648 result: "already_used",
649 usageFailsAfterClaim: true,
650 });
651 expect(outcome).toMatchObject({ _tag: "Success", success: "alreadyRedeemed" });
652 }),
653 );
654
655 it.live(
656 "shadows instances whose driver is not registered in this build without failing boot",
657 () =>
658 Effect.gen(function* () {
659 const codexId = ProviderInstanceId.make("codex_main");
660 const ghostId = ProviderInstanceId.make("ghost_main");
661
662 const configMap: ProviderInstanceConfigMap = {
663 [codexId]: {
664 driver: ProviderDriverKind.make("codex"),
665 enabled: false,
666 config: makeCodexConfig({}),
667 },
668 [ghostId]: {
669 driver: ProviderDriverKind.make("ghostDriver"),
670 displayName: "A fork-only driver we don't ship",
671 enabled: false,
672 config: { arbitrary: "payload", preserved: true },
673 },
674 };
675
676 const { registry } = yield* makeProviderInstanceRegistry<CodexDriverEnv>({
677 drivers: [CodexDriver],
678 configMap,
679 });
680
681 const instances = yield* registry.listInstances;
682 expect(instances).toHaveLength(1);
683 expect(instances[0]!.instanceId).toBe(codexId);
684
685 const unavailable = yield* registry.listUnavailable;
686 expect(unavailable).toHaveLength(1);
687 const ghost = unavailable[0]!;
688 expect(ghost.instanceId).toBe(ghostId);
689 expect(ghost.driver).toBe("ghostDriver");
690 expect(ghost.availability).toBe("unavailable");
691 expect(ghost.unavailableReason).toMatch(/ghostDriver/);
692 }).pipe(Effect.provide(layerTest)),
693 );
694});
695
696describe("ProviderInstanceRegistry — all drivers slice", () => {
697 // All drivers need `NodeServices` (ChildProcessSpawner + FileSystem +
698 // Path). `OpenCodeDriver.create` additionally yields `OpenCodeRuntime`
699 // at construction time, so we wire `OpenCodeRuntimeLive` into the stack.
700 // `OpenCodeRuntimeLive` bundles its own `NetService.layer` via
701 // `Layer.provide`, so the only external requirement it still exposes is
702 // `ChildProcessSpawner` — resolved here by piping it through
703 // `provideMerge(NodeServices.layer)`.
704 //
705 // The nested `provideMerge`s read bottom-up: `NodeServices.layer`
706 // provides `OpenCodeRuntimeLive`'s deps while keeping its own outputs
707 // surfaced; that merged layer then provides `ServerConfig.layerTest`'s
708 // `FileSystem` dep while keeping everything else surfaced to the test.
709 const layerInfra = OpenCodeRuntime.layer.pipe(
710 Layer.provide(OpenCodeServerLedger.layerTest),
711 Layer.provideMerge(NodeServices.layer),
712 Layer.provideMerge(
713 Layer.mock(CodexInstallation.CodexInstallation)({
714 managedDirectory: "unused-managed-installation",
715 }),
716 ),
717 Layer.provideMerge(
718 Layer.succeed(ServerEnvironment.ServerEnvironmentIdentity, {
719 getEnvironmentId: Effect.succeed(
720 EnvironmentId.make("00000000-0000-4000-8000-000000000001"),
721 ),
722 }),
723 ),
724 );
725 const layerBaseDeps = AntigravityInstallation.AntigravityInstallation.layer.pipe(
726 Layer.provideMerge(ServerSecretStore.layer),
727 Layer.provideMerge(
728 ServerConfig.layerTest(process.cwd(), {
729 prefix: "provider-instance-registry-all-drivers-test",
730 }),
731 ),
732 Layer.provideMerge(layerInfra),
733 Layer.provideMerge(layerBackgroundPolicyAlwaysRun),
734 Layer.provideMerge(ServerSettings.layerTest()),
735 Layer.provideMerge(layerTestHttpClient),
736 Layer.provideMerge(ServerSettings.layerTest()),
737 Layer.provideMerge(
738 Layer.succeed(
739 ProviderEventLoggers.ProviderEventLoggers,
740 ProviderEventLoggers.NoOpProviderEventLoggers,
741 ),
742 ),
743 Layer.provideMerge(ProviderLatestVersions.layer),
744 Layer.provideMerge(Layer.merge(McpProviderSessions.layer, ScriptProgress.layerUnavailable)),
745 Layer.provideMerge(ModelManifest.layerTest),
746 Layer.provideMerge(ResetCreditCoordinator.layerTest),
747 );
748 const layerBase = ProviderHostLive.layer.pipe(
749 Layer.provideMerge(layerSharedAccounts),
750 Layer.provideMerge(layerBaseDeps),
751 );
752 const layerTest = ProviderOrchestrationAdapterInfrastructure.layer.pipe(
753 Layer.provideMerge(layerBase),
754 );
755
756 it.live("boots one instance of every shipped driver from a single config map", () =>
757 Effect.gen(function* () {
758 const codexId = ProviderInstanceId.make("codex_default");
759 const claudeId = ProviderInstanceId.make("claude_default");
760 const cursorId = ProviderInstanceId.make("cursor_default");
761 const grokId = ProviderInstanceId.make("grok_default");
762 const openCodeId = ProviderInstanceId.make("opencode_default");
763
764 const codexDriverKind = ProviderDriverKind.make("codex");
765 const claudeDriverKind = ProviderDriverKind.make("claudeAgent");
766 const cursorDriverKind = ProviderDriverKind.make("cursor");
767 const grokDriverKind = ProviderDriverKind.make("grok");
768 const openCodeDriverKind = ProviderDriverKind.make("opencode");
769
770 const configMap: ProviderInstanceConfigMap = {
771 [codexId]: {
772 driver: codexDriverKind,
773 displayName: "Codex",
774 enabled: false,
775 config: makeCodexConfig({ homePath: "/home/julius/.codex" }),
776 },
777 [claudeId]: {
778 driver: claudeDriverKind,
779 displayName: "Claude",
780 enabled: false,
781 config: makeClaudeConfig({
782 homePath: "/home/julius/.claude-work",
783 launchArgs: "--verbose",
784 }),
785 },
786 [cursorId]: {
787 driver: cursorDriverKind,
788 displayName: "Cursor",
789 enabled: false,
790 config: makeCursorConfig({}),
791 },
792 [grokId]: {
793 driver: grokDriverKind,
794 displayName: "Grok",
795 enabled: false,
796 config: makeGrokConfig({}),
797 },
798 [openCodeId]: {
799 driver: openCodeDriverKind,
800 displayName: "OpenCode",
801 enabled: false,
802 config: makeOpenCodeConfig({}),
803 },
804 };
805
806 const { registry } = yield* makeProviderInstanceRegistry<
807 CodexDriverEnv | ClaudeDriverEnv | CursorDriverEnv | GrokDriverEnv | OpenCodeDriverEnv
808 >({
809 drivers: [CodexDriver, ClaudeDriver, CursorDriver, GrokDriver, OpenCodeDriver],
810 configMap,
811 });
812
813 // Every configured instance must materialize — none downgraded to a
814 // shadow snapshot, because every driver in the map is registered.
815 const unavailable = yield* registry.listUnavailable;
816 expect(unavailable).toEqual([]);
817
818 const instances = yield* registry.listInstances;
819 expect(instances).toHaveLength(5);
820 expect(instances.map((instance) => instance.instanceId).toSorted()).toEqual(
821 [codexId, claudeId, cursorId, grokId, openCodeId].toSorted(),
822 );
823
824 // Instance lookup by id resolves each instance to its own bundle —
825 // this is how rest-of-server routes turn/session calls in the new
826 // model. Each driver's bundle carries its advertised `driverKind`.
827 const codex = yield* registry.getInstance(codexId);
828 const claude = yield* registry.getInstance(claudeId);
829 const cursor = yield* registry.getInstance(cursorId);
830 const grok = yield* registry.getInstance(grokId);
831 const openCode = yield* registry.getInstance(openCodeId);
832 expect(codex?.driverKind).toBe(codexDriverKind);
833 expect(claude?.driverKind).toBe(claudeDriverKind);
834 expect(cursor?.driverKind).toBe(cursorDriverKind);
835 expect(grok?.driverKind).toBe(grokDriverKind);
836 expect(openCode?.driverKind).toBe(openCodeDriverKind);
837 expect(codex?.displayName).toBe("Codex");
838 expect(claude?.displayName).toBe("Claude");
839 expect(cursor?.displayName).toBe("Cursor");
840 expect(grok?.displayName).toBe("Grok");
841 expect(openCode?.displayName).toBe("OpenCode");
842
843 // Every instance owns its own set of closures — no sharing across
844 // drivers. `orchestrationAdapter` / `textGeneration` / `snapshot` are all
845 // distinct references even when two instances happen to share a
846 // trait (e.g. Cursor + others all use a stub-or-real
847 // `textGeneration`; they must still be different object values).
848 const adapters = [
849 codex!.orchestrationAdapter,
850 claude!.orchestrationAdapter,
851 cursor!.orchestrationAdapter,
852 grok!.orchestrationAdapter,
853 openCode!.orchestrationAdapter,
854 ];
855 expect(new Set(adapters).size).toBe(adapters.length);
856 const textGenerations = [
857 codex!.textGeneration,
858 claude!.textGeneration,
859 cursor!.textGeneration,
860 grok!.textGeneration,
861 openCode!.textGeneration,
862 ];
863 expect(new Set(textGenerations).size).toBe(textGenerations.length);
864 const snapshots = [
865 codex!.snapshot,
866 claude!.snapshot,
867 cursor!.snapshot,
868 grok!.snapshot,
869 openCode!.snapshot,
870 ];
871 expect(new Set(snapshots).size).toBe(snapshots.length);
872
873 // Snapshots identify themselves by `instanceId` + `driver` so
874 // downstream aggregation in `ProviderRegistry` can tell instances
875 // apart even when two share a driver. With `enabled: false`, the
876 // check short-circuits and we get a disabled/pending snapshot back
877 // — that's enough signal to validate the stamping wrapper without
878 // spawning real binaries.
879 const codexSnapshot = yield* codex!.snapshot.getSnapshot;
880 expect(codexSnapshot.instanceId).toBe(codexId);
881 expect(codexSnapshot.driver).toBe(codexDriverKind);
882 expect(codexSnapshot.enabled).toBe(false);
883 expect(codexSnapshot.continuation?.groupKey).toBe(
884 `codex:home:${(yield* Path.Path).resolve("/home/julius/.codex")}`,
885 );
886
887 const claudeSnapshot = yield* claude!.snapshot.getSnapshot;
888 expect(claudeSnapshot.instanceId).toBe(claudeId);
889 expect(claudeSnapshot.driver).toBe(claudeDriverKind);
890 expect(claudeSnapshot.enabled).toBe(false);
891 expect(claudeSnapshot.continuation?.groupKey).toBe(
892 `claude:home:${(yield* Path.Path).resolve("/home/julius/.claude-work")}`,
893 );
894
895 const cursorSnapshot = yield* cursor!.snapshot.getSnapshot;
896 expect(cursorSnapshot.instanceId).toBe(cursorId);
897 expect(cursorSnapshot.driver).toBe(cursorDriverKind);
898 expect(cursorSnapshot.enabled).toBe(false);
899 expect(cursorSnapshot.continuation?.groupKey).toBe(
900 `${cursorDriverKind}:instance:${cursorId}`,
901 );
902
903 const grokSnapshot = yield* grok!.snapshot.getSnapshot;
904 expect(grokSnapshot.instanceId).toBe(grokId);
905 expect(grokSnapshot.driver).toBe(grokDriverKind);
906 expect(grokSnapshot.enabled).toBe(false);
907 expect(grokSnapshot.continuation?.groupKey).toBe(`${grokDriverKind}:instance:${grokId}`);
908
909 const openCodeSnapshot = yield* openCode!.snapshot.getSnapshot;
910 expect(openCodeSnapshot.instanceId).toBe(openCodeId);
911 expect(openCodeSnapshot.driver).toBe(openCodeDriverKind);
912 expect(openCodeSnapshot.enabled).toBe(false);
913 expect(openCodeSnapshot.continuation?.groupKey).toBe(
914 `${openCodeDriverKind}:instance:${openCodeId}`,
915 );
916 }).pipe(Effect.provide(layerTest)),
917 );
918});