diff --git a/bot/test/route-privacy.test.mjs b/bot/test/route-privacy.test.mjs new file mode 100644 index 0000000..d9d1c51 --- /dev/null +++ b/bot/test/route-privacy.test.mjs @@ -0,0 +1,79 @@ +#!/usr/bin/env node +/* + * route-privacy.test.mjs — the D-05 Worker boundary, gated (05-05, T-05-16). + * ------------------------------------------------------------------------------ + * D-05: "The Worker gains no /api/route this phase. Fixtures ride in the web + * bundle." The routing stack lives ENTIRELY in the webview — @araharah/routing + * is imported by web/src only; the Worker imports only @araharah/search. + * The pivot's privacy posture ("your coordinates were never sent anywhere") + * is true partly BECAUSE no route endpoint exists to send them to — so the + * abstinence is pinned by reading the source itself (the privacy.test.mjs / + * PG-gate pattern: comment-stripped, prefer-fail token scans): + * + * RP1 no route proxy — zero `/api/route` tokens AND zero + * `registerRoute` / `routeProxy` tokens anywhere in bot/src + * (comment-stripped): the Worker gains no route endpoint, not even + * an accidentally-named one + * RP2 no routing import — zero `@araharah/routing` specifiers in bot/src: + * routing workspace code cannot leak into the Worker bundle even as + * a dead import (the bundle would grow and the boundary would blur) + * + * The scans cover EVERY TypeScript file under bot/src and grow with the + * tree automatically — a future route proxy fails here at test time, not + * in review. + */ +import { strict as assert } from "node:assert"; +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; +import { test } from "./harness.mjs"; + +const SRC = path.join(fileURLToPath(new URL(import.meta.url)), "..", "..", "src"); + +function listFiles(dir, exts, acc = []) { + for (const entry of readdirSync(dir)) { + const full = path.join(dir, entry); + if (statSync(full).isDirectory()) listFiles(full, exts, acc); + else if (exts.some((e) => entry.endsWith(e))) acc.push(full); + } + return acc; +} + +function stripComments(src) { + return src + .replace(/\/\*[\s\S]*?\*\//g, " ") // block comments + .replace(/(^|[^:\\])\/\/[^\n]*/g, "$1"); // line comments (not URLs inside strings — benign over-stripping, gate is prefer-fail reading) +} + +function codeText(file) { + return stripComments(readFileSync(file, "utf8")); +} + +/* ————— RP1 — the Worker gains no route endpoint (D-05, T-05-16) ————— */ + +test("RP1 no route proxy — zero /api/route + registerRoute/routeProxy tokens in bot/src (comment-stripped, D-05)", () => { + const files = listFiles(SRC, [".ts"]); + assert.ok(files.length >= 8, `scanned a real tree (found ${files.length} files)`); + for (const file of files) { + const code = codeText(file); + for (const banned of ["/api/route", "registerRoute", "routeProxy"]) { + assert.ok( + !code.includes(banned), + `${path.relative(process.cwd(), file)} references "${banned}" — D-05 forbids a Worker route proxy (T-05-16); fixtures ride the web bundle`, + ); + } + } +}); + +/* ————— RP2 — routing workspace code never enters the Worker ————— */ + +test("RP2 no routing import — zero @araharah/routing specifiers in bot/src (D-05)", () => { + const files = listFiles(SRC, [".ts"]); + for (const file of files) { + const code = codeText(file); + assert.ok( + !code.includes("@araharah/routing"), + `${path.relative(process.cwd(), file)} imports @araharah/routing — routing lives in the webview bundle only; the Worker imports only @araharah/search (D-05)`, + ); + } +});