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, closeCalls: [] as string[], promptResult: undefined as { data?: { info?: { structured?: unknown } } } | 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( "../../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: { info: { structured: { 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 missing-output error when OpenCode omits info.structured", () => 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 no 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())), ); }, );