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

Lines 1–500 of 5024.