import assert from "node:assert/strict";import * as NodeServices from "@effect/platform-node/NodeServices";import { it } from "@effect/vitest";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 Layer from "effect/Layer";import * as Option from "effect/Option";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 "vitest"; import { 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, 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>()( "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[], authHeaders: [] as Array<string | null>, abortCalls: [] as string[], closeCalls: [] as string[], revertCalls: [] as Array<{ sessionID: string; messageID?: string }>, promptCalls: [] as Array<unknown>, promptAsyncError: null as Error | null, closeError: null as Error | null, messages: [] as MessageEntry[], subscribedEvents: [] as unknown[], }, reset() { this.state.startCalls.length = 0; this.state.sessionCreateUrls.length = 0; this.state.authHeaders.length = 0; this.state.abortCalls.length = 0; this.state.closeCalls.length = 0; this.state.revertCalls.length = 0; this.state.promptCalls.length = 0; this.state.promptAsyncError = null; this.state.closeError = null; this.state.messages = []; this.state.subscribedEvents = []; },}; const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = { startOpenCodeServerProcess: ({ binaryPath }) => 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, exitCode: Effect.never, }; }), connectToOpenCodeServer: ({ serverUrl }) => Effect.gen(function* () { const url = serverUrl ?? "http://127.0.0.1:4301"; // Unconditionally register a scope finalizer for test observability — // preserves the `closeCalls` / `closeError` probes that the existing // suites rely on. Production code never attaches a finalizer to an // external server (it simply returns `Effect.succeed(...)`). yield* Effect.addFinalizer(() => Effect.sync(() => { runtimeMock.state.closeCalls.push(url); if (runtimeMock.state.closeError) { throw runtimeMock.state.closeError; } }), ); return { url, exitCode: null, external: Boolean(serverUrl), }; }), runOpenCodeCommand: () => Effect.succeed({ stdout: "", stderr: "", code: 0 }), createOpenCodeSdkClient: ({ baseUrl, serverPassword }) => ({ session: { create: async () => { runtimeMock.state.sessionCreateUrls.push(baseUrl); runtimeMock.state.authHeaders.push( serverPassword ? `Basic ${btoa(`opencode:${serverPassword}`)}` : null, ); return { data: { id: `${baseUrl}/session` } }; }, abort: async ({ sessionID }: { sessionID: string }) => { runtimeMock.state.abortCalls.push(sessionID); }, promptAsync: async (input: unknown) => { runtimeMock.state.promptCalls.push(input); if (runtimeMock.state.promptAsyncError) { throw runtimeMock.state.promptAsyncError; } }, messages: async () => ({ data: runtimeMock.state.messages }), 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 () => ({ stream: (async function* () { for (const event of runtimeMock.state.subscribedEvents) { yield event; } })(), }), }, }) as unknown as ReturnType<OpenCodeRuntimeShape["createOpenCodeSdkClient"]>, loadOpenCodeInventory: () => Effect.fail( new OpenCodeRuntimeError({ operation: "loadOpenCodeInventory", detail: "OpenCodeRuntimeTestDouble.loadOpenCodeInventory 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)); it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { it.effect("reuses a configured OpenCode server URL instead of spawning a local server", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const session = yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId: asThreadId("thread-opencode"), runtimeMode: "full-access", }); assert.equal(session.provider, "opencode"); assert.equal(session.threadId, "thread-opencode"); assert.deepEqual(runtimeMock.state.startCalls, []); assert.deepEqual(runtimeMock.state.sessionCreateUrls, ["http://127.0.0.1:9999"]); assert.deepEqual(runtimeMock.state.authHeaders, [ `Basic ${btoa("opencode:secret-password")}`, ]); }), ); it.effect("stops a configured-server session without trying to own server lifecycle", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId: asThreadId("thread-opencode"), runtimeMode: "full-access", }); yield* adapter.stopSession(asThreadId("thread-opencode")); assert.deepEqual(runtimeMock.state.startCalls, []); assert.deepEqual( runtimeMock.state.abortCalls.includes("http://127.0.0.1:9999/session"), true, ); }), ); it.effect("emits one session.exited event when stopping a session", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const threadId = asThreadId("thread-opencode-stop-event"); const eventsFiber = yield* adapter.streamEvents.pipe( Stream.filter((event) => event.threadId === threadId), Stream.take(3), Stream.runCollect, Effect.forkChild, ); yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId, runtimeMode: "full-access", }); yield* adapter.stopSession(threadId); const events = Array.from(yield* Fiber.join(eventsFiber).pipe(Effect.timeout("1 second"))); assert.deepEqual( events.map((event) => event.type), ["session.started", "thread.started", "session.exited"], ); }), ); it.effect("clears session state even when cleanup finalizers throw", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId: asThreadId("thread-stop-all-a"), runtimeMode: "full-access", }); yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId: asThreadId("thread-stop-all-b"), runtimeMode: "full-access", }); runtimeMock.state.closeError = new Error("close failed"); // `stopAll` relies on `stopOpenCodeContext`, which is typed as // never-failing. A throwing finalizer surfaces as a defect — `Effect.exit` // captures it so the assertions can still run. The key invariant we're // validating is "the sessions map and close-call probes reflect cleanup // attempts regardless of finalizer outcome". yield* Effect.exit(adapter.stopAll()); const sessions = yield* adapter.listSessions(); assert.deepEqual(runtimeMock.state.closeCalls, [ "http://127.0.0.1:9999", "http://127.0.0.1:9999", ]); assert.deepEqual(sessions, []); }), ); it.effect("completes streamEvents when the adapter scope closes", () => Effect.gen(function* () { const scope = yield* Scope.make("sequential"); let scopeClosed = false; try { const adapterLayer = Layer.effect( OpenCodeAdapter, makeOpenCodeAdapter(openCodeAdapterTestSettings), ).pipe( Layer.provideMerge(Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble)), Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), Layer.provideMerge(ServerSettingsService.layerTest()), Layer.provideMerge(providerSessionDirectoryTestLayer), Layer.provideMerge(NodeServices.layer), ); const context = yield* Layer.buildWithScope(adapterLayer, scope); const adapter = yield* Effect.service(OpenCodeAdapter).pipe(Effect.provide(context)); const eventsFiber = yield* adapter.streamEvents.pipe(Stream.runCollect, Effect.forkChild); yield* Scope.close(scope, Exit.void); scopeClosed = true; const exit = yield* Fiber.await(eventsFiber).pipe(Effect.timeout("1 second")); assert.equal(Exit.hasInterrupts(exit), true); } finally { if (!scopeClosed) { yield* Scope.close(scope, Exit.void).pipe(Effect.ignore); } } }), ); it.effect("rolls back session state when sendTurn fails before OpenCode accepts the prompt", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId: asThreadId("thread-send-turn-failure"), runtimeMode: "full-access", }); runtimeMock.state.promptAsyncError = new Error("prompt failed"); const error = yield* adapter .sendTurn({ threadId: asThreadId("thread-send-turn-failure"), input: "Fix it", modelSelection: { instanceId: ProviderInstanceId.make("opencode"), model: "openai/gpt-5", }, }) .pipe(Effect.flip); const sessions = yield* adapter.listSessions(); assert.equal(error._tag, "ProviderAdapterRequestError"); if (error._tag !== "ProviderAdapterRequestError") { throw new Error("Unexpected error type"); } assert.equal(error.detail, "prompt failed"); assert.equal( error.message, "Provider adapter request failed (opencode) for session.promptAsync: prompt failed", ); assert.equal(sessions.length, 1); assert.equal(sessions[0]?.status, "ready"); assert.equal(sessions[0]?.activeTurnId, undefined); assert.equal(sessions[0]?.lastError, "prompt failed"); }), ); it.effect("passes agent and variant options for the adapter's bound custom instance id", () => { const instanceId = ProviderInstanceId.make("opencode_zen"); const adapterLayer = Layer.effect( OpenCodeAdapter, makeOpenCodeAdapter(openCodeAdapterTestSettings, { instanceId }), ).pipe( Layer.provideMerge(Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble)), Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), Layer.provideMerge(ServerSettingsService.layerTest()), Layer.provideMerge(providerSessionDirectoryTestLayer), Layer.provideMerge(NodeServices.layer), ); return Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId: asThreadId("thread-custom-instance"), runtimeMode: "full-access", }); yield* adapter.sendTurn({ threadId: asThreadId("thread-custom-instance"), input: "Fix it", modelSelection: createModelSelection( ProviderInstanceId.make("opencode_zen"), "anthropic/claude-sonnet-4-5", [ { id: "agent", value: "github-copilot" }, { id: "variant", value: "high" }, ], ), }); assert.deepEqual(runtimeMock.state.promptCalls.at(-1), { sessionID: "http://127.0.0.1:9999/session", model: { providerID: "anthropic", modelID: "claude-sonnet-4-5", }, agent: "github-copilot", variant: "high", parts: [{ type: "text", text: "Fix it" }], }); }).pipe(Effect.provide(adapterLayer)); }); it.effect("uses the bound custom instance id for fallback sendTurn model selection", () => { const instanceId = ProviderInstanceId.make("opencode_zen"); const adapterLayer = Layer.effect( OpenCodeAdapter, makeOpenCodeAdapter(openCodeAdapterTestSettings, { instanceId }), ).pipe( Layer.provideMerge(Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble)), Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), Layer.provideMerge(ServerSettingsService.layerTest()), Layer.provideMerge(providerSessionDirectoryTestLayer), Layer.provideMerge(NodeServices.layer), ); return Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const threadId = asThreadId("thread-custom-instance-fallback-model"); yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId, runtimeMode: "full-access", modelSelection: createModelSelection( ProviderInstanceId.make("opencode_zen"), "anthropic/claude-sonnet-4-5", ), }); yield* adapter.sendTurn({ threadId, input: "Fix it", }); assert.deepEqual(runtimeMock.state.promptCalls.at(-1), { sessionID: "http://127.0.0.1:9999/session", model: { providerID: "anthropic", modelID: "claude-sonnet-4-5", }, parts: [{ type: "text", text: "Fix it" }], }); }).pipe(Effect.provide(adapterLayer)); }); it.effect("rejects sendTurn model selections for another instance id", () => { const instanceId = ProviderInstanceId.make("opencode_zen"); const adapterLayer = Layer.effect( OpenCodeAdapter, makeOpenCodeAdapter(openCodeAdapterTestSettings, { instanceId }), ).pipe( Layer.provideMerge(Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble)), Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), Layer.provideMerge(ServerSettingsService.layerTest()), Layer.provideMerge(providerSessionDirectoryTestLayer), Layer.provideMerge(NodeServices.layer), ); return Effect.gen(function* () {Lines 1–500 of 840. Load more
import assert from "node:assert/strict"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { it } from "@effect/vitest"; 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 Layer from "effect/Layer"; import * as Option from "effect/Option"; 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 "vitest"; import { 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, 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>()( "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[], authHeaders: [] as Array<string | null>, abortCalls: [] as string[], closeCalls: [] as string[], revertCalls: [] as Array<{ sessionID: string; messageID?: string }>, promptCalls: [] as Array<unknown>, promptAsyncError: null as Error | null, closeError: null as Error | null, messages: [] as MessageEntry[], subscribedEvents: [] as unknown[], }, reset() { this.state.startCalls.length = 0; this.state.sessionCreateUrls.length = 0; this.state.authHeaders.length = 0; this.state.abortCalls.length = 0; this.state.closeCalls.length = 0; this.state.revertCalls.length = 0; this.state.promptCalls.length = 0; this.state.promptAsyncError = null; this.state.closeError = null; this.state.messages = []; this.state.subscribedEvents = []; }, }; const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = { startOpenCodeServerProcess: ({ binaryPath }) => 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, exitCode: Effect.never, }; }), connectToOpenCodeServer: ({ serverUrl }) => Effect.gen(function* () { const url = serverUrl ?? "http://127.0.0.1:4301"; // Unconditionally register a scope finalizer for test observability — // preserves the `closeCalls` / `closeError` probes that the existing // suites rely on. Production code never attaches a finalizer to an // external server (it simply returns `Effect.succeed(...)`). yield* Effect.addFinalizer(() => Effect.sync(() => { runtimeMock.state.closeCalls.push(url); if (runtimeMock.state.closeError) { throw runtimeMock.state.closeError; } }), ); return { url, exitCode: null, external: Boolean(serverUrl), }; }), runOpenCodeCommand: () => Effect.succeed({ stdout: "", stderr: "", code: 0 }), createOpenCodeSdkClient: ({ baseUrl, serverPassword }) => ({ session: { create: async () => { runtimeMock.state.sessionCreateUrls.push(baseUrl); runtimeMock.state.authHeaders.push( serverPassword ? `Basic ${btoa(`opencode:${serverPassword}`)}` : null, ); return { data: { id: `${baseUrl}/session` } }; }, abort: async ({ sessionID }: { sessionID: string }) => { runtimeMock.state.abortCalls.push(sessionID); }, promptAsync: async (input: unknown) => { runtimeMock.state.promptCalls.push(input); if (runtimeMock.state.promptAsyncError) { throw runtimeMock.state.promptAsyncError; } }, messages: async () => ({ data: runtimeMock.state.messages }), 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 () => ({ stream: (async function* () { for (const event of runtimeMock.state.subscribedEvents) { yield event; } })(), }), }, }) as unknown as ReturnType<OpenCodeRuntimeShape["createOpenCodeSdkClient"]>, loadOpenCodeInventory: () => Effect.fail( new OpenCodeRuntimeError({ operation: "loadOpenCodeInventory", detail: "OpenCodeRuntimeTestDouble.loadOpenCodeInventory 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)); it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { it.effect("reuses a configured OpenCode server URL instead of spawning a local server", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const session = yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId: asThreadId("thread-opencode"), runtimeMode: "full-access", }); assert.equal(session.provider, "opencode"); assert.equal(session.threadId, "thread-opencode"); assert.deepEqual(runtimeMock.state.startCalls, []); assert.deepEqual(runtimeMock.state.sessionCreateUrls, ["http://127.0.0.1:9999"]); assert.deepEqual(runtimeMock.state.authHeaders, [ `Basic ${btoa("opencode:secret-password")}`, ]); }), ); it.effect("stops a configured-server session without trying to own server lifecycle", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId: asThreadId("thread-opencode"), runtimeMode: "full-access", }); yield* adapter.stopSession(asThreadId("thread-opencode")); assert.deepEqual(runtimeMock.state.startCalls, []); assert.deepEqual( runtimeMock.state.abortCalls.includes("http://127.0.0.1:9999/session"), true, ); }), ); it.effect("emits one session.exited event when stopping a session", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const threadId = asThreadId("thread-opencode-stop-event"); const eventsFiber = yield* adapter.streamEvents.pipe( Stream.filter((event) => event.threadId === threadId), Stream.take(3), Stream.runCollect, Effect.forkChild, ); yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId, runtimeMode: "full-access", }); yield* adapter.stopSession(threadId); const events = Array.from(yield* Fiber.join(eventsFiber).pipe(Effect.timeout("1 second"))); assert.deepEqual( events.map((event) => event.type), ["session.started", "thread.started", "session.exited"], ); }), ); it.effect("clears session state even when cleanup finalizers throw", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId: asThreadId("thread-stop-all-a"), runtimeMode: "full-access", }); yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId: asThreadId("thread-stop-all-b"), runtimeMode: "full-access", }); runtimeMock.state.closeError = new Error("close failed"); // `stopAll` relies on `stopOpenCodeContext`, which is typed as // never-failing. A throwing finalizer surfaces as a defect — `Effect.exit` // captures it so the assertions can still run. The key invariant we're // validating is "the sessions map and close-call probes reflect cleanup // attempts regardless of finalizer outcome". yield* Effect.exit(adapter.stopAll()); const sessions = yield* adapter.listSessions(); assert.deepEqual(runtimeMock.state.closeCalls, [ "http://127.0.0.1:9999", "http://127.0.0.1:9999", ]); assert.deepEqual(sessions, []); }), ); it.effect("completes streamEvents when the adapter scope closes", () => Effect.gen(function* () { const scope = yield* Scope.make("sequential"); let scopeClosed = false; try { const adapterLayer = Layer.effect( OpenCodeAdapter, makeOpenCodeAdapter(openCodeAdapterTestSettings), ).pipe( Layer.provideMerge(Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble)), Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), Layer.provideMerge(ServerSettingsService.layerTest()), Layer.provideMerge(providerSessionDirectoryTestLayer), Layer.provideMerge(NodeServices.layer), ); const context = yield* Layer.buildWithScope(adapterLayer, scope); const adapter = yield* Effect.service(OpenCodeAdapter).pipe(Effect.provide(context)); const eventsFiber = yield* adapter.streamEvents.pipe(Stream.runCollect, Effect.forkChild); yield* Scope.close(scope, Exit.void); scopeClosed = true; const exit = yield* Fiber.await(eventsFiber).pipe(Effect.timeout("1 second")); assert.equal(Exit.hasInterrupts(exit), true); } finally { if (!scopeClosed) { yield* Scope.close(scope, Exit.void).pipe(Effect.ignore); } } }), ); it.effect("rolls back session state when sendTurn fails before OpenCode accepts the prompt", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId: asThreadId("thread-send-turn-failure"), runtimeMode: "full-access", }); runtimeMock.state.promptAsyncError = new Error("prompt failed"); const error = yield* adapter .sendTurn({ threadId: asThreadId("thread-send-turn-failure"), input: "Fix it", modelSelection: { instanceId: ProviderInstanceId.make("opencode"), model: "openai/gpt-5", }, }) .pipe(Effect.flip); const sessions = yield* adapter.listSessions(); assert.equal(error._tag, "ProviderAdapterRequestError"); if (error._tag !== "ProviderAdapterRequestError") { throw new Error("Unexpected error type"); } assert.equal(error.detail, "prompt failed"); assert.equal( error.message, "Provider adapter request failed (opencode) for session.promptAsync: prompt failed", ); assert.equal(sessions.length, 1); assert.equal(sessions[0]?.status, "ready"); assert.equal(sessions[0]?.activeTurnId, undefined); assert.equal(sessions[0]?.lastError, "prompt failed"); }), ); it.effect("passes agent and variant options for the adapter's bound custom instance id", () => { const instanceId = ProviderInstanceId.make("opencode_zen"); const adapterLayer = Layer.effect( OpenCodeAdapter, makeOpenCodeAdapter(openCodeAdapterTestSettings, { instanceId }), ).pipe( Layer.provideMerge(Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble)), Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), Layer.provideMerge(ServerSettingsService.layerTest()), Layer.provideMerge(providerSessionDirectoryTestLayer), Layer.provideMerge(NodeServices.layer), ); return Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId: asThreadId("thread-custom-instance"), runtimeMode: "full-access", }); yield* adapter.sendTurn({ threadId: asThreadId("thread-custom-instance"), input: "Fix it", modelSelection: createModelSelection( ProviderInstanceId.make("opencode_zen"), "anthropic/claude-sonnet-4-5", [ { id: "agent", value: "github-copilot" }, { id: "variant", value: "high" }, ], ), }); assert.deepEqual(runtimeMock.state.promptCalls.at(-1), { sessionID: "http://127.0.0.1:9999/session", model: { providerID: "anthropic", modelID: "claude-sonnet-4-5", }, agent: "github-copilot", variant: "high", parts: [{ type: "text", text: "Fix it" }], }); }).pipe(Effect.provide(adapterLayer)); }); it.effect("uses the bound custom instance id for fallback sendTurn model selection", () => { const instanceId = ProviderInstanceId.make("opencode_zen"); const adapterLayer = Layer.effect( OpenCodeAdapter, makeOpenCodeAdapter(openCodeAdapterTestSettings, { instanceId }), ).pipe( Layer.provideMerge(Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble)), Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), Layer.provideMerge(ServerSettingsService.layerTest()), Layer.provideMerge(providerSessionDirectoryTestLayer), Layer.provideMerge(NodeServices.layer), ); return Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const threadId = asThreadId("thread-custom-instance-fallback-model"); yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId, runtimeMode: "full-access", modelSelection: createModelSelection( ProviderInstanceId.make("opencode_zen"), "anthropic/claude-sonnet-4-5", ), }); yield* adapter.sendTurn({ threadId, input: "Fix it", }); assert.deepEqual(runtimeMock.state.promptCalls.at(-1), { sessionID: "http://127.0.0.1:9999/session", model: { providerID: "anthropic", modelID: "claude-sonnet-4-5", }, parts: [{ type: "text", text: "Fix it" }], }); }).pipe(Effect.provide(adapterLayer)); }); it.effect("rejects sendTurn model selections for another instance id", () => { const instanceId = ProviderInstanceId.make("opencode_zen"); const adapterLayer = Layer.effect( OpenCodeAdapter, makeOpenCodeAdapter(openCodeAdapterTestSettings, { instanceId }), ).pipe( Layer.provideMerge(Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble)), Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), Layer.provideMerge(ServerSettingsService.layerTest()), Layer.provideMerge(providerSessionDirectoryTestLayer), Layer.provideMerge(NodeServices.layer), ); return Effect.gen(function* () {