* feat(db): add pair schema and stage restore migrations for #193 * feat(database): persist reusable ordered prompt pairs (#193) * feat(api): expose reusable pairs and atomic metadata enrichment (#193) * feat(metadata): resolve canonical prompt pairs with bounded provenance (#193) * feat(scan): enrich image pairs without guessing or duplicate links (#193) * fix(database): preserve pair tags during global maintenance (#193) * feat(tracking): enrich live image pairs and cancel scans safely (#193) * feat(nodes): encode paired prompts and count cached recipe runs (#193) * fix(database): validate restored pair ownership and provenance (#193) * feat(admin): report pair scan outcomes and cancellation (#193) * feat(ui): add paired prompt editor and explicit recipe library for #193 * fix(autocomplete): include reusable pair metadata vocabulary (#193) * fix(pairs): honor recent sorting and conservative scan outcomes (#193) * fix(ui): protect paired draft saves and clarify negative variants (#193) * fix(metadata): preserve bounded paired execution evidence (#193) * fix(tracking): follow actual pair output roles and prefer execution evidence (#193) * docs(pairs): document live validation and retain renderer screenshots (#193) * test(api): inject disposable databases before API startup (#194) * test(api): canonicalize portable temporary gallery roots (#194) * docs(ui): prototype branching negative variant history (#193) * docs(ui): show selected negative beneath positive in timeline concept (#193) * docs: retain approved timeline for later gallery redesign (#193) * test(db): close restore fixture connections before cleanup (#194) * test(db): reuse existing handle guards in all pair fixtures (#194) * chore: clarify rollback test and intentional exception handling (#193)
106 lines
5.4 KiB
JavaScript
106 lines
5.4 KiB
JavaScript
// Run with: node --test tests/js/
|
|
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const {
|
|
initialState,
|
|
reduceScanEvent,
|
|
fromStatus,
|
|
dismissAction,
|
|
progressLabel,
|
|
} = require("../../web/js/scan-progress.js");
|
|
|
|
test("initialState is idle with zero counts", () => {
|
|
assert.deepEqual(initialState(), {
|
|
status: "idle", progress: 0, statusText: "", processed: 0, found: 0, added: 0, linked: 0, message: "",
|
|
});
|
|
});
|
|
|
|
test("a progress event moves the scan to running and copies the counts", () => {
|
|
const before = initialState();
|
|
const after = reduceScanEvent(before, {
|
|
type: "progress", progress: 42, status: "Processing file 84/200...", processed: 84, found: 12,
|
|
});
|
|
assert.equal(after.status, "running");
|
|
assert.equal(after.progress, 42);
|
|
assert.equal(after.statusText, "Processing file 84/200...");
|
|
assert.equal(after.processed, 84);
|
|
assert.equal(after.found, 12);
|
|
assert.equal(before.status, "idle", "reducer must not mutate its input");
|
|
});
|
|
|
|
test("a complete event finishes the scan with the final totals", () => {
|
|
const running = reduceScanEvent(initialState(), { type: "progress", progress: 90, status: "x", processed: 9, found: 1 });
|
|
const done = reduceScanEvent(running, { type: "complete", processed: 10, found: 3, added: 2, linked: 1 });
|
|
assert.equal(done.status, "done");
|
|
assert.equal(done.progress, 100);
|
|
assert.deepEqual([done.processed, done.found, done.added, done.linked], [10, 3, 2, 1]);
|
|
});
|
|
|
|
test("an error event fails the scan and keeps the server message", () => {
|
|
const failed = reduceScanEvent(initialState(), { type: "error", message: "No output directories found." });
|
|
assert.equal(failed.status, "failed");
|
|
assert.equal(failed.message, "No output directories found.");
|
|
});
|
|
|
|
test("unknown events leave the state untouched", () => {
|
|
const state = reduceScanEvent(initialState(), { type: "progress", progress: 5, status: "s", processed: 1, found: 0 });
|
|
assert.equal(reduceScanEvent(state, { type: "mystery" }), state);
|
|
assert.equal(reduceScanEvent(state, null), state);
|
|
});
|
|
|
|
test("fromStatus rebuilds a running scan from the status endpoint", () => {
|
|
const state = fromStatus({
|
|
running: true,
|
|
last_event: { type: "progress", progress: 30, status: "Processing file 3/10...", processed: 3, found: 1 },
|
|
});
|
|
assert.equal(state.status, "running");
|
|
assert.equal(state.progress, 30);
|
|
assert.equal(state.processed, 3);
|
|
});
|
|
|
|
test("fromStatus is idle when nothing is running, even if a last event exists", () => {
|
|
assert.equal(fromStatus({ running: false, last_event: { type: "complete", processed: 10 } }).status, "idle");
|
|
assert.equal(fromStatus({ running: false, last_event: null }).status, "idle");
|
|
assert.equal(fromStatus(null).status, "idle");
|
|
});
|
|
|
|
test("dismissing the modal minimizes a running scan and closes otherwise", () => {
|
|
const running = reduceScanEvent(initialState(), { type: "progress", progress: 1, status: "s", processed: 0, found: 0 });
|
|
assert.equal(dismissAction(running), "minimize");
|
|
assert.equal(dismissAction(initialState()), "close");
|
|
assert.equal(dismissAction(reduceScanEvent(running, { type: "complete" })), "close");
|
|
});
|
|
|
|
test("progressLabel summarises a running scan for the minimized pill", () => {
|
|
const running = reduceScanEvent(initialState(), { type: "progress", progress: 42, status: "s", processed: 84, found: 12 });
|
|
assert.equal(progressLabel(running), "Scanning 42% · 84 files · 12 prompts");
|
|
assert.equal(progressLabel(initialState()), "");
|
|
});
|
|
|
|
test('pair totals survive completion and cancellation closes the running state', () => {
|
|
const running=reduceScanEvent(initialState(),{type:'progress',progress:30,processed:3});
|
|
const done=reduceScanEvent(running,{type:'complete',pairs_added:2,pair_links_added:3,enriched:1,unchanged:4,ambiguous:1,failed:2});
|
|
assert.deepEqual([done.pairs_added,done.pair_links_added,done.enriched,done.unchanged,done.ambiguous,done.failed],[2,3,1,4,1,2]);
|
|
const cancelled=reduceScanEvent(running,{type:'cancelled',processed:3,pairs_added:1});
|
|
assert.equal(cancelled.status,'done');
|
|
assert.equal(cancelled.cancelled,true);
|
|
assert.equal(cancelled.statusText,'Scan cancelled.');
|
|
assert.equal(cancelled.progress,30);
|
|
});
|
|
|
|
test('completion label reports pair enrichment, unresolved work and failures', () => {
|
|
const {completionLabel}=require('../../web/js/scan-progress.js');
|
|
const state=reduceScanEvent(initialState(),{type:'complete',processed:10,found:8,added:1,linked:2,pairs_added:3,pair_links_added:4,enriched:2,unchanged:1,ambiguous:1,unresolved:2,conflict:1,failed:1});
|
|
assert.match(completionLabel(state),/3 new pairs, 4 pair links/);
|
|
assert.match(completionLabel(state),/4 need review, 1 failed/);
|
|
assert.match(completionLabel({...state,cancelled:true}),/^Scan cancelled!/);
|
|
});
|
|
|
|
test('older servers without pair counts retain a complete readable summary', () => {
|
|
const {completionLabel}=require('../../web/js/scan-progress.js');
|
|
const state=reduceScanEvent(initialState(),{type:'complete',processed:2,found:1,added:1,linked:0});
|
|
assert.match(completionLabel(initialState()),/0 new pairs, 0 pair links; 0 need review, 0 failed/);
|
|
assert.match(completionLabel(state),/0 enriched, 0 unchanged/);
|
|
assert.equal(fromStatus({running:true,last_event:{type:'unknown'}}).status,'running');
|
|
});
|