| 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,6 +4,7 @@ import { assertFailure } from "@effect/vitest/utils"; |
| 4 | 4 | ||
| 5 | import { Effect, Layer, Stream } from "effect"; | 5 | import { Effect, Layer, Stream } from "effect"; |
| 6 | 6 | ||
| 7 | import { ClaudeCodeAdapter, ClaudeCodeAdapterShape } from "../Services/ClaudeCodeAdapter.ts"; | ||
| 7 | import { CodexAdapter, CodexAdapterShape } from "../Services/CodexAdapter.ts"; | 8 | import { CodexAdapter, CodexAdapterShape } from "../Services/CodexAdapter.ts"; |
| 8 | import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts"; | 9 | import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts"; |
| 9 | import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts"; | 10 | import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts"; |
| ... | @@ -25,9 +26,30 @@ const fakeCodexAdapter: CodexAdapterShape = { | ... | @@ -25,9 +26,30 @@ const fakeCodexAdapter: CodexAdapterShape = { |
| 25 | streamEvents: Stream.empty, | 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 | const layer = it.layer( | 44 | const layer = it.layer( |
| 29 | Layer.mergeAll( | 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 | NodeServices.layer, | 53 | NodeServices.layer, |
| 32 | ), | 54 | ), |
| 33 | ); | 55 | ); |
| ... | @@ -36,11 +58,13 @@ layer("ProviderAdapterRegistryLive", (it) => { | ... | @@ -36,11 +58,13 @@ layer("ProviderAdapterRegistryLive", (it) => { |
| 36 | it.effect("resolves a registered provider adapter", () => | 58 | it.effect("resolves a registered provider adapter", () => |
| 37 | Effect.gen(function* () { | 59 | Effect.gen(function* () { |
| 38 | const registry = yield* ProviderAdapterRegistry; | 60 | const registry = yield* ProviderAdapterRegistry; |
| 39 | const adapter = yield* registry.getByProvider("codex"); | 61 | const codex = yield* registry.getByProvider("codex"); |
| 40 | assert.equal(adapter, fakeCodexAdapter); | 62 | const claude = yield* registry.getByProvider("claudeCode"); |
| 63 | assert.equal(codex, fakeCodexAdapter); | ||
| 64 | assert.equal(claude, fakeClaudeAdapter); | ||
| 41 | 65 | ||
| 42 | const providers = yield* registry.listProviders(); | 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,6 +15,7 @@ import { |
| 15 | ProviderAdapterRegistry, | 15 | ProviderAdapterRegistry, |
| 16 | type ProviderAdapterRegistryShape, | 16 | type ProviderAdapterRegistryShape, |
| 17 | } from "../Services/ProviderAdapterRegistry.ts"; | 17 | } from "../Services/ProviderAdapterRegistry.ts"; |
| 18 | import { ClaudeCodeAdapter } from "../Services/ClaudeCodeAdapter.ts"; | ||
| 18 | import { CodexAdapter } from "../Services/CodexAdapter.ts"; | 19 | import { CodexAdapter } from "../Services/CodexAdapter.ts"; |
| 19 | 20 | ||
| 20 | export interface ProviderAdapterRegistryLiveOptions { | 21 | export interface ProviderAdapterRegistryLiveOptions { |
| ... | @@ -23,7 +24,10 @@ export interface ProviderAdapterRegistryLiveOptions { | ... | @@ -23,7 +24,10 @@ export interface ProviderAdapterRegistryLiveOptions { |
| 23 | 24 | ||
| 24 | const makeProviderAdapterRegistry = (options?: ProviderAdapterRegistryLiveOptions) => | 25 | const makeProviderAdapterRegistry = (options?: ProviderAdapterRegistryLiveOptions) => |
| 25 | Effect.gen(function* () { | 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 | const byProvider = new Map(adapters.map((adapter) => [adapter.provider, adapter])); | 31 | const byProvider = new Map(adapters.map((adapter) => [adapter.provider, adapter])); |
| 28 | 32 | ||
| 29 | const getByProvider: ProviderAdapterRegistryShape["getByProvider"] = (provider) => { | 33 | const getByProvider: ProviderAdapterRegistryShape["getByProvider"] = (provider) => { |
apps/server/src/provider/Layers/ProviderService.test.ts+32-6| ... | @@ -12,6 +12,7 @@ import type { | ... | @@ -12,6 +12,7 @@ import type { |
| 12 | import { | 12 | import { |
| 13 | ApprovalRequestId, | 13 | ApprovalRequestId, |
| 14 | EventId, | 14 | EventId, |
| 15 | type ProviderKind, | ||
| 15 | ProviderSessionStartInput, | 16 | ProviderSessionStartInput, |
| 16 | ProviderSessionId, | 17 | ProviderSessionId, |
| 17 | ProviderThreadId, | 18 | ProviderThreadId, |
| ... | @@ -52,7 +53,7 @@ const asRequestId = (value: string): ApprovalRequestId => ApprovalRequestId.make | ... | @@ -52,7 +53,7 @@ const asRequestId = (value: string): ApprovalRequestId => ApprovalRequestId.make |
| 52 | const asEventId = (value: string): EventId => EventId.makeUnsafe(value); | 53 | const asEventId = (value: string): EventId => EventId.makeUnsafe(value); |
| 53 | const asThreadId = (value: string): ThreadId => ThreadId.makeUnsafe(value); | 54 | const asThreadId = (value: string): ThreadId => ThreadId.makeUnsafe(value); |
| 54 | 55 | ||
| 55 | function makeFakeCodexAdapter() { | 56 | function makeFakeCodexAdapter(provider: ProviderKind = "codex") { |
| 56 | const sessions = new Map<ProviderSessionId, ProviderSession>(); | 57 | const sessions = new Map<ProviderSessionId, ProviderSession>(); |
| 57 | const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>()); | 58 | const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>()); |
| 58 | let nextSession = 1; | 59 | let nextSession = 1; |
| ... | @@ -64,7 +65,7 @@ function makeFakeCodexAdapter() { | ... | @@ -64,7 +65,7 @@ function makeFakeCodexAdapter() { |
| 64 | const threadId = ProviderThreadId.makeUnsafe(`thread-${next}`); | 65 | const threadId = ProviderThreadId.makeUnsafe(`thread-${next}`); |
| 65 | const session: ProviderSession = { | 66 | const session: ProviderSession = { |
| 66 | sessionId: ProviderSessionId.makeUnsafe(`sess-${next}`), | 67 | sessionId: ProviderSessionId.makeUnsafe(`sess-${next}`), |
| 67 | provider: "codex", | 68 | provider, |
| 68 | status: "ready", | 69 | status: "ready", |
| 69 | threadId, | 70 | threadId, |
| 70 | resumeCursor: input.resumeCursor ?? { opaque: `cursor-${next}` }, | 71 | resumeCursor: input.resumeCursor ?? { opaque: `cursor-${next}` }, |
| ... | @@ -85,7 +86,7 @@ function makeFakeCodexAdapter() { | ... | @@ -85,7 +86,7 @@ function makeFakeCodexAdapter() { |
| 85 | if (!sessions.has(input.sessionId)) { | 86 | if (!sessions.has(input.sessionId)) { |
| 86 | return Effect.fail( | 87 | return Effect.fail( |
| 87 | new ProviderAdapterSessionNotFoundError({ | 88 | new ProviderAdapterSessionNotFoundError({ |
| 88 | provider: "codex", | 89 | provider, |
| 89 | sessionId: input.sessionId, | 90 | sessionId: input.sessionId, |
| 90 | }), | 91 | }), |
| 91 | ); | 92 | ); |
| ... | @@ -160,7 +161,7 @@ function makeFakeCodexAdapter() { | ... | @@ -160,7 +161,7 @@ function makeFakeCodexAdapter() { |
| 160 | ); | 161 | ); |
| 161 | 162 | ||
| 162 | const adapter: ProviderAdapterShape<ProviderAdapterError> = { | 163 | const adapter: ProviderAdapterShape<ProviderAdapterError> = { |
| 163 | provider: "codex", | 164 | provider, |
| 164 | startSession, | 165 | startSession, |
| 165 | sendTurn, | 166 | sendTurn, |
| 166 | interruptTurn, | 167 | interruptTurn, |
| ... | @@ -199,12 +200,15 @@ const sleep = (ms: number) => | ... | @@ -199,12 +200,15 @@ const sleep = (ms: number) => |
| 199 | 200 | ||
| 200 | function makeProviderServiceLayer() { | 201 | function makeProviderServiceLayer() { |
| 201 | const codex = makeFakeCodexAdapter(); | 202 | const codex = makeFakeCodexAdapter(); |
| 203 | const claude = makeFakeCodexAdapter("claudeCode"); | ||
| 202 | const registry: typeof ProviderAdapterRegistry.Service = { | 204 | const registry: typeof ProviderAdapterRegistry.Service = { |
| 203 | getByProvider: (provider) => | 205 | getByProvider: (provider) => |
| 204 | provider === "codex" | 206 | provider === "codex" |
| 205 | ? Effect.succeed(codex.adapter) | 207 | ? Effect.succeed(codex.adapter) |
| 206 | : Effect.fail(new ProviderUnsupportedError({ provider })), | 208 | : provider === "claudeCode" |
| 207 | listProviders: () => Effect.succeed(["codex"]), | 209 | ? Effect.succeed(claude.adapter) |
| 210 | : Effect.fail(new ProviderUnsupportedError({ provider })), | ||
| 211 | listProviders: () => Effect.succeed(["codex", "claudeCode"]), | ||
| 208 | }; | 212 | }; |
| 209 | 213 | ||
| 210 | const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); | 214 | const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); |
| ... | @@ -227,6 +231,7 @@ function makeProviderServiceLayer() { | ... | @@ -227,6 +231,7 @@ function makeProviderServiceLayer() { |
| 227 | 231 | ||
| 228 | return { | 232 | return { |
| 229 | codex, | 233 | codex, |
| 234 | claude, | ||
| 230 | layer, | 235 | layer, |
| 231 | }; | 236 | }; |
| 232 | } | 237 | } |
| ... | @@ -448,6 +453,27 @@ routing.layer("ProviderServiceLive routing", (it) => { | ... | @@ -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 | it.effect("recovers stale persisted sessions for rollback by resuming thread identity", () => | 477 | it.effect("recovers stale persisted sessions for rollback by resuming thread identity", () => |
| 452 | Effect.gen(function* () { | 478 | Effect.gen(function* () { |
| 453 | const provider = yield* ProviderService; | 479 | const provider = yield* ProviderService; |
apps/server/src/serverLayers.ts+3-1| ... | @@ -18,6 +18,7 @@ import { OrchestrationProjectionPipelineLive } from "./orchestration/Layers/Proj | ... | @@ -18,6 +18,7 @@ import { OrchestrationProjectionPipelineLive } from "./orchestration/Layers/Proj |
| 18 | import { OrchestrationProjectionSnapshotQueryLive } from "./orchestration/Layers/ProjectionSnapshotQuery"; | 18 | import { OrchestrationProjectionSnapshotQueryLive } from "./orchestration/Layers/ProjectionSnapshotQuery"; |
| 19 | import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRuntimeIngestion"; | 19 | import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRuntimeIngestion"; |
| 20 | import { ProviderUnsupportedError } from "./provider/Errors"; | 20 | import { ProviderUnsupportedError } from "./provider/Errors"; |
| 21 | import { makeClaudeCodeAdapterLive } from "./provider/Layers/ClaudeCodeAdapter"; | ||
| 21 | import { makeCodexAdapterLive } from "./provider/Layers/CodexAdapter"; | 22 | import { makeCodexAdapterLive } from "./provider/Layers/CodexAdapter"; |
| 22 | import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry"; | 23 | import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry"; |
| 23 | import { makeProviderServiceLive } from "./provider/Layers/ProviderService"; | 24 | import { makeProviderServiceLive } from "./provider/Layers/ProviderService"; |
| ... | @@ -56,8 +57,9 @@ export function makeServerProviderLayer(): Layer.Layer< | ... | @@ -56,8 +57,9 @@ export function makeServerProviderLayer(): Layer.Layer< |
| 56 | const codexAdapterLayer = makeCodexAdapterLive( | 57 | const codexAdapterLayer = makeCodexAdapterLive( |
| 57 | nativeEventLogger ? { nativeEventLogger } : undefined, | 58 | nativeEventLogger ? { nativeEventLogger } : undefined, |
| 58 | ); | 59 | ); |
| 60 | const claudeAdapterLayer = makeClaudeCodeAdapterLive(); | ||
| 59 | const adapterRegistryLayer = ProviderAdapterRegistryLive.pipe( | 61 | const adapterRegistryLayer = ProviderAdapterRegistryLive.pipe( |
| 60 | Layer.provide(codexAdapterLayer), | 62 | Layer.provide(Layer.mergeAll(codexAdapterLayer, claudeAdapterLayer)), |
| 61 | Layer.provideMerge(providerSessionDirectoryLayer), | 63 | Layer.provideMerge(providerSessionDirectoryLayer), |
| 62 | ); | 64 | ); |
| 63 | return makeProviderServiceLive( | 65 | return makeProviderServiceLive( |