import * as NodeAssert 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 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 { 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>>, 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[], sessionGetIds: [] as string[], missingSessionIds: new Set<string>(), transientErrorSessionIds: new Set<string>(), sessionDirectoryById: new Map<string, string>(), 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.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 = []; this.state.sessionGetIds.length = 0; this.state.missingSessionIds.clear(); this.state.transientErrorSessionIds.clear(); this.state.sessionDirectoryById.clear(); this.state.sessionUpdateCalls.length = 0; this.state.forkCalls.length = 0; },}; 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"; // 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, 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: `${baseUrl}/session` } }; }, get: async ({ sessionID }: { sessionID: string }) => { runtimeMock.state.sessionGetIds.push(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); return { data: { id: sessionID, ...(directory ? { directory } : {}) } }; }, 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 }) => { 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, }), ), 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)); 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", }); NodeAssert.equal(session.provider, "opencode"); NodeAssert.equal(session.threadId, "thread-opencode"); NodeAssert.deepEqual(runtimeMock.state.startCalls, []); NodeAssert.deepEqual(runtimeMock.state.sessionCreateUrls, ["http://127.0.0.1:9999"]); NodeAssert.deepEqual(runtimeMock.state.authHeaders, [ `Basic ${btoa("opencode:secret-password")}`, ]); }), ); it.effect("returns a durable resume cursor for a freshly created session", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const threadId = asThreadId("thread-opencode-cursor"); const session = yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId, runtimeMode: "full-access", }); // Without a persisted cursor, a session is created and its id is // surfaced as a resume cursor so the upper layer can persist it. NodeAssert.deepEqual(runtimeMock.state.sessionGetIds, []); NodeAssert.deepEqual(session.resumeCursor, { schemaVersion: 1, sessionId: "http://127.0.0.1:9999/session", }); yield* adapter.stopSession(threadId); }), ); it.effect("resumes the persisted OpenCode session instead of creating a new one", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const threadId = asThreadId("thread-opencode-resume"); const session = yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId, runtimeMode: "full-access", resumeCursor: { schemaVersion: 1, sessionId: "ses_persisted" }, }); // The adapter validates the persisted id with session.get and re-adopts // it — no new session is minted (issue #3604). NodeAssert.deepEqual(runtimeMock.state.sessionGetIds, ["ses_persisted"]); NodeAssert.deepEqual(runtimeMock.state.sessionCreateUrls, []); NodeAssert.deepEqual(session.resumeCursor, { schemaVersion: 1, sessionId: "ses_persisted", }); // Resume re-asserts the permission ruleset for the current runtimeMode. NodeAssert.equal(runtimeMock.state.sessionUpdateCalls.length, 1); NodeAssert.equal(runtimeMock.state.sessionUpdateCalls[0]?.sessionID, "ses_persisted"); NodeAssert.equal(runtimeMock.state.sessionUpdateCalls[0]?.permission != null, true); yield* adapter.stopSession(threadId); }), ); it.effect("sends follow-up turns to the resumed session id", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const threadId = asThreadId("thread-opencode-resume-turn"); yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId, runtimeMode: "full-access", resumeCursor: { schemaVersion: 1, sessionId: "ses_persisted" }, }); const result = yield* adapter.sendTurn({ threadId, input: "continue where we left off", modelSelection: createModelSelection( ProviderInstanceId.make("opencode"), "anthropic/sonnet", ), }); // The prompt targets the resumed id, and the turn re-surfaces the cursor. NodeAssert.deepEqual( (runtimeMock.state.promptCalls[0] as { sessionID: string }).sessionID, "ses_persisted", ); NodeAssert.deepEqual(result.resumeCursor, { schemaVersion: 1, sessionId: "ses_persisted", }); yield* adapter.stopSession(threadId); }), ); it.effect("falls back to a fresh session when the persisted session is gone", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const threadId = asThreadId("thread-opencode-stale"); runtimeMock.state.missingSessionIds.add("ses_stale"); const session = yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId, runtimeMode: "full-access", resumeCursor: { schemaVersion: 1, sessionId: "ses_stale" }, }); // get probed the stale id, found nothing, then created a new session and // emitted a fresh cursor rather than wedging the thread. NodeAssert.deepEqual(runtimeMock.state.sessionGetIds, ["ses_stale"]); NodeAssert.deepEqual(runtimeMock.state.sessionCreateUrls, ["http://127.0.0.1:9999"]); NodeAssert.deepEqual(session.resumeCursor, { schemaVersion: 1, sessionId: "http://127.0.0.1:9999/session", }); yield* adapter.stopSession(threadId); }), ); it.effect("ignores a malformed or wrong-version resume cursor", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const threadId = asThreadId("thread-opencode-badcursor"); const session = yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId, runtimeMode: "full-access", resumeCursor: { schemaVersion: 99, sessionId: "ses_persisted" }, }); // A foreign/stale-shaped cursor is treated as "no resume": never probed, // a fresh session is created. NodeAssert.deepEqual(runtimeMock.state.sessionGetIds, []); NodeAssert.deepEqual(runtimeMock.state.sessionCreateUrls, ["http://127.0.0.1:9999"]); NodeAssert.deepEqual(session.resumeCursor, { schemaVersion: 1, sessionId: "http://127.0.0.1:9999/session", }); yield* adapter.stopSession(threadId); }), ); it.effect("surfaces a non-not-found resume probe error instead of silently starting fresh", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const threadId = asThreadId("thread-opencode-transient"); // session.get returns a 500 (not a 404) for this id. runtimeMock.state.transientErrorSessionIds.add("ses_transient"); const exit = yield* Effect.exit( adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId, runtimeMode: "full-access", resumeCursor: { schemaVersion: 1, sessionId: "ses_transient" }, }), ); // A transient/transport/auth failure must propagate — NOT be masked as a // brand-new empty session (the #3604 class of silent context loss). NodeAssert.equal(Exit.isFailure(exit), true); NodeAssert.deepEqual(runtimeMock.state.sessionGetIds, ["ses_transient"]); NodeAssert.deepEqual(runtimeMock.state.sessionCreateUrls, []); }), ); it.effect("re-applies the current runtimeMode permissions when resuming", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const threadId = asThreadId("thread-opencode-perms"); yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), // A different runtimeMode than the original create — resume must not // leave the upstream session on stale permissions. runtimeMode: "approval-required", threadId, resumeCursor: { schemaVersion: 1, sessionId: "ses_perms" }, }); NodeAssert.deepEqual(runtimeMock.state.sessionGetIds, ["ses_perms"]); NodeAssert.deepEqual(runtimeMock.state.sessionCreateUrls, []); NodeAssert.equal(runtimeMock.state.sessionUpdateCalls.length, 1); NodeAssert.equal(runtimeMock.state.sessionUpdateCalls[0]?.sessionID, "ses_perms"); NodeAssert.equal(runtimeMock.state.sessionUpdateCalls[0]?.permission != null, true); yield* adapter.stopSession(threadId); }), ); it.effect( "forks the resumed session into the requested directory instead of losing context", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const threadId = asThreadId("thread-opencode-cwd"); // The persisted session still exists but was created in another working dir // (e.g. the thread moved from the project root into a git worktree). runtimeMock.state.sessionDirectoryById.set("ses_otherdir", "/some/other/worktree"); const session = yield* adapter.startSession({Lines 1–500 of 1377. Load more
import * as NodeAssert 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 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 { 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>>, 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[], sessionGetIds: [] as string[], missingSessionIds: new Set<string>(), transientErrorSessionIds: new Set<string>(), sessionDirectoryById: new Map<string, string>(), 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.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 = []; this.state.sessionGetIds.length = 0; this.state.missingSessionIds.clear(); this.state.transientErrorSessionIds.clear(); this.state.sessionDirectoryById.clear(); this.state.sessionUpdateCalls.length = 0; this.state.forkCalls.length = 0; }, }; 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"; // 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, 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: `${baseUrl}/session` } }; }, get: async ({ sessionID }: { sessionID: string }) => { runtimeMock.state.sessionGetIds.push(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); return { data: { id: sessionID, ...(directory ? { directory } : {}) } }; }, 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 }) => { 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, }), ), 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)); 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", }); NodeAssert.equal(session.provider, "opencode"); NodeAssert.equal(session.threadId, "thread-opencode"); NodeAssert.deepEqual(runtimeMock.state.startCalls, []); NodeAssert.deepEqual(runtimeMock.state.sessionCreateUrls, ["http://127.0.0.1:9999"]); NodeAssert.deepEqual(runtimeMock.state.authHeaders, [ `Basic ${btoa("opencode:secret-password")}`, ]); }), ); it.effect("returns a durable resume cursor for a freshly created session", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const threadId = asThreadId("thread-opencode-cursor"); const session = yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId, runtimeMode: "full-access", }); // Without a persisted cursor, a session is created and its id is // surfaced as a resume cursor so the upper layer can persist it. NodeAssert.deepEqual(runtimeMock.state.sessionGetIds, []); NodeAssert.deepEqual(session.resumeCursor, { schemaVersion: 1, sessionId: "http://127.0.0.1:9999/session", }); yield* adapter.stopSession(threadId); }), ); it.effect("resumes the persisted OpenCode session instead of creating a new one", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const threadId = asThreadId("thread-opencode-resume"); const session = yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId, runtimeMode: "full-access", resumeCursor: { schemaVersion: 1, sessionId: "ses_persisted" }, }); // The adapter validates the persisted id with session.get and re-adopts // it — no new session is minted (issue #3604). NodeAssert.deepEqual(runtimeMock.state.sessionGetIds, ["ses_persisted"]); NodeAssert.deepEqual(runtimeMock.state.sessionCreateUrls, []); NodeAssert.deepEqual(session.resumeCursor, { schemaVersion: 1, sessionId: "ses_persisted", }); // Resume re-asserts the permission ruleset for the current runtimeMode. NodeAssert.equal(runtimeMock.state.sessionUpdateCalls.length, 1); NodeAssert.equal(runtimeMock.state.sessionUpdateCalls[0]?.sessionID, "ses_persisted"); NodeAssert.equal(runtimeMock.state.sessionUpdateCalls[0]?.permission != null, true); yield* adapter.stopSession(threadId); }), ); it.effect("sends follow-up turns to the resumed session id", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const threadId = asThreadId("thread-opencode-resume-turn"); yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId, runtimeMode: "full-access", resumeCursor: { schemaVersion: 1, sessionId: "ses_persisted" }, }); const result = yield* adapter.sendTurn({ threadId, input: "continue where we left off", modelSelection: createModelSelection( ProviderInstanceId.make("opencode"), "anthropic/sonnet", ), }); // The prompt targets the resumed id, and the turn re-surfaces the cursor. NodeAssert.deepEqual( (runtimeMock.state.promptCalls[0] as { sessionID: string }).sessionID, "ses_persisted", ); NodeAssert.deepEqual(result.resumeCursor, { schemaVersion: 1, sessionId: "ses_persisted", }); yield* adapter.stopSession(threadId); }), ); it.effect("falls back to a fresh session when the persisted session is gone", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const threadId = asThreadId("thread-opencode-stale"); runtimeMock.state.missingSessionIds.add("ses_stale"); const session = yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId, runtimeMode: "full-access", resumeCursor: { schemaVersion: 1, sessionId: "ses_stale" }, }); // get probed the stale id, found nothing, then created a new session and // emitted a fresh cursor rather than wedging the thread. NodeAssert.deepEqual(runtimeMock.state.sessionGetIds, ["ses_stale"]); NodeAssert.deepEqual(runtimeMock.state.sessionCreateUrls, ["http://127.0.0.1:9999"]); NodeAssert.deepEqual(session.resumeCursor, { schemaVersion: 1, sessionId: "http://127.0.0.1:9999/session", }); yield* adapter.stopSession(threadId); }), ); it.effect("ignores a malformed or wrong-version resume cursor", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const threadId = asThreadId("thread-opencode-badcursor"); const session = yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId, runtimeMode: "full-access", resumeCursor: { schemaVersion: 99, sessionId: "ses_persisted" }, }); // A foreign/stale-shaped cursor is treated as "no resume": never probed, // a fresh session is created. NodeAssert.deepEqual(runtimeMock.state.sessionGetIds, []); NodeAssert.deepEqual(runtimeMock.state.sessionCreateUrls, ["http://127.0.0.1:9999"]); NodeAssert.deepEqual(session.resumeCursor, { schemaVersion: 1, sessionId: "http://127.0.0.1:9999/session", }); yield* adapter.stopSession(threadId); }), ); it.effect("surfaces a non-not-found resume probe error instead of silently starting fresh", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const threadId = asThreadId("thread-opencode-transient"); // session.get returns a 500 (not a 404) for this id. runtimeMock.state.transientErrorSessionIds.add("ses_transient"); const exit = yield* Effect.exit( adapter.startSession({ provider: ProviderDriverKind.make("opencode"), threadId, runtimeMode: "full-access", resumeCursor: { schemaVersion: 1, sessionId: "ses_transient" }, }), ); // A transient/transport/auth failure must propagate — NOT be masked as a // brand-new empty session (the #3604 class of silent context loss). NodeAssert.equal(Exit.isFailure(exit), true); NodeAssert.deepEqual(runtimeMock.state.sessionGetIds, ["ses_transient"]); NodeAssert.deepEqual(runtimeMock.state.sessionCreateUrls, []); }), ); it.effect("re-applies the current runtimeMode permissions when resuming", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const threadId = asThreadId("thread-opencode-perms"); yield* adapter.startSession({ provider: ProviderDriverKind.make("opencode"), // A different runtimeMode than the original create — resume must not // leave the upstream session on stale permissions. runtimeMode: "approval-required", threadId, resumeCursor: { schemaVersion: 1, sessionId: "ses_perms" }, }); NodeAssert.deepEqual(runtimeMock.state.sessionGetIds, ["ses_perms"]); NodeAssert.deepEqual(runtimeMock.state.sessionCreateUrls, []); NodeAssert.equal(runtimeMock.state.sessionUpdateCalls.length, 1); NodeAssert.equal(runtimeMock.state.sessionUpdateCalls[0]?.sessionID, "ses_perms"); NodeAssert.equal(runtimeMock.state.sessionUpdateCalls[0]?.permission != null, true); yield* adapter.stopSession(threadId); }), ); it.effect( "forks the resumed session into the requested directory instead of losing context", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; const threadId = asThreadId("thread-opencode-cwd"); // The persisted session still exists but was created in another working dir // (e.g. the thread moved from the project root into a git worktree). runtimeMock.state.sessionDirectoryById.set("ses_otherdir", "/some/other/worktree"); const session = yield* adapter.startSession({