History

fix(opencode): handle child approvals, stops, and model catalogs (#8480)

Theo Browne committed 2 months agov0.0.36-nightly.20260828.1213

Commit cb49e5d1 parent 0009aac42 files · +7983 −675Browse files

Showing the changes to apps/server/src/textGeneration/OpenCodeTextGeneration.test.ts (1 of 42 files). Show all

Unified · Split

apps/server/src/textGeneration/OpenCodeTextGeneration.test.ts+124−8
@@ -11,6 +11,7 @@ import { beforeEach, expect } from "vite-plus/test"; · 10 unmodified lines
11 11
12import * as ServerConfig from "../config.ts";12import * as ServerConfig from "../config.ts";
13import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts";13import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts";
14import * as OpenCodeServerOwner from "../provider/OpenCodeServerOwner.ts";
14import * as OpenCodeTextGeneration from "./OpenCodeTextGeneration.ts";15import * as OpenCodeTextGeneration from "./OpenCodeTextGeneration.ts";
15import * as TextGeneration from "./TextGeneration.ts";16import * as TextGeneration from "./TextGeneration.ts";
16 17
@@ -21,6 +22,8 @@ const runtimeMock = { · 4 unmodified lines
21 promptParts: [] as ReadonlyArray<unknown>[],22 promptParts: [] as ReadonlyArray<unknown>[],
22 authHeaders: [] as Array<string | null>,23 authHeaders: [] as Array<string | null>,
23 closeCalls: [] as string[],24 closeCalls: [] as string[],
25 sessionCreateCalls: 0,
26 connectionError: undefined as Error | undefined,
24 sessionCreateError: undefined as unknown,27 sessionCreateError: undefined as unknown,
25 sessionResult: undefined as { data?: { id: string } } | undefined,28 sessionResult: undefined as { data?: { id: string } } | undefined,
26 promptRequestError: undefined as unknown,29 promptRequestError: undefined as unknown,
@@ -34,6 +37,8 @@ const runtimeMock = { · 7 unmodified lines
34 this.state.promptParts.length = 0;37 this.state.promptParts.length = 0;
35 this.state.authHeaders.length = 0;38 this.state.authHeaders.length = 0;
36 this.state.closeCalls.length = 0;39 this.state.closeCalls.length = 0;
40 this.state.sessionCreateCalls = 0;
41 this.state.connectionError = undefined;
37 this.state.sessionCreateError = undefined;42 this.state.sessionCreateError = undefined;
38 this.state.sessionResult = undefined;43 this.state.sessionResult = undefined;
39 this.state.promptRequestError = undefined;44 this.state.promptRequestError = undefined;
@@ -42,7 +47,7 @@ const runtimeMock = { · 2 unmodified lines
42};47};
43 48
44const OpenCodeRuntimeTestDouble: OpenCodeRuntime.OpenCodeRuntimeShape = {49const OpenCodeRuntimeTestDouble: OpenCodeRuntime.OpenCodeRuntimeShape = {
45 startOpenCodeServerProcess: ({ binaryPath }) =>50 startOpenCodeServerProcess: ({ binaryPath, serverPassword, environment }) =>
46 Effect.gen(function* () {51 Effect.gen(function* () {
47 const index = runtimeMock.state.startCalls.length + 1;52 const index = runtimeMock.state.startCalls.length + 1;
48 const url = `http://127.0.0.1:${4_300 + index}`;53 const url = `http://127.0.0.1:${4_300 + index}`;
@@ -54,22 +59,43 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntime.OpenCodeRuntimeShape = { · 5 unmodified lines
54 runtimeMock.state.closeCalls.push(url);59 runtimeMock.state.closeCalls.push(url);
55 }),60 }),
56 );61 );
62 const effectiveServerPassword = OpenCodeRuntime.resolveOpenCodeServerPassword({
63 external: false,
64 ...(serverPassword !== undefined ? { serverPassword } : {}),
65 ...(environment !== undefined ? { environment } : {}),
66 });
57 return {67 return {
58 url,68 url,
69 ...(effectiveServerPassword !== undefined
70 ? { serverPassword: effectiveServerPassword }
71 : {}),
72 version: "1.14.19",
73 isRunning: Effect.succeed(true),
59 exitCode: Effect.never,74 exitCode: Effect.never,
60 };75 };
61 }),76 }),
62 connectToOpenCodeServer: ({ serverUrl }) =>77 connectToOpenCodeServer: ({ serverUrl, serverPassword }) =>
63 Effect.succeed({78 runtimeMock.state.connectionError
64 url: serverUrl ?? "http://127.0.0.1:4301",79 ? Effect.fail(
65 exitCode: null,80 new OpenCodeRuntime.OpenCodeRuntimeError({
66 external: Boolean(serverUrl),81 operation: "global.health",
67 }),82 detail: runtimeMock.state.connectionError.message,
83 cause: runtimeMock.state.connectionError,
84 }),
85 )
86 : Effect.succeed({
87 url: serverUrl ?? "http://127.0.0.1:4301",
88 ...(serverPassword ? { serverPassword } : {}),
89 version: "1.14.19",
90 exitCode: null,
91 external: Boolean(serverUrl),
92 }),
68 runOpenCodeCommand: () => Effect.succeed({ stdout: "", stderr: "", code: 0 }),93 runOpenCodeCommand: () => Effect.succeed({ stdout: "", stderr: "", code: 0 }),
69 createOpenCodeSdkClient: ({ baseUrl, serverPassword }) =>94 createOpenCodeSdkClient: ({ baseUrl, serverPassword }) =>
70 ({95 ({
71 session: {96 session: {
72 create: async () => {97 create: async () => {
98 runtimeMock.state.sessionCreateCalls += 1;
73 if (runtimeMock.state.sessionCreateError !== undefined) {99 if (runtimeMock.state.sessionCreateError !== undefined) {
74 throw runtimeMock.state.sessionCreateError;100 throw runtimeMock.state.sessionCreateError;
75 }101 }
@@ -163,18 +189,35 @@ const OpenCodeTextGenerationExistingServerTestLayer = Layer.succeed( · 87 unmodified lines
163const DEFAULT_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettings)({189const DEFAULT_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettings)({
164 binaryPath: "fake-opencode",190 binaryPath: "fake-opencode",
165});191});
192const LOCAL_AUTH_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettings)({
193 binaryPath: "fake-opencode",
194 serverPassword: "secret-password",
195});
166const EXISTING_SERVER_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettings)({196const EXISTING_SERVER_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettings)({
167 binaryPath: "fake-opencode",197 binaryPath: "fake-opencode",
168 serverUrl: "http://127.0.0.1:9999",198 serverUrl: "http://127.0.0.1:9999",
169 serverPassword: "secret-password",199 serverPassword: "secret-password",
170});200});
201const EXTERNAL_SERVER_WITHOUT_AUTH_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettings)({
202 binaryPath: "fake-opencode",
203 serverUrl: "http://127.0.0.1:9999",
204});
171 205
172function withOpenCodeTextGeneration<A, E, R>(206function withOpenCodeTextGeneration<A, E, R>(
173 settings: OpenCodeSettings,207 settings: OpenCodeSettings,
174 effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>,208 effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>,
209 environment?: NodeJS.ProcessEnv,
175) {210) {
176 return Effect.gen(function* () {211 return Effect.gen(function* () {
177 const textGeneration = yield* OpenCodeTextGeneration.makeOpenCodeTextGeneration(settings);212 const serverOwner = yield* OpenCodeServerOwner.make({
213 binaryPath: settings.binaryPath,
214 directory: process.cwd(),
215 ...(settings.serverPassword ? { serverPassword: settings.serverPassword } : {}),
216 ...(environment ? { environment } : {}),
217 });
218 const textGeneration = yield* OpenCodeTextGeneration.makeOpenCodeTextGeneration(settings).pipe(
219 Effect.provideService(OpenCodeServerOwner.OpenCodeServerOwner, serverOwner),
220 );
178 return yield* effectFn(textGeneration);221 return yield* effectFn(textGeneration);
179 }).pipe(Effect.scoped);222 }).pipe(Effect.scoped);
180}223}
@@ -229,6 +272,49 @@ it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGeneration", (it) => { · 48 unmodified lines
229 ),272 ),
230 );273 );
231 274
275 it.effect("passes configured authentication to a locally spawned server", () =>
276 withOpenCodeTextGeneration(LOCAL_AUTH_OPENCODE_SETTINGS, (textGeneration) =>
277 Effect.gen(function* () {
278 yield* textGeneration.generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT);
279
280 expect(runtimeMock.state.startCalls).toEqual(["fake-opencode"]);
281 expect(runtimeMock.state.authHeaders).toEqual([
282 `Basic ${btoa("opencode:secret-password")}`,
283 ]);
284 }),
285 ),
286 );
287
288 it.effect("uses an environment-only password for a locally spawned server", () =>
289 withOpenCodeTextGeneration(
290 DEFAULT_OPENCODE_SETTINGS,
291 (textGeneration) =>
292 Effect.gen(function* () {
293 yield* textGeneration.generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT);
294
295 expect(runtimeMock.state.authHeaders).toEqual([
296 `Basic ${btoa("opencode:environment-password")}`,
297 ]);
298 }),
299 { OPENCODE_SERVER_PASSWORD: "environment-password" },
300 ),
301 );
302
303 it.effect("uses settings auth when the local environment password differs", () =>
304 withOpenCodeTextGeneration(
305 LOCAL_AUTH_OPENCODE_SETTINGS,
306 (textGeneration) =>
307 Effect.gen(function* () {
308 yield* textGeneration.generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT);
309
310 expect(runtimeMock.state.authHeaders).toEqual([
311 `Basic ${btoa("opencode:secret-password")}`,
312 ]);
313 }),
314 { OPENCODE_SERVER_PASSWORD: "environment-password" },
315 ),
316 );
317
232 it.effect("reuses a warm server across back-to-back requests and closes it after idling", () =>318 it.effect("reuses a warm server across back-to-back requests and closes it after idling", () =>
233 withOpenCodeTextGeneration(DEFAULT_OPENCODE_SETTINGS, (textGeneration) =>319 withOpenCodeTextGeneration(DEFAULT_OPENCODE_SETTINGS, (textGeneration) =>
234 Effect.gen(function* () {320 Effect.gen(function* () {
@@ -460,6 +546,36 @@ it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGeneration", (it) => { · 225 unmodified lines
460it.layer(OpenCodeTextGenerationExistingServerTestLayer)(546it.layer(OpenCodeTextGenerationExistingServerTestLayer)(
461 "OpenCodeTextGeneration with configured server URL",547 "OpenCodeTextGeneration with configured server URL",
462 (it) => {548 (it) => {
549 it.effect("does not send a local environment password to a configured server", () =>
550 withOpenCodeTextGeneration(
551 EXTERNAL_SERVER_WITHOUT_AUTH_OPENCODE_SETTINGS,
552 (textGeneration) =>
553 Effect.gen(function* () {
554 yield* textGeneration.generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT);
555 expect(runtimeMock.state.authHeaders).toEqual([null]);
556 }),
557 { OPENCODE_SERVER_PASSWORD: "local-secret" },
558 ),
559 );
560
561 it.effect("does not create a session when the server version is unsupported", () =>
562 withOpenCodeTextGeneration(EXISTING_SERVER_OPENCODE_SETTINGS, (textGeneration) =>
563 Effect.gen(function* () {
564 runtimeMock.state.connectionError = new Error(
565 "OpenCode v1.14.18 is too old. Upgrade to v1.14.19 or newer.",
566 );
567
568 const error = yield* textGeneration
569 .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT)
570 .pipe(Effect.flip);
571
572 expect(error).toBeInstanceOf(TextGenerationError);
573 expect(error.message).toContain("v1.14.18 is too old");
574 expect(runtimeMock.state.sessionCreateCalls).toBe(0);
575 }),
576 ),
577 );
578
463 it.effect("reuses a configured OpenCode server URL without spawning or applying idle TTL", () =>579 it.effect("reuses a configured OpenCode server URL without spawning or applying idle TTL", () =>
464 withOpenCodeTextGeneration(EXISTING_SERVER_OPENCODE_SETTINGS, (textGeneration) =>580 withOpenCodeTextGeneration(EXISTING_SERVER_OPENCODE_SETTINGS, (textGeneration) =>
465 Effect.gen(function* () {581 Effect.gen(function* () {