- 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.
160 lines
6.0 KiB
JavaScript
160 lines
6.0 KiB
JavaScript
#!/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`,
|
|
);
|
|
});
|