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 @@
1import type { ChildProcess } from "node:child_process";
2
3import * as NodeServices from "@effect/platform-node/NodeServices";
4import { it } from "@effect/vitest";
5import { Duration, Effect, Layer } from "effect";
6import { TestClock } from "effect/testing";
7import { beforeEach, expect, vi } from "vitest";
8
9import { ServerConfig } from "../../config.ts";
10import { ServerSettingsService } from "../../serverSettings.ts";
11import { TextGeneration } from "../Services/TextGeneration.ts";
12import { OpenCodeTextGenerationLive } from "./OpenCodeTextGeneration.ts";
13
14const 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
35vi.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
82const DEFAULT_TEST_MODEL_SELECTION = {
83 provider: "opencode" as const,
84 model: "openai/gpt-5",
85};
86
87const OPENCODE_TEXT_GENERATION_IDLE_TTL_MS = 30_000;
88
89const 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
107const 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
127beforeEach(() => {
128 runtimeMock.reset();
129});
130
131const 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
137it.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
221it.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