Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@effect/platform-bun": "catalog:",
"@effect/platform-node": "catalog:",
"@effect/sql-sqlite-bun": "catalog:",
"@opencode-ai/sdk": "^1.3.15",
"@pierre/diffs": "^1.1.0-beta.16",
"effect": "catalog:",
"node-pty": "^1.1.0",
Expand Down
224 changes: 224 additions & 0 deletions apps/server/src/git/Layers/OpenCodeTextGeneration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
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[],
closeCalls: [] as string[],
};

return {
state,
reset() {
state.startCalls.length = 0;
state.promptUrls.length = 0;
state.closeCalls.length = 0;
},
};
});

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 }: { baseUrl: string }) => ({
session: {
create: vi.fn(async () => ({ data: { id: `${baseUrl}/session` } })),
prompt: vi.fn(async () => {
runtimeMock.state.promptUrls.push(baseUrl);
return {
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",
},
},
}),
),
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.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",
]);

yield* advanceIdleClock;

expect(runtimeMock.state.closeCalls).toEqual([]);
}).pipe(Effect.provide(TestClock.layer())),
);
},
);
Loading
Loading