feat(03-02): D-11 Mock-Telegram harness + the four requirement gates (17 cases green)
- telegram-mock.ts: fake window.Telegram.WebApp injected into the SAME window
(never an iframe — Bot API 10.2 origin restriction, Pitfall 6); Web-Crypto
dev signer mirroring bot/src/security/initdata.ts's two-step HMAC (async
makeDevInitData — node:crypto stays out of the browser bundle path);
controllable LocationManager stream, themeChanged emitter, BackButton clicks
- location-chain.test.mjs (10 cases): GEO-01 version gate / null-WebApp guard
/ null-denial / hard timeout / stale-detector + one-read nudge; GEO-02 pin
pickup / bounded 60s poll / Desktop no-geolocation leg ' same denial keys
- no-storage.test.mjs (3 cases): GEO-03 comment-stripped grep gate over
web/src/**/*.{ts,html}, no-SW invariant, store.ts storage-assignment probe
- theme.test.mjs (4 cases): PX-02 colorScheme + luminance fallback + setStyle
source-introspection (Pitfall 7) + themeChanged re-apply; document stub
installed and restored per case
- Rule 1 fix in telegram.ts: looksStale/nudgeAndRetryTelegram accept an
omitted lastAccepted (undefined normalized to null) ' acquireFix(deps)
first-call contract
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* no-storage.test.mjs — the GEO-03 grep gate (zero persistence, source-introspection).
|
||||
* -----------------------------------------------------------------------------------
|
||||
* Inverted form of bot/test/privacy.test.mjs (PG1 pins writes to carry TTLs; this
|
||||
* pin that web/src contains NO persistence surface at all — D-03 (memory-only
|
||||
* token) + D-07 (coordinates live in memory only)).
|
||||
*
|
||||
* The gate MUST strip comments before scanning: web/src/mock/telegram-mock.ts's
|
||||
* header documents the ban in prose, and prose is not a call site. Strip block
|
||||
* comments (/* … */) and line comments (// …), THEN assert none of the banned
|
||||
* tokens appear — code occurrences only.
|
||||
*/
|
||||
import { strict as assert } from "node:assert";
|
||||
import { readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { test } from "./harness.mjs";
|
||||
|
||||
const SRC = path.join(path.dirname(fileURLToPath(new URL(import.meta.url))), "..", "src");
|
||||
|
||||
const BANNED = [
|
||||
"localStorage", "sessionStorage", "indexedDB", "caches", "CacheStorage",
|
||||
"document.cookie", "WebApp.DeviceStorage", "WebApp.SecureStorage", "WebApp.CloudStorage",
|
||||
];
|
||||
|
||||
/* path-scrape + comment strip (the plan's contract for this gate) */
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
/* ————— GEO3 — the mechanical gate over every web/src source ————— */
|
||||
|
||||
test("GEO3 no banned storage token appears in web/src code (comments stripped, all *.ts/*.html)", () => {
|
||||
const files = listFiles(SRC, [".ts", ".html"]);
|
||||
assert.ok(files.length >= 12, `scanned a real tree (found ${files.length} files)`);
|
||||
for (const file of files) {
|
||||
const code = codeText(file);
|
||||
for (const banned of BANNED) {
|
||||
assert.ok(!code.includes(banned), `${path.relative(process.cwd(), file)} references ${banned} — GEO-03 violated`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("GEO3 the runtime-probe-preceding invariant: no service worker / registration call exists", () => {
|
||||
// Vite emits no SW by default; this pins the "and none was added" half (RESEARCH GEO-03 §).
|
||||
for (const file of listFiles(SRC, [".ts", ".html"])) {
|
||||
const code = codeText(file);
|
||||
for (const token of ["serviceWorker.register", "navigator.serviceWorker", "workbox"]) {
|
||||
assert.ok(!code.includes(token), `${path.relative(process.cwd(), file)} references ${token} — GEO-03 surface added`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("GEO3 web/src/session/store.ts assigns nothing to any storage object (the D-03 half)", () => {
|
||||
const code = codeText(path.join(SRC, "session", "store.ts"));
|
||||
assert.ok(!/Storage\s*\.\s*setItem|Storage\s*\.\s*getItem/.test(code), "store.ts touches a Storage object");
|
||||
assert.ok(!/indexedDB\s*\.\s*open/.test(code), "store.ts opens IndexedDB");
|
||||
assert.ok(!/document\s*\.\s*cookie\s*=/.test(code), "store.ts writes a cookie");
|
||||
assert.ok(!/caches\s*\.\s*open/.test(code), "store.ts opens a CacheStorage");
|
||||
// ...and keeps the named TTL constant the client revalidates against (D-03).
|
||||
assert.ok(code.includes("SESSION_TTL_SECONDS = 900"), "the 900s TTL mirror by name is present");
|
||||
});
|
||||
Reference in New Issue
Block a user