Files
SnapOtter/tests/integration/generated/format-matrix-1.test.ts
T
SnapOtterandGitHub f1ec3beaf7 test: settle 202 jobs instead of returning without asserting (#652)
A 202 means the sync window expired while the job was still running. Tests treated it as a terminal pass: `if (isAsyncFallback(res)) return;` checked the envelope and returned, asserting nothing about the outcome and leaving the job running into the next test, which is the leak cancelAcceptedJobAndWait exists to prevent.

Because the window only expires under load, coverage tracked runner load. On CI 44 tests took this path and verified nothing; the same tests on a dev machine asserted in full (one measured 6.4s locally against 31s on CI).

settleAsyncFallback waits for a terminal state and asserts the job finished, and that a failure carries a message rather than being a crash. A clean failure stays valid, since the exotic-format fixtures are meant to be rejected. All 82 call sites moved over.

per-fork-env no longer floors SYNC_WAIT_MS, so forcing it to 0 drives every request through its 202 path. 570 tests were validated that way and matched their normal-window results exactly.

The 29-34s band dropped from 44 tests (23.4% of test time) to 6 (3.0%). Total test time rose 5.8% and CI wall went 12.8 to 13.1 min: the forks were doing real work during that wait, so this buys determinism, not speed. Per-shard totals unchanged at 9903 tests, 9435 passed, 468 skipped.
2026-07-27 12:16:48 +08:00

155 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Cross-format matrix, part 1 of 2 (see format-matrix.shared.ts).
*
* Vitest shards by file and runs a file's tests serially in one fork, so the
* cross-format matrix is striped across two part files that together cover
* every entry of FORMAT_SAMPLES exactly once.
*/
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { apiToolPath } from "@snapotter/shared";
import { describe, expect, it } from "vitest";
import { fixtureDir } from "../../fixtures/index.js";
import { settleAsyncFallback } from "../settle-job.js";
import {
ACCEPTABLE_FALLBACK_CODES,
adminToken,
app,
buildPayload,
formatSamplesForPart,
needsFallback,
setupMatrixApp,
TOOLS,
} from "./format-matrix.shared.js";
setupMatrixApp();
// ---------------------------------------------------------------------------
// Cross-format matrix: every tool x every format
// ---------------------------------------------------------------------------
describe("Cross-format matrix", () => {
for (const fmt of formatSamplesForPart(1)) {
describe(`${fmt.name} input (${fmt.file})`, () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
for (const tool of TOOLS) {
// Skip "Convert to PNG" when input is already PNG (no-op conversion)
if (tool.id === "convert" && fmt.name === "PNG") continue;
const perTestTimeout =
(fmt.needsHeifDecoder || fmt.needsCliDecoder) && tool.id === "image-enhancement"
? 300_000
: fmt.needsHeifDecoder || fmt.needsCliDecoder
? 180_000
: tool.id === "image-enhancement"
? 120_000
: 60_000; // 2× SYNC_WAIT_MS so a 202 fallback never races the Vitest timeout
it(
`${tool.label}`,
async () => {
if (!existsSync(fixturePath)) return;
const buffer = readFileSync(fixturePath);
const { body: payload, contentType } = buildPayload(fmt, tool, buffer);
const res = await app.inject({
method: "POST",
url: apiToolPath(tool.id),
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
// ------------------------------------------------------------------
// Assert status code
// ------------------------------------------------------------------
// A heavy encode may fall back to async (202) under CI load -- accept it.
if (await settleAsyncFallback(res)) return;
if (needsFallback(fmt)) {
// Formats with optional decoders: accept success or graceful error
expect(ACCEPTABLE_FALLBACK_CODES).toContain(res.statusCode);
} else {
// Core formats must always succeed
expect(res.statusCode).toBe(200);
}
// ------------------------------------------------------------------
// If successful, validate the response shape
// ------------------------------------------------------------------
if (res.statusCode === 200) {
const body = JSON.parse(res.body);
switch (tool.responseType) {
case "download":
expect(body.downloadUrl).toBeDefined();
expect(typeof body.downloadUrl).toBe("string");
expect(body.processedSize).toBeGreaterThan(0);
expect(body.originalSize).toBeGreaterThan(0);
break;
case "info":
expect(body.width).toBeGreaterThan(0);
expect(body.height).toBeGreaterThan(0);
expect(body.fileSize).toBeGreaterThan(0);
expect(body.format).toBeDefined();
expect(body.channels).toBeGreaterThan(0);
break;
case "base64":
// image-to-base64 returns { results: [...], errors: [...] }
expect(Array.isArray(body.results)).toBe(true);
expect(body.results.length + body.errors.length).toBeGreaterThan(0);
if (body.results.length > 0) {
const r = body.results[0];
expect(r.base64).toBeDefined();
expect(typeof r.base64).toBe("string");
expect(r.base64.length).toBeGreaterThan(0);
expect(r.dataUri).toMatch(/^data:/);
expect(r.width).toBeGreaterThan(0);
expect(r.height).toBeGreaterThan(0);
}
break;
case "palette":
// color-palette returns { colors: string[], count: number }
expect(Array.isArray(body.colors)).toBe(true);
expect(body.colors.length).toBeGreaterThan(0);
expect(body.count).toBeGreaterThan(0);
// Each color should be a hex string
for (const color of body.colors) {
expect(color).toMatch(/^#[0-9a-f]{6}$/);
}
break;
case "pdf":
// image-to-pdf returns { downloadUrl, processedSize, pages }
expect(body.downloadUrl).toBeDefined();
expect(typeof body.downloadUrl).toBe("string");
expect(body.processedSize).toBeGreaterThan(0);
expect(body.pages).toBeGreaterThanOrEqual(1);
break;
}
}
// ------------------------------------------------------------------
// If the API returned an error, verify it is a clean JSON error
// (not a raw crash / stack trace / HTML error page)
// ------------------------------------------------------------------
if (res.statusCode >= 400) {
const body = JSON.parse(res.body);
expect(body.error).toBeDefined();
expect(typeof body.error).toBe("string");
}
},
perTestTimeout,
);
}
});
}
});