#!/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)}`); });