523 lines · 17.3 KB · TypeScript
RawDownload
import assert from "node:assert/strict"; import * as NodeServices from "@effect/platform-node/NodeServices";import { it } from "@effect/vitest";import { Effect, Layer, Option } from "effect";import { beforeEach } from "vitest"; import { ThreadId } from "@t3tools/contracts";import { ServerConfig } from "../../config.ts";import { ServerSettingsService } from "../../serverSettings.ts";import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts";import { OpenCodeAdapter } from "../Services/OpenCodeAdapter.ts";import {  OpenCodeRuntime,  OpenCodeRuntimeError,  type OpenCodeRuntimeShape,} from "../opencodeRuntime.ts";import {  appendOpenCodeAssistantTextDelta,  makeOpenCodeAdapterLive,  mergeOpenCodeAssistantText,} from "./OpenCodeAdapter.ts"; 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 }>,    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.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 () => {          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([]),}); const OpenCodeAdapterTestLayer = makeOpenCodeAdapterLive().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: "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: "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("clears session state even when cleanup finalizers throw", () =>    Effect.gen(function* () {      const adapter = yield* OpenCodeAdapter;      yield* adapter.startSession({        provider: "opencode",        threadId: asThreadId("thread-stop-all-a"),        runtimeMode: "full-access",      });      yield* adapter.startSession({        provider: "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("rolls back session state when sendTurn fails before OpenCode accepts the prompt", () =>    Effect.gen(function* () {      const adapter = yield* OpenCodeAdapter;      yield* adapter.startSession({        provider: "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: {            provider: "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("reverts the full thread when rollback removes every assistant turn", () =>    Effect.gen(function* () {      const adapter = yield* OpenCodeAdapter;      const threadId = asThreadId("thread-rollback-all");      yield* adapter.startSession({        provider: "opencode",        threadId,        runtimeMode: "full-access",      });       runtimeMock.state.messages = [        {          info: { id: "assistant-1", role: "assistant" },          parts: [],        },        {          info: { id: "assistant-2", role: "assistant" },          parts: [],        },      ];       const snapshot = yield* adapter.rollbackThread(threadId, 2);       assert.deepEqual(runtimeMock.state.revertCalls, [        { sessionID: "http://127.0.0.1:9999/session" },      ]);      assert.deepEqual(snapshot.turns, []);    }),  );   it.effect("deduplicates overlapping assistant text deltas after part updates", () =>    Effect.sync(() => {      const firstUpdate = mergeOpenCodeAssistantText(undefined, "Hello");      const overlapDelta = appendOpenCodeAssistantTextDelta(firstUpdate.latestText, "lo world");      const secondUpdate = mergeOpenCodeAssistantText(overlapDelta.nextText, "Hello world!");       assert.deepEqual(        [firstUpdate.deltaToEmit, overlapDelta.deltaToEmit, secondUpdate.deltaToEmit],        ["Hello", " world", "!"],      );      assert.equal(secondUpdate.latestText, "Hello world!");    }),  );   it.effect("writes provider-native observability records using the session thread id", () =>    Effect.gen(function* () {      const nativeEvents: Array<{        readonly event?: {          readonly provider?: string;          readonly threadId?: string;          readonly providerThreadId?: string;          readonly type?: string;        };      }> = [];      const nativeThreadIds: Array<string | null> = [];      runtimeMock.state.subscribedEvents = [        {          type: "message.updated",          properties: {            info: {              id: "msg-missing-session",              role: "assistant",            },          },        },        {          type: "message.updated",          properties: {            sessionID: "http://127.0.0.1:9999/other-session",            info: {              id: "msg-other-session",              role: "assistant",            },          },        },        {          type: "message.updated",          properties: {            sessionID: "http://127.0.0.1:9999/session",            info: {              id: "msg-native-log",              role: "assistant",            },          },        },      ];       const nativeEventLogger = {        filePath: "memory://opencode-native-events",        write: (event: unknown, threadId: ThreadId | null) => {          nativeEvents.push(event as (typeof nativeEvents)[number]);          nativeThreadIds.push(threadId ?? null);          return Effect.void;        },        close: () => Effect.void,      };       const adapterLayer = makeOpenCodeAdapterLive({        nativeEventLogger,      }).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),      );       const session = yield* Effect.gen(function* () {        const adapter = yield* OpenCodeAdapter;        const started = yield* adapter.startSession({          provider: "opencode",          threadId: asThreadId("thread-native-log"),          runtimeMode: "full-access",        });        yield* sleep(10);        return started;      }).pipe(Effect.provide(adapterLayer));       assert.equal(session.threadId, "thread-native-log");      assert.equal(nativeEvents.length, 1);      assert.equal(        nativeEvents.some((record) => record.event?.provider === "opencode"),        true,      );      assert.equal(        nativeEvents.some(          (record) => record.event?.providerThreadId === "http://127.0.0.1:9999/session",        ),        true,      );      assert.equal(        nativeEvents.some((record) => record.event?.threadId === "thread-native-log"),        true,      );      assert.equal(        nativeEvents.some((record) => record.event?.type === "message.updated"),        true,      );      assert.equal(        nativeThreadIds.every((threadId) => threadId === "thread-native-log"),        true,      );    }),  );   it.effect("keeps the event pump alive when native event logging fails", () =>    Effect.gen(function* () {      runtimeMock.state.subscribedEvents = [        {          type: "message.updated",          properties: {            sessionID: "http://127.0.0.1:9999/session",            info: {              id: "msg-native-log-failure",              role: "assistant",            },          },        },      ];       const nativeEventLogger = {        filePath: "memory://opencode-native-events",        write: () => Effect.die(new Error("native log write failed")),        close: () => Effect.void,      };       const adapterLayer = makeOpenCodeAdapterLive({        nativeEventLogger,      }).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),      );       // Capture closeCalls *inside* the provided layer scope: the adapter's      // layer finalizer now tears down any live sessions when the layer      // closes (which is exactly what we want for leak prevention), so

Lines 1–500 of 523.