| @@ -1,13 +1,13 @@ |
| 1 | import { OpenCodeSettings, ProviderInstanceId, TextGenerationError } from "@t3tools/contracts"; | 1 | import { OpenCodeSettings, ProviderInstanceId, TextGenerationError } from "@t3tools/contracts"; |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | import { it } from "@effect/vitest"; | 3 | import { assert, it } from "@effect/vitest"; |
| 4 | import * as Duration from "effect/Duration"; | 4 | import * as Duration from "effect/Duration"; |
| 5 | import * as Effect from "effect/Effect"; | 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as Layer from "effect/Layer"; | 6 | import * as Layer from "effect/Layer"; |
| 7 | import * as Schema from "effect/Schema"; | 7 | import * as Schema from "effect/Schema"; |
| 8 | import * as TestClock from "effect/testing/TestClock"; | 8 | import * as TestClock from "effect/testing/TestClock"; |
| 9 | import * as NetService from "@t3tools/shared/Net"; | 9 | import * as NetService from "@t3tools/shared/Net"; |
| 10 | import { beforeEach, expect } from "vite-plus/test"; | 10 | import { beforeEach } from "vite-plus/test"; |
| 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"; |
| @@ -39,7 +39,7 @@ const runtimeMock = { · 25 unmodified lines |
| 39 | }, | 39 | }, |
| 40 | }; | 40 | }; |
| 41 | | 41 | |
| 42 | const OpenCodeRuntimeTestDouble: OpenCodeRuntime.OpenCodeRuntimeShape = { | 42 | const OpenCodeRuntimeTestLayer = Layer.mock(OpenCodeRuntime.OpenCodeRuntime)({ |
| 43 | startOpenCodeServerProcess: ({ binaryPath }) => | 43 | startOpenCodeServerProcess: ({ binaryPath }) => |
| 44 | Effect.gen(function* () { | 44 | Effect.gen(function* () { |
| 45 | const index = runtimeMock.state.startCalls.length + 1; | 45 | const index = runtimeMock.state.startCalls.length + 1; |
| @@ -57,13 +57,6 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntime.OpenCodeRuntimeShape = { · 11 unmodified lines |
| 57 | exitCode: Effect.never, | 57 | exitCode: Effect.never, |
| 58 | }; | 58 | }; |
| 59 | }), | 59 | }), |
| 60 | connectToOpenCodeServer: ({ serverUrl }) => | | |
| 61 | Effect.succeed({ | | |
| 62 | url: serverUrl ?? "http://127.0.0.1:4301", | | |
| 63 | exitCode: null, | | |
| 64 | external: Boolean(serverUrl), | | |
| 65 | }), | | |
| 66 | runOpenCodeCommand: () => Effect.succeed({ stdout: "", stderr: "", code: 0 }), | | |
| 67 | createOpenCodeSdkClient: ({ baseUrl, serverPassword }) => | 60 | createOpenCodeSdkClient: ({ baseUrl, serverPassword }) => |
| 68 | ({ | 61 | ({ |
| 69 | session: { | 62 | session: { |
| @@ -99,15 +92,7 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntime.OpenCodeRuntimeShape = { · 29 unmodified lines |
| 99 | }, | 92 | }, |
| 100 | }, | 93 | }, |
| 101 | }) as unknown as ReturnType<OpenCodeRuntime.OpenCodeRuntimeShape["createOpenCodeSdkClient"]>, | 94 | }) as unknown as ReturnType<OpenCodeRuntime.OpenCodeRuntimeShape["createOpenCodeSdkClient"]>, |
| 102 | loadOpenCodeInventory: () => | 95 | }); |
| 103 | Effect.fail( | | |
| 104 | new OpenCodeRuntime.OpenCodeRuntimeError({ | | |
| 105 | operation: "loadOpenCodeInventory", | | |
| 106 | detail: "OpenCodeRuntimeTestDouble.loadOpenCodeInventory not used in this test", | | |
| 107 | cause: null, | | |
| 108 | }), | | |
| 109 | ), | | |
| 110 | }; | | |
| 111 | | 96 | |
| 112 | const DEFAULT_TEST_MODEL_SELECTION = { | 97 | const DEFAULT_TEST_MODEL_SELECTION = { |
| 113 | instanceId: ProviderInstanceId.make("opencode"), | 98 | instanceId: ProviderInstanceId.make("opencode"), |
| @@ -123,10 +108,7 @@ const DEFAULT_COMMIT_MESSAGE_INPUT = { · 9 unmodified lines |
| 123 | | 108 | |
| 124 | const OPENCODE_TEXT_GENERATION_IDLE_TTL_MS = 30_000; | 109 | const OPENCODE_TEXT_GENERATION_IDLE_TTL_MS = 30_000; |
| 125 | | 110 | |
| 126 | const OpenCodeTextGenerationTestLayer = Layer.succeed( | 111 | const OpenCodeTextGenerationTestLayer = OpenCodeRuntimeTestLayer.pipe( |
| 127 | OpenCodeRuntime.OpenCodeRuntime, | | |
| 128 | OpenCodeRuntimeTestDouble, | | |
| 129 | ).pipe( | | |
| 130 | Layer.provideMerge( | 112 | Layer.provideMerge( |
| 131 | ServerConfig.ServerConfig.layerTest(process.cwd(), { | 113 | ServerConfig.ServerConfig.layerTest(process.cwd(), { |
| 132 | prefix: "t3code-opencode-text-generation-test-", | 114 | prefix: "t3code-opencode-text-generation-test-", |
| @@ -136,10 +118,7 @@ const OpenCodeTextGenerationTestLayer = Layer.succeed( · 3 unmodified lines |
| 136 | Layer.provideMerge(NodeServices.layer), | 118 | Layer.provideMerge(NodeServices.layer), |
| 137 | ); | 119 | ); |
| 138 | | 120 | |
| 139 | const OpenCodeTextGenerationExistingServerTestLayer = Layer.succeed( | 121 | const OpenCodeTextGenerationExistingServerTestLayer = OpenCodeRuntimeTestLayer.pipe( |
| 140 | OpenCodeRuntime.OpenCodeRuntime, | | |
| 141 | OpenCodeRuntimeTestDouble, | | |
| 142 | ).pipe( | | |
| 143 | Layer.provideMerge( | 122 | Layer.provideMerge( |
| 144 | ServerConfig.ServerConfig.layerTest(process.cwd(), { | 123 | ServerConfig.ServerConfig.layerTest(process.cwd(), { |
| 145 | prefix: "t3code-opencode-text-generation-existing-server-test-", | 124 | prefix: "t3code-opencode-text-generation-existing-server-test-", |
| @@ -197,16 +176,16 @@ it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGeneration", (it) => { · 51 unmodified lines |
| 197 | modelSelection: DEFAULT_TEST_MODEL_SELECTION, | 176 | modelSelection: DEFAULT_TEST_MODEL_SELECTION, |
| 198 | }); | 177 | }); |
| 199 | | 178 | |
| 200 | expect(runtimeMock.state.startCalls).toEqual(["fake-opencode"]); | 179 | assert.deepStrictEqual(runtimeMock.state.startCalls, ["fake-opencode"]); |
| 201 | expect(runtimeMock.state.promptUrls).toEqual([ | 180 | assert.deepStrictEqual(runtimeMock.state.promptUrls, [ |
| 202 | "http://127.0.0.1:4301", | 181 | "http://127.0.0.1:4301", |
| 203 | "http://127.0.0.1:4301", | 182 | "http://127.0.0.1:4301", |
| 204 | ]); | 183 | ]); |
| 205 | expect(runtimeMock.state.closeCalls).toEqual([]); | 184 | assert.deepStrictEqual(runtimeMock.state.closeCalls, []); |
| 206 | | 185 | |
| 207 | yield* advanceIdleClock; | 186 | yield* advanceIdleClock; |
| 208 | | 187 | |
| 209 | expect(runtimeMock.state.closeCalls).toEqual(["http://127.0.0.1:4301"]); | 188 | assert.deepStrictEqual(runtimeMock.state.closeCalls, ["http://127.0.0.1:4301"]); |
| 210 | }), | 189 | }), |
| 211 | ).pipe(Effect.provide(TestClock.layer())), | 190 | ).pipe(Effect.provide(TestClock.layer())), |
| 212 | ); | 191 | ); |
| @@ -232,12 +211,15 @@ it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGeneration", (it) => { · 19 unmodified lines |
| 232 | modelSelection: DEFAULT_TEST_MODEL_SELECTION, | 211 | modelSelection: DEFAULT_TEST_MODEL_SELECTION, |
| 233 | }); | 212 | }); |
| 234 | | 213 | |
| 235 | expect(runtimeMock.state.startCalls).toEqual(["fake-opencode", "fake-opencode"]); | 214 | assert.deepStrictEqual(runtimeMock.state.startCalls, [ |
| 236 | expect(runtimeMock.state.promptUrls).toEqual([ | 215 | "fake-opencode", |
| | 216 | "fake-opencode", |
| | 217 | ]); |
| | 218 | assert.deepStrictEqual(runtimeMock.state.promptUrls, [ |
| 237 | "http://127.0.0.1:4301", | 219 | "http://127.0.0.1:4301", |
| 238 | "http://127.0.0.1:4302", | 220 | "http://127.0.0.1:4302", |
| 239 | ]); | 221 | ]); |
| 240 | expect(runtimeMock.state.closeCalls).toEqual(["http://127.0.0.1:4301"]); | 222 | assert.deepStrictEqual(runtimeMock.state.closeCalls, ["http://127.0.0.1:4301"]); |
| 241 | }), | 223 | }), |
| 242 | ).pipe(Effect.provide(TestClock.layer())), | 224 | ).pipe(Effect.provide(TestClock.layer())), |
| 243 | ); | 225 | ); |
| @@ -252,15 +234,16 @@ it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGeneration", (it) => { · 8 unmodified lines |
| 252 | .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT) | 234 | .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT) |
| 253 | .pipe(Effect.flip); | 235 | .pipe(Effect.flip); |
| 254 | | 236 | |
| 255 | expect(error).toBeInstanceOf(TextGenerationError); | 237 | assert.instanceOf(error, TextGenerationError); |
| 256 | expect(error.message).toContain("OpenCode session.create request failed."); | 238 | assert.include(error.message, "OpenCode session.create request failed."); |
| 257 | expect(error.cause).toMatchObject({ | 239 | const cause = error.cause; |
| 258 | _tag: "OpenCodeTextGenerationSessionRequestError", | 240 | assert.instanceOf( |
| 259 | operation: "generateCommitMessage", | 241 | cause, |
| 260 | cwd: process.cwd(), | 242 | OpenCodeTextGeneration.OpenCodeTextGenerationSessionRequestError, |
| 261 | cause: sdkCause, | 243 | ); |
| 262 | }); | 244 | assert.strictEqual(cause.operation, "generateCommitMessage"); |
| 263 | expect((error.cause as { cause: unknown }).cause).toBe(sdkCause); | 245 | assert.strictEqual(cause.cwd, process.cwd()); |
| | 246 | assert.strictEqual(cause.cause, sdkCause); |
| 264 | }), | 247 | }), |
| 265 | ), | 248 | ), |
| 266 | ); | 249 | ); |
| @@ -274,13 +257,15 @@ it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGeneration", (it) => { · 7 unmodified lines |
| 274 | .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT) | 257 | .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT) |
| 275 | .pipe(Effect.flip); | 258 | .pipe(Effect.flip); |
| 276 | | 259 | |
| 277 | expect(error.message).toContain("OpenCode session.create returned no session payload."); | 260 | assert.include(error.message, "OpenCode session.create returned no session payload."); |
| 278 | expect(error.cause).toMatchObject({ | 261 | const cause = error.cause; |
| 279 | _tag: "OpenCodeTextGenerationSessionPayloadError", | 262 | assert.instanceOf( |
| 280 | operation: "generateCommitMessage", | 263 | cause, |
| 281 | cwd: process.cwd(), | 264 | OpenCodeTextGeneration.OpenCodeTextGenerationSessionPayloadError, |
| 282 | }); | 265 | ); |
| 283 | expect(error.cause).not.toHaveProperty("cause"); | 266 | assert.strictEqual(cause.operation, "generateCommitMessage"); |
| | 267 | assert.strictEqual(cause.cwd, process.cwd()); |
| | 268 | assert.notProperty(cause, "cause"); |
| 284 | }), | 269 | }), |
| 285 | ), | 270 | ), |
| 286 | ); | 271 | ); |
| @@ -295,17 +280,18 @@ it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGeneration", (it) => { · 8 unmodified lines |
| 295 | .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT) | 280 | .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT) |
| 296 | .pipe(Effect.flip); | 281 | .pipe(Effect.flip); |
| 297 | | 282 | |
| 298 | expect(error.message).toContain("OpenCode session.prompt request failed."); | 283 | assert.include(error.message, "OpenCode session.prompt request failed."); |
| 299 | expect(error.cause).toMatchObject({ | 284 | const cause = error.cause; |
| 300 | _tag: "OpenCodeTextGenerationPromptRequestError", | 285 | assert.instanceOf( |
| 301 | operation: "generateCommitMessage", | 286 | cause, |
| 302 | cwd: process.cwd(), | 287 | OpenCodeTextGeneration.OpenCodeTextGenerationPromptRequestError, |
| 303 | sessionId: "http://127.0.0.1:4301/session", | 288 | ); |
| 304 | providerId: "openai", | 289 | assert.strictEqual(cause.operation, "generateCommitMessage"); |
| 305 | modelId: "gpt-5", | 290 | assert.strictEqual(cause.cwd, process.cwd()); |
| 306 | cause: sdkCause, | 291 | assert.strictEqual(cause.sessionId, "http://127.0.0.1:4301/session"); |
| 307 | }); | 292 | assert.strictEqual(cause.providerId, "openai"); |
| 308 | expect((error.cause as { cause: unknown }).cause).toBe(sdkCause); | 293 | assert.strictEqual(cause.modelId, "gpt-5"); |
| | 294 | assert.strictEqual(cause.cause, sdkCause); |
| 309 | }), | 295 | }), |
| 310 | ), | 296 | ), |
| 311 | ); | 297 | ); |
| @@ -323,18 +309,20 @@ it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGeneration", (it) => { · 11 unmodified lines |
| 323 | .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT) | 309 | .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT) |
| 324 | .pipe(Effect.flip); | 310 | .pipe(Effect.flip); |
| 325 | | 311 | |
| 326 | expect(error.message).toContain("OpenCode returned empty output."); | 312 | assert.include(error.message, "OpenCode returned empty output."); |
| 327 | expect(error.cause).toMatchObject({ | 313 | const cause = error.cause; |
| 328 | _tag: "OpenCodeTextGenerationEmptyOutputError", | 314 | assert.instanceOf( |
| 329 | operation: "generateCommitMessage", | 315 | cause, |
| 330 | cwd: process.cwd(), | 316 | OpenCodeTextGeneration.OpenCodeTextGenerationEmptyOutputError, |
| 331 | sessionId: "http://127.0.0.1:4301/session", | 317 | ); |
| 332 | providerId: "openai", | 318 | assert.strictEqual(cause.operation, "generateCommitMessage"); |
| 333 | modelId: "gpt-5", | 319 | assert.strictEqual(cause.cwd, process.cwd()); |
| 334 | responsePartCount: 3, | 320 | assert.strictEqual(cause.sessionId, "http://127.0.0.1:4301/session"); |
| 335 | textPartCount: 1, | 321 | assert.strictEqual(cause.providerId, "openai"); |
| 336 | }); | 322 | assert.strictEqual(cause.modelId, "gpt-5"); |
| 337 | expect(error.cause).not.toHaveProperty("cause"); | 323 | assert.strictEqual(cause.responsePartCount, 3); |
| | 324 | assert.strictEqual(cause.textPartCount, 1); |
| | 325 | assert.notProperty(cause, "cause"); |
| 338 | }), | 326 | }), |
| 339 | ), | 327 | ), |
| 340 | ); | 328 | ); |
| @@ -361,7 +349,7 @@ it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGeneration", (it) => { · 20 unmodified lines |
| 361 | modelSelection: DEFAULT_TEST_MODEL_SELECTION, | 349 | modelSelection: DEFAULT_TEST_MODEL_SELECTION, |
| 362 | }); | 350 | }); |
| 363 | | 351 | |
| 364 | expect(result).toEqual({ | 352 | assert.deepStrictEqual(result, { |
| 365 | subject: "Tighten OpenCode parsing", | 353 | subject: "Tighten OpenCode parsing", |
| 366 | body: "Handle JSON text output locally.", | 354 | body: "Handle JSON text output locally.", |
| 367 | }); | 355 | }); |
| @@ -390,18 +378,20 @@ it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGeneration", (it) => { · 22 unmodified lines |
| 390 | .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT) | 378 | .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT) |
| 391 | .pipe(Effect.flip); | 379 | .pipe(Effect.flip); |
| 392 | | 380 | |
| 393 | expect(error.message).toContain("Model did not produce structured output"); | 381 | assert.include(error.message, "Model did not produce structured output"); |
| 394 | expect(error.cause).toMatchObject({ | 382 | const cause = error.cause; |
| 395 | _tag: "OpenCodeTextGenerationPromptResponseError", | 383 | assert.instanceOf( |
| 396 | operation: "generateCommitMessage", | 384 | cause, |
| 397 | cwd: process.cwd(), | 385 | OpenCodeTextGeneration.OpenCodeTextGenerationPromptResponseError, |
| 398 | sessionId: "http://127.0.0.1:4301/session", | 386 | ); |
| 399 | providerId: "openai", | 387 | assert.strictEqual(cause.operation, "generateCommitMessage"); |
| 400 | modelId: "gpt-5", | 388 | assert.strictEqual(cause.cwd, process.cwd()); |
| 401 | providerErrorName: "StructuredOutputError", | 389 | assert.strictEqual(cause.sessionId, "http://127.0.0.1:4301/session"); |
| 402 | providerMessage: "Model did not produce structured output", | 390 | assert.strictEqual(cause.providerId, "openai"); |
| 403 | }); | 391 | assert.strictEqual(cause.modelId, "gpt-5"); |
| 404 | expect(error.cause).not.toHaveProperty("cause"); | 392 | assert.strictEqual(cause.providerErrorName, "StructuredOutputError"); |
| | 393 | assert.strictEqual(cause.providerMessage, "Model did not produce structured output"); |
| | 394 | assert.notProperty(cause, "cause"); |
| 405 | }), | 395 | }), |
| 406 | ), | 396 | ), |
| 407 | ); | 397 | ); |
| @@ -428,19 +418,19 @@ it.layer(OpenCodeTextGenerationExistingServerTestLayer)( · 20 unmodified lines |
| 428 | modelSelection: DEFAULT_TEST_MODEL_SELECTION, | 418 | modelSelection: DEFAULT_TEST_MODEL_SELECTION, |
| 429 | }); | 419 | }); |
| 430 | | 420 | |
| 431 | expect(runtimeMock.state.startCalls).toEqual([]); | 421 | assert.deepStrictEqual(runtimeMock.state.startCalls, []); |
| 432 | expect(runtimeMock.state.promptUrls).toEqual([ | 422 | assert.deepStrictEqual(runtimeMock.state.promptUrls, [ |
| 433 | "http://127.0.0.1:9999", | 423 | "http://127.0.0.1:9999", |
| 434 | "http://127.0.0.1:9999", | 424 | "http://127.0.0.1:9999", |
| 435 | ]); | 425 | ]); |
| 436 | expect(runtimeMock.state.authHeaders).toEqual([ | 426 | assert.deepStrictEqual(runtimeMock.state.authHeaders, [ |
| 437 | `Basic ${btoa("opencode:secret-password")}`, | 427 | `Basic ${btoa("opencode:secret-password")}`, |
| 438 | `Basic ${btoa("opencode:secret-password")}`, | 428 | `Basic ${btoa("opencode:secret-password")}`, |
| 439 | ]); | 429 | ]); |
| 440 | | 430 | |
| 441 | yield* advanceIdleClock; | 431 | yield* advanceIdleClock; |
| 442 | | 432 | |
| 443 | expect(runtimeMock.state.closeCalls).toEqual([]); | 433 | assert.deepStrictEqual(runtimeMock.state.closeCalls, []); |
| 444 | }), | 434 | }), |
| 445 | ).pipe(Effect.provide(TestClock.layer())), | 435 | ).pipe(Effect.provide(TestClock.layer())), |
| 446 | ); | 436 | ); |