import { execFile } from "node:child_process"; import { cp, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; /** * Browser-native dialogs — `alert`, `confirm`, `prompt` — are prohibited in product UI (DESIGN.md * §8.1, ADR 0059). The guard is repository-wide and two-part: * * 1. Biome's scope-aware `suspicious/noAlert`, an error in the root `biome.json`, so `pnpm lint` * refuses a dialog call anywhere in this repository — the shell, `@ekklesia/ui`, the module * template — while leaving a local function or a method that shares the name alone. * 2. A narrow scan of browser source for the two forms that rule cannot see: a call through `self`, * `top`, `parent`, or `frames`, and borrowing the function without calling it. * * This file proves both against fixtures, with the repository's own lint configuration, so the * guard is shown to catch real usage and to stay quiet about lookalikes. */ const execFileAsync = promisify(execFile); const repositoryRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url))); const fixtures = path.join(repositoryRoot, "tests", "fixtures", "native-dialogs"); const biome = path.join(repositoryRoot, "node_modules", ".bin", "biome"); /** Where browser code lives in this repository. */ const BROWSER_SOURCE_ROOTS = [ "surfaces", path.join("packages", "ui", "src"), path.join("packages", "module-ui-sdk", "src"), path.join("packages", "module-ui-host", "src"), path.join("templates", "module", "src"), ]; /** A dialog reached through a window alias, called or not: `self.prompt(`, `= window.confirm;`. */ const DIALOG_THROUGH_A_WINDOW = /\b(?:window|globalThis|self|top|parent|frames)\s*(?:\.\s*|\[\s*["'`])(?:alert|confirm|prompt)\b/u; function withoutComments(source: string): string { return source.replace(/\/\*[\s\S]*?\*\//gu, "").replace(/(^|[^:\\])\/\/.*$/gmu, "$1"); } /** Lines that reach a dialog in a way Biome's rule does not report. */ function scan(source: string): number[] { return withoutComments(source) .split("\n") .flatMap((line, index) => { const code = line.replace(/\/(?:\\.|[^/\n])+\/[a-z]*/gu, "").replace(/"[^"]*"|'[^']*'/gu, ""); return DIALOG_THROUGH_A_WINDOW.test(code) ? [index + 1] : []; }); } async function sourceFiles(directory: string): Promise { const entries = await readdir(directory, { withFileTypes: true }).catch(() => []); const files: string[] = []; for (const entry of entries) { const full = path.join(directory, entry.name); if (entry.isDirectory()) { if (!["node_modules", "dist", ".turbo", "tests", "test"].includes(entry.name)) { files.push(...(await sourceFiles(full))); } } else if (/\.(?:ts|tsx|js|jsx|mjs)$/u.test(entry.name)) { files.push(full); } } return files; } let workspace: string; /** Lints a fixture with exactly the root configuration's rules, from a directory the root * configuration's own `files.includes` does not exclude. */ async function lint( fixture: string, ): Promise> { try { await execFileAsync(biome, ["lint", "--max-diagnostics=100", fixture], { cwd: workspace, }); return []; } catch (error) { const output = `${(error as { stdout?: string }).stdout ?? ""}${(error as { stderr?: string }).stderr ?? ""}`; return [...output.matchAll(/^\S*?(\d+):\d+ (lint\/\S+)/gmu)].map((match) => ({ line: Number(match[1]), rule: match[2] ?? "", })); } } beforeAll(async () => { workspace = await mkdtemp(path.join(tmpdir(), "ekklesia-native-dialogs-")); const config = JSON.parse(await readFile(path.join(repositoryRoot, "biome.json"), "utf8")) as { files?: unknown; }; delete config.files; await writeFile(path.join(workspace, "biome.json"), JSON.stringify(config)); await cp(fixtures, workspace, { recursive: true }); }); afterAll(async () => { await rm(workspace, { force: true, recursive: true }); }); describe("the repository's lint refuses browser-native dialogs", () => { it("makes noAlert an error in the root configuration", async () => { const config = JSON.parse(await readFile(path.join(repositoryRoot, "biome.json"), "utf8")) as { linter?: { rules?: { suspicious?: { noAlert?: unknown } } }; }; expect(config.linter?.rules?.suspicious?.noAlert).toBe("error"); }); it("reports every dialog call", async () => { const findings = await lint("prohibited.ts"); expect(findings.filter((finding) => finding.rule === "lint/suspicious/noAlert")).toEqual( [2, 3, 4, 5, 6, 7, 8, 9].map((line) => ({ line, rule: "lint/suspicious/noAlert" })), ); }, 30_000); it("reports nothing about a local function, a method, a property, a string, or a comment", async () => { expect(await lint("permitted.ts")).toEqual([]); }, 30_000); }); describe("the repository scan covers what the lint rule cannot see", () => { it("catches a dialog called through self, or borrowed without being called", async () => { const source = await readFile(path.join(fixtures, "prohibited-references.ts"), "utf8"); expect(scan(source)).toEqual([2, 3]); }); it("stays quiet about the permitted fixture", async () => { const source = await readFile(path.join(fixtures, "permitted.ts"), "utf8"); expect(scan(source)).toEqual([]); }); it("finds no dialog anywhere in this repository's browser source", async () => { const offenders: string[] = []; let scanned = 0; for (const sourceRoot of BROWSER_SOURCE_ROOTS) { for (const file of await sourceFiles(path.join(repositoryRoot, sourceRoot))) { scanned += 1; for (const line of scan(await readFile(file, "utf8"))) { offenders.push(`${path.relative(repositoryRoot, file)}:${line}`); } } } expect(scanned).toBeGreaterThan(20); expect(offenders).toEqual([]); }); });