Skip to content

Commit 4d3dca4

Browse files
fix(bun-esm): clear the two remaining masked build failures at source
Both reds in the cascade had root causes, not flakes: 1. tools/run_codegen_bun_tests.sh — "legacy runtime reference emitted in host_profile.bun.js". The gate greps the --bun-esm artefact for 'deno' (case-insensitive) and the only hit is a comment in common_prelude, which is emitted for BOTH host profiles: "otherwise globalThis so a Deno/Node harness can install document/window mocks". That is Deno-era prose inside a Bun-targeted artefact; reword it runtime-neutrally. The gate also now prints the offending lines instead of only the verdict, and the runner is fail-late (collects every check, reports a roll-call) so one red check cannot hide the others. 2. tests/codegen-deno corpus — "EACCES: permission denied, scandir '/root'". The Deno-scripting harnesses mocked a globalThis.Deno object with an in-memory FS, but this corpus is compiled with --bun-esm: the Bun prelude resolves node:fs lazily through process.getBuiltinModule() and reads process.argv / process.exit, so the Deno stub was inert and walkRecursive walked the runner's real /root. The harnesses now stub the seam the emission actually uses (getBuiltinModule + argv + exit). Verified locally by running both harnesses against modules that replicate the emitted call patterns; the corpus runner is fail-late too. Local mimic modules used for that verification are gitignored build artefacts and are not part of this commit. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
1 parent eb2ce17 commit 4d3dca4

5 files changed

Lines changed: 197 additions & 65 deletions

File tree

‎lib/codegen_deno.ml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -733,7 +733,7 @@ const __as_dbGroupBy = (h, sql, params) => String(globalThis.__as_sqlite.gro
733733
const __as_dbGroupCount = (h, table, keyCol) => String(globalThis.__as_sqlite.groupCount(h, table, keyCol));
734734
// ---- Dom (#56 PR 4): Window / Document / utility ----
735735
// Host is `globalThis.window` when present (browser, jsdom, idaptik
736-
// WebView); otherwise `globalThis` so a Deno/Node harness can install
736+
// WebView); otherwise `globalThis` so a Node/Bun harness can install
737737
// document/window mocks on the global. No consumer-side init required
738738
// beyond providing those web-platform objects.
739739
const __as_domWin = () =>

‎tests/codegen-deno/deno_scripting.harness.mjs‎

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
// SPDX-License-Identifier: MPL-2.0
2-
// issue #122 follow-up — Node-ESM harness for new Deno-scripting externs.
2+
// issue #122 follow-up — Node-ESM harness for the Deno-era scripting externs,
3+
// now exercised through the Bun-ESM backend.
34
//
4-
// Stubs only what the new surface needs: a tiny in-memory FS for
5-
// `Deno.readDirSync` (so `walkRecursive` traverses it deterministically),
6-
// `Deno.args` / `Deno.exit`, and captures `console.error`.
5+
// Stubs only what the surface needs: a tiny in-memory FS (so `walkRecursive`
6+
// traverses it deterministically), `args`, `exit`, and captures
7+
// `console.error`.
8+
//
9+
// IMPORTANT: this corpus is compiled with `--bun-esm`, and the emitted Bun
10+
// prelude resolves `node:fs` lazily via `process.getBuiltinModule()` and reads
11+
// `process.argv` / `process.exit`. Stubbing a `globalThis.Deno` object is
12+
// inert under that backend, so the host is stubbed at the seam the emission
13+
// actually uses.
714

815
import assert from "node:assert/strict";
916

1017
// ── In-memory FS stub for walkRecursive ─────────────────────────────
11-
// Shape: { "/root": ["a", "b/", ".hidden"], "/root/b": ["c"] }
18+
// Shape: { "/root": ["a.txt", "sub/"], "/root/sub": ["b.txt", "deeper/"],
19+
// "/root/sub/deeper": ["c.txt"], "/empty": [] }
1220
const fs = {
1321
"/root": [{ name: "a.txt", isFile: true, isDirectory: false },
1422
{ name: "sub", isFile: false, isDirectory: true }],
@@ -18,17 +26,46 @@ const fs = {
1826
"/empty": [],
1927
};
2028

21-
globalThis.Deno = globalThis.Deno || {};
22-
globalThis.Deno.readDirSync = (path) => {
23-
const entries = fs[path];
24-
if (!entries) throw new Error(`stub: no such dir ${path}`);
25-
return entries;
29+
const fsStub = {
30+
readdirSync: (path) => {
31+
const entries = fs[path];
32+
if (!entries) {
33+
const err = new Error(`stub: no such dir ${path}`);
34+
err.code = "ENOENT";
35+
throw err;
36+
}
37+
return entries.map((e) => ({
38+
name: e.name,
39+
isFile: () => e.isFile,
40+
isDirectory: () => e.isDirectory,
41+
}));
42+
},
43+
mkdirSync: () => {},
44+
writeFileSync: () => {},
45+
rmSync: () => {},
46+
readFileSync: () => new Uint8Array(),
47+
statSync: (path) => {
48+
const err = new Error(`stub: no such path ${path}`);
49+
err.code = "ENOENT";
50+
throw err;
51+
},
52+
};
53+
54+
const realGetBuiltinModule = process.getBuiltinModule?.bind(process);
55+
process.getBuiltinModule = (name) => {
56+
if (name === "node:fs") return fsStub;
57+
if (!realGetBuiltinModule) {
58+
throw new Error(`host builtin unavailable: ${name}`);
59+
}
60+
return realGetBuiltinModule(name);
2661
};
2762

2863
// args / exit stubs — exit captures the code instead of terminating.
64+
// `args` lowers to process.argv.slice(2), so the leading entries are dropped.
65+
const realArgv = process.argv;
66+
process.argv = ["node", "deno_scripting.harness.mjs", "alpha", "beta", "gamma"];
2967
let lastExit = null;
30-
globalThis.Deno.args = ["alpha", "beta", "gamma"];
31-
globalThis.Deno.exit = (code) => { lastExit = code; return code; };
68+
process.exit = (code) => { lastExit = code; };
3269

3370
// Capture stderr writes from consoleError.
3471
const stderrLog = [];
@@ -71,4 +108,5 @@ exit_with(2);
71108
assert.equal(lastExit, 2, "Deno.exit lowered correctly");
72109

73110
console.error = origError;
111+
process.argv = realArgv;
74112
console.log("deno_scripting.harness.mjs OK");

‎tests/codegen-deno/deno_scripting_part2.harness.mjs‎

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
// SPDX-License-Identifier: MPL-2.0
22
// campaign #239 STEP 3 part 2 — Node-ESM harness for the second wave
3-
// of Deno-scripting externs (stat predicates, byte accessors, module URL)
4-
// plus the `let _ = X` wildcard-binding codegen fix.
3+
// of Deno-era scripting externs (stat predicates, byte accessors, module URL)
4+
// plus the `let _ = X` wildcard-binding codegen fix, exercised through the
5+
// Bun-ESM backend.
6+
//
7+
// The Bun prelude reaches the filesystem through process.getBuiltinModule(),
8+
// so the in-memory FS is installed at that seam — a `globalThis.Deno` stub
9+
// would never be consulted.
510

611
import assert from "node:assert/strict";
712

@@ -12,24 +17,38 @@ const files = {
1217
"/empty.bin": { kind: "file", bytes: new Uint8Array() },
1318
};
1419

15-
globalThis.Deno = globalThis.Deno || {};
16-
globalThis.Deno.statSync = (path) => {
17-
const e = files[path];
18-
if (!e) {
19-
const err = new Error(`stub: no such path ${path}`);
20-
err.code = "ENOENT";
21-
throw err;
22-
}
23-
return {
24-
size: e.kind === "file" ? e.bytes.length : 0,
25-
isFile: e.kind === "file",
26-
isDirectory: e.kind === "dir",
27-
};
20+
const fsStub = {
21+
statSync: (path) => {
22+
const e = files[path];
23+
if (!e) {
24+
const err = new Error(`stub: no such path ${path}`);
25+
err.code = "ENOENT";
26+
throw err;
27+
}
28+
return {
29+
size: e.kind === "file" ? e.bytes.length : 0,
30+
isFile: () => e.kind === "file",
31+
isDirectory: () => e.kind === "dir",
32+
};
33+
},
34+
readFileSync: (path) => {
35+
const e = files[path];
36+
if (!e || e.kind !== "file") {
37+
const err = new Error(`stub: not a file ${path}`);
38+
err.code = "ENOENT";
39+
throw err;
40+
}
41+
return e.bytes;
42+
},
2843
};
29-
globalThis.Deno.readFileSync = (path) => {
30-
const e = files[path];
31-
if (!e || e.kind !== "file") throw new Error(`stub: not a file ${path}`);
32-
return e.bytes;
44+
45+
const realGetBuiltinModule = process.getBuiltinModule?.bind(process);
46+
process.getBuiltinModule = (name) => {
47+
if (name === "node:fs") return fsStub;
48+
if (!realGetBuiltinModule) {
49+
throw new Error(`host builtin unavailable: ${name}`);
50+
}
51+
return realGetBuiltinModule(name);
3352
};
3453

3554
const {

‎tools/run_codegen_bun_tests.sh‎

Lines changed: 69 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
#!/usr/bin/env bash
22
# SPDX-License-Identifier: MPL-2.0
33
# Issue #734 — native Bun-ESM backend acceptance runner.
4-
set -euo pipefail
4+
#
5+
# Fail-late by design: every check runs and is reported, so one red check
6+
# cannot hide the state of the checks behind it. Failures are echoed as
7+
# `::error::` lines so they survive into CI annotations.
8+
set -uo pipefail
59

610
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
711
TEST_DIR="$ROOT_DIR/tests/codegen-bun"
@@ -14,48 +18,88 @@ else
1418
COMPILE_CMD=(dune exec affinescript -- compile)
1519
fi
1620

17-
command -v bun >/dev/null 2>&1 || {
18-
echo "error: Bun is required for the Bun-ESM acceptance tests" >&2
21+
if ! command -v bun >/dev/null 2>&1; then
22+
echo "::error::Bun is required for the Bun-ESM acceptance tests" >&2
1923
exit 1
24+
fi
25+
26+
failures=()
27+
fail() {
28+
failures+=("$1")
29+
echo "::error::$1" >&2
2030
}
2131

2232
src="$TEST_DIR/host_profile.affine"
2333
out="${src%.affine}.bun.js"
24-
"${COMPILE_CMD[@]}" "$src" -o "$out" --bun-esm
25-
if grep -qi 'deno' "$out"; then
26-
echo "error: legacy runtime reference emitted in $(basename "$out")" >&2
27-
exit 1
34+
35+
# 1. Compile, then scan the artefact for legacy runtime references.
36+
compiled=1
37+
if ! "${COMPILE_CMD[@]}" "$src" -o "$out" --bun-esm; then
38+
compiled=0
39+
fail "compile failed for $(basename "$src")"
40+
elif grep -qi 'deno' "$out"; then
41+
fail "legacy runtime reference emitted in $(basename "$out")"
42+
grep -in 'deno' "$out" | head -4 | sed 's/^/ /' >&2
43+
fi
44+
45+
if [ "$compiled" = 1 ]; then
46+
# 2. The emitted module must be syntactically valid Bun/JS.
47+
if ! bun --check "$out"; then
48+
fail "bun --check rejected $(basename "$out")"
49+
fi
50+
51+
# 3. Emission must be byte-for-byte reproducible.
52+
second="$TEST_DIR/reproducibility.bun.js"
53+
if "${COMPILE_CMD[@]}" "$src" -o "$second" --bun-esm; then
54+
if ! cmp "$out" "$second"; then
55+
fail "Bun-ESM emission is not reproducible ($(basename "$out") vs $(basename "$second"))"
56+
fi
57+
else
58+
fail "reproducibility recompile failed for $(basename "$src")"
59+
fi
2860
fi
29-
bun --check "$out"
30-
second="$TEST_DIR/reproducibility.bun.js"
31-
"${COMPILE_CMD[@]}" "$src" -o "$second" --bun-esm
32-
cmp "$out" "$second"
3361

62+
# 4. The retired --deno-esm flag must be rejected with its explanation.
3463
removed_log="$TEST_DIR/deno-removed.log"
35-
if "${COMPILE_CMD[@]}" "$src" -o "$out" --deno-esm \
36-
>"$removed_log" 2>&1; then
37-
echo "error: retired --deno-esm compiled successfully" >&2
38-
exit 1
64+
if "${COMPILE_CMD[@]}" "$src" -o "$out" --deno-esm >"$removed_log" 2>&1; then
65+
fail "retired --deno-esm compiled successfully"
66+
elif ! grep -q 'Deno-ESM was removed' "$removed_log"; then
67+
fail "retired --deno-esm rejection lacks 'Deno-ESM was removed'"
3968
fi
40-
grep -q 'Deno-ESM was removed' "$removed_log"
4169

70+
# 5. The retired .deno.js output extension must be rejected as E0826.
4271
removed_json="$TEST_DIR/deno-removed.json"
43-
if "${COMPILE_CMD[@]}" "$src" -o "${out%.bun.js}.deno.js" --json \
44-
>"$removed_json" 2>&1; then
45-
echo "error: retired .deno.js output compiled successfully" >&2
46-
exit 1
72+
if "${COMPILE_CMD[@]}" "$src" -o "${out%.bun.js}.deno.js" --json >"$removed_json" 2>&1; then
73+
fail "retired .deno.js output compiled successfully"
74+
else
75+
grep -q '"code":"E0826"' "$removed_json" ||
76+
fail "retired .deno.js rejection lacks error code E0826"
77+
grep -q '"success":false' "$removed_json" ||
78+
fail "retired .deno.js rejection lacks success:false"
4779
fi
48-
grep -q '"code":"E0826"' "$removed_json"
49-
grep -q '"success":false' "$removed_json"
5080

81+
# 6. Native Bun harnesses.
82+
harness_total=0
5183
for js in "$TEST_DIR"/*.harness.mjs; do
52-
(cd "$TEST_DIR" && AFFINESCRIPT_BUN_PROBE=estate bun "$(basename "$js")" alpha beta)
84+
name="$(basename "$js")"
85+
harness_total=$((harness_total + 1))
86+
if ! (cd "$TEST_DIR" && AFFINESCRIPT_BUN_PROBE=estate bun "$name" alpha beta); then
87+
fail "bun harness failed: $name"
88+
fi
5389
done
5490

91+
# 7. Unsupported host operations must not compile.
5592
if "${COMPILE_CMD[@]}" "$TEST_DIR/unsupported_host.affine" \
5693
-o "$TEST_DIR/unsupported_host.bun.js" --bun-esm; then
57-
echo "error: unsupported Bun host operation compiled successfully" >&2
94+
fail "unsupported Bun host operation compiled successfully"
95+
fi
96+
97+
if [ "${#failures[@]}" -gt 0 ]; then
98+
echo ""
99+
echo "✗ ${#failures[@]} native Bun-ESM check(s) failed:"
100+
printf ' - %s\n' "${failures[@]}"
58101
exit 1
59102
fi
60103

61-
echo "All native Bun-ESM tests passed."
104+
echo ""
105+
echo "All native Bun-ESM tests passed ($harness_total harnesses)."

‎tools/run_codegen_deno_tests.sh‎

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@
44
#
55
# Directory name `tests/codegen-deno/` is historical. Every fixture is
66
# compiled with `--bun-esm` to FILE.bun.js. Harnesses still run under
7-
# `node` in CI (Node 20 is provisioned; these fixtures do not need the
8-
# Bun binary — they mock host objects). Native Bun acceptance lives in
9-
# tools/run_codegen_bun_tests.sh.
10-
set -euo pipefail
7+
# `node` in CI (Node 20 is provisioned; fixtures that need a host stub must
8+
# stub the *Bun* host surface — process.getBuiltinModule / process.argv /
9+
# process.exit — because that is what the emitted prelude actually reads;
10+
# a `globalThis.Deno` stub is inert under this backend).
11+
#
12+
# Deliberately NOT `set -e`: failures are collected so one broken harness
13+
# cannot hide every harness after it (the same masking bug that hid 33
14+
# tests/codegen harnesses; see tools/run_codegen_wasm_tests.sh).
15+
set -uo pipefail
1116

1217
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
1318
TEST_DIR="$ROOT_DIR/tests/codegen-deno"
@@ -22,17 +27,43 @@ fi
2227

2328
echo "Using compiler: ${COMPILE_CMD[*]}"
2429

30+
compile_failures=()
2531
for src in "$TEST_DIR"/*.affine; do
2632
out="${src%.affine}.bun.js"
2733
echo "Compiling $(basename "$src") -> $(basename "$out")"
28-
"${COMPILE_CMD[@]}" "$src" -o "$out" --bun-esm
34+
if ! "${COMPILE_CMD[@]}" "$src" -o "$out" --bun-esm; then
35+
compile_failures+=("$(basename "$src")")
36+
fi
2937
done
3038

39+
if [ "${#compile_failures[@]}" -gt 0 ]; then
40+
echo ""
41+
echo "::error::${#compile_failures[@]} Bun-ESM fixture(s) failed to compile"
42+
printf ' - %s\n' "${compile_failures[@]}"
43+
exit 1
44+
fi
45+
3146
echo ""
3247
echo "Running ESM harnesses (node, Bun-ESM artefacts)"
48+
harness_total=0
49+
harness_failures=()
3350
for js in "$TEST_DIR"/*.harness.mjs; do
34-
echo "node $(basename "$js")"
35-
(cd "$TEST_DIR" && node "$(basename "$js")")
51+
name="$(basename "$js")"
52+
harness_total=$((harness_total + 1))
53+
echo "node $name"
54+
if ! (cd "$TEST_DIR" && node "$name"); then
55+
echo "::error file=tests/codegen-deno/$name::ESM harness failed"
56+
harness_failures+=("$name")
57+
fi
3658
done
3759

38-
echo "All codegen Bun-ESM (legacy codegen-deno corpus) tests passed."
60+
if [ "${#harness_failures[@]}" -gt 0 ]; then
61+
echo ""
62+
echo "::error::${#harness_failures[@]} of $harness_total Bun-ESM harness(es) failed"
63+
echo "Failed harnesses (full output above):"
64+
printf ' - %s\n' "${harness_failures[@]}"
65+
exit 1
66+
fi
67+
68+
echo ""
69+
echo "All codegen Bun-ESM (legacy codegen-deno corpus) tests passed ($harness_total harnesses)."

0 commit comments

Comments
 (0)