mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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.
267 lines
9.5 KiB
JavaScript
267 lines
9.5 KiB
JavaScript
/**
|
|
* Semantic oracles for benchmark artifacts.
|
|
*
|
|
* `validateArtifact` in job-aware.mjs proves an artifact is a structurally
|
|
* intact file of the declared type. That still lets a fast wrong answer pass:
|
|
* a resize that ignores its width, a page extraction that returns the whole
|
|
* document, a trim that returns the untrimmed audio are all valid PNG/PDF/WAV
|
|
* bytes. These oracles read the property the operation was asked to change out
|
|
* of the output itself, so a benchmark row can only be green when the work was
|
|
* actually done.
|
|
*
|
|
* Everything here is pure and dependency-free (node:zlib only), so the oracles
|
|
* run on the measuring host without needing ffprobe, qpdf or Sharp.
|
|
*/
|
|
import { inflateSync } from "node:zlib";
|
|
|
|
function ascii(bytes, start, end) {
|
|
return bytes.subarray(start, end).toString("latin1");
|
|
}
|
|
|
|
/** PNG: the IHDR chunk always sits at byte 16 and is big-endian. */
|
|
function pngDimensions(bytes) {
|
|
if (ascii(bytes, 12, 16) !== "IHDR") return null;
|
|
return { width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) };
|
|
}
|
|
|
|
/**
|
|
* JPEG: walk the marker chain to the first SOFn frame header. SOF4 (0xC4),
|
|
* SOF8 (0xC8) and SOF12 (0xCC) are DHT/JPG/DAC, not frame headers.
|
|
*/
|
|
function jpegDimensions(bytes) {
|
|
let offset = 2;
|
|
while (offset + 9 < bytes.length) {
|
|
if (bytes[offset] !== 0xff) {
|
|
offset += 1;
|
|
continue;
|
|
}
|
|
const marker = bytes[offset + 1];
|
|
if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {
|
|
offset += 2;
|
|
continue;
|
|
}
|
|
const length = bytes.readUInt16BE(offset + 2);
|
|
const isFrameHeader =
|
|
marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc;
|
|
if (isFrameHeader) {
|
|
return { height: bytes.readUInt16BE(offset + 5), width: bytes.readUInt16BE(offset + 7) };
|
|
}
|
|
if (marker === 0xda) return null;
|
|
offset += 2 + length;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** GIF: logical screen descriptor, little-endian, immediately after the header. */
|
|
function gifDimensions(bytes) {
|
|
return { width: bytes.readUInt16LE(6), height: bytes.readUInt16LE(8) };
|
|
}
|
|
|
|
/** WebP: simple lossy (VP8), lossless (VP8L) and extended (VP8X) all differ. */
|
|
function webpDimensions(bytes) {
|
|
const chunk = ascii(bytes, 12, 16);
|
|
if (chunk === "VP8X") {
|
|
return {
|
|
width: 1 + (bytes[24] | (bytes[25] << 8) | (bytes[26] << 16)),
|
|
height: 1 + (bytes[27] | (bytes[28] << 8) | (bytes[29] << 16)),
|
|
};
|
|
}
|
|
if (chunk === "VP8 ") {
|
|
return { width: bytes.readUInt16LE(26) & 0x3fff, height: bytes.readUInt16LE(28) & 0x3fff };
|
|
}
|
|
if (chunk === "VP8L") {
|
|
const bits = bytes.readUInt32LE(21);
|
|
return { width: (bits & 0x3fff) + 1, height: ((bits >> 14) & 0x3fff) + 1 };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** BMP: DIB header dimensions are signed (a negative height is top-down). */
|
|
function bmpDimensions(bytes) {
|
|
return { width: bytes.readInt32LE(18), height: Math.abs(bytes.readInt32LE(22)) };
|
|
}
|
|
|
|
/**
|
|
* Pixel dimensions of an encoded image, or null when the format is one this
|
|
* module deliberately does not parse (AVIF, HEIC, TIFF, SVG).
|
|
*/
|
|
export function imageDimensions(input) {
|
|
const bytes = Buffer.from(input);
|
|
if (bytes.length < 32) return null;
|
|
if (ascii(bytes, 1, 4) === "PNG") return pngDimensions(bytes);
|
|
if (bytes[0] === 0xff && bytes[1] === 0xd8) return jpegDimensions(bytes);
|
|
if (ascii(bytes, 0, 3) === "GIF") return gifDimensions(bytes);
|
|
if (ascii(bytes, 0, 4) === "RIFF" && ascii(bytes, 8, 12) === "WEBP") return webpDimensions(bytes);
|
|
if (ascii(bytes, 0, 2) === "BM") return bmpDimensions(bytes);
|
|
return null;
|
|
}
|
|
|
|
const PAGE_OBJECT = /\/Type\s*\/Page(?![sA-Za-z])/g;
|
|
|
|
function countPageObjects(text) {
|
|
return (text.match(PAGE_OBJECT) ?? []).length;
|
|
}
|
|
|
|
/**
|
|
* Number of pages in a PDF.
|
|
*
|
|
* Classic PDFs keep page objects in the clear; anything written through a
|
|
* cross-reference stream (qpdf and pdfcpu both do by default) hides them
|
|
* inside FlateDecode object streams, so raw regex counting silently returns 0
|
|
* there. Inflating every Flate stream first is what makes this oracle work on
|
|
* real tool output rather than only on hand-built fixtures.
|
|
*/
|
|
export function pdfPageCount(input) {
|
|
const bytes = Buffer.from(input);
|
|
let pages = countPageObjects(bytes.toString("latin1"));
|
|
|
|
const open = Buffer.from("stream", "latin1");
|
|
const close = Buffer.from("endstream", "latin1");
|
|
let cursor = 0;
|
|
while (cursor < bytes.length) {
|
|
const start = bytes.indexOf(open, cursor);
|
|
if (start === -1) break;
|
|
// "endstream" contains "stream"; matching it would desynchronise the scan.
|
|
if (start >= 3 && ascii(bytes, start - 3, start) === "end") {
|
|
cursor = start + open.length;
|
|
continue;
|
|
}
|
|
let payload = start + open.length;
|
|
if (bytes[payload] === 0x0d) payload += 1;
|
|
if (bytes[payload] === 0x0a) payload += 1;
|
|
const end = bytes.indexOf(close, payload);
|
|
if (end === -1) break;
|
|
cursor = end + close.length;
|
|
try {
|
|
pages += countPageObjects(inflateSync(bytes.subarray(payload, end)).toString("latin1"));
|
|
} catch {
|
|
// Not a Flate stream (JPEG image data, plain content). Skipping is
|
|
// correct: those streams never carry page objects.
|
|
}
|
|
}
|
|
return pages;
|
|
}
|
|
|
|
/** Duration in seconds of a RIFF/WAVE file, from its fmt and data chunks. */
|
|
export function wavDurationS(input) {
|
|
const bytes = Buffer.from(input);
|
|
if (ascii(bytes, 0, 4) !== "RIFF" || ascii(bytes, 8, 12) !== "WAVE") return null;
|
|
let cursor = 12;
|
|
let byteRate = 0;
|
|
while (cursor + 8 <= bytes.length) {
|
|
const id = ascii(bytes, cursor, cursor + 4);
|
|
const size = bytes.readUInt32LE(cursor + 4);
|
|
if (id === "fmt ") byteRate = bytes.readUInt32LE(cursor + 16);
|
|
if (id === "data") return byteRate > 0 ? size / byteRate : null;
|
|
cursor += 8 + size + (size % 2);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Duration in seconds of an ISO base media file (MP4/MOV/M4A), from the mvhd
|
|
* box. Version 1 mvhd uses 64-bit times; the low 32 bits are enough for any
|
|
* duration a benchmark produces.
|
|
*/
|
|
export function isoDurationS(input) {
|
|
const bytes = Buffer.from(input);
|
|
const mvhd = bytes.indexOf(Buffer.from("mvhd", "latin1"));
|
|
if (mvhd === -1) return null;
|
|
const version = bytes[mvhd + 4];
|
|
if (version === 1) {
|
|
if (mvhd + 36 > bytes.length) return null;
|
|
const timescale = bytes.readUInt32BE(mvhd + 24);
|
|
const duration = Number(bytes.readBigUInt64BE(mvhd + 28));
|
|
return timescale > 0 ? duration / timescale : null;
|
|
}
|
|
if (mvhd + 24 > bytes.length) return null;
|
|
const timescale = bytes.readUInt32BE(mvhd + 16);
|
|
const duration = bytes.readUInt32BE(mvhd + 20);
|
|
return timescale > 0 ? duration / timescale : null;
|
|
}
|
|
|
|
/** Number of Matroska/WebM clusters, a cheap "this actually has content" probe. */
|
|
export function webmHasClusters(input) {
|
|
return Buffer.from(input).includes(Buffer.from([0x1f, 0x43, 0xb6, 0x75]));
|
|
}
|
|
|
|
function fail(label, expected, actual) {
|
|
throw new Error(`oracle ${label}: expected ${expected}, measured ${actual}`);
|
|
}
|
|
|
|
function assertClose(label, actual, expected, tolerance) {
|
|
if (actual === null || !Number.isFinite(actual)) fail(label, expected, "unreadable");
|
|
if (Math.abs(actual - expected) > tolerance) {
|
|
fail(label, `${expected} +/- ${tolerance}`, actual.toFixed(3));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply a declarative oracle to artifact bytes. Every supported key names a
|
|
* property the tool under benchmark was explicitly asked to produce.
|
|
*
|
|
* { width: 800 } image output is exactly 800 px wide
|
|
* { height: 600 }
|
|
* { pages: 3 } PDF has exactly 3 pages
|
|
* { durationS: 5, toleranceS: 0.4 }
|
|
* { minBytes: 1024 }
|
|
* { json: { key: "value" } } JSON output contains these fields
|
|
* { textIncludes: "hello" }
|
|
*/
|
|
export function assertOracle(input, oracle) {
|
|
if (!oracle) return;
|
|
const bytes = Buffer.from(input);
|
|
|
|
if (oracle.width !== undefined || oracle.height !== undefined) {
|
|
const dimensions = imageDimensions(bytes);
|
|
if (!dimensions) fail("dimensions", "a parseable raster header", "unreadable");
|
|
if (oracle.width !== undefined && dimensions.width !== oracle.width) {
|
|
fail("width", oracle.width, dimensions.width);
|
|
}
|
|
if (oracle.height !== undefined && dimensions.height !== oracle.height) {
|
|
fail("height", oracle.height, dimensions.height);
|
|
}
|
|
}
|
|
|
|
if (oracle.pages !== undefined) {
|
|
const pages = pdfPageCount(bytes);
|
|
if (pages !== oracle.pages) fail("pages", oracle.pages, pages);
|
|
}
|
|
|
|
if (oracle.durationS !== undefined) {
|
|
const tolerance = oracle.toleranceS ?? 0.5;
|
|
const duration = wavDurationS(bytes) ?? isoDurationS(bytes);
|
|
assertClose("durationS", duration, oracle.durationS, tolerance);
|
|
}
|
|
|
|
if (oracle.minBytes !== undefined && bytes.length < oracle.minBytes) {
|
|
fail("minBytes", `>= ${oracle.minBytes}`, bytes.length);
|
|
}
|
|
|
|
if (oracle.maxBytes !== undefined && bytes.length > oracle.maxBytes) {
|
|
fail("maxBytes", `<= ${oracle.maxBytes}`, bytes.length);
|
|
}
|
|
|
|
if (oracle.json !== undefined) {
|
|
let payload;
|
|
try {
|
|
payload = JSON.parse(bytes.toString("utf8"));
|
|
} catch {
|
|
fail("json", "parseable JSON", "unparseable");
|
|
}
|
|
for (const [key, expected] of Object.entries(oracle.json)) {
|
|
const actual = key.split(".").reduce((node, part) => node?.[part], payload);
|
|
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
fail(`json.${key}`, JSON.stringify(expected), JSON.stringify(actual));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (oracle.textIncludes !== undefined) {
|
|
const text = bytes.toString("utf8");
|
|
if (!text.includes(oracle.textIncludes)) {
|
|
fail("textIncludes", JSON.stringify(oracle.textIncludes), "absent");
|
|
}
|
|
}
|
|
}
|