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:
araharah-executor
2026-10-24 00:56:08 +08:00
parent b2689f71b1
commit ec98c8d454
2 changed files with 284 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env node
/*
* converter.test.mjs — the D-05 JSON→SQLite converter's laws.
* -----------------------------------------------------------
* Backend #2 (the Malaysia-only WASM geocoder) must stay a FORMAT change,
* not a rewrite (D-05): the converter reads the SAME committed dataset the
* Worker serves from and emits a dataset with the SAME id set, today.
*
* CV1 the converter preserves the id set — same members, same count
* CV2 the converter reads the committed dataset (kl-gazetteer.json)
* CV3 the converter is not a runtime dependency — nothing under
* bot/src/ or web/src/ mentions sqlite or the converter
*
* Run: npm run search:test (exit 0 = all green)
*/
import { strict as assert } from "node:assert";
import { execFileSync } from "node:child_process";
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { DatabaseSync } from "node:sqlite";
import { test } from "./harness.mjs";
const SEARCH_DIR = path.resolve(import.meta.dirname, "..");
const CONVERTER = path.join(SEARCH_DIR, "scripts", "json-to-sqlite.mjs");
const GAZETTEER = path.join(SEARCH_DIR, "src", "data", "kl-gazetteer.json");
const SRC_TREES = [
path.join(SEARCH_DIR, "..", "bot", "src"),
path.join(SEARCH_DIR, "..", "web", "src"),
];
function scriptSource() {
return readFileSync(CONVERTER, "utf8");
}
function gazetteerIds() {
const gazetteer = JSON.parse(readFileSync(GAZETTEER, "utf8"));
return gazetteer.entries.map((e) => e.id).sort();
}
/** 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;
}
/** Run the converter into a fresh temp dir; returns { dir, files }. */
function runConverter() {
const dir = mkdtempSync(path.join(tmpdir(), "araharah-conv-"));
execFileSync(process.execPath, [CONVERTER, "--out", dir], { encoding: "utf8" });
return { dir, files: readdirSync(dir) };
}
/** Read the id set back out of whatever artifact the converter emitted
* (a real .sqlite via node:sqlite, or the documented .sql dump). */
function readIdsFrom(dir, files) {
const sqliteFile = files.find((f) => f.endsWith(".sqlite"));
if (sqliteFile) {
const db = new DatabaseSync(path.join(dir, sqliteFile), { readOnly: true });
const rows = db.prepare("SELECT id FROM places ORDER BY id").all();
db.close();
return rows.map((r) => r.id);
}
const dumpFile = files.find((f) => f.endsWith(".sql"));
assert.ok(dumpFile, `converter emitted no readable artifact: ${files.join(", ")}`);
const dump = readFileSync(path.join(dir, dumpFile), "utf8");
const ids = [];
for (const match of dump.matchAll(/INSERT INTO places[\s\S]*?\(\s*'([^']+)'/g)) {
ids.push(match[1]);
}
return ids.sort();
}
/* ————— CV1 — the id set is preserved exactly ————— */
test("CV1 the converter preserves the gazetteer's id set exactly", () => {
const { dir, files } = runConverter();
try {
assert.ok(files.length > 0, "converter emitted nothing");
const emitted = readIdsFrom(dir, files);
assert.deepEqual(
emitted,
gazetteerIds(),
"backend #2's dataset format must carry the SAME id set as the committed JSON (D-05)",
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
/* ————— CV2 — it reads the committed dataset ————— */
test("CV2 the converter reads the committed dataset", () => {
const src = scriptSource();
assert.match(
src,
/kl-gazetteer\.json/,
"the converter must read the same committed JSON the Worker serves from (D-07 single source)",
);
});
/* ————— CV3 — never a runtime dependency ————— */
test("CV3 the converter is not a runtime dependency", () => {
const offenders = [];
for (const tree of SRC_TREES) {
for (const file of walkFiles(tree)) {
const src = readFileSync(file, "utf8");
if (/sqlite|json-to-sqlite/i.test(src)) offenders.push(path.relative(process.cwd(), file));
}
}
assert.equal(
offenders.length,
0,
`D-05 violation: sqlite/json-to-sqlite appears under bot/src/ or web/src/ — ` +
`the converter is a repo tool (the Worker's canonical format is typed JSON, D-07): ` +
`${offenders.join(", ")}`,
);
});
+159
View File
@@ -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`,
);
});