use super::*;
#[test]
fn index_present_matches_each_shape() {
let strings = serde_json::json!(["other", "trusty-mpm"]);
assert!(index_present(&strings, "trusty-mpm"));
let objects = serde_json::json!({"indexes": [{"id": "trusty-mpm"}]});
assert!(index_present(&objects, "trusty-mpm"));
let named = serde_json::json!([{"name": "trusty-mpm"}]);
assert!(index_present(&named, "trusty-mpm"));
let missing = serde_json::json!(["a", "b"]);
assert!(!index_present(&missing, "trusty-mpm"));
}
#[tokio::test]
async fn memory_unreachable_is_fail() {
unsafe {
std::env::set_var("TRUSTY_MEMORY_ADDR", "127.0.0.1:0");
}
let tmp = tempfile::tempdir().unwrap();
let check = check_memory(tmp.path()).await;
unsafe {
std::env::remove_var("TRUSTY_MEMORY_ADDR");
}
assert_eq!(check.status, CheckStatus::Fail);
}
#[test]
fn expected_search_index_id_derives_from_project_dir_not_hardcoded() {
let tmp = tempfile::tempdir().unwrap();
let other_repo = tmp.path().join("trusty-tools");
std::fs::create_dir_all(other_repo.join(".git")).unwrap();
let other_id = expected_search_index_id(Some(&other_repo));
assert_eq!(other_id, "trusty-tools");
assert_ne!(
other_id, "trusty-mpm",
"must not fall back to the old hardcoded literal for an unrelated project"
);
let mpm_repo = tmp.path().join("trusty-mpm");
std::fs::create_dir_all(mpm_repo.join(".git")).unwrap();
let mpm_id = expected_search_index_id(Some(&mpm_repo));
assert_eq!(mpm_id, "trusty-mpm");
let nested = other_repo.join("crates").join("trusty-mpm");
std::fs::create_dir_all(&nested).unwrap();
assert_eq!(expected_search_index_id(Some(&nested)), "trusty-tools");
}
#[tokio::test]
async fn search_unreachable_is_fail() {
unsafe {
std::env::set_var("TRUSTY_SEARCH_ADDR", "127.0.0.1:0");
}
let tmp = tempfile::tempdir().unwrap();
let check = check_search(tmp.path(), None).await;
unsafe {
std::env::remove_var("TRUSTY_SEARCH_ADDR");
}
assert_eq!(check.status, CheckStatus::Fail);
}
#[tokio::test]
async fn agents_check_probes_the_managed_config_tier_not_the_workspace() {
let project = tempfile::tempdir().unwrap();
let report = run_doctor(Some(project.path()), None, &[]).await;
let agents_check = report
.checks
.iter()
.find(|c| c.name == "agents")
.expect("agents check present");
let workspace_tier = project.path().join(".claude").join("agents");
assert!(
!agents_check
.message
.contains(&workspace_tier.display().to_string()),
"the agents probe must not read the workspace tier any more: {}",
agents_check.message
);
let expected_suffix = std::path::Path::new(".trusty-tools")
.join(crate::core::trusty_tools_config::CRATE_NAME)
.join(crate::core::trusty_tools_config::MANAGED_CLAUDE_CONFIG_SUBDIR)
.join("agents");
assert!(
agents_check
.message
.contains(&expected_suffix.display().to_string()),
"message must name the tm-managed agent deploy tier (…/{}): {}",
expected_suffix.display(),
agents_check.message
);
}
#[tokio::test]
async fn run_doctor_produces_twenty_nine_checks() {
let report = run_doctor(None, None, &[]).await;
let names: Vec<&str> = report.checks.iter().map(|c| c.name.as_str()).collect();
let expected = [
"instructions",
"agents",
"agent_reachability",
"asset_tier",
"transcript_saving",
"skills",
"skill_source",
"output_style",
"output_style_staleness",
"output_style_legacy_ids",
"deployment",
"skill_staleness",
"skill_unmanaged",
"legacy_sources",
"legacy_overrides",
"agent_skills",
"agent_skills_prose_hints",
"memory",
"search",
"worktrees",
"worktree_disk",
"gh_account",
"oauth_token",
"hooks_contamination",
"hooks_foreign_conflict",
"tcc_taint",
"scaffold_tracking",
"push_guard",
"binary_provenance",
];
assert_eq!(names, expected);
assert_eq!(report.checks.len(), expected.len());
}
#[test]
fn build_gh_account_check_single_ok() {
let check =
build_gh_account_check(Some("bobmatnyc".to_string()), vec!["bobmatnyc".to_string()]);
assert_eq!(check.status, CheckStatus::Ok);
assert_eq!(check.name, "gh_account");
assert!(check.message.contains("bobmatnyc"));
}
#[test]
fn build_gh_account_check_multi_warn() {
let check = build_gh_account_check(
Some("bob-duetto".to_string()),
vec!["bob-duetto".to_string(), "bobmatnyc".to_string()],
);
assert_eq!(check.status, CheckStatus::Warn);
assert!(check.message.contains("bob-duetto"));
assert!(check.message.contains("bobmatnyc"));
assert!(check.message.contains("gh auth switch"));
}
#[test]
fn build_gh_account_check_unauthenticated_warn() {
let check = build_gh_account_check(None, Vec::new());
assert_eq!(check.status, CheckStatus::Warn);
assert!(check.message.contains("not authenticated"));
assert!(check.message.contains("gh auth login"));
}
#[test]
fn gh_account_check_is_advisory_only() {
for check in [
build_gh_account_check(Some("a".to_string()), vec!["a".to_string()]),
build_gh_account_check(
Some("a".to_string()),
vec!["a".to_string(), "b".to_string()],
),
build_gh_account_check(None, Vec::new()),
build_gh_account_check(None, vec!["a".to_string()]),
] {
assert_ne!(
check.status,
CheckStatus::Fail,
"gh_account must never Fail"
);
}
}
#[test]
fn oauth_token_check_warns_when_relocated_with_no_token_or_key() {
let check = build_oauth_token_check(true, false, false);
assert_eq!(check.status, CheckStatus::Warn);
assert!(check.message.contains("tm auth set-token"));
assert!(check.message.contains("claude setup-token"));
}
#[test]
fn oauth_token_check_ok_when_token_stored() {
let check = build_oauth_token_check(true, true, false);
assert_eq!(check.status, CheckStatus::Ok);
}
#[test]
fn oauth_token_check_ok_when_env_var_set() {
let check = build_oauth_token_check(true, false, true);
assert_eq!(check.status, CheckStatus::Ok);
}
#[test]
fn oauth_token_check_warns_when_only_api_key_set() {
let check = build_oauth_token_check(true, false, false);
assert_eq!(
check.status,
CheckStatus::Warn,
"an ambient ANTHROPIC_API_KEY must NOT count as managed-session auth"
);
}
#[test]
fn oauth_token_check_ok_when_not_relocated() {
let check = build_oauth_token_check(false, false, false);
assert_eq!(check.status, CheckStatus::Ok);
}
#[tokio::test]
async fn worktrees_no_repos_root_is_ok() {
let check = check_worktrees(None, &[]).await;
assert_eq!(check.status, CheckStatus::Ok);
assert!(check.message.contains("no managed workspace"));
}
#[tokio::test]
async fn worktrees_no_orphans_is_ok() {
let fx = crate::session_manager::worktree_git_fixture::GitWorktreeFixture::new();
let wt = fx.add_worktree("session-abc");
let check = check_worktrees(Some(&fx.repos_root), &[wt]).await;
assert_eq!(check.status, CheckStatus::Ok);
}
#[tokio::test]
async fn worktrees_with_orphan_is_warn() {
let fx = crate::session_manager::worktree_git_fixture::GitWorktreeFixture::new();
let live = fx.add_worktree("session-live");
let _dead = fx.add_worktree("session-dead");
let check = check_worktrees(Some(&fx.repos_root), &[live]).await;
assert_eq!(check.status, CheckStatus::Warn);
assert!(check.message.contains("1 orphaned"));
assert!(check.message.contains("tm session prune --worktrees"));
}
async fn spawn_health_listener(body: serde_json::Value, delay: std::time::Duration) -> String {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
tokio::spawn(async move {
loop {
let Ok((mut sock, _)) = listener.accept().await else {
return;
};
let body = body.to_string();
tokio::spawn(async move {
let mut buf = [0u8; 1024];
let _ = sock.read(&mut buf).await;
tokio::time::sleep(delay).await;
let resp = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
let _ = sock.write_all(resp.as_bytes()).await;
let _ = sock.flush().await;
});
}
});
addr
}
fn healthy_body() -> serde_json::Value {
serde_json::json!({
"status": "ok",
"daemon_state": "ready",
"worker": {"in_flight": 0, "wedged": false},
})
}
#[tokio::test]
async fn memory_wedged_worker_pool_is_not_ok() {
let body = serde_json::json!({
"status": "ok",
"daemon_state": "ready",
"worker": {"in_flight": 6, "oldest_age_secs": 1800, "wedged": true},
});
let addr = spawn_health_listener(body, std::time::Duration::ZERO).await;
let check = probe_health(
"memory",
"trusty-memory",
&format!("http://{addr}/health"),
&addr,
)
.await;
assert_ne!(
check.status,
CheckStatus::Ok,
"a live listener over a wedged worker pool must NOT report healthy: {}",
check.message
);
assert_eq!(check.status, CheckStatus::Fail);
assert!(
check.message.contains("WEDGED"),
"message must name the wedge: {}",
check.message
);
assert!(
check.message.contains("1800"),
"message must carry the observed age: {}",
check.message
);
}
#[tokio::test]
async fn memory_slow_but_serving_daemon_is_ok() {
let addr = spawn_health_listener(healthy_body(), std::time::Duration::from_millis(2500)).await;
let check = probe_health(
"memory",
"trusty-memory",
&format!("http://{addr}/health"),
&addr,
)
.await;
assert_eq!(
check.status,
CheckStatus::Ok,
"a serving daemon that is merely slow must not be reported unreachable: {}",
check.message
);
assert!(
!check.message.contains("unreachable"),
"message must not claim unreachable: {}",
check.message
);
}
#[tokio::test]
async fn memory_warming_is_warn_not_fail() {
let body = serde_json::json!({
"status": "ok",
"daemon_state": "warming",
"worker": {"in_flight": 1, "oldest_age_secs": 2, "wedged": false},
});
let addr = spawn_health_listener(body, std::time::Duration::ZERO).await;
let check = probe_health(
"memory",
"trusty-memory",
&format!("http://{addr}/health"),
&addr,
)
.await;
assert_eq!(
check.status,
CheckStatus::Warn,
"post-restart warm-up must be a warning, not a failure: {}",
check.message
);
assert!(
check.message.contains("WARMING"),
"message must name the warm-up: {}",
check.message
);
}
#[tokio::test]
async fn memory_timeout_is_unknown_not_fail() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
tokio::spawn(async move {
let mut held = Vec::new();
while let Ok((sock, _)) = listener.accept().await {
held.push(sock); }
});
let check = tokio::time::timeout(
std::time::Duration::from_secs(90),
probe_health(
"memory",
"trusty-memory",
&format!("http://{addr}/health"),
&addr,
),
)
.await
.expect("probe must stay bounded");
assert_eq!(
check.status,
CheckStatus::Unknown,
"a timed-out probe must be UNKNOWN, not a verdict: {}",
check.message
);
assert!(
check.message.contains("could not be determined"),
"message must say so plainly: {}",
check.message
);
}
#[tokio::test]
async fn health_body_without_worker_block_is_unknown() {
let body = serde_json::json!({"status": "ok", "daemon_state": "ready"});
let addr = spawn_health_listener(body, std::time::Duration::ZERO).await;
let check = probe_health(
"memory",
"trusty-memory",
&format!("http://{addr}/health"),
&addr,
)
.await;
assert_eq!(
check.status,
CheckStatus::Unknown,
"no worker observation means health is undetermined: {}",
check.message
);
}
#[test]
fn unknown_never_aggregates_to_ok() {
let report = DoctorReport::from_checks(vec![
DoctorCheck::new("a", CheckStatus::Ok, "fine"),
DoctorCheck::new("b", CheckStatus::Unknown, "no idea"),
]);
assert_eq!(report.overall, CheckStatus::Unknown);
let with_warn = DoctorReport::from_checks(vec![
DoctorCheck::new("a", CheckStatus::Warn, "meh"),
DoctorCheck::new("b", CheckStatus::Unknown, "no idea"),
]);
assert_eq!(with_warn.overall, CheckStatus::Unknown);
let with_fail = DoctorReport::from_checks(vec![
DoctorCheck::new("a", CheckStatus::Unknown, "no idea"),
DoctorCheck::new("b", CheckStatus::Fail, "broken"),
]);
assert_eq!(with_fail.overall, CheckStatus::Fail);
}