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, &[], 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 unmanaged_cwd_audits_the_operator_home_tier() {
let project = tempfile::tempdir().unwrap();
let report = run_doctor(Some(project.path()), None, &[], None).await;
let skills = report
.checks
.iter()
.find(|c| c.name == "skills")
.expect("skills check present");
let workspace_tier = project.path().join(".claude").join("skills");
assert!(
!skills
.message
.contains(&workspace_tier.display().to_string()),
"an unregistered directory is not a managed workspace and must not be \
probed as one: {}",
skills.message
);
}
#[tokio::test]
async fn a_registered_workspace_still_gets_the_workspace_layout() {
let project = tempfile::tempdir().unwrap();
let active = vec![project.path().to_path_buf()];
let report = run_doctor(Some(project.path()), None, &active, None).await;
let skills = report
.checks
.iter()
.find(|c| c.name == "skills")
.expect("skills check present");
let workspace_tier = project.path().join(".claude").join("skills");
assert!(
skills
.message
.contains(&workspace_tier.display().to_string()),
"a registered workspace must still be probed at its own tier: {}",
skills.message
);
}
#[test]
fn is_managed_workspace_matches_only_a_registered_path() {
let registered = tempfile::tempdir().unwrap();
let other = tempfile::tempdir().unwrap();
let active = vec![registered.path().to_path_buf()];
assert!(is_managed_workspace(registered.path(), &active));
assert!(!is_managed_workspace(other.path(), &active));
assert!(
!is_managed_workspace(registered.path(), &[]),
"with no live sessions nothing is a managed workspace"
);
}
#[test]
fn is_managed_workspace_sees_through_a_symlinked_path() {
let real = tempfile::tempdir().unwrap();
let link_parent = tempfile::tempdir().unwrap();
let link = link_parent.path().join("workspace-link");
#[cfg(unix)]
std::os::unix::fs::symlink(real.path(), &link).unwrap();
#[cfg(not(unix))]
return;
let active = vec![real.path().to_path_buf()];
assert!(
is_managed_workspace(&link, &active),
"a symlink to a registered workspace is that workspace"
);
}
#[test]
fn an_uncanonicalizable_path_is_not_a_managed_workspace() {
let registered = tempfile::tempdir().unwrap();
let scratch = tempfile::tempdir().unwrap();
let active = vec![registered.path().to_path_buf()];
let absent = scratch.path().join("never-created");
assert!(
!is_managed_workspace(&absent, &active),
"a directory that is not on disk is not a provisioned workspace"
);
#[cfg(unix)]
{
let broken = scratch.path().join("dangling-link");
std::os::unix::fs::symlink(scratch.path().join("no-such-target"), &broken).unwrap();
assert!(
!is_managed_workspace(&broken, &active),
"a symlink to nothing resolves to nothing, not to a workspace"
);
}
let stale = vec![scratch.path().join("reaped-workspace")];
assert!(
!is_managed_workspace(registered.path(), &stale),
"a stale recorded path must not match a live, unrelated directory"
);
}
#[cfg(unix)]
#[test]
fn an_unreadable_directory_is_not_a_managed_workspace() {
use std::os::unix::fs::PermissionsExt;
let registered = tempfile::tempdir().unwrap();
let scratch = tempfile::tempdir().unwrap();
let locked = scratch.path().join("locked");
let inner = locked.join("workspace");
std::fs::create_dir_all(&inner).unwrap();
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).unwrap();
if std::fs::canonicalize(&inner).is_ok() {
eprintln!("skipping: cannot deny traversal on this platform/privilege level");
let _ = std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755));
return;
}
let managed = is_managed_workspace(&inner, &[registered.path().to_path_buf()]);
let _ = std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755));
assert!(
!managed,
"a directory tm cannot even resolve is not one to deploy into"
);
}
#[test]
fn an_absent_path_still_matches_the_recorded_spelling_of_itself() {
let scratch = tempfile::tempdir().unwrap();
let reaped = scratch.path().join("reaped-workspace");
assert!(
is_managed_workspace(&reaped, std::slice::from_ref(&reaped)),
"an unresolvable path is compared verbatim, so it matches only its own \
recorded spelling — never an unregistered directory"
);
}
#[tokio::test]
async fn run_doctor_produces_thirty_two_checks() {
let report = run_doctor(None, 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",
"search_index_pin",
"worktrees",
"worktree_disk",
"gh_account",
"oauth_token",
"hooks_contamination",
"hooks_foreign_conflict",
"tcc_taint",
"scaffold_tracking",
"push_guard",
"binary_provenance",
"session_store",
"stray_mcp_json",
];
assert_eq!(names, expected);
assert_eq!(report.checks.len(), expected.len());
}
#[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);
}
fn counts_for(repos_root: &std::path::Path) -> WorktreeOrphanCounts {
let report = crate::session_manager::worktree_reconcile::reconcile_worktrees(
repos_root,
&[],
&[],
chrono::Utc::now(),
);
WorktreeOrphanCounts::from_reconcile(&report)
}
#[tokio::test]
async fn worktrees_no_repos_root_is_ok() {
let check = check_worktrees(None, 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), Some(counts_for(&fx.repos_root))).await;
assert_eq!(check.status, CheckStatus::Ok, "{}", check.message);
}
#[tokio::test]
async fn worktrees_unowned_worktree_is_not_an_orphan() {
let fx = crate::session_manager::worktree_git_fixture::GitWorktreeFixture::new();
let _a = fx.add_worktree("session-live");
let _b = fx.add_worktree("session-dead");
let counts = counts_for(&fx.repos_root);
assert_eq!(counts.orphaned, 0, "reconcile must report zero orphans");
let check = check_worktrees(Some(&fx.repos_root), Some(counts)).await;
assert_eq!(check.status, CheckStatus::Ok, "{}", check.message);
assert!(check.message.contains("no orphaned worktrees found"));
}
#[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");
crate::session_manager::worktree_git_fixture::GitWorktreeFixture::stamp_reclaimable_sentinel(
&dead,
);
let check = check_worktrees(Some(&fx.repos_root), Some(counts_for(&fx.repos_root))).await;
assert_eq!(check.status, CheckStatus::Warn, "{}", check.message);
assert!(check.message.contains("1 orphaned"));
assert!(check.message.contains(WORKTREE_REMEDIATION_COMMAND));
assert!(
!check.message.contains("prune --worktrees"),
"the nonexistent flag must not come back: {}",
check.message
);
}
#[tokio::test]
async fn worktrees_without_a_reconciled_inventory_is_unknown() {
let fx = crate::session_manager::worktree_git_fixture::GitWorktreeFixture::new();
let check = check_worktrees(Some(&fx.repos_root), None).await;
assert_eq!(check.status, CheckStatus::Unknown, "{}", check.message);
assert!(check.message.contains("not established"));
}
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);
}