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 | import * as ServerConfig from "../config.ts"; | 12 | import * as ServerConfig from "../config.ts"; |
| 13 | import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts"; | 13 | import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts"; |
| 14 | import * as OpenCodeServerOwner from "../provider/OpenCodeServerOwner.ts"; | ||
| 14 | import * as OpenCodeTextGeneration from "./OpenCodeTextGeneration.ts"; | 15 | import * as OpenCodeTextGeneration from "./OpenCodeTextGeneration.ts"; |
| 15 | import * as TextGeneration from "./TextGeneration.ts"; | 16 | import * 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 | ||
| 44 | const OpenCodeRuntimeTestDouble: OpenCodeRuntime.OpenCodeRuntimeShape = { | 49 | const 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 | |||
| 163 | const DEFAULT_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettings)({ | 189 | const DEFAULT_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettings)({ |
| 164 | binaryPath: "fake-opencode", | 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 | const EXISTING_SERVER_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettings)({ | 196 | const 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 | }); |
| 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 | function withOpenCodeTextGeneration<A, E, R>( | 206 | function 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 | |||
| 460 | it.layer(OpenCodeTextGenerationExistingServerTestLayer)( | 546 | it.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* () { |