Files

92 lines
3.2 KiB
JavaScript

// Loads a bundle in an isolated node:vm context with only the globals the Go
// host provides. If a test passes here, the artifact that ships is the thing
// that passed — not the TypeScript it was built from.
//
// The context is deliberately spare: no `require`, no `process`, no `fetch`, no
// `Date.now`. Anything the bundle reaches for that is not listed here is a bug
// in the bundle, and it surfaces as a ReferenceError rather than as a surprise
// in production.
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import vm from "node:vm";
import { build } from "../build.mjs";
const here = dirname(fileURLToPath(import.meta.url));
export const NOW = 1_760_000_000_000; // a fixed clock, so `generated` is exact
// The tenant store, as the Go host presents it: the four statements db.ts
// issues, over a map. It is a MODEL of the host and not a second SQLite — the Go
// side runs the real one against the real schema, and its own test is what says
// the two agree. Keeping it here means a bundle test never needs a database.
function store() {
const rows = new Map(); // "kindid" -> { kind, id, ord, data, updated }
const key = (kind, id) => `${kind}${id}`;
return {
rows,
db: {
query(sql, args) {
if (sql.includes("AND id = ?")) {
const r = rows.get(key(args[0], args[1]));
return r ? [r] : [];
}
return [...rows.values()]
.filter((r) => r.kind === args[0])
.sort((a, b) => a.ord - b.ord || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
},
exec(sql, args) {
if (sql.startsWith("DELETE")) {
return { changes: rows.delete(key(args[0], args[1])) ? 1 : 0, lastId: 0 };
}
const [kind, id, ord, data, updated] = args;
rows.set(key(kind, id), { kind, id, ord, data, updated });
return { changes: 1, lastId: 0 };
},
},
};
}
// `own` defaults TRUE: the committed bundle carries this deployment's own
// inventory, and that is the tenant almost every case is about. A case that
// wants a fresh organization passes own: false and gets exactly what a new
// customer's first request gets — no baseline, no rows, no numbers.
export function run(src, { now = () => NOW, audit, own = true, seed } = {}) {
const s = store();
if (seed) for (const r of seed) s.rows.set(`${r.kind}${r.id}`, r);
let n = 0;
const ctx = {
__now: now,
__db: s.db,
__own: () => own,
__newId: () => `id${++n}`,
JSON,
Math,
RegExp,
Number,
String,
Array,
Object,
isFinite,
};
if (audit) ctx.__audit = audit;
ctx.globalThis = ctx;
vm.createContext(ctx);
vm.runInContext(src, ctx);
return { handle: ctx.handle, trust: ctx.trust, rows: s.rows };
}
export const source = () => readFileSync(join(here, "..", "bundle.js"), "utf8");
// The committed artifact, which is what ships.
export function load(opts) {
return run(source(), opts);
}
// A bundle built from a SYNTHETIC inventory. This is how the coverage fold is
// exercised on data written for one case: same code, same vm, different data.
export async function loadWith(controls, frameworks, opts) {
return run(await build({ write: false, controls, frameworks }), opts);
}