mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix(ocr): unblock and harden accurate-OCR install (#552)
Two OCR-install fixes surfaced while verifying the accurate-OCR (v3 runtime) path end to end: - Installer timeout must be a safe integer, not a performance.now() float. With the default INSTALL_MAX_MS this failed every accurate-OCR install via the app right after the download (masked by the unpublished runtime; CI drives install_runtime.py directly so it never surfaced). Fixed via remainingInstallerTimeoutMs(). - Classify an absent or forbidden runtime index (401/403/404/410) as OcrRuntimeNotPublishedError with a clear "Fast OCR still works" message instead of a raw HTTP 404, without retrying. Refs #552
This commit is contained in:
@@ -28,10 +28,12 @@ import {
|
||||
downloadVerifiedRuntimeRelease as downloadVerifiedRuntimeReleaseWithLease,
|
||||
loadOcrRuntimeTrustKeys,
|
||||
OcrRuntimeImportValidationError,
|
||||
OcrRuntimeNotPublishedError,
|
||||
type OcrRuntimeTrustKey,
|
||||
prepareOfflineRuntimeIndex,
|
||||
prepareOfflineRuntimeRelease,
|
||||
purgeOcrRuntimeDownloads as purgeOcrRuntimeDownloadsWithLease,
|
||||
remainingInstallerTimeoutMs,
|
||||
verifyRuntimeIndex,
|
||||
writeBufferFully,
|
||||
} from "../../../apps/api/src/lib/ocr-runtime-install.js";
|
||||
@@ -157,6 +159,28 @@ function signedIndex(
|
||||
return { artifact, index, raw: Buffer.from(canonicalRuntimeJson(index)), trustKey };
|
||||
}
|
||||
|
||||
describe("remainingInstallerTimeoutMs", () => {
|
||||
it("floors a fractional remaining budget to the safe integer the installer requires", () => {
|
||||
// The deadline is set at one performance.now() read and the remaining time is
|
||||
// computed at a later read, so `deadline - later` is fractional in practice.
|
||||
const started = 1_000.4269;
|
||||
const deadline = started + 7_200_000;
|
||||
const later = 1_500.8731;
|
||||
// The old caller passed this raw float; runOcrRuntimeInstaller rejected it as
|
||||
// a non-integer timeout, which broke every default-config install.
|
||||
expect(Number.isSafeInteger(deadline - later)).toBe(false);
|
||||
const remaining = remainingInstallerTimeoutMs(deadline, later);
|
||||
expect(Number.isSafeInteger(remaining)).toBe(true);
|
||||
expect(remaining).toBe(Math.floor(deadline - later));
|
||||
});
|
||||
|
||||
it("returns 0 when there is no deadline and clamps a passed deadline to at least 1ms", () => {
|
||||
expect(remainingInstallerTimeoutMs(undefined, 5)).toBe(0);
|
||||
expect(remainingInstallerTimeoutMs(10.9, 10.1)).toBe(1);
|
||||
expect(remainingInstallerTimeoutMs(5, 999.7)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("verifyRuntimeIndex", () => {
|
||||
it("loads the independently pinned release key from the official image environment", () => {
|
||||
const fixture = signedIndex();
|
||||
@@ -451,6 +475,64 @@ describe("downloadVerifiedRuntimeRelease", () => {
|
||||
expect(fetchImpl).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("reports a clear not-published error when the runtime index is absent", async () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), "snapotter-ocr-index-absent-"));
|
||||
temporaryDirectories.push(directory);
|
||||
const canceled = vi.fn();
|
||||
const missingBody = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(Buffer.from("Entry not found"));
|
||||
},
|
||||
cancel: canceled,
|
||||
});
|
||||
const fetchImpl = vi
|
||||
.fn<typeof fetch>()
|
||||
.mockResolvedValue(new Response(missingBody, { status: 404 }));
|
||||
|
||||
const error = await downloadVerifiedRuntimeRelease({
|
||||
aiDataDir: directory,
|
||||
bundleRepo: "snapotter-hq/feature-bundles",
|
||||
version: "2.1.0",
|
||||
target: TARGET,
|
||||
trustKeys: [],
|
||||
fetchImpl,
|
||||
}).then(
|
||||
() => {
|
||||
throw new Error("expected the absent runtime index to reject");
|
||||
},
|
||||
(reason: unknown) => reason,
|
||||
);
|
||||
|
||||
expect(error).toBeInstanceOf(OcrRuntimeNotPublishedError);
|
||||
const message = (error as Error).message;
|
||||
expect(message).toContain("2.1.0");
|
||||
expect(message).toContain("Fast OCR");
|
||||
expect(message).toContain("404");
|
||||
// Absent means absent: this is not a transient failure and must not retry.
|
||||
expect(fetchImpl).toHaveBeenCalledOnce();
|
||||
expect(canceled).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("treats a forbidden runtime index as not published rather than a hard error", async () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), "snapotter-ocr-index-forbidden-"));
|
||||
temporaryDirectories.push(directory);
|
||||
const fetchImpl = vi
|
||||
.fn<typeof fetch>()
|
||||
.mockResolvedValue(new Response("Forbidden", { status: 403 }));
|
||||
|
||||
await expect(
|
||||
downloadVerifiedRuntimeRelease({
|
||||
aiDataDir: directory,
|
||||
bundleRepo: "snapotter-hq/feature-bundles",
|
||||
version: "2.1.0",
|
||||
target: TARGET,
|
||||
trustKeys: [],
|
||||
fetchImpl,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(OcrRuntimeNotPublishedError);
|
||||
expect(fetchImpl).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not retry an oversized index and cancels its unconsumed body", async () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), "snapotter-ocr-index-size-policy-"));
|
||||
temporaryDirectories.push(directory);
|
||||
|
||||
Reference in New Issue
Block a user