Files
SnapOtter/tests/unit/qa/verify-installed-ai-production.test.ts
T
SnapOtterandGitHub d10d0f544f 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.
2026-07-27 15:37:30 +08:00

261 lines
9.5 KiB
TypeScript

import sharp from "sharp";
import { beforeAll, describe, expect, it, vi } from "vitest";
import {
type VerifierFixtures,
verifyInstalledAiProduction,
} from "../../qa/verify-installed-ai-production.mjs";
interface FakeServerOptions {
artifactMime?: string;
blurIsUnchanged?: boolean;
failTool?: string;
hangLogin?: boolean;
installFailure?: boolean;
preinstalled?: boolean;
processedSizeDelta?: number;
progressContentType?: string;
untrustedDownload?: boolean;
wrongProgressJob?: boolean;
}
let fixtures: VerifierFixtures;
let artifacts: Record<string, { body: Buffer; contentType: string }>;
let unblurredWebp: Buffer;
beforeAll(async () => {
const pixels = Buffer.alloc(200 * 200 * 3);
for (let y = 0; y < 200; y += 1) {
for (let x = 0; x < 200; x += 1) {
const offset = (y * 200 + x) * 3;
const background = (x + y) % 2 === 0 ? 20 : 230;
const subject = x >= 80 && x < 120 && y >= 66 && y < 146;
pixels[offset] = subject ? 32 : background;
pixels[offset + 1] = subject ? 128 : background;
pixels[offset + 2] = subject ? 224 : background;
}
}
const portrait = await sharp(pixels, { raw: { width: 200, height: 200, channels: 3 } })
.jpeg({ quality: 100 })
.toBuffer();
const subject = await sharp(portrait)
.extract({ left: 80, top: 66, width: 40, height: 80 })
.png()
.toBuffer();
const blurred = await sharp(portrait)
.blur(12)
.composite([{ input: subject, left: 80, top: 66 }])
.webp({ lossless: true })
.toBuffer();
// A visible brightness shift changes decoded border pixels and file bytes,
// but deliberately preserves background high-frequency detail.
unblurredWebp = await sharp(portrait).linear(1, 20).webp({ lossless: true }).toBuffer();
const replaced = await sharp({
create: { width: 200, height: 200, channels: 3, background: "#ff0000" },
})
.composite([{ input: subject, left: 80, top: 66 }])
.png()
.toBuffer();
fixtures = {
portrait,
speechMp4: Buffer.from("fake speech video"),
speechWav: Buffer.from("fake speech audio"),
};
artifacts = {
"transcribe-audio": {
body: Buffer.from("The quick brown fox transcribes audio files reliably."),
contentType: "text/plain",
},
"auto-subtitles": {
body: Buffer.from(
"1\n00:00:00,000 --> 00:00:02,000\nThe quick brown fox transcribes audio.\n",
),
contentType: "application/x-subrip",
},
"blur-background": { body: blurred, contentType: "image/webp" },
"background-replace": { body: replaced, contentType: "image/png" },
};
});
function json(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
});
}
function createFakeServer(options: FakeServerOptions = {}): typeof fetch {
const installed = options.preinstalled ?? false;
let installRequests = 0;
const jobTools = new Map<string, string>();
return vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
const url = new URL(String(input));
if (url.pathname === "/api/auth/login") {
if (options.hangLogin) {
return new Promise<Response>((_resolve, reject) => {
const signal = init?.signal;
const rejectAbort = () => reject(new Error("request aborted"));
if (signal?.aborted) rejectAbort();
else signal?.addEventListener("abort", rejectAbort, { once: true });
});
}
return json({ token: "admin-token" });
}
expect(new Headers(init?.headers).get("authorization")).toBe("Bearer admin-token");
if (url.pathname === "/api/v1/features") {
const status = installed || installRequests === 2 ? "installed" : "not_installed";
return json({
bundles: [
{ id: "transcription", status },
{
id: "background-removal",
status: options.installFailure && installRequests === 2 ? "error" : status,
error: options.installFailure ? "bundle checksum mismatch" : null,
},
],
});
}
const install = url.pathname.match(/^\/api\/v1\/admin\/features\/([^/]+)\/install$/u);
if (install) {
installRequests += 1;
return json({ jobId: `install-${install[1]}`, queued: installRequests > 1 }, 202);
}
const submittedTool = Object.keys(artifacts).find((toolId) => url.pathname.endsWith(toolId));
if (submittedTool) {
const jobId = `job-${submittedTool}`;
jobTools.set(jobId, submittedTool);
return json({ jobId, async: true }, 202);
}
const progress = url.pathname.match(/^\/api\/v1\/jobs\/([^/]+)\/progress$/u);
if (progress) {
const jobId = decodeURIComponent(progress[1]);
const toolId = jobTools.get(jobId);
if (!toolId) return json({ error: "unknown job" }, 404);
const artifact = artifacts[toolId];
const body =
options.blurIsUnchanged && toolId === "blur-background" ? unblurredWebp : artifact.body;
const frame =
options.failTool === toolId
? {
jobId,
type: "single",
phase: "failed",
error: "worker exploded",
}
: {
jobId: options.wrongProgressJob ? "different-job" : jobId,
type: "single",
phase: "complete",
result: {
downloadUrl: options.untrustedDownload
? `https://attacker.invalid/api/v1/download/${jobId}/output.bin`
: `/api/v1/download/${jobId}/output.bin`,
processedSize: body.length + (options.processedSizeDelta ?? 0),
},
};
return new Response(`data: ${JSON.stringify(frame)}\n\n`, {
headers: { "content-type": options.progressContentType ?? "text/event-stream" },
});
}
const download = url.pathname.match(/^\/api\/v1\/download\/([^/]+)\/output\.bin$/u);
if (download) {
const toolId = jobTools.get(decodeURIComponent(download[1]));
if (!toolId) return json({ error: "unknown artifact" }, 404);
const source = artifacts[toolId];
const body =
options.blurIsUnchanged && toolId === "blur-background" ? unblurredWebp : source.body;
return new Response(new Uint8Array(body), {
headers: { "content-type": options.artifactMime ?? source.contentType },
});
}
return json({ error: `unhandled ${url.pathname}` }, 404);
}) as typeof fetch;
}
function verify(fetchImpl: typeof fetch): Promise<void> {
return verifyInstalledAiProduction({
baseUrl: "http://127.0.0.1:13499",
username: "admin",
password: "test-password",
fetchImpl,
fixtures,
installPollMs: 1,
installTimeoutMs: 100,
jobTimeoutMs: 100,
requestTimeoutMs: 50,
sleep: async () => {},
});
}
describe("production installed-AI verifier", () => {
it("installs both real bundles and verifies all four downloaded artifacts", async () => {
const fetchImpl = createFakeServer();
await expect(verify(fetchImpl)).resolves.toBeUndefined();
const requested = vi.mocked(fetchImpl).mock.calls.map(([input]) => String(input));
expect(
requested.filter((url) => url.includes("/admin/features/") && url.endsWith("/install")),
).toHaveLength(2);
expect(
requested.filter((url) => url.includes("/jobs/") && url.endsWith("/progress")),
).toHaveLength(4);
expect(requested.filter((url) => url.includes("/download/"))).toHaveLength(4);
for (const [, init] of vi.mocked(fetchImpl).mock.calls) {
expect(init?.redirect).toBe("error");
}
});
it("fails when the fresh production volume already claims bundles are installed", async () => {
await expect(verify(createFakeServer({ preinstalled: true }))).rejects.toThrow(
"fresh production data unexpectedly reports transcription as installed",
);
});
it("fails closed on a terminal worker failure", async () => {
await expect(verify(createFakeServer({ failTool: "auto-subtitles" }))).rejects.toThrow(
"job failed: worker exploded",
);
});
it("fails closed when bundle installation reports an error", async () => {
await expect(verify(createFakeServer({ installFailure: true }))).rejects.toThrow(
"background-removal install failed: bundle checksum mismatch",
);
});
it("rejects non-SSE progress responses and wrong-job frames", async () => {
await expect(
verify(createFakeServer({ progressContentType: "application/json" })),
).rejects.toThrow("job progress MIME mismatch");
await expect(verify(createFakeServer({ wrongProgressJob: true }))).rejects.toThrow(
"wrong job or job type",
);
});
it("rejects cross-origin downloads, MIME drift, and processed-size drift", async () => {
await expect(verify(createFakeServer({ untrustedDownload: true }))).rejects.toThrow(
"untrusted download URL",
);
await expect(
verify(createFakeServer({ artifactMime: "application/octet-stream" })),
).rejects.toThrow("artifact MIME mismatch");
await expect(verify(createFakeServer({ processedSizeDelta: 1 }))).rejects.toThrow(
"artifact size does not match",
);
});
it("bounds non-SSE HTTP requests", async () => {
await expect(verify(createFakeServer({ hangLogin: true }))).rejects.toThrow(
"admin login timed out after 50ms",
);
});
it("fails when blur changes bytes but does not reduce background energy", async () => {
await expect(verify(createFakeServer({ blurIsUnchanged: true }))).rejects.toThrow(
"background high-frequency energy ratio",
);
});
});