feat(web/m7-coverage-gauge): coverage API client + query
Adds CoverageRollup type, getCoverageRollup() against /api/admin/library/coverage, and createCoverageQuery() with the same 3s/30s pacing used by scan-status. New qk.coverage() key follows the existing zero-arg tuple pattern. Vitest covers the GET path and the empty-library zero-state plus the with_art + pending + settled = total invariant.
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { getCoverageRollup, type CoverageRollup } from './admin';
|
||||||
|
|
||||||
|
vi.mock('./client', () => ({
|
||||||
|
api: { get: vi.fn(), post: vi.fn() }
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { api } from './client';
|
||||||
|
|
||||||
|
describe('admin coverage API', () => {
|
||||||
|
beforeEach(() => vi.clearAllMocks());
|
||||||
|
|
||||||
|
it('getCoverageRollup GETs the correct path', async () => {
|
||||||
|
const sample: CoverageRollup = {
|
||||||
|
total: 18026,
|
||||||
|
with_art: 12400,
|
||||||
|
pending: 5400,
|
||||||
|
settled: 226,
|
||||||
|
pending_no_mbid: 320
|
||||||
|
};
|
||||||
|
(api.get as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce(sample);
|
||||||
|
const got = await getCoverageRollup();
|
||||||
|
expect(api.get).toHaveBeenCalledWith('/api/admin/library/coverage');
|
||||||
|
expect(got).toEqual(sample);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns zeros on an empty library', async () => {
|
||||||
|
const empty: CoverageRollup = {
|
||||||
|
total: 0,
|
||||||
|
with_art: 0,
|
||||||
|
pending: 0,
|
||||||
|
settled: 0,
|
||||||
|
pending_no_mbid: 0
|
||||||
|
};
|
||||||
|
(api.get as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce(empty);
|
||||||
|
const got = await getCoverageRollup();
|
||||||
|
expect(got.total).toBe(0);
|
||||||
|
expect(got.with_art + got.pending + got.settled).toBe(got.total);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -236,3 +236,29 @@ export function createScanStatusQuery() {
|
|||||||
refetchInterval: 3_000
|
refetchInterval: 3_000
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Library coverage --------------------------------------------------------
|
||||||
|
|
||||||
|
export type CoverageRollup = {
|
||||||
|
total: number;
|
||||||
|
with_art: number;
|
||||||
|
pending: number;
|
||||||
|
settled: number;
|
||||||
|
pending_no_mbid: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getCoverageRollup(): Promise<CoverageRollup> {
|
||||||
|
return api.get<CoverageRollup>('/api/admin/library/coverage');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same pacing as scan-status — 3s while polling, 30s stale-time. The
|
||||||
|
// query is cheap server-side (one aggregate per call) and we want
|
||||||
|
// the gauge to tick during a scan.
|
||||||
|
export function createCoverageQuery() {
|
||||||
|
return createQuery({
|
||||||
|
queryKey: qk.coverage(),
|
||||||
|
queryFn: getCoverageRollup,
|
||||||
|
staleTime: 30_000,
|
||||||
|
refetchInterval: 3_000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export const qk = {
|
|||||||
adminQuarantineActions: (limit?: number) =>
|
adminQuarantineActions: (limit?: number) =>
|
||||||
['adminQuarantineActions', { limit: limit ?? 50 }] as const,
|
['adminQuarantineActions', { limit: limit ?? 50 }] as const,
|
||||||
scanStatus: () => ['scanStatus'] as const,
|
scanStatus: () => ['scanStatus'] as const,
|
||||||
|
coverage: () => ['coverage'] as const,
|
||||||
suggestions: (limit?: number) =>
|
suggestions: (limit?: number) =>
|
||||||
['suggestions', { limit: limit ?? 12 }] as const,
|
['suggestions', { limit: limit ?? 12 }] as const,
|
||||||
home: () => ['home'] as const,
|
home: () => ['home'] as const,
|
||||||
|
|||||||
Reference in New Issue
Block a user