767 lines · 26.2 KB · TypeScript
RawDownload
import assert from "node:assert/strict"; import * as NodeServices from "@effect/platform-node/NodeServices";import { it } from "@effect/vitest";import { Context, Effect, Exit, Fiber, Layer, Option, Schema, Scope, Stream } from "effect";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,  Effect.gen(function* () {    return yield* 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 sleep = (ms: number) =>  Effect.promise(() => new Promise<void>((resolve) => setTimeout(resolve, ms))); 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 customInstanceId = ProviderInstanceId.make("opencode_zen");    const adapterLayer = Layer.effect(      OpenCodeAdapter,      Effect.gen(function* () {        return yield* makeOpenCodeAdapter(openCodeAdapterTestSettings, {          instanceId: customInstanceId,        });      }),    ).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 customInstanceId = ProviderInstanceId.make("opencode_zen");    const adapterLayer = Layer.effect(      OpenCodeAdapter,      Effect.gen(function* () {        return yield* makeOpenCodeAdapter(openCodeAdapterTestSettings, {          instanceId: customInstanceId,        });      }),    ).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 customInstanceId = ProviderInstanceId.make("opencode_zen");    const adapterLayer = Layer.effect(      OpenCodeAdapter,      Effect.gen(function* () {        return yield* makeOpenCodeAdapter(openCodeAdapterTestSettings, {          instanceId: customInstanceId,        });      }),    ).pipe(      Layer.provideMerge(Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble)),      Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),

Lines 1–500 of 767.