use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::{Duration, Instant};
fn have(tool: &str) -> bool {
Command::new(tool)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn tmp() -> PathBuf {
static C: AtomicU32 = AtomicU32::new(0);
std::env::temp_dir().join(format!(
"bynk_devdbg_{}_{}",
std::process::id(),
C.fetch_add(1, Ordering::Relaxed)
))
}
#[test]
fn workerd_breakpoint_in_bynk_handler_binds_and_pauses() {
if !have("wrangler") || !have("node") {
eprintln!("skipping: `wrangler` and/or `node` not available");
return;
}
let dir = tmp();
std::fs::create_dir_all(dir.join("src")).unwrap();
std::fs::write(dir.join("bynk.toml"), "[project]\nname = \"svc\"\n").unwrap();
let svc = "context svc\n\nconsumes bynk { Logger }\n\nservice api from http {\n\ton GET(\"/\") by v: Visitor () -> Effect[HttpResult[String]] given Logger {\n\t\tlet _ <- Logger.info(\"hit\")\n\t\tOk(\"ok\")\n\t}\n}\n";
std::fs::write(dir.join("src").join("svc.bynk"), svc).unwrap();
let bynk_line = svc.lines().position(|l| l.contains("Logger.info")).unwrap() + 1;
let build = dir.join("build");
let opts = bynk_emit::project::CompileOptions::split(
dir.clone(),
bynk_emit::project::read_project_paths(&dir),
)
.target(bynk_emit::project::BuildTarget::Workers);
let out = bynk_emit::project::compile_project(&opts)
.map_err(bynk_emit::project::ProjectFailure::flatten)
.unwrap_or_else(|e| panic!("compile failed: {e:?}"));
bynk_emit::write_output(&out, &build).unwrap();
let worker_dir = build.join("workers").join("svc");
assert!(
worker_dir.join("handlers.ts.map").exists(),
"worker output must carry handlers.ts.map"
);
let base = 9000 + (std::process::id() % 500) as u16 * 2;
let (inspector_port, app_port) = (base + 1, base);
let mut wrangler = Command::new("wrangler")
.args([
"dev",
"--inspector-port",
&inspector_port.to_string(),
"--port",
&app_port.to_string(),
])
.current_dir(&worker_dir)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn wrangler dev");
let result = drive(&worker_dir, inspector_port, app_port, bynk_line);
let _ = wrangler.kill();
let _ = wrangler.wait();
let _ = Command::new("pkill").arg("-f").arg("workerd").output();
let _ = std::fs::remove_dir_all(&dir);
let (status, stdout, stderr) = result;
assert!(
status,
"breakpoint did not bind/pause on the worker\n--- harness stdout ---\n{stdout}\n--- stderr ---\n{stderr}"
);
assert!(
stdout.contains("BIND OK"),
"harness did not confirm bind\n{stdout}"
);
}
fn drive(
_worker_dir: &Path,
inspector_port: u16,
app_port: u16,
bynk_line: usize,
) -> (bool, String, String) {
let deadline = Instant::now() + Duration::from_secs(30);
let ready = loop {
if Instant::now() > deadline {
break false;
}
let probe = Command::new("node")
.arg("-e")
.arg(format!(
"fetch('http://127.0.0.1:{inspector_port}/json').then(r=>r.json()).then(j=>process.exit(j[0]?0:1)).catch(()=>process.exit(1))"
))
.output();
if probe.map(|o| o.status.success()).unwrap_or(false) {
break true;
}
std::thread::sleep(Duration::from_millis(500));
};
if !ready {
return (
false,
String::new(),
"inspector never became reachable".to_string(),
);
}
let harness = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("support")
.join("wrangler_attach.mjs");
let out = Command::new("node")
.arg(harness)
.arg(inspector_port.to_string())
.arg(app_port.to_string())
.arg(bynk_line.to_string())
.output()
.expect("run the CDP harness");
(
out.status.success(),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}