Showing the changes to apps/server/src/textGeneration/OpenCodeTextGeneration.test.ts (1 of 42 files). Show all
Unified · Split
| @@ -11,6 +11,7 @@ import { beforeEach, expect } from "vite-plus/test"; · 10 unmodified lines | ||
| 11 | 11 | |
| 12 | 12 | import * as ServerConfig from "../config.ts"; |
| 13 | 13 | import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts"; |
| 14 | +import * as OpenCodeServerOwner from "../provider/OpenCodeServerOwner.ts"; | |
| 14 | 15 | import * as OpenCodeTextGeneration from "./OpenCodeTextGeneration.ts"; |
| 15 | 16 | import * as TextGeneration from "./TextGeneration.ts"; |
| 16 | 17 | |
| @@ -21,6 +22,8 @@ const runtimeMock = { · 4 unmodified lines | ||
| 21 | 22 | promptParts: [] as ReadonlyArray<unknown>[], |
| 22 | 23 | authHeaders: [] as Array<string | null>, |
| 23 | 24 | closeCalls: [] as string[], |
| 25 | + sessionCreateCalls: 0, | |
| 26 | + connectionError: undefined as Error | undefined, | |
| 24 | 27 | sessionCreateError: undefined as unknown, |
| 25 | 28 | sessionResult: undefined as { data?: { id: string } } | undefined, |
| 26 | 29 | promptRequestError: undefined as unknown, |
| @@ -34,6 +37,8 @@ const runtimeMock = { · 7 unmodified lines | ||
| 34 | 37 | this.state.promptParts.length = 0; |
| 35 | 38 | this.state.authHeaders.length = 0; |
| 36 | 39 | this.state.closeCalls.length = 0; |
| 40 | + this.state.sessionCreateCalls = 0; | |
| 41 | + this.state.connectionError = undefined; | |
| 37 | 42 | this.state.sessionCreateError = undefined; |
| 38 | 43 | this.state.sessionResult = undefined; |
| 39 | 44 | this.state.promptRequestError = undefined; |
| @@ -42,7 +47,7 @@ const runtimeMock = { · 2 unmodified lines | ||
| 42 | 47 | }; |
| 43 | 48 | |
| 44 | 49 | const OpenCodeRuntimeTestDouble: OpenCodeRuntime.OpenCodeRuntimeShape = { |
| 45 | − startOpenCodeServerProcess: ({ binaryPath }) => | |
| 50 | + startOpenCodeServerProcess: ({ binaryPath, serverPassword, environment }) => | |
| 46 | 51 | Effect.gen(function* () { |
| 47 | 52 | const index = runtimeMock.state.startCalls.length + 1; |
| 48 | 53 | const url = `http://127.0.0.1:${4_300 + index}`; |
| @@ -54,22 +59,43 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntime.OpenCodeRuntimeShape = { · 5 unmodified lines | ||
| 54 | 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 | 67 | return { |
| 58 | 68 | url, |
| 69 | + ...(effectiveServerPassword !== undefined | |
| 70 | + ? { serverPassword: effectiveServerPassword } | |
| 71 | + : {}), | |
| 72 | + version: "1.14.19", | |
| 73 | + isRunning: Effect.succeed(true), | |
| 59 | 74 | exitCode: Effect.never, |
| 60 | 75 | }; |
| 61 | 76 | }), |
| 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 | + }), | |
| 68 | 93 | runOpenCodeCommand: () => Effect.succeed({ stdout: "", stderr: "", code: 0 }), |
| 69 | 94 | createOpenCodeSdkClient: ({ baseUrl, serverPassword }) => |
| 70 | 95 | ({ |
| 71 | 96 | session: { |
| 72 | 97 | create: async () => { |
| 98 | + runtimeMock.state.sessionCreateCalls += 1; | |
| 73 | 99 | if (runtimeMock.state.sessionCreateError !== undefined) { |
| 74 | 100 | throw runtimeMock.state.sessionCreateError; |
| 75 | 101 | } |
| @@ -163,18 +189,35 @@ const OpenCodeTextGenerationExistingServerTestLayer = Layer.succeed( · 87 unmodified lines | ||
| 163 | 189 | const DEFAULT_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettings)({ |
| 164 | 190 | binaryPath: "fake-opencode", |
| 165 | 191 | }); |
| 192 | +const LOCAL_AUTH_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettings)({ | |
| 193 | + binaryPath: "fake-opencode", | |
| 194 | + serverPassword: "secret-password", | |
| 195 | +}); | |
| 166 | 196 | const EXISTING_SERVER_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettings)({ |
| 167 | 197 | binaryPath: "fake-opencode", |
| 168 | 198 | serverUrl: "http://127.0.0.1:9999", |
| 169 | 199 | serverPassword: "secret-password", |
| 170 | 200 | }); |
| 201 | +const EXTERNAL_SERVER_WITHOUT_AUTH_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettings)({ | |
| 202 | + binaryPath: "fake-opencode", | |
| 203 | + serverUrl: "http://127.0.0.1:9999", | |
| 204 | +}); | |
| 171 | 205 | |
| 172 | 206 | function withOpenCodeTextGeneration<A, E, R>( |
| 173 | 207 | settings: OpenCodeSettings, |
| 174 | 208 | effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>, |
| 209 | + environment?: NodeJS.ProcessEnv, | |
| 175 | 210 | ) { |
| 176 | 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 | 221 | return yield* effectFn(textGeneration); |
| 179 | 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 | 318 | it.effect("reuses a warm server across back-to-back requests and closes it after idling", () => |
| 233 | 319 | withOpenCodeTextGeneration(DEFAULT_OPENCODE_SETTINGS, (textGeneration) => |
| 234 | 320 | Effect.gen(function* () { |
| @@ -460,6 +546,36 @@ it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGeneration", (it) => { · 225 unmodified lines | ||
| 460 | 546 | it.layer(OpenCodeTextGenerationExistingServerTestLayer)( |
| 461 | 547 | "OpenCodeTextGeneration with configured server URL", |
| 462 | 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 | 579 | it.effect("reuses a configured OpenCode server URL without spawning or applying idle TTL", () => |
| 464 | 580 | withOpenCodeTextGeneration(EXISTING_SERVER_OPENCODE_SETTINGS, (textGeneration) => |
| 465 | 581 | Effect.gen(function* () { |