| ... | ... | @@ -290,6 +290,114 @@ it.layer(NodeServices.layer)("effect-codex-app-server protocol", (it) => { |
| 290 | 290 | }), |
| 291 | 291 | ); |
| 292 | 292 | |
| 293 | it.effect("routes a large notification fragmented across thousands of input chunks", () => |
| 294 | Effect.gen(function* () { |
| 295 | const { stdio, input, output } = yield* makeInMemoryStdio(); |
| 296 | const notifications: Array<CodexProtocol.CodexAppServerIncomingNotification> = []; |
| 297 | const transport = yield* CodexProtocol.makeCodexAppServerPatchedProtocol({ |
| 298 | stdio, |
| 299 | onNotification: (notification) => |
| 300 | Effect.sync(() => { |
| 301 | notifications.push(notification); |
| 302 | }), |
| 303 | }); |
| 304 | const response = yield* transport.request("thread/read", {}).pipe(Effect.forkScoped); |
| 305 | yield* Queue.take(output); |
| 306 | |
| 307 | const notification = { |
| 308 | method: "turn/diff/updated", |
| 309 | params: { |
| 310 | threadId: "thread-1", |
| 311 | turnId: "turn-1", |
| 312 | diff: "x".repeat(4 * 1024 * 1024), |
| 313 | }, |
| 314 | }; |
| 315 | const bytes = encoder.encode( |
| 316 | `${encodeUnknownJsonString(notification)}\n${encodeUnknownJsonString({ id: 1, result: { ok: true } })}\n`, |
| 317 | ); |
| 318 | for (let offset = 0; offset < bytes.length; offset += 1024) { |
| 319 | yield* Queue.offer(input, bytes.subarray(offset, offset + 1024)); |
| 320 | } |
| 321 | |
| 322 | assert.deepEqual(yield* Fiber.join(response), { ok: true }); |
| 323 | assert.deepEqual(notifications, [notification]); |
| 324 | }), |
| 325 | ); |
| 326 | |
| 327 | it.effect.each([1, 7, 1024])( |
| 328 | "preserves JSONL framing and UTF-8 across %i-byte input chunks", |
| 329 | (chunkSize) => |
| 330 | Effect.gen(function* () { |
| 331 | const { stdio, input } = yield* makeInMemoryStdio(); |
| 332 | const notifications: Array<CodexProtocol.CodexAppServerIncomingNotification> = []; |
| 333 | const rawLines: Array<unknown> = []; |
| 334 | const termination = yield* Deferred.make<CodexError.CodexAppServerError>(); |
| 335 | yield* CodexProtocol.makeCodexAppServerPatchedProtocol({ |
| 336 | stdio, |
| 337 | logIncoming: true, |
| 338 | logger: (event) => |
| 339 | Effect.sync(() => { |
| 340 | if (event.stage === "raw") { |
| 341 | rawLines.push(event.payload); |
| 342 | } |
| 343 | }), |
| 344 | onNotification: (notification) => |
| 345 | Effect.sync(() => { |
| 346 | notifications.push(notification); |
| 347 | }), |
| 348 | onTermination: (error) => Deferred.succeed(termination, error).pipe(Effect.asVoid), |
| 349 | }); |
| 350 | |
| 351 | const firstLine = '{"method":"x/first",\r"params":{"text":"héπ"}}'; |
| 352 | const secondLine = '{"method":"x/second","params":{"value":2}}'; |
| 353 | const finalLine = '{"method":"x/final","params":{"text":"ζεΎ"}}\r'; |
| 354 | const bytes = encoder.encode(`\n \t\r\n${firstLine}\r\n\n${secondLine}\n${finalLine}`); |
| 355 | for (let offset = 0; offset < bytes.length; offset += chunkSize) { |
| 356 | yield* Queue.offer(input, bytes.subarray(offset, offset + chunkSize)); |
| 357 | } |
| 358 | yield* Queue.end(input); |
| 359 | |
| 360 | assert.instanceOf( |
| 361 | yield* Deferred.await(termination), |
| 362 | CodexError.CodexAppServerInputStreamEndedError, |
| 363 | ); |
| 364 | assert.deepEqual(notifications, [ |
| 365 | { method: "x/first", params: { text: "héπ" } }, |
| 366 | { method: "x/second", params: { value: 2 } }, |
| 367 | { method: "x/final", params: { text: "ζεΎ" } }, |
| 368 | ]); |
| 369 | assert.deepEqual(rawLines, [firstLine, secondLine, finalLine]); |
| 370 | }), |
| 371 | ); |
| 372 | |
| 373 | it.effect("reports a malformed fragmented final line before input stream termination", () => |
| 374 | Effect.gen(function* () { |
| 375 | const { stdio, input, output } = yield* makeInMemoryStdio(); |
| 376 | const termination = yield* Deferred.make<CodexError.CodexAppServerError>(); |
| 377 | const transport = yield* CodexProtocol.makeCodexAppServerPatchedProtocol({ |
| 378 | stdio, |
| 379 | onTermination: (error) => Deferred.succeed(termination, error).pipe(Effect.asVoid), |
| 380 | }); |
| 381 | const response = yield* transport.request("thread/read", {}).pipe(Effect.forkScoped); |
| 382 | yield* Queue.take(output); |
| 383 | |
| 384 | yield* Queue.offer(input, encoder.encode('{"id":1,')); |
| 385 | yield* Queue.offer(input, encoder.encode('"result":')); |
| 386 | yield* Queue.end(input); |
| 387 | |
| 388 | const error = yield* Deferred.await(termination); |
| 389 | assert.instanceOf(error, CodexError.CodexAppServerProtocolParseError); |
| 390 | assert.equal(error.operation, "decode-wire-message"); |
| 391 | const responseError = yield* Fiber.join(response).pipe( |
| 392 | Effect.match({ |
| 393 | onFailure: (failure) => failure, |
| 394 | onSuccess: () => assert.fail("Expected the malformed response to fail the request"), |
| 395 | }), |
| 396 | ); |
| 397 | assert.strictEqual(responseError, error); |
| 398 | }), |
| 399 | ); |
| 400 | |
| 293 | 401 | it.effect("surfaces JSON encoding failures as protocol parse errors", () => |
| 294 | 402 | Effect.gen(function* () { |
| 295 | 403 | const { stdio } = yield* makeInMemoryStdio(); |