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:
SnapOtter
2026-06-10 22:01:13 +08:00
committed by GitHub
parent 3b8d529b44
commit 4ec39c556f
62 changed files with 2888 additions and 298 deletions
+7 -7
View File
@@ -55,14 +55,14 @@ export function ensureAiDirs(): void {
mkdirSync(MODELS_DIR, { recursive: true });
mkdirSync(join(AI_DIR, "pip-cache"), { recursive: true });
} catch (err: unknown) {
// Never refuse to boot over the AI data dir. On native checkouts the
// default DATA_DIR (/data) is often uncreatable (ENOENT/EROFS on a
// sealed macOS root, EACCES on restrictive volumes); AI tools simply
// report as not installed until DATA_DIR points somewhere writable.
const code = (err as NodeJS.ErrnoException).code;
if (code === "EACCES") {
console.error(
`WARNING: Cannot create AI directories under "${AI_DIR}". AI features will be unavailable. Check volume permissions (PUID/PGID).`,
);
return;
}
throw err;
console.error(
`WARNING: Cannot create AI directories under "${AI_DIR}" (${code}). AI features will be unavailable. Set DATA_DIR to a writable path (or check volume permissions / PUID / PGID in Docker).`,
);
}
}
+9
View File
@@ -10,6 +10,7 @@ import { eq } from "drizzle-orm";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod";
import { db, schema } from "../db/index.js";
import { auditLog } from "../lib/audit.js";
import { requirePermission } from "../permissions.js";
import { requireAuth } from "../plugins/auth.js";
@@ -86,6 +87,14 @@ export async function settingsRoutes(app: FastifyInstance): Promise<void> {
}
}
if (entries.length > 0) {
auditLog(request.log, "SETTINGS_UPDATED", {
adminId: admin.id,
username: admin.username,
keys: entries.map((e) => e.key),
});
}
return reply.send({ ok: true, updatedCount: entries.length });
});
+3
View File
@@ -103,6 +103,9 @@ export function registerBulkRename(app: FastifyInstance) {
details: err instanceof Error ? err.message : "Unknown error",
});
}
// The ZIP stream already started; end the connection so clients see a
// truncated transfer instead of hanging forever.
reply.raw.destroy(err instanceof Error ? err : new Error(String(err)));
}
});
}
+10
View File
@@ -106,6 +106,13 @@ export function registerFavicon(app: FastifyInstance) {
buf = await autoOrient(buf);
}
// Force a full pixel decode now. Header validation alone lets
// truncated files through, and a decode failure after reply.hijack()
// cannot be turned into an error response anymore.
if (validation.format !== "svg") {
await sharp(buf).stats();
}
decodedFiles.push({ buffer: buf, filename: file.filename });
} catch (err) {
const reason = err instanceof Error ? err.message : "Unknown decode error";
@@ -206,6 +213,9 @@ export function registerFavicon(app: FastifyInstance) {
details: err instanceof Error ? err.message : "Unknown error",
});
}
// The ZIP stream already started; end the connection so clients see a
// truncated transfer instead of hanging forever.
reply.raw.destroy(err instanceof Error ? err : new Error(String(err)));
}
});
}
+10
View File
@@ -139,6 +139,13 @@ export function registerSplit(app: FastifyInstance) {
const fullW = metadata.width ?? 0;
const fullH = metadata.height ?? 0;
// Force a full pixel decode before streaming starts. Metadata alone
// lets truncated files through, and a decode failure after
// reply.hijack() cannot become an error response anymore.
if (validation.format !== "svg") {
await sharp(fileBuffer).stats();
}
let cols = settings.columns;
let rows = settings.rows;
if (settings.tileWidth && settings.tileHeight) {
@@ -221,6 +228,9 @@ export function registerSplit(app: FastifyInstance) {
details: err instanceof Error ? err.message : "Unknown error",
});
}
// The ZIP stream already started; end the connection so clients see a
// truncated transfer instead of hanging forever.
reply.raw.destroy(err instanceof Error ? err : new Error(String(err)));
}
});