#![cfg(unix)]
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use codewhale_secrets::{FileKeyringStore, KeyringStore};
use tempfile::TempDir;
#[test]
fn dispatcher_diagnostics_are_in_process_and_read_only() {
for (args, expects_entries_json) in [
(&["doctor"][..], false),
(&["doctor", "--json"][..], false),
(&["doctor", "--context-json"][..], true),
(&["setup", "--status"][..], false),
] {
let fixture = TempDir::new().expect("fixture root");
let sealed_home = fixture.path().join("sealed-home");
let codewhale_home = fixture.path().join("sealed-codewhale-home");
let primary_home = sealed_home.join(".codewhale");
let legacy = sealed_home
.join(".deepseek")
.join("secrets")
.join("secrets.json");
let legacy_settings = sealed_home.join(".deepseek").join("settings.toml");
let legacy_settings_bytes = b"default_mode = \"plan\"\n";
FileKeyringStore::new(&legacy)
.set("deepseek", "synthetic-legacy-fixture")
.expect("seed synthetic legacy store");
fs::write(&legacy_settings, legacy_settings_bytes).expect("seed legacy settings");
let before_paths = relative_paths(&sealed_home);
let before_legacy = fs::read(&legacy).expect("read synthetic legacy store");
let output = Command::new(codewhale_binary())
.args(args)
.env_clear()
.env("HOME", &sealed_home)
.env("USERPROFILE", &sealed_home)
.env("CODEWHALE_HOME", &codewhale_home)
.env("CODEWHALE_SECRET_BACKEND", "file")
.output()
.expect("run dispatcher diagnostic");
assert!(
output.status.success(),
"dispatcher {args:?} failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
if expects_entries_json {
let report: serde_json::Value = serde_json::from_slice(&output.stdout)
.unwrap_or_else(|error| {
panic!(
"doctor --context-json must emit a machine-readable context source map: {error}\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
});
assert!(
report["entries"].is_array(),
"doctor --context-json must carry an `entries` array\nstdout:\n{}",
String::from_utf8_lossy(&output.stdout)
);
}
assert_eq!(
relative_paths(&sealed_home),
before_paths,
"dispatcher {args:?} must not create or migrate state below HOME"
);
assert_eq!(
fs::read(&legacy).expect("read synthetic legacy store after diagnostic"),
before_legacy,
"dispatcher {args:?} must not rewrite the legacy store"
);
assert_eq!(
fs::read(&legacy_settings).expect("read legacy settings after diagnostic"),
legacy_settings_bytes,
"dispatcher {args:?} must not rewrite legacy settings"
);
assert!(
!primary_home.exists(),
"dispatcher {args:?} must not create a primary Codewhale home or migrated state"
);
assert!(
!codewhale_home.exists(),
"dispatcher {args:?} must not create an explicit CODEWHALE_HOME"
);
}
}
fn relative_paths(root: &Path) -> Vec<PathBuf> {
let mut paths = Vec::new();
collect_relative_paths(root, root, &mut paths);
paths.sort();
paths
}
fn collect_relative_paths(root: &Path, current: &Path, paths: &mut Vec<PathBuf>) {
let entries = fs::read_dir(current).expect("read synthetic state directory");
for entry in entries {
let entry = entry.expect("synthetic state directory entry");
let path = entry.path();
paths.push(
path.strip_prefix(root)
.expect("synthetic path below root")
.to_path_buf(),
);
if entry.file_type().expect("synthetic entry type").is_dir() {
collect_relative_paths(root, &path, paths);
}
}
}
fn codewhale_binary() -> PathBuf {
if let Some(path) = option_env!("CARGO_BIN_EXE_codewhale") {
return PathBuf::from(path);
}
if let Ok(path) = std::env::var("CARGO_BIN_EXE_codewhale") {
return PathBuf::from(path);
}
let mut path = std::env::current_exe().expect("current test executable path");
path.pop();
if path.ends_with("deps") {
path.pop();
}
path.push(format!("codewhale{}", std::env::consts::EXE_SUFFIX));
path
}