mirror of
https://github.com/mvschwarz/openrig-studio.git
synced 2026-11-08 08:11:09 -10:00
The pathname does not exist downstream, and the TDZ judge is the evaluator
Round 5, built to PM's ratified criterion and approved proposal. Both findings
were the same disease — a guard trusting a region or a spelling — and both are
closed by removing the material the violation needs, not by a fifth guard.
PROPERTY 1 (MUST-FIX): the API flow holds no pathname AT ALL. Routing is now
resolveApi() in app/verbs.mjs — the grammar module — which takes the raw
request URL, matches the table, and returns an execute closure. Handlers
receive { searchParams, verb, rest }; verified first that no handler used
anything but searchParams. handleRequest's api flow has neither `u` nor
API_PREFIX as a binding, and the static flow's URL is constructed AFTER the
api branch returns — that ordering is load-bearing and is recorded, with the
whole invariant, in docs/design/routing-data-boundary.md (PM's addition: a
future maintainer must know they are breaking the property on purpose).
sdk-qa's derived-prefix arm — reproduced on the unfixed tree first: 200,
unreported — is now UNWRITABLE at the ordinary place: `u` is in TDZ and
API_PREFIX is not a binding, so the plant detonates as a ReferenceError on
first request and never serves. The unwritability control asserts exactly
that for both their round-4 literal and round-5 derived-prefix plants, kept
permanent. New property tests pin routing to its definition: routes=[] misses
everything, routes=[X] resolves exactly X, and the resolved request carries no
pathname field. 404 body byte-identical; handler signature change is internal
shape (PM confirmed no contract-doc change).
PROPERTY 2 (HIGH): the static TDZ analyzer is DELETED — the fourth deletion on
this branch, per the pattern that a thing that keeps needing guards should
stop existing. Four static versions each classified source text and each
missed the next ordinary spelling (misplaced walk, direct initializer, IIFE
initializer, standalone IIFE statement — reproduced on the unfixed tree first:
boot exit 1, analyzer 0 violations). The guard is now the evaluator: spawn the
module, assert it evaluates; the control plants all four historical spellings
and asserts Node refuses each naming the binding. Coverage of future spellings
is the evaluator's by construction — the check is not a model of evaluation,
it is evaluation. Stated trade: no early failure on merely-fragile
arrangements; the position discipline stays as the init-block comment.
350 -> 352 tests, 352/352 green.
This commit is contained in:
1 parent
ab936f810c
commit
6fc145b608
5 files changed
+326
-280
No files matched your search
+60
-54
@@ -20,7 +20,7 @@ import http from "node:http";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { RUNTIME_OWNED_VERBS, SUBSTITUTABLE_VERBS, openVocabMap, lookup, verbMatches } from "./verbs.mjs";
|
||||
import { RUNTIME_OWNED_VERBS, SUBSTITUTABLE_VERBS, openVocabMap, lookup, verbMatches, resolveApi } from "./verbs.mjs";
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(HERE, "..");
|
||||
@@ -1033,21 +1033,20 @@ function filesSearch(q) {
|
||||
// two markers, enforced by routingSurfaceViolations() in app/verbs.mjs and the
|
||||
// reserved-verbs tests. A mention outside the fence fails the suite.
|
||||
//
|
||||
// THE FENCE CONTAINS DATA, NOT DISPATCH — PM's ruling, fourth round on this
|
||||
// class, and the reason it is the last: every earlier fix defined a trusted
|
||||
// region (a spelling list, a fenced file region, a dispatcher body) and the
|
||||
// next raw arm simply moved inside the newest region. A trusted region is
|
||||
// always one special-case away from wrong. So the fence now holds three
|
||||
// constants and nothing executable with unbounded reach: the prefix, the
|
||||
// contract verb, and the table. Code inside a handler runs only AFTER its row
|
||||
// matched — bounded by the row that registered it.
|
||||
// THE FENCE CONTAINS DATA, NOT DISPATCH — and since round 5, THIS FILE DOES
|
||||
// NOT ROUTE AT ALL. Every earlier fix defined a trusted region (a spelling
|
||||
// list, a fenced file region, a dispatcher body) and the next raw arm simply
|
||||
// moved inside the newest region: a trusted region is always one special-case
|
||||
// away from wrong. The fence now holds two constants and the table — nothing
|
||||
// executable with unbounded reach. Code inside a handler runs only AFTER its
|
||||
// row matched, bounded by the row that registered it, and handlers receive
|
||||
// { searchParams, verb, rest } — never a pathname.
|
||||
//
|
||||
// ROUTING is the single table lookup in handleRequest below — the ONLY
|
||||
// operation in the runtime that matches a pathname to a handler. DISPATCH
|
||||
// receives the resolved row. A special case in the dispatch path has no
|
||||
// pathname to branch on without typing an /api literal outside this fence,
|
||||
// which fails the suite. "A route cannot serve without a row" is therefore
|
||||
// structural: a served route IS a dispatched row.
|
||||
// ROUTING lives in resolveApi (app/verbs.mjs), the one place a pathname meets
|
||||
// this table. handleRequest hands over the raw URL and receives an execute
|
||||
// closure; the pathname is OUT OF SCOPE downstream by construction, so "a
|
||||
// route cannot serve without a row" is a fact about the data flow. See
|
||||
// docs/design/routing-data-boundary.md for the load-bearing invariant.
|
||||
//
|
||||
// ONE STRUCTURE, TWO READERS: this table is what routing consults at request
|
||||
// time AND what `runtime.routes` and classification enumerate, so enumeration
|
||||
@@ -1058,10 +1057,9 @@ function filesSearch(q) {
|
||||
//
|
||||
// ADD A ROUTE BY ADDING A ROW, then classify the verb in app/verbs.mjs.
|
||||
// A verb ending in `/` is a PREFIX, per the shared grammar (verbMatches).
|
||||
const API_PREFIX = "/api/";
|
||||
const CONTRACT_VERB = "/api/contract";
|
||||
const API_ROUTES = [
|
||||
{ verb: "/api/focus", handler: async (u, req, res) => {
|
||||
{ verb: "/api/focus", handler: async (request, req, res) => {
|
||||
if (req.method === "POST") {
|
||||
const raw = await readBody(req);
|
||||
let patch;
|
||||
@@ -1075,11 +1073,11 @@ const API_ROUTES = [
|
||||
}
|
||||
// The read the record never had. `?since=` is the change-signal contract,
|
||||
// not a second polling mechanism — see contract/change-signal.md.
|
||||
const since = u.searchParams.get("since");
|
||||
const since = request.searchParams.get("since");
|
||||
const marker = focusMarker();
|
||||
return sendJson(res, 200, { ok: true, changed: since === null || since !== marker, marker, focus: focusRecord });
|
||||
} },
|
||||
{ verb: "/api/drive", handler: async (u, req, res) => {
|
||||
{ verb: "/api/drive", handler: async (request, req, res) => {
|
||||
if (req.method === "POST") {
|
||||
const raw = await readBody(req);
|
||||
let op;
|
||||
@@ -1092,11 +1090,11 @@ const API_ROUTES = [
|
||||
// focus, so a surface that already watches one is watching this with the
|
||||
// primitive it has rather than a third mechanism of its own.
|
||||
driveReads += 1;
|
||||
const since = u.searchParams.get("since");
|
||||
const since = request.searchParams.get("since");
|
||||
const marker = driveMarker();
|
||||
return sendJson(res, 200, { ok: true, changed: since === null || since !== marker, marker, op: driveOp });
|
||||
} },
|
||||
{ verb: "/api/capture-target", handler: async (u, req, res) => {
|
||||
{ verb: "/api/capture-target", handler: async (request, req, res) => {
|
||||
if (req.method === "POST") {
|
||||
const raw = await readBody(req);
|
||||
let body;
|
||||
@@ -1117,7 +1115,7 @@ const API_ROUTES = [
|
||||
}
|
||||
return sendJson(res, 200, { ok: true, target: captureTarget });
|
||||
} },
|
||||
{ verb: "/api/annotations", handler: async (u, req, res) => {
|
||||
{ verb: "/api/annotations", handler: async (request, req, res) => {
|
||||
if (req.method === "POST") {
|
||||
const raw = await readBody(req);
|
||||
let body;
|
||||
@@ -1135,10 +1133,10 @@ const API_ROUTES = [
|
||||
// Read is scoped. Without a scope this answers an EMPTY set rather than
|
||||
// every scope on the box: a caller that forgot the parameter would otherwise
|
||||
// receive another surface's marks and render them over this one.
|
||||
const scope = u.searchParams.get("scope");
|
||||
const scope = request.searchParams.get("scope");
|
||||
return sendJson(res, 200, { ok: true, scope: scope || null, records: scope ? (annotations.get(scope) || []) : [] });
|
||||
} },
|
||||
{ verb: CONTRACT_VERB, handler: (u, req, res) => {
|
||||
{ verb: CONTRACT_VERB, handler: (request, req, res) => {
|
||||
return sendJson(res, 200, {
|
||||
contractVersion: CONTRACT_VERSION,
|
||||
// VERSION IS READ, NEVER TYPED. A runtime that states its own version from a
|
||||
@@ -1181,21 +1179,21 @@ const API_ROUTES = [
|
||||
manifest: manifestReport(),
|
||||
});
|
||||
} },
|
||||
{ verb: "/api/factory/state", handler: (u, req, res) => sendJson(res, 200, factoryState()) },
|
||||
{ verb: "/api/events", handler: (u, req, res) => {
|
||||
{ verb: "/api/factory/state", handler: (request, req, res) => sendJson(res, 200, factoryState()) },
|
||||
{ verb: "/api/events", handler: (request, req, res) => {
|
||||
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", connection: "keep-alive" });
|
||||
res.write(`: connected\n\n`);
|
||||
sseClients.add(res);
|
||||
req.on("close", () => sseClients.delete(res));
|
||||
return;
|
||||
} },
|
||||
{ verb: "/api/files/tree", handler: (u, req, res) => {
|
||||
const t = filesTree(u.searchParams.get("dir"));
|
||||
{ verb: "/api/files/tree", handler: (request, req, res) => {
|
||||
const t = filesTree(request.searchParams.get("dir"));
|
||||
return t ? sendJson(res, 200, { ok: true, ...t })
|
||||
: sendJson(res, 400, { ok: false, error: "path outside the files root — files verbs are pinned to the runtime's files-root" });
|
||||
} },
|
||||
{ verb: "/api/files/read", handler: (u, req, res) => {
|
||||
const r = insideRoot(u.searchParams.get("path") || "");
|
||||
{ verb: "/api/files/read", handler: (request, req, res) => {
|
||||
const r = insideRoot(request.searchParams.get("path") || "");
|
||||
if (!r || !fs.existsSync(r) || fs.statSync(r).isDirectory())
|
||||
return sendJson(res, 404, { ok: false, error: "not found or outside the files root" });
|
||||
const kind = fileKind(r);
|
||||
@@ -1204,36 +1202,38 @@ const API_ROUTES = [
|
||||
content: fs.readFileSync(r, "utf8"), mtime: fs.statSync(r).mtimeMs });
|
||||
return sendJson(res, 200, { ok: true, kind, raw: "/api/files/raw?path=" + encodeURIComponent(r), mtime: fs.statSync(r).mtimeMs });
|
||||
} },
|
||||
{ verb: "/api/files/raw", handler: (u, req, res) => {
|
||||
const r = insideRoot(u.searchParams.get("path") || "");
|
||||
{ verb: "/api/files/raw", handler: (request, req, res) => {
|
||||
const r = insideRoot(request.searchParams.get("path") || "");
|
||||
if (!r || !fs.existsSync(r)) { res.writeHead(404); return res.end(); }
|
||||
res.writeHead(200, { "content-type": TYPES[path.extname(r).toLowerCase()] || "application/octet-stream",
|
||||
"content-length": fs.statSync(r).size, "cache-control": "no-store" });
|
||||
return fs.createReadStream(r).pipe(res);
|
||||
} },
|
||||
{ verb: "/api/files/search", handler: (u, req, res) => {
|
||||
const q = (u.searchParams.get("q") || "").trim();
|
||||
{ verb: "/api/files/search", handler: (request, req, res) => {
|
||||
const q = (request.searchParams.get("q") || "").trim();
|
||||
return sendJson(res, 200, { ok: true, hits: q.length < 2 ? [] : filesSearch(q) });
|
||||
} },
|
||||
];
|
||||
// <<< API ROUTING SURFACE
|
||||
|
||||
const handleRequest = async (req, res) => {
|
||||
const u = new URL(req.url, `http://127.0.0.1:${PORT}`);
|
||||
// ROUTING, then DISPATCH — deliberately not a function, so there is no
|
||||
// dispatcher body to trust. The find() below is the only operation in the
|
||||
// runtime that matches a pathname to a handler; dispatch receives the
|
||||
// resolved ROW. Claims the whole /api namespace: a row matches or the
|
||||
// request 404s, so no later code ever sees an /api path.
|
||||
if (u.pathname.startsWith(API_PREFIX)) {
|
||||
const row = API_ROUTES.find((r) => verbMatches(r.verb, u.pathname));
|
||||
try {
|
||||
if (row) await row.handler(u, req, res);
|
||||
else sendJson(res, 404, { ok: false,
|
||||
error: `no such contract route: ${u.pathname} — see GET ${CONTRACT_VERB} for capabilities and contract/runtime-api.md for the verb set` });
|
||||
} catch (e) { return sendJson(res, 500, { ok: false, error: String(e.message || e) }); }
|
||||
// THE API FLOW HOLDS NO PATHNAME. Routing happens inside resolveApi (the
|
||||
// grammar module): this function hands over the raw request URL and receives
|
||||
// either null (not the api namespace) or an execute closure that already
|
||||
// carries its resolved row. Neither `u` nor the namespace prefix exists as a
|
||||
// binding anywhere in this flow — a pathname special-case here is a
|
||||
// ReferenceError, not a code-review question. THE ORDERING BELOW IS
|
||||
// LOAD-BEARING: the static-flow URL is deliberately constructed AFTER this
|
||||
// branch returns, so no code that could see an api request ever holds a
|
||||
// pathname. See docs/design/routing-data-boundary.md before "simplifying"
|
||||
// either fact away.
|
||||
const api = resolveApi(req.url, `http://127.0.0.1:${PORT}`, API_ROUTES, sendJson);
|
||||
if (api) {
|
||||
try { await api.execute(req, res); }
|
||||
catch (e) { return sendJson(res, 500, { ok: false, error: String(e.message || e) }); }
|
||||
return;
|
||||
}
|
||||
const u = new URL(req.url, `http://127.0.0.1:${PORT}`);
|
||||
|
||||
// the manifest is served as its VALIDATED projection, not the raw file —
|
||||
// this is where "invalid rows are excluded from the served rail" is true.
|
||||
@@ -1348,13 +1348,19 @@ const handleRequest = async (req, res) => {
|
||||
|
||||
// ---- init: everything above is declarations -------------------------------
|
||||
// Ordering here is a correctness property, not a style preference. This block
|
||||
// sits after EVERY module binding — verified by a test, not by reading — so
|
||||
// startup cannot land in a temporal dead zone no matter what these statements
|
||||
// grow to touch later. Being last is NOT the same as being after everything you
|
||||
// touch: a boot-time statement once sat in an init block that was "at the
|
||||
// bottom" of the declarations it happened to know about while `handleRequest`
|
||||
// was declared beneath it, and died in that binding's dead zone. The test
|
||||
// asserts both properties — position AND reachability.
|
||||
// sits after EVERY module binding, so startup cannot land in a temporal dead
|
||||
// zone no matter what these statements grow to touch later. Being last is NOT
|
||||
// the same as being after everything you touch: a boot-time statement once sat
|
||||
// in an init block that was "at the bottom" of the declarations it happened to
|
||||
// know about while `handleRequest` was declared beneath it, and died in that
|
||||
// binding's dead zone.
|
||||
//
|
||||
// THE PROPERTY IS JUDGED BY THE EVALUATOR, not by a scan of this text: the
|
||||
// init-eval guard in test/runtime-redeploy.test.mjs spawns this module and
|
||||
// asserts it evaluates, and its controls prove every historical TDZ spelling
|
||||
// refuses to. Four static analyzers each missed the next spelling; the
|
||||
// position discipline in this comment is convention, the guarantee lives with
|
||||
// the judge.
|
||||
//
|
||||
// The weaker arrangement — init partway down, "safe" because the current call
|
||||
// graph happens not to reach the later bindings — is a temporal dead zone
|
||||
|
||||
@@ -77,6 +77,37 @@ export const verbMatches = (declared, pathname) =>
|
||||
export const isRuntimeOwned = (pathname) =>
|
||||
RUNTIME_OWNED_VERBS.some((v) => verbMatches(v, pathname));
|
||||
|
||||
// ---- routing: the ONE place a pathname meets the table ----------------------
|
||||
//
|
||||
// THE DATA BOUNDARY (PM's ratified criterion, round 5): after the table lookup
|
||||
// resolves a row, the raw pathname and the namespace prefix are OUT OF SCOPE
|
||||
// for everything downstream. The runtime's api flow never constructs a URL at
|
||||
// all — it hands this function the raw request URL and receives back either
|
||||
// null (not this namespace) or an `execute` closure. The row branch closes over
|
||||
// a request object carrying ONLY { searchParams, verb, rest }; the miss branch
|
||||
// closes over the pathname internally for its 404 message. So a pathname
|
||||
// special-case cannot be WRITTEN downstream: there is no pathname there to
|
||||
// branch on. That is a fact about the data flow, not a claim a guard checks —
|
||||
// the guards that remain (the literal fence, the unwritability control) police
|
||||
// the residue, which is this function itself: the definition of routing, where
|
||||
// any special case must now be written in plain sight and where the
|
||||
// routes=[] property test sees it.
|
||||
|
||||
export const API_PREFIX = "/api/";
|
||||
|
||||
export function resolveApi(rawUrl, base, routes, respond) {
|
||||
const u = new URL(rawUrl, base);
|
||||
if (!u.pathname.startsWith(API_PREFIX)) return null;
|
||||
const row = routes.find((r) => verbMatches(r.verb, u.pathname));
|
||||
if (!row) {
|
||||
const pathname = u.pathname;
|
||||
return { execute: (req, res) => respond(res, 404, { ok: false,
|
||||
error: `no such contract route: ${pathname} — see GET /api/contract for capabilities and contract/runtime-api.md for the verb set` }) };
|
||||
}
|
||||
const request = { searchParams: u.searchParams, verb: row.verb, rest: u.pathname.slice(row.verb.length) };
|
||||
return { execute: (req, res) => row.handler(request, req, res) };
|
||||
}
|
||||
|
||||
// ---- the routing surface tripwire ------------------------------------------
|
||||
//
|
||||
// PM's ruling, third round on this class: OPT-IN DISCOVERY IS THE HOLE. A
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# The routing data boundary — a load-bearing invariant
|
||||
|
||||
**Status: deliberate, PM-ratified (2026-08-08). Breaking this is a decision,
|
||||
not a refactor.** This is an as-built note for maintainers of this repository.
|
||||
It is not part of the consumer contract; nothing in `contract/` depends on it.
|
||||
|
||||
## The invariant
|
||||
|
||||
**After the route table resolves a row, the raw pathname and the API namespace
|
||||
prefix are out of scope for everything downstream.** In this codebase that is
|
||||
implemented more strongly: the runtime's API flow in `app/serve-studio.mjs`
|
||||
never holds a pathname at all.
|
||||
|
||||
- Routing happens in exactly one place: `resolveApi()` in `app/verbs.mjs`. It
|
||||
receives the raw request URL, constructs the URL object, matches the table,
|
||||
and returns an `execute` closure.
|
||||
- Handlers receive `{ searchParams, verb, rest }` — never a pathname, never a
|
||||
URL object. A handler needing path data uses `rest`, which is already scoped
|
||||
to its own matched verb.
|
||||
- `handleRequest` calls `resolveApi` and executes the result. Neither `u` nor
|
||||
`API_PREFIX` is a binding anywhere in that flow.
|
||||
- The static-file flow's `const u = new URL(...)` is constructed **after** the
|
||||
API branch has returned. That ordering is load-bearing: it is what guarantees
|
||||
no code that can see an API request ever holds a pathname.
|
||||
|
||||
## Why it exists
|
||||
|
||||
Five review rounds of one defect: a route serving without being enumerated or
|
||||
classified (public 404 / internal 200 while `/api/contract` advertised the
|
||||
capability). Each of four fixes defined a *trusted region* — a spelling
|
||||
denylist, a registration wrapper, a fenced file region, a dispatcher body — and
|
||||
the next raw arm was simply written inside the newest region. A trusted region
|
||||
is always one special-case away from wrong.
|
||||
|
||||
The data boundary ends the series because it removes the material, not the
|
||||
place: a pathname special-case downstream of routing **cannot be written** —
|
||||
there is no pathname there to branch on. The claim "a route cannot serve
|
||||
without a row" is a fact about the data flow, not a promise a guard checks.
|
||||
|
||||
## What looks arbitrary but is not
|
||||
|
||||
- **`const u = new URL(...)` sits after the API branch.** Hoisting it to the
|
||||
top of `handleRequest` "for tidiness" re-introduces a pathname binding that
|
||||
is live while API requests are in flight — the exact material of the round-5
|
||||
finding. The unwritability control in `test/reserved-verbs.test.mjs` will
|
||||
fail if you do this.
|
||||
- **Handlers take `request`, not `u`.** Widening the handler signature to pass
|
||||
the URL object hands every handler a pathname again.
|
||||
- **`resolveApi` lives in `app/verbs.mjs`, not beside the table.** The grammar
|
||||
module is the routing definition; a special case must be written into the
|
||||
definition of routing itself, where the `routes=[]` property test and review
|
||||
can see it.
|
||||
|
||||
## The guards that police the residue
|
||||
|
||||
None of these *create* the property — the structure does. They detect attempts
|
||||
to remove it:
|
||||
|
||||
- **Unwritability control** (`test/reserved-verbs.test.mjs`): plants sdk-qa's
|
||||
round-4 literal arm and round-5 derived-prefix arm at the top of the API flow
|
||||
and asserts each detonates as a `ReferenceError` naming the absent binding —
|
||||
and above all, never serves.
|
||||
- **Property tests on `resolveApi`**: with `routes=[]` every API path misses;
|
||||
with one route, exactly its matches resolve; the resolved request carries no
|
||||
pathname field.
|
||||
- **The literal fence** (`routingSurfaceViolations`): every textual `/api`
|
||||
mention in `serve-studio.mjs` lives inside the marked data-only surface.
|
||||
- **Bypass control**: raw arms planted in the static flow demonstrably cannot
|
||||
serve API requests (404), in all the spellings sdk-qa used across rounds.
|
||||
|
||||
## If you need to change this
|
||||
|
||||
If a future feature genuinely needs path data downstream, extend the resolved
|
||||
`request` (e.g., a richer `rest`) inside `resolveApi` — do not reintroduce the
|
||||
pathname. If you believe the boundary itself must move, that is a PM-level
|
||||
design change: say so in the commit and update this note in the same change.
|
||||
@@ -29,7 +29,7 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { spawn } from "node:child_process";
|
||||
import { RUNTIME_OWNED_VERBS, SUBSTITUTABLE_VERBS, openVocabMap, lookup,
|
||||
routingSurfaceViolations, verbMatches, isRuntimeOwned } from "../app/verbs.mjs";
|
||||
routingSurfaceViolations, verbMatches, isRuntimeOwned, resolveApi } from "../app/verbs.mjs";
|
||||
|
||||
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const read = (...p) => fs.readFileSync(path.join(REPO, ...p), "utf8");
|
||||
@@ -209,23 +209,8 @@ test("BYPASS CONTROL: a raw handler that does not go through the table cannot la
|
||||
["comment-cut evasion", ' { const cut = /\\/\\//; if (cut && u.pathname === "/api/qa-raw-evade") return sendJson(res, 200, { ok: true }); }\n',
|
||||
"/api/qa-raw-evade", "qa-raw-evade"],
|
||||
];
|
||||
// sdk-qa's fourth-round plant, kept as a STANDING control per PM: a raw arm at
|
||||
// the MOST ORDINARY place — inside the routing block, after the namespace
|
||||
// gate, where a competent author writes a special case by accident. Under the
|
||||
// previous design this spot was INSIDE the fence and therefore trusted: the
|
||||
// arm served 200, runtime.routes stayed empty, and every committed control
|
||||
// passed. The fence now holds only data, so this spot is outside it and the
|
||||
// literal is named. If this control ever stops firing, a trusted region has
|
||||
// been reintroduced.
|
||||
const routingAnchor = /if \(u\.pathname\.startsWith\(API_PREFIX\)\) \{\n/;
|
||||
assert.match(src, routingAnchor, "the routing block moved — re-anchor sdk-qa's standing plant before trusting it");
|
||||
forms.push(["inside the routing block (sdk-qa round 4)",
|
||||
null, "/api/qa-inside-fence", "qa-inside-fence"]);
|
||||
for (const [form, arm, probe, needle] of forms) {
|
||||
const planted = arm !== null
|
||||
? src.replace(anchor, (m) => m + arm)
|
||||
: src.replace(routingAnchor, (m) =>
|
||||
m + ' if (u.pathname === "/api/qa-inside-fence") { sendJson(res, 200, { ok: true, source: "raw-dispatch-arm" }); return; }\n');
|
||||
const planted = src.replace(anchor, (m) => m + arm);
|
||||
assert.notEqual(planted, src, `${form}: the plant did not apply`);
|
||||
const { statuses, routes } = await bootMutated(planted, [probe]);
|
||||
|
||||
@@ -236,12 +221,105 @@ test("BYPASS CONTROL: a raw handler that does not go through the table cannot la
|
||||
`${form}: a raw handler SERVES (200), is NOT in runtime.routes, and the routing-surface ` +
|
||||
`tripwire does not name it — a live route invisible to classification, the original product failure`);
|
||||
} else {
|
||||
// Since round 5 this is the branch every raw arm lands in: the static
|
||||
// flow's `u` exists only AFTER the api branch has returned, so an arm
|
||||
// planted beside it never sees an /api request at all.
|
||||
assert.equal(statuses[probe], 404,
|
||||
`${form}: a raw handler neither served nor 404'd — some residual path half-handled it`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("UNWRITABILITY CONTROL: sdk-qa's derived-prefix arm at the ordinary place is a ReferenceError", async () => {
|
||||
// PM's ratified criterion, asserted as PM specified: the special case must be
|
||||
// UNWRITABLE downstream — not caught, unwritable. sdk-qa's round-4/5 plants
|
||||
// (the literal arm inside the dispatcher, then the derived-prefix arm built
|
||||
// from the public constant) both go at the most ordinary place: the top of
|
||||
// the api flow, where the namespace is established. Since round 5 neither
|
||||
// `u` nor `API_PREFIX` is a binding there — routing lives in resolveApi and
|
||||
// the api flow holds no pathname — so both plants must fail to EVALUATE,
|
||||
// naming the missing binding. If either ever boots again, a pathname has
|
||||
// been reintroduced into the api flow and the data boundary is gone.
|
||||
const src = read("app", "serve-studio.mjs");
|
||||
const ordinary = /const api = resolveApi\(req\.url[^\n]*\n/;
|
||||
assert.match(src, ordinary, "the api flow entry moved — re-anchor this control before trusting it");
|
||||
|
||||
const plants = [
|
||||
["derived-prefix (no literal, sdk-qa round 5)",
|
||||
' if (u.pathname === API_PREFIX + "qa-derived") { sendJson(res, 200, { ok: true }); return; }\n',
|
||||
/ReferenceError.*\b(u|API_PREFIX)\b/],
|
||||
["literal arm (sdk-qa round 4)",
|
||||
' if (u.pathname === "/api/qa-inside-fence") { sendJson(res, 200, { ok: true }); return; }\n',
|
||||
/ReferenceError.*\bu\b/],
|
||||
];
|
||||
// The detonation is at REQUEST time, not load time: the plant sits inside
|
||||
// handleRequest, where `u` is in its temporal dead zone (declared below, in
|
||||
// the static flow) and API_PREFIX is not a binding at all. The first request
|
||||
// of ANY kind throws the ReferenceError, the async handler's rejection is
|
||||
// unhandled, and the process dies naming the absent binding. What must NEVER
|
||||
// happen is the arm answering 200 — that would mean a pathname is back in
|
||||
// scope in the api flow.
|
||||
for (const [name, arm, expect] of plants) {
|
||||
const planted = src.replace(ordinary, (m) => arm + m);
|
||||
assert.notEqual(planted, src, `${name}: the plant did not apply`);
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "unwritable-"));
|
||||
fs.cpSync(path.join(REPO, "app"), path.join(dir, "app"), { recursive: true });
|
||||
fs.cpSync(path.join(REPO, "fixtures"), path.join(dir, "fixtures"), { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, "app", "serve-studio.mjs"), planted);
|
||||
const port = 9985 + Math.floor(Math.random() * 10);
|
||||
const proc = spawn(process.execPath, [path.join(dir, "app", "serve-studio.mjs"), "--port", String(port)],
|
||||
{ cwd: dir, stdio: ["ignore", "pipe", "pipe"] });
|
||||
let stderr = "";
|
||||
proc.stderr.on("data", (d) => { stderr += d; });
|
||||
let served200 = false;
|
||||
try {
|
||||
for (let i = 0; i < 60; i++) {
|
||||
try {
|
||||
const r = await fetch(`http://127.0.0.1:${port}/api/qa-derived`, { cache: "no-store" });
|
||||
if (r.status === 200) served200 = true;
|
||||
break;
|
||||
} catch { await new Promise((r) => setTimeout(r, 50)); }
|
||||
}
|
||||
await new Promise((r) => { proc.on("exit", r); setTimeout(r, 3000); });
|
||||
} finally { proc.kill(); fs.rmSync(dir, { recursive: true, force: true }); }
|
||||
assert.equal(served200, false,
|
||||
`${name}: the planted arm ANSWERED 200 — a pathname binding is back in scope in the api flow`);
|
||||
assert.match(stderr, expect,
|
||||
`${name}: the plant did not detonate as a ReferenceError naming the absent binding:\n${stderr}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("routing is exactly the table: routes=[] misses everything, routes=[X] resolves exactly X", async () => {
|
||||
// The property test the pure function enables — routing asserted against its
|
||||
// DEFINITION rather than against source text. A special case smuggled into
|
||||
// resolveApi that does not consult the table would let some path resolve
|
||||
// with an EMPTY table, which is the first assertion here.
|
||||
const BASE = "http://127.0.0.1:9999";
|
||||
const sent = [];
|
||||
const respond = (res, code, obj) => sent.push({ code, obj });
|
||||
|
||||
for (const p of ["/api/focus", "/api/contract", "/api/anything/else", "/api/qa-derived"]) {
|
||||
const decision = resolveApi(p, BASE, [], respond);
|
||||
assert.ok(decision, `routes=[]: '${p}' left the api namespace unclaimed`);
|
||||
decision.execute(null, null);
|
||||
assert.equal(sent.at(-1).code, 404, `routes=[]: '${p}' resolved despite an empty table`);
|
||||
}
|
||||
assert.equal(resolveApi("/not-api", BASE, [], respond), null, "a non-api path was claimed");
|
||||
|
||||
let got = null;
|
||||
const X = [{ verb: "/api/export-status/", handler: (request) => { got = request; } }];
|
||||
const hit = resolveApi("/api/export-status/42?a=b", BASE, X, respond);
|
||||
await hit.execute(null, null);
|
||||
assert.equal(got.verb, "/api/export-status/");
|
||||
assert.equal(got.rest, "42", "the prefix remainder is not handed to the handler as rest");
|
||||
assert.equal(got.searchParams.get("a"), "b");
|
||||
assert.equal(got.pathname, undefined,
|
||||
"the resolved request carries a pathname — the data boundary leaks the thing it exists to remove");
|
||||
const miss = resolveApi("/api/export-status", BASE, X, respond);
|
||||
miss.execute(null, null);
|
||||
assert.equal(sent.at(-1).code, 404, "an exact path matched a prefix verb without the trailing slash");
|
||||
});
|
||||
|
||||
test("the runtime source keeps every /api mention inside the routing surface", () => {
|
||||
// The standing half of the bypass control: the tripwire that makes a raw arm
|
||||
// unable to LAND in the repo. The control above proves this fires on a live
|
||||
|
||||
+64
-209
@@ -480,220 +480,75 @@ test("the SSE change-signal still fires on a manifest change", async (t) => {
|
||||
// fourth, the comparison became structural and descends everywhere. Its controls
|
||||
// plant a field at all four levels and assert the path each is reported at.
|
||||
|
||||
// The startup-ordering analysis, extracted so a control can run it against a
|
||||
// PLANTED violation. The previous guard asserted POSITION only — "startup after
|
||||
// every module binding" — and it invited exactly the fix that reproduces the
|
||||
// defect it exists to prevent: startup work was moved "to the bottom" of the
|
||||
// init block, below every binding the mover knew about, and directly into the
|
||||
// temporal dead zone of a `const` declared beneath it. Being last is not the
|
||||
// same as being after everything you TOUCH. So this checks two properties:
|
||||
// THE INIT-TDZ GUARD IS THE EVALUATOR — the fifth version of this guard, and
|
||||
// the first that is not a model. Four static versions each classified source
|
||||
// TEXT (a name whitelist, a position rule, a reachability scan, an
|
||||
// initializer grammar) and each missed the next ordinary spelling: the
|
||||
// misplaced walk, the direct initializer, the IIFE initializer, the standalone
|
||||
// IIFE statement. PM's ruling ended the series: do not MODEL the property, RUN
|
||||
// it. "Does this evaluate at module init" is asked of the code by the only
|
||||
// judge with no grammar to be wrong about — Node evaluates the module, and
|
||||
// either it evaluates or it exits naming the binding and the line. Coverage of
|
||||
// spellings nobody has planted yet is the evaluator's, by construction.
|
||||
//
|
||||
// REACHABILITY — no module-level executable statement references a `let`/
|
||||
// `const` binding declared later. This is the property whose violation IS the
|
||||
// boot-time ReferenceError, checked against what each statement actually
|
||||
// touches rather than against line order.
|
||||
//
|
||||
// POSITION — every executable statement still follows every binding. This is
|
||||
// deliberately broader than reachability: it also covers what the functions
|
||||
// startup CALLS may grow to touch later, which no textual reference scan of
|
||||
// the call site can see.
|
||||
//
|
||||
// It also recognises EVERY module-level executable statement — bare calls,
|
||||
// top-level awaits, method chains — where the old guard matched two whitelisted
|
||||
// call names and never saw the statement that carried the defect. A checker that
|
||||
// cannot see the statement is a checker nothing runs, for that statement.
|
||||
// Blank out string contents so braces and identifier-lookalikes inside them
|
||||
// neither break depth tracking nor register as references.
|
||||
function stripStrings(text) {
|
||||
return text
|
||||
.replace(/`(?:[^`\\]|\\[\s\S])*`/g, "``")
|
||||
.replace(/"(?:[^"\\]|\\.)*"/g, '""')
|
||||
.replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
||||
// What this deliberately gives up, stated rather than hidden: a static scan
|
||||
// could in principle fail EARLY on a merely-fragile arrangement (init not at
|
||||
// the bottom) before any TDZ is real. In practice every static version passed
|
||||
// on real TDZs and the "early" failures it produced were the wrong fixes. The
|
||||
// position discipline stays as the init-block source comment; the property
|
||||
// lives here, with its judge.
|
||||
|
||||
async function evaluatesAtInit(mutate) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "init-eval-"));
|
||||
fs.cpSync(path.join(REPO, "app"), path.join(dir, "app"), { recursive: true });
|
||||
fs.cpSync(path.join(REPO, "fixtures"), path.join(dir, "fixtures"), { recursive: true });
|
||||
if (mutate) {
|
||||
const src = fs.readFileSync(path.join(dir, "app", "serve-studio.mjs"), "utf8");
|
||||
const mutated = mutate(src);
|
||||
if (mutated === src) throw new Error("the mutation did not apply — the result would be meaningless");
|
||||
fs.writeFileSync(path.join(dir, "app", "serve-studio.mjs"), mutated);
|
||||
}
|
||||
const port = 9880 + Math.floor(Math.random() * 40);
|
||||
const proc = spawn(process.execPath, [path.join(dir, "app", "serve-studio.mjs"), "--port", String(port)],
|
||||
{ cwd: dir, stdio: ["ignore", "pipe", "pipe"] });
|
||||
let stderr = "";
|
||||
proc.stderr.on("data", (d) => { stderr += d; });
|
||||
try {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
if (proc.exitCode !== null) return { evaluated: false, code: proc.exitCode, stderr };
|
||||
try {
|
||||
await fetch(`http://127.0.0.1:${port}/api/contract`, { cache: "no-store" });
|
||||
return { evaluated: true, code: null, stderr };
|
||||
} catch { await new Promise((r) => setTimeout(r, 50)); }
|
||||
}
|
||||
return { evaluated: false, code: proc.exitCode, stderr: stderr + "\n(timed out: neither served nor exited)" };
|
||||
} finally { proc.kill(); fs.rmSync(dir, { recursive: true, force: true }); }
|
||||
}
|
||||
|
||||
// Remove `=> { … }` and `function … { … }` bodies — UNLESS the function is
|
||||
// immediately invoked. The property is "does this code evaluate at module
|
||||
// load", not "does this text look like a function": a deferred closure's body
|
||||
// belongs to call time and is dropped, but an IIFE's body RUNS DURING
|
||||
// INITIALIZER EVALUATION and must stay in for scanning. The previous version
|
||||
// stripped every body without asking, which is how
|
||||
// `const x = (() => later)();` crashed boot while the guard passed. The
|
||||
// invocation paren may sit after a wrapping close-paren: `(() => {…})()`.
|
||||
// Expects string-stripped input so brace counting cannot be derailed.
|
||||
function stripFunctionBodies(text) {
|
||||
let out = "", i = 0;
|
||||
for (;;) {
|
||||
const m = /(?:=>\s*\{|function\b[^{]*\{)/.exec(text.slice(i));
|
||||
if (!m) { out += text.slice(i); break; }
|
||||
let depth = 1, j = i + m.index + m[0].length;
|
||||
while (j < text.length && depth > 0) {
|
||||
if (text[j] === "{") depth++;
|
||||
else if (text[j] === "}") depth--;
|
||||
j++;
|
||||
}
|
||||
const invokedNow = /^\s*\)?\s*\(/.test(text.slice(j));
|
||||
if (invokedNow) out += text.slice(i, j);
|
||||
else out += text.slice(i, i + m.index) + m[0] + "}";
|
||||
i = j;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function startupOrderingViolations(src) {
|
||||
const lines = src.split("\n");
|
||||
|
||||
// Module-level bindings (column 0), including simple destructuring.
|
||||
const bindings = []; // { name, line }
|
||||
lines.forEach((line, i) => {
|
||||
const m = line.match(/^(?:const|let)\s+(?:\{([^}]*)\}|\[([^\]]*)\]|([A-Za-z_$][\w$]*))/);
|
||||
if (!m) return;
|
||||
const names = m[3] ? [m[3]]
|
||||
: (m[1] || m[2]).split(",").map((s) => s.split(/[:=]/)[0].trim()).filter(Boolean);
|
||||
for (const name of names) bindings.push({ name, line: i + 1 });
|
||||
});
|
||||
|
||||
// Consume a multi-line statement starting at line i: cumulative ()[]{} depth
|
||||
// over string-stripped lines, ending on the line where depth returns to zero
|
||||
// and the line closes with `;`.
|
||||
const statementEnd = (i) => {
|
||||
let depth = 0;
|
||||
for (let j = i; j < lines.length; j++) {
|
||||
const s = stripStrings(lines[j]);
|
||||
for (const ch of s) {
|
||||
if ("([{".includes(ch)) depth++;
|
||||
else if (")]}".includes(ch)) depth--;
|
||||
}
|
||||
if (depth <= 0 && /;\s*$/.test(s)) return j;
|
||||
}
|
||||
return lines.length - 1;
|
||||
};
|
||||
|
||||
// What EXECUTES at module load, in both spellings:
|
||||
// - executable statements (calls, awaits, chains), and
|
||||
// - const/let INITIALIZERS — an initializer runs at load exactly like a
|
||||
// statement, unless the whole initializer is a function expression, whose
|
||||
// body waits for call time. Missing the second spelling is how
|
||||
// `const probe = handleRequest;` crashed the runtime while this guard
|
||||
// passed: it enforced reachability over the wrong SET of statements.
|
||||
const executables = []; // { line, text, kind: "statement" }
|
||||
const initializers = []; // { line, text, kind: "initializer" }
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const decl = line.match(/^(?:const|let)\s+[^=]*=\s*(.*)$/);
|
||||
if (decl) {
|
||||
const end = statementEnd(i);
|
||||
const raw = lines.slice(i, end + 1).join("\n");
|
||||
const init = raw.slice(raw.indexOf("=") + 1).trim();
|
||||
const stripped = stripStrings(init);
|
||||
// Exempt only what PROVABLY defers: a BARE function expression. A bare
|
||||
// arrow cannot be immediately invoked without first being parenthesized
|
||||
// (which changes the leading shape), and a bare `function` expression is
|
||||
// deferred only when the statement ends at its closing brace — ending in
|
||||
// `)` means it was called. Anything else — including an IIFE, which
|
||||
// textually BEGINS like a function expression and runs at load — is
|
||||
// scanned. The previous exemption keyed on how the text STARTED, which is
|
||||
// a spelling, not the property.
|
||||
const bareArrow = /^(?:async\s+)?(?:\([^()]*\)|[A-Za-z_$][\w$]*)\s*=>/.test(init);
|
||||
const bareFunction = /^(?:async\s+)?function\b/.test(init) && /\}\s*;?\s*$/.test(stripped);
|
||||
if (!bareArrow && !bareFunction) {
|
||||
initializers.push({ line: i + 1, text: stripFunctionBodies(stripped) });
|
||||
}
|
||||
i = end;
|
||||
continue;
|
||||
}
|
||||
if (!/^(?:await\s+|[A-Za-z_$][\w$]*\s*[.(])/.test(line)) continue;
|
||||
if (/^(?:import|export|const|let|var|function|class|async\s+function)\b/.test(line)) continue;
|
||||
let text = line;
|
||||
let j = i;
|
||||
while (j < lines.length - 1 && !/;\s*$/.test(lines[j]) ) { j++; text += "\n" + lines[j]; }
|
||||
executables.push({ line: i + 1, text });
|
||||
i = j;
|
||||
}
|
||||
|
||||
const violations = [];
|
||||
for (const ex of [...executables, ...initializers]) {
|
||||
const what = ex === undefined ? "" : (executables.includes(ex) ? "statement" : "initializer");
|
||||
const referenced = new Set(ex.text.match(/[A-Za-z_$][\w$]*/g) || []);
|
||||
for (const b of bindings) {
|
||||
if (b.line > ex.line && referenced.has(b.name)) {
|
||||
violations.push({
|
||||
kind: "reachability", statement: ex.line, binding: b.name, declared: b.line,
|
||||
message:
|
||||
`the ${what} at line ${ex.line} references '${b.name}', declared at line ${b.line} — ` +
|
||||
`an ${what === "initializer" ? "initializer executes at module load exactly like a statement, so this" : "executable statement"} ` +
|
||||
`is a ReferenceError in the temporal dead zone the moment the module loads. ` +
|
||||
`Being last is not the same as being after everything you TOUCH: load-time code must follow ` +
|
||||
`the bindings it references, so move it below line ${b.line}, not the binding above.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lastBinding = Math.max(0, ...bindings.map((b) => b.line));
|
||||
for (const ex of executables) {
|
||||
if (ex.line < lastBinding) {
|
||||
violations.push({
|
||||
kind: "position", statement: ex.line, declared: lastBinding,
|
||||
message:
|
||||
`the statement at line ${ex.line} runs before the last module binding (line ${lastBinding}). ` +
|
||||
`Even if it touches nothing declared later TODAY, the functions it calls can grow a ` +
|
||||
`dependency on a later binding and land in its dead zone — position is the property that ` +
|
||||
`covers what your callees may come to touch. Put startup work in the init block at the ` +
|
||||
`bottom, after every declaration AND after everything it touches.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
return { bindings, executables, violations };
|
||||
}
|
||||
|
||||
test("startup follows every binding it touches AND every binding, so the TDZ class cannot return", () => {
|
||||
// This asserts the STRUCTURE the source comment claims, rather than trusting
|
||||
// the comment. Reachability is checked as well as position because the two
|
||||
// properties fail differently: position says "not at the bottom", reachability
|
||||
// names the exact binding whose dead zone the statement would die in — which
|
||||
// is the message that would have prevented the fifth TDZ from this file.
|
||||
const src = fs.readFileSync(path.join(REPO, "app", "serve-studio.mjs"), "utf8");
|
||||
const { bindings, executables, violations } = startupOrderingViolations(src);
|
||||
|
||||
assert.ok(bindings.length >= 10, `positive control: found only ${bindings.length} module bindings`);
|
||||
assert.ok(executables.length >= 4,
|
||||
`positive control: found only ${executables.length} module-level executable statements — ` +
|
||||
`expected at least watchForChanges, loadManifest, loadAnnotations, the route walk, and .listen`);
|
||||
assert.deepEqual(violations.map((v) => v.message), [],
|
||||
"startup ordering violations in app/serve-studio.mjs");
|
||||
test("the module EVALUATES at init — the TDZ property judged by the evaluator, not a model of it", async () => {
|
||||
const r = await evaluatesAtInit(null);
|
||||
assert.equal(r.evaluated, true,
|
||||
`the runtime failed to evaluate at init:\n${r.stderr}`);
|
||||
});
|
||||
|
||||
test("positive control: the ordering checker fires on BOTH load-time spellings of the dead zone", () => {
|
||||
// The two defects this guard exists for, planted verbatim: the historical
|
||||
// top-level statement referencing a const declared beneath it, and sdk-qa's
|
||||
// `const probe = handleRequest;` — an INITIALIZER, which executes at module
|
||||
// load exactly like a statement and which the previous scan excluded
|
||||
// wholesale. A checker that has never been seen to fire is not a control.
|
||||
const planted = [
|
||||
"const early = 1;",
|
||||
"doStartup();",
|
||||
"await handleThing(",
|
||||
" { url: x },",
|
||||
").catch(() => {});",
|
||||
"const qaInitializerProbe = handleThing;",
|
||||
"const qaInitializerIife = (() => handleThing)();",
|
||||
"const qaBraceIife = (function () { return handleThing; })();",
|
||||
"const notALoad = () => handleThing;",
|
||||
"const notALoadBrace = () => { return handleThing; };",
|
||||
"const handleThing = async () => {};",
|
||||
"listenSomething();",
|
||||
].join("\n");
|
||||
const { violations } = startupOrderingViolations(planted);
|
||||
const reach = violations.filter((v) => v.kind === "reachability");
|
||||
assert.deepEqual(reach.map((v) => [v.statement, v.binding]).sort((a, b) => a[0] - b[0]),
|
||||
[[3, "handleThing"], [6, "handleThing"], [7, "handleThing"], [8, "handleThing"]],
|
||||
`expected the STATEMENT plant, the direct INITIALIZER, and BOTH IIFE spellings — an IIFE is not ` +
|
||||
`a deferred closure, its body runs during initializer evaluation: ${JSON.stringify(reach)}`);
|
||||
assert.ok(!reach.some((v) => v.statement === 9 || v.statement === 10),
|
||||
"a bare function-expression initializer was flagged — its body runs at call time, not load time, " +
|
||||
"and over-firing there would force every helper above the binding it eventually calls");
|
||||
assert.ok(violations.some((v) => v.kind === "position"),
|
||||
"the position property must also fire on statements before the last binding");
|
||||
test("controls: every historical TDZ spelling refuses to evaluate, named by the evaluator", async () => {
|
||||
// The four mutants sdk-qa (and round 2) actually used, one per round. Each
|
||||
// must exit nonzero with the ReferenceError naming the binding. Any FUTURE
|
||||
// spelling is covered by the same mechanism these prove alive: the check is
|
||||
// not a model of evaluation, it is evaluation.
|
||||
const DECL = "const handleRequest = async (req, res) => {";
|
||||
const MUTANTS = [
|
||||
["round-2: top-level statement", `await handleRequest;\n${DECL}`],
|
||||
["round-4: direct initializer", `const qaInitializerProbe = handleRequest;\n${DECL}`],
|
||||
["round-4: IIFE initializer", `const qaInitializerIife = (() => handleRequest)();\n${DECL}`],
|
||||
["round-5: standalone IIFE statement", `(() => handleRequest)();\n${DECL}`],
|
||||
];
|
||||
for (const [name, planted] of MUTANTS) {
|
||||
const r = await evaluatesAtInit((src) => src.replace(DECL, planted));
|
||||
assert.equal(r.evaluated, false, `${name}: the mutant EVALUATED — the guard is not guarding`);
|
||||
assert.match(r.stderr, /ReferenceError[\s\S]*handleRequest/,
|
||||
`${name}: refused to evaluate but not with the ReferenceError naming the binding:\n${r.stderr}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("positive control: this suite is capable of failing", () => {
|
||||
|
||||
Reference in new issue
Block a user