mod helpers;
use helpers::TestContext;
use std::io::Write;
use std::time::Duration;
#[test]
fn test_run_with_port_shows_url() {
let ctx = TestContext::new("t-port");
let run_output = ctx
.cmd()
.args([
"--session",
&ctx.session,
"run",
"echo 'listening' && sleep 60",
"--name",
"api",
"--port",
"4567",
"--proxy",
])
.output()
.unwrap();
assert!(
run_output.status.success(),
"run failed: stdout={} stderr={}",
String::from_utf8_lossy(&run_output.stdout),
String::from_utf8_lossy(&run_output.stderr)
);
let stdout = String::from_utf8_lossy(&run_output.stdout);
assert!(
stdout.contains("http://"),
"expected a URL in stdout, got: {}",
stdout
);
let status_output = ctx
.cmd()
.args(["--session", &ctx.session, "status"])
.output()
.unwrap();
assert!(status_output.status.success());
let status_stdout = String::from_utf8_lossy(&status_output.stdout);
assert!(
status_stdout.contains("http://"),
"expected URL in status output, got: {}",
status_stdout
);
let json_output = ctx
.cmd()
.args(["--session", &ctx.session, "status", "--json"])
.output()
.unwrap();
assert!(json_output.status.success());
let json_str = String::from_utf8_lossy(&json_output.stdout);
assert!(
json_str.contains("\"port\""),
"expected 'port' field in JSON status, got: {}",
json_str
);
assert!(
json_str.contains("\"url\""),
"expected 'url' field in JSON status, got: {}",
json_str
);
let _ = ctx
.cmd()
.args(["--session", &ctx.session, "stop-all"])
.output();
}
#[test]
fn test_run_without_port_has_no_url() {
let ctx = TestContext::new("t-noport");
let output = ctx
.cmd()
.args(["--session", &ctx.session, "run", "sleep 60", "--name", "bg"])
.output()
.unwrap();
assert!(
output.status.success(),
"run failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("http://"),
"expected no URL in stdout, got: {}",
stdout
);
let _ = ctx
.cmd()
.args(["--session", &ctx.session, "stop-all"])
.output();
}
#[test]
fn test_proxy_routes_request() {
let ctx = TestContext::new("t-proxy-route");
let run_output = ctx
.cmd()
.args([
"--session",
&ctx.session,
"run",
"python3 -m http.server 4500 --bind 127.0.0.1",
"--name",
"web",
"--port",
"4500",
"--proxy",
])
.output()
.unwrap();
assert!(
run_output.status.success(),
"run failed: stdout={} stderr={}",
String::from_utf8_lossy(&run_output.stdout),
String::from_utf8_lossy(&run_output.stderr)
);
let stderr = String::from_utf8_lossy(&run_output.stderr);
assert!(
stderr.contains("Proxy listening"),
"expected 'Proxy listening' in stderr, got: {}",
stderr
);
let proxy_port = {
let prefix = "http://localhost:";
stderr.find(prefix).and_then(|idx| {
let after = &stderr[idx + prefix.len()..];
let port_str: String = after.chars().take_while(char::is_ascii_digit).collect();
port_str.parse::<u16>().ok()
})
};
let proxy_port = match proxy_port {
Some(p) => p,
None => {
let _ = ctx
.cmd()
.args(["--session", &ctx.session, "stop-all"])
.output();
panic!("could not parse proxy port from stderr: {}", stderr);
}
};
std::thread::sleep(Duration::from_millis(500));
let curl_result = std::process::Command::new("curl")
.args([
"-s",
"--max-time",
"5",
"-H",
&format!("Host: web.localhost:{}", proxy_port),
&format!("http://127.0.0.1:{}/", proxy_port),
])
.output();
if let Ok(curl_output) = curl_result
&& curl_output.status.success()
{
let body = String::from_utf8_lossy(&curl_output.stdout);
assert!(!body.is_empty(), "expected non-empty response from proxy");
}
let _ = ctx
.cmd()
.args(["--session", &ctx.session, "stop-all"])
.output();
}
#[test]
fn test_up_with_proxy_config() {
let ctx = TestContext::new("t-proxy-up");
let config_dir = tempfile::TempDir::new().unwrap();
let config_path = config_dir.path().join("agent-procs.yaml");
let mut f = std::fs::File::create(&config_path).unwrap();
write!(
f,
"proxy: true\nprocesses:\n echo-srv:\n cmd: \"echo 'started' && sleep 60\"\n port: 4600\n"
)
.unwrap();
let output = ctx
.cmd()
.args([
"--session",
&ctx.session,
"up",
"--config",
config_path.to_str().unwrap(),
])
.timeout(Duration::from_secs(30))
.output()
.unwrap();
assert!(
output.status.success(),
"up failed: stdout={} stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Proxy listening"),
"expected 'Proxy listening' in stderr, got: {}",
stderr
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("echo-srv"),
"expected 'echo-srv' in stdout, got: {}",
stdout
);
let status_output = ctx
.cmd()
.args(["--session", &ctx.session, "status"])
.output()
.unwrap();
assert!(status_output.status.success());
let status_stdout = String::from_utf8_lossy(&status_output.stdout);
assert!(
status_stdout.contains("echo-srv.localhost"),
"expected 'echo-srv.localhost' in status output, got: {}",
status_stdout
);
let _ = ctx.cmd().args(["--session", &ctx.session, "down"]).output();
}
#[test]
fn test_proxy_drops_route_after_process_exit() {
let ctx = TestContext::new("t-proxy-exit");
let run_output = ctx
.cmd()
.args([
"--session",
&ctx.session,
"run",
"python3 -c \"import http.server, socketserver, threading; httpd = socketserver.TCPServer(('127.0.0.1', 4510), http.server.SimpleHTTPRequestHandler); threading.Timer(1.0, httpd.shutdown).start(); httpd.serve_forever()\"",
"--name",
"web",
"--port",
"4510",
"--proxy",
])
.output()
.unwrap();
assert!(
run_output.status.success(),
"run failed: stdout={} stderr={}",
String::from_utf8_lossy(&run_output.stdout),
String::from_utf8_lossy(&run_output.stderr)
);
let stderr = String::from_utf8_lossy(&run_output.stderr);
let proxy_port = {
let prefix = "http://localhost:";
stderr.find(prefix).and_then(|idx| {
let after = &stderr[idx + prefix.len()..];
let port_str: String = after.chars().take_while(char::is_ascii_digit).collect();
port_str.parse::<u16>().ok()
})
}
.expect("could not parse proxy port");
std::thread::sleep(Duration::from_secs(2));
let curl_output = std::process::Command::new("curl")
.args([
"-s",
"--max-time",
"5",
"-H",
&format!("Host: web.localhost:{}", proxy_port),
&format!("http://127.0.0.1:{}/", proxy_port),
])
.output()
.unwrap();
assert!(curl_output.status.success());
let body = String::from_utf8_lossy(&curl_output.stdout);
assert!(
body.contains("no running process named 'web'"),
"expected stale route to be removed, got: {}",
body
);
let _ = ctx
.cmd()
.args(["--session", &ctx.session, "stop-all"])
.output();
}