mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
test: testing overhaul -- CI e2e gates, parallel suites, generated matrices, mutation testing (#215)
Closes the "e2e never runs in CI" hole. Adds per-PR e2e smoke gate, nightly full-suite workflows, parallel vitest forks (per-fork DBs), Playwright parallel/serial/visual projects against production builds, metadata-generated test suites (drift guards, hostile inputs, format matrix, pairwise settings, property-based fuzz), Stryker mutation testing, Schemathesis API fuzz, coverage ratchet, and fixes for three session-poisoning bugs that caused 200+ serial-bucket failures. Bug fix included: favicon/split/bulk-rename could hang clients forever when ZIP streaming failed after reply.hijack().
This commit is contained in:
@@ -601,3 +601,39 @@ describe("verifyBundleModels", () => {
|
||||
expect(mod.verifyBundleModels("background-removal")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ensureAiDirs", () => {
|
||||
it("creates AI directories when the manifest exists and DATA_DIR is writable", () => {
|
||||
writeTestManifest({});
|
||||
mod.ensureAiDirs();
|
||||
expect(existsSync(join(aiDir, "venv"))).toBe(true);
|
||||
expect(existsSync(modelsDir)).toBe(true);
|
||||
expect(existsSync(join(aiDir, "pip-cache"))).toBe(true);
|
||||
});
|
||||
|
||||
it("warns instead of throwing when DATA_DIR is uncreatable", async () => {
|
||||
// Point DATA_DIR below a regular file so mkdir fails (ENOTDIR), the same
|
||||
// failure class as the default /data on a sealed macOS root (ENOENT).
|
||||
const blocker = join(tempDir, "blocker");
|
||||
writeFileSync(blocker, "not a directory");
|
||||
process.env.DATA_DIR = join(blocker, "data");
|
||||
writeTestManifest({});
|
||||
vi.resetModules();
|
||||
mod = await import("../../../apps/api/src/lib/feature-status.js");
|
||||
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
expect(() => mod.ensureAiDirs()).not.toThrow();
|
||||
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("Cannot create AI directories"));
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("is a no-op outside managed environments (no manifest, no /.dockerenv)", async () => {
|
||||
process.env.FEATURE_MANIFEST_PATH = join(tempDir, "missing-manifest.json");
|
||||
process.env.DATA_DIR = join(tempDir, "fresh-data");
|
||||
vi.resetModules();
|
||||
mod = await import("../../../apps/api/src/lib/feature-status.js");
|
||||
|
||||
mod.ensureAiDirs();
|
||||
expect(existsSync(join(tempDir, "fresh-data"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { pairwise } from "../helpers/pairwise.js";
|
||||
|
||||
describe("pairwise covering-array generator", () => {
|
||||
it("covers every pair of values across all axis pairs", () => {
|
||||
const axes = [
|
||||
{ key: "fit", values: ["contain", "cover", "fill", "inside"] },
|
||||
{ key: "format", values: ["png", "jpeg", "webp"] },
|
||||
{ key: "withMetadata", values: [true, false] },
|
||||
{ key: "quality", values: [1, 50, 100] },
|
||||
];
|
||||
const cases = pairwise(axes);
|
||||
|
||||
for (let i = 0; i < axes.length; i++) {
|
||||
for (let j = i + 1; j < axes.length; j++) {
|
||||
for (const vi of axes[i].values) {
|
||||
for (const vj of axes[j].values) {
|
||||
const covered = cases.some((c) => c[axes[i].key] === vi && c[axes[j].key] === vj);
|
||||
expect(covered, `pair ${axes[i].key}=${vi} x ${axes[j].key}=${vj} not covered`).toBe(
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("produces far fewer cases than the full cartesian product", () => {
|
||||
const axes = [
|
||||
{ key: "a", values: [1, 2, 3, 4] },
|
||||
{ key: "b", values: [1, 2, 3] },
|
||||
{ key: "c", values: [true, false] },
|
||||
{ key: "d", values: ["x", "y", "z"] },
|
||||
];
|
||||
const cases = pairwise(axes);
|
||||
// Cartesian product is 72; pairwise needs at least 12 (largest axis pair).
|
||||
expect(cases.length).toBeGreaterThanOrEqual(12);
|
||||
expect(cases.length).toBeLessThan(30);
|
||||
});
|
||||
|
||||
it("is deterministic", () => {
|
||||
const axes = [
|
||||
{ key: "a", values: [1, 2, 3] },
|
||||
{ key: "b", values: ["x", "y"] },
|
||||
{ key: "c", values: [true, false] },
|
||||
];
|
||||
expect(pairwise(axes)).toEqual(pairwise(axes));
|
||||
});
|
||||
|
||||
it("handles degenerate inputs", () => {
|
||||
expect(pairwise([])).toEqual([]);
|
||||
expect(pairwise([{ key: "only", values: [1, 2] }])).toEqual([{ only: 1 }, { only: 2 }]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { TOOLS } from "@snapotter/shared";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { TOOL_DISPLAY_MODES } from "@/lib/tool-display-modes";
|
||||
import { toolRegistry } from "@/lib/tool-registry";
|
||||
|
||||
/**
|
||||
* Drift guards: the shared TOOLS catalog, the frontend registry, and the
|
||||
* display-mode map must always describe the same set of tools. A new tool
|
||||
* that misses one of the three fails here at PR time instead of shipping
|
||||
* a dead tool page.
|
||||
*/
|
||||
describe("tool registry drift", () => {
|
||||
it("every TOOLS entry has a frontend registry entry", () => {
|
||||
for (const tool of TOOLS) {
|
||||
expect(toolRegistry.has(tool.id), `tool "${tool.id}" missing from tool-registry.tsx`).toBe(
|
||||
true,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("every TOOLS entry has a display mode", () => {
|
||||
for (const tool of TOOLS) {
|
||||
expect(
|
||||
TOOL_DISPLAY_MODES[tool.id],
|
||||
`tool "${tool.id}" missing from tool-display-modes.ts`,
|
||||
).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it("registry has no orphan entries (tools removed from TOOLS but not the registry)", () => {
|
||||
const ids = new Set(TOOLS.map((t) => t.id));
|
||||
for (const id of toolRegistry.keys()) {
|
||||
expect(ids.has(id), `registry entry "${id}" has no TOOLS definition`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("display-mode map has no orphan entries", () => {
|
||||
const ids = new Set(TOOLS.map((t) => t.id));
|
||||
for (const id of Object.keys(TOOL_DISPLAY_MODES)) {
|
||||
expect(ids.has(id), `display-mode entry "${id}" has no TOOLS definition`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("every registry entry has a Settings component and a valid display mode", () => {
|
||||
for (const [id, entry] of toolRegistry) {
|
||||
expect(entry.Settings, `tool "${id}" has no Settings component`).toBeTruthy();
|
||||
expect(entry.displayMode, `tool "${id}" has no displayMode`).toBe(TOOL_DISPLAY_MODES[id]);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user