polygonhunter_searchosaurus/tests/ocr-queue.test.ts
polygonhunter d24010ec14 M5: offline OCR and PDF text extraction
Images run through a persistent tesseract.js worker (deu+eng
tessdata_fast), PDFs through Obsidian's bundled pdf.js — both in idle
time on a serial deduplicating queue, feeding a low-weight
extractedText field. Results live in a synced ocr-cache.json keyed by
path+mtime+size+langs, so no device ever recognizes a file twice.

The ~7 MB runtime (worker, WASM core, language models) is not bundled:
a fixed ocr-assets-v1 release (built by the new ocr-assets workflow)
is downloaded once into the plugin dir when the user enables OCR, then
everything is offline. Dev builds copy the same files from assets/ocr
(npm run fetch-ocr-assets) into the test vault. Settings gain the OCR
toggle, language choice, and PDF switch; mobile degrades gracefully.

Verified end-to-end: a rendered test image round-trips through the
bundled models as "Angebot für die Küche / Rechnung Nr. 4711".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 08:18:13 +00:00

91 lines
2.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { OcrQueue, type Scheduler } from "../src/core/ocr-queue";
/** Runs scheduled work immediately — jobs still resolve as microtasks. */
const sync: Scheduler = (run) => run();
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
describe("OcrQueue", () => {
it("runs jobs serially in order", async () => {
const queue = new OcrQueue(sync);
const order: string[] = [];
queue.push("a", async () => void order.push("a"));
queue.push("b", async () => void order.push("b"));
await tick();
expect(order).toEqual(["a", "b"]);
});
it("runs high-priority jobs before queued low ones", async () => {
let release: () => void = () => undefined;
const gate = new Promise<void>((resolve) => (release = resolve));
const queue = new OcrQueue(sync);
const order: string[] = [];
queue.push("blocker", async () => gate); // occupies the queue
queue.push("low", async () => void order.push("low"), "low");
queue.push("high", async () => void order.push("high"), "high");
release();
await tick();
expect(order).toEqual(["high", "low"]);
});
it("dedupes by key", async () => {
const queue = new OcrQueue(sync);
let runs = 0;
let release: () => void = () => undefined;
const gate = new Promise<void>((resolve) => (release = resolve));
queue.push("blocker", async () => gate);
queue.push("x", async () => void runs++);
queue.push("x", async () => void runs++); // ignored
release();
await tick();
expect(runs).toBe(1);
});
it("re-allows a key after its job finished", async () => {
const queue = new OcrQueue(sync);
let runs = 0;
queue.push("x", async () => void runs++);
await tick();
queue.push("x", async () => void runs++);
await tick();
expect(runs).toBe(2);
});
it("drop removes a queued job", async () => {
let release: () => void = () => undefined;
const gate = new Promise<void>((resolve) => (release = resolve));
const queue = new OcrQueue(sync);
let ran = false;
queue.push("blocker", async () => gate);
queue.push("victim", async () => void (ran = true));
queue.drop("victim");
release();
await tick();
expect(ran).toBe(false);
});
it("pause holds jobs, resume runs them", async () => {
const queue = new OcrQueue(sync);
let ran = false;
queue.pause();
queue.push("x", async () => void (ran = true));
await tick();
expect(ran).toBe(false);
queue.resume();
await tick();
expect(ran).toBe(true);
});
it("a throwing job does not stall the queue", async () => {
const queue = new OcrQueue(sync);
const order: string[] = [];
queue.push("bad", async () => {
order.push("bad");
throw new Error("boom");
});
queue.push("good", async () => void order.push("good"));
await tick();
expect(order).toEqual(["bad", "good"]);
});
});