324 lines · 10.2 KB · TypeScript
RawDownload
import type { ChildProcess } from "node:child_process"; import * as NodeServices from "@effect/platform-node/NodeServices";import { it } from "@effect/vitest";import { Duration, Effect, Layer } from "effect";import { TestClock } from "effect/testing";import { beforeEach, expect, vi } from "vitest"; import { ServerConfig } from "../../config.ts";import { ServerSettingsService } from "../../serverSettings.ts";import { TextGeneration } from "../Services/TextGeneration.ts";import { OpenCodeTextGenerationLive } from "./OpenCodeTextGeneration.ts"; const runtimeMock = vi.hoisted(() => {  const state = {    startCalls: [] as string[],    promptUrls: [] as string[],    authHeaders: [] as Array<string | null>,    closeCalls: [] as string[],    promptResult: undefined as      | { data?: { info?: { error?: unknown }; parts?: Array<{ type: string; text?: string }> } }      | undefined,  };   return {    state,    reset() {      state.startCalls.length = 0;      state.promptUrls.length = 0;      state.authHeaders.length = 0;      state.closeCalls.length = 0;      state.promptResult = undefined;    },  };}); vi.mock("../../provider/opencodeRuntime.ts", async () => {  const actual = await vi.importActual<typeof import("../../provider/opencodeRuntime.ts")>(    "../../provider/opencodeRuntime.ts",  );   return {    ...actual,    startOpenCodeServerProcess: vi.fn(async ({ binaryPath }: { binaryPath: string }) => {      const index = runtimeMock.state.startCalls.length + 1;      const url = `http://127.0.0.1:${4_300 + index}`;      runtimeMock.state.startCalls.push(binaryPath);      return {        url,        process: {} as ChildProcess,        close: () => {          runtimeMock.state.closeCalls.push(url);        },      };    }),    createOpenCodeSdkClient: vi.fn(      ({ baseUrl, serverPassword }: { baseUrl: string; serverPassword?: string }) => ({        session: {          create: vi.fn(async () => ({ data: { id: `${baseUrl}/session` } })),          prompt: vi.fn(async () => {            runtimeMock.state.promptUrls.push(baseUrl);            runtimeMock.state.authHeaders.push(              serverPassword ? `Basic ${btoa(`opencode:${serverPassword}`)}` : null,            );            return (              runtimeMock.state.promptResult ?? {                data: {                  parts: [                    {                      type: "text",                      text: JSON.stringify({                        subject: "Improve OpenCode reuse",                        body: "Reuse one server for the full action.",                      }),                    },                  ],                },              }            );          }),        },      }),    ),  };}); const DEFAULT_TEST_MODEL_SELECTION = {  provider: "opencode" as const,  model: "openai/gpt-5",}; const OPENCODE_TEXT_GENERATION_IDLE_TTL_MS = 30_000; const OpenCodeTextGenerationTestLayer = OpenCodeTextGenerationLive.pipe(  Layer.provideMerge(    ServerSettingsService.layerTest({      providers: {        opencode: {          binaryPath: "fake-opencode",        },      },    }),  ),  Layer.provideMerge(    ServerConfig.layerTest(process.cwd(), {      prefix: "t3code-opencode-text-generation-test-",    }),  ),  Layer.provideMerge(NodeServices.layer),); const OpenCodeTextGenerationExistingServerTestLayer = OpenCodeTextGenerationLive.pipe(  Layer.provideMerge(    ServerSettingsService.layerTest({      providers: {        opencode: {          binaryPath: "fake-opencode",          serverUrl: "http://127.0.0.1:9999",          serverPassword: "secret-password",        },      },    }),  ),  Layer.provideMerge(    ServerConfig.layerTest(process.cwd(), {      prefix: "t3code-opencode-text-generation-existing-server-test-",    }),  ),  Layer.provideMerge(NodeServices.layer),); beforeEach(() => {  runtimeMock.reset();}); const advanceIdleClock = Effect.gen(function* () {  yield* Effect.yieldNow;  yield* TestClock.adjust(Duration.millis(OPENCODE_TEXT_GENERATION_IDLE_TTL_MS + 1));  yield* Effect.yieldNow;}); it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGenerationLive", (it) => {  it.effect("reuses a warm server across back-to-back requests and closes it after idling", () =>    Effect.gen(function* () {      const textGeneration = yield* TextGeneration;       yield* textGeneration.generateCommitMessage({        cwd: process.cwd(),        branch: "feature/opencode-reuse",        stagedSummary: "M README.md",        stagedPatch: "diff --git a/README.md b/README.md",        modelSelection: DEFAULT_TEST_MODEL_SELECTION,      });      yield* textGeneration.generateCommitMessage({        cwd: process.cwd(),        branch: "feature/opencode-reuse",        stagedSummary: "M README.md",        stagedPatch: "diff --git a/README.md b/README.md",        modelSelection: DEFAULT_TEST_MODEL_SELECTION,      });       expect(runtimeMock.state.startCalls).toEqual(["fake-opencode"]);      expect(runtimeMock.state.promptUrls).toEqual([        "http://127.0.0.1:4301",        "http://127.0.0.1:4301",      ]);      expect(runtimeMock.state.closeCalls).toEqual([]);       yield* advanceIdleClock;       expect(runtimeMock.state.closeCalls).toEqual(["http://127.0.0.1:4301"]);    }).pipe(Effect.provide(TestClock.layer())),  );   it.effect("starts a new server after the warm server idles out", () =>    Effect.gen(function* () {      const textGeneration = yield* TextGeneration;       yield* textGeneration.generateCommitMessage({        cwd: process.cwd(),        branch: "feature/opencode-reuse",        stagedSummary: "M README.md",        stagedPatch: "diff --git a/README.md b/README.md",        modelSelection: DEFAULT_TEST_MODEL_SELECTION,      });       yield* advanceIdleClock;       yield* textGeneration.generateCommitMessage({        cwd: process.cwd(),        branch: "feature/opencode-reuse",        stagedSummary: "M README.md",        stagedPatch: "diff --git a/README.md b/README.md",        modelSelection: DEFAULT_TEST_MODEL_SELECTION,      });       expect(runtimeMock.state.startCalls).toEqual(["fake-opencode", "fake-opencode"]);      expect(runtimeMock.state.promptUrls).toEqual([        "http://127.0.0.1:4301",        "http://127.0.0.1:4302",      ]);      expect(runtimeMock.state.closeCalls).toEqual(["http://127.0.0.1:4301"]);    }).pipe(Effect.provide(TestClock.layer())),  );   it.effect("returns a typed empty-output error when OpenCode returns no text parts", () =>    Effect.gen(function* () {      runtimeMock.state.promptResult = { data: {} };      const textGeneration = yield* TextGeneration;       const error = yield* textGeneration        .generateCommitMessage({          cwd: process.cwd(),          branch: "feature/opencode-reuse",          stagedSummary: "M README.md",          stagedPatch: "diff --git a/README.md b/README.md",          modelSelection: DEFAULT_TEST_MODEL_SELECTION,        })        .pipe(Effect.flip);       expect(error.message).toContain("OpenCode returned empty output.");    }),  );   it.effect("parses JSON returned as plain text output", () =>    Effect.gen(function* () {      runtimeMock.state.promptResult = {        data: {          parts: [            {              type: "text",              text: 'Here is the result:\n{"subject":"Tighten OpenCode parsing","body":"Handle JSON text output locally."}',            },          ],        },      };      const textGeneration = yield* TextGeneration;       const result = yield* textGeneration.generateCommitMessage({        cwd: process.cwd(),        branch: "feature/opencode-reuse",        stagedSummary: "M README.md",        stagedPatch: "diff --git a/README.md b/README.md",        modelSelection: DEFAULT_TEST_MODEL_SELECTION,      });       expect(result).toEqual({        subject: "Tighten OpenCode parsing",        body: "Handle JSON text output locally.",      });    }),  );   it.effect("surfaces the upstream OpenCode structured-output error message", () =>    Effect.gen(function* () {      runtimeMock.state.promptResult = {        data: {          info: {            error: {              name: "StructuredOutputError",              data: {                message: "Model did not produce structured output",                retries: 2,              },            },          },        },      };      const textGeneration = yield* TextGeneration;       const error = yield* textGeneration        .generateCommitMessage({          cwd: process.cwd(),          branch: "feature/opencode-reuse",          stagedSummary: "M README.md",          stagedPatch: "diff --git a/README.md b/README.md",          modelSelection: DEFAULT_TEST_MODEL_SELECTION,        })        .pipe(Effect.flip);       expect(error.message).toContain("Model did not produce structured output");    }),  );}); it.layer(OpenCodeTextGenerationExistingServerTestLayer)(  "OpenCodeTextGenerationLive with configured server URL",  (it) => {    it.effect("reuses a configured OpenCode server URL without spawning or applying idle TTL", () =>      Effect.gen(function* () {        const textGeneration = yield* TextGeneration;         yield* textGeneration.generateCommitMessage({          cwd: process.cwd(),          branch: "feature/opencode-reuse",          stagedSummary: "M README.md",          stagedPatch: "diff --git a/README.md b/README.md",          modelSelection: DEFAULT_TEST_MODEL_SELECTION,        });        yield* textGeneration.generateCommitMessage({          cwd: process.cwd(),          branch: "feature/opencode-reuse",          stagedSummary: "M README.md",          stagedPatch: "diff --git a/README.md b/README.md",          modelSelection: DEFAULT_TEST_MODEL_SELECTION,        });         expect(runtimeMock.state.startCalls).toEqual([]);        expect(runtimeMock.state.promptUrls).toEqual([          "http://127.0.0.1:9999",          "http://127.0.0.1:9999",        ]);        expect(runtimeMock.state.authHeaders).toEqual([          `Basic ${btoa("opencode:secret-password")}`,          `Basic ${btoa("opencode:secret-password")}`,        ]);         yield* advanceIdleClock;         expect(runtimeMock.state.closeCalls).toEqual([]);      }).pipe(Effect.provide(TestClock.layer())),    );  },);