test(frontend): pin abort-chain + signal pass-through contracts (0.87.16)
Three 0.87.9 follow-ups: 1. hooks.server.ts: two new tests covering the already-aborted-at-entry and mid-flight client-disconnect branches of the abort chain. Mutation-confirmed. 2. lib/api/admin.ts: two new tests pinning the getAnalysisMangaCoverage(init?.signal) pass-through and the AbortError-rejection propagation the dashboard's debounce-race handler relies on. Mutation-confirmed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,15 @@ function makeEvent(path: string, init?: RequestInit) {
|
||||
return { url, request } as Parameters<typeof handle>[0]['event'];
|
||||
}
|
||||
|
||||
/** Like {@link makeEvent} but lets a test inject a specific `signal`
|
||||
* on the underlying Request — the path SvelteKit uses to surface the
|
||||
* client-disconnect signal. Used by the abort-chain tests. */
|
||||
function makeEventWithSignal(path: string, signal: AbortSignal) {
|
||||
const url = new URL(`http://app.example.com${path}`);
|
||||
const request = new Request(url, { signal });
|
||||
return { url, request } as Parameters<typeof handle>[0]['event'];
|
||||
}
|
||||
|
||||
describe('hooks.server proxy', () => {
|
||||
let fetchSpy: MockInstance<typeof globalThis.fetch>;
|
||||
|
||||
@@ -191,6 +200,52 @@ describe('hooks.server proxy', () => {
|
||||
// presence is the contract this test is pinning.
|
||||
expect(init.signal?.aborted).toBe(false);
|
||||
});
|
||||
|
||||
// T2 — client-disconnect abort chain (0.87.9). Closing an admin tab
|
||||
// must propagate into the upstream fetch so the backend's SSE
|
||||
// broadcast::Receiver + spawned task tear down immediately. Before
|
||||
// 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.
|
||||
let upstream: AbortSignal | undefined;
|
||||
fetchSpy.mockImplementation(async (_url, init) => {
|
||||
upstream = (init as RequestInit).signal ?? undefined;
|
||||
return new Response('[]', { status: 200 });
|
||||
});
|
||||
const resolve = vi.fn();
|
||||
// Build an event with an ALREADY-aborted request signal — the
|
||||
// shape SvelteKit hands us after a client disconnect that has
|
||||
// already settled.
|
||||
const ctrl = new AbortController();
|
||||
ctrl.abort();
|
||||
const event = makeEventWithSignal('/api/v1/health', ctrl.signal);
|
||||
await handle({ event, resolve });
|
||||
expect(upstream).toBeInstanceOf(AbortSignal);
|
||||
expect(upstream?.aborted).toBe(true);
|
||||
});
|
||||
|
||||
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.
|
||||
const clientCtrl = new AbortController();
|
||||
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.
|
||||
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();
|
||||
const event = makeEventWithSignal('/api/v1/health', clientCtrl.signal);
|
||||
await handle({ event, resolve });
|
||||
expect(upstream?.aborted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user