test(frontend): extract CancellableLoader; pin debounce-race invariants in unit tests (0.87.23)
0.87.16 followup. Extract the four debounce-race invariants (predecessor abort, late-result suppression, AbortError swallow, ownership-aware finally) from `loadCoverage` into a reusable `CancellableLoader` helper at $lib/util/cancellable, with 9 unit tests covering each. Page rewrite uses the helper. Also corrects: hooks.server.test.ts (added Node-fetch rejection shape for already-aborted entry; removed redundant tick); admin.test.ts (AbortError-propagation now uses pre-aborted signal + mockRejectedValueOnce mirroring real Node fetch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -207,7 +207,7 @@ describe('hooks.server proxy', () => {
|
||||
// 0.87.9 the upstream fetch kept running until the proxy wall-clock
|
||||
// timeout fired.
|
||||
it('chains an already-aborted client signal into the upstream signal immediately', async () => {
|
||||
// Capture the signal the proxy hands fetch; resolve when called.
|
||||
// Capture the signal the proxy hands fetch.
|
||||
let upstream: AbortSignal | undefined;
|
||||
fetchSpy.mockImplementation(async (_url, init) => {
|
||||
upstream = (init as RequestInit).signal ?? undefined;
|
||||
@@ -225,6 +225,26 @@ describe('hooks.server proxy', () => {
|
||||
expect(upstream?.aborted).toBe(true);
|
||||
});
|
||||
|
||||
it('returns 502 when fetch itself rejects with AbortError on an already-aborted signal', async () => {
|
||||
// Production Node fetch rejects synchronously with AbortError if
|
||||
// the signal is already aborted on entry — the prior test mocks
|
||||
// fetch to resolve 200, which leaves the rejection-path
|
||||
// observation uncovered. Mirror Node's real behaviour and
|
||||
// confirm the handler maps the rejection to 502 / upstream_unavailable
|
||||
// (same envelope as any other network failure).
|
||||
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
fetchSpy.mockRejectedValueOnce(new DOMException('aborted', 'AbortError'));
|
||||
const resolve = vi.fn();
|
||||
const ctrl = new AbortController();
|
||||
ctrl.abort();
|
||||
const event = makeEventWithSignal('/api/v1/health', ctrl.signal);
|
||||
const resp = await handle({ event, resolve });
|
||||
expect(resp.status).toBe(502);
|
||||
const body = await resp.json();
|
||||
expect(body.error.code).toBe('upstream_unavailable');
|
||||
expect(errSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('chains a mid-flight client-disconnect into the upstream signal', async () => {
|
||||
// The mid-flight shape: request.signal is live when the handler
|
||||
// starts, then aborts while the upstream fetch is in progress.
|
||||
@@ -232,13 +252,13 @@ describe('hooks.server proxy', () => {
|
||||
let upstream: AbortSignal | undefined;
|
||||
fetchSpy.mockImplementation(async (_url, init) => {
|
||||
upstream = (init as RequestInit).signal ?? undefined;
|
||||
// Let the handler's chaining listener attach, then trip the
|
||||
// client signal — the upstream signal should reflect.
|
||||
// Let the handler's chaining listener attach (it runs on
|
||||
// the next microtask after `handle()` calls fetch), THEN
|
||||
// trip the client signal. `AbortSignal#dispatchEvent` is
|
||||
// synchronous, so the listener runs in-line — no further
|
||||
// tick needed; we observe `upstream.aborted` immediately.
|
||||
await Promise.resolve();
|
||||
clientCtrl.abort();
|
||||
// Tick once so the listener observes the abort before we
|
||||
// check.
|
||||
await Promise.resolve();
|
||||
return new Response('[]', { status: 200 });
|
||||
});
|
||||
const resolve = vi.fn();
|
||||
|
||||
Reference in New Issue
Block a user