History

feat: add opencode provider support (#1758)

Shoubhit Dash committed 7 months ago

Commit ce94fee1 parent 39ca3ee52 files · +4727 −228Browse files

Showing the changes to apps/server/src/git/Layers/OpenCodeTextGeneration.test.ts (1 of 52 files). Show all

Unified · Split

apps/server/src/git/Layers/OpenCodeTextGeneration.test.ts+259−0
@@ -0,0 +1,259 @@
1+import type { ChildProcess } from "node:child_process";
2+
3+import * as NodeServices from "@effect/platform-node/NodeServices";
4+import { it } from "@effect/vitest";
5+import { Duration, Effect, Layer } from "effect";
6+import { TestClock } from "effect/testing";
7+import { beforeEach, expect, vi } from "vitest";
8+
9+import { ServerConfig } from "../../config.ts";
10+import { ServerSettingsService } from "../../serverSettings.ts";
11+import { TextGeneration } from "../Services/TextGeneration.ts";
12+import { OpenCodeTextGenerationLive } from "./OpenCodeTextGeneration.ts";
13+
14+const runtimeMock = vi.hoisted(() => {
15+ const state = {
16+ startCalls: [] as string[],
17+ promptUrls: [] as string[],
18+ authHeaders: [] as Array<string | null>,
19+ closeCalls: [] as string[],
20+ promptResult: undefined as { data?: { info?: { structured?: unknown } } } | undefined,
21+ };
22+
23+ return {
24+ state,
25+ reset() {
26+ state.startCalls.length = 0;
27+ state.promptUrls.length = 0;
28+ state.authHeaders.length = 0;
29+ state.closeCalls.length = 0;
30+ state.promptResult = undefined;
31+ },
32+ };
33+});
34+
35+vi.mock("../../provider/opencodeRuntime.ts", async () => {
36+ const actual = await vi.importActual<typeof import("../../provider/opencodeRuntime.ts")>(
37+ "../../provider/opencodeRuntime.ts",
38+ );
39+
40+ return {
41+ ...actual,
42+ startOpenCodeServerProcess: vi.fn(async ({ binaryPath }: { binaryPath: string }) => {
43+ const index = runtimeMock.state.startCalls.length + 1;
44+ const url = `http://127.0.0.1:${4_300 + index}`;
45+ runtimeMock.state.startCalls.push(binaryPath);
46+ return {
47+ url,
48+ process: {} as ChildProcess,
49+ close: () => {
50+ runtimeMock.state.closeCalls.push(url);
51+ },
52+ };
53+ }),
54+ createOpenCodeSdkClient: vi.fn(
55+ ({ baseUrl, serverPassword }: { baseUrl: string; serverPassword?: string }) => ({
56+ session: {
57+ create: vi.fn(async () => ({ data: { id: `${baseUrl}/session` } })),
58+ prompt: vi.fn(async () => {
59+ runtimeMock.state.promptUrls.push(baseUrl);
60+ runtimeMock.state.authHeaders.push(
61+ serverPassword ? `Basic ${btoa(`opencode:${serverPassword}`)}` : null,
62+ );
63+ return (
64+ runtimeMock.state.promptResult ?? {
65+ data: {
66+ info: {
67+ structured: {
68+ subject: "Improve OpenCode reuse",
69+ body: "Reuse one server for the full action.",
70+ },
71+ },
72+ },
73+ }
74+ );
75+ }),
76+ },
77+ }),
78+ ),
79+ };
80+});
81+
82+const DEFAULT_TEST_MODEL_SELECTION = {
83+ provider: "opencode" as const,
84+ model: "openai/gpt-5",
85+};
86+
87+const OPENCODE_TEXT_GENERATION_IDLE_TTL_MS = 30_000;
88+
89+const OpenCodeTextGenerationTestLayer = OpenCodeTextGenerationLive.pipe(
90+ Layer.provideMerge(
91+ ServerSettingsService.layerTest({
92+ providers: {
93+ opencode: {
94+ binaryPath: "fake-opencode",
95+ },
96+ },
97+ }),
98+ ),
99+ Layer.provideMerge(
100+ ServerConfig.layerTest(process.cwd(), {
101+ prefix: "t3code-opencode-text-generation-test-",
102+ }),
103+ ),
104+ Layer.provideMerge(NodeServices.layer),
105+);
106+
107+const OpenCodeTextGenerationExistingServerTestLayer = OpenCodeTextGenerationLive.pipe(
108+ Layer.provideMerge(
109+ ServerSettingsService.layerTest({
110+ providers: {
111+ opencode: {
112+ binaryPath: "fake-opencode",
113+ serverUrl: "http://127.0.0.1:9999",
114+ serverPassword: "secret-password",
115+ },
116+ },
117+ }),
118+ ),
119+ Layer.provideMerge(
120+ ServerConfig.layerTest(process.cwd(), {
121+ prefix: "t3code-opencode-text-generation-existing-server-test-",
122+ }),
123+ ),
124+ Layer.provideMerge(NodeServices.layer),
125+);
126+
127+beforeEach(() => {
128+ runtimeMock.reset();
129+});
130+
131+const advanceIdleClock = Effect.gen(function* () {
132+ yield* Effect.yieldNow;
133+ yield* TestClock.adjust(Duration.millis(OPENCODE_TEXT_GENERATION_IDLE_TTL_MS + 1));
134+ yield* Effect.yieldNow;
135+});
136+
137+it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGenerationLive", (it) => {
138+ it.effect("reuses a warm server across back-to-back requests and closes it after idling", () =>
139+ Effect.gen(function* () {
140+ const textGeneration = yield* TextGeneration;
141+
142+ yield* textGeneration.generateCommitMessage({
143+ cwd: process.cwd(),
144+ branch: "feature/opencode-reuse",
145+ stagedSummary: "M README.md",
146+ stagedPatch: "diff --git a/README.md b/README.md",
147+ modelSelection: DEFAULT_TEST_MODEL_SELECTION,
148+ });
149+ yield* textGeneration.generateCommitMessage({
150+ cwd: process.cwd(),
151+ branch: "feature/opencode-reuse",
152+ stagedSummary: "M README.md",
153+ stagedPatch: "diff --git a/README.md b/README.md",
154+ modelSelection: DEFAULT_TEST_MODEL_SELECTION,
155+ });
156+
157+ expect(runtimeMock.state.startCalls).toEqual(["fake-opencode"]);
158+ expect(runtimeMock.state.promptUrls).toEqual([
159+ "http://127.0.0.1:4301",
160+ "http://127.0.0.1:4301",
161+ ]);
162+ expect(runtimeMock.state.closeCalls).toEqual([]);
163+
164+ yield* advanceIdleClock;
165+
166+ expect(runtimeMock.state.closeCalls).toEqual(["http://127.0.0.1:4301"]);
167+ }).pipe(Effect.provide(TestClock.layer())),
168+ );
169+
170+ it.effect("starts a new server after the warm server idles out", () =>
171+ Effect.gen(function* () {
172+ const textGeneration = yield* TextGeneration;
173+
174+ yield* textGeneration.generateCommitMessage({
175+ cwd: process.cwd(),
176+ branch: "feature/opencode-reuse",
177+ stagedSummary: "M README.md",
178+ stagedPatch: "diff --git a/README.md b/README.md",
179+ modelSelection: DEFAULT_TEST_MODEL_SELECTION,
180+ });
181+
182+ yield* advanceIdleClock;
183+
184+ yield* textGeneration.generateCommitMessage({
185+ cwd: process.cwd(),
186+ branch: "feature/opencode-reuse",
187+ stagedSummary: "M README.md",
188+ stagedPatch: "diff --git a/README.md b/README.md",
189+ modelSelection: DEFAULT_TEST_MODEL_SELECTION,
190+ });
191+
192+ expect(runtimeMock.state.startCalls).toEqual(["fake-opencode", "fake-opencode"]);
193+ expect(runtimeMock.state.promptUrls).toEqual([
194+ "http://127.0.0.1:4301",
195+ "http://127.0.0.1:4302",
196+ ]);
197+ expect(runtimeMock.state.closeCalls).toEqual(["http://127.0.0.1:4301"]);
198+ }).pipe(Effect.provide(TestClock.layer())),
199+ );
200+
201+ it.effect("returns a typed missing-output error when OpenCode omits info.structured", () =>
202+ Effect.gen(function* () {
203+ runtimeMock.state.promptResult = { data: {} };
204+ const textGeneration = yield* TextGeneration;
205+
206+ const error = yield* textGeneration
207+ .generateCommitMessage({
208+ cwd: process.cwd(),
209+ branch: "feature/opencode-reuse",
210+ stagedSummary: "M README.md",
211+ stagedPatch: "diff --git a/README.md b/README.md",
212+ modelSelection: DEFAULT_TEST_MODEL_SELECTION,
213+ })
214+ .pipe(Effect.flip);
215+
216+ expect(error.message).toContain("OpenCode returned no structured output.");
217+ }),
218+ );
219+});
220+
221+it.layer(OpenCodeTextGenerationExistingServerTestLayer)(
222+ "OpenCodeTextGenerationLive with configured server URL",
223+ (it) => {
224+ it.effect("reuses a configured OpenCode server URL without spawning or applying idle TTL", () =>
225+ Effect.gen(function* () {
226+ const textGeneration = yield* TextGeneration;
227+
228+ yield* textGeneration.generateCommitMessage({
229+ cwd: process.cwd(),
230+ branch: "feature/opencode-reuse",
231+ stagedSummary: "M README.md",
232+ stagedPatch: "diff --git a/README.md b/README.md",
233+ modelSelection: DEFAULT_TEST_MODEL_SELECTION,
234+ });
235+ yield* textGeneration.generateCommitMessage({
236+ cwd: process.cwd(),
237+ branch: "feature/opencode-reuse",
238+ stagedSummary: "M README.md",
239+ stagedPatch: "diff --git a/README.md b/README.md",
240+ modelSelection: DEFAULT_TEST_MODEL_SELECTION,
241+ });
242+
243+ expect(runtimeMock.state.startCalls).toEqual([]);
244+ expect(runtimeMock.state.promptUrls).toEqual([
245+ "http://127.0.0.1:9999",
246+ "http://127.0.0.1:9999",
247+ ]);
248+ expect(runtimeMock.state.authHeaders).toEqual([
249+ `Basic ${btoa("opencode:secret-password")}`,
250+ `Basic ${btoa("opencode:secret-password")}`,
251+ ]);
252+
253+ yield* advanceIdleClock;
254+
255+ expect(runtimeMock.state.closeCalls).toEqual([]);
256+ }).pipe(Effect.provide(TestClock.layer())),
257+ );
258+ },
259+);

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t