| ... | @@ -701,6 +701,84 @@ describe("DesktopBackendManager", () => { | ... | @@ -701,6 +701,84 @@ describe("DesktopBackendManager", () => { |
| 701 | ), | 701 | ), |
| 702 | ); | 702 | ); |
| 703 | | 703 | |
| | 704 | it.effect( |
| | 705 | "re-probes readiness after the first budget expires while the backend is still alive", |
| | 706 | () => |
| | 707 | Effect.scoped( |
| | 708 | Effect.gen(function* () { |
| | 709 | const requestUrls: Array<string> = []; |
| | 710 | let requestCount = 0; |
| | 711 | let readyCount = 0; |
| | 712 | let readinessTimeoutCount = 0; |
| | 713 | const firstProbe = yield* Deferred.make<void>(); |
| | 714 | const childExit = yield* Deferred.make<void>(); |
| | 715 | |
| | 716 | const spawnerLayer = Layer.succeed( |
| | 717 | ChildProcessSpawner.ChildProcessSpawner, |
| | 718 | ChildProcessSpawner.make(() => |
| | 719 | Effect.succeed( |
| | 720 | makeProcess({ |
| | 721 | exitCode: Deferred.await(childExit).pipe( |
| | 722 | Effect.as(ChildProcessSpawner.ExitCode(0)), |
| | 723 | ), |
| | 724 | }), |
| | 725 | ), |
| | 726 | ), |
| | 727 | ); |
| | 728 | |
| | 729 | // The backend stays 503 through the first *two* readiness budgets |
| | 730 | // and only becomes healthy (200) for the third round, i.e. it comes |
| | 731 | // up well after the initial 50ms budget has expired. |
| | 732 | const httpLayer = httpClientLayer((request) => |
| | 733 | Effect.gen(function* () { |
| | 734 | requestCount += 1; |
| | 735 | requestUrls.push(request.url); |
| | 736 | yield* Deferred.succeed(firstProbe, void 0); |
| | 737 | return responseForRequest(request, requestCount <= 2 ? 503 : 200); |
| | 738 | }), |
| | 739 | ); |
| | 740 | |
| | 741 | const runFiber = yield* DesktopBackendManager.runBackendProcess({ |
| | 742 | ...baseConfig, |
| | 743 | desktopTelemetryStream: Stream.empty, |
| | 744 | readinessTimeout: Duration.millis(50), |
| | 745 | onReady: () => |
| | 746 | Effect.sync(() => { |
| | 747 | readyCount += 1; |
| | 748 | }), |
| | 749 | onReadinessFailure: () => |
| | 750 | Effect.sync(() => { |
| | 751 | readinessTimeoutCount += 1; |
| | 752 | }), |
| | 753 | }).pipe(Effect.provide(Layer.merge(spawnerLayer, httpLayer)), Effect.forkChild); |
| | 754 | |
| | 755 | yield* Deferred.await(firstProbe); |
| | 756 | assert.equal(readyCount, 0); |
| | 757 | assert.equal(readinessTimeoutCount, 0); |
| | 758 | |
| | 759 | // The first 50ms readiness budget expires while the backend still |
| | 760 | // answers 503. The child is alive and may yet become healthy, so the |
| | 761 | // probe must start a fresh round instead of stopping permanently — |
| | 762 | // the pre-fix behavior left the app stuck on "Connecting to WSL…" |
| | 763 | // forever even though the backend kept running. |
| | 764 | yield* TestClock.adjust(Duration.millis(50)); |
| | 765 | assert.equal(readinessTimeoutCount, 1); |
| | 766 | assert.equal(readyCount, 0); |
| | 767 | |
| | 768 | // The second budget also expires (backend still 503), then the third |
| | 769 | // round connects. The point is the probe persisted across budgets |
| | 770 | // while the process was alive instead of giving up after the first. |
| | 771 | yield* TestClock.adjust(Duration.millis(100)); |
| | 772 | assert.equal(readinessTimeoutCount, 2); |
| | 773 | assert.equal(readyCount, 1); |
| | 774 | assert.equal(requestUrls.length, 3); |
| | 775 | |
| | 776 | yield* Deferred.succeed(childExit, void 0); |
| | 777 | assert.equal((yield* Fiber.join(runFiber)).code.pipe(Option.getOrUndefined), 0); |
| | 778 | }).pipe(Effect.provide(TestClock.layer())), |
| | 779 | ), |
| | 780 | ); |
| | 781 | |
| 704 | it.effect("starts the configured backend and closes the scoped process on stop", () => | 782 | it.effect("starts the configured backend and closes the scoped process on stop", () => |
| 705 | Effect.scoped( | 783 | Effect.scoped( |
| 706 | Effect.gen(function* () { | 784 | Effect.gen(function* () { |