| ... | @@ -431,6 +431,60 @@ describe("AgentAwarenessRelay", () => { | ... | @@ -431,6 +431,60 @@ describe("AgentAwarenessRelay", () => { |
| 431 | }), | 431 | }), |
| 432 | ); | 432 | ); |
| 433 | | 433 | |
| | 434 | it.effect("a stalled response body releases later threads and uses bounded retries", () => |
| | 435 | Effect.gen(function* () { |
| | 436 | const bodyStarted = Promise.withResolvers<void>(); |
| | 437 | const secondRead = yield* Deferred.make<void>(); |
| | 438 | const currentShell = yield* Ref.make(shell()); |
| | 439 | const { relay, publications } = yield* makeTestRelay({ |
| | 440 | readShell: (threadId) => |
| | 441 | threadId === SECOND_THREAD_ID |
| | 442 | ? Deferred.succeed(secondRead, undefined).pipe( |
| | 443 | Effect.as(shell({ id: SECOND_THREAD_ID })), |
| | 444 | ) |
| | 445 | : Ref.get(currentShell), |
| | 446 | respond: (attempt) => { |
| | 447 | if (attempt !== 1) return new Response("relay unavailable", { status: 503 }); |
| | 448 | const response = new Response(null, { |
| | 449 | headers: { "content-type": "application/json" }, |
| | 450 | }); |
| | 451 | return Object.assign(response, { |
| | 452 | arrayBuffer: () => { |
| | 453 | bodyStarted.resolve(); |
| | 454 | return new Promise<ArrayBuffer>(() => {}); |
| | 455 | }, |
| | 456 | }); |
| | 457 | }, |
| | 458 | }); |
| | 459 | const first = yield* relay.publishThread(THREAD_ID).pipe(Effect.forkChild); |
| | 460 | yield* Effect.promise(() => bodyStarted.promise); |
| | 461 | const second = yield* relay.publishThread(SECOND_THREAD_ID).pipe(Effect.forkChild); |
| | 462 | yield* TestClock.adjust("9999 millis"); |
| | 463 | assert.equal(publications.length, 1); |
| | 464 | yield* Ref.set(currentShell, shell({ title: "Latest title" })); |
| | 465 | yield* TestClock.adjust("1 millis"); |
| | 466 | // Assert progress before draining so the base fails instead of hanging. |
| | 467 | assert.isTrue(yield* Deferred.isDone(secondRead)); |
| | 468 | yield* Fiber.join(first); |
| | 469 | yield* Fiber.join(second); |
| | 470 | assert.equal(publications.length, 2); |
| | 471 | assert.isTrue(publications[1]?.url.includes(SECOND_THREAD_ID) ?? false); |
| | 472 | for (const delay of [1_000, 2_000, 4_000, 8_000, 16_000]) { |
| | 473 | const attempts = publications.length; |
| | 474 | yield* TestClock.adjust(delay - 1); |
| | 475 | yield* relay.drain; |
| | 476 | assert.equal(publications.length, attempts); |
| | 477 | yield* TestClock.adjust(1); |
| | 478 | yield* relay.drain; |
| | 479 | assert.equal(publications.length, attempts + 2); |
| | 480 | assert.equal(publications[attempts]?.state?.threadTitle, "Latest title"); |
| | 481 | } |
| | 482 | yield* TestClock.adjust("1 minute"); |
| | 483 | yield* relay.drain; |
| | 484 | assert.equal(publications.length, 12); |
| | 485 | }), |
| | 486 | ); |
| | 487 | |
| 434 | it.effect("retries a failed final state without another thread event", () => | 488 | it.effect("retries a failed final state without another thread event", () => |
| 435 | Effect.gen(function* () { | 489 | Effect.gen(function* () { |
| 436 | const { relay, currentShell, shellReads, publications } = yield* makeTestRelay({ | 490 | const { relay, currentShell, shellReads, publications } = yield* makeTestRelay({ |