From 7649488f6b26e48b0d34d5ec09eb7719a1f8af5a Mon Sep 17 00:00:00 2001 From: xodapi <4956501+xodapi@users.noreply.github.com> Date: Sat, 4 Jul 2026 11:42:11 +0700 Subject: [PATCH 1/2] test(cli): add command integration coverage (closes #120) --- tests/cli_integration.rs | 162 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 tests/cli_integration.rs diff --git a/tests/cli_integration.rs b/tests/cli_integration.rs new file mode 100644 index 0000000..25bdfaf --- /dev/null +++ b/tests/cli_integration.rs @@ -0,0 +1,162 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::Value; + +struct TestHome { + root: PathBuf, + appdata: PathBuf, + home: PathBuf, +} + +impl TestHome { + fn new() -> Self { + let unique = format!( + "vimit-cli-it-{}-{}", + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + ); + let root = std::env::temp_dir().join(unique); + let appdata = root.join("appdata"); + let home = root.join("home"); + fs::create_dir_all(&appdata).unwrap(); + fs::create_dir_all(&home).unwrap(); + + let state_dir = if cfg!(windows) { + appdata.join("vimit") + } else { + home.join(".config").join("vimit") + }; + fs::create_dir_all(&state_dir).unwrap(); + fs::write( + state_dir.join("state.json"), + r#"{"auto_update_check":false,"auto_api_failover":true}"#, + ) + .unwrap(); + + Self { + root, + appdata, + home, + } + } + + fn command(&self) -> Command { + let mut cmd = Command::new(env!("CARGO_BIN_EXE_vimit")); + cmd.current_dir(env!("CARGO_MANIFEST_DIR")); + if cfg!(windows) { + cmd.env("APPDATA", &self.appdata); + cmd.env("USERPROFILE", &self.home); + } else { + cmd.env("HOME", &self.home); + } + cmd.env_remove("VIBEMODE_API_KEY"); + cmd.env_remove("NEUROGATE_API_KEY"); + cmd + } +} + +impl Drop for TestHome { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.root); + } +} + +fn fixture_path(path: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join(path) +} + +fn stdout_json(output: &std::process::Output) -> Value { + serde_json::from_slice(&output.stdout).expect("stdout should be valid json") +} + +#[test] +fn demo_json_runs_real_cli_binary() { + let test_home = TestHome::new(); + let output = test_home + .command() + .args(["--demo", "--json"]) + .output() + .expect("demo command should run"); + + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let json = stdout_json(&output); + assert_eq!(json["active_endpoint"], "demo"); + assert_eq!(json["source"], "vibemode"); + assert_eq!(json["api_status"], "online"); + assert!( + json["windows"] + .as_array() + .is_some_and(|windows| !windows.is_empty()) + ); +} + +#[test] +fn mock_json_runs_real_cli_binary() { + let test_home = TestHome::new(); + let fixture = fixture_path("tests/fixtures/me.json"); + let output = test_home + .command() + .args(["--mock", fixture.to_str().unwrap(), "--json"]) + .output() + .expect("mock command should run"); + + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let json = stdout_json(&output); + assert_eq!(json["active_endpoint"], "mock"); + assert_eq!(json["source"], "vibemode"); + assert_eq!(json["windows"][1]["window"], "5h"); + assert_eq!(json["windows"][1]["credits"]["used"], 39.0); +} + +#[test] +fn invalid_args_return_error_from_cli() { + let test_home = TestHome::new(); + let fixture = fixture_path("tests/fixtures/me.json"); + let output = test_home + .command() + .args(["--demo", "--mock", fixture.to_str().unwrap()]) + .output() + .expect("invalid command should run"); + + assert_eq!(output.status.code(), Some(2)); + assert!(output.stdout.is_empty()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("--demo and --mock are mutually exclusive")); +} + +#[test] +fn cli_output_does_not_leak_api_keys() { + let test_home = TestHome::new(); + let fixture = fixture_path("tests/fixtures/me.json"); + let secret = "vm_test_secret_123456789"; + let output = test_home + .command() + .env("VIBEMODE_API_KEY", secret) + .args(["--mock", fixture.to_str().unwrap(), "--json"]) + .output() + .expect("mock command with env should run"); + + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(!stdout.contains(secret), "stdout leaked secret: {stdout}"); + assert!(!stderr.contains(secret), "stderr leaked secret: {stderr}"); +} From 25a41693c7301aee75c323794df593454451400a Mon Sep 17 00:00:00 2001 From: xodapi <4956501+xodapi@users.noreply.github.com> Date: Sat, 4 Jul 2026 12:58:07 +0700 Subject: [PATCH 2/2] test(cli): serialize CLI integration tests --- tests/cli_integration.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/cli_integration.rs b/tests/cli_integration.rs index 25bdfaf..358397c 100644 --- a/tests/cli_integration.rs +++ b/tests/cli_integration.rs @@ -1,10 +1,20 @@ use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; +use std::sync::{Mutex, OnceLock}; use std::time::{SystemTime, UNIX_EPOCH}; use serde_json::Value; +static CLI_TEST_LOCK: OnceLock> = OnceLock::new(); + +fn cli_test_lock() -> std::sync::MutexGuard<'static, ()> { + CLI_TEST_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .expect("cli integration test lock should not be poisoned") +} + struct TestHome { root: PathBuf, appdata: PathBuf, @@ -77,6 +87,7 @@ fn stdout_json(output: &std::process::Output) -> Value { #[test] fn demo_json_runs_real_cli_binary() { + let _lock = cli_test_lock(); let test_home = TestHome::new(); let output = test_home .command() @@ -102,6 +113,7 @@ fn demo_json_runs_real_cli_binary() { #[test] fn mock_json_runs_real_cli_binary() { + let _lock = cli_test_lock(); let test_home = TestHome::new(); let fixture = fixture_path("tests/fixtures/me.json"); let output = test_home @@ -124,6 +136,7 @@ fn mock_json_runs_real_cli_binary() { #[test] fn invalid_args_return_error_from_cli() { + let _lock = cli_test_lock(); let test_home = TestHome::new(); let fixture = fixture_path("tests/fixtures/me.json"); let output = test_home @@ -140,6 +153,7 @@ fn invalid_args_return_error_from_cli() { #[test] fn cli_output_does_not_leak_api_keys() { + let _lock = cli_test_lock(); let test_home = TestHome::new(); let fixture = fixture_path("tests/fixtures/me.json"); let secret = "vm_test_secret_123456789";