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