import * as NodeAssert from "node:assert/strict";import * as NodeServices from "@effect/platform-node/NodeServices";import { it } from "@effect/vitest";import * as Cause from "effect/Cause";import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Fiber from "effect/Fiber";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as Schema from "effect/Schema";import * as Scope from "effect/Scope";import * as Stream from "effect/Stream";import * as TestClock from "effect/testing/TestClock";import { beforeEach } from "vite-plus/test";import type { PermissionRequest, QuestionRequest } from "@opencode-ai/sdk/v2"; import { ApprovalRequestId, OpenCodeSettings, ProviderDriverKind, ProviderInstanceId, ThreadId,} from "@t3tools/contracts";import { createModelSelection } from "@t3tools/shared/model";import { ServerConfig } from "../../config.ts";import { ServerSettingsService } from "../../serverSettings.ts";import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts";import type { OpenCodeAdapterShape } from "../Services/OpenCodeAdapter.ts";import { OpenCodeRuntime, OpenCodeRuntimeError, type OpenCodeRuntimeShape,} from "../opencodeRuntime.ts";import { appendOpenCodeAssistantTextDelta, isOpenCodeNotFound, isSameOpenCodeDirectory, makeOpenCodeAdapter, mergeOpenCodeAssistantText,} from "./OpenCodeAdapter.ts"; // Test-local service tag so the rest of the file can keep using `yield* OpenCodeAdapter`.class OpenCodeAdapter extends Context.Service<OpenCodeAdapter, OpenCodeAdapterShape>()( "t3/provider/Layers/OpenCodeAdapter.test/OpenCodeAdapter",) {} const asThreadId = (value: string): ThreadId => ThreadId.make(value); type MessageEntry = { info: { id: string; role: "user" | "assistant"; }; parts: Array<unknown>;}; const runtimeMock = { state: { startCalls: [] as string[], sessionCreateUrls: [] as string[], sessionCreateInputs: [] as Array<Record<string, unknown>>, createdSessionIds: [] as string[], authHeaders: [] as Array<string | null>, abortCalls: [] as string[], abortSignals: [] as AbortSignal[], abortImplementation: null as | ((sessionID: string, signal?: AbortSignal) => Promise<void>) | null, sessionChildrenCalls: [] as string[], sessionChildrenById: new Map<string, Array<{ id: string }>>(), sessionChildrenImplementation: null as | ((sessionID: string) => Promise<Array<{ id: string }>>) | null, closeCalls: [] as string[], revertCalls: [] as Array<{ sessionID: string; messageID?: string }>, messageCalls: [] as Array<{ sessionID: string; messageID: string }>, messageFailures: 0, promptCalls: [] as Array<unknown>, promptAsyncError: null as Error | null, promptAsyncImplementation: null as (() => Promise<void>) | null, autoPromptEcho: true, autoConnect: true, promptEchoEvents: [] as Array<unknown>, closeError: null as Error | null, messages: [] as MessageEntry[], subscribedEvents: [] as Array<unknown | Promise<unknown>>, eventSubscribeObserved: null as (() => void) | null, permissionReplyCalls: [] as Array<{ requestID: string; reply: string }>, questionReplyCalls: [] as Array<{ requestID: string; answers: ReadonlyArray<ReadonlyArray<string>>; }>, sessionStatus: "idle" as "idle" | "busy", sessionStatusFailures: 0, sessionStatusCalls: 0, sessionStatusImplementation: null as (() => Promise<unknown>) | null, sessionGetIds: [] as string[], sessionGetObserved: null as ((sessionID: string) => void) | null, missingSessionIds: new Set<string>(), transientErrorSessionIds: new Set<string>(), sessionDirectoryById: new Map<string, string>(), sessionParentById: new Map<string, string>(), pendingPermissions: [] as Array<PermissionRequest>, pendingQuestions: [] as Array<QuestionRequest>, permissionListCalls: 0, questionListCalls: 0, permissionListImplementation: null as (() => Promise<Array<PermissionRequest>>) | null, questionListImplementation: null as (() => Promise<Array<QuestionRequest>>) | null, sessionUpdateCalls: [] as Array<{ sessionID: string; permission: unknown }>, forkCalls: [] as Array<{ sessionID: string; directory?: string }>, }, reset() { this.state.startCalls.length = 0; this.state.sessionCreateUrls.length = 0; this.state.sessionCreateInputs.length = 0; this.state.createdSessionIds.length = 0; this.state.authHeaders.length = 0; this.state.abortCalls.length = 0; this.state.abortSignals.length = 0; this.state.abortImplementation = null; this.state.sessionChildrenCalls.length = 0; this.state.sessionChildrenById.clear(); this.state.sessionChildrenImplementation = null; this.state.closeCalls.length = 0; this.state.revertCalls.length = 0; this.state.messageCalls.length = 0; this.state.messageFailures = 0; this.state.promptCalls.length = 0; this.state.promptAsyncError = null; this.state.promptAsyncImplementation = null; this.state.autoPromptEcho = true; this.state.autoConnect = true; this.state.promptEchoEvents.length = 0; this.state.closeError = null; this.state.messages = []; this.state.subscribedEvents = []; this.state.eventSubscribeObserved = null; this.state.permissionReplyCalls.length = 0; this.state.questionReplyCalls.length = 0; this.state.sessionStatus = "idle"; this.state.sessionStatusFailures = 0; this.state.sessionStatusCalls = 0; this.state.sessionStatusImplementation = null; this.state.sessionGetIds.length = 0; this.state.sessionGetObserved = null; this.state.missingSessionIds.clear(); this.state.transientErrorSessionIds.clear(); this.state.sessionDirectoryById.clear(); this.state.sessionParentById.clear(); this.state.pendingPermissions = []; this.state.pendingQuestions = []; this.state.permissionListCalls = 0; this.state.questionListCalls = 0; this.state.permissionListImplementation = null; this.state.questionListImplementation = null; this.state.sessionUpdateCalls.length = 0; this.state.forkCalls.length = 0; },}; const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = { startOpenCodeServerProcess: ({ binaryPath, serverPassword }) => Effect.gen(function* () { runtimeMock.state.startCalls.push(binaryPath); const url = "http://127.0.0.1:4301"; yield* Effect.addFinalizer(() => Effect.sync(() => { runtimeMock.state.closeCalls.push(url); if (runtimeMock.state.closeError) { throw runtimeMock.state.closeError; } }), ); return { url, version: "1.15.13", ...(serverPassword ? { serverPassword } : {}), exitCode: Effect.never, isRunning: Effect.succeed(true), }; }), connectToOpenCodeServer: ({ serverUrl, serverPassword }) => Effect.gen(function* () { const url = serverUrl ?? "http://127.0.0.1:4301"; // Always register a finalizer so the closeCalls/closeError probes fire; // production attaches none for external servers. yield* Effect.addFinalizer(() => Effect.sync(() => { runtimeMock.state.closeCalls.push(url); if (runtimeMock.state.closeError) { throw runtimeMock.state.closeError; } }), ); return { url, version: "1.15.13", ...(serverPassword ? { serverPassword } : {}), exitCode: null, external: Boolean(serverUrl), }; }), runOpenCodeCommand: () => Effect.succeed({ stdout: "", stderr: "", code: 0 }), createOpenCodeSdkClient: ({ baseUrl, serverPassword }) => ({ session: { create: async (input: Record<string, unknown>) => { runtimeMock.state.sessionCreateUrls.push(baseUrl); runtimeMock.state.sessionCreateInputs.push(input); runtimeMock.state.authHeaders.push( serverPassword ? `Basic ${btoa(`opencode:${serverPassword}`)}` : null, ); return { data: { id: runtimeMock.state.createdSessionIds.shift() ?? `${baseUrl}/session` }, }; }, get: async ({ sessionID }: { sessionID: string }) => { runtimeMock.state.sessionGetIds.push(sessionID); runtimeMock.state.sessionGetObserved?.(sessionID); // The real client is `throwOnError: true`: non-2xx rejects rather // than resolving, so missing → 404 throw, transient → 500 throw. if (runtimeMock.state.transientErrorSessionIds.has(sessionID)) { throw new Error("opencode server error", { cause: { status: 500 } }); } if (runtimeMock.state.missingSessionIds.has(sessionID)) { throw new Error(`Session not found: ${sessionID}`, { cause: { status: 404, body: { name: "NotFoundError" } }, }); } const directory = runtimeMock.state.sessionDirectoryById.get(sessionID); const parentID = runtimeMock.state.sessionParentById.get(sessionID); return { data: { id: sessionID, ...(directory ? { directory } : {}), ...(parentID ? { parentID } : {}), }, }; }, update: async ({ sessionID, permission }: { sessionID: string; permission: unknown }) => { runtimeMock.state.sessionUpdateCalls.push({ sessionID, permission }); return { data: { id: sessionID } }; }, fork: async ({ sessionID, directory }: { sessionID: string; directory?: string }) => { // Fork clones history into a new session bound to the directory. const forkedId = `${sessionID}_fork`; runtimeMock.state.forkCalls.push({ sessionID, ...(directory ? { directory } : {}) }); if (directory) { runtimeMock.state.sessionDirectoryById.set(forkedId, directory); } return { data: { id: forkedId, ...(directory ? { directory } : {}) } }; }, abort: async ({ sessionID }: { sessionID: string }, options?: { signal?: AbortSignal }) => { runtimeMock.state.abortCalls.push(sessionID); if (options?.signal) { runtimeMock.state.abortSignals.push(options.signal); } await runtimeMock.state.abortImplementation?.(sessionID, options?.signal); }, children: async ({ sessionID }: { sessionID: string }) => { runtimeMock.state.sessionChildrenCalls.push(sessionID); return { data: runtimeMock.state.sessionChildrenImplementation ? await runtimeMock.state.sessionChildrenImplementation(sessionID) : (runtimeMock.state.sessionChildrenById.get(sessionID) ?? []), }; }, status: async () => { runtimeMock.state.sessionStatusCalls += 1; if (runtimeMock.state.sessionStatusImplementation) { return await runtimeMock.state.sessionStatusImplementation(); } if (runtimeMock.state.sessionStatusFailures > 0) { runtimeMock.state.sessionStatusFailures -= 1; throw new Error("status failed"); } return { data: runtimeMock.state.sessionStatus === "idle" ? {} : { "http://127.0.0.1:9999/session": { type: "busy" as const } }, }; }, promptAsync: async (input: unknown) => { runtimeMock.state.promptCalls.push(input); await runtimeMock.state.promptAsyncImplementation?.(); if (runtimeMock.state.promptAsyncError) { throw runtimeMock.state.promptAsyncError; } if ( runtimeMock.state.autoPromptEcho && typeof input === "object" && input !== null && "sessionID" in input && "messageID" in input && typeof input.sessionID === "string" && typeof input.messageID === "string" ) { runtimeMock.state.messages.push({ info: { id: input.messageID, role: "user" }, parts: [], }); runtimeMock.state.promptEchoEvents.push({ id: `evt-auto-user-${input.messageID}`, type: "message.updated", properties: { sessionID: input.sessionID, info: { id: input.messageID, role: "user" }, }, }); } }, messages: async () => ({ data: runtimeMock.state.messages }), message: async ({ sessionID, messageID }: { sessionID: string; messageID: string }) => { runtimeMock.state.messageCalls.push({ sessionID, messageID }); if (runtimeMock.state.messageFailures > 0) { runtimeMock.state.messageFailures -= 1; throw new Error("message lookup failed", { cause: { status: 500 } }); } const message = runtimeMock.state.messages.find((entry) => entry.info.id === messageID); if (!message) { throw new Error(`Message not found: ${messageID}`, { cause: { status: 404, body: { name: "NotFoundError" } }, }); } return { data: message }; }, revert: async ({ sessionID, messageID }: { sessionID: string; messageID?: string }) => { runtimeMock.state.revertCalls.push({ sessionID, ...(messageID ? { messageID } : {}), }); if (!messageID) { runtimeMock.state.messages = []; return; } const targetIndex = runtimeMock.state.messages.findIndex( (entry) => entry.info.id === messageID, ); runtimeMock.state.messages = targetIndex >= 0 ? runtimeMock.state.messages.slice(0, targetIndex + 1) : runtimeMock.state.messages; }, }, event: { subscribe: async () => { runtimeMock.state.eventSubscribeObserved?.(); return { stream: (async function* () { if (runtimeMock.state.autoConnect) { yield { id: "evt-auto-connected", type: "server.connected", properties: {} }; } for (const event of runtimeMock.state.subscribedEvents) { const resolved = await event; while (runtimeMock.state.promptEchoEvents.length > 0) { yield runtimeMock.state.promptEchoEvents.shift(); } yield resolved; } })(), }; }, }, permission: { list: async () => { runtimeMock.state.permissionListCalls += 1; return { data: runtimeMock.state.permissionListImplementation ? await runtimeMock.state.permissionListImplementation() : runtimeMock.state.pendingPermissions, }; }, reply: async ({ requestID, reply }: { requestID: string; reply: string }) => { runtimeMock.state.permissionReplyCalls.push({ requestID, reply }); }, }, question: { list: async () => { runtimeMock.state.questionListCalls += 1; return { data: runtimeMock.state.questionListImplementation ? await runtimeMock.state.questionListImplementation() : runtimeMock.state.pendingQuestions, }; }, reply: async ({ requestID, answers, }: { requestID: string; answers: ReadonlyArray<ReadonlyArray<string>>; }) => { runtimeMock.state.questionReplyCalls.push({ requestID, answers }); }, }, }) as unknown as ReturnType<OpenCodeRuntimeShape["createOpenCodeSdkClient"]>, loadOpenCodeInventory: () => Effect.fail( new OpenCodeRuntimeError({ operation: "loadOpenCodeInventory", detail: "OpenCodeRuntimeTestDouble.loadOpenCodeInventory not used in this test", cause: null, }), ), loadInventoryFromCli: () => Effect.fail( new OpenCodeRuntimeError({ operation: "loadInventoryFromCli", detail: "OpenCodeRuntimeTestDouble.loadInventoryFromCli not used in this test", cause: null, }), ),}; const providerSessionDirectoryTestLayer = Layer.succeed(ProviderSessionDirectory, { upsert: () => Effect.void, getProvider: () => Effect.die(new Error("ProviderSessionDirectory.getProvider is not used in test")), getBinding: () => Effect.succeed(Option.none()), listThreadIds: () => Effect.succeed([]), listBindings: () => Effect.succeed([]),}); // The adapter now receives its settings as a plain argument (the old design// read from `ServerSettingsService` internally). The test-only// `ServerSettingsService` below is still kept because other dependencies in// the layer graph reach for it — but the routing values the assertions// probe (serverUrl, serverPassword) must be threaded directly through the// decoded `OpenCodeSettings`.const openCodeAdapterTestSettings = Schema.decodeSync(OpenCodeSettings)({ binaryPath: "fake-opencode", serverUrl: "http://127.0.0.1:9999", serverPassword: "secret-password",}); const OpenCodeAdapterTestLayer = Layer.effect( OpenCodeAdapter, makeOpenCodeAdapter(openCodeAdapterTestSettings),).pipe( Layer.provideMerge(Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble)), Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), Layer.provideMerge( ServerSettingsService.layerTest({ providers: { opencode: { binaryPath: "fake-opencode", serverUrl: "http://127.0.0.1:9999", serverPassword: "secret-password", }, }, }), ), Layer.provideMerge(providerSessionDirectoryTestLayer), Layer.provideMerge(NodeServices.layer),); beforeEach(() => { runtimeMock.reset();}); const advanceTestClock = (ms: number) => TestClock.adjust(`${ms} millis`).pipe(Effect.andThen(Effect.yieldNow)); function promiseWithResolvers<T>() { let resolve!: (value: T | PromiseLike<T>) => void; let reject!: (reason?: unknown) => void; const promise = new Promise<T>((resolvePromise, rejectPromise) => { resolve = resolvePromise; reject = rejectPromise; }); return { promise, resolve, reject };} const permissionRequest = (id: string, sessionID: string): PermissionRequest => ({ id, sessionID, permission: "bash", patterns: ["pwd"], metadata: {}, always: [],}); const questionRequest = (id: string, sessionID: string): QuestionRequest => ({ id, sessionID, questions: [ { header: "Scope", question: "Which scope should OpenCode use?", options: [{ label: "Workspace", description: "Use this workspace." }], }, ],}); it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {Lines 1–500 of 5356. Load more
import * as NodeAssert from "node:assert/strict"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { it } from "@effect/vitest"; import * as Cause from "effect/Cause"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Fiber from "effect/Fiber"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; import * as TestClock from "effect/testing/TestClock"; import { beforeEach } from "vite-plus/test"; import type { PermissionRequest, QuestionRequest } from "@opencode-ai/sdk/v2"; import { ApprovalRequestId, OpenCodeSettings, ProviderDriverKind, ProviderInstanceId, ThreadId, } from "@t3tools/contracts"; import { createModelSelection } from "@t3tools/shared/model"; import { ServerConfig } from "../../config.ts"; import { ServerSettingsService } from "../../serverSettings.ts"; import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; import type { OpenCodeAdapterShape } from "../Services/OpenCodeAdapter.ts"; import { OpenCodeRuntime, OpenCodeRuntimeError, type OpenCodeRuntimeShape, } from "../opencodeRuntime.ts"; import { appendOpenCodeAssistantTextDelta, isOpenCodeNotFound, isSameOpenCodeDirectory, makeOpenCodeAdapter, mergeOpenCodeAssistantText, } from "./OpenCodeAdapter.ts"; // Test-local service tag so the rest of the file can keep using `yield* OpenCodeAdapter`. class OpenCodeAdapter extends Context.Service<OpenCodeAdapter, OpenCodeAdapterShape>()( "t3/provider/Layers/OpenCodeAdapter.test/OpenCodeAdapter", ) {} const asThreadId = (value: string): ThreadId => ThreadId.make(value); type MessageEntry = { info: { id: string; role: "user" | "assistant"; }; parts: Array<unknown>; }; const runtimeMock = { state: { startCalls: [] as string[], sessionCreateUrls: [] as string[], sessionCreateInputs: [] as Array<Record<string, unknown>>, createdSessionIds: [] as string[], authHeaders: [] as Array<string | null>, abortCalls: [] as string[], abortSignals: [] as AbortSignal[], abortImplementation: null as | ((sessionID: string, signal?: AbortSignal) => Promise<void>) | null, sessionChildrenCalls: [] as string[], sessionChildrenById: new Map<string, Array<{ id: string }>>(), sessionChildrenImplementation: null as | ((sessionID: string) => Promise<Array<{ id: string }>>) | null, closeCalls: [] as string[], revertCalls: [] as Array<{ sessionID: string; messageID?: string }>, messageCalls: [] as Array<{ sessionID: string; messageID: string }>, messageFailures: 0, promptCalls: [] as Array<unknown>, promptAsyncError: null as Error | null, promptAsyncImplementation: null as (() => Promise<void>) | null, autoPromptEcho: true, autoConnect: true, promptEchoEvents: [] as Array<unknown>, closeError: null as Error | null, messages: [] as MessageEntry[], subscribedEvents: [] as Array<unknown | Promise<unknown>>, eventSubscribeObserved: null as (() => void) | null, permissionReplyCalls: [] as Array<{ requestID: string; reply: string }>, questionReplyCalls: [] as Array<{ requestID: string; answers: ReadonlyArray<ReadonlyArray<string>>; }>, sessionStatus: "idle" as "idle" | "busy", sessionStatusFailures: 0, sessionStatusCalls: 0, sessionStatusImplementation: null as (() => Promise<unknown>) | null, sessionGetIds: [] as string[], sessionGetObserved: null as ((sessionID: string) => void) | null, missingSessionIds: new Set<string>(), transientErrorSessionIds: new Set<string>(), sessionDirectoryById: new Map<string, string>(), sessionParentById: new Map<string, string>(), pendingPermissions: [] as Array<PermissionRequest>, pendingQuestions: [] as Array<QuestionRequest>, permissionListCalls: 0, questionListCalls: 0, permissionListImplementation: null as (() => Promise<Array<PermissionRequest>>) | null, questionListImplementation: null as (() => Promise<Array<QuestionRequest>>) | null, sessionUpdateCalls: [] as Array<{ sessionID: string; permission: unknown }>, forkCalls: [] as Array<{ sessionID: string; directory?: string }>, }, reset() { this.state.startCalls.length = 0; this.state.sessionCreateUrls.length = 0; this.state.sessionCreateInputs.length = 0; this.state.createdSessionIds.length = 0; this.state.authHeaders.length = 0; this.state.abortCalls.length = 0; this.state.abortSignals.length = 0; this.state.abortImplementation = null; this.state.sessionChildrenCalls.length = 0; this.state.sessionChildrenById.clear(); this.state.sessionChildrenImplementation = null; this.state.closeCalls.length = 0; this.state.revertCalls.length = 0; this.state.messageCalls.length = 0; this.state.messageFailures = 0; this.state.promptCalls.length = 0; this.state.promptAsyncError = null; this.state.promptAsyncImplementation = null; this.state.autoPromptEcho = true; this.state.autoConnect = true; this.state.promptEchoEvents.length = 0; this.state.closeError = null; this.state.messages = []; this.state.subscribedEvents = []; this.state.eventSubscribeObserved = null; this.state.permissionReplyCalls.length = 0; this.state.questionReplyCalls.length = 0; this.state.sessionStatus = "idle"; this.state.sessionStatusFailures = 0; this.state.sessionStatusCalls = 0; this.state.sessionStatusImplementation = null; this.state.sessionGetIds.length = 0; this.state.sessionGetObserved = null; this.state.missingSessionIds.clear(); this.state.transientErrorSessionIds.clear(); this.state.sessionDirectoryById.clear(); this.state.sessionParentById.clear(); this.state.pendingPermissions = []; this.state.pendingQuestions = []; this.state.permissionListCalls = 0; this.state.questionListCalls = 0; this.state.permissionListImplementation = null; this.state.questionListImplementation = null; this.state.sessionUpdateCalls.length = 0; this.state.forkCalls.length = 0; }, }; const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = { startOpenCodeServerProcess: ({ binaryPath, serverPassword }) => Effect.gen(function* () { runtimeMock.state.startCalls.push(binaryPath); const url = "http://127.0.0.1:4301"; yield* Effect.addFinalizer(() => Effect.sync(() => { runtimeMock.state.closeCalls.push(url); if (runtimeMock.state.closeError) { throw runtimeMock.state.closeError; } }), ); return { url, version: "1.15.13", ...(serverPassword ? { serverPassword } : {}), exitCode: Effect.never, isRunning: Effect.succeed(true), }; }), connectToOpenCodeServer: ({ serverUrl, serverPassword }) => Effect.gen(function* () { const url = serverUrl ?? "http://127.0.0.1:4301"; // Always register a finalizer so the closeCalls/closeError probes fire; // production attaches none for external servers. yield* Effect.addFinalizer(() => Effect.sync(() => { runtimeMock.state.closeCalls.push(url); if (runtimeMock.state.closeError) { throw runtimeMock.state.closeError; } }), ); return { url, version: "1.15.13", ...(serverPassword ? { serverPassword } : {}), exitCode: null, external: Boolean(serverUrl), }; }), runOpenCodeCommand: () => Effect.succeed({ stdout: "", stderr: "", code: 0 }), createOpenCodeSdkClient: ({ baseUrl, serverPassword }) => ({ session: { create: async (input: Record<string, unknown>) => { runtimeMock.state.sessionCreateUrls.push(baseUrl); runtimeMock.state.sessionCreateInputs.push(input); runtimeMock.state.authHeaders.push( serverPassword ? `Basic ${btoa(`opencode:${serverPassword}`)}` : null, ); return { data: { id: runtimeMock.state.createdSessionIds.shift() ?? `${baseUrl}/session` }, }; }, get: async ({ sessionID }: { sessionID: string }) => { runtimeMock.state.sessionGetIds.push(sessionID); runtimeMock.state.sessionGetObserved?.(sessionID); // The real client is `throwOnError: true`: non-2xx rejects rather // than resolving, so missing → 404 throw, transient → 500 throw. if (runtimeMock.state.transientErrorSessionIds.has(sessionID)) { throw new Error("opencode server error", { cause: { status: 500 } }); } if (runtimeMock.state.missingSessionIds.has(sessionID)) { throw new Error(`Session not found: ${sessionID}`, { cause: { status: 404, body: { name: "NotFoundError" } }, }); } const directory = runtimeMock.state.sessionDirectoryById.get(sessionID); const parentID = runtimeMock.state.sessionParentById.get(sessionID); return { data: { id: sessionID, ...(directory ? { directory } : {}), ...(parentID ? { parentID } : {}), }, }; }, update: async ({ sessionID, permission }: { sessionID: string; permission: unknown }) => { runtimeMock.state.sessionUpdateCalls.push({ sessionID, permission }); return { data: { id: sessionID } }; }, fork: async ({ sessionID, directory }: { sessionID: string; directory?: string }) => { // Fork clones history into a new session bound to the directory. const forkedId = `${sessionID}_fork`; runtimeMock.state.forkCalls.push({ sessionID, ...(directory ? { directory } : {}) }); if (directory) { runtimeMock.state.sessionDirectoryById.set(forkedId, directory); } return { data: { id: forkedId, ...(directory ? { directory } : {}) } }; }, abort: async ({ sessionID }: { sessionID: string }, options?: { signal?: AbortSignal }) => { runtimeMock.state.abortCalls.push(sessionID); if (options?.signal) { runtimeMock.state.abortSignals.push(options.signal); } await runtimeMock.state.abortImplementation?.(sessionID, options?.signal); }, children: async ({ sessionID }: { sessionID: string }) => { runtimeMock.state.sessionChildrenCalls.push(sessionID); return { data: runtimeMock.state.sessionChildrenImplementation ? await runtimeMock.state.sessionChildrenImplementation(sessionID) : (runtimeMock.state.sessionChildrenById.get(sessionID) ?? []), }; }, status: async () => { runtimeMock.state.sessionStatusCalls += 1; if (runtimeMock.state.sessionStatusImplementation) { return await runtimeMock.state.sessionStatusImplementation(); } if (runtimeMock.state.sessionStatusFailures > 0) { runtimeMock.state.sessionStatusFailures -= 1; throw new Error("status failed"); } return { data: runtimeMock.state.sessionStatus === "idle" ? {} : { "http://127.0.0.1:9999/session": { type: "busy" as const } }, }; }, promptAsync: async (input: unknown) => { runtimeMock.state.promptCalls.push(input); await runtimeMock.state.promptAsyncImplementation?.(); if (runtimeMock.state.promptAsyncError) { throw runtimeMock.state.promptAsyncError; } if ( runtimeMock.state.autoPromptEcho && typeof input === "object" && input !== null && "sessionID" in input && "messageID" in input && typeof input.sessionID === "string" && typeof input.messageID === "string" ) { runtimeMock.state.messages.push({ info: { id: input.messageID, role: "user" }, parts: [], }); runtimeMock.state.promptEchoEvents.push({ id: `evt-auto-user-${input.messageID}`, type: "message.updated", properties: { sessionID: input.sessionID, info: { id: input.messageID, role: "user" }, }, }); } }, messages: async () => ({ data: runtimeMock.state.messages }), message: async ({ sessionID, messageID }: { sessionID: string; messageID: string }) => { runtimeMock.state.messageCalls.push({ sessionID, messageID }); if (runtimeMock.state.messageFailures > 0) { runtimeMock.state.messageFailures -= 1; throw new Error("message lookup failed", { cause: { status: 500 } }); } const message = runtimeMock.state.messages.find((entry) => entry.info.id === messageID); if (!message) { throw new Error(`Message not found: ${messageID}`, { cause: { status: 404, body: { name: "NotFoundError" } }, }); } return { data: message }; }, revert: async ({ sessionID, messageID }: { sessionID: string; messageID?: string }) => { runtimeMock.state.revertCalls.push({ sessionID, ...(messageID ? { messageID } : {}), }); if (!messageID) { runtimeMock.state.messages = []; return; } const targetIndex = runtimeMock.state.messages.findIndex( (entry) => entry.info.id === messageID, ); runtimeMock.state.messages = targetIndex >= 0 ? runtimeMock.state.messages.slice(0, targetIndex + 1) : runtimeMock.state.messages; }, }, event: { subscribe: async () => { runtimeMock.state.eventSubscribeObserved?.(); return { stream: (async function* () { if (runtimeMock.state.autoConnect) { yield { id: "evt-auto-connected", type: "server.connected", properties: {} }; } for (const event of runtimeMock.state.subscribedEvents) { const resolved = await event; while (runtimeMock.state.promptEchoEvents.length > 0) { yield runtimeMock.state.promptEchoEvents.shift(); } yield resolved; } })(), }; }, }, permission: { list: async () => { runtimeMock.state.permissionListCalls += 1; return { data: runtimeMock.state.permissionListImplementation ? await runtimeMock.state.permissionListImplementation() : runtimeMock.state.pendingPermissions, }; }, reply: async ({ requestID, reply }: { requestID: string; reply: string }) => { runtimeMock.state.permissionReplyCalls.push({ requestID, reply }); }, }, question: { list: async () => { runtimeMock.state.questionListCalls += 1; return { data: runtimeMock.state.questionListImplementation ? await runtimeMock.state.questionListImplementation() : runtimeMock.state.pendingQuestions, }; }, reply: async ({ requestID, answers, }: { requestID: string; answers: ReadonlyArray<ReadonlyArray<string>>; }) => { runtimeMock.state.questionReplyCalls.push({ requestID, answers }); }, }, }) as unknown as ReturnType<OpenCodeRuntimeShape["createOpenCodeSdkClient"]>, loadOpenCodeInventory: () => Effect.fail( new OpenCodeRuntimeError({ operation: "loadOpenCodeInventory", detail: "OpenCodeRuntimeTestDouble.loadOpenCodeInventory not used in this test", cause: null, }), ), loadInventoryFromCli: () => Effect.fail( new OpenCodeRuntimeError({ operation: "loadInventoryFromCli", detail: "OpenCodeRuntimeTestDouble.loadInventoryFromCli not used in this test", cause: null, }), ), }; const providerSessionDirectoryTestLayer = Layer.succeed(ProviderSessionDirectory, { upsert: () => Effect.void, getProvider: () => Effect.die(new Error("ProviderSessionDirectory.getProvider is not used in test")), getBinding: () => Effect.succeed(Option.none()), listThreadIds: () => Effect.succeed([]), listBindings: () => Effect.succeed([]), }); // The adapter now receives its settings as a plain argument (the old design // read from `ServerSettingsService` internally). The test-only // `ServerSettingsService` below is still kept because other dependencies in // the layer graph reach for it — but the routing values the assertions // probe (serverUrl, serverPassword) must be threaded directly through the // decoded `OpenCodeSettings`. const openCodeAdapterTestSettings = Schema.decodeSync(OpenCodeSettings)({ binaryPath: "fake-opencode", serverUrl: "http://127.0.0.1:9999", serverPassword: "secret-password", }); const OpenCodeAdapterTestLayer = Layer.effect( OpenCodeAdapter, makeOpenCodeAdapter(openCodeAdapterTestSettings), ).pipe( Layer.provideMerge(Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble)), Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), Layer.provideMerge( ServerSettingsService.layerTest({ providers: { opencode: { binaryPath: "fake-opencode", serverUrl: "http://127.0.0.1:9999", serverPassword: "secret-password", }, }, }), ), Layer.provideMerge(providerSessionDirectoryTestLayer), Layer.provideMerge(NodeServices.layer), ); beforeEach(() => { runtimeMock.reset(); }); const advanceTestClock = (ms: number) => TestClock.adjust(`${ms} millis`).pipe(Effect.andThen(Effect.yieldNow)); function promiseWithResolvers<T>() { let resolve!: (value: T | PromiseLike<T>) => void; let reject!: (reason?: unknown) => void; const promise = new Promise<T>((resolvePromise, rejectPromise) => { resolve = resolvePromise; reject = rejectPromise; }); return { promise, resolve, reject }; } const permissionRequest = (id: string, sessionID: string): PermissionRequest => ({ id, sessionID, permission: "bash", patterns: ["pwd"], metadata: {}, always: [], }); const questionRequest = (id: string, sessionID: string): QuestionRequest => ({ id, sessionID, questions: [ { header: "Scope", question: "Which scope should OpenCode use?", options: [{ label: "Workspace", description: "Use this workspace." }], }, ], }); it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {