mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
294 lines
9.8 KiB
Rust
294 lines
9.8 KiB
Rust
//! End-to-end `bsk status --json` and `bsk doctor` tests.
|
|
|
|
#![cfg(unix)]
|
|
|
|
use std::path::PathBuf;
|
|
use std::process::{Command, Stdio};
|
|
use std::sync::{Mutex, MutexGuard};
|
|
use std::time::Duration;
|
|
|
|
use tempfile::TempDir;
|
|
|
|
static STATUS_CMD_TEST_LOCK: Mutex<()> = Mutex::new(());
|
|
|
|
fn status_cmd_test_guard() -> MutexGuard<'static, ()> {
|
|
STATUS_CMD_TEST_LOCK
|
|
.lock()
|
|
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
}
|
|
|
|
fn bsk_bin() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_BIN_EXE_bsk"))
|
|
}
|
|
|
|
fn wait_for_pid_exit(pid: i32, timeout: Duration) -> bool {
|
|
let deadline = std::time::Instant::now() + timeout;
|
|
while std::time::Instant::now() < deadline {
|
|
let alive = unsafe { libc::kill(pid, 0) } == 0;
|
|
if !alive {
|
|
return true;
|
|
}
|
|
std::thread::sleep(Duration::from_millis(50));
|
|
}
|
|
false
|
|
}
|
|
|
|
#[test]
|
|
fn bsk_status_json_returns_structured_payload() {
|
|
let _guard = status_cmd_test_guard();
|
|
let tmp = TempDir::new().unwrap();
|
|
let home = tmp.path().join("bsk");
|
|
std::fs::create_dir_all(&home).unwrap();
|
|
|
|
// Auto-spawn via `bsk status` — should bring up the daemon.
|
|
let out = Command::new(bsk_bin())
|
|
.args(["--json", "status"])
|
|
.env("BSK_HOME", &home)
|
|
.env("BSK_BROWSER_WAIT_MS", "0")
|
|
.env("RUST_LOG", "warn")
|
|
.output()
|
|
.expect("run bsk status");
|
|
assert!(
|
|
out.status.success(),
|
|
"bsk status failed: stdout={}\nstderr={}",
|
|
String::from_utf8_lossy(&out.stdout),
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
|
|
let stdout = String::from_utf8(out.stdout).unwrap();
|
|
let parsed: serde_json::Value =
|
|
serde_json::from_str(stdout.trim()).expect("status --json should be valid JSON");
|
|
|
|
assert!(parsed["pid"].as_u64().unwrap() > 0);
|
|
assert!(!parsed["daemon_version"].as_str().unwrap().is_empty());
|
|
assert_eq!(parsed["protocol_version"], "1.1");
|
|
assert!(parsed["sock_path"].as_str().is_some());
|
|
assert_eq!(parsed["browsers"], serde_json::json!([]));
|
|
assert_eq!(parsed["sessions"], serde_json::json!([]));
|
|
|
|
let pid = parsed["pid"].as_u64().unwrap() as i32;
|
|
|
|
// Cleanup.
|
|
let _ = Command::new(bsk_bin())
|
|
.args(["daemon", "stop"])
|
|
.env("BSK_HOME", &home)
|
|
.output();
|
|
assert!(wait_for_pid_exit(pid, Duration::from_secs(5)));
|
|
}
|
|
|
|
#[test]
|
|
fn bsk_doctor_runs_without_running_daemon() {
|
|
let _guard = status_cmd_test_guard();
|
|
let tmp = TempDir::new().unwrap();
|
|
let home = tmp.path().join("bsk");
|
|
std::fs::create_dir_all(&home).unwrap();
|
|
|
|
// `bsk doctor` auto-spawns the daemon when none is running; use a short
|
|
// browser-connect wait so the test doesn't block for 5 s.
|
|
let out = Command::new(bsk_bin())
|
|
.args(["doctor"])
|
|
.env("BSK_HOME", &home)
|
|
.env("RUST_LOG", "warn")
|
|
.env("BSK_DOCTOR_BROWSER_WAIT_MS", "200")
|
|
.output()
|
|
.expect("run bsk doctor");
|
|
assert_eq!(out.status.code(), Some(1));
|
|
let stdout = String::from_utf8(out.stdout).unwrap();
|
|
assert!(
|
|
stdout.contains("bsk home writable"),
|
|
"doctor should mention bsk home check: {stdout}"
|
|
);
|
|
assert!(
|
|
stdout.contains("daemon running"),
|
|
"doctor should mention daemon check: {stdout}"
|
|
);
|
|
assert!(
|
|
stdout.contains("extension connected"),
|
|
"doctor should mention extension connected check: {stdout}"
|
|
);
|
|
|
|
// Clean up the daemon that was auto-spawned above.
|
|
if let Ok(bytes) = std::fs::read(home.join("daemon.json")) {
|
|
if let Ok(info) = serde_json::from_slice::<serde_json::Value>(&bytes) {
|
|
if let Some(pid) = info["pid"].as_u64() {
|
|
let _ = Command::new(bsk_bin())
|
|
.args(["daemon", "stop"])
|
|
.env("BSK_HOME", &home)
|
|
.output();
|
|
wait_for_pid_exit(pid as i32, Duration::from_secs(5));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn bsk_doctor_json_returns_structured_checks() {
|
|
let _guard = status_cmd_test_guard();
|
|
let tmp = TempDir::new().unwrap();
|
|
let home = tmp.path().join("bsk");
|
|
std::fs::create_dir_all(&home).unwrap();
|
|
|
|
// `bsk doctor` auto-spawns the daemon when none is running; use a short
|
|
// browser-connect wait so the test doesn't block for 5 s.
|
|
let out = Command::new(bsk_bin())
|
|
.args(["--json", "doctor"])
|
|
.env("BSK_HOME", &home)
|
|
.env("RUST_LOG", "warn")
|
|
.env("BSK_DOCTOR_BROWSER_WAIT_MS", "200")
|
|
.output()
|
|
.expect("run bsk doctor --json");
|
|
assert_eq!(out.status.code(), Some(1));
|
|
let stdout = String::from_utf8(out.stdout).unwrap();
|
|
let parsed: serde_json::Value =
|
|
serde_json::from_str(stdout.trim()).expect("doctor --json should be valid JSON");
|
|
let checks = parsed.as_array().expect("doctor output should be an array");
|
|
assert!(
|
|
checks
|
|
.iter()
|
|
.any(|check| check["name"] == "bsk home writable"),
|
|
"doctor JSON should include home check: {stdout}"
|
|
);
|
|
assert!(
|
|
checks
|
|
.iter()
|
|
.any(|check| check["name"] == "extension connected"),
|
|
"doctor JSON should include extension connected check: {stdout}"
|
|
);
|
|
|
|
// Clean up the daemon that was auto-spawned above.
|
|
if let Ok(bytes) = std::fs::read(home.join("daemon.json")) {
|
|
if let Ok(info) = serde_json::from_slice::<serde_json::Value>(&bytes) {
|
|
if let Some(pid) = info["pid"].as_u64() {
|
|
let _ = Command::new(bsk_bin())
|
|
.args(["daemon", "stop"])
|
|
.env("BSK_HOME", &home)
|
|
.output();
|
|
wait_for_pid_exit(pid as i32, Duration::from_secs(5));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn bsk_doctor_does_not_treat_live_non_daemon_pid_as_running() {
|
|
let _guard = status_cmd_test_guard();
|
|
let tmp = TempDir::new().unwrap();
|
|
let home = tmp.path().join("bsk");
|
|
std::fs::create_dir_all(home.join("run")).unwrap();
|
|
|
|
let mut child = Command::new("sleep")
|
|
.arg("30")
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.spawn()
|
|
.expect("spawn controlled non-daemon child");
|
|
|
|
let info = serde_json::json!({
|
|
"pid": child.id(),
|
|
"sock_path": home.join("run").join("not-a-daemon.sock"),
|
|
"ws_port": 0,
|
|
"version": env!("CARGO_PKG_VERSION"),
|
|
"started_at_epoch_secs": 1
|
|
});
|
|
std::fs::write(
|
|
home.join("daemon.json"),
|
|
serde_json::to_vec_pretty(&info).unwrap(),
|
|
)
|
|
.unwrap();
|
|
|
|
let out = Command::new(bsk_bin())
|
|
.args(["--json", "doctor"])
|
|
.env("BSK_HOME", &home)
|
|
.env("RUST_LOG", "warn")
|
|
.output()
|
|
.expect("run bsk doctor --json");
|
|
assert_eq!(out.status.code(), Some(1));
|
|
let stdout = String::from_utf8(out.stdout).unwrap();
|
|
let checks: Vec<serde_json::Value> = serde_json::from_str(stdout.trim()).unwrap();
|
|
let daemon = checks
|
|
.iter()
|
|
.find(|check| check["name"] == "daemon running")
|
|
.expect("daemon running check");
|
|
assert_eq!(daemon["ok"], false);
|
|
|
|
let _ = child.kill();
|
|
let _ = child.wait();
|
|
}
|
|
|
|
#[test]
|
|
fn bsk_doctor_flags_pid_mismatch_against_running_daemon() {
|
|
let _guard = status_cmd_test_guard();
|
|
let tmp = TempDir::new().unwrap();
|
|
let home = tmp.path().join("bsk");
|
|
std::fs::create_dir_all(&home).unwrap();
|
|
|
|
// Bring up a real daemon so the socket is live and IPC works.
|
|
let out = Command::new(bsk_bin())
|
|
.args(["daemon", "start", "--port", "0", "--daemon-idle", "60s"])
|
|
.env("BSK_HOME", &home)
|
|
.env("RUST_LOG", "warn")
|
|
.output()
|
|
.expect("daemon start");
|
|
assert!(
|
|
out.status.success(),
|
|
"daemon start failed: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
|
|
let info_path = home.join("daemon.json");
|
|
let original: serde_json::Value =
|
|
serde_json::from_slice(&std::fs::read(&info_path).unwrap()).unwrap();
|
|
let real_pid = original["pid"].as_u64().expect("pid number") as i32;
|
|
|
|
// Spawn a controlled "other" process so the rewritten pid is alive
|
|
// but is definitely not our daemon. Without an alive pid the check
|
|
// would fail for a different reason (stale-dead) than what we want.
|
|
let mut decoy = Command::new("sleep")
|
|
.arg("30")
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.spawn()
|
|
.expect("spawn decoy pid");
|
|
|
|
let mut tampered = original.clone();
|
|
tampered["pid"] = serde_json::Value::from(decoy.id());
|
|
std::fs::write(&info_path, serde_json::to_vec_pretty(&tampered).unwrap()).unwrap();
|
|
|
|
let out = Command::new(bsk_bin())
|
|
.args(["--json", "doctor"])
|
|
.env("BSK_HOME", &home)
|
|
.env("RUST_LOG", "warn")
|
|
.output()
|
|
.expect("bsk doctor --json");
|
|
assert_eq!(out.status.code(), Some(1));
|
|
let stdout = String::from_utf8(out.stdout).unwrap();
|
|
let checks: Vec<serde_json::Value> = serde_json::from_str(stdout.trim()).unwrap();
|
|
|
|
let daemon = checks
|
|
.iter()
|
|
.find(|check| check["name"] == "daemon running")
|
|
.expect("daemon running check");
|
|
assert_eq!(
|
|
daemon["ok"], false,
|
|
"daemon running check should fail when daemon.json pid was tampered: {stdout}"
|
|
);
|
|
let detail = daemon["detail"].as_str().unwrap_or_default();
|
|
assert!(
|
|
detail.contains(&decoy.id().to_string()) && detail.contains(&real_pid.to_string()),
|
|
"daemon running detail should mention both pids; got {detail}"
|
|
);
|
|
|
|
// Restore the real pid so cleanup `bsk daemon stop` can find the daemon.
|
|
std::fs::write(&info_path, serde_json::to_vec_pretty(&original).unwrap()).unwrap();
|
|
let _ = decoy.kill();
|
|
let _ = decoy.wait();
|
|
|
|
let _ = Command::new(bsk_bin())
|
|
.args(["daemon", "stop"])
|
|
.env("BSK_HOME", &home)
|
|
.output();
|
|
assert!(wait_for_pid_exit(real_pid, Duration::from_secs(5)));
|
|
}
|