mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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().
168 lines
5.2 KiB
TypeScript
168 lines
5.2 KiB
TypeScript
import type { z } from "zod";
|
|
|
|
/**
|
|
* Derives PICT combinatorial axes from a tool's Zod settings schema.
|
|
*
|
|
* Enums and booleans enumerate their members; bounded numbers contribute
|
|
* min/mid/max; optional fields add `undefined` so "field omitted" is part of
|
|
* the matrix. Free-form strings, arrays, and nested objects are skipped here
|
|
* (the fast-check fuzz layer covers those).
|
|
*
|
|
* Zod v3 internals (`_def`) are accessed deliberately; if a Zod upgrade breaks
|
|
* this helper, the pairwise suite fails loudly at collection time.
|
|
*/
|
|
export interface PictAxis {
|
|
key: string;
|
|
values: unknown[];
|
|
}
|
|
|
|
interface ZodDefLike {
|
|
typeName?: string;
|
|
schema?: ZodSchemaLike;
|
|
innerType?: ZodSchemaLike;
|
|
in?: ZodSchemaLike;
|
|
shape?: () => Record<string, ZodSchemaLike>;
|
|
values?: unknown;
|
|
options?: ZodSchemaLike[];
|
|
checks?: Array<{ kind: string; value?: number }>;
|
|
value?: unknown;
|
|
}
|
|
|
|
interface ZodSchemaLike {
|
|
_def?: ZodDefLike;
|
|
}
|
|
|
|
/** Unwraps effects/defaults/optional/nullable wrappers around a schema. */
|
|
function unwrap(schema: ZodSchemaLike): ZodSchemaLike {
|
|
let current = schema;
|
|
for (let i = 0; i < 10; i++) {
|
|
const def = current._def;
|
|
if (!def) return current;
|
|
if (def.typeName === "ZodEffects" && def.schema) current = def.schema;
|
|
else if (def.typeName === "ZodPipeline" && def.in) current = def.in;
|
|
else if (
|
|
(def.typeName === "ZodDefault" ||
|
|
def.typeName === "ZodOptional" ||
|
|
def.typeName === "ZodNullable") &&
|
|
def.innerType
|
|
)
|
|
current = def.innerType;
|
|
else return current;
|
|
}
|
|
return current;
|
|
}
|
|
|
|
function isOmittable(schema: ZodSchemaLike): boolean {
|
|
const t = schema._def?.typeName;
|
|
return t === "ZodOptional" || t === "ZodDefault" || t === "ZodNullable";
|
|
}
|
|
|
|
function deriveValues(field: ZodSchemaLike): unknown[] | null {
|
|
const omittable = isOmittable(field);
|
|
const inner = unwrap(field);
|
|
const def = inner._def;
|
|
if (!def) return null;
|
|
|
|
let values: unknown[] | null = null;
|
|
switch (def.typeName) {
|
|
case "ZodEnum":
|
|
values = [...(def.values as string[])];
|
|
break;
|
|
case "ZodNativeEnum":
|
|
values = Object.values(def.values as Record<string, unknown>);
|
|
break;
|
|
case "ZodBoolean":
|
|
values = [true, false];
|
|
break;
|
|
case "ZodLiteral":
|
|
values = [def.value];
|
|
break;
|
|
case "ZodNumber": {
|
|
const checks = def.checks ?? [];
|
|
let min: number | undefined;
|
|
let max: number | undefined;
|
|
let isInt = false;
|
|
for (const c of checks) {
|
|
if (c.kind === "min") min = c.value;
|
|
if (c.kind === "max") max = c.value;
|
|
if (c.kind === "int") isInt = true;
|
|
}
|
|
const lo = min ?? 0;
|
|
const hi = max ?? Math.max(lo + 100, 100);
|
|
const mid = isInt ? Math.round((lo + hi) / 2) : (lo + hi) / 2;
|
|
values = [...new Set([lo, mid, hi])];
|
|
break;
|
|
}
|
|
case "ZodUnion": {
|
|
const merged = (def.options ?? []).flatMap((option) => deriveValues(option) ?? []);
|
|
values = merged.length > 0 ? [...new Set(merged)] : null;
|
|
break;
|
|
}
|
|
default:
|
|
values = null;
|
|
}
|
|
|
|
if (values && omittable) values = [...values, undefined];
|
|
return values;
|
|
}
|
|
|
|
/**
|
|
* Returns the combinatorial axes for a settings schema, or [] when the schema
|
|
* is not an object or has no enumerable fields.
|
|
*/
|
|
export function deriveAxes(schema: z.ZodType<unknown, z.ZodTypeDef, unknown>): PictAxis[] {
|
|
const obj = unwrap(schema as ZodSchemaLike);
|
|
if (obj._def?.typeName !== "ZodObject" || typeof obj._def.shape !== "function") return [];
|
|
|
|
const axes: PictAxis[] = [];
|
|
for (const [key, field] of Object.entries(obj._def.shape())) {
|
|
const values = deriveValues(field);
|
|
// An axis needs at least two values to contribute to pair coverage.
|
|
if (values && values.length >= 2) axes.push({ key, values });
|
|
}
|
|
return axes;
|
|
}
|
|
|
|
/** Removes undefined-valued keys so "omitted" really means omitted. */
|
|
export function compactCase(combo: Record<string, unknown>): Record<string, unknown> {
|
|
return Object.fromEntries(Object.entries(combo).filter(([, v]) => v !== undefined));
|
|
}
|
|
|
|
/**
|
|
* Recursively collects every string sub-schema that carries a regex check
|
|
* (hex colors and similar). zod-fast-check cannot generate for those without
|
|
* an override, so the fuzz suite overrides each collected instance.
|
|
*/
|
|
export function collectRegexStringSchemas(schema: unknown): unknown[] {
|
|
const found: unknown[] = [];
|
|
const seen = new Set<unknown>();
|
|
|
|
const visit = (node: ZodSchemaLike | undefined): void => {
|
|
if (!node || typeof node !== "object" || seen.has(node)) return;
|
|
seen.add(node);
|
|
const def = node._def;
|
|
if (!def) return;
|
|
|
|
if (def.typeName === "ZodString") {
|
|
const hasRegex = (def.checks ?? []).some((c) => c.kind === "regex");
|
|
if (hasRegex) found.push(node);
|
|
return;
|
|
}
|
|
|
|
if (def.typeName === "ZodObject" && typeof def.shape === "function") {
|
|
for (const field of Object.values(def.shape())) visit(field);
|
|
return;
|
|
}
|
|
|
|
// Wrappers and containers
|
|
visit(def.schema);
|
|
visit(def.innerType);
|
|
visit(def.in);
|
|
visit((def as { type?: ZodSchemaLike }).type); // ZodArray element
|
|
for (const option of def.options ?? []) visit(option);
|
|
};
|
|
|
|
visit(schema as ZodSchemaLike);
|
|
return found;
|
|
}
|