feat(web/m7-scan-progress): stageState inference + tests
Pure-function module that derives per-stage StageState (one of pending / in_progress / done / skipped) from the existing ScanStatus payload. No new query, no new endpoint — reuses what createScanStatusQuery already polls. Truth table covered by tests: scan undefined → skipped; in-flight with no tallies → all pending; in-flight with library tally only → library in_progress, rest pending; in-flight with library + mbid_backfill → library done, mbid in_progress; finished with all tallies → all done; finished with missing library tally → library skipped, others done; in-flight with last stage tally → only last stage in_progress (all earlier done).
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { stageState } from './stage-state';
|
||||
import type { ScanStatus } from '$lib/api/admin';
|
||||
|
||||
const FINISHED_AT = '2026-05-06T12:00:00Z';
|
||||
|
||||
function makeScan(partial: Partial<ScanStatus>): ScanStatus {
|
||||
return {
|
||||
id: 'test-id',
|
||||
started_at: '2026-05-06T11:00:00Z',
|
||||
finished_at: null,
|
||||
in_flight: true,
|
||||
...partial
|
||||
} as ScanStatus;
|
||||
}
|
||||
|
||||
describe('stageState', () => {
|
||||
it('returns skipped when scan is undefined', () => {
|
||||
expect(stageState(undefined, 'library')).toBe('skipped');
|
||||
expect(stageState(undefined, 'cover_enrich')).toBe('skipped');
|
||||
});
|
||||
|
||||
it('in-flight + no tallies → all stages pending', () => {
|
||||
const scan = makeScan({ in_flight: true });
|
||||
expect(stageState(scan, 'library')).toBe('pending');
|
||||
expect(stageState(scan, 'mbid_backfill')).toBe('pending');
|
||||
expect(stageState(scan, 'cover_enrich')).toBe('pending');
|
||||
expect(stageState(scan, 'artist_art_enrich')).toBe('pending');
|
||||
});
|
||||
|
||||
it('in-flight + only library tally → library in_progress, rest pending', () => {
|
||||
const scan = makeScan({
|
||||
in_flight: true,
|
||||
library: { scanned: 100, added: 1, updated: 0, skipped: 99, errored: 0 }
|
||||
});
|
||||
expect(stageState(scan, 'library')).toBe('in_progress');
|
||||
expect(stageState(scan, 'mbid_backfill')).toBe('pending');
|
||||
expect(stageState(scan, 'cover_enrich')).toBe('pending');
|
||||
expect(stageState(scan, 'artist_art_enrich')).toBe('pending');
|
||||
});
|
||||
|
||||
it('in-flight + library + mbid_backfill tallies → library done, mbid in_progress', () => {
|
||||
const scan = makeScan({
|
||||
in_flight: true,
|
||||
library: { scanned: 100, added: 1, updated: 0, skipped: 99, errored: 0 },
|
||||
mbid_backfill: { processed: 50, healed: 40, skipped: 10, duplicates: 0 }
|
||||
});
|
||||
expect(stageState(scan, 'library')).toBe('done');
|
||||
expect(stageState(scan, 'mbid_backfill')).toBe('in_progress');
|
||||
expect(stageState(scan, 'cover_enrich')).toBe('pending');
|
||||
expect(stageState(scan, 'artist_art_enrich')).toBe('pending');
|
||||
});
|
||||
|
||||
it('finished + all tallies → all done', () => {
|
||||
const scan = makeScan({
|
||||
in_flight: false,
|
||||
finished_at: FINISHED_AT,
|
||||
library: { scanned: 100, added: 1, updated: 0, skipped: 99, errored: 0 },
|
||||
mbid_backfill: { processed: 50, healed: 40, skipped: 10, duplicates: 0 },
|
||||
cover_enrich: { processed: 30, succeeded: 25, failed: 5 },
|
||||
artist_art_enrich: { processed: 20, succeeded: 18, failed: 2 }
|
||||
});
|
||||
expect(stageState(scan, 'library')).toBe('done');
|
||||
expect(stageState(scan, 'mbid_backfill')).toBe('done');
|
||||
expect(stageState(scan, 'cover_enrich')).toBe('done');
|
||||
expect(stageState(scan, 'artist_art_enrich')).toBe('done');
|
||||
});
|
||||
|
||||
it('finished + missing library tally → library skipped, others done if present', () => {
|
||||
const scan = makeScan({
|
||||
in_flight: false,
|
||||
finished_at: FINISHED_AT,
|
||||
mbid_backfill: { processed: 50, healed: 40, skipped: 10, duplicates: 0 },
|
||||
cover_enrich: { processed: 30, succeeded: 25, failed: 5 }
|
||||
});
|
||||
expect(stageState(scan, 'library')).toBe('skipped');
|
||||
expect(stageState(scan, 'mbid_backfill')).toBe('done');
|
||||
expect(stageState(scan, 'cover_enrich')).toBe('done');
|
||||
expect(stageState(scan, 'artist_art_enrich')).toBe('skipped');
|
||||
});
|
||||
|
||||
it('in-flight + last stage has tally → last stage in_progress, all earlier done', () => {
|
||||
const scan = makeScan({
|
||||
in_flight: true,
|
||||
library: { scanned: 100, added: 1, updated: 0, skipped: 99, errored: 0 },
|
||||
mbid_backfill: { processed: 50, healed: 40, skipped: 10, duplicates: 0 },
|
||||
cover_enrich: { processed: 30, succeeded: 25, failed: 5 },
|
||||
artist_art_enrich: { processed: 5, succeeded: 4, failed: 1 }
|
||||
});
|
||||
expect(stageState(scan, 'library')).toBe('done');
|
||||
expect(stageState(scan, 'mbid_backfill')).toBe('done');
|
||||
expect(stageState(scan, 'cover_enrich')).toBe('done');
|
||||
expect(stageState(scan, 'artist_art_enrich')).toBe('in_progress');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { ScanStatus } from '$lib/api/admin';
|
||||
|
||||
export type StageState = 'pending' | 'in_progress' | 'done' | 'skipped';
|
||||
|
||||
export type StageKey =
|
||||
| 'library'
|
||||
| 'mbid_backfill'
|
||||
| 'cover_enrich'
|
||||
| 'artist_art_enrich';
|
||||
|
||||
const ORDER: readonly StageKey[] = [
|
||||
'library',
|
||||
'mbid_backfill',
|
||||
'cover_enrich',
|
||||
'artist_art_enrich'
|
||||
];
|
||||
|
||||
// stageState derives the per-stage state from a ScanStatus snapshot.
|
||||
//
|
||||
// Truth table:
|
||||
// in_progress = scan in-flight AND this stage's tally is non-empty AND
|
||||
// no later stage has started yet.
|
||||
// done = scan finished_at is set, OR a later stage has a tally
|
||||
// (this stage ran to completion before the next started).
|
||||
// pending = scan in-flight AND this stage hasn't started.
|
||||
// skipped = scan finished AND this stage has no tally
|
||||
// (e.g. ScanOnStartup=false skips the library walk).
|
||||
export function stageState(scan: ScanStatus | undefined, stage: StageKey): StageState {
|
||||
if (!scan) return 'skipped';
|
||||
const idx = ORDER.indexOf(stage);
|
||||
const thisStage = scan[stage];
|
||||
const inFlight = scan.in_flight === true;
|
||||
|
||||
let highestStarted = -1;
|
||||
for (let i = 0; i < ORDER.length; i++) {
|
||||
if (scan[ORDER[i]]) highestStarted = i;
|
||||
}
|
||||
|
||||
if (thisStage) {
|
||||
if (!inFlight) return 'done';
|
||||
if (highestStarted > idx) return 'done';
|
||||
return 'in_progress';
|
||||
}
|
||||
|
||||
if (inFlight) return 'pending';
|
||||
return 'skipped';
|
||||
}
|
||||
Reference in New Issue
Block a user