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
1111
1212 import * as ServerConfig from "../config.ts";
1313 import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts";
14+import * as OpenCodeServerOwner from "../provider/OpenCodeServerOwner.ts";
1415 import * as OpenCodeTextGeneration from "./OpenCodeTextGeneration.ts";
1516 import * as TextGeneration from "./TextGeneration.ts";
1617
@@ -21,6 +22,8 @@ const runtimeMock = { · 4 unmodified lines
2122 promptParts: [] as ReadonlyArray<unknown>[],
2223 authHeaders: [] as Array<string | null>,
2324 closeCalls: [] as string[],
25+ sessionCreateCalls: 0,
26+ connectionError: undefined as Error | undefined,
2427 sessionCreateError: undefined as unknown,
2528 sessionResult: undefined as { data?: { id: string } } | undefined,
2629 promptRequestError: undefined as unknown,
@@ -34,6 +37,8 @@ const runtimeMock = { · 7 unmodified lines
3437 this.state.promptParts.length = 0;
3538 this.state.authHeaders.length = 0;
3639 this.state.closeCalls.length = 0;
40+ this.state.sessionCreateCalls = 0;
41+ this.state.connectionError = undefined;
3742 this.state.sessionCreateError = undefined;
3843 this.state.sessionResult = undefined;
3944 this.state.promptRequestError = undefined;
@@ -42,7 +47,7 @@ const runtimeMock = { · 2 unmodified lines
4247 };
4348
4449 const OpenCodeRuntimeTestDouble: OpenCodeRuntime.OpenCodeRuntimeShape = {
45− startOpenCodeServerProcess: ({ binaryPath }) =>
50+ startOpenCodeServerProcess: ({ binaryPath, serverPassword, environment }) =>
4651 Effect.gen(function* () {
4752 const index = runtimeMock.state.startCalls.length + 1;
4853 const url = `http://127.0.0.1:${4_300 + index}`;
@@ -54,22 +59,43 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntime.OpenCodeRuntimeShape = { · 5 unmodified lines
5459 runtimeMock.state.closeCalls.push(url);
5560 }),
5661 );
62+ const effectiveServerPassword = OpenCodeRuntime.resolveOpenCodeServerPassword({
63+ external: false,
64+ ...(serverPassword !== undefined ? { serverPassword } : {}),
65+ ...(environment !== undefined ? { environment } : {}),
66+ });
5767 return {
5868 url,
69+ ...(effectiveServerPassword !== undefined
70+ ? { serverPassword: effectiveServerPassword }
71+ : {}),
72+ version: "1.14.19",
73+ isRunning: Effect.succeed(true),
5974 exitCode: Effect.never,
6075 };
6176 }),
62− connectToOpenCodeServer: ({ serverUrl }) =>
63− Effect.succeed({
64− url: serverUrl ?? "http://127.0.0.1:4301",
65− exitCode: null,
66− external: Boolean(serverUrl),
67− }),
77+ connectToOpenCodeServer: ({ serverUrl, serverPassword }) =>
78+ runtimeMock.state.connectionError
79+ ? Effect.fail(
80+ new OpenCodeRuntime.OpenCodeRuntimeError({
81+ operation: "global.health",
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+ }),
6893 runOpenCodeCommand: () => Effect.succeed({ stdout: "", stderr: "", code: 0 }),
6994 createOpenCodeSdkClient: ({ baseUrl, serverPassword }) =>
7095 ({
7196 session: {
7297 create: async () => {
98+ runtimeMock.state.sessionCreateCalls += 1;
7399 if (runtimeMock.state.sessionCreateError !== undefined) {
74100 throw runtimeMock.state.sessionCreateError;
75101 }
@@ -163,18 +189,35 @@ const OpenCodeTextGenerationExistingServerTestLayer = Layer.succeed( · 87 unmodified lines
163189 const DEFAULT_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettings)({
164190 binaryPath: "fake-opencode",
165191 });
192+const LOCAL_AUTH_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettings)({
193+ binaryPath: "fake-opencode",
194+ serverPassword: "secret-password",
195+});
166196 const EXISTING_SERVER_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettings)({
167197 binaryPath: "fake-opencode",
168198 serverUrl: "http://127.0.0.1:9999",
169199 serverPassword: "secret-password",
170200 });
201+const EXTERNAL_SERVER_WITHOUT_AUTH_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettings)({
202+ binaryPath: "fake-opencode",
203+ serverUrl: "http://127.0.0.1:9999",
204+});
171205
172206 function withOpenCodeTextGeneration<A, E, R>(
173207 settings: OpenCodeSettings,
174208 effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>,
209+ environment?: NodeJS.ProcessEnv,
175210 ) {
176211 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+ );
178221 return yield* effectFn(textGeneration);
179222 }).pipe(Effect.scoped);
180223 }
@@ -229,6 +272,49 @@ it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGeneration", (it) => { · 48 unmodified lines
229272 ),
230273 );
231274
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+
232318 it.effect("reuses a warm server across back-to-back requests and closes it after idling", () =>
233319 withOpenCodeTextGeneration(DEFAULT_OPENCODE_SETTINGS, (textGeneration) =>
234320 Effect.gen(function* () {
@@ -460,6 +546,36 @@ it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGeneration", (it) => { · 225 unmodified lines
460546 it.layer(OpenCodeTextGenerationExistingServerTestLayer)(
461547 "OpenCodeTextGeneration with configured server URL",
462548 (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+
463579 it.effect("reuses a configured OpenCode server URL without spawning or applying idle TTL", () =>
464580 withOpenCodeTextGeneration(EXISTING_SERVER_OPENCODE_SETTINGS, (textGeneration) =>
465581 Effect.gen(function* () {