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:
MechaCat02
2026-06-23 19:22:48 +02:00
parent 27fc1a52f6
commit 54cbbfc440
5 changed files with 100 additions and 3 deletions

View File

@@ -713,6 +713,48 @@ describe('admin crawler api client', () => {
expect(url).toContain('offset=50');
});
it('getAnalysisMangaCoverage forwards the AbortSignal to fetch', async () => {
// The dashboard's debounce-race fix (0.87.9) relies on this knob
// landing on `init.signal`. Without it, a slow first request
// can still overwrite a faster second one because `request()`
// builds a fresh fetch with no cancellation hook.
fetchSpy.mockResolvedValueOnce(
ok({ items: [], page: { limit: 50, offset: 0, total: 0 } })
);
const ctrl = new AbortController();
await getAnalysisMangaCoverage(
{ limit: 50 },
{ signal: ctrl.signal }
);
const init = fetchSpy.mock.calls[0][1] as RequestInit;
expect(init.signal).toBe(ctrl.signal);
});
it('getAnalysisMangaCoverage propagates an abort rejection', async () => {
// Mid-flight abort: caller flips the AbortController, the fetch
// promise rejects, and the api client lets the rejection
// surface to its caller (the debounce-race handler treats
// AbortError as silent cancellation rather than an error toast).
const ctrl = new AbortController();
fetchSpy.mockImplementationOnce(
(_url, init) =>
new Promise((_resolve, reject) => {
const signal = (init as RequestInit).signal;
signal?.addEventListener(
'abort',
() => reject(new DOMException('aborted', 'AbortError')),
{ once: true }
);
})
);
const inflight = getAnalysisMangaCoverage(
{ limit: 25 },
{ signal: ctrl.signal }
);
ctrl.abort();
await expect(inflight).rejects.toMatchObject({ name: 'AbortError' });
});
it('getAnalysisChapterCoverage unwraps items', async () => {
fetchSpy.mockResolvedValueOnce(
ok({ items: [{ chapter_id: 'c1', number: 1, title: null, total_pages: 2, analyzed_pages: 1 }] })