use bao_engine::context::JsContext;
use bao_engine::value::JsValue;
use std::cell::Cell;
fn eval_str(ctx: &mut JsContext, code: &str) -> String {
match ctx.eval(code, "<test>") {
Ok(JsValue::String(s)) => s,
Ok(JsValue::Number(n)) => format!("{}", n),
Ok(JsValue::Bool(b)) => if b { "true" } else { "false" }.to_string(),
Ok(v) => format!("{:?}", v),
Err(e) => format!("ERROR: {:?}", e),
}
}
thread_local! {
static HOOK_BUDGET: Cell<usize> = const { Cell::new(0) };
}
fn bounded_drain_hook(cx: &mut mozjs::context::JSContext) -> bool {
let exhausted = HOOK_BUDGET.with(|b| {
let n = b.get();
if n == 0 {
return true;
}
b.set(n - 1);
false
});
if exhausted {
return false;
}
bun_runtime::timers::drain_and_check(cx)
}
fn wait_until(ctx: &mut JsContext, js_condition: &str, budget: usize) -> bool {
for _ in 0..40 {
HOOK_BUDGET.with(|b| b.set(budget));
if eval_str(ctx, js_condition) == "y" {
return true;
}
}
false
}
fn settle(ctx: &mut JsContext, budget: usize) {
HOOK_BUDGET.with(|b| b.set(budget));
let _ = eval_str(ctx, "'settle'");
}
#[test]
fn net_echo_e2e_real_tcp_roundtrip() {
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.set_post_eval_hook(bounded_drain_hook);
let setup = eval_str(
&mut ctx,
r#"
var net = require('net');
var log = [];
globalThis.__done = false;
var server = net.createServer(function(sock) {
log.push('connection');
log.push('remotePort=' + (sock.remotePort > 0));
// Echo the received bytes straight back — byte-for-byte, via the
// ArrayBuffer the data event delivers.
sock.on('data', function(d) {
sock.write(new Uint8Array(d));
});
// Synchronous same-object re-emit: end() emits 'end'+'close' on
// this socket from inside the 'end' listener — the canonical Node
// shape, safe since the node_events single-owner invariant fix.
sock.on('end', function() { sock.end(); });
});
server.listen(0, '127.0.0.1', function() {
var port = server.address().port;
log.push('port=' + (port > 0));
var payload = new Uint8Array([112, 105, 110, 103, 0xff, 0x00, 0x80]);
// The connect callback fires synchronously inside net.connect
// (before the `var client` assignment lands), so defer the first
// write; 'data' delivery is poll-based and lossless either way.
var client = net.connect(port, '127.0.0.1', function() {
log.push('client_connected');
});
setTimeout(function() { client.write(payload); }, 0);
client.on('data', function(d) {
var got = Array.prototype.slice.call(new Uint8Array(d));
var want = Array.prototype.slice.call(payload);
var same = got.length === want.length && got.every(function(b, i) { return b === want[i]; });
log.push('echo=' + same);
// Synchronous same-object re-emit (end() emits 'end'/'close'
// on this socket) — canonical Node shape, see the note above.
client.end();
});
client.on('close', function() {
server.close(function() {
log.push('server_closed');
globalThis.__done = true;
});
});
});
globalThis.__log = function() { return log.join('|'); };
'setup-ok'
"#,
);
assert_eq!(setup, "setup-ok", "echo wiring must eval cleanly");
let done = wait_until(&mut ctx, "globalThis.__done === true ? 'y' : 'n'", 50);
let diag = eval_str(&mut ctx, "globalThis.__log ? globalThis.__log() : '(no log)'");
assert!(done, "echo roundtrip must complete; log so far: {diag}");
let log = eval_str(&mut ctx, "globalThis.__log()");
for part in [
"port=true",
"client_connected",
"connection",
"remotePort=true",
"echo=true",
"server_closed",
] {
assert!(
log.split('|').any(|entry| entry == part),
"echo log must contain '{part}' in order-independent position, got: {log}"
);
}
assert!(
log.starts_with("port=true"),
"listening must be logged first, got: {log}"
);
assert!(
log.ends_with("server_closed"),
"server close must be the final event, got: {log}"
);
assert!(
!bun_runtime::node_http::has_active_servers(),
"server.close() must unregister the net liveness token"
);
assert_eq!(
eval_str(&mut ctx, "typeof require('net').Server === 'function' ? 'ok' : 'broken'"),
"ok"
);
}
#[test]
fn net_peer_fin_delivers_end_event() {
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.set_post_eval_hook(bounded_drain_hook);
let setup = eval_str(
&mut ctx,
r#"
var net = require('net');
var log = [];
globalThis.__done = false;
var server = net.createServer(function(sock) {
// Synchronous same-object re-emit on 'end' — canonical shape.
sock.on('end', function() { log.push('saw_end'); sock.end(); });
sock.on('close', function() { log.push('sock_closed'); });
sock.on('data', function(d) { sock.write(new Uint8Array(d)); });
});
server.listen(0, '127.0.0.1', function() {
var client = net.connect(server.address().port, '127.0.0.1', function() {});
setTimeout(function() { client.write('fin-probe'); }, 0);
// Synchronous same-object re-emit (end() emits 'end'/'close').
client.on('data', function() { client.end(); });
});
var watcher = setInterval(function() {
if (log.indexOf('sock_closed') >= 0) {
clearInterval(watcher);
server.close(function() { globalThis.__done = true; });
}
}, 0);
globalThis.__log = function() { return log.join('|'); };
'setup-ok'
"#,
);
assert_eq!(setup, "setup-ok");
let done = wait_until(&mut ctx, "globalThis.__done === true ? 'y' : 'n'", 50);
let diag = eval_str(&mut ctx, "globalThis.__log ? globalThis.__log() : '(no log)'");
assert!(done, "peer-FIN lifecycle must settle; log: {diag}");
let log = eval_str(&mut ctx, "globalThis.__log()");
assert!(
log.contains("saw_end") && log.contains("sock_closed"),
"peer FIN must deliver 'end' then 'close' on the server-side socket, got: {log}"
);
settle(&mut ctx, 30);
assert!(!bun_runtime::node_http::has_active_servers());
let mut cxm = ctx.cx();
assert!(
!bun_runtime::timers::drain_and_check(&mut cxm),
"after full teardown the loop must have no pending work (no spinning poll chain)"
);
}