- 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.
75 lines
3.1 KiB
TypeScript
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);
|
|
});
|
|
});
|