| author | |
| committer | |
| log | e9ced44ac6729116a1ee8332077736ca648ee419 |
| tree | dbecd418a43a8193c83df0905d13507e896cfdc2 |
| parent | 802cd127d17d77ba9d598675574b9d01480a94b0 |
Add provider-service routing coverage for explicit claudeCode sessions.
Co-authored-by: codex <codex@users.noreply.github.com>4 files changed, 68 insertions(+), 12 deletions(-)
apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts+28-4| ... | ... | @@ -4,6 +4,7 @@ import { assertFailure } from "@effect/vitest/utils"; |
| 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 = { |
| 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) => { |
| 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 |
apps/server/src/provider/Layers/ProviderAdapterRegistry.ts+5-1| ... | ... | @@ -15,6 +15,7 @@ import { |
| 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 { |
| 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) => { |
apps/server/src/provider/Layers/ProviderService.test.ts+32-6| ... | ... | @@ -12,6 +12,7 @@ import type { |
| 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 |
| 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() { |
| 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() { |
| 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() { |
| 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) => |
| 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() { |
| 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) => { |
| 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; |
apps/server/src/serverLayers.ts+3-1| ... | ... | @@ -18,6 +18,7 @@ import { OrchestrationProjectionPipelineLive } from "./orchestration/Layers/Proj |
| 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< |
| 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( |