- G1-G6: dataset invariants — schema, slug-id uniqueness, canonical-name
law ('KLCC' is an alias, never a name), DUKE-as-alias, KL bbox, the
ODbL header traveling inside the JSON
- G7-G9: the licensing gate — verbatim GPL-3.0 LICENSE, LICENSES.md,
the README no longer claiming 'all rights reserved'
- G10-G11: no logging/storage anywhere in the module (D-15); the loader
builds its index inside a function, never at module top level
- L1-L3: the phase's top mechanical risk — bare-Node loading with the
SRCH-01/negative probes, the static JSON import law, esbuild INLINING
the dataset + the wrangler dry-run build guard
13 cases fail while the dataset, loader, LICENSE files, and README fix
are absent — the RED gate.
136 lines
6.3 KiB
JavaScript
136 lines
6.3 KiB
JavaScript
#!/usr/bin/env node
|
|
/*
|
|
* loader-env.test.mjs — the THREE-ENVIRONMENT loader proof (D-07).
|
|
* ----------------------------------------------------------------
|
|
* THE phase's top mechanical risk (RESEARCH): the JSON dataset must be
|
|
* reachable identically from (1) the bare-Node corpus runner, (2) the
|
|
* Vite bundle, and (3) the wrangler/esbuild Worker bundle — Workers have
|
|
* no fs, so the dataset MUST be a static build-time import, never a
|
|
* runtime fetch.
|
|
*
|
|
* L1 bare Node (type-stripping): the module graph loads, the dataset
|
|
* parses, search('KLCC') resolves suria-klcc, out-of-area is refused
|
|
* L2 static JSON import in the loader + (once plan 04-04 lands) the
|
|
* Vite bundle carries the dataset marker; until then that half is
|
|
* SKIPPED visibly while the static-import half is still asserted
|
|
* L3 esbuild — the bundler family wrangler uses — INLINES the dataset
|
|
* (its bundle carries the 'suria-klcc' marker), and the Worker
|
|
* dry-run build guard exits 0
|
|
*
|
|
* Run: npm run search:test (exit 0 = all green)
|
|
*/
|
|
import { strict as assert } from "node:assert";
|
|
import { spawnSync } from "node:child_process";
|
|
import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { test } from "./harness.mjs";
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url)); // search/test
|
|
const searchDir = path.resolve(here, ".."); // search/
|
|
const repoRoot = path.resolve(searchDir, ".."); // repo root
|
|
const esbuildBin = path.join(repoRoot, "node_modules", ".bin", "esbuild");
|
|
const wranglerBin = path.join(repoRoot, "node_modules", ".bin", "wrangler");
|
|
|
|
// Lazy index load — missing module => per-case ERR_MODULE_NOT_FOUND (the RED state).
|
|
let indexPromise = null;
|
|
function loadIndexModule() {
|
|
if (!indexPromise) {
|
|
indexPromise = import("../src/index.ts");
|
|
}
|
|
return indexPromise;
|
|
}
|
|
|
|
test("L1 bare Node (type-stripping) — the loader works with zero infrastructure", async () => {
|
|
const mod = await loadIndexModule();
|
|
assert.ok(mod.getEntries().length >= 30, "the dataset parses in bare Node");
|
|
const source = mod.createGazetteerSource();
|
|
assert.equal(source.name, "gazetteer");
|
|
const hits = await source.search({ text: "KLCC" });
|
|
assert.ok(hits.length > 0, "KLCC must resolve");
|
|
assert.equal(hits[0].entry.id, "suria-klcc", "SRCH-01: KLCC resolves to suria-klcc at rank 1");
|
|
assert.equal((await source.search({ text: "Kuching" })).length, 0, "out-of-area is refused honestly (D-13)");
|
|
assert.equal((await source.search({ text: "Penang" })).length, 0, "out-of-area is refused honestly (D-13)");
|
|
assert.equal(source.byId("suria-klcc")?.name, "Suria KLCC", "byId is synchronous and honest");
|
|
assert.equal(source.byId("../etc/passwd"), null, "byId rejects non-slug ids");
|
|
});
|
|
|
|
test("L2 static JSON import in the loader (Vite-bundle half guarded until 04-04)", async () => {
|
|
const loaderPath = path.join(searchDir, "src", "gazetteer.ts");
|
|
const loaderSrc = readFileSync(loaderPath, "utf8");
|
|
assert.match(
|
|
loaderSrc,
|
|
/import raw from "\.\/data\/kl-gazetteer\.json"/,
|
|
"the dataset must be a STATIC import (build-time inlined)",
|
|
);
|
|
assert.ok(!/fetch\(/.test(loaderSrc), "a runtime fetch would hard-fail in a Worker");
|
|
assert.ok(!/readFileSync/.test(loaderSrc), "Workers have no fs — never read the dataset at runtime");
|
|
|
|
// The Vite-bundle half is only assertable once plan 04-04 wires web/ to
|
|
// @araharah/search. Until then: visible skip, static-import half asserted.
|
|
const webSrcDir = path.join(repoRoot, "web", "src");
|
|
let webImportsSearch = false;
|
|
try {
|
|
for (const entry of readdirSync(webSrcDir, { recursive: true })) {
|
|
const rel = String(entry);
|
|
if (rel.endsWith(".ts") || rel.endsWith(".tsx")) {
|
|
if (readFileSync(path.join(webSrcDir, rel), "utf8").includes("@araharah/search")) {
|
|
webImportsSearch = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// no web/src — treat as not-yet-landed
|
|
}
|
|
if (!webImportsSearch) {
|
|
console.log("SKIP L2 (web-bundle marker) — web/ does not import @araharah/search yet (lands in plan 04-04); the static-import half is asserted above");
|
|
return;
|
|
}
|
|
const distAssets = path.join(repoRoot, "web", "dist", "assets");
|
|
assert.ok(existsSync(distAssets), "web/dist/assets must exist once web imports the search module");
|
|
let marker = false;
|
|
for (const file of readdirSync(distAssets)) {
|
|
if (file.endsWith(".js") && readFileSync(path.join(distAssets, file), "utf8").includes("suria-klcc")) {
|
|
marker = true;
|
|
break;
|
|
}
|
|
}
|
|
assert.ok(marker, "the Vite bundle must inline the gazetteer (build-time, never fetched)");
|
|
});
|
|
|
|
test("L3 esbuild inlines the gazetteer; the wrangler dry-run build guard exits 0", () => {
|
|
// — the inline half (provable at wave 1): bundle the search module with the
|
|
// SAME bundler family wrangler uses and grep the output for the marker —
|
|
const bundleDir = "/tmp/opencode/araharah-search-bundle";
|
|
mkdirSync(bundleDir, { recursive: true });
|
|
const outfile = path.join(bundleDir, "index.js");
|
|
const bundle = spawnSync(
|
|
esbuildBin,
|
|
[path.join(searchDir, "src", "index.ts"), "--bundle", `--outfile=${outfile}`, "--log-level=error"],
|
|
{ encoding: "utf8", timeout: 60000 },
|
|
);
|
|
assert.equal(bundle.status, 0, `esbuild bundle failed: ${bundle.stderr || bundle.stdout}`);
|
|
const bundled = readFileSync(outfile, "utf8");
|
|
assert.ok(
|
|
bundled.includes("suria-klcc"),
|
|
"a static JSON import MUST be inlined at build time — a runtime fetch would be a hard Worker failure",
|
|
);
|
|
|
|
// — the Worker dry-run half (build regression guard ONLY): the bundle-
|
|
// marker grep for the WORKER bundle is plan 04-03 task 2's criterion,
|
|
// after bot.ts imports createGazetteerSource() —
|
|
if (!existsSync(path.join(repoRoot, "web", "dist", "index.html"))) {
|
|
console.log("SKIP L3 (wrangler dry-run) — web/dist is not built; run npm run build first");
|
|
return;
|
|
}
|
|
const dryrunDir = "/tmp/opencode/araharah-worker-dryrun";
|
|
mkdirSync(dryrunDir, { recursive: true });
|
|
const dry = spawnSync(wranglerBin, ["deploy", "--dry-run", `--outdir=${dryrunDir}`], {
|
|
cwd: path.join(repoRoot, "bot"),
|
|
encoding: "utf8",
|
|
timeout: 180000,
|
|
});
|
|
assert.equal(dry.status, 0, `wrangler dry-run failed: ${(dry.stderr || dry.stdout).slice(-800)}`);
|
|
});
|