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 Crypto from "effect/Crypto";import * as Deferred from "effect/Deferred";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, vi } from "vite-plus/test";import type { Event as OpenCodeEvent, PermissionRequest, QuestionRequest, ToolPart,} 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 { buildRuntimeInstructions } from "../RuntimeInstructions.ts";import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts";import type { OpenCodeAdapterShape } from "../Services/OpenCodeAdapter.ts";import { OpenCodeRuntime, OpenCodeRuntimeError, type OpenCodeRuntimeShape,} from "../opencodeRuntime.ts";import { isOpenCodeNotFound, isSameOpenCodeDirectory, makeOpenCodeAdapter, mergeOpenCodeAssistantText,} from "./OpenCodeAdapter.ts";import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; // 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[], revertMessageID: undefined as string | undefined, revertCalls: [] as Array<{ sessionID: string; messageID?: string }>, messageCalls: [] as Array<{ sessionID: string; messageID: string }>, messageFailures: 0, promptCalls: [] as Array<unknown>, summarizeCalls: [] as Array<unknown>, promptAsyncError: null as Error | null, promptAsyncImplementation: null as (() => Promise<void>) | null, autoPromptEcho: true, autoConnect: true, endEventStream: false, promptEchoEvents: [] as Array<unknown>, closeError: null as Error | null, messages: [] as MessageEntry[], subscribedEvents: [] as Array<unknown | Promise<unknown>>, eventSubscribeObserved: null as (() => void) | null, eventStreamError: null as ((cause: unknown) => void) | null, permissionReplyCalls: [] as Array<{ requestID: string; reply: string }>, permissionReplyImplementation: null as ((signal?: AbortSignal) => Promise<void>) | null, permissionReplySignals: [] as AbortSignal[], questionReplyCalls: [] as Array<{ requestID: string; answers: ReadonlyArray<ReadonlyArray<string>>; }>, questionReplyImplementation: null as ((signal?: AbortSignal) => Promise<void>) | null, 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, sessionGetImplementation: null as | ((sessionID: string, signal?: AbortSignal) => Promise<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.revertMessageID = undefined; this.state.revertCalls.length = 0; this.state.messageCalls.length = 0; this.state.messageFailures = 0; this.state.promptCalls.length = 0; this.state.summarizeCalls.length = 0; this.state.promptAsyncError = null; this.state.promptAsyncImplementation = null; this.state.autoPromptEcho = true; this.state.autoConnect = true; this.state.endEventStream = false; this.state.promptEchoEvents.length = 0; this.state.closeError = null; this.state.messages = []; this.state.subscribedEvents = []; this.state.eventSubscribeObserved = null; this.state.eventStreamError = null; this.state.permissionReplyCalls.length = 0; this.state.permissionReplyImplementation = null; this.state.permissionReplySignals.length = 0; this.state.questionReplyCalls.length = 0; this.state.questionReplyImplementation = null; 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.sessionGetImplementation = 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 }, options?: { signal?: AbortSignal }) => { runtimeMock.state.sessionGetIds.push(sessionID); runtimeMock.state.sessionGetObserved?.(sessionID); if (runtimeMock.state.sessionGetImplementation) { await runtimeMock.state.sessionGetImplementation(sessionID, options?.signal); } // 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, ...(runtimeMock.state.revertMessageID ? { revert: { messageID: runtimeMock.state.revertMessageID } } : {}), ...(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); runtimeMock.state.pendingPermissions = runtimeMock.state.pendingPermissions.filter( (request) => request.sessionID !== sessionID, ); runtimeMock.state.pendingQuestions = runtimeMock.state.pendingQuestions.filter( (request) => request.sessionID !== sessionID, ); }, 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" }, }, }); } }, summarize: async (input: unknown) => { runtimeMock.state.summarizeCalls.push(input); return { data: true }; }, 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) { throw new Error("Expected messageID"); } let lastUserID: string | undefined; for (const entry of runtimeMock.state.messages) { if (entry.info.role === "user") lastUserID = entry.info.id; if (entry.info.id === messageID && entry.parts.length > 0) { runtimeMock.state.revertMessageID = lastUserID ?? messageID; break; } } }, }, event: { subscribe: async ( _input: unknown, options?: { signal?: AbortSignal; onSseError?: (cause: unknown) => void }, ) => { runtimeMock.state.eventSubscribeObserved?.(); runtimeMock.state.eventStreamError = options?.onSseError ?? null; return { stream: (async function* () { const aborted = promiseWithResolvers<void>(); const onAbort = () => aborted.resolve(undefined); options?.signal?.addEventListener("abort", onAbort, { once: true }); try { if (runtimeMock.state.autoConnect) { yield { id: "evt-auto-connected", type: "server.connected", properties: {} }; } for (const event of runtimeMock.state.subscribedEvents) { if (options?.signal?.aborted) return; const resolved = await Promise.race([event, aborted.promise]); if (options?.signal?.aborted) return; while (runtimeMock.state.promptEchoEvents.length > 0) { yield runtimeMock.state.promptEchoEvents.shift(); } const nativeEvent = resolved as OpenCodeEvent; if (nativeEvent.type === "permission.asked") { runtimeMock.state.pendingPermissions = runtimeMock.state.pendingPermissions.filter( (request) => request.id !== nativeEvent.properties.id, ); runtimeMock.state.pendingPermissions.push(nativeEvent.properties); } else if (nativeEvent.type === "permission.replied") { runtimeMock.state.pendingPermissions = runtimeMock.state.pendingPermissions.filter( (request) => request.id !== nativeEvent.properties.requestID, ); } else if (nativeEvent.type === "question.asked") { runtimeMock.state.pendingQuestions = runtimeMock.state.pendingQuestions.filter( (request) => request.id !== nativeEvent.properties.id, ); runtimeMock.state.pendingQuestions.push(nativeEvent.properties); } else if ( nativeEvent.type === "question.replied" || nativeEvent.type === "question.rejected" ) { runtimeMock.state.pendingQuestions = runtimeMock.state.pendingQuestions.filter( (request) => request.id !== nativeEvent.properties.requestID, ); } yield resolved; } if (!runtimeMock.state.endEventStream && !options?.signal?.aborted) { await aborted.promise; } } finally { options?.signal?.removeEventListener("abort", onAbort); } })(), }; }, }, 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 }, options?: { signal?: AbortSignal }, ) => { runtimeMock.state.permissionReplyCalls.push({ requestID, reply }); if (options?.signal) runtimeMock.state.permissionReplySignals.push(options.signal); if (runtimeMock.state.permissionReplyImplementation) { await runtimeMock.state.permissionReplyImplementation(options?.signal); } runtimeMock.state.pendingPermissions = runtimeMock.state.pendingPermissions.filter( (request) => request.id !== requestID, ); }, }, 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>>; }, options?: { signal?: AbortSignal }, ) => { runtimeMock.state.questionReplyCalls.push({ requestID, answers }); await runtimeMock.state.questionReplyImplementation?.(options?.signal); runtimeMock.state.pendingQuestions = runtimeMock.state.pendingQuestions.filter( (request) => request.id !== requestID, ); }, }, }) as unknown as ReturnType<OpenCodeRuntimeShape["createOpenCodeSdkClient"]>,Lines 1–500 of 7586. 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 Crypto from "effect/Crypto"; import * as Deferred from "effect/Deferred"; 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, vi } from "vite-plus/test"; import type { Event as OpenCodeEvent, PermissionRequest, QuestionRequest, ToolPart, } 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 { buildRuntimeInstructions } from "../RuntimeInstructions.ts"; import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; import type { OpenCodeAdapterShape } from "../Services/OpenCodeAdapter.ts"; import { OpenCodeRuntime, OpenCodeRuntimeError, type OpenCodeRuntimeShape, } from "../opencodeRuntime.ts"; import { isOpenCodeNotFound, isSameOpenCodeDirectory, makeOpenCodeAdapter, mergeOpenCodeAssistantText, } from "./OpenCodeAdapter.ts"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; // 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[], revertMessageID: undefined as string | undefined, revertCalls: [] as Array<{ sessionID: string; messageID?: string }>, messageCalls: [] as Array<{ sessionID: string; messageID: string }>, messageFailures: 0, promptCalls: [] as Array<unknown>, summarizeCalls: [] as Array<unknown>, promptAsyncError: null as Error | null, promptAsyncImplementation: null as (() => Promise<void>) | null, autoPromptEcho: true, autoConnect: true, endEventStream: false, promptEchoEvents: [] as Array<unknown>, closeError: null as Error | null, messages: [] as MessageEntry[], subscribedEvents: [] as Array<unknown | Promise<unknown>>, eventSubscribeObserved: null as (() => void) | null, eventStreamError: null as ((cause: unknown) => void) | null, permissionReplyCalls: [] as Array<{ requestID: string; reply: string }>, permissionReplyImplementation: null as ((signal?: AbortSignal) => Promise<void>) | null, permissionReplySignals: [] as AbortSignal[], questionReplyCalls: [] as Array<{ requestID: string; answers: ReadonlyArray<ReadonlyArray<string>>; }>, questionReplyImplementation: null as ((signal?: AbortSignal) => Promise<void>) | null, 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, sessionGetImplementation: null as | ((sessionID: string, signal?: AbortSignal) => Promise<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.revertMessageID = undefined; this.state.revertCalls.length = 0; this.state.messageCalls.length = 0; this.state.messageFailures = 0; this.state.promptCalls.length = 0; this.state.summarizeCalls.length = 0; this.state.promptAsyncError = null; this.state.promptAsyncImplementation = null; this.state.autoPromptEcho = true; this.state.autoConnect = true; this.state.endEventStream = false; this.state.promptEchoEvents.length = 0; this.state.closeError = null; this.state.messages = []; this.state.subscribedEvents = []; this.state.eventSubscribeObserved = null; this.state.eventStreamError = null; this.state.permissionReplyCalls.length = 0; this.state.permissionReplyImplementation = null; this.state.permissionReplySignals.length = 0; this.state.questionReplyCalls.length = 0; this.state.questionReplyImplementation = null; 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.sessionGetImplementation = 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 }, options?: { signal?: AbortSignal }) => { runtimeMock.state.sessionGetIds.push(sessionID); runtimeMock.state.sessionGetObserved?.(sessionID); if (runtimeMock.state.sessionGetImplementation) { await runtimeMock.state.sessionGetImplementation(sessionID, options?.signal); } // 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, ...(runtimeMock.state.revertMessageID ? { revert: { messageID: runtimeMock.state.revertMessageID } } : {}), ...(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); runtimeMock.state.pendingPermissions = runtimeMock.state.pendingPermissions.filter( (request) => request.sessionID !== sessionID, ); runtimeMock.state.pendingQuestions = runtimeMock.state.pendingQuestions.filter( (request) => request.sessionID !== sessionID, ); }, 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" }, }, }); } }, summarize: async (input: unknown) => { runtimeMock.state.summarizeCalls.push(input); return { data: true }; }, 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) { throw new Error("Expected messageID"); } let lastUserID: string | undefined; for (const entry of runtimeMock.state.messages) { if (entry.info.role === "user") lastUserID = entry.info.id; if (entry.info.id === messageID && entry.parts.length > 0) { runtimeMock.state.revertMessageID = lastUserID ?? messageID; break; } } }, }, event: { subscribe: async ( _input: unknown, options?: { signal?: AbortSignal; onSseError?: (cause: unknown) => void }, ) => { runtimeMock.state.eventSubscribeObserved?.(); runtimeMock.state.eventStreamError = options?.onSseError ?? null; return { stream: (async function* () { const aborted = promiseWithResolvers<void>(); const onAbort = () => aborted.resolve(undefined); options?.signal?.addEventListener("abort", onAbort, { once: true }); try { if (runtimeMock.state.autoConnect) { yield { id: "evt-auto-connected", type: "server.connected", properties: {} }; } for (const event of runtimeMock.state.subscribedEvents) { if (options?.signal?.aborted) return; const resolved = await Promise.race([event, aborted.promise]); if (options?.signal?.aborted) return; while (runtimeMock.state.promptEchoEvents.length > 0) { yield runtimeMock.state.promptEchoEvents.shift(); } const nativeEvent = resolved as OpenCodeEvent; if (nativeEvent.type === "permission.asked") { runtimeMock.state.pendingPermissions = runtimeMock.state.pendingPermissions.filter( (request) => request.id !== nativeEvent.properties.id, ); runtimeMock.state.pendingPermissions.push(nativeEvent.properties); } else if (nativeEvent.type === "permission.replied") { runtimeMock.state.pendingPermissions = runtimeMock.state.pendingPermissions.filter( (request) => request.id !== nativeEvent.properties.requestID, ); } else if (nativeEvent.type === "question.asked") { runtimeMock.state.pendingQuestions = runtimeMock.state.pendingQuestions.filter( (request) => request.id !== nativeEvent.properties.id, ); runtimeMock.state.pendingQuestions.push(nativeEvent.properties); } else if ( nativeEvent.type === "question.replied" || nativeEvent.type === "question.rejected" ) { runtimeMock.state.pendingQuestions = runtimeMock.state.pendingQuestions.filter( (request) => request.id !== nativeEvent.properties.requestID, ); } yield resolved; } if (!runtimeMock.state.endEventStream && !options?.signal?.aborted) { await aborted.promise; } } finally { options?.signal?.removeEventListener("abort", onAbort); } })(), }; }, }, 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 }, options?: { signal?: AbortSignal }, ) => { runtimeMock.state.permissionReplyCalls.push({ requestID, reply }); if (options?.signal) runtimeMock.state.permissionReplySignals.push(options.signal); if (runtimeMock.state.permissionReplyImplementation) { await runtimeMock.state.permissionReplyImplementation(options?.signal); } runtimeMock.state.pendingPermissions = runtimeMock.state.pendingPermissions.filter( (request) => request.id !== requestID, ); }, }, 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>>; }, options?: { signal?: AbortSignal }, ) => { runtimeMock.state.questionReplyCalls.push({ requestID, answers }); await runtimeMock.state.questionReplyImplementation?.(options?.signal); runtimeMock.state.pendingQuestions = runtimeMock.state.pendingQuestions.filter( (request) => request.id !== requestID, ); }, }, }) as unknown as ReturnType<OpenCodeRuntimeShape["createOpenCodeSdkClient"]>,