Files
Hankan/test/source-hygiene.test.ts
MechaCat02 0b04931699 fix: a NUL byte was hiding two source files from grep, git and diff
TaskHost.tsx used a literal NUL as the delimiter in its drag-and-drop
payload, and tools/dict/build.mjs used one to join headword and part of
speech. Both work at runtime. Both also make the file *binary* to every
text tool: git shows "Bin 12259 bytes" instead of a diff, and grep prints
nothing at all for a match.

That is not hypothetical. Searching TaskHost.tsx for "<input" came back
empty three times while reviewing it, which is how its four exercise
inputs came to be reported as absent -- and why the accessibility defect
in them went unseen. Written as the escape \u0000 the value is identical
and the file stays text.

test/source-hygiene.test.ts fails on any control byte in a source file, so
this cannot come back quietly.

With the files readable again, the sweep the NUL had been hiding: eleven
form controls had no accessible name. The exercise blanks announced only
an ellipsis, and the part-of-speech select announced nothing. A
placeholder is not a label -- it disappears the moment you type. All
eleven now carry one, named after the thing they answer.

`npm run lint` gains --max-warnings 0. exhaustive-deps is configured as a
warning, so a hooks-dependency bug would have passed CI silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 21:06:39 +02:00

51 lines
1.8 KiB
TypeScript

/* Source files must stay text.
TaskHost.tsx carried a literal NUL byte, used as a delimiter in a
drag-and-drop payload. It worked at runtime, and it made the file binary
to git, grep and diff -- every search for that component's markup came
back empty, including the one meant to find an accessibility defect in
it. Written as the escape \\u0000 it behaves identically and stays text. */
import { describe, it, expect } from "vitest";
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
const ROOTS = ["app/src", "server/src", "shared", "tools", "test", "types"];
const SKIP = new Set(["node_modules", "dist", "android", ".vite"]);
const TEXT = /\.(ts|tsx|mjs|js|css|json|md|html|yml|yaml|sql)$/;
function walk(dir: string, out: string[] = []): string[] {
for (const e of readdirSync(dir)) {
if (SKIP.has(e)) continue;
const full = join(dir, e);
if (statSync(full).isDirectory()) walk(full, out);
else if (TEXT.test(e)) out.push(full);
}
return out;
}
describe("source hygiene", () => {
const files = ROOTS.flatMap((r) => walk(r));
it("scans a meaningful number of files", () => {
expect(files.length).toBeGreaterThan(50);
});
it("has no control bytes that would make a file binary to git and grep", () => {
const offenders: string[] = [];
for (const f of files) {
const buf = readFileSync(f);
for (let i = 0; i < buf.length; i++) {
const b = buf[i]!;
// Everything below 0x20 except tab, LF and CR.
if (b < 0x20 && b !== 0x09 && b !== 0x0a && b !== 0x0d) {
const line = buf.subarray(0, i).toString("utf8").split("\n").length;
offenders.push(`${f}:${line} -- byte 0x${b.toString(16).padStart(2, "0")}`);
break;
}
}
}
expect(offenders).toEqual([]);
});
});