mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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.
241 lines
8.0 KiB
TypeScript
241 lines
8.0 KiB
TypeScript
/**
|
|
* Comprehensive cross-format parameterized integration tests, part 4 of 4.
|
|
*
|
|
* Split from format-matrix-comprehensive.test.ts so the parts shard and run in
|
|
* parallel; shared setup and helpers live in
|
|
* ./format-matrix-comprehensive.shared.ts.
|
|
*/
|
|
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { settleAsyncFallback } from "../settle-job.js";
|
|
import {
|
|
assertDownloadResponse,
|
|
CORE_FORMATS,
|
|
callTool,
|
|
type FormatDef,
|
|
getTimeout,
|
|
PRIMARY_FORMATS,
|
|
setupMatrixApp,
|
|
} from "./format-matrix-comprehensive.shared.js";
|
|
|
|
vi.setConfig({ testTimeout: 60_000 });
|
|
|
|
setupMatrixApp();
|
|
|
|
describe("Crop across all 16 primary formats", () => {
|
|
const CROP_CONFIGS = [
|
|
{ label: "10x10 px at origin", settings: { width: 10, height: 10, left: 0, top: 0 } },
|
|
{ label: "50x50 px at 5,5", settings: { width: 50, height: 50, left: 5, top: 5 } },
|
|
] as const;
|
|
|
|
for (const cfg of CROP_CONFIGS) {
|
|
describe(`crop ${cfg.label}`, () => {
|
|
for (const fmt of PRIMARY_FORMATS) {
|
|
it(
|
|
`${fmt.name}`,
|
|
async () => {
|
|
const res = await callTool("crop", fmt, { ...cfg.settings });
|
|
if (!res) return;
|
|
await assertDownloadResponse(res, fmt);
|
|
},
|
|
getTimeout(fmt),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// =========================================================================
|
|
// 3. ROTATE -- 16 formats x 4 angles + flip combos
|
|
// =========================================================================
|
|
|
|
describe("Rotate across all 16 primary formats", () => {
|
|
const ROTATE_CONFIGS = [
|
|
{ label: "90 degrees", settings: { angle: 90 } },
|
|
{ label: "180 degrees", settings: { angle: 180 } },
|
|
{ label: "270 degrees", settings: { angle: 270 } },
|
|
{ label: "horizontal flip", settings: { angle: 0, horizontal: true } },
|
|
] as const;
|
|
|
|
for (const cfg of ROTATE_CONFIGS) {
|
|
describe(`rotate ${cfg.label}`, () => {
|
|
for (const fmt of PRIMARY_FORMATS) {
|
|
it(
|
|
`${fmt.name}`,
|
|
async () => {
|
|
const res = await callTool("rotate", fmt, { ...cfg.settings });
|
|
if (!res) return;
|
|
await assertDownloadResponse(res, fmt);
|
|
},
|
|
getTimeout(fmt),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// =========================================================================
|
|
// 4. CONVERT -- each of the 16 formats -> JPEG, PNG, WebP
|
|
// =========================================================================
|
|
|
|
describe("Sharpening across all 16 primary formats", () => {
|
|
const SHARPEN_CONFIGS = [
|
|
{ label: "adaptive method", settings: { method: "adaptive" } },
|
|
{ label: "unsharp-mask method", settings: { method: "unsharp-mask" } },
|
|
] as const;
|
|
|
|
for (const cfg of SHARPEN_CONFIGS) {
|
|
describe(`sharpening ${cfg.label}`, () => {
|
|
for (const fmt of PRIMARY_FORMATS) {
|
|
it(
|
|
`${fmt.name}`,
|
|
async () => {
|
|
const res = await callTool("sharpening", fmt, { ...cfg.settings });
|
|
if (!res) return;
|
|
await assertDownloadResponse(res, fmt);
|
|
},
|
|
getTimeout(fmt),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// =========================================================================
|
|
// 8. STRIP-METADATA -- 16 formats + verify stripped output has fewer bytes
|
|
// =========================================================================
|
|
|
|
describe("Border across all 16 primary formats", () => {
|
|
const BORDER_CONFIGS = [
|
|
{ label: "5px red", settings: { borderWidth: 5, borderColor: "#FF0000" } },
|
|
{ label: "10px blue", settings: { borderWidth: 10, borderColor: "#0000FF" } },
|
|
] as const;
|
|
|
|
for (const cfg of BORDER_CONFIGS) {
|
|
describe(`border ${cfg.label}`, () => {
|
|
for (const fmt of PRIMARY_FORMATS) {
|
|
it(
|
|
`${fmt.name}`,
|
|
async () => {
|
|
const res = await callTool("border", fmt, { ...cfg.settings });
|
|
if (!res) return;
|
|
await assertDownloadResponse(res, fmt);
|
|
},
|
|
getTimeout(fmt),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// =========================================================================
|
|
// 13. CHAINED OPERATIONS: core formats through resize then compress
|
|
// =========================================================================
|
|
|
|
describe("Extended conversion targets (core formats)", () => {
|
|
const EXTENDED_TARGETS = [
|
|
{ format: "avif", ext: ".avif" },
|
|
{ format: "tiff", ext: ".tiff" },
|
|
{ format: "gif", ext: ".gif" },
|
|
] as const;
|
|
|
|
for (const target of EXTENDED_TARGETS) {
|
|
describe(`convert to ${target.format}`, () => {
|
|
for (const fmt of CORE_FORMATS) {
|
|
// Skip identity conversions
|
|
if (fmt.name.toLowerCase() === target.format) continue;
|
|
if (fmt.name === "AVIF" && target.format === "avif") continue;
|
|
|
|
const testTimeout = target.format === "avif" || target.format === "gif" ? 120_000 : 120_000;
|
|
it(`${fmt.name} -> ${target.format}`, { timeout: testTimeout }, async () => {
|
|
const res = await callTool("convert", fmt, { format: target.format });
|
|
if (!res) return;
|
|
if (await settleAsyncFallback(res)) return;
|
|
expect(res.statusCode).toBe(200);
|
|
|
|
const body = JSON.parse(res.body);
|
|
expect(body.downloadUrl).toBeDefined();
|
|
expect(body.downloadUrl).toContain(target.ext);
|
|
expect(body.processedSize).toBeGreaterThan(0);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// =========================================================================
|
|
// 17. INFO CONSISTENCY: dimensions are consistent with resize output
|
|
//
|
|
// For core formats: get info, then resize to specific dimensions and
|
|
// verify the resize succeeded (demonstrates info output is meaningful).
|
|
// =========================================================================
|
|
|
|
describe("Info consistency check (core formats)", () => {
|
|
for (const fmt of CORE_FORMATS) {
|
|
it(`${fmt.name}: info dimensions are positive`, async () => {
|
|
const res = await callTool("info", fmt, {});
|
|
if (!res) return;
|
|
expect(res.statusCode).toBe(200);
|
|
|
|
const body = JSON.parse(res.body);
|
|
expect(body.width).toBeGreaterThan(0);
|
|
expect(body.height).toBeGreaterThan(0);
|
|
|
|
// Verify we can resize to half the original dimensions
|
|
const halfW = Math.max(1, Math.floor(body.width / 2));
|
|
const halfH = Math.max(1, Math.floor(body.height / 2));
|
|
|
|
const resizeRes = await callTool("resize", fmt, { width: halfW, height: halfH });
|
|
if (!resizeRes) return;
|
|
expect(resizeRes.statusCode).toBe(200);
|
|
|
|
const resizeBody = JSON.parse(resizeRes.body);
|
|
expect(resizeBody.processedSize).toBeGreaterThan(0);
|
|
});
|
|
}
|
|
});
|
|
|
|
// =========================================================================
|
|
// 18. EXOTIC FORMAT ERROR SHAPE VERIFICATION
|
|
//
|
|
// Exotic formats that fail should return structured errors, not raw
|
|
// stack traces or HTML error pages. This is a deeper check than the
|
|
// no-crash matrix.
|
|
// =========================================================================
|
|
|
|
describe("ICO (multi-size format) through tools", () => {
|
|
const icoFmt: FormatDef = {
|
|
name: "ICO",
|
|
file: "sample.ico",
|
|
mime: "image/x-icon",
|
|
needsCliDecoder: true,
|
|
needsHeifDecoder: false,
|
|
mayFailValidation: false,
|
|
};
|
|
|
|
const ICO_TOOLS = [
|
|
{ id: "resize", settings: { width: 32, height: 32 } },
|
|
{ id: "convert", settings: { format: "png" } },
|
|
{ id: "info", settings: {} },
|
|
{ id: "border", settings: { borderWidth: 2, borderColor: "#000000" } },
|
|
{ id: "image-enhancement", settings: { mode: "auto", intensity: 50 } },
|
|
];
|
|
|
|
for (const tool of ICO_TOOLS) {
|
|
it(`${tool.id}: handles ICO input (may need ImageMagick)`, async () => {
|
|
const res = await callTool(tool.id, icoFmt, tool.settings);
|
|
if (!res) return;
|
|
|
|
// ICO requires CLI decoder; accept success or clean error
|
|
expect(res.statusCode).not.toBe(500);
|
|
expect([200, 202, 400, 422]).toContain(res.statusCode);
|
|
|
|
const body = JSON.parse(res.body);
|
|
if (res.statusCode >= 400) {
|
|
expect(body.error).toBeDefined();
|
|
expect(typeof body.error).toBe("string");
|
|
}
|
|
}, 180_000);
|
|
}
|
|
});
|