test(04-02): RED — D-06 topup law suite (TP1-TP7) + D-05 converter suite (CV1-CV3)
- TP1 no 'overpass' under bot/src|web/src (never a runtime dependency) - TP2 identifying User-Agent pinned (missing UA = HTTP 406, live-verified) - TP3 bbox-bounded query tuple present - TP4 busy fixture -> exit 0 + 'Overpass instance busy' soft-exit line - TP5 committed fixture -> PROPOSAL output with name= lines + NEVER-writes law - TP6 source never names the curated dataset file - TP7 exactly one fetch( call site (shared helper for both live paths) - CV1 converter preserves the gazetteer id set exactly (read back via node:sqlite) - CV2 converter reads the committed kl-gazetteer.json - CV3 no sqlite/json-to-sqlite under bot/src|web/src 8 cases verified failing (scripts absent) before GREEN.
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* topup.test.mjs — the D-06 Overpass top-up tool's hard laws.
|
||||
* -----------------------------------------------------------
|
||||
* The top-up script is a MAINTAINER-TIME REPO TOOL, never a runtime
|
||||
* dependency (D-06). Its six laws are pinned here:
|
||||
*
|
||||
* TP1 it is not imported by the Worker — zero `overpass` occurrences
|
||||
* under bot/src/ or web/src/ (RESEARCH Pitfall 6)
|
||||
* TP2 it sends an identifying User-Agent — a request without one gets
|
||||
* HTTP 406 (live-verified 2026-10-23)
|
||||
* TP3 the query is bbox-bounded — an unbounded Overpass query times out
|
||||
* and invites rate-limiting
|
||||
* TP4 a busy instance is a NORMAL outcome: a fixture that is not an
|
||||
* Overpass response exits 0 printing "Overpass instance busy"
|
||||
* TP5 the committed fixture produces a valid-shaped PROPOSAL (no live
|
||||
* network in the test suite)
|
||||
* TP6 it never writes the curated dataset — the source never even names it
|
||||
* TP7 exactly ONE request per run — the Overpass and Nominatim paths
|
||||
* share a single fetch helper, so the source has exactly one fetch
|
||||
* call site regardless of the selected source
|
||||
*
|
||||
* Run: npm run search:test (exit 0 = all green)
|
||||
*/
|
||||
import { strict as assert } from "node:assert";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { test } from "./harness.mjs";
|
||||
|
||||
const SEARCH_DIR = path.resolve(import.meta.dirname, "..");
|
||||
const SCRIPT = path.join(SEARCH_DIR, "scripts", "overpass-topup.mjs");
|
||||
const FIXTURE = path.join(SEARCH_DIR, "test", "fixtures", "overpass-response.json");
|
||||
const SRC_TREES = [
|
||||
path.join(SEARCH_DIR, "..", "bot", "src"),
|
||||
path.join(SEARCH_DIR, "..", "web", "src"),
|
||||
];
|
||||
|
||||
function scriptSource() {
|
||||
return readFileSync(SCRIPT, "utf8");
|
||||
}
|
||||
|
||||
/** Recursively list every file under dir (source-introspection walk). */
|
||||
function walkFiles(dir) {
|
||||
const out = [];
|
||||
if (!existsSync(dir)) return out;
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) out.push(...walkFiles(full));
|
||||
else out.push(full);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function runScript(args) {
|
||||
return spawnSync(process.execPath, [path.relative(SEARCH_DIR, SCRIPT), ...args], {
|
||||
cwd: SEARCH_DIR,
|
||||
encoding: "utf8",
|
||||
});
|
||||
}
|
||||
|
||||
/* ————— TP1 — never a runtime dependency ————— */
|
||||
|
||||
test("TP1 the top-up script is not imported by the Worker", () => {
|
||||
const offenders = [];
|
||||
for (const tree of SRC_TREES) {
|
||||
for (const file of walkFiles(tree)) {
|
||||
const src = readFileSync(file, "utf8");
|
||||
if (/overpass/i.test(src)) offenders.push(path.relative(process.cwd(), file));
|
||||
}
|
||||
}
|
||||
assert.equal(
|
||||
offenders.length,
|
||||
0,
|
||||
`D-06 violation: 'overpass' appears under bot/src/ or web/src/ — the top-up ` +
|
||||
`is a repo tool and the Worker only ever reads the committed dataset: ${offenders.join(", ")}`,
|
||||
);
|
||||
});
|
||||
|
||||
/* ————— TP2 — the User-Agent is load-bearing ————— */
|
||||
|
||||
test("TP2 the script sends an identifying User-Agent", () => {
|
||||
const src = scriptSource();
|
||||
assert.ok(
|
||||
src.includes("User-Agent: araharah-gazetteer-topup/1.0"),
|
||||
"a request without this User-Agent gets HTTP 406 (live-verified) — the header is load-bearing",
|
||||
);
|
||||
});
|
||||
|
||||
/* ————— TP3 — bbox-bounded query ————— */
|
||||
|
||||
test("TP3 the query is bbox-bounded", () => {
|
||||
const src = scriptSource();
|
||||
assert.match(
|
||||
src,
|
||||
/\(\d+\.\d+,\s*\d+\.\d+,\s*\d+\.\d+,\s*\d+\.\d+\)/,
|
||||
"the Overpass query must carry a literal (s,w,n,e) bbox tuple — an unbounded query times out",
|
||||
);
|
||||
});
|
||||
|
||||
/* ————— TP4 — a busy instance exits 0 ————— */
|
||||
|
||||
test("TP4 the script exits 0 on a busy instance", () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "araharah-topup-busy-"));
|
||||
try {
|
||||
const busyFixture = path.join(dir, "busy.json");
|
||||
writeFileSync(busyFixture, JSON.stringify({ error: "busy" }));
|
||||
const res = runScript(["--source=fixture", `--fixture=${busyFixture}`]);
|
||||
assert.equal(
|
||||
res.status,
|
||||
0,
|
||||
`a busy/unusable Overpass instance is a NORMAL retryable outcome — the script must exit 0 ` +
|
||||
`(got ${res.status}; stderr: ${res.stderr})`,
|
||||
);
|
||||
assert.match(
|
||||
res.stdout,
|
||||
/Overpass instance busy — retry later/,
|
||||
"the busy path must print exactly the soft-exit line",
|
||||
);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
/* ————— TP5 — the committed fixture yields a PROPOSAL ————— */
|
||||
|
||||
test("TP5 the script produces a valid-shaped proposal from the committed fixture", () => {
|
||||
const res = runScript(["--source=fixture", `--fixture=${path.relative(SEARCH_DIR, FIXTURE)}`]);
|
||||
assert.equal(res.status, 0, `fixture run must exit 0 (stderr: ${res.stderr})`);
|
||||
assert.match(res.stdout, /PROPOSAL/, "output must carry the PROPOSAL header");
|
||||
assert.match(res.stdout, /name=/, "output must list at least one name= candidate line");
|
||||
assert.match(res.stdout, /NEVER writes/, "the proposal header must repeat the never-writes law");
|
||||
});
|
||||
|
||||
/* ————— TP6 — it cannot write the curated file ————— */
|
||||
|
||||
test("TP6 the script never writes the curated file", () => {
|
||||
const src = scriptSource();
|
||||
assert.doesNotMatch(
|
||||
src,
|
||||
/kl-gazetteer\.json/,
|
||||
"the script's source must never name the curated dataset file — it cannot write what it cannot name",
|
||||
);
|
||||
assert.match(src, /--out/, "the only permitted write is an explicit --out proposal path");
|
||||
});
|
||||
|
||||
/* ————— TP7 — exactly one request per run ————— */
|
||||
|
||||
test("TP7 exactly one request per run — one shared fetch helper", () => {
|
||||
const src = scriptSource();
|
||||
const callSites = (src.match(/fetch\(/g) ?? []).length;
|
||||
assert.equal(
|
||||
callSites,
|
||||
1,
|
||||
`the Overpass and Nominatim live paths must share ONE fetch helper — ` +
|
||||
`found ${callSites} fetch( call sites`,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user