Files
siftlode/frontend/src/lib/mutationFailures.contract.test.ts
T
peter d44d8d6dda fix(gate): knip sees the watch entry, the build must emit watch.html, and two hand copies are held
- The raw-source contract tests read files from disk (rawSources.testkit)
  instead of import.meta.glob, which knip counted as importing every src file —
  its unused-files lane could never name an orphan. knip now names
  src/watchMain.tsx as an entry (registered in docs/gates.md).
- vite build fails when index.html or watch.html is not emitted.
- The watch-entry import walk follows dynamic import() too.
- audit_metrics.py's plan-code patterns are held equal to the guard's.
- CrashBoundary is domain-free: it moves into components/ui.
2026-10-06 03:19:33 +02:00

75 lines
3.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { readSources } from "./rawSources.testkit";
// Every `useMutation` NAMES what failed (owner decision: the toast's title says what, the
// line under it why). The app-wide reporter (lib/queryClient) reports a mutation that names nothing
// too — so nothing is silent any more — but under the generic "Couldn't complete that", which is the
// very toast that says nothing about what it is about when it lands a few seconds after the click.
//
// A mutation passes when it carries one of:
// - `meta: { failTitle }` the reporter's title for it,
// - `meta: { errorShownInline }` the page itself renders the failure,
// - an `onError` which reports (or rolls back — the reporter then speaks for it,
// under the title from `meta` if there is one).
//
// The assertion pins the SET of unnamed mutations, never a count, and checks the glob matched at
// all: a guard whose glob stops matching is green over nothing.
const NAMES = /\bfailTitle\b|\berrorShownInline\b|\bonError\b/;
/** The source of each `useMutation(...)` call, found by paren balance with comments stripped —
* a comment with a stray "(" would otherwise run one block into the next one's `failTitle`. */
export function mutationCalls(source: string): string[] {
const code = source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:"'`])\/\/.*$/gm, "$1");
const calls: string[] = [];
let from = 0;
for (;;) {
const at = code.indexOf("useMutation(", from);
if (at < 0) return calls;
let depth = 0;
let i = at + "useMutation".length;
for (; i < code.length; i++) {
if (code[i] === "(") depth++;
else if (code[i] === ")" && --depth === 0) break;
}
calls.push(code.slice(at, i + 1));
from = i + 1;
}
}
function unnamed(): { hits: string[]; mutations: number } {
const files = readSources(__dirname, "../**/*.{ts,tsx}");
const hits: string[] = [];
let mutations = 0;
for (const [rel, source] of Object.entries(files)) {
const path = rel.startsWith("./") ? `lib/${rel.slice(2)}` : rel.replace(/^\.\.\//, "");
if (/\.test\.tsx?$|\.d\.ts$/.test(path)) continue;
mutationCalls(String(source)).forEach((call, n) => {
mutations++;
if (!NAMES.test(call)) hits.push(`${path}#${n + 1}`);
});
}
return { hits: hits.sort(), mutations };
}
describe("the failed-write contract", () => {
it("has no mutation that leaves its failure unnamed", () => {
const { hits, mutations } = unnamed();
expect(mutations).toBeGreaterThan(50);
expect(hits).toEqual([]);
});
it("reads a mutation call to its end, whatever its body holds", () => {
const src = `
const a = useMutation({
mutationFn: () => api.x(fn(1), (2)),
// a stray "(" in a comment must not swallow the next call
});
const b = useMutation({ mutationFn: api.y, meta: { failTitle: "k" } });
`;
const calls = mutationCalls(src);
expect(calls).toHaveLength(2);
expect(NAMES.test(calls[0] ?? "")).toBe(false);
expect(NAMES.test(calls[1] ?? "")).toBe(true);
});
});