fix: release QA hardening across processing, media, security, and CI gates (#649)

A release-readiness QA pass over the whole product. The commits split into
defects a user would hit and gates that were reporting green while measuring
nothing.

## Fixes that change behaviour

Rate limiting was bypassable on every install: TRUST_PROXY defaulted to true, so
request.ip came from a client-set header and a forged X-Forwarded-For got past
the login limiter. The default is now a private-network trust list.

A transient Postgres outage stranded in-flight jobs, leaving finished output on
disk with no row pointing at it. A reconciler now resolves those rows and adopts
the bytes rather than dropping the work.

A Redis connection that moved to a new address wedged every read-blocked
consumer, so completions stopped signalling while health still answered 200.
Socket timeouts plus subscriber pings recover it.

Installing more than one AI bundle left the shared venv multi-versioned and
silently broke three tools. The installer now reconciles distributions to one
version each.

Converting an image to JXL at quality 1 through 4 returned a 500, because
libjxl 0.7 rejects the distance those values compute. The quality is floored at
what the encoder honours. A missing ffmpeg was also reported to the user as a
corrupt upload; it now says the engine is unavailable.

RAW uploads reached an unpatched LibRaw on arm64, so it is built from source at
0.22.2, and the release scan was split so it can fail on an unfixed critical
instead of hiding it behind ignore-unfixed.

## Gates that could not fail

Two mutation lanes ran zero mutants because Stryker crawled the gitignored docs
build; coverage discarded its whole report on any failing test; the lint gate
skipped root tests, scripts, and two workspaces; and several generated matrices
counted a host missing ffmpeg as a passing tool. Each now measures what it
claims.

Full evidence and the outstanding release items are tracked locally and are not
part of this branch.
This commit is contained in:
SnapOtter
2026-07-27 15:37:30 +08:00
committed by GitHub
parent bc32f86a07
commit d10d0f544f
855 changed files with 54564 additions and 13092 deletions
+13
View File
@@ -127,6 +127,19 @@ describe("removeBackground", () => {
});
});
it("forwards AbortSignal to Python without serializing it into sidecar options", async () => {
const controller = new AbortController();
await removeBackground(FAKE_INPUT, FAKE_OUTPUT_DIR, {
model: "u2net",
signal: controller.signal,
});
const [, args, bridgeOptions] = vi.mocked(runPythonWithProgress).mock.calls[0];
expect(JSON.parse(args[2])).toEqual({ model: "u2net" });
expect(bridgeOptions).toEqual(expect.objectContaining({ signal: controller.signal }));
});
it("converts input to PNG via sharp before writing to disk", async () => {
await removeBackground(FAKE_INPUT, FAKE_OUTPUT_DIR);
+133
View File
@@ -0,0 +1,133 @@
import { type ChildProcess, spawn } from "node:child_process";
import { EventEmitter } from "node:events";
import { Writable } from "node:stream";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("node:child_process", () => ({ spawn: vi.fn() }));
interface MockProcess {
process: ChildProcess;
stdinWrites: string[];
stdout: EventEmitter;
stderr: EventEmitter;
emit: (event: string, ...args: unknown[]) => void;
}
function createMockProcess(): MockProcess {
const stdinWrites: string[] = [];
const stdin = new Writable({
write(chunk, _encoding, callback) {
stdinWrites.push(chunk.toString());
callback();
},
});
const stdout = new EventEmitter();
const stderr = new EventEmitter();
const process = new EventEmitter() as unknown as ChildProcess;
Object.assign(process, {
stdin,
stdout,
stderr,
pid: 12345,
killed: false,
kill: vi.fn(() => {
(process as { killed: boolean }).killed = true;
return true;
}),
});
return {
process,
stdinWrites,
stdout,
stderr,
emit: (event, ...args) => process.emit(event, ...args),
};
}
describe("Python bridge cancellation", () => {
let shutdownDispatcher: (() => void) | undefined;
beforeEach(() => {
vi.resetModules();
vi.mocked(spawn).mockReset();
delete process.env.PROCESSING_TIMEOUT_S;
});
afterEach(() => {
shutdownDispatcher?.();
shutdownDispatcher = undefined;
delete process.env.PROCESSING_TIMEOUT_S;
vi.restoreAllMocks();
});
it("aborts and terminates a per-request Python process even when timeouts are unlimited", async () => {
process.env.PROCESSING_TIMEOUT_S = "0";
const dispatcher = createMockProcess();
const request = createMockProcess();
vi.mocked(spawn).mockReturnValueOnce(dispatcher.process).mockReturnValueOnce(request.process);
const bridge = await import("../../../packages/ai/src/bridge.js");
shutdownDispatcher = bridge.shutdownDispatcher;
const controller = new AbortController();
const result = bridge.runPythonWithProgress("test.py", [], { signal: controller.signal });
controller.abort("job canceled");
request.stdout.emit("data", Buffer.from('{"success":true}\n'));
request.emit("close", 0, null);
await expect(result).rejects.toMatchObject({ name: "AbortError" });
expect(request.process.kill).toHaveBeenCalledWith("SIGTERM");
});
it("aborts a dispatcher request, kills that dispatcher, and ignores its stale response", async () => {
const dispatcher = createMockProcess();
vi.mocked(spawn).mockReturnValue(dispatcher.process);
const bridge = await import("../../../packages/ai/src/bridge.js");
shutdownDispatcher = bridge.shutdownDispatcher;
const ready = bridge.initDispatcher();
dispatcher.stderr.emit("data", Buffer.from('{"ready":true,"gpu":false}\n'));
await ready;
const controller = new AbortController();
const result = bridge.runPythonWithProgress("test.py", [], { signal: controller.signal });
const request = JSON.parse(dispatcher.stdinWrites.at(-1) ?? "{}") as { id?: string };
controller.abort("job canceled");
dispatcher.stdout.emit(
"data",
Buffer.from(
`${JSON.stringify({ id: request.id, exitCode: 0, stdout: '{"success":true}' })}\n`,
),
);
await expect(result).rejects.toMatchObject({ name: "AbortError" });
expect(dispatcher.process.kill).toHaveBeenCalledWith("SIGTERM");
});
it("terminates only the active ENOENT fallback attempt when cancellation races startup", async () => {
const dispatcher = createMockProcess();
const missingVenvPython = createMockProcess();
const fallbackPython = createMockProcess();
vi.mocked(spawn)
.mockReturnValueOnce(dispatcher.process)
.mockReturnValueOnce(missingVenvPython.process)
.mockReturnValueOnce(fallbackPython.process);
const bridge = await import("../../../packages/ai/src/bridge.js");
shutdownDispatcher = bridge.shutdownDispatcher;
const controller = new AbortController();
const result = bridge.runPythonWithProgress("test.py", [], { signal: controller.signal });
const enoent = Object.assign(new Error("spawn ENOENT"), { code: "ENOENT" });
missingVenvPython.emit("error", enoent);
controller.abort("job canceled");
fallbackPython.stdout.emit("data", Buffer.from('{"success":true}\n'));
fallbackPython.emit("close", 0, null);
missingVenvPython.emit("close", null, "SIGTERM");
await expect(result).rejects.toMatchObject({ name: "AbortError" });
expect(fallbackPython.process.kill).toHaveBeenCalledWith("SIGTERM");
expect(missingVenvPython.process.kill).not.toHaveBeenCalled();
});
});
+45
View File
@@ -0,0 +1,45 @@
import { afterEach, describe, expect, it } from "vitest";
import { buildMinimalEnv } from "../../../packages/ai/src/bridge.js";
const modelDownloadEnvKey = "SNAPOTTER_ALLOW_MODEL_DOWNLOAD";
const originalAllowModelDownload = process.env[modelDownloadEnvKey];
function setModelDownloadPolicy(value?: string): void {
if (value === undefined) delete process.env[modelDownloadEnvKey];
else process.env[modelDownloadEnvKey] = value;
}
afterEach(() => {
setModelDownloadPolicy(originalAllowModelDownload);
});
describe("AI sidecar model download policy", () => {
it("defaults the runtime sidecar to strict offline mode", () => {
setModelDownloadPolicy();
expect(buildMinimalEnv()).toMatchObject({
SNAPOTTER_ALLOW_MODEL_DOWNLOAD: "0",
HF_HUB_OFFLINE: "1",
TRANSFORMERS_OFFLINE: "1",
});
});
it("allows an explicit model download opt-in", () => {
setModelDownloadPolicy("1");
const env = buildMinimalEnv();
expect(env.SNAPOTTER_ALLOW_MODEL_DOWNLOAD).toBe("1");
expect(env.HF_HUB_OFFLINE).toBeUndefined();
expect(env.TRANSFORMERS_OFFLINE).toBeUndefined();
});
it("keeps unknown policy values fail-closed", () => {
setModelDownloadPolicy("yes");
expect(buildMinimalEnv()).toMatchObject({
SNAPOTTER_ALLOW_MODEL_DOWNLOAD: "0",
HF_HUB_OFFLINE: "1",
TRANSFORMERS_OFFLINE: "1",
});
});
});
+55 -16
View File
@@ -1,5 +1,8 @@
import { type ChildProcess, spawn } from "node:child_process";
import { EventEmitter } from "node:events";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Writable } from "node:stream";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -455,26 +458,32 @@ describe("bridge - PYTHON_VENV_PATH env handling", () => {
delete process.env.PYTHON_VENV_PATH;
});
it("uses custom PYTHON_VENV_PATH when set", async () => {
process.env.PYTHON_VENV_PATH = "/custom/venv";
it("uses custom PYTHON_VENV_PATH when its interpreter exists", async () => {
const venvPath = mkdtempSync(join(tmpdir(), "snapotter-bridge-venv-"));
const pythonPath =
process.platform === "win32"
? join(venvPath, "Scripts", "python.exe")
: join(venvPath, "bin", "python3");
mkdirSync(join(pythonPath, ".."), { recursive: true });
writeFileSync(pythonPath, "");
process.env.PYTHON_VENV_PATH = venvPath;
const mod = await import("../../../packages/ai/src/bridge.js");
runPythonWithProgress = mod.runPythonWithProgress;
try {
const mod = await import("../../../packages/ai/src/bridge.js");
runPythonWithProgress = mod.runPythonWithProgress;
const mock = createMockProcess();
vi.mocked(spawn).mockReturnValue(mock.process);
const mock = createMockProcess();
vi.mocked(spawn).mockReturnValue(mock.process);
const promise = runPythonWithProgress("test.py", []);
mock.stdout.emit("data", Buffer.from('{"ok": true}\n'));
mock.emitEvent("close", 0, null);
await promise;
const promise = runPythonWithProgress("test.py", []);
mock.stdout.emit("data", Buffer.from('{"ok": true}\n'));
mock.emitEvent("close", 0, null);
await promise;
// At least one spawn call should use the custom venv path
const allCalls = vi.mocked(spawn).mock.calls;
const usesCustomVenv = allCalls.some(
(call) => typeof call[0] === "string" && call[0].includes("/custom/venv"),
);
expect(usesCustomVenv).toBe(true);
expect(vi.mocked(spawn).mock.calls.some((call) => call[0] === pythonPath)).toBe(true);
} finally {
rmSync(venvPath, { force: true, recursive: true });
}
});
it("passes PYTHON_VENV_PATH through to env when set", async () => {
@@ -1162,6 +1171,36 @@ describe("bridge - per-request ENOENT retry with non-ENOENT fallback error", ()
await expect(promise).rejects.toThrow("Permission denied");
});
it("ignores the missing venv child's close after the python3 fallback starts", async () => {
const mockDisp = createMockProcess();
const mockVenv = createMockProcess();
const mockFallback = createMockProcess();
let callCount = 0;
vi.mocked(spawn).mockImplementation(() => {
callCount++;
if (callCount === 1) return mockDisp.process;
if (callCount === 2) return mockVenv.process;
return mockFallback.process;
});
const promise = runPythonWithProgress("test.py", []);
const dispatcherError = new Error("ENOENT") as NodeJS.ErrnoException;
dispatcherError.code = "ENOENT";
mockDisp.emitEvent("error", dispatcherError);
const venvError = new Error("ENOENT") as NodeJS.ErrnoException;
venvError.code = "ENOENT";
mockVenv.emitEvent("error", venvError);
mockVenv.emitEvent("close", -2, null);
mockFallback.stdout.emit("data", Buffer.from('{"ok":true}\n'));
mockFallback.emitEvent("close", 0, null);
await expect(promise).resolves.toEqual({ stdout: '{"ok":true}', stderr: "" });
});
});
// ── Dispatcher error event rejects pending with extractPythonError ──
+113
View File
@@ -0,0 +1,113 @@
import type { ChildProcess } from "node:child_process";
import { spawn } from "node:child_process";
import { EventEmitter } from "node:events";
import { existsSync } from "node:fs";
import { Writable } from "node:stream";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("node:child_process", () => ({
spawn: vi.fn(),
}));
vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs")>();
return {
...actual,
existsSync: vi.fn(),
};
});
const ORIGINAL_PLATFORM = Object.getOwnPropertyDescriptor(process, "platform");
const ORIGINAL_VENV_PATH = process.env.PYTHON_VENV_PATH;
function setPlatform(platform: NodeJS.Platform): void {
Object.defineProperty(process, "platform", { configurable: true, value: platform });
}
function createMockProcess(): {
process: ChildProcess;
stderr: EventEmitter;
} {
const stdin = new Writable({
write(_chunk, _encoding, callback) {
callback();
},
});
const stdout = new EventEmitter();
const stderr = new EventEmitter();
const process = new EventEmitter() as unknown as ChildProcess;
Object.assign(process, {
stdin,
stdout,
stderr,
pid: 12345,
killed: false,
kill: vi.fn(() => true),
});
return { process, stderr };
}
async function dispatcherExecutable(): Promise<string> {
const child = createMockProcess();
vi.mocked(spawn).mockReturnValue(child.process);
const { PythonDispatcher } = await import("../../../packages/ai/src/bridge.js");
const dispatcher = new PythonDispatcher({ profile: "docs" });
const initialized = dispatcher.init(500);
child.stderr.emit("data", Buffer.from('{"ready":true,"gpu":false}\n'));
await expect(initialized).resolves.toEqual({ ready: true, gpu: false });
const executable = vi.mocked(spawn).mock.calls[0]?.[0];
dispatcher.shutdown();
expect(typeof executable).toBe("string");
return executable as string;
}
describe("bridge Python interpreter selection", () => {
beforeEach(() => {
vi.resetModules();
vi.mocked(spawn).mockReset();
vi.mocked(existsSync).mockReset();
delete process.env.PYTHON_VENV_PATH;
});
afterEach(() => {
if (ORIGINAL_PLATFORM) Object.defineProperty(process, "platform", ORIGINAL_PLATFORM);
if (ORIGINAL_VENV_PATH === undefined) delete process.env.PYTHON_VENV_PATH;
else process.env.PYTHON_VENV_PATH = ORIGINAL_VENV_PATH;
});
it("selects an existing Unix venv interpreter", async () => {
setPlatform("linux");
process.env.PYTHON_VENV_PATH = "/custom/venv";
vi.mocked(existsSync).mockImplementation((path) => path === "/custom/venv/bin/python3");
await expect(dispatcherExecutable()).resolves.toBe("/custom/venv/bin/python3");
});
it("selects python3 directly when the Unix venv interpreter is missing", async () => {
setPlatform("linux");
process.env.PYTHON_VENV_PATH = "/missing/venv";
vi.mocked(existsSync).mockReturnValue(false);
await expect(dispatcherExecutable()).resolves.toBe("python3");
});
it("selects an existing Windows venv interpreter", async () => {
setPlatform("win32");
process.env.PYTHON_VENV_PATH = "C:\\custom\\venv";
vi.mocked(existsSync).mockImplementation(
(path) => path === "C:\\custom\\venv\\Scripts\\python.exe",
);
await expect(dispatcherExecutable()).resolves.toBe("C:\\custom\\venv\\Scripts\\python.exe");
});
it("selects python directly when the Windows venv interpreter is missing", async () => {
setPlatform("win32");
process.env.PYTHON_VENV_PATH = "C:\\missing\\venv";
vi.mocked(existsSync).mockReturnValue(false);
await expect(dispatcherExecutable()).resolves.toBe("python");
});
});
+32 -32
View File
@@ -139,40 +139,40 @@ describe("extractPdfText tier routing", () => {
expect(runOcrRuntime).not.toHaveBeenCalled();
});
it.each([
"balanced",
"best",
] as const)("keeps explicit Korean PDF OCR on the %s accurate tier", async (quality) => {
vi.mocked(runOcrRuntime).mockResolvedValueOnce(
runtimeResponse({
success: true,
text: "정확한 PDF 텍스트",
pages: 2,
engine: "rapidocr-onnx",
requestedQuality: quality,
actualQuality: quality,
device: "cpu",
provider: "CPUExecutionProvider",
degraded: false,
warnings: [],
}),
);
it.each(["balanced", "best"] as const)(
"keeps explicit Korean PDF OCR on the %s accurate tier",
async (quality) => {
vi.mocked(runOcrRuntime).mockResolvedValueOnce(
runtimeResponse({
success: true,
text: "정확한 PDF 텍스트",
pages: 2,
engine: "rapidocr-onnx",
requestedQuality: quality,
actualQuality: quality,
device: "cpu",
provider: "CPUExecutionProvider",
degraded: false,
warnings: [],
}),
);
const result = await extractPdfText("/tmp/job/document.pdf", {
quality,
language: "ko",
});
const result = await extractPdfText("/tmp/job/document.pdf", {
quality,
language: "ko",
});
expect(result.requestedQuality).toBe(quality);
expect(result.actualQuality).toBe(quality);
expect(runTesseractPdf).not.toHaveBeenCalled();
expect(preparePdfOcrPages).toHaveBeenCalledTimes(1);
expect(runOcrRuntime).toHaveBeenCalledTimes(1);
const runtimeOptions = JSON.parse(
vi.mocked(runOcrRuntime).mock.calls[0]?.[1][1] ?? "null",
) as Record<string, unknown>;
expect(runtimeOptions).toMatchObject({ language: "ko", quality });
});
expect(result.requestedQuality).toBe(quality);
expect(result.actualQuality).toBe(quality);
expect(runTesseractPdf).not.toHaveBeenCalled();
expect(preparePdfOcrPages).toHaveBeenCalledTimes(1);
expect(runOcrRuntime).toHaveBeenCalledTimes(1);
const runtimeOptions = JSON.parse(
vi.mocked(runOcrRuntime).mock.calls[0]?.[1][1] ?? "null",
) as Record<string, unknown>;
expect(runtimeOptions).toMatchObject({ language: "ko", quality });
},
);
it("keeps Fast PDF OCR jobs alive while native processing is quiet", async () => {
vi.useFakeTimers();
+27 -27
View File
@@ -108,35 +108,35 @@ describe("extractText error and progress behavior", () => {
expect(runOcrRuntime).not.toHaveBeenCalled();
});
it.each([
"balanced",
"best",
] as const)("keeps explicit Korean on the %s accurate tier without silent rerouting", async (quality) => {
vi.mocked(runOcrRuntime).mockResolvedValueOnce(
runtimeResponse({
success: true,
text: "한글",
engine: "rapidocr-onnx",
requestedQuality: quality,
actualQuality: quality,
device: "cpu",
provider: "CPUExecutionProvider",
degraded: false,
warnings: [],
}),
);
it.each(["balanced", "best"] as const)(
"keeps explicit Korean on the %s accurate tier without silent rerouting",
async (quality) => {
vi.mocked(runOcrRuntime).mockResolvedValueOnce(
runtimeResponse({
success: true,
text: "한글",
engine: "rapidocr-onnx",
requestedQuality: quality,
actualQuality: quality,
device: "cpu",
provider: "CPUExecutionProvider",
degraded: false,
warnings: [],
}),
);
const result = await extractText(INPUT, "/tmp/ocr", { quality, language: "ko" });
const result = await extractText(INPUT, "/tmp/ocr", { quality, language: "ko" });
expect(result.requestedQuality).toBe(quality);
expect(result.actualQuality).toBe(quality);
expect(runAdaptiveTesseract).not.toHaveBeenCalled();
expect(runOcrRuntime).toHaveBeenCalledTimes(1);
const runtimeOptions = JSON.parse(
vi.mocked(runOcrRuntime).mock.calls[0]?.[1][1] ?? "null",
) as Record<string, unknown>;
expect(runtimeOptions).toMatchObject({ language: "ko", quality });
});
expect(result.requestedQuality).toBe(quality);
expect(result.actualQuality).toBe(quality);
expect(runAdaptiveTesseract).not.toHaveBeenCalled();
expect(runOcrRuntime).toHaveBeenCalledTimes(1);
const runtimeOptions = JSON.parse(
vi.mocked(runOcrRuntime).mock.calls[0]?.[1][1] ?? "null",
) as Record<string, unknown>;
expect(runtimeOptions).toMatchObject({ language: "ko", quality });
},
);
it("applies requested local-contrast preprocessing for Fast OCR", async () => {
const recognitionPipeline = {
+71 -69
View File
@@ -279,27 +279,30 @@ describe("OCR runtime memory compatibility", () => {
"31 29 0:26 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw",
],
],
])("ignores a hidden old child when its replacement has the same mountpoint (%s)", (_label, mounts) => {
const files = new Map([
["/proc/self/cgroup", "0::/child/job\n"],
["/proc/self/mountinfo", mounts.join("\n")],
["/sys/fs/cgroup/child/job/memory.max", String(5 * GiB)],
["/sys/fs/cgroup/child/memory.max", String(6 * GiB)],
["/sys/fs/cgroup/memory.max", String(7 * GiB)],
]);
])(
"ignores a hidden old child when its replacement has the same mountpoint (%s)",
(_label, mounts) => {
const files = new Map([
["/proc/self/cgroup", "0::/child/job\n"],
["/proc/self/mountinfo", mounts.join("\n")],
["/sys/fs/cgroup/child/job/memory.max", String(5 * GiB)],
["/sys/fs/cgroup/child/memory.max", String(6 * GiB)],
["/sys/fs/cgroup/memory.max", String(7 * GiB)],
]);
expect(
getOcrRuntimeEffectiveMemoryBytes({
hostPlatform: "linux",
physicalMemoryBytes: 8 * GiB,
readTextFile: (path) => {
const value = files.get(path);
if (value === undefined) throw Object.assign(new Error("missing"), { code: "ENOENT" });
return value;
},
}),
).toBe(5 * GiB);
});
expect(
getOcrRuntimeEffectiveMemoryBytes({
hostPlatform: "linux",
physicalMemoryBytes: 8 * GiB,
readTextFile: (path) => {
const value = files.get(path);
if (value === undefined) throw Object.assign(new Error("missing"), { code: "ENOENT" });
return value;
},
}),
).toBe(5 * GiB);
},
);
it("fails closed when two reachable mounts claim the same mountpoint", () => {
const files = new Map([
@@ -610,30 +613,30 @@ describe("OCR runtime memory compatibility", () => {
).toBe(5 * GiB);
});
it.each([
"ENOENT",
"EACCES",
])("fails closed when an absent v2 limit cannot be verified against its cgroup directory (%s)", (coreErrorCode) => {
const files = new Map([
["/proc/self/cgroup", "0::/deleted\n"],
[
"/proc/self/mountinfo",
"29 23 0:26 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw\n",
],
]);
it.each(["ENOENT", "EACCES"])(
"fails closed when an absent v2 limit cannot be verified against its cgroup directory (%s)",
(coreErrorCode) => {
const files = new Map([
["/proc/self/cgroup", "0::/deleted\n"],
[
"/proc/self/mountinfo",
"29 23 0:26 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw\n",
],
]);
expect(() =>
getOcrRuntimeEffectiveMemoryBytes({
physicalMemoryBytes: 8 * GiB,
readTextFile: (path) => {
const value = files.get(path);
if (value !== undefined) return value;
const code = path.endsWith("/cgroup.controllers") ? coreErrorCode : "ENOENT";
throw Object.assign(new Error("unavailable"), { code });
},
}),
).toThrow("cgroup memory capacity");
});
expect(() =>
getOcrRuntimeEffectiveMemoryBytes({
physicalMemoryBytes: 8 * GiB,
readTextFile: (path) => {
const value = files.get(path);
if (value !== undefined) return value;
const code = path.endsWith("/cgroup.controllers") ? coreErrorCode : "ENOENT";
throw Object.assign(new Error("unavailable"), { code });
},
}),
).toThrow("cgroup memory capacity");
},
);
it("fails closed when a cgroup v2 ancestor limit is unreadable", () => {
const files = new Map([
@@ -869,33 +872,32 @@ describe("OCR runtime memory compatibility", () => {
).toThrow("cgroup memory capacity");
});
it.each([
"x::/job\n",
"1::/job\n",
"0:memory:/job\n",
])("rejects invalid Linux cgroup hierarchy metadata: %s", (membership) => {
const files = new Map([
["/proc/self/cgroup", membership],
[
"/proc/self/mountinfo",
"29 23 0:26 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory\n",
],
["/sys/fs/cgroup/memory/job/memory.limit_in_bytes", String(5 * GiB)],
["/sys/fs/cgroup/memory/memory.limit_in_bytes", String(8 * GiB)],
]);
it.each(["x::/job\n", "1::/job\n", "0:memory:/job\n"])(
"rejects invalid Linux cgroup hierarchy metadata: %s",
(membership) => {
const files = new Map([
["/proc/self/cgroup", membership],
[
"/proc/self/mountinfo",
"29 23 0:26 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory\n",
],
["/sys/fs/cgroup/memory/job/memory.limit_in_bytes", String(5 * GiB)],
["/sys/fs/cgroup/memory/memory.limit_in_bytes", String(8 * GiB)],
]);
expect(() =>
getOcrRuntimeEffectiveMemoryBytes({
hostPlatform: "linux",
physicalMemoryBytes: 8 * GiB,
readTextFile: (path) => {
const value = files.get(path);
if (value === undefined) throw Object.assign(new Error("missing"), { code: "ENOENT" });
return value;
},
}),
).toThrow("cgroup memory capacity");
});
expect(() =>
getOcrRuntimeEffectiveMemoryBytes({
hostPlatform: "linux",
physicalMemoryBytes: 8 * GiB,
readTextFile: (path) => {
const value = files.get(path);
if (value === undefined) throw Object.assign(new Error("missing"), { code: "ENOENT" });
return value;
},
}),
).toThrow("cgroup memory capacity");
},
);
it("accepts a legitimate v1 named hierarchy without treating it as cgroup v2", () => {
let membershipReads = 0;
+6 -12
View File
@@ -111,18 +111,12 @@ describe("parsePdfPageSpec", () => {
expect(parsePdfPageSpec("5, 1-3, 2, 7-8", 10)).toEqual([1, 2, 3, 5, 7, 8]);
});
it.each([
"",
"1,,2",
"0",
"-1",
"3-1",
"1-a",
"1-2-3",
"2,",
])("rejects invalid page selection %j", (spec) => {
expect(() => parsePdfPageSpec(spec, 10)).toThrow(/Invalid|No pages/);
});
it.each(["", "1,,2", "0", "-1", "3-1", "1-a", "1-2-3", "2,"])(
"rejects invalid page selection %j",
(spec) => {
expect(() => parsePdfPageSpec(spec, 10)).toThrow(/Invalid|No pages/);
},
);
it("rejects pages outside the document", () => {
expect(() => parsePdfPageSpec("1,11", 10)).toThrow("document has 10 pages");
+24 -27
View File
@@ -130,22 +130,22 @@ describe("runTesseract", () => {
await resultPromise;
});
it.each([
"Hangul",
"kor",
])("ignores legacy installed Korean model %s when resolving Fast auto languages", async (koreanModel) => {
const child = createMockChild();
const inventory = new Set(["eng", koreanModel, "osd"]);
mockGetCachedTesseractLanguages.mockReturnValueOnce(inventory);
mockSpawn.mockReturnValue(child);
it.each(["Hangul", "kor"])(
"ignores legacy installed Korean model %s when resolving Fast auto languages",
async (koreanModel) => {
const child = createMockChild();
const inventory = new Set(["eng", koreanModel, "osd"]);
mockGetCachedTesseractLanguages.mockReturnValueOnce(inventory);
mockSpawn.mockReturnValue(child);
const resultPromise = runTesseract("/tmp/mixed.png", { language: "auto" });
const language = mockSpawn.mock.calls[0]?.[1]?.[3];
child.emit("close", 0, null);
const resultPromise = runTesseract("/tmp/mixed.png", { language: "auto" });
const language = mockSpawn.mock.calls[0]?.[1]?.[3];
child.emit("close", 0, null);
await resultPromise;
expect(language).toBe("eng");
});
await resultPromise;
expect(language).toBe("eng");
},
);
it("uses the installed supported subset for auto on a partial native host", async () => {
const child = createMockChild();
@@ -188,19 +188,16 @@ describe("runTesseract", () => {
expect(mockSpawn).not.toHaveBeenCalled();
});
it.each([
"Hangul",
"kor",
"jpn+Hangul",
"Hangul/../../eng",
"Hangul+kor",
])("rejects unsafe internal language set %s before spawning", async (tesseractLanguages) => {
mockGetCachedTesseractLanguages.mockReturnValueOnce(new Set(["eng", "osd"]));
await expect(
runTesseract("/tmp/input.png", { language: "auto", tesseractLanguages }),
).rejects.toThrow("Unsupported internal Tesseract language set");
expect(mockSpawn).not.toHaveBeenCalled();
});
it.each(["Hangul", "kor", "jpn+Hangul", "Hangul/../../eng", "Hangul+kor"])(
"rejects unsafe internal language set %s before spawning",
async (tesseractLanguages) => {
mockGetCachedTesseractLanguages.mockReturnValueOnce(new Set(["eng", "osd"]));
await expect(
runTesseract("/tmp/input.png", { language: "auto", tesseractLanguages }),
).rejects.toThrow("Unsupported internal Tesseract language set");
expect(mockSpawn).not.toHaveBeenCalled();
},
);
it("fails auto clearly when no supported traineddata is installed", async () => {
mockGetCachedTesseractLanguages.mockReturnValueOnce(new Set(["osd"]));
+12
View File
@@ -261,5 +261,17 @@ describe("transcribeAudio", () => {
const options = vi.mocked(runPythonWithProgress).mock.calls[0][2];
expect(options.onProgress).toBeUndefined();
});
it("forwards the caller AbortSignal to the Python bridge", async () => {
const controller = new AbortController();
await transcribeAudio(FAKE_AUDIO, { language: "auto", signal: controller.signal });
expect(runPythonWithProgress).toHaveBeenCalledWith(
"transcribe.py",
expect.any(Array),
expect.objectContaining({ signal: controller.signal }),
);
});
});
});