use bao_engine::context::JsContext;
use bao_engine::value::JsValue;
fn eval_string(ctx: &mut JsContext, source: &str) -> String {
match ctx.eval(source, "<test>") {
Ok(JsValue::String(s)) => s,
Ok(JsValue::Number(n)) => format!("{}", n),
Ok(JsValue::Bool(b)) => if b { "true" } else { "false" }.to_string(),
Ok(JsValue::Null) => "null".to_string(),
Ok(JsValue::Undefined) => "undefined".to_string(),
Ok(JsValue::Object(_)) => "[object]".to_string(),
Err(e) => format!("ERROR:{}", e.message),
}
}
fn setup_ctx() -> JsContext {
bun_runtime::install_exit_handler();
bun_runtime::bun_api::init_process_start();
let mut ctx = JsContext::for_test().expect("JsContext");
ctx.set_global_setup(bun_runtime::globals::install_all);
ctx
}
fn pump_until_quiescent(ctx: &mut JsContext, deadline_ms: u64) {
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(deadline_ms);
while std::time::Instant::now() < deadline {
let mut cxm = ctx.cx();
if !bun_runtime::timers::drain_and_check(&mut cxm) {
return;
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
}
fn js_escape(p: &std::path::Path) -> String {
p.to_string_lossy().replace('\\', "\\\\").replace('"', "\\\"")
}
#[test]
fn fs_async_callback_readfile_utf8_delivered() {
let mut ctx = setup_ctx();
let dir = std::env::temp_dir().join("bao_fs_async_cb_readfile");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("hello.txt");
std::fs::write(&path, "hello fs async").unwrap();
let out = eval_string(
&mut ctx,
&format!(
r#"
var fs = require('fs');
globalThis.__rf = {{ fired: false, err: null, data: null }};
fs.readFile("{}", "utf8", function(err, data) {{
globalThis.__rf.fired = true;
globalThis.__rf.err = err ? String((err && err.code) || err.message || err) : null;
globalThis.__rf.data = (typeof data === 'string') ? data : '<nonstring:' + typeof data + '>';
}});
'scheduled'
"#,
js_escape(&path)
),
);
assert_eq!(out, "scheduled");
pump_until_quiescent(&mut ctx, 10_000);
let state = eval_string(&mut ctx, "JSON.stringify(globalThis.__rf)");
assert!(
state.contains("\"fired\":true"),
"fs.readFile callback never delivered (state: {})",
state
);
assert!(
state.contains("\"err\":null"),
"fs.readFile callback delivered an error (state: {})",
state
);
assert!(
state.contains("hello fs async"),
"fs.readFile callback delivered wrong content (state: {})",
state
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn fs_async_callback_readfile_enoent_error_delivered() {
let mut ctx = setup_ctx();
let missing = std::env::temp_dir().join("bao_fs_async_cb_missing_definitely_absent.txt");
let _ = std::fs::remove_file(&missing);
let out = eval_string(
&mut ctx,
&format!(
r#"
var fs = require('fs');
globalThis.__re = {{ fired: false, code: null }};
fs.readFile("{}", function(err, data) {{
globalThis.__re.fired = true;
globalThis.__re.code = err ? String((err && err.code) || err.message || err) : null;
}});
'scheduled'
"#,
js_escape(&missing)
),
);
assert_eq!(out, "scheduled");
pump_until_quiescent(&mut ctx, 10_000);
let state = eval_string(&mut ctx, "JSON.stringify(globalThis.__re)");
assert!(
state.contains("\"fired\":true"),
"fs.readFile error callback never delivered (state: {})",
state
);
assert!(
state.contains("\"code\":\"ENOENT\""),
"fs.readFile error callback must carry ENOENT (state: {})",
state
);
}
#[test]
fn fs_async_callback_promise_readfile_regression() {
let mut ctx = setup_ctx();
let dir = std::env::temp_dir().join("bao_fs_async_cb_promises");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("promises.txt");
std::fs::write(&path, "promise payload").unwrap();
let out = eval_string(
&mut ctx,
&format!(
r#"
var fs = require('fs');
globalThis.__pv = 'unset';
fs.promises.readFile("{}", "utf8").then(
function(d) {{ globalThis.__pv = d; }},
function(e) {{ globalThis.__pv = 'ERR:' + ((e && e.message) || e); }}
);
'scheduled'
"#,
js_escape(&path)
),
);
assert_eq!(out, "scheduled");
let state = eval_string(&mut ctx, "globalThis.__pv");
assert_eq!(
state, "promise payload",
"fs.promises.readFile must still resolve with file content"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn fs_async_callback_readfilesync_regression() {
let mut ctx = setup_ctx();
let dir = std::env::temp_dir().join("bao_fs_async_cb_sync");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("sync.txt");
std::fs::write(&path, "sync body").unwrap();
let out = eval_string(
&mut ctx,
&format!(
r#"require('fs').readFileSync("{}", "utf8")"#,
js_escape(&path)
),
);
assert_eq!(out, "sync body");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn fs_async_callback_crypto_pbkdf2_delivered() {
let mut ctx = setup_ctx();
let out = eval_string(
&mut ctx,
r#"
var crypto = require('crypto');
globalThis.__pk = { fired: false, err: null, len: 0 };
crypto.pbkdf2("password", "salt", 1000, 32, "sha256", function(err, key) {
globalThis.__pk.fired = true;
globalThis.__pk.err = err ? String((err && err.message) || err) : null;
globalThis.__pk.len = (key && key.length) ? key.length : 0;
});
'scheduled'
"#,
);
assert_eq!(out, "scheduled");
pump_until_quiescent(&mut ctx, 10_000);
let state = eval_string(&mut ctx, "JSON.stringify(globalThis.__pk)");
assert!(
state.contains("\"fired\":true"),
"crypto.pbkdf2 callback never delivered (state: {})",
state
);
assert!(
state.contains("\"err\":null"),
"crypto.pbkdf2 callback delivered an error (state: {})",
state
);
assert!(
state.contains("\"len\":32"),
"crypto.pbkdf2 callback must deliver a 32-byte key (state: {})",
state
);
}