1377 lines · 50.9 KB · TypeScript
RawDownload
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.