import assert from "node:assert/strict"; import { HoverIntent, sameScope, processorMatches, preparationQueue, selectSuccessfulAnnotationJobs, mergeCachedAnnotationEntries, selectAnnotationRetryJobs, annotationRetryStatus, explicitAnnotationRetry, } from "../src/work.js"; const oldProcessor = { key: "processor-v1:scope" }; assert(processorMatches(oldProcessor, "processor-v1")); assert( !processorMatches(oldProcessor, "processor-v2"), "A checked processor edit invalidates saved results even when document and feature settings are unchanged", ); assert(!processorMatches({ key: "processor-v10:scope" }, "processor-v1")); const feature = { processor: "definition", generation: "focus", scope: "word", reveal: "hover", configRevision: 2, enabled: true, }; assert( sameScope( { sourceRevision: "s", configuration: "compute", featureRevision: 2, start: 0, end: 2, status: "ready" }, "s", "compute", { start: 0, end: 2, }, ), ); assert( !sameScope( { sourceRevision: "s", configuration: "different", featureRevision: 1, start: 0, end: 2, status: "ready" }, "s", "compute", { start: 0, end: 2, }, ), ); const intent = new HoverIntent(10); let calls: string[] = []; intent.schedule(() => calls.push("old")); intent.schedule(() => calls.push("new")); await new Promise((resolve) => setTimeout(resolve, 30)); assert.deepEqual(calls, ["new"]); intent.schedule(() => calls.push("left")); intent.cancel(); await new Promise((resolve) => setTimeout(resolve, 30)); assert.deepEqual(calls, ["new"]); intent.schedule(() => calls.push("unmounted")); intent.cancel(); console.log("PASS heavy hover intent, cancellation, scopes and stale cache guards"); const scope = (key: string, configuration: string, start: number, local = false) => ({ key, configuration, range: { start, end: start + 10 }, local, }); const empty = new Set(); const a = scope("a", "pinyin", 0), b = scope("b", "pinyin", 10), c = scope("c", "pinyin", 20); assert.deepEqual( preparationQueue([a, b, c], { start: 0, end: 30 }, 0, empty, empty).admit, [a, b], "Independent sections of the same annotator can use both model slots", ); const d = scope("d", "names", 0); assert.deepEqual( preparationQueue([a, b, c, d], { start: 0, end: 30 }, 0, empty, empty).admit, [a, d], "Features take turns when their scopes are equally close", ); const full = preparationQueue([a, b, c], { start: 0, end: 30 }, 2, empty, empty); assert.deepEqual(full.admit, []); assert.deepEqual(full.waiting, [a, b, c], "Unadmitted scopes remain available for queued outlines"); assert.deepEqual( preparationQueue([a, a, b, c], { start: 0, end: 30 }, 0, new Set(["b"]), new Set(["a"])).waiting, [c], "Cached ready or failed scopes, admitted intents and shared-computation duplicates do not get queued outlines", ); assert.deepEqual( preparationQueue([a, b, c], { start: 10, end: 20 }, 1, empty, empty).admit, [b], "Visible work takes the released slot before neighboring scopes", ); const local = scope("local", "local", 20, true); assert.deepEqual( preparationQueue([a, local], { start: 0, end: 30 }, 2, empty, empty).admit, [local], "Local annotations do not wait for inference slots", ); console.log("PASS queued outlines, bounded independent scope admission, fair features and cache exclusion"); import { presentAnnotations } from "../src/annotation-display.js"; const readings = [ { start: 0, end: 2, text: "Běijīng", tag: "hsk:3" }, { start: 2, end: 4, text: "xīngqīliù", tag: "hsk:1" }, ]; assert.equal(presentAnnotations("pinyin", readings, { minimumLevel: "2" }).length, 1); assert.equal(presentAnnotations("pinyin", readings, { minimumLevel: "0" }).length, 2); assert.equal( presentAnnotations("pinyinWord", readings, { minimumLevel: "8" }).length, 2, "Hover includes easy words too", ); assert.equal( presentAnnotations("names", [{ ...readings[0], tag: "place" }], { placeColor: "#123456" })[0].color, "#123456", ); console.log("PASS local HSK filtering, all-word hover pinyin and category recoloring from raw cache"); import { renderAnnotations } from "../src/render-hooks.js"; import type { Annotation } from "../src/canvas/types.js"; const source = "🙂 Un mot littéraire."; const semantic: Annotation = { id: "item", feature: "source", start: 6, end: 9, text: "word", style: "replace", color: "#123456", }; const outputs = renderAnnotations(semantic, source, [ { style: "replace", reveal: "always" }, { style: "color", reveal: "always" }, { style: "tooltip", reveal: "hover", content: "source" }, ]); assert.deepEqual( outputs.map(({ style, text, reveal }) => ({ style, text, reveal })), [ { style: "replace", text: "word", reveal: "always" }, { style: "color", text: "word", reveal: "always" }, { style: "tooltip", text: "mot", reveal: "hover" }, ], ); assert.equal(new Set(outputs.map((output) => output.id)).size, 3); assert(outputs.every((output) => output.feature === semantic.feature && output.start === 6 && output.end === 9)); assert.equal(semantic.text, "word", "Rendering never mutates cached semantic data"); assert.deepEqual(renderAnnotations(semantic, source, undefined), [semantic]); console.log("PASS one source rendered as replacement, color and original-text hover tooltip"); assert( sameScope( { sourceRevision: "doc@1", featureRevision: 4, start: 0, end: 6, status: "ready", configuration: JSON.stringify({ processor: "pinyin", scope: "word", generation: "section", options: {} }), }, "doc@1", JSON.stringify({ processor: "pinyin", scope: "word", options: {} }), { start: 0, end: 6 }, ), "Changing preparation timing reuses a persisted semantic result", ); assert(processorMatches({ key: "verified-previous:light-fast:result" }, "current", ["verified-previous"])); assert(!processorMatches({ key: "unknown:light-fast:result" }, "current", ["verified-previous"])); const scopeConfiguration = JSON.stringify({ processor: "pinyin", scope: "word", options: {} }); const savedJob = (object: string, createdAt: string, items: unknown[], key = "current:scope") => ({ _camino: { ref: { object } }, configuration: scopeConfiguration, data: { key, kind: "annotation", status: "ready", sourceRevision: "doc@1", start: 0, end: 6, createdAt, output: JSON.stringify({ items }), }, }); const oldFacts = savedJob("old", "2026-10-01", [ { start: 0, end: 2 }, { start: 2, end: 4 }, ]), clearedFacts = savedJob("cleared", "2026-10-02", []), narrowedFacts = savedJob("narrowed", "2026-10-03", [{ start: 0, end: 2 }]), failedFacts = { ...savedJob("failed", "2026-10-04", []), data: { ...oldFacts.data, status: "failed", createdAt: "2026-10-04" }, }; const accepts = (job: typeof oldFacts) => processorMatches(job.data, "current", ["verified-previous"]); assert.deepEqual( selectSuccessfulAnnotationJobs([oldFacts, clearedFacts], accepts), [clearedFacts], "An empty successful regeneration clears its old items", ); assert.deepEqual( selectSuccessfulAnnotationJobs([oldFacts, clearedFacts, narrowedFacts, failedFacts], accepts), [narrowedFacts], "A smaller successful output replaces the whole scope; failure does not erase success", ); const oldAlias = { ...oldFacts, data: { ...oldFacts.data, key: "verified-previous:light-fast:legacy" }, configuration: JSON.stringify({ processor: "pinyin", scope: "word", generation: "section", options: {} }), }; const unknown = savedJob("unknown", "2026-10-05", [{ start: 4, end: 6 }], "unknown:scope"); assert.deepEqual( selectSuccessfulAnnotationJobs([oldAlias, clearedFacts, unknown], accepts), [clearedFacts], "Verified revision aliases and old generation formatting describe the same semantic scope; unknown revisions cannot replace it", ); const anotherRange = { ...oldFacts, data: { ...oldFacts.data, start: 6, end: 8 } }, anotherSource = { ...oldFacts, data: { ...oldFacts.data, sourceRevision: "doc@2" } }, anotherInput = { ...oldFacts, configuration: JSON.stringify({ processor: "pinyin", scope: "word", options: { targetLanguage: "French" } }), }; assert.equal( selectSuccessfulAnnotationJobs([clearedFacts, anotherRange, anotherSource, anotherInput], accepts).length, 4, "Different semantic inputs, ranges and sources retain independent results", ); const canonical = mergeCachedAnnotationEntries([{ node: oldFacts }], [{ node: narrowedFacts }, { node: failedFacts }]); assert.deepEqual( canonical.map(({ node }) => node._camino.ref.object), ["old", "failed"], "Indexed success takes precedence over newer attempt history for its exact key", ); assert.deepEqual( selectSuccessfulAnnotationJobs( canonical.map(({ node }) => node), accepts, ), [oldFacts], ); assert.deepEqual( mergeCachedAnnotationEntries([{ node: clearedFacts }, { node: oldFacts }], []), [{ node: clearedFacts }], "Overlapping pagination snapshots retain one canonical pointer per key", ); assert.deepEqual( JSON.parse(oldFacts.data.output).items, [ { start: 0, end: 2 }, { start: 2, end: 4 }, ], "Selection never mutates retained historical output", ); console.log( "PASS canonical cache pointers and whole-scope replacement, including empty/narrowed successes, aliases and failed retries", ); const retryFeature = { feature: "pinyin", configuration: scopeConfiguration, revision: "current", aliases: ["verified-previous"], styles: ["overhead"], }; const retryAttempt = (object: string, status: string, start = 0, createdAt = "2026-10-04") => ({ ...savedJob(object, createdAt, []), data: { ...savedJob(object, createdAt, []).data, status, start, end: start + 6 }, }); const skipped = retryAttempt("skipped", "skipped"), failed = retryAttempt("failed", "failed", 6), uncertain = retryAttempt("uncertain", "uncertain", 12), offPage = retryAttempt("off-page", "skipped", 30), ready = retryAttempt("ready", "ready", 18), queued = retryAttempt("queued", "queued", 24); const retryHistory = [skipped, failed, uncertain, offPage, ready, queued]; const retryHistorySnapshot = structuredClone(retryHistory); const visiblePage = { start: 0, end: 30 }; assert.deepEqual( selectAnnotationRetryJobs( retryHistory, [retryFeature, { ...retryFeature, feature: "hover pinyin", styles: ["tooltip"] }], "doc@1", visiblePage, ).map(({ feature, job }) => [feature, job._camino.ref.object]), [ ["pinyin", "skipped"], ["pinyin", "failed"], ["pinyin", "uncertain"], ], "Failed/skipped/uncertain visible scopes appear once across shared annotators; queued/ready/off-page scopes do not", ); const legacySkipped = { ...skipped, configuration: oldAlias.configuration, data: { ...skipped.data, key: "verified-previous:legacy", createdAt: "2026-10-03" }, }; assert.equal( selectAnnotationRetryJobs([legacySkipped, skipped], [retryFeature], "doc@1", visiblePage).length, 1, "Verified aliases and legacy scheduling configuration deduplicate the same scope", ); for (const status of ["ready", "queued"]) { assert.deepEqual( selectAnnotationRetryJobs( [skipped, legacySkipped, retryAttempt("replacement", status, 0, "2026-10-05")], [retryFeature], "doc@1", visiblePage, ), [], "Newer queued or successful work supersedes old errors", ); } assert.deepEqual( selectAnnotationRetryJobs([skipped], [{ ...retryFeature, styles: ["sidebar"] }], "doc@1", visiblePage), [], ); assert.deepEqual( selectAnnotationRetryJobs([skipped], [{ ...retryFeature, styles: ["underline", "sidebar"] }], "doc@1", visiblePage), [], "Multi-hook sidebar processors already have controls", ); assert.deepEqual( selectAnnotationRetryJobs( [skipped], [retryFeature, { ...retryFeature, feature: "notes", styles: ["sidebar"] }], "doc@1", visiblePage, ), [], "A shared sidebar renderer does not get duplicate controls", ); assert.deepEqual(selectAnnotationRetryJobs([skipped], [retryFeature], "doc@2", visiblePage), []); assert.deepEqual(selectAnnotationRetryJobs([skipped], [retryFeature], "doc@1", { start: 0, end: 0 }), []); assert.deepEqual( selectAnnotationRetryJobs( [{ ...skipped, data: { ...skipped.data, kind: "variant" } }], [retryFeature], "doc@1", visiblePage, ), [], ); assert.deepEqual( selectAnnotationRetryJobs( [{ ...unknown, data: { ...unknown.data, status: "failed" } }], [retryFeature], "doc@1", visiblePage, ), [], ); assert.deepEqual( selectAnnotationRetryJobs( [{ ...anotherInput, data: { ...anotherInput.data, status: "failed" } }], [retryFeature], "doc@1", visiblePage, ), [], ); assert.deepEqual( retryHistory, retryHistorySnapshot, "Status selection preserves all cached values and attempt history", ); assert.equal( annotationRetryStatus("skipped", "AGENT_SETUP_UNAVAILABLE: Authentication required"), "Setup was unavailable. Try again when connected.", ); assert.equal(annotationRetryStatus("uncertain", "raw transport error"), "Preparation could not be confirmed."); const retrying = new Set(); let explicitCalls = 0, finishRetry: () => void = () => {}, rerenders = 0; const dispatch = (settled: () => void) => { explicitCalls++; // The view delegates the original scope with retry:true, without replanning. const { feature, job } = selectAnnotationRetryJobs([skipped], [retryFeature], "doc@1", visiblePage)[0]; assert.deepEqual( { feature, start: job.data.start, end: job.data.end, retry: true }, { feature: "pinyin", start: 0, end: 6, retry: true }, ); finishRetry = settled; return true; }; assert.equal(explicitCalls, 0, "Selecting failed statuses never dispatches inference"); assert(explicitAnnotationRetry("skipped", retrying, dispatch, () => rerenders++)); assert(!explicitAnnotationRetry("skipped", retrying, dispatch, () => rerenders++)); assert.equal(explicitCalls, 1, "Double clicks cannot repeat an in-flight retry"); finishRetry(); assert.equal(explicitCalls, 1, "Settlement never automatically retries"); assert.equal(retrying.size, 0); assert.equal(rerenders, 2); assert( !explicitAnnotationRetry( "not-ready", retrying, () => false, () => {}, ), ); assert.equal(retrying.size, 0, "An unavailable operation releases the button"); assert.throws(() => explicitAnnotationRetry( "rejected", retrying, () => { throw Error("rejected"); }, () => {}, ), ); assert.equal(retrying.size, 0); console.log( "PASS explicit nearby annotator retries, shared identities/aliases, sidebar/off-page exclusion and superseded attempts", );