#![cfg(all(feature = "dashboard", unix))]
use std::io::Read;
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
fn gaze_binary() -> Command {
Command::new(assert_cmd::cargo::cargo_bin("gaze"))
}
struct ServeProbe {
child: Child,
stderr: std::sync::mpsc::Receiver<String>,
}
impl ServeProbe {
fn spawn(dashboard_flags: &[&str]) -> Self {
let mut command = gaze_binary();
command
.args(["proxy", "serve", "--bind", "127.0.0.1:0"])
.args(dashboard_flags)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped());
let mut child = command.spawn().expect("spawn gaze proxy serve");
let mut stderr_pipe = child.stderr.take().expect("piped stderr");
let (sender, receiver) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let mut buffer = [0_u8; 4096];
let mut pending = String::new();
loop {
match stderr_pipe.read(&mut buffer) {
Ok(0) | Err(_) => break,
Ok(n) => {
pending.push_str(&String::from_utf8_lossy(&buffer[..n]));
while let Some(newline) = pending.find('\n') {
let line = pending[..newline].to_owned();
pending = pending[newline + 1..].to_owned();
if sender.send(line).is_err() {
return;
}
}
}
}
}
});
Self {
child,
stderr: receiver,
}
}
fn wait_for_line(&mut self, needle: &str) -> String {
let deadline = Instant::now() + Duration::from_secs(10);
loop {
let remaining = deadline
.checked_duration_since(Instant::now())
.expect("timed out waiting for dashboard status line");
match self.stderr.recv_timeout(remaining) {
Ok(line) if line.contains(needle) => return line,
Ok(_) => continue,
Err(_) => panic!("stderr closed or timed out before `{needle}` appeared"),
}
}
}
fn assert_no_line_containing(&mut self, needle: &str, window: Duration) {
let deadline = Instant::now() + window;
while let Some(remaining) = deadline.checked_duration_since(Instant::now()) {
match self.stderr.recv_timeout(remaining) {
Ok(line) => {
assert!(
!line.contains(needle),
"unexpected line containing `{needle}`: {line}"
);
}
Err(_) => break,
}
}
}
fn assert_provider_still_running(&mut self) {
std::thread::sleep(Duration::from_millis(300));
assert!(
self.child.try_wait().expect("try_wait").is_none(),
"provider process exited; dashboard failure must leave the provider running"
);
}
}
impl Drop for ServeProbe {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
#[test]
fn serve_without_dashboard_flags_prints_no_dashboard_line_and_serves() {
let mut probe = ServeProbe::spawn(&[]);
probe.assert_no_line_containing("gaze dashboard", Duration::from_millis(700));
probe.assert_provider_still_running();
}
#[test]
fn owner_raw_flag_without_dashboard_disables_dashboard_only() {
let mut probe = ServeProbe::spawn(&[
"--dashboard-capture-owner-raw",
"--dashboard-acknowledge-owner-raw-risk",
]);
let line = probe.wait_for_line("gaze dashboard disabled:");
assert!(line.contains("--dashboard"));
probe.assert_provider_still_running();
}
#[test]
fn owner_raw_capture_without_acknowledgement_disables_dashboard_only() {
let mut probe = ServeProbe::spawn(&["--dashboard", "--dashboard-capture-owner-raw"]);
probe.wait_for_line("gaze dashboard disabled:");
probe.assert_provider_still_running();
}
#[test]
fn owner_restored_acknowledgement_without_capture_disables_dashboard_only() {
let mut probe =
ServeProbe::spawn(&["--dashboard", "--dashboard-acknowledge-owner-restored-risk"]);
probe.wait_for_line("gaze dashboard disabled:");
probe.assert_provider_still_running();
}
#[test]
fn stdio_pairing_descriptor_disables_dashboard_only() {
for fd in ["0", "1", "2"] {
let mut probe = ServeProbe::spawn(&["--dashboard", "--dashboard-pairing-fd", fd]);
let line = probe.wait_for_line("gaze dashboard disabled:");
assert!(line.contains("descriptor"));
probe.assert_provider_still_running();
}
}
#[test]
fn non_loopback_dashboard_bind_disables_dashboard_only() {
let mut probe = ServeProbe::spawn(&["--dashboard", "--dashboard-bind", "192.0.2.7:0"]);
let line = probe.wait_for_line("gaze dashboard disabled:");
assert!(line.contains("loopback"));
probe.assert_provider_still_running();
}
#[test]
fn loopback_dashboard_bind_with_port_disables_dashboard_only() {
let mut probe = ServeProbe::spawn(&["--dashboard", "--dashboard-bind", "127.0.0.1:8080"]);
probe.wait_for_line("gaze dashboard disabled:");
probe.assert_provider_still_running();
}
#[test]
fn retention_over_crate_ceiling_disables_dashboard_only() {
let mut probe = ServeProbe::spawn(&["--dashboard", "--dashboard-max-events", "1025"]);
let line = probe.wait_for_line("gaze dashboard disabled:");
assert!(line.contains("ceiling"));
probe.assert_provider_still_running();
}
#[test]
fn invalid_dashboard_ttl_disables_dashboard_only() {
let mut probe = ServeProbe::spawn(&["--dashboard", "--dashboard-ttl", "soon"]);
probe.wait_for_line("gaze dashboard disabled:");
probe.assert_provider_still_running();
}
#[test]
fn dashboard_with_unopened_pairing_descriptor_disables_and_provider_continues() {
let mut probe = ServeProbe::spawn(&["--dashboard", "--dashboard-pairing-fd", "27"]);
let line = probe.wait_for_line("gaze dashboard disabled:");
assert!(line.contains("descriptor"));
probe.assert_no_line_containing("gaze dashboard active", Duration::from_millis(300));
probe.assert_provider_still_running();
}
#[test]
fn child_mode_without_inherited_handles_exits_sanitized() {
let output = gaze_binary()
.args([
"proxy",
"_dashboard-child",
"--bind-addr",
"127.0.0.1",
"--ttl-secs",
"30",
"--max-events",
"4",
"--max-bytes",
"65536",
])
.env_remove("GAZE_DASHBOARD_CONTROL_SOCKET_V1")
.env_remove("GAZE_DASHBOARD_INSPECTION_SOCKET_V1")
.stdin(Stdio::null())
.output()
.expect("run child mode");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("dashboard child:"));
assert!(!stderr.contains("GazeDashboardV1"));
assert!(output.stdout.is_empty());
}
#[test]
fn child_mode_is_hidden_from_help() {
let output = gaze_binary()
.args(["proxy", "--help"])
.output()
.expect("proxy help");
assert!(output.status.success());
let help = String::from_utf8_lossy(&output.stdout);
assert!(!help.contains("_dashboard-child"));
}
#[test]
fn start_exposes_dashboard_flags_and_restart_does_not() {
let start_help = gaze_binary()
.args(["proxy", "start", "--help"])
.output()
.expect("start help");
assert!(start_help.status.success());
let start_text = String::from_utf8_lossy(&start_help.stdout);
for flag in [
"--dashboard",
"--dashboard-capture-owner-raw",
"--dashboard-acknowledge-owner-raw-risk",
"--dashboard-capture-owner-restored",
"--dashboard-acknowledge-owner-restored-risk",
"--dashboard-bind",
"--dashboard-ttl",
"--dashboard-max-events",
"--dashboard-max-bytes",
"--dashboard-pairing-fd",
] {
assert!(start_text.contains(flag), "start help must list {flag}");
}
let restart_help = gaze_binary()
.args(["proxy", "restart", "--help"])
.output()
.expect("restart help");
assert!(restart_help.status.success());
let restart_text = String::from_utf8_lossy(&restart_help.stdout);
assert!(!restart_text.contains("--dashboard"));
}
#[test]
fn serve_exposes_dashboard_flags() {
let output = gaze_binary()
.args(["proxy", "serve", "--help"])
.output()
.expect("serve help");
assert!(output.status.success());
let help = String::from_utf8_lossy(&output.stdout);
for flag in ["--dashboard", "--dashboard-bind", "--dashboard-pairing-fd"] {
assert!(help.contains(flag), "serve help must list {flag}");
}
}