mod support;
use std::collections::HashMap;
use std::time::Duration;
use serde_json::Value;
use support::{
assert_node_available, authenticate_wire, collect_process_output_wire_with_timeout,
create_vm_wire_with_metadata, dispose_vm_and_close_session_wire, execute_wire, new_sidecar,
open_session_wire, temp_dir, write_fixture,
};
const ALLOWED_NODE_BUILTINS: &[&str] = &["fs", "util"];
const GUEST_SCRIPT: &str = r#"
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
// `node:util` and `node:fs` are resolved at module load. If `util.promisify`
// throws synchronously on the eager TypeError path *outside* of a guard, this
// whole module would fail to load and the process would crash (non-zero exit),
// reproducing issue #11.
const util = require("node:util");
const fs = require("node:fs");
// (1) The eager TypeError must be containable by user code rather than tearing
// down the module/process. This mirrors `promisify(undefined)` which is what
// the old incomplete polyfill handed to extract-zip / get-stream at load time.
let undefinedThrewTypeError = false;
try {
util.promisify(undefined);
} catch (error) {
undefinedThrewTypeError = error != null && error.name === "TypeError";
}
// (2) Root-cause regression guard: a genuine builtin function that adapters
// actually promisify must be a real function (not an `undefined` stub), so
// `promisify` yields a usable function.
const readFileIsFunction = typeof fs.readFile === "function";
const promisifiedReadFileIsFunction =
typeof util.promisify(fs.readFile) === "function";
console.log(
JSON.stringify({
undefinedThrewTypeError,
readFileIsFunction,
promisifiedReadFileIsFunction,
}),
);
"#;
fn run_guest(case_name: &str, script: &str, allowed_builtins: &[&str]) -> (Value, String, i32) {
assert_node_available();
let cwd = temp_dir(&format!("promisify-{case_name}"));
let entrypoint = cwd.join("entry.mjs");
write_fixture(&entrypoint, script);
let mut sidecar = new_sidecar(case_name);
let connection_id = authenticate_wire(&mut sidecar, &format!("conn-{case_name}"));
let session_id = open_session_wire(&mut sidecar, 2, &connection_id);
let mut metadata = HashMap::new();
metadata.insert(
String::from("env.AGENT_OS_ALLOWED_NODE_BUILTINS"),
serde_json::to_string(allowed_builtins).expect("serialize builtin allowlist"),
);
let (vm_id, _create) = create_vm_wire_with_metadata(
&mut sidecar,
3,
&connection_id,
&session_id,
secure_exec_sidecar::wire::GuestRuntimeKind::JavaScript,
&cwd,
metadata,
);
let process_id = format!("proc-{case_name}");
execute_wire(
&mut sidecar,
4,
&connection_id,
&session_id,
&vm_id,
&process_id,
secure_exec_sidecar::wire::GuestRuntimeKind::JavaScript,
&entrypoint,
Vec::new(),
);
let (stdout, stderr, exit_code) = collect_process_output_wire_with_timeout(
&mut sidecar,
&connection_id,
&session_id,
&vm_id,
&process_id,
Duration::from_secs(30),
);
dispose_vm_and_close_session_wire(&mut sidecar, &connection_id, &session_id, &vm_id);
let parsed = serde_json::from_str(stdout.trim())
.unwrap_or_else(|_| panic!("parse guest JSON\nstdout:\n{stdout}\nstderr:\n{stderr}"));
(parsed, stderr, exit_code)
}
#[test]
fn promisify_undefined_does_not_crash_module_load() {
let (parsed, stderr, exit_code) =
run_guest("promisify-module-load", GUEST_SCRIPT, ALLOWED_NODE_BUILTINS);
assert_eq!(
exit_code, 0,
"guest module load crashed (issue #11)\nstderr:\n{stderr}\nparsed:\n{parsed}"
);
assert_eq!(
parsed["undefinedThrewTypeError"],
Value::Bool(true),
"promisify(undefined) should raise a containable TypeError, parsed={parsed}"
);
assert_eq!(
parsed["readFileIsFunction"],
Value::Bool(true),
"fs.readFile must be a real function, parsed={parsed}"
);
assert_eq!(
parsed["promisifiedReadFileIsFunction"],
Value::Bool(true),
"promisify(fs.readFile) must return a function, parsed={parsed}"
);
}