Add provider-service routing coverage for explicit claudeCode sessions.
Unified · Split
| @@ -4,6 +4,7 @@ import { assertFailure } from "@effect/vitest/utils"; · 3 unmodified lines | ||
| 4 | 4 | |
| 5 | 5 | import { Effect, Layer, Stream } from "effect"; |
| 6 | 6 | |
| 7 | +import { ClaudeCodeAdapter, ClaudeCodeAdapterShape } from "../Services/ClaudeCodeAdapter.ts"; | |
| 7 | 8 | import { CodexAdapter, CodexAdapterShape } from "../Services/CodexAdapter.ts"; |
| 8 | 9 | import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts"; |
| 9 | 10 | import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts"; |
| @@ -25,9 +26,30 @@ const fakeCodexAdapter: CodexAdapterShape = { · 15 unmodified lines | ||
| 25 | 26 | streamEvents: Stream.empty, |
| 26 | 27 | }; |
| 27 | 28 | |
| 29 | +const fakeClaudeAdapter: ClaudeCodeAdapterShape = { | |
| 30 | + provider: "claudeCode", | |
| 31 | + startSession: vi.fn(), | |
| 32 | + sendTurn: vi.fn(), | |
| 33 | + interruptTurn: vi.fn(), | |
| 34 | + respondToRequest: vi.fn(), | |
| 35 | + stopSession: vi.fn(), | |
| 36 | + listSessions: vi.fn(), | |
| 37 | + hasSession: vi.fn(), | |
| 38 | + readThread: vi.fn(), | |
| 39 | + rollbackThread: vi.fn(), | |
| 40 | + stopAll: vi.fn(), | |
| 41 | + streamEvents: Stream.empty, | |
| 42 | +}; | |
| 43 | + | |
| 28 | 44 | const layer = it.layer( |
| 29 | 45 | Layer.mergeAll( |
| 30 | − Layer.provide(ProviderAdapterRegistryLive, Layer.succeed(CodexAdapter, fakeCodexAdapter)), | |
| 46 | + Layer.provide( | |
| 47 | + ProviderAdapterRegistryLive, | |
| 48 | + Layer.mergeAll( | |
| 49 | + Layer.succeed(CodexAdapter, fakeCodexAdapter), | |
| 50 | + Layer.succeed(ClaudeCodeAdapter, fakeClaudeAdapter), | |
| 51 | + ), | |
| 52 | + ), | |
| 31 | 53 | NodeServices.layer, |
| 32 | 54 | ), |
| 33 | 55 | ); |
| @@ -36,11 +58,13 @@ layer("ProviderAdapterRegistryLive", (it) => { · 2 unmodified lines | ||
| 36 | 58 | it.effect("resolves a registered provider adapter", () => |
| 37 | 59 | Effect.gen(function* () { |
| 38 | 60 | const registry = yield* ProviderAdapterRegistry; |
| 39 | − const adapter = yield* registry.getByProvider("codex"); | |
| 40 | − assert.equal(adapter, fakeCodexAdapter); | |
| 61 | + const codex = yield* registry.getByProvider("codex"); | |
| 62 | + const claude = yield* registry.getByProvider("claudeCode"); | |
| 63 | + assert.equal(codex, fakeCodexAdapter); | |
| 64 | + assert.equal(claude, fakeClaudeAdapter); | |
| 41 | 65 | |
| 42 | 66 | const providers = yield* registry.listProviders(); |
| 43 | − assert.deepEqual(providers, ["codex"]); | |
| 67 | + assert.deepEqual(providers, ["codex", "claudeCode"]); | |
| 44 | 68 | }), |
| 45 | 69 | ); |
| 46 | 70 | |
| @@ -15,6 +15,7 @@ import { · 14 unmodified lines | ||
| 15 | 15 | ProviderAdapterRegistry, |
| 16 | 16 | type ProviderAdapterRegistryShape, |
| 17 | 17 | } from "../Services/ProviderAdapterRegistry.ts"; |
| 18 | +import { ClaudeCodeAdapter } from "../Services/ClaudeCodeAdapter.ts"; | |
| 18 | 19 | import { CodexAdapter } from "../Services/CodexAdapter.ts"; |
| 19 | 20 | |
| 20 | 21 | export interface ProviderAdapterRegistryLiveOptions { |
| @@ -23,7 +24,10 @@ export interface ProviderAdapterRegistryLiveOptions { · 2 unmodified lines | ||
| 23 | 24 | |
| 24 | 25 | const makeProviderAdapterRegistry = (options?: ProviderAdapterRegistryLiveOptions) => |
| 25 | 26 | Effect.gen(function* () { |
| 26 | − const adapters = options?.adapters !== undefined ? options.adapters : [yield* CodexAdapter]; | |
| 27 | + const adapters = | |
| 28 | + options?.adapters !== undefined | |
| 29 | + ? options.adapters | |
| 30 | + : [yield* CodexAdapter, yield* ClaudeCodeAdapter]; | |
| 27 | 31 | const byProvider = new Map(adapters.map((adapter) => [adapter.provider, adapter])); |
| 28 | 32 | |
| 29 | 33 | const getByProvider: ProviderAdapterRegistryShape["getByProvider"] = (provider) => { |
| @@ -12,6 +12,7 @@ import type { · 11 unmodified lines | ||
| 12 | 12 | import { |
| 13 | 13 | ApprovalRequestId, |
| 14 | 14 | EventId, |
| 15 | + type ProviderKind, | |
| 15 | 16 | ProviderSessionStartInput, |
| 16 | 17 | ProviderSessionId, |
| 17 | 18 | ProviderThreadId, |
| @@ -52,7 +53,7 @@ const asRequestId = (value: string): ApprovalRequestId => ApprovalRequestId.make · 34 unmodified lines | ||
| 52 | 53 | const asEventId = (value: string): EventId => EventId.makeUnsafe(value); |
| 53 | 54 | const asThreadId = (value: string): ThreadId => ThreadId.makeUnsafe(value); |
| 54 | 55 | |
| 55 | −function makeFakeCodexAdapter() { | |
| 56 | +function makeFakeCodexAdapter(provider: ProviderKind = "codex") { | |
| 56 | 57 | const sessions = new Map<ProviderSessionId, ProviderSession>(); |
| 57 | 58 | const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>()); |
| 58 | 59 | let nextSession = 1; |
| @@ -64,7 +65,7 @@ function makeFakeCodexAdapter() { · 5 unmodified lines | ||
| 64 | 65 | const threadId = ProviderThreadId.makeUnsafe(`thread-${next}`); |
| 65 | 66 | const session: ProviderSession = { |
| 66 | 67 | sessionId: ProviderSessionId.makeUnsafe(`sess-${next}`), |
| 67 | − provider: "codex", | |
| 68 | + provider, | |
| 68 | 69 | status: "ready", |
| 69 | 70 | threadId, |
| 70 | 71 | resumeCursor: input.resumeCursor ?? { opaque: `cursor-${next}` }, |
| @@ -85,7 +86,7 @@ function makeFakeCodexAdapter() { · 14 unmodified lines | ||
| 85 | 86 | if (!sessions.has(input.sessionId)) { |
| 86 | 87 | return Effect.fail( |
| 87 | 88 | new ProviderAdapterSessionNotFoundError({ |
| 88 | − provider: "codex", | |
| 89 | + provider, | |
| 89 | 90 | sessionId: input.sessionId, |
| 90 | 91 | }), |
| 91 | 92 | ); |
| @@ -160,7 +161,7 @@ function makeFakeCodexAdapter() { · 68 unmodified lines | ||
| 160 | 161 | ); |
| 161 | 162 | |
| 162 | 163 | const adapter: ProviderAdapterShape<ProviderAdapterError> = { |
| 163 | − provider: "codex", | |
| 164 | + provider, | |
| 164 | 165 | startSession, |
| 165 | 166 | sendTurn, |
| 166 | 167 | interruptTurn, |
| @@ -199,12 +200,15 @@ const sleep = (ms: number) => · 32 unmodified lines | ||
| 199 | 200 | |
| 200 | 201 | function makeProviderServiceLayer() { |
| 201 | 202 | const codex = makeFakeCodexAdapter(); |
| 203 | + const claude = makeFakeCodexAdapter("claudeCode"); | |
| 202 | 204 | const registry: typeof ProviderAdapterRegistry.Service = { |
| 203 | 205 | getByProvider: (provider) => |
| 204 | 206 | provider === "codex" |
| 205 | 207 | ? Effect.succeed(codex.adapter) |
| 206 | − : Effect.fail(new ProviderUnsupportedError({ provider })), | |
| 207 | − listProviders: () => Effect.succeed(["codex"]), | |
| 208 | + : provider === "claudeCode" | |
| 209 | + ? Effect.succeed(claude.adapter) | |
| 210 | + : Effect.fail(new ProviderUnsupportedError({ provider })), | |
| 211 | + listProviders: () => Effect.succeed(["codex", "claudeCode"]), | |
| 208 | 212 | }; |
| 209 | 213 | |
| 210 | 214 | const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); |
| @@ -227,6 +231,7 @@ function makeProviderServiceLayer() { · 16 unmodified lines | ||
| 227 | 231 | |
| 228 | 232 | return { |
| 229 | 233 | codex, |
| 234 | + claude, | |
| 230 | 235 | layer, |
| 231 | 236 | }; |
| 232 | 237 | } |
| @@ -448,6 +453,27 @@ routing.layer("ProviderServiceLive routing", (it) => { · 215 unmodified lines | ||
| 448 | 453 | }), |
| 449 | 454 | ); |
| 450 | 455 | |
| 456 | + it.effect("routes explicit claudeCode provider session starts to the claude adapter", () => | |
| 457 | + Effect.gen(function* () { | |
| 458 | + const provider = yield* ProviderService; | |
| 459 | + | |
| 460 | + const session = yield* provider.startSession(asThreadId("thread-claude"), { | |
| 461 | + provider: "claudeCode", | |
| 462 | + cwd: "/tmp/project-claude", | |
| 463 | + }); | |
| 464 | + | |
| 465 | + assert.equal(session.provider, "claudeCode"); | |
| 466 | + assert.equal(routing.claude.startSession.mock.calls.length, 1); | |
| 467 | + const startInput = routing.claude.startSession.mock.calls[0]?.[0]; | |
| 468 | + assert.equal(typeof startInput === "object" && startInput !== null, true); | |
| 469 | + if (startInput && typeof startInput === "object") { | |
| 470 | + const startPayload = startInput as { provider?: string; cwd?: string }; | |
| 471 | + assert.equal(startPayload.provider, "claudeCode"); | |
| 472 | + assert.equal(startPayload.cwd, "/tmp/project-claude"); | |
| 473 | + } | |
| 474 | + }), | |
| 475 | + ); | |
| 476 | + | |
| 451 | 477 | it.effect("recovers stale persisted sessions for rollback by resuming thread identity", () => |
| 452 | 478 | Effect.gen(function* () { |
| 453 | 479 | const provider = yield* ProviderService; |
| @@ -18,6 +18,7 @@ import { OrchestrationProjectionPipelineLive } from "./orchestration/Layers/Proj · 17 unmodified lines | ||
| 18 | 18 | import { OrchestrationProjectionSnapshotQueryLive } from "./orchestration/Layers/ProjectionSnapshotQuery"; |
| 19 | 19 | import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRuntimeIngestion"; |
| 20 | 20 | import { ProviderUnsupportedError } from "./provider/Errors"; |
| 21 | +import { makeClaudeCodeAdapterLive } from "./provider/Layers/ClaudeCodeAdapter"; | |
| 21 | 22 | import { makeCodexAdapterLive } from "./provider/Layers/CodexAdapter"; |
| 22 | 23 | import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry"; |
| 23 | 24 | import { makeProviderServiceLive } from "./provider/Layers/ProviderService"; |
| @@ -56,8 +57,9 @@ export function makeServerProviderLayer(): Layer.Layer< · 32 unmodified lines | ||
| 56 | 57 | const codexAdapterLayer = makeCodexAdapterLive( |
| 57 | 58 | nativeEventLogger ? { nativeEventLogger } : undefined, |
| 58 | 59 | ); |
| 60 | + const claudeAdapterLayer = makeClaudeCodeAdapterLive(); | |
| 59 | 61 | const adapterRegistryLayer = ProviderAdapterRegistryLive.pipe( |
| 60 | − Layer.provide(codexAdapterLayer), | |
| 62 | + Layer.provide(Layer.mergeAll(codexAdapterLayer, claudeAdapterLayer)), | |
| 61 | 63 | Layer.provideMerge(providerSessionDirectoryLayer), |
| 62 | 64 | ); |
| 63 | 65 | return makeProviderServiceLive( |