use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::download::{
cache_file_usable, purge_corrupt_cache_files_in, verify_cache_file, CacheIntegrity,
};
const KNOWN_STATE_FILES: &[&str] = &[
"messaging.json",
"models.json",
"connectors.json",
"car-connectors.json",
"agents.json",
"routing.json",
"declagents.json",
"lane-defaults.json",
"update-prefs.json",
"upgrade-cache.json",
"catalog-cache.json",
"discovered_models.json",
"a2a-peers.json",
"external-agents.jsonl",
"nudge-state.json",
"benchmark_priors.json",
"key_pool_stats.json",
"model_profiles.json",
"agent-permissions.json",
"version.json",
"secret_index.json",
"gateway-state.json",
"parslee-credential-state.json",
"model-resource-policy.json",
"parslee-auth-authority.json",
"messages-inbound-watermark.json",
"catalog-status.json",
"identity.json",
"work-contexts.json",
"agent-handshakes.json",
"catalog-retained.json",
"catalog-revoked.json",
"catalog-revoked-models.json",
"fleet-worker.json",
"model-resource-policy.concierge.json",
"chat-goals.json",
"memory.json",
];
const KNOWN_NON_JSON_FILES: &[&str] = &[
"env",
".env-retired",
".env-migration.json",
"peer-identity.key",
"coder.toml",
"concierge-auto-since",
"host-state.json",
"label-secret",
"config.toml",
"coder-trust.toml",
"heal.toml",
"host-multiplayer-stages.json",
"host-state.json.corrupt",
];
const KNOWN_DIRS: &[&str] = &[
"secrets",
"models",
"journals",
"logs",
"agents",
"runs",
"run",
"workflow-runs",
"workflows",
"tasks",
"trajectories",
"registry",
"meetings",
"speech-runtime",
"visual-runtime",
"coder",
"projects",
"memory",
"reason",
"doctor",
"bin",
"voiceprints",
"sync",
"proposal-completed-index",
"selfheal",
"policies",
"commons",
"topology",
"feedback-outbox",
"browser-profile",
"browser-profile-user",
".car-model-management",
"model-management",
"lattice",
"peer-certs",
"work-contexts",
"tools",
"onnxruntime",
"agent-log-scrubs",
"workers",
"bundles",
"labels",
"agent-refusals",
"spend",
"agent-runs",
"ai.parslee.car",
"agent-starts",
"ui-sign-ins",
"credentials",
"fleet-worker",
"proposal-finalization",
"proposal-execution",
"proposal-retry-rollbacks",
"proposal-id-claims",
"proposal-completed",
"CarHost",
"workflow-dedup",
"policies",
"policy-hook-sessions",
];
const TOLERATED_SUFFIXES: &[&str] = &[".lock", ".tmp", ".bak", ".bin", ".jsonl"];
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionStamp {
pub car_version: String,
pub state_schema_version: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub previous_car_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub previous_state_schema_version: Option<u32>,
}
pub const STATE_SCHEMA_VERSION: u32 = 1;
impl VersionStamp {
pub fn current() -> Self {
VersionStamp {
car_version: env!("CARGO_PKG_VERSION").to_string(),
state_schema_version: STATE_SCHEMA_VERSION,
previous_car_version: None,
previous_state_schema_version: None,
}
}
pub fn succeeding(existing: Option<&VersionStamp>) -> Self {
let mut next = VersionStamp::current();
if let Some(prior) = existing {
if prior.car_version != next.car_version
|| prior.state_schema_version != next.state_schema_version
{
next.previous_car_version = Some(prior.car_version.clone());
next.previous_state_schema_version = Some(prior.state_schema_version);
} else {
next.previous_car_version = prior.previous_car_version.clone();
next.previous_state_schema_version = prior.previous_state_schema_version;
}
}
next
}
}
#[derive(Debug, Clone)]
pub struct StampTransition {
pub previous: Option<VersionStamp>,
pub current: VersionStamp,
}
impl StampTransition {
pub fn upgraded(&self) -> bool {
self.previous
.as_ref()
.is_some_and(|p| p.car_version != self.current.car_version)
}
pub fn schema_from_the_future(&self) -> bool {
self.previous
.as_ref()
.is_some_and(|p| p.state_schema_version > self.current.state_schema_version)
}
}
pub fn car_home() -> PathBuf {
::car_home::root_or_relative()
}
pub fn write_version_stamp(car_home: &Path) -> std::io::Result<()> {
stamp_version(car_home).map(|_| ())
}
pub fn stamp_version(car_home: &Path) -> std::io::Result<StampTransition> {
std::fs::create_dir_all(car_home)?;
let previous = read_version_stamp(car_home);
let stamp = VersionStamp::succeeding(previous.as_ref());
let json = serde_json::to_string_pretty(&stamp)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let tmp = car_home.join(format!("version.json.{}.tmp", std::process::id()));
std::fs::write(&tmp, json)?;
std::fs::rename(&tmp, car_home.join("version.json"))?;
Ok(StampTransition {
previous,
current: stamp,
})
}
const DOCTOR_DIR: &str = "doctor";
const DAEMON_TMPDIR_MARKER_FILE: &str = "daemon-tmpdir.json";
const DAEMON_AUTH_MODE_FILE: &str = "daemon-auth-mode.json";
const TMPDIR_PROBE_BYTES: &[u8] = b"car tmpdir probe\n";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DaemonTmpdirMarker {
pub pid: u32,
pub booted_at_unix: u64,
pub car_version: String,
pub checked_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub checked_path_absolute: Option<String>,
pub ok: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "status")]
pub enum DaemonTmpdirStatus {
Current { marker: DaemonTmpdirMarker },
CurrentPathGone { marker: DaemonTmpdirMarker },
CurrentPathRecheckFailed {
marker: DaemonTmpdirMarker,
error: String,
},
Stale { marker: DaemonTmpdirMarker },
Unverified { marker: DaemonTmpdirMarker },
Unreadable { error: String },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DaemonTmpdirCheck {
pub marker_path: String,
#[serde(flatten)]
pub status: DaemonTmpdirStatus,
}
impl DaemonTmpdirCheck {
pub fn is_failing(&self) -> bool {
matches!(
&self.status,
DaemonTmpdirStatus::Current { marker } if !marker.ok
) || matches!(
&self.status,
DaemonTmpdirStatus::CurrentPathGone { .. }
| DaemonTmpdirStatus::CurrentPathRecheckFailed { .. }
)
}
}
pub fn daemon_tmpdir_marker_path(car_home: &Path) -> PathBuf {
car_home.join(DOCTOR_DIR).join(DAEMON_TMPDIR_MARKER_FILE)
}
pub fn record_daemon_tmpdir_probe(
car_home: &Path,
tmpdir: &Path,
) -> std::io::Result<DaemonTmpdirMarker> {
record_daemon_tmpdir_probe_result(car_home, tmpdir, probe_daemon_tmpdir(tmpdir))
}
pub fn probe_daemon_tmpdir(tmpdir: &Path) -> Result<Option<String>, String> {
probe_tmpdir(tmpdir, std::process::id())
}
pub fn record_daemon_tmpdir_probe_result(
car_home: &Path,
tmpdir: &Path,
result: Result<Option<String>, String>,
) -> std::io::Result<DaemonTmpdirMarker> {
record_daemon_tmpdir_probe_result_with_cwd(car_home, tmpdir, result, std::env::current_dir())
}
fn record_daemon_tmpdir_probe_result_with_cwd(
car_home: &Path,
tmpdir: &Path,
result: Result<Option<String>, String>,
current_dir: std::io::Result<PathBuf>,
) -> std::io::Result<DaemonTmpdirMarker> {
let (error, mut note) = match result {
Ok(note) => (None, note),
Err(error) => (Some(error), None),
};
let (checked_path_absolute, path_note) = exact_absolute_tmpdir_path(tmpdir, current_dir);
append_note(&mut note, path_note);
let marker = DaemonTmpdirMarker {
pid: std::process::id(),
booted_at_unix: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
car_version: env!("CARGO_PKG_VERSION").to_string(),
checked_path: tmpdir.display().to_string(),
checked_path_absolute,
ok: error.is_none(),
error,
note,
};
write_daemon_tmpdir_marker(car_home, &marker)?;
Ok(marker)
}
fn exact_absolute_tmpdir_path(
tmpdir: &Path,
current_dir: std::io::Result<PathBuf>,
) -> (Option<String>, Option<String>) {
let absolute = if tmpdir.is_absolute() {
tmpdir.to_path_buf()
} else {
let current_dir = match current_dir {
Ok(current_dir) => current_dir,
Err(error) => {
return (
None,
Some(format!(
"TMPDIR recheck skipped: could not capture the daemon working directory: {error}"
)),
);
}
};
current_dir.join(tmpdir)
};
match absolute.to_str() {
Some(path) => (Some(path.to_string()), None),
None => (
None,
Some(
"TMPDIR recheck skipped: the exact absolute probe path is not valid UTF-8"
.to_string(),
),
),
}
}
fn append_note(note: &mut Option<String>, additional: Option<String>) {
let Some(additional) = additional else {
return;
};
match note {
Some(note) => {
note.push_str("; ");
note.push_str(&additional);
}
None => *note = Some(additional),
}
}
pub fn read_daemon_tmpdir_marker(car_home: &Path) -> Option<Result<DaemonTmpdirMarker, String>> {
let text = match std::fs::read_to_string(daemon_tmpdir_marker_path(car_home)) {
Ok(text) => text,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return None,
Err(e) => return Some(Err(e.to_string())),
};
Some(serde_json::from_str(&text).map_err(|e| e.to_string()))
}
fn unique_probe_suffix(pid: u32) -> String {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
format!("{pid}.{nanos}")
}
fn probe_tmpdir(tmpdir: &Path, pid: u32) -> Result<Option<String>, String> {
use std::io::Write;
let path = tmpdir.join(format!(".car-tmpdir-probe.{}", unique_probe_suffix(pid)));
let mut options = std::fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
use windows::Win32::Storage::FileSystem::FILE_FLAG_DELETE_ON_CLOSE;
options.custom_flags(FILE_FLAG_DELETE_ON_CLOSE.0);
}
let mut file = options
.open(&path)
.map_err(|e| format!("create failed: {e}"))?;
let created = file.metadata().map_err(|e| format!("stat failed: {e}"));
let wrote = file
.write_all(TMPDIR_PROBE_BYTES)
.map_err(|e| format!("write failed: {e}"));
drop(file);
let removed = cleanup_probe_file(&path, &created);
combine_probe_results(created.map(|_| ()), wrote, removed)
}
fn cleanup_probe_file(
path: &Path,
created: &Result<std::fs::Metadata, String>,
) -> Result<(), String> {
match created {
Ok(created) => remove_probe_file(path, created),
#[cfg(windows)]
Err(_) => Ok(()),
#[cfg(not(windows))]
Err(_) => Err(format!(
"cleanup skipped: probe file identity could not be proven; left {}",
path.display()
)),
}
}
fn combine_probe_results(
stat: Result<(), String>,
write: Result<(), String>,
cleanup: Result<(), String>,
) -> Result<Option<String>, String> {
match (stat, write, cleanup) {
(Ok(()), Ok(()), Ok(())) => Ok(None),
(Err(stat), Ok(()), Ok(())) => Ok(Some(stat)),
(Err(stat), Ok(()), Err(cleanup)) => Ok(Some(format!("{stat}; {cleanup}"))),
(Ok(()), Ok(()), Err(cleanup)) => Err(cleanup),
(stat, Err(write), cleanup) => {
let mut errors = Vec::new();
if let Err(stat) = stat {
errors.push(stat);
}
errors.push(write);
if let Err(cleanup) = cleanup {
errors.push(cleanup);
}
Err(errors.join("; "))
}
}
}
#[cfg(unix)]
fn remove_probe_file(path: &Path, created: &std::fs::Metadata) -> Result<(), String> {
use std::os::unix::fs::MetadataExt;
let on_disk = std::fs::symlink_metadata(path).map_err(|e| format!("remove failed: {e}"))?;
if !on_disk.file_type().is_file()
|| created.dev() != on_disk.dev()
|| created.ino() != on_disk.ino()
{
return Err(
"remove skipped: the probe path no longer names the file the probe created".to_string(),
);
}
std::fs::remove_file(path).map_err(|e| format!("remove failed: {e}"))
}
#[cfg(not(unix))]
fn remove_probe_file(_path: &Path, _created: &std::fs::Metadata) -> Result<(), String> {
Ok(())
}
fn write_daemon_tmpdir_marker(car_home: &Path, marker: &DaemonTmpdirMarker) -> std::io::Result<()> {
write_doctor_record(car_home, DAEMON_TMPDIR_MARKER_FILE, marker.pid, marker)
}
fn write_doctor_record<T: Serialize>(
car_home: &Path,
file_name: &str,
pid: u32,
record: &T,
) -> std::io::Result<()> {
use std::io::Write;
let dir = car_home.join(DOCTOR_DIR);
car_secrets::ensure_private_dir(&dir)?;
let mut json = serde_json::to_vec_pretty(record)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
json.push(b'\n');
let tmp = dir.join(format!("{file_name}.{}.tmp", unique_probe_suffix(pid)));
let mut file = car_secrets::create_private_file(&tmp)?;
file.write_all(&json)?;
car_secrets::atomic_replace_private_file(&tmp, &dir.join(file_name))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DaemonAuthModeMarker {
pub pid: u32,
pub booted_at_unix: u64,
pub car_version: String,
pub single_principal: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cause: Option<String>,
}
pub fn daemon_auth_mode_marker_path(car_home: &Path) -> PathBuf {
car_home.join(DOCTOR_DIR).join(DAEMON_AUTH_MODE_FILE)
}
pub fn record_daemon_auth_mode(
car_home: &Path,
single_principal: bool,
cause: Option<String>,
) -> std::io::Result<DaemonAuthModeMarker> {
let marker = DaemonAuthModeMarker {
pid: std::process::id(),
booted_at_unix: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
car_version: env!("CARGO_PKG_VERSION").to_string(),
single_principal,
cause: if single_principal { cause } else { None },
};
write_doctor_record(car_home, DAEMON_AUTH_MODE_FILE, marker.pid, &marker)?;
Ok(marker)
}
pub fn running_daemon_is_single_principal(car_home: &Path) -> bool {
running_daemon_host_role_unavailable(car_home).is_some()
}
pub fn running_daemon_host_role_unavailable(car_home: &Path) -> Option<Option<String>> {
use std::io::Read;
let mut text = String::new();
let read = car_secrets::open_private_read(&daemon_auth_mode_marker_path(car_home))
.and_then(|mut file| file.read_to_string(&mut text));
if read.is_err() {
return None;
}
let marker = serde_json::from_str::<DaemonAuthModeMarker>(&text).ok()?;
(marker.single_principal && daemon_writer_alive(marker.pid) == Some(true))
.then_some(marker.cause)
}
fn classify_daemon_tmpdir_marker(
marker: DaemonTmpdirMarker,
writer_alive: Option<bool>,
) -> DaemonTmpdirStatus {
match writer_alive {
Some(true) => DaemonTmpdirStatus::Current { marker },
Some(false) => DaemonTmpdirStatus::Stale { marker },
None => DaemonTmpdirStatus::Unverified { marker },
}
}
fn daemon_writer_alive(pid: u32) -> Option<bool> {
process_alive(pid)
}
pub fn process_alive(pid: u32) -> Option<bool> {
#[cfg(unix)]
{
Some(unix_pid_alive(pid))
}
#[cfg(windows)]
{
Some(windows_pid_alive(pid))
}
#[cfg(not(any(unix, windows)))]
{
let _ = pid;
None
}
}
#[cfg(unix)]
fn unix_pid_alive(pid: u32) -> bool {
let Ok(pid) = libc::pid_t::try_from(pid) else {
return false;
};
if pid <= 0 {
return false;
}
let rc = unsafe { libc::kill(pid, 0) };
rc == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
}
#[cfg(windows)]
fn windows_pid_alive(pid: u32) -> bool {
use windows::Win32::Foundation::{CloseHandle, ERROR_ACCESS_DENIED, STILL_ACTIVE};
use windows::Win32::System::Threading::{
GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
};
if pid == 0 {
return false;
}
let handle = match unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) } {
Ok(handle) => handle,
Err(error) if error.code() == ERROR_ACCESS_DENIED.to_hresult() => return true,
Err(_) => return false,
};
let mut exit_code = 0u32;
let queried = unsafe { GetExitCodeProcess(handle, &mut exit_code) };
let _ = unsafe { CloseHandle(handle) };
queried.is_ok() && exit_code == STILL_ACTIVE.0 as u32
}
fn recheck_current_tmpdir_with<F>(status: DaemonTmpdirStatus, stat: F) -> DaemonTmpdirStatus
where
F: FnOnce(&Path) -> std::io::Result<()>,
{
let mut marker = match status {
DaemonTmpdirStatus::Current { marker } => marker,
status => return status,
};
if !marker.ok {
return DaemonTmpdirStatus::Current { marker };
}
let Some(exact_path) = marker.checked_path_absolute.clone() else {
append_note(
&mut marker.note,
Some(
"TMPDIR recheck skipped: the startup record has no exact absolute probe path"
.to_string(),
),
);
return DaemonTmpdirStatus::Current { marker };
};
match stat(Path::new(&exact_path)) {
Ok(()) => DaemonTmpdirStatus::Current { marker },
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
DaemonTmpdirStatus::CurrentPathGone { marker }
}
Err(error) => DaemonTmpdirStatus::CurrentPathRecheckFailed {
marker,
error: error.to_string(),
},
}
}
fn check_daemon_tmpdir(home: &Path) -> Option<DaemonTmpdirCheck> {
let status = match read_daemon_tmpdir_marker(home)? {
Ok(marker) => {
let writer_alive = daemon_writer_alive(marker.pid);
let status = classify_daemon_tmpdir_marker(marker, writer_alive);
recheck_current_tmpdir_with(status, |path| std::fs::metadata(path).map(|_| ()))
}
Err(error) => DaemonTmpdirStatus::Unreadable { error },
};
Some(DaemonTmpdirCheck {
marker_path: daemon_tmpdir_marker_path(home).display().to_string(),
status,
})
}
#[derive(Debug, Clone, Default)]
pub struct DoctorOptions {
pub deep: bool,
pub repair: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "status")]
pub enum StateFileStatus {
Absent,
Ok { schema_version: Option<u32> },
Unparseable {
error: String,
backed_up_to: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateFileCheck {
pub name: String,
#[serde(flatten)]
pub status: StateFileStatus,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "status")]
pub enum ModelStatus {
Healthy,
Corrupt {
bad_files: Vec<String>,
purged: usize,
},
Incomplete {
detail: String,
#[serde(default)]
purged: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
superseded_by: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Leftover {
pub path: String,
pub bytes: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub age_secs: Option<u64>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub reclaimable: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelCheck {
pub name: String,
#[serde(flatten)]
pub status: ModelStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeCheck {
pub name: String,
pub root: String,
pub present: bool,
pub interpreter_ok: bool,
}
impl RuntimeCheck {
pub fn is_broken(&self) -> bool {
self.present && !self.interpreter_ok
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DoctorReport {
pub car_home: String,
pub binary_version: String,
pub on_disk_stamp: Option<VersionStamp>,
pub version_skew: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub carried_from_version: Option<String>,
#[serde(default)]
pub schema_from_the_future: bool,
pub state_files: Vec<StateFileCheck>,
pub models: Vec<ModelCheck>,
#[serde(default)]
pub installed_models: usize,
#[serde(default)]
pub leftovers: Vec<Leftover>,
#[serde(default)]
pub revoked_copies: Vec<crate::retire::RevokedCopy>,
#[serde(default)]
pub catalog: crate::catalog::CatalogFreshness,
#[serde(default)]
pub runtimes: Vec<RuntimeCheck>,
pub unrecognized: Vec<String>,
pub repairs: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub daemon_tmpdir: Option<DaemonTmpdirCheck>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub daemon_single_principal: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub daemon_host_role_cause: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub coder_state: Option<CoderStateUsage>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CoderStateUsage {
pub path: String,
pub bytes: u64,
pub files: u64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub unreadable: Vec<String>,
}
impl CoderStateUsage {
pub fn is_complete(&self) -> bool {
self.unreadable.is_empty()
}
}
pub fn measure_coder_state(home: &Path) -> Option<CoderStateUsage> {
let root = home.join("coder");
let meta = std::fs::symlink_metadata(&root).ok()?;
let mut usage = CoderStateUsage {
path: root.display().to_string(),
bytes: 0,
files: 0,
unreadable: Vec::new(),
};
if !meta.is_dir() {
usage.bytes = meta.len();
usage.files = 1;
return Some(usage);
}
let mut stack = vec![root];
while let Some(dir) = stack.pop() {
let entries = match std::fs::read_dir(&dir) {
Ok(entries) => entries,
Err(e) => {
usage.unreadable.push(format!("{} ({e})", dir.display()));
continue;
}
};
for entry in entries {
let entry = match entry {
Ok(entry) => entry,
Err(e) => {
usage.unreadable.push(format!("{} ({e})", dir.display()));
continue;
}
};
let path = entry.path();
match std::fs::symlink_metadata(&path) {
Ok(m) if m.is_dir() => stack.push(path),
Ok(m) if m.is_file() => {
usage.bytes += m.len();
usage.files += 1;
}
Ok(_) => {}
Err(e) => usage.unreadable.push(format!("{} ({e})", path.display())),
}
}
}
usage.unreadable.sort();
Some(usage)
}
impl DoctorReport {
pub fn is_healthy(&self) -> bool {
!self.schema_from_the_future
&& !self.version_skew
&& self
.state_files
.iter()
.all(|f| !matches!(f.status, StateFileStatus::Unparseable { .. }))
&& self
.models
.iter()
.all(|m| matches!(m.status, ModelStatus::Healthy))
&& self.runtimes.iter().all(|r| !r.is_broken())
&& !self
.daemon_tmpdir
.as_ref()
.is_some_and(DaemonTmpdirCheck::is_failing)
}
}
pub fn diagnose(opts: &DoctorOptions) -> DoctorReport {
diagnose_at(&car_home(), &crate::default_models_dir(), opts)
}
pub fn diagnose_in(home: &Path, opts: &DoctorOptions) -> DoctorReport {
diagnose_at(home, &home.join("models"), opts)
}
pub fn diagnose_at(home: &Path, models_dir: &Path, opts: &DoctorOptions) -> DoctorReport {
let catalog_public_key = crate::catalog::catalog_public_key();
let registry = crate::registry::UnifiedRegistry::new_with_session(
home.to_path_buf(),
models_dir.to_path_buf(),
Some(&catalog_public_key),
crate::registry::SessionProbe::Inert,
);
diagnose_at_with_registry(home, models_dir, opts, ®istry, None)
}
#[doc(hidden)]
pub fn diagnose_at_isolated(
home: &Path,
models_dir: &Path,
huggingface_hub_root: &Path,
opts: &DoctorOptions,
) -> DoctorReport {
let registry = crate::registry::UnifiedRegistry::new_isolated_for_diagnosis(
home.to_path_buf(),
models_dir.to_path_buf(),
);
diagnose_at_with_registry(
home,
models_dir,
opts,
®istry,
Some(huggingface_hub_root),
)
}
fn diagnose_at_with_registry(
home: &Path,
models_dir: &Path,
opts: &DoctorOptions,
registry: &crate::registry::UnifiedRegistry,
huggingface_hub_root: Option<&Path>,
) -> DoctorReport {
let mut repairs = Vec::new();
let on_disk_stamp = read_version_stamp(home);
let binary = VersionStamp::current();
let version_skew = on_disk_stamp
.as_ref()
.map(|s| {
s.car_version != binary.car_version
|| s.state_schema_version != binary.state_schema_version
})
.unwrap_or(false);
let carried_from_version = on_disk_stamp
.as_ref()
.and_then(|s| s.previous_car_version.clone());
let schema_from_the_future = on_disk_stamp
.as_ref()
.is_some_and(|s| s.state_schema_version > binary.state_schema_version);
let mut state_files = Vec::new();
for name in KNOWN_STATE_FILES {
state_files.push(check_state_file(home, name, opts, &mut repairs));
}
let default_hub = crate::hf_cache::hub_dir();
let hub = huggingface_hub_root.unwrap_or(&default_hub);
let facts = |name: &str| -> ManagedDirFacts {
let Some(schema) = registry.all().find(|schema| schema.name == name) else {
return ManagedDirFacts::default();
};
let has_receipt = !matches!(
crate::model_management::read_receipt(home, &schema.id),
Ok(None)
);
let superseded_by = registry
.curated_replacement(&schema.id)
.filter(|to| {
crate::registry::physical_weights_ready_with_huggingface_hub(
to,
models_dir,
huggingface_hub_root,
)
})
.map(|to| to.id.clone());
ManagedDirFacts {
model_id: Some(schema.id.clone()),
has_receipt,
superseded_by,
state_root: Some(home.to_path_buf()),
}
};
let models = check_models_with(models_dir, hub, opts, &mut repairs, &facts);
let installed_models = registry
.list()
.into_iter()
.filter(|schema| {
schema.downloads_weights()
&& crate::registry::physical_weights_ready_with_huggingface_hub(
schema,
models_dir,
huggingface_hub_root,
)
})
.count();
let owned = crate::retire::ownership(
crate::retire::catalog_ownership(),
crate::retire::recorded_files(home, hub),
);
if opts.repair {
let (discarded, errors) = crate::retire::discard_abandoned_partials(hub, &owned);
if !discarded.is_empty() {
let bytes: u64 = discarded.iter().map(|(_, b)| b).sum();
repairs.push(format!(
"removed {} abandoned partial download(s) of CAR's ({bytes} bytes) from the HuggingFace cache",
discarded.len()
));
}
for error in errors {
repairs.push(format!(
"could not remove an abandoned partial download: {error}"
));
}
}
let leftovers = find_leftovers_in(hub, &owned);
let revoked_copies = crate::retire::revoked_copies(home, models_dir);
let catalog = crate::catalog::catalog_freshness_with_liveness(
&crate::catalog::load_status(&crate::catalog::status_path(home)),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
);
let unrecognized = find_unrecognized(home);
if opts.repair {
let already_current = on_disk_stamp
.as_ref()
.map(|s| {
s.car_version == binary.car_version
&& s.state_schema_version == binary.state_schema_version
})
.unwrap_or(false);
match write_version_stamp(home) {
Ok(()) if !already_current => repairs.push(format!(
"refreshed version stamp to {} (schema v{})",
binary.car_version, binary.state_schema_version
)),
Ok(()) => {}
Err(e) => repairs.push(format!("failed to refresh version stamp: {e}")),
}
}
let empty_journals = find_empty_journals(home);
if opts.repair && !empty_journals.is_empty() {
let mut removed = 0usize;
for p in &empty_journals {
if std::fs::remove_file(p).is_ok() {
removed += 1;
}
}
if removed > 0 {
repairs.push(format!(
"removed {removed} empty event journal(s) from journals/"
));
}
}
let runtimes = check_runtimes(home);
let daemon_tmpdir = check_daemon_tmpdir(home);
let host_role_unavailable = running_daemon_host_role_unavailable(home);
let daemon_single_principal = host_role_unavailable.is_some();
let daemon_host_role_cause = host_role_unavailable.flatten();
let coder_state = measure_coder_state(home);
DoctorReport {
car_home: home.display().to_string(),
binary_version: binary.car_version,
on_disk_stamp,
version_skew,
carried_from_version,
schema_from_the_future,
state_files,
models,
installed_models,
leftovers,
revoked_copies,
catalog,
runtimes,
unrecognized,
repairs,
daemon_tmpdir,
daemon_single_principal,
daemon_host_role_cause,
coder_state,
}
}
const MANAGED_RUNTIMES: &[&str] = &["speech-runtime", "visual-runtime"];
fn check_runtimes(home: &Path) -> Vec<RuntimeCheck> {
MANAGED_RUNTIMES
.iter()
.filter_map(|name| {
let root = home.join(name);
if !root.exists() {
return None;
}
Some(RuntimeCheck {
name: (*name).to_string(),
root: root.display().to_string(),
present: true,
interpreter_ok: crate::managed_venv::interpreter_healthy(&root),
})
})
.collect()
}
fn read_version_stamp(home: &Path) -> Option<VersionStamp> {
let text = std::fs::read_to_string(home.join("version.json")).ok()?;
serde_json::from_str(&text).ok()
}
fn check_state_file(
home: &Path,
name: &str,
opts: &DoctorOptions,
repairs: &mut Vec<String>,
) -> StateFileCheck {
let path = home.join(name);
let is_jsonl = name.ends_with(".jsonl");
let status = match std::fs::read_to_string(&path) {
Err(_) => StateFileStatus::Absent,
Ok(text) if text.trim().is_empty() => StateFileStatus::Ok {
schema_version: None,
},
Ok(text) if is_jsonl => match jsonl_first_bad_line(&text) {
None => StateFileStatus::Ok {
schema_version: None,
},
Some(e) => unparseable(&path, name, e, opts, repairs),
},
Ok(text) => match serde_json::from_str::<serde_json::Value>(&text) {
Ok(value) => StateFileStatus::Ok {
schema_version: value
.get("schema_version")
.and_then(serde_json::Value::as_u64)
.map(|v| v as u32),
},
Err(e) => unparseable(&path, name, e.to_string(), opts, repairs),
},
};
StateFileCheck {
name: name.to_string(),
status,
}
}
fn jsonl_first_bad_line(text: &str) -> Option<String> {
for (i, line) in text.lines().enumerate() {
if line.trim().is_empty() {
continue;
}
if let Err(e) = serde_json::from_str::<serde_json::Value>(line) {
return Some(format!("line {}: {e}", i + 1));
}
}
None
}
fn unparseable(
path: &Path,
name: &str,
error: String,
opts: &DoctorOptions,
repairs: &mut Vec<String>,
) -> StateFileStatus {
let backed_up_to = if opts.repair {
let plain = path.with_file_name(format!("{name}.corrupt.bak"));
let bak = if plain.exists() {
let epoch = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
path.with_file_name(format!("{name}.corrupt.{epoch}.bak"))
} else {
plain
};
match std::fs::rename(path, &bak) {
Ok(()) => {
repairs.push(format!("backed up unparseable {name} → {}", bak.display()));
Some(bak.display().to_string())
}
Err(err) => {
repairs.push(format!("failed to back up {name}: {err}"));
None
}
}
} else {
None
};
StateFileStatus::Unparseable {
error,
backed_up_to,
}
}
#[derive(Debug, Clone, Default)]
struct ManagedDirFacts {
model_id: Option<String>,
has_receipt: bool,
superseded_by: Option<String>,
state_root: Option<PathBuf>,
}
#[cfg(test)]
fn check_models(
models_dir: &Path,
hub: &Path,
opts: &DoctorOptions,
repairs: &mut Vec<String>,
) -> Vec<ModelCheck> {
check_models_with(models_dir, hub, opts, repairs, &|_| {
ManagedDirFacts::default()
})
}
fn check_models_with(
models_dir: &Path,
hub: &Path,
opts: &DoctorOptions,
repairs: &mut Vec<String>,
facts: &dyn Fn(&str) -> ManagedDirFacts,
) -> Vec<ModelCheck> {
let Ok(entries) = std::fs::read_dir(models_dir) else {
return Vec::new();
};
let mut out = Vec::new();
for entry in entries.filter_map(Result::ok) {
let dir = entry.path();
if !dir.is_dir() {
continue;
}
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with(".car-") {
continue;
}
if let Some(status) = check_one_model(&dir, hub, opts, &name, repairs, &facts(&name)) {
out.push(ModelCheck { name, status });
}
}
out.sort_by(|a, b| a.name.cmp(&b.name));
out
}
fn check_one_model(
dir: &Path,
hub: &Path,
opts: &DoctorOptions,
name: &str,
repairs: &mut Vec<String>,
facts: &ManagedDirFacts,
) -> Option<ModelStatus> {
let weights = weight_files(dir);
if weights.is_empty() {
if is_interrupted_install(dir) {
return Some(incomplete_status(dir, hub, opts, name, repairs, facts));
}
return None;
}
let mut bad_files = Vec::new();
for w in &weights {
let corrupt = if opts.deep {
verify_cache_file(w) == CacheIntegrity::Corrupt
} else {
!cache_file_usable(w)
};
if corrupt {
bad_files.push(
w.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
);
}
}
if bad_files.is_empty() {
return Some(ModelStatus::Healthy);
}
let purged = if opts.repair {
let n = purge_corrupt_cache_files_in(dir, hub);
if n > 0 {
repairs.push(format!(
"purged {n} corrupt file(s) from model '{name}' — re-pull with `car models pull {name}`"
));
}
n
} else {
0
};
Some(ModelStatus::Corrupt { bad_files, purged })
}
fn incomplete_status(
dir: &Path,
hub: &Path,
opts: &DoctorOptions,
name: &str,
repairs: &mut Vec<String>,
facts: &ManagedDirFacts,
) -> ModelStatus {
let pull_target = facts.model_id.as_deref().unwrap_or(name);
let superseded_by = facts.superseded_by.clone();
if facts.has_receipt {
return ModelStatus::Incomplete {
detail: format!(
"installed by CAR, but no weights resolve — `car models remove {pull_target}`, \
then `car models pull {pull_target}`"
),
purged: false,
superseded_by,
};
}
let mut purged = false;
let mut refusal = None;
if opts.repair {
let models_dir = dir.parent().unwrap_or(dir);
match clear_interrupted_install(models_dir, name, hub, facts) {
Ok(()) => {
purged = true;
repairs.push(match &superseded_by {
Some(by) => format!(
"removed the interrupted install of '{name}' (superseded by {by}, which is installed; nothing to re-pull)"
),
None => format!(
"removed the interrupted install of '{name}' — if you still want it, `car models pull {pull_target}`"
),
});
}
Err(reason) => {
repairs.push(format!(
"left the interrupted install of '{name}' in place: {reason}"
));
refusal = Some(reason);
}
}
}
let detail = match (purged, &superseded_by, refusal) {
(true, Some(by), _) => format!(
"no weights resolved; removed — superseded by {by}, which is installed, so there is nothing to re-pull"
),
(true, None, _) => format!(
"no weights resolved; removed — if you still want it, `car models pull {pull_target}`"
),
(false, _, Some(reason)) => format!(
"manifest linked into the HuggingFace cache but no weights resolve; not removed ({reason})"
),
(false, Some(by), None) => format!(
"manifest linked into the HuggingFace cache but no weights resolve, and {by} \
(installed) supersedes it — remove it with `car doctor --repair`"
),
(false, None, None) => format!(
"manifest linked into the HuggingFace cache but no weights resolve — \
`car doctor --repair` clears it; then, if you still want it, \
`car models pull {pull_target}`"
),
};
ModelStatus::Incomplete {
detail,
purged,
superseded_by,
}
}
fn clear_interrupted_install(
models_dir: &Path,
name: &str,
hub: &Path,
facts: &ManagedDirFacts,
) -> Result<(), String> {
let (Some(state_root), Some(model_id)) = (&facts.state_root, &facts.model_id) else {
return purge_interrupted_install(models_dir, name, hub);
};
let store = crate::model_management::ModelManagementStore::new(
state_root.clone(),
models_dir.to_path_buf(),
);
let _guard = match store.begin_mutation(model_id) {
Ok(guard) => guard,
Err(crate::model_management::ModelManagementError::MutationInProgress { .. }) => {
return Err(
"a pull, adoption or removal of it is running right now; run `car doctor --repair` again once it finishes"
.into(),
)
}
Err(error) => return Err(format!("cannot take its model lock: {error}")),
};
match store.load_receipt(model_id) {
Ok(None) => {}
Ok(Some(_)) => {
return Err(format!(
"CAR now holds an install receipt for it; `car models remove {model_id}` owns it"
))
}
Err(error) => return Err(format!("its install receipt cannot be read: {error}")),
}
purge_interrupted_install(models_dir, name, hub)
}
pub(crate) fn purge_interrupted_install(
models_dir: &Path,
name: &str,
hub: &Path,
) -> Result<(), String> {
use crate::model_management::{unlink_checked, UnlinkKind};
let single_component = matches!(
Path::new(name).components().collect::<Vec<_>>().as_slice(),
[std::path::Component::Normal(_)]
);
if !single_component || name.starts_with('.') {
return Err(format!("{name:?} is not a managed model directory name"));
}
let leaf = models_dir.join(name);
let meta = std::fs::symlink_metadata(&leaf).map_err(|e| format!("cannot read it: {e}"))?;
if !is_interrupted_install(&leaf) {
return Err(
"it is not an interrupted install (weights resolve, or no linked manifest)".into(),
);
}
if is_link_or_junction(&meta) {
let inside_hub = match (
car_winenv::canonicalize(&leaf),
car_winenv::canonicalize(hub),
) {
(Ok(target), Ok(hub)) => target.starts_with(hub),
_ => false,
};
if !inside_hub {
return Err(
"it links outside the HuggingFace cache, to storage that is not CAR's".into(),
);
}
return unlink_checked(&leaf, UnlinkKind::Link).map_err(|e| format!("unlink failed: {e}"));
}
if !meta.is_dir() {
return Err("it is not a directory".into());
}
for (path, kind) in purge_plan(&leaf)? {
unlink_checked(&path, kind)
.map_err(|e| format!("could not remove {}: {e}", path.display()))?;
}
std::fs::remove_dir(&leaf).map_err(|e| format!("could not remove the directory: {e}"))
}
fn purge_plan(leaf: &Path) -> Result<Vec<(PathBuf, crate::model_management::UnlinkKind)>, String> {
use crate::model_management::UnlinkKind;
const MAX_MANIFEST_BYTES: u64 = 64 * 1024 * 1024;
let mut files = Vec::new();
let mut links = Vec::new();
let entries = std::fs::read_dir(leaf).map_err(|e| format!("cannot list it: {e}"))?;
for entry in entries {
let entry = entry.map_err(|e| format!("cannot list it: {e}"))?;
let path = entry.path();
let meta = std::fs::symlink_metadata(&path)
.map_err(|e| format!("cannot read {}: {e}", path.display()))?;
if is_link_or_junction(&meta) {
links.push((path, UnlinkKind::Link));
} else if meta.is_file() {
let pulled = entry
.file_name()
.to_str()
.is_some_and(is_pulled_regular_file);
if !pulled || meta.len() > MAX_MANIFEST_BYTES {
return Err(format!(
"it holds {}, which a pull does not leave behind",
path.display()
));
}
files.push((path, UnlinkKind::File));
} else {
return Err(format!(
"it holds {}, which a pull does not leave behind",
path.display()
));
}
}
files.extend(links);
Ok(files)
}
const PULLED_REGULAR_FILES: &[&str] = &[
"config.json",
"model_index.json",
"tokenizer.json",
"tokenizer_config.json",
"model.safetensors.index.json",
"generation_config.json",
"special_tokens_map.json",
"added_tokens.json",
"preprocessor_config.json",
"chat_template.jinja",
"chat_template.json",
];
fn is_pulled_regular_file(name: &str) -> bool {
let known = |n: &str| {
PULLED_REGULAR_FILES.contains(&n) || crate::registry::TOKENIZER_FILENAMES.contains(&n)
};
if known(name) {
return true;
}
name.strip_prefix('.')
.and_then(|rest| rest.strip_suffix(".tmp"))
.and_then(|rest| rest.split_once(".car-install-"))
.is_some_and(|(original, _)| known(original))
}
fn is_link_or_junction(meta: &std::fs::Metadata) -> bool {
#[cfg(windows)]
{
use std::os::windows::fs::MetadataExt;
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
if meta.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return true;
}
}
meta.file_type().is_symlink()
}
pub(crate) fn is_interrupted_install(dir: &Path) -> bool {
const MANIFESTS: &[&str] = &[
"config.json",
"model_index.json",
"tokenizer.json",
"tokenizer_config.json",
"model.safetensors.index.json",
];
let has_symlinked_manifest = MANIFESTS.iter().any(|m| {
let p = dir.join(m);
std::fs::symlink_metadata(&p)
.map(|meta| meta.file_type().is_symlink())
.unwrap_or(false)
});
has_symlinked_manifest && !crate::registry::mlx_dir_has_weights(dir)
}
fn find_empty_journals(home: &Path) -> Vec<PathBuf> {
let mut out: Vec<PathBuf> = car_home::journal_dirs(home)
.iter()
.filter_map(|dir| std::fs::read_dir(dir).ok())
.flatten()
.filter_map(Result::ok)
.filter(|e| {
e.path().extension().and_then(|x| x.to_str()) == Some("jsonl")
&& e.metadata()
.map(|m| m.is_file() && m.len() == 0)
.unwrap_or(false)
})
.map(|e| e.path())
.collect();
out.sort();
out
}
fn find_leftovers_in(hub: &Path, owned: &crate::retire::Ownership) -> Vec<Leftover> {
let reclaimable: std::collections::BTreeSet<PathBuf> =
crate::retire::abandoned_partials(hub, owned)
.into_iter()
.collect();
let mut out = Vec::new();
let Ok(repos) = std::fs::read_dir(hub) else {
return out;
};
for repo in repos.filter_map(Result::ok) {
let blobs = repo.path().join("blobs");
let Ok(entries) = std::fs::read_dir(&blobs) else {
continue;
};
for e in entries.filter_map(Result::ok) {
let p = e.path();
let is_partial = p
.file_name()
.and_then(|n| n.to_str())
.map(|n| n.ends_with(".sync.part") || n.ends_with(".incomplete"))
.unwrap_or(false);
if !is_partial {
continue;
}
let meta = e.metadata().ok();
let bytes = meta.as_ref().map(|m| m.len()).unwrap_or(0);
let age_secs = meta
.and_then(|m| m.modified().ok())
.and_then(|written| std::time::SystemTime::now().duration_since(written).ok())
.map(|age| age.as_secs());
out.push(Leftover {
reclaimable: reclaimable.contains(&p),
path: p.display().to_string(),
bytes,
age_secs,
});
}
}
out.sort_by(|a, b| b.bytes.cmp(&a.bytes).then_with(|| a.path.cmp(&b.path)));
out
}
fn weight_files(dir: &Path) -> Vec<PathBuf> {
fn is_weight(p: &Path) -> bool {
matches!(
p.extension().and_then(|e| e.to_str()),
Some("safetensors") | Some("gguf")
)
}
let mut out = Vec::new();
let Ok(entries) = std::fs::read_dir(dir) else {
return out;
};
for entry in entries.filter_map(Result::ok) {
let p = entry.path();
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
out.extend(weight_files(&p));
} else if is_weight(&p) {
out.push(p);
}
}
out
}
fn find_unrecognized(home: &Path) -> Vec<String> {
let Ok(entries) = std::fs::read_dir(home) else {
return Vec::new();
};
let mut out: Vec<String> = entries
.filter_map(Result::ok)
.filter_map(|e| {
let name = e.file_name().to_string_lossy().to_string();
let is_dir = e.file_type().map(|t| t.is_dir()).unwrap_or(false);
let known = if is_dir {
KNOWN_DIRS.contains(&name.as_str())
} else {
KNOWN_STATE_FILES.contains(&name.as_str())
|| KNOWN_NON_JSON_FILES.contains(&name.as_str())
|| TOLERATED_SUFFIXES.iter().any(|s| name.ends_with(s))
|| name.starts_with('.')
};
if known {
None
} else {
Some(name)
}
})
.collect();
out.sort();
out
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn opts(deep: bool, repair: bool) -> DoctorOptions {
DoctorOptions { deep, repair }
}
fn diagnose_repair_with_isolated_model_roots(home: &Path) -> DoctorReport {
let models_dir = TempDir::new().unwrap();
let huggingface_hub = TempDir::new().unwrap();
let blobs = huggingface_hub.path().join("models--fixture/blobs");
std::fs::create_dir_all(&blobs).unwrap();
std::fs::write(blobs.join("isolated.sync.part"), b"partial").unwrap();
let report = diagnose_at_isolated(
home,
models_dir.path(),
huggingface_hub.path(),
&opts(false, true),
);
assert_eq!(
report.leftovers.len(),
1,
"the isolated seam must scan its injected Hugging Face root, not skip leftovers"
);
assert!(
Path::new(&report.leftovers[0].path).starts_with(huggingface_hub.path()),
"the leftover must come from the test-owned Hugging Face root"
);
report
}
#[test]
fn clean_home_is_healthy() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("models.json"), "{}").unwrap();
std::fs::create_dir_all(tmp.path().join("models")).unwrap();
let r = diagnose_in(tmp.path(), &opts(false, false));
assert!(r.is_healthy(), "clean home should be healthy: {r:?}");
assert!(r.unrecognized.is_empty());
}
#[test]
fn secret_index_is_recognized_not_a_leftover() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("models.json"), "{}").unwrap();
std::fs::create_dir_all(tmp.path().join("models")).unwrap();
std::fs::write(tmp.path().join("secret_index.json"), r#"{"entries":[]}"#).unwrap();
let r = diagnose_in(tmp.path(), &opts(false, false));
assert!(
!r.unrecognized.contains(&"secret_index.json".to_string()),
"secret_index.json must not be flagged as unrecognized: {:?}",
r.unrecognized
);
assert!(
r.is_healthy(),
"home with a secret index should be healthy: {r:?}"
);
}
#[test]
fn unparseable_state_file_is_flagged_and_backed_up_on_repair() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("connectors.json"), "{not json").unwrap();
let r = diagnose_in(tmp.path(), &opts(false, false));
let c = r
.state_files
.iter()
.find(|f| f.name == "connectors.json")
.unwrap();
assert!(matches!(
c.status,
StateFileStatus::Unparseable {
backed_up_to: None,
..
}
));
assert!(!r.is_healthy());
assert!(
tmp.path().join("connectors.json").exists(),
"untouched without --repair"
);
let r = diagnose_in(tmp.path(), &opts(false, true));
let c = r
.state_files
.iter()
.find(|f| f.name == "connectors.json")
.unwrap();
assert!(matches!(
c.status,
StateFileStatus::Unparseable {
backed_up_to: Some(_),
..
}
));
assert!(!tmp.path().join("connectors.json").exists());
assert!(tmp.path().join("connectors.json.corrupt.bak").exists());
}
#[test]
fn dotenv_env_file_is_never_parsed_or_moved() {
let tmp = TempDir::new().unwrap();
std::fs::write(
tmp.path().join("env"),
"ANTHROPIC_API_KEY=sk-secret\nFOO=bar\n",
)
.unwrap();
let r = diagnose_in(tmp.path(), &opts(false, true));
assert!(
r.is_healthy(),
"dotenv env must not make the install unhealthy"
);
assert!(
!r.unrecognized.contains(&"env".to_string()),
"env is recognized"
);
assert!(
r.state_files.iter().all(|f| f.name != "env"),
"env is never JSON-checked"
);
assert!(
tmp.path().join("env").exists(),
"repair must not move the secrets file"
);
assert!(!tmp.path().join("env.corrupt.bak").exists());
}
#[test]
fn empty_state_file_is_ok() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("messaging.json"), "").unwrap();
let r = diagnose_in(tmp.path(), &opts(false, false));
let c = r
.state_files
.iter()
.find(|f| f.name == "messaging.json")
.unwrap();
assert!(matches!(c.status, StateFileStatus::Ok { .. }));
}
#[test]
#[cfg(unix)]
fn interrupted_pull_is_reported_not_skipped() {
let tmp = TempDir::new().unwrap();
let snap = tmp.path().join("hfsnap");
std::fs::create_dir_all(&snap).unwrap();
std::fs::write(snap.join("config.json"), "{}").unwrap();
std::fs::write(snap.join("tokenizer.json"), "{}").unwrap();
let m = tmp.path().join("models").join("Qwen3-4B-MLX");
std::fs::create_dir_all(&m).unwrap();
std::os::unix::fs::symlink(snap.join("config.json"), m.join("config.json")).unwrap();
std::os::unix::fs::symlink(snap.join("tokenizer.json"), m.join("tokenizer.json")).unwrap();
let r = diagnose_in(tmp.path(), &opts(false, false));
let check = r
.models
.iter()
.find(|c| c.name == "Qwen3-4B-MLX")
.expect("an interrupted install must appear in the report, not be dropped");
assert!(
matches!(check.status, ModelStatus::Incomplete { .. }),
"expected Incomplete, got {:?}",
check.status
);
assert!(
!r.is_healthy(),
"a half-installed model must not read as healthy"
);
}
#[test]
fn abandoned_partial_downloads_are_reported_never_deleted() {
let hf = TempDir::new().unwrap();
let blobs = hf.path().join("hub").join("models--x--y").join("blobs");
std::fs::create_dir_all(&blobs).unwrap();
let part = blobs.join("deadbeef.sync.part");
std::fs::write(&part, vec![0u8; 4096]).unwrap();
std::fs::write(blobs.join("finished"), b"whole").unwrap();
let home = TempDir::new().unwrap();
let r = diagnose_at_isolated(
home.path(),
&home.path().join("models"),
&hf.path().join("hub"),
&opts(false, true),
);
assert_eq!(
r.leftovers.len(),
1,
"expected one partial: {:?}",
r.leftovers
);
assert_eq!(r.leftovers[0].bytes, 4096);
assert!(r.leftovers[0].path.ends_with("deadbeef.sync.part"));
assert!(
part.exists(),
"--repair must NOT delete a partial: the HF cache is shared and the \
transfer may still be running"
);
assert!(r.is_healthy());
}
#[test]
fn repair_does_not_report_an_unchanged_version_stamp() {
let tmp = TempDir::new().unwrap();
let first = diagnose_in(tmp.path(), &opts(false, true));
assert!(
first.repairs.iter().any(|r| r.contains("version stamp")),
"writing a missing stamp IS a repair: {:?}",
first.repairs
);
let second = diagnose_in(tmp.path(), &opts(false, true));
assert!(
!second.repairs.iter().any(|r| r.contains("version stamp")),
"an unchanged stamp is not a repair: {:?}",
second.repairs
);
}
#[test]
fn empty_journals_are_reaped_on_repair_only() {
let tmp = TempDir::new().unwrap();
let journals = tmp.path().join("journals");
std::fs::create_dir_all(&journals).unwrap();
let empty = journals.join("aaaaaaaaaaaa.jsonl");
let full = journals.join("bbbbbbbbbbbb.jsonl");
let other = journals.join("notes.txt");
let private = tmp.path().join("work-contexts/work-fly/journals");
std::fs::create_dir_all(&private).unwrap();
let empty_private = private.join("cccccccccccc.jsonl");
std::fs::write(&empty_private, b"").unwrap();
std::fs::write(&empty, b"").unwrap();
std::fs::write(&full, b"{\"kind\":\"proposal_received\"}\n").unwrap();
std::fs::write(&other, b"").unwrap();
let _ = diagnose_in(tmp.path(), &opts(false, false));
assert!(empty.exists(), "no --repair, no deletion");
let r = diagnose_in(tmp.path(), &opts(false, true));
assert!(!empty.exists(), "empty journal should be reaped");
assert!(!empty_private.exists(), "a private store's too");
assert!(full.exists(), "a journal with events must be kept");
assert!(other.exists(), "non-.jsonl files are not ours to remove");
assert!(
r.repairs.iter().any(|x| x.contains("empty event journal")),
"the reap should be reported: {:?}",
r.repairs
);
}
#[cfg(unix)]
#[test]
fn repair_never_follows_a_linked_work_context() {
use std::os::unix::fs::symlink;
let tmp = TempDir::new().unwrap();
let outside = TempDir::new().unwrap();
std::fs::create_dir_all(outside.path().join("journals")).unwrap();
let keep = outside.path().join("journals/keep.jsonl");
std::fs::write(&keep, b"").unwrap();
let contexts = tmp.path().join("work-contexts");
let real = contexts.join("work-real/journals");
std::fs::create_dir_all(&real).unwrap();
let empty_real = real.join("dddddddddddd.jsonl");
std::fs::write(&empty_real, b"").unwrap();
symlink(outside.path(), contexts.join("work-linked")).unwrap();
std::fs::create_dir_all(contexts.join("work-journals-linked")).unwrap();
symlink(
outside.path().join("journals"),
contexts.join("work-journals-linked/journals"),
)
.unwrap();
let personal_target = outside.path().join("personal");
std::fs::create_dir_all(&personal_target).unwrap();
let foreign = personal_target.join("foreign.jsonl");
std::fs::write(&foreign, b"").unwrap();
symlink(&personal_target, tmp.path().join("journals")).unwrap();
let r = diagnose_in(tmp.path(), &opts(false, true));
assert!(
foreign.exists(),
"a file behind a linked journals/ is not CAR's"
);
assert!(keep.exists(), "a file behind a linked context is not CAR's");
assert!(!empty_real.exists(), "a real context is still reaped");
assert!(
r.repairs
.iter()
.any(|x| x.contains("removed 1 empty event journal")),
"only the real context's journal is counted: {:?}",
r.repairs
);
}
#[cfg(windows)]
fn junction(target: &Path, link: &Path) {
use std::os::windows::fs::MetadataExt;
let status = std::process::Command::new("cmd")
.args(["/C", "mklink", "/J"])
.arg(link)
.arg(target)
.status()
.unwrap();
assert!(status.success(), "mklink /J {}", link.display());
let meta = std::fs::symlink_metadata(link).unwrap();
assert_ne!(
meta.file_attributes() & 0x400,
0,
"{link:?} is a reparse point"
);
assert!(
std::fs::metadata(link).unwrap().is_dir(),
"{link:?} resolves"
);
}
#[cfg(windows)]
#[test]
fn repair_never_follows_a_junctioned_work_context() {
let tmp = TempDir::new().unwrap();
let outside = TempDir::new().unwrap();
let outside_journals = outside.path().join("journals");
std::fs::create_dir_all(&outside_journals).unwrap();
let keep = outside_journals.join("keep.jsonl");
std::fs::write(&keep, b"").unwrap();
let contexts = tmp.path().join("work-contexts");
let real = contexts.join("work-real").join("journals");
std::fs::create_dir_all(&real).unwrap();
let empty_real = real.join("dddddddddddd.jsonl");
std::fs::write(&empty_real, b"").unwrap();
junction(outside.path(), &contexts.join("work-linked"));
std::fs::create_dir_all(contexts.join("work-journals-linked")).unwrap();
junction(
&outside_journals,
&contexts.join("work-journals-linked").join("journals"),
);
let personal_target = outside.path().join("personal");
std::fs::create_dir_all(&personal_target).unwrap();
let foreign = personal_target.join("foreign.jsonl");
std::fs::write(&foreign, b"").unwrap();
junction(&personal_target, &tmp.path().join("journals"));
let r = diagnose_in(tmp.path(), &opts(false, true));
assert!(
foreign.exists(),
"a file behind a junctioned journals/ is not CAR's"
);
assert!(
keep.exists(),
"a file behind a junctioned context is not CAR's"
);
assert!(!empty_real.exists(), "a real context is still reaped");
assert!(
r.repairs
.iter()
.any(|x| x.contains("removed 1 empty event journal")),
"only the real context's journal is counted: {:?}",
r.repairs
);
}
#[cfg(windows)]
#[test]
fn repair_purges_a_junctioned_model_without_deleting_outside_its_target() {
let tmp = TempDir::new().unwrap();
let models = tmp.path().join("models");
let hub = tmp.path().join("hub");
std::fs::create_dir_all(&models).unwrap();
std::fs::create_dir_all(&hub).unwrap();
let external_dir = tmp.path().join("external-drive").join("Out");
std::fs::create_dir_all(&external_dir).unwrap();
let external = external_dir.join("model.safetensors");
std::fs::write(&external, b"").unwrap();
let unrelated_dir = tmp.path().join("unrelated");
std::fs::create_dir_all(&unrelated_dir).unwrap();
let unrelated = unrelated_dir.join("model.safetensors");
std::fs::write(&unrelated, b"").unwrap();
junction(&unrelated_dir, &external_dir.join("sub"));
junction(&external_dir, &models.join("Out"));
let mut repairs = Vec::new();
let checks = check_models(&models, &hub, &opts(false, true), &mut repairs);
let out = checks.iter().find(|c| c.name == "Out").unwrap();
assert!(
matches!(out.status, ModelStatus::Corrupt { purged, .. } if purged >= 1),
"a junctioned model dir is diagnosed and repaired: {:?}",
out.status
);
assert!(!external.exists(), "its own corrupt weight goes");
assert!(
unrelated.exists(),
"a file behind a junction inside the model is not CAR's"
);
}
#[test]
fn config_only_stub_is_skipped_not_flagged() {
let tmp = TempDir::new().unwrap();
let m = tmp.path().join("models").join("Stub");
std::fs::create_dir_all(&m).unwrap();
std::fs::write(m.join("config.json"), "{}").unwrap();
let r = diagnose_in(tmp.path(), &opts(false, false));
assert!(
r.models.iter().all(|m| m.name != "Stub"),
"stub should be skipped"
);
assert!(r.is_healthy());
}
#[cfg(unix)]
#[test]
fn corrupt_model_weight_is_purged_on_repair() {
let tmp = TempDir::new().unwrap();
let m = tmp.path().join("models").join("Qwen3-Test");
std::fs::create_dir_all(&m).unwrap();
std::os::unix::fs::symlink(m.join("gone"), m.join("model.safetensors")).unwrap();
let r = diagnose_in(tmp.path(), &opts(false, false));
let mc = r.models.iter().find(|m| m.name == "Qwen3-Test").unwrap();
assert!(matches!(mc.status, ModelStatus::Corrupt { purged: 0, .. }));
let r = diagnose_in(tmp.path(), &opts(false, true));
let mc = r.models.iter().find(|m| m.name == "Qwen3-Test").unwrap();
match &mc.status {
ModelStatus::Corrupt { purged, .. } => assert_eq!(*purged, 1),
other => panic!("expected Corrupt, got {other:?}"),
}
assert!(std::fs::symlink_metadata(m.join("model.safetensors")).is_err());
}
#[cfg(unix)]
#[test]
fn repair_purges_a_linked_model_without_deleting_outside_its_target() {
use std::os::unix::fs::symlink;
let tmp = TempDir::new().unwrap();
let models = tmp.path().join("models");
let hub = tmp.path().join("hub");
std::fs::create_dir_all(&models).unwrap();
let weight = |dir: &Path| {
std::fs::create_dir_all(dir).unwrap();
let file = dir.join("model.safetensors");
std::fs::write(&file, b"").unwrap();
file
};
let external = weight(&tmp.path().join("external-drive/Out"));
let unrelated = tmp.path().join("unrelated/notes.txt");
std::fs::create_dir_all(unrelated.parent().unwrap()).unwrap();
std::fs::write(&unrelated, b"").unwrap();
symlink(&unrelated, external.with_file_name("b.safetensors")).unwrap();
symlink(external.parent().unwrap(), models.join("Out")).unwrap();
let in_hub = weight(&hub.join("models--x--in/snapshots/rev"));
symlink(in_hub.parent().unwrap(), models.join("In")).unwrap();
let real = weight(&models.join("Real"));
let mut repairs = Vec::new();
let checks = check_models(&models, &hub, &opts(false, true), &mut repairs);
let purged = |name: &str| match &checks.iter().find(|c| c.name == name).unwrap().status {
ModelStatus::Corrupt { purged, .. } => *purged,
other => panic!("{name}: expected Corrupt, got {other:?}"),
};
assert_eq!(purged("Out"), 2, "its weight and the inner link go");
assert!(!external.exists(), "a linked model dir is still repaired");
assert!(
unrelated.exists(),
"a file outside the linked folder is not CAR's"
);
assert_eq!(purged("In"), 1);
assert!(!in_hub.exists());
assert_eq!(purged("Real"), 1);
assert!(!real.exists());
}
#[cfg(unix)]
#[test]
fn isolated_diagnosis_purges_blobs_in_its_own_hub() {
let tmp = TempDir::new().unwrap();
let hub = tmp.path().join("hub");
let blob = hub.join("models--x--y/blobs").join("a".repeat(64));
std::fs::create_dir_all(blob.parent().unwrap()).unwrap();
std::fs::write(&blob, b"").unwrap();
let model = tmp.path().join("models/Y");
std::fs::create_dir_all(&model).unwrap();
std::os::unix::fs::symlink(&blob, model.join("model.safetensors")).unwrap();
let _ = diagnose_at_isolated(
tmp.path(),
&tmp.path().join("models"),
&hub,
&opts(false, true),
);
assert!(!blob.exists(), "the isolated hub's corrupt blob is purged");
assert!(std::fs::symlink_metadata(model.join("model.safetensors")).is_err());
}
#[test]
fn shared_huggingface_weights_count_as_installed() {
let tmp = TempDir::new().unwrap();
let models_dir = tmp.path().join("models");
std::fs::create_dir(&models_dir).unwrap();
let huggingface_hub = tmp.path().join("huggingface-hub");
let snapshot = huggingface_hub
.join("models--example--doctor-only")
.join("snapshots")
.join("revision");
std::fs::create_dir_all(&snapshot).unwrap();
std::fs::write(snapshot.join("config.json"), b"{}").unwrap();
std::fs::write(snapshot.join("model.safetensors"), b"weights").unwrap();
let schema: crate::schema::ModelSchema = serde_json::from_value(serde_json::json!({
"id": "example/doctor-only:4bit",
"name": "doctor-only",
"provider": "example",
"family": "doctor-test",
"capabilities": ["generate"],
"context_length": 4096,
"source": {
"type": "mlx",
"hf_repo": "example/doctor-only",
"hf_weight_file": null
}
}))
.unwrap();
let mut registry = crate::registry::UnifiedRegistry::new_empty(models_dir.clone());
registry.register(schema);
let report = diagnose_at_with_registry(
tmp.path(),
&models_dir,
&opts(false, false),
®istry,
Some(&huggingface_hub),
);
assert!(
report.models.is_empty(),
"shared-cache weights do not create a managed model directory check"
);
assert_eq!(
report.installed_models, 1,
"registry physical readiness must count shared-cache-only weights"
);
}
#[test]
fn current_version_state_entries_are_recognized() {
let tmp = TempDir::new().unwrap();
for name in [
"model-resource-policy.json",
"parslee-auth-authority.json",
"messages-inbound-watermark.json",
"catalog-status.json",
"identity.json",
"work-contexts.json",
"coder.toml",
"concierge-auto-since",
"host-state.json",
] {
std::fs::write(tmp.path().join(name), b"{}").unwrap();
}
for name in [
"proposal-completed-index",
"selfheal",
"onnxruntime",
"model-management",
"lattice",
"peer-certs",
"work-contexts",
"tools",
] {
std::fs::create_dir(tmp.path().join(name)).unwrap();
}
let unrecognized = find_unrecognized(tmp.path());
assert!(
unrecognized.is_empty(),
"current-version state must not be reported as older-install debris: {unrecognized:?}"
);
}
#[test]
fn repair_never_json_validates_or_moves_the_binary_peer_identity_key() {
let tmp = TempDir::new().unwrap();
let key_path = tmp.path().join("peer-identity.key");
let key_bytes = b"0123456789abcdef0123456789abcdef";
std::fs::write(&key_path, key_bytes).unwrap();
let report = diagnose_repair_with_isolated_model_roots(tmp.path());
assert_eq!(
std::fs::read(&key_path).unwrap(),
key_bytes,
"repair must preserve the binary peer identity byte-for-byte"
);
assert!(
!tmp.path().join("peer-identity.key.corrupt.bak").exists(),
"the non-JSON identity key must never be classified as corrupt JSON"
);
assert!(
report
.state_files
.iter()
.all(|file| file.name != "peer-identity.key"),
"the peer identity belongs to the non-JSON allowlist"
);
assert!(
!report
.unrecognized
.contains(&"peer-identity.key".to_string()),
"the live identity key must be recognized"
);
}
#[test]
fn repair_preserves_a_non_utf8_peer_identity_key() {
let tmp = TempDir::new().unwrap();
let key_path = tmp.path().join("peer-identity.key");
let key_bytes = [0xff, 0xfe, 0xfd, 0x00, 0x80, 0x81, 0x82, 0x83];
std::fs::write(&key_path, key_bytes).unwrap();
let report = diagnose_repair_with_isolated_model_roots(tmp.path());
assert_eq!(
std::fs::read(&key_path).unwrap(),
key_bytes,
"repair must preserve genuinely non-UTF-8 identity bytes"
);
assert!(
!tmp.path().join("peer-identity.key.corrupt.bak").exists(),
"repair must not create a corrupt-JSON backup for a binary key"
);
assert!(
report
.state_files
.iter()
.all(|file| file.name != "peer-identity.key"),
"the binary key must bypass the JSON state-file checker"
);
assert!(
!report
.unrecognized
.contains(&"peer-identity.key".to_string()),
"the binary key must take the recognized non-JSON branch"
);
}
#[test]
fn directories_car_creates_are_never_reported_as_leftovers() {
let tmp = TempDir::new().unwrap();
let live = [
"feedback-outbox",
"browser-profile",
"browser-profile-user",
".car-model-management",
];
for dir in live {
std::fs::create_dir_all(tmp.path().join(dir)).unwrap();
}
let report = diagnose_in(tmp.path(), &opts(false, true));
for dir in live {
assert!(
!report.unrecognized.contains(&dir.to_string()),
"{dir} is created by CAR and must not be reported as a leftover; \
unrecognized = {:?}",
report.unrecognized
);
assert!(tmp.path().join(dir).exists(), "{dir} must survive --repair");
}
}
#[cfg(unix)]
struct InterruptedInstall {
_tmp: TempDir,
home: PathBuf,
models: PathBuf,
hub: PathBuf,
snapshot: PathBuf,
dir: PathBuf,
}
#[cfg(unix)]
fn interrupted_install(name: &str) -> InterruptedInstall {
let tmp = TempDir::new().unwrap();
let home = tmp.path().join("car-home");
let models = home.join("models");
let hub = tmp.path().join("hub");
let snapshot = hub.join("models--org--repo/snapshots/rev");
std::fs::create_dir_all(&snapshot).unwrap();
let index = serde_json::json!({
"weight_map": {"model.embed": "model-00001-of-00002.safetensors"}
});
for (file, body) in [
("config.json", "{}".to_string()),
("tokenizer.json", "{}".to_string()),
("model.safetensors.index.json", index.to_string()),
] {
std::fs::write(snapshot.join(file), body).unwrap();
}
let dir = models.join(name);
std::fs::create_dir_all(&dir).unwrap();
for file in [
"config.json",
"tokenizer.json",
"model.safetensors.index.json",
] {
std::os::unix::fs::symlink(snapshot.join(file), dir.join(file)).unwrap();
}
InterruptedInstall {
_tmp: tmp,
home,
models,
hub,
snapshot,
dir,
}
}
#[cfg(unix)]
fn incomplete(report: &DoctorReport, name: &str) -> (String, bool, Option<String>) {
let check = report
.models
.iter()
.find(|c| c.name == name)
.unwrap_or_else(|| panic!("{name} missing from {:?}", report.models));
match &check.status {
ModelStatus::Incomplete {
detail,
purged,
superseded_by,
} => (detail.clone(), *purged, superseded_by.clone()),
other => panic!("{name}: expected Incomplete, got {other:?}"),
}
}
#[cfg(unix)]
#[test]
fn repair_clears_an_interrupted_install_and_never_the_hub() {
let fx = interrupted_install("Qwen3.5-9B-MLX-vLLM");
let diagnosed = diagnose_at_isolated(&fx.home, &fx.models, &fx.hub, &opts(false, false));
let (detail, purged, _) = incomplete(&diagnosed, "Qwen3.5-9B-MLX-vLLM");
assert!(!purged);
assert!(
detail.contains("`car doctor --repair`"),
"the remedy must name the command that clears it: {detail}"
);
assert!(fx.dir.exists(), "diagnosis alone never deletes");
let repaired = diagnose_at_isolated(&fx.home, &fx.models, &fx.hub, &opts(false, true));
let (detail, purged, _) = incomplete(&repaired, "Qwen3.5-9B-MLX-vLLM");
assert!(purged, "repair must clear it: {detail}");
assert!(
std::fs::symlink_metadata(&fx.dir).is_err(),
"the managed directory is gone"
);
assert!(
repaired
.repairs
.iter()
.any(|r| r.contains("removed the interrupted install of 'Qwen3.5-9B-MLX-vLLM'")),
"the purge is reported as a repair: {:?}",
repaired.repairs
);
for file in [
"config.json",
"tokenizer.json",
"model.safetensors.index.json",
] {
assert!(
fx.snapshot.join(file).exists(),
"the shared hub's {file} must survive"
);
}
let after = diagnose_at_isolated(&fx.home, &fx.models, &fx.hub, &opts(false, false));
assert!(after.models.is_empty(), "nothing left to report");
}
#[cfg(unix)]
#[test]
fn repair_refuses_a_directory_holding_what_a_pull_does_not_write() {
let fx = interrupted_install("Mixed");
std::fs::create_dir(fx.dir.join("notes")).unwrap();
std::fs::write(fx.dir.join("notes/mine.txt"), b"keep").unwrap();
let r = diagnose_at_isolated(&fx.home, &fx.models, &fx.hub, &opts(false, true));
let (detail, purged, _) = incomplete(&r, "Mixed");
assert!(!purged, "{detail}");
assert!(fx.dir.join("notes/mine.txt").exists());
assert!(
std::fs::symlink_metadata(fx.dir.join("config.json")).is_ok(),
"a refusal touches nothing, not even the links"
);
assert!(
r.repairs
.iter()
.any(|x| x.contains("left the interrupted install of 'Mixed' in place")),
"{:?}",
r.repairs
);
}
#[cfg(unix)]
#[test]
fn repair_never_clears_through_a_link_outside_the_hub() {
let fx = interrupted_install("InHub");
std::fs::remove_dir_all(&fx.dir).unwrap();
let hub_snapshot_links = fx.hub.join("models--org--repo/snapshots/linked");
std::fs::create_dir_all(&hub_snapshot_links).unwrap();
std::os::unix::fs::symlink(
fx.snapshot.join("config.json"),
hub_snapshot_links.join("config.json"),
)
.unwrap();
std::os::unix::fs::symlink(&hub_snapshot_links, &fx.dir).unwrap();
let theirs = fx.home.parent().unwrap().join("external/Theirs");
std::fs::create_dir_all(&theirs).unwrap();
std::os::unix::fs::symlink(fx.snapshot.join("config.json"), theirs.join("config.json"))
.unwrap();
let theirs_leaf = fx.models.join("Theirs");
std::os::unix::fs::symlink(&theirs, &theirs_leaf).unwrap();
let r = diagnose_at_isolated(&fx.home, &fx.models, &fx.hub, &opts(false, true));
let (_, purged_in_hub, _) = incomplete(&r, "InHub");
assert!(purged_in_hub);
assert!(
std::fs::symlink_metadata(&fx.dir).is_err(),
"the hub link goes"
);
assert!(
std::fs::symlink_metadata(hub_snapshot_links.join("config.json")).is_ok(),
"what it pointed at in the hub stays"
);
let (detail, purged_theirs, _) = incomplete(&r, "Theirs");
assert!(!purged_theirs, "{detail}");
assert!(std::fs::symlink_metadata(&theirs_leaf).is_ok());
assert!(std::fs::symlink_metadata(theirs.join("config.json")).is_ok());
}
#[cfg(unix)]
#[test]
fn a_live_pulls_staging_directory_is_never_judged() {
let fx = interrupted_install(".car-install-0123456789abcdef-00");
let r = diagnose_at_isolated(&fx.home, &fx.models, &fx.hub, &opts(false, true));
assert!(r.models.is_empty(), "{:?}", r.models);
assert!(
fx.dir.exists(),
"a running pull's staging must survive --repair"
);
}
#[cfg(unix)]
fn mlx_row(id: &str, name: &str, repo: &str) -> crate::schema::ModelSchema {
serde_json::from_value(serde_json::json!({
"id": id,
"name": name,
"provider": "mlx",
"family": "doctor-test",
"capabilities": ["generate"],
"context_length": 4096,
"source": {"type": "mlx", "hf_repo": repo, "hf_weight_file": null}
}))
.unwrap()
}
#[cfg(unix)]
#[test]
fn a_superseded_incomplete_install_is_removable_not_re_pulled() {
let fx = interrupted_install("Old-30B");
let new_snapshot = fx.hub.join("models--org--new/snapshots/rev");
std::fs::create_dir_all(&new_snapshot).unwrap();
std::fs::write(new_snapshot.join("config.json"), b"{}").unwrap();
std::fs::write(new_snapshot.join("model.safetensors"), b"weights").unwrap();
let mut registry = crate::registry::UnifiedRegistry::new_empty(fx.models.clone());
registry.register(mlx_row("mlx/qwen3-30b-a3b:4bit", "Old-30B", "org/repo"));
registry.register(mlx_row(
"vllm-mlx/qwen3.6-35b-a3b:4bit",
"New-35B",
"org/new",
));
let diagnosed = diagnose_at_with_registry(
&fx.home,
&fx.models,
&opts(false, false),
®istry,
Some(&fx.hub),
);
let (detail, _, superseded_by) = incomplete(&diagnosed, "Old-30B");
assert_eq!(
superseded_by.as_deref(),
Some("vllm-mlx/qwen3.6-35b-a3b:4bit")
);
assert!(!detail.contains("car models pull"), "{detail}");
assert!(detail.contains("supersedes"), "{detail}");
let repaired = diagnose_at_with_registry(
&fx.home,
&fx.models,
&opts(false, true),
®istry,
Some(&fx.hub),
);
assert!(
repaired.repairs.iter().any(|r| r
.contains("superseded by vllm-mlx/qwen3.6-35b-a3b:4bit")
&& r.contains("nothing to re-pull")),
"{:?}",
repaired.repairs
);
assert!(std::fs::symlink_metadata(&fx.dir).is_err());
let fx = interrupted_install("Old-30B");
let mut registry = crate::registry::UnifiedRegistry::new_empty(fx.models.clone());
registry.register(mlx_row("mlx/qwen3-30b-a3b:4bit", "Old-30B", "org/repo"));
registry.register(mlx_row(
"vllm-mlx/qwen3.6-35b-a3b:4bit",
"New-35B",
"org/new",
));
let r = diagnose_at_with_registry(
&fx.home,
&fx.models,
&opts(false, false),
®istry,
Some(&fx.hub),
);
let (detail, _, superseded_by) = incomplete(&r, "Old-30B");
assert_eq!(superseded_by, None);
assert!(
detail.contains("`car models pull mlx/qwen3-30b-a3b:4bit`"),
"{detail}"
);
}
#[cfg(unix)]
#[test]
fn repair_never_clears_a_receipt_backed_install() {
let fx = interrupted_install("Owned");
let mut registry = crate::registry::UnifiedRegistry::new_empty(fx.models.clone());
registry.register(mlx_row("mlx/owned:4bit", "Owned", "org/repo"));
let receipts = fx.home.join("model-management/receipts");
std::fs::create_dir_all(&receipts).unwrap();
let receipt = receipts.join(crate::model_management::receipt_file_name_for_test(
"mlx/owned:4bit",
));
std::fs::write(&receipt, b"{}").unwrap();
let r = diagnose_at_with_registry(
&fx.home,
&fx.models,
&opts(false, true),
®istry,
Some(&fx.hub),
);
let (detail, purged, _) = incomplete(&r, "Owned");
assert!(!purged);
assert!(
detail.contains("`car models remove mlx/owned:4bit`"),
"{detail}"
);
assert!(fx.dir.exists());
}
#[cfg(unix)]
#[test]
fn repair_waits_out_a_pull_of_the_same_model_in_another_process() {
let fx = interrupted_install("Busy");
let mut registry = crate::registry::UnifiedRegistry::new_empty(fx.models.clone());
registry.register(mlx_row("mlx/busy:4bit", "Busy", "org/repo"));
let store =
crate::model_management::ModelManagementStore::new(fx.home.clone(), fx.models.clone());
let held = store.begin_mutation("mlx/busy:4bit").unwrap();
let r = diagnose_at_with_registry(
&fx.home,
&fx.models,
&opts(false, true),
®istry,
Some(&fx.hub),
);
let (detail, purged, _) = incomplete(&r, "Busy");
assert!(!purged, "{detail}");
assert!(
std::fs::symlink_metadata(fx.dir.join("config.json")).is_ok(),
"nothing is removed while another process holds the model"
);
assert!(
r.repairs.iter().any(|x| x.contains("running right now")),
"{:?}",
r.repairs
);
drop(held);
let r = diagnose_at_with_registry(
&fx.home,
&fx.models,
&opts(false, true),
®istry,
Some(&fx.hub),
);
let (detail, purged, _) = incomplete(&r, "Busy");
assert!(purged, "{detail}");
assert!(
detail.contains("if you still want it, `car models pull mlx/busy:4bit`"),
"the re-pull is an option, not the next step: {detail}"
);
}
#[cfg(unix)]
#[test]
fn repair_keeps_small_files_a_pull_does_not_write() {
for stray in ["weights.npz", "notes.txt", "pytorch_model.bin"] {
let fx = interrupted_install("Stray");
std::fs::write(fx.dir.join(stray), b"not a pull's").unwrap();
let r = diagnose_at_isolated(&fx.home, &fx.models, &fx.hub, &opts(false, true));
let (detail, purged, _) = incomplete(&r, "Stray");
assert!(!purged, "{stray}: {detail}");
assert!(fx.dir.join(stray).exists(), "{stray} survives");
assert!(
std::fs::symlink_metadata(fx.dir.join("config.json")).is_ok(),
"{stray}: a refusal touches nothing"
);
}
let fx = interrupted_install("Copied");
for name in [
"generation_config.json",
"vocab.json",
".tokenizer_config.json.car-install-42-0.tmp",
] {
std::fs::write(fx.dir.join(name), b"{}").unwrap();
}
let r = diagnose_at_isolated(&fx.home, &fx.models, &fx.hub, &opts(false, true));
let (detail, purged, _) = incomplete(&r, "Copied");
assert!(purged, "{detail}");
assert!(std::fs::symlink_metadata(&fx.dir).is_err());
}
#[cfg(unix)]
#[test]
fn a_purge_removes_the_links_that_mark_an_interrupted_install_last() {
use crate::model_management::UnlinkKind;
let fx = interrupted_install("Halfway");
for name in ["generation_config.json", "vocab.json"] {
std::fs::write(fx.dir.join(name), b"{}").unwrap();
}
let plan = purge_plan(&fx.dir).unwrap();
let first_link = plan
.iter()
.position(|(_, kind)| *kind == UnlinkKind::Link)
.expect("the manifests are links");
assert!(
plan[first_link..]
.iter()
.all(|(_, kind)| *kind == UnlinkKind::Link),
"a regular file is planned after a link: {plan:?}"
);
assert_eq!(plan.len(), 5, "{plan:?}");
}
#[cfg(unix)]
#[test]
fn repair_reclaims_only_abandoned_partials_of_car_s_own_repos() {
let (repo, _) = crate::retire::catalog_ownership()
.into_iter()
.find(|(_, owned)| *owned == crate::retire::Owned::Whole)
.expect("the builtin catalog fetches at least one repo whole");
let hf = TempDir::new().unwrap();
let hub = hf.path().join("hub");
let blobs_of = |repo: &str| {
let dir = hub
.join(format!("models--{}", repo.replace('/', "--")))
.join("blobs");
std::fs::create_dir_all(&dir).unwrap();
dir
};
let stale = std::time::SystemTime::now()
- std::time::Duration::from_secs(crate::retire::ABANDONED_PARTIAL_SECS + 60);
let plant = |path: &Path, old: bool| {
std::fs::write(path, vec![0u8; 1024]).unwrap();
if old {
std::fs::File::options()
.write(true)
.open(path)
.unwrap()
.set_modified(stale)
.unwrap();
}
};
let ours = blobs_of(&repo);
let abandoned = ours.join("old.sync.part");
let running = ours.join("fresh.sync.part");
let theirs = blobs_of("someone/else").join("old.sync.part");
plant(&abandoned, true);
plant(&running, false);
plant(&theirs, true);
let home = TempDir::new().unwrap();
let diagnose = |repair| {
diagnose_at_isolated(
home.path(),
&home.path().join("models"),
&hub,
&opts(false, repair),
)
};
let reclaimable = |r: &DoctorReport, path: &Path| {
r.leftovers
.iter()
.find(|l| Path::new(&l.path) == path)
.map(|l| l.reclaimable)
};
let r = diagnose(false);
assert_eq!(reclaimable(&r, &abandoned), Some(true), "{:?}", r.leftovers);
assert_eq!(reclaimable(&r, &running), Some(false), "{:?}", r.leftovers);
assert_eq!(reclaimable(&r, &theirs), Some(false), "{:?}", r.leftovers);
assert!(abandoned.exists(), "diagnosis alone deletes nothing");
let r = diagnose(true);
assert!(!abandoned.exists(), "{:?}", r.repairs);
assert!(
running.exists(),
"a partial written just now may be a live pull"
);
assert!(
theirs.exists(),
"a repo CAR does not own is not CAR's to clean"
);
assert!(
r.repairs
.iter()
.any(|x| x.contains("removed 1 abandoned partial download(s)")),
"{:?}",
r.repairs
);
assert_eq!(r.leftovers.len(), 2, "{:?}", r.leftovers);
assert!(r.leftovers.iter().all(|l| !l.reclaimable));
}
#[test]
fn unrecognized_entries_are_reported_not_removed() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("mystery-leftover.json"), "{}").unwrap();
std::fs::create_dir_all(tmp.path().join("old_install_dir")).unwrap();
let r = diagnose_in(tmp.path(), &opts(false, true));
assert!(r
.unrecognized
.contains(&"mystery-leftover.json".to_string()));
assert!(r.unrecognized.contains(&"old_install_dir".to_string()));
assert!(tmp.path().join("mystery-leftover.json").exists());
assert!(tmp.path().join("old_install_dir").exists());
}
#[test]
fn version_skew_detected() {
let tmp = TempDir::new().unwrap();
let stale = VersionStamp {
car_version: "0.0.1-ancient".to_string(),
state_schema_version: 1,
previous_car_version: None,
previous_state_schema_version: None,
};
std::fs::write(
tmp.path().join("version.json"),
serde_json::to_string(&stale).unwrap(),
)
.unwrap();
let r = diagnose_in(tmp.path(), &opts(false, false));
assert!(r.version_skew);
assert!(!r.is_healthy());
let _ = diagnose_in(tmp.path(), &opts(false, true));
let r = diagnose_in(tmp.path(), &opts(false, false));
assert!(!r.version_skew, "stamp refreshed, skew cleared");
}
}
#[cfg(test)]
mod version_stamp_continuity_tests {
use super::*;
fn write(home: &Path, stamp: &VersionStamp) {
std::fs::create_dir_all(home).unwrap();
std::fs::write(
home.join("version.json"),
serde_json::to_string_pretty(stamp).unwrap(),
)
.unwrap();
}
fn stamp(version: &str, schema: u32) -> VersionStamp {
VersionStamp {
car_version: version.to_string(),
state_schema_version: schema,
previous_car_version: None,
previous_state_schema_version: None,
}
}
#[test]
fn stamping_after_an_upgrade_preserves_what_it_replaced() {
let dir = tempfile::tempdir().unwrap();
write(dir.path(), &stamp("0.39.0", 1));
let t = stamp_version(dir.path()).unwrap();
assert!(t.upgraded(), "0.39.0 -> current is an upgrade");
assert_eq!(
t.current.previous_car_version.as_deref(),
Some("0.39.0"),
"the predecessor must survive the stamp that replaced it"
);
let report = diagnose_in(dir.path(), &DoctorOptions::default());
assert_eq!(
report.carried_from_version.as_deref(),
Some("0.39.0"),
"doctor must still see it AFTER the daemon stamped"
);
}
#[test]
fn rebooting_on_the_same_version_keeps_the_original_predecessor() {
let dir = tempfile::tempdir().unwrap();
write(dir.path(), &stamp("0.39.0", 1));
stamp_version(dir.path()).unwrap(); for _ in 0..5 {
stamp_version(dir.path()).unwrap(); }
let report = diagnose_in(dir.path(), &DoctorOptions::default());
assert_eq!(
report.carried_from_version.as_deref(),
Some("0.39.0"),
"five reboots must not rewrite the predecessor to the current version"
);
}
#[test]
fn a_fresh_install_records_no_predecessor() {
let dir = tempfile::tempdir().unwrap();
let t = stamp_version(dir.path()).unwrap();
assert!(t.previous.is_none());
assert!(!t.upgraded());
assert!(t.current.previous_car_version.is_none());
assert!(!diagnose_in(dir.path(), &DoctorOptions::default()).schema_from_the_future);
}
#[test]
fn state_from_a_newer_schema_is_flagged() {
let dir = tempfile::tempdir().unwrap();
write(dir.path(), &stamp("99.0.0", STATE_SCHEMA_VERSION + 7));
let t = stamp_version(dir.path()).unwrap();
assert!(
t.schema_from_the_future(),
"newer on-disk schema must be flagged"
);
write(dir.path(), &stamp("99.0.0", STATE_SCHEMA_VERSION + 7));
let report = diagnose_in(dir.path(), &DoctorOptions::default());
assert!(report.schema_from_the_future);
assert!(!report.is_healthy());
}
#[test]
fn carrying_state_across_an_upgrade_is_not_unhealthy() {
let dir = tempfile::tempdir().unwrap();
write(dir.path(), &stamp("0.39.0", STATE_SCHEMA_VERSION));
stamp_version(dir.path()).unwrap();
let report = diagnose_in(dir.path(), &DoctorOptions::default());
assert!(report.carried_from_version.is_some());
assert!(report.is_healthy(), "an ordinary upgrade is not a defect");
}
#[test]
fn a_pre_existing_stamp_without_the_new_fields_still_loads() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path()).unwrap();
std::fs::write(
dir.path().join("version.json"),
r#"{"car_version":"0.39.0","state_schema_version":1}"#,
)
.unwrap();
let t = stamp_version(dir.path()).unwrap();
assert_eq!(
t.previous.as_ref().map(|p| p.car_version.as_str()),
Some("0.39.0")
);
assert_eq!(t.current.previous_car_version.as_deref(), Some("0.39.0"));
}
#[test]
fn a_corrupt_stamp_is_treated_as_absent_and_replaced() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path()).unwrap();
std::fs::write(dir.path().join("version.json"), "{ not json").unwrap();
let t = stamp_version(dir.path()).unwrap();
assert!(t.previous.is_none(), "unreadable == no usable predecessor");
assert_eq!(t.current.car_version, VersionStamp::current().car_version);
}
}
#[cfg(test)]
mod runtime_check_tests {
use super::*;
#[test]
fn absent_runtimes_are_not_reported() {
let dir = tempfile::tempdir().unwrap();
assert!(check_runtimes(dir.path()).is_empty());
}
#[cfg(unix)]
#[test]
fn rotated_away_interpreter_is_reported_broken() {
let dir = tempfile::tempdir().unwrap();
let bin = dir.path().join("visual-runtime").join("bin");
std::fs::create_dir_all(&bin).unwrap();
std::os::unix::fs::symlink(
"/opt/homebrew/opt/python@0.0/bin/python0.0",
bin.join("python"),
)
.unwrap();
let checks = check_runtimes(dir.path());
assert_eq!(checks.len(), 1);
assert_eq!(checks[0].name, "visual-runtime");
assert!(checks[0].present);
assert!(!checks[0].interpreter_ok);
assert!(checks[0].is_broken());
}
#[cfg(unix)]
fn running_as_root() -> bool {
std::process::Command::new("id")
.arg("-u")
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim() == "0")
.unwrap_or(false)
}
#[test]
fn coder_state_usage_is_measured_whole_and_says_when_it_is_not() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
assert!(measure_coder_state(home).is_none(), "no coder dir, no row");
let kept = home.join("coder/worktrees/kept-session");
std::fs::create_dir_all(&kept).unwrap();
std::fs::write(kept.join("edit.rs"), vec![b'x'; 1000]).unwrap();
std::fs::create_dir_all(home.join("coder/sessions")).unwrap();
std::fs::write(home.join("coder/sessions/s.json"), vec![b'y'; 24]).unwrap();
let outside = home.join("elsewhere");
std::fs::create_dir_all(&outside).unwrap();
std::fs::write(outside.join("big"), vec![b'z'; 5000]).unwrap();
#[cfg(unix)]
std::os::unix::fs::symlink(&outside, home.join("coder/link")).unwrap();
let usage = measure_coder_state(home).unwrap();
assert_eq!((usage.bytes, usage.files), (1024, 2), "{usage:?}");
assert!(usage.is_complete(), "{usage:?}");
assert_eq!(
std::fs::read(kept.join("edit.rs")).unwrap().len(),
1000,
"nothing removed"
);
let report = diagnose_in(home, &DoctorOptions::default());
assert_eq!(report.coder_state.as_ref().map(|u| u.bytes), Some(1024));
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let locked = home.join("coder/locked");
std::fs::create_dir_all(&locked).unwrap();
std::fs::write(locked.join("hidden"), b"h").unwrap();
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).unwrap();
let usage = measure_coder_state(home).unwrap();
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755)).unwrap();
if usage.unreadable.is_empty() && running_as_root() {
return;
}
assert!(!usage.is_complete(), "{usage:?}");
assert!(usage.unreadable[0].contains("locked"), "{usage:?}");
assert_eq!(usage.bytes, 1024, "the readable part is still counted");
}
}
#[test]
fn broken_runtime_makes_report_unhealthy() {
let mut report = DoctorReport {
car_home: "/tmp/x".into(),
binary_version: VersionStamp::current().car_version,
on_disk_stamp: None,
version_skew: false,
carried_from_version: None,
schema_from_the_future: false,
state_files: Vec::new(),
models: Vec::new(),
installed_models: 0,
leftovers: Vec::new(),
revoked_copies: Vec::new(),
catalog: Default::default(),
runtimes: Vec::new(),
unrecognized: Vec::new(),
repairs: Vec::new(),
daemon_tmpdir: None,
daemon_single_principal: false,
daemon_host_role_cause: None,
coder_state: None,
};
assert!(report.is_healthy());
report.runtimes.push(RuntimeCheck {
name: "speech-runtime".into(),
root: "/tmp/x/speech-runtime".into(),
present: true,
interpreter_ok: false,
});
assert!(!report.is_healthy());
}
}
#[cfg(all(test, unix))]
mod daemon_auth_mode_tests {
use super::*;
fn diagnose_home(home: &Path) -> DoctorReport {
let models = tempfile::tempdir().unwrap();
let hub = tempfile::tempdir().unwrap();
diagnose_at_isolated(home, models.path(), hub.path(), &DoctorOptions::default())
}
fn write_marker(home: &Path, pid: u32, single_principal: bool) {
let path = daemon_auth_mode_marker_path(home);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let marker = DaemonAuthModeMarker {
pid,
booted_at_unix: 1_789_000_000,
car_version: VersionStamp::current().car_version,
single_principal,
cause: None,
};
std::fs::write(path, serde_json::to_string(&marker).unwrap()).unwrap();
}
#[test]
fn a_running_single_principal_daemon_is_reported_and_stays_healthy() {
let home = tempfile::tempdir().unwrap();
let recorded =
record_daemon_auth_mode(home.path(), true, Some("--no-auth".into())).expect("record");
assert_eq!(recorded.pid, std::process::id());
let report = diagnose_home(home.path());
assert!(report.daemon_single_principal, "{report:?}");
assert_eq!(
report.daemon_host_role_cause.as_deref(),
Some("--no-auth"),
"{report:?}"
);
assert!(report.is_healthy(), "informational, never a health failure");
}
#[test]
fn a_host_token_daemon_a_gone_daemon_and_no_record_are_not_reported() {
let home = tempfile::tempdir().unwrap();
assert!(!diagnose_home(home.path()).daemon_single_principal);
record_daemon_auth_mode(home.path(), false, Some("ignored".into())).expect("record");
assert!(!running_daemon_is_single_principal(home.path()));
let mut child = std::process::Command::new("true")
.spawn()
.expect("spawn `true`");
let dead = child.id();
child.wait().expect("reap `true`");
write_marker(home.path(), dead, true);
assert!(
!running_daemon_is_single_principal(home.path()),
"a record from a daemon that exited describes no running daemon"
);
std::fs::write(daemon_auth_mode_marker_path(home.path()), b"{not json").unwrap();
assert!(!running_daemon_is_single_principal(home.path()));
}
}
#[cfg(test)]
mod daemon_tmpdir_tests {
use super::*;
const NOT_A_DIRECTORY: &[u8] = b"a regular file where TMPDIR should be";
fn marker(pid: u32, ok: bool) -> DaemonTmpdirMarker {
let checked_path = "/private/tmp/PKInstallSandbox.test/tmp".to_string();
DaemonTmpdirMarker {
pid,
booted_at_unix: 1_789_000_000,
car_version: VersionStamp::current().car_version,
checked_path: checked_path.clone(),
checked_path_absolute: Some(checked_path),
ok,
error: (!ok)
.then(|| "create failed: No such file or directory (os error 2)".to_string()),
note: None,
}
}
fn write_marker(home: &Path, marker: &DaemonTmpdirMarker) {
let path = daemon_tmpdir_marker_path(home);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, serde_json::to_string_pretty(marker).unwrap()).unwrap();
}
fn diagnose_home(home: &Path) -> DoctorReport {
let models = tempfile::tempdir().unwrap();
let hub = tempfile::tempdir().unwrap();
diagnose_at_isolated(home, models.path(), hub.path(), &DoctorOptions::default())
}
fn status_of(report: &DoctorReport) -> DaemonTmpdirStatus {
report
.daemon_tmpdir
.clone()
.expect("the record must be reported")
.status
}
#[cfg(unix)]
fn dead_pid() -> u32 {
let mut child = std::process::Command::new("true")
.spawn()
.expect("spawn `true`");
let pid = child.id();
child.wait().expect("reap `true`");
assert!(
!unix_pid_alive(pid),
"a reaped child's pid must read as gone"
);
pid
}
fn healthy_tmpdir(parent: &Path) -> PathBuf {
let dir = parent.join("healthy-tmpdir");
std::fs::create_dir(&dir).unwrap();
dir
}
#[test]
fn probing_a_file_tmpdir_records_a_failure() {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path().join("car-home");
let tmpdir = tmp.path().join("tmpdir-is-a-file");
std::fs::write(&tmpdir, NOT_A_DIRECTORY).unwrap();
let recorded = record_daemon_tmpdir_probe(&home, &tmpdir)
.expect("a failed probe is still a successful record");
assert!(!recorded.ok, "{recorded:?}");
assert_eq!(recorded.pid, std::process::id());
assert_eq!(recorded.checked_path, tmpdir.display().to_string());
let error = recorded.error.clone().expect("a failure names its error");
assert!(error.starts_with("create failed"), "{error}");
#[cfg(unix)]
assert!(
error.contains(&format!("os error {}", libc::ENOTDIR)),
"a file where the directory should be is ENOTDIR: {error}"
);
assert_eq!(read_daemon_tmpdir_marker(&home), Some(Ok(recorded)));
assert_eq!(std::fs::read(&tmpdir).unwrap(), NOT_A_DIRECTORY);
}
#[test]
fn a_healthy_probe_replaces_a_prior_failure_and_leaves_nothing_behind() {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path().join("car-home");
let broken = tmp.path().join("tmpdir-is-a-file");
std::fs::write(&broken, NOT_A_DIRECTORY).unwrap();
let healthy = healthy_tmpdir(tmp.path());
assert!(!record_daemon_tmpdir_probe(&home, &broken).unwrap().ok);
let recorded = record_daemon_tmpdir_probe(&home, &healthy).unwrap();
assert!(recorded.ok, "{recorded:?}");
assert_eq!(recorded.error, None);
assert_eq!(recorded.checked_path_absolute.as_deref(), healthy.to_str());
assert_eq!(read_daemon_tmpdir_marker(&home), Some(Ok(recorded)));
#[cfg(any(unix, windows))]
assert!(
std::fs::read_dir(&healthy).unwrap().next().is_none(),
"Unix unlinks the identity-checked probe and Windows deletes it on close"
);
#[cfg(all(not(unix), not(windows)))]
assert_eq!(
std::fs::read_dir(&healthy).unwrap().count(),
1,
"without portable file identity the tiny probe file is left safely behind"
);
let names: Vec<_> = std::fs::read_dir(home.join(DOCTOR_DIR))
.unwrap()
.map(|entry| entry.unwrap().file_name())
.collect();
assert_eq!(
names,
vec![std::ffi::OsString::from(DAEMON_TMPDIR_MARKER_FILE)],
"no temporary record may be left behind"
);
}
#[cfg(unix)]
#[test]
fn a_symlinked_tmpdir_is_followed_and_its_probe_file_is_removed() {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path().join("car-home");
let target = healthy_tmpdir(tmp.path());
let link = tmp.path().join("tmpdir-link");
std::os::unix::fs::symlink(&target, &link).unwrap();
let recorded = record_daemon_tmpdir_probe(&home, &link).unwrap();
assert!(
recorded.ok,
"a directory symlink is valid TMPDIR: {recorded:?}"
);
assert_eq!(recorded.checked_path, link.display().to_string());
assert!(
std::fs::read_dir(&target).unwrap().next().is_none(),
"the identity-checked probe file is removed from the resolved directory"
);
}
#[cfg(unix)]
#[test]
fn cleanup_does_not_remove_a_different_file_at_the_probe_path() {
let tmp = tempfile::tempdir().unwrap();
let created_path = tmp.path().join("created");
let probe_path = tmp.path().join("substitute");
std::fs::write(&created_path, b"created").unwrap();
std::fs::write(&probe_path, b"substitute").unwrap();
let created = std::fs::metadata(&created_path).unwrap();
let error = remove_probe_file(&probe_path, &created).unwrap_err();
assert!(error.starts_with("remove skipped"), "{error}");
assert_eq!(std::fs::read(&probe_path).unwrap(), b"substitute");
}
#[test]
fn a_stat_failure_after_create_and_write_records_healthy_with_a_note() {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path().join("car-home");
let tmpdir = healthy_tmpdir(tmp.path());
let result = combine_probe_results(
Err("stat failed: injected failure".to_string()),
Ok(()),
Err("cleanup skipped: identity could not be proven; file left".to_string()),
);
let recorded = record_daemon_tmpdir_probe_result(&home, &tmpdir, result).unwrap();
assert!(
recorded.ok,
"create and write proved TMPDIR works: {recorded:?}"
);
assert_eq!(recorded.error, None);
let note = recorded.note.as_deref().expect("stat failure is retained");
assert!(note.contains("stat failed: injected failure"), "{note}");
assert!(note.contains("identity could not be proven"), "{note}");
assert!(note.contains("left"), "{note}");
let report = diagnose_home(&home);
assert!(!report.daemon_tmpdir.as_ref().unwrap().is_failing());
assert!(
report.is_healthy(),
"a note must keep doctor at exit 0: {report:?}"
);
}
#[cfg(windows)]
#[test]
fn windows_deletes_the_probe_file_when_its_handle_closes() {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path().join("car-home");
let healthy = healthy_tmpdir(tmp.path());
let recorded = record_daemon_tmpdir_probe(&home, &healthy).unwrap();
assert!(recorded.ok, "{recorded:?}");
assert!(
std::fs::read_dir(&healthy).unwrap().next().is_none(),
"FILE_FLAG_DELETE_ON_CLOSE must remove the probe file"
);
}
#[test]
fn the_record_does_not_trip_find_unrecognized() {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path().join("car-home");
let healthy = healthy_tmpdir(tmp.path());
record_daemon_tmpdir_probe(&home, &healthy).unwrap();
assert!(daemon_tmpdir_marker_path(&home).is_file());
assert!(KNOWN_DIRS.contains(&DOCTOR_DIR));
assert!(
find_unrecognized(&home).is_empty(),
"{:?}",
find_unrecognized(&home)
);
assert!(diagnose_home(&home).unrecognized.is_empty());
}
#[test]
fn an_absent_record_is_not_an_error_and_adds_no_json_key() {
let home = tempfile::tempdir().unwrap();
let report = diagnose_home(home.path());
assert!(report.daemon_tmpdir.is_none());
assert!(report.is_healthy(), "{report:?}");
let json = serde_json::to_value(&report).unwrap();
assert!(
json.get("daemon_tmpdir").is_none(),
"additive: a report without a record serializes as before"
);
}
#[cfg(unix)]
#[test]
fn doctor_reads_failure_healthy_stale_and_absent_records() {
let home = tempfile::tempdir().unwrap();
let live = std::process::id();
let absent = diagnose_home(home.path());
assert!(absent.daemon_tmpdir.is_none() && absent.is_healthy());
let failed = marker(live, false);
write_marker(home.path(), &failed);
let report = diagnose_home(home.path());
assert_eq!(
status_of(&report),
DaemonTmpdirStatus::Current { marker: failed }
);
assert!(!report.is_healthy(), "failure from the running daemon");
let mut healthy = marker(live, true);
healthy.checked_path = healthy_tmpdir(home.path()).display().to_string();
healthy.checked_path_absolute = Some(healthy.checked_path.clone());
write_marker(home.path(), &healthy);
let report = diagnose_home(home.path());
assert_eq!(
status_of(&report),
DaemonTmpdirStatus::Current { marker: healthy }
);
assert!(
report.is_healthy(),
"healthy record from the running daemon"
);
let stale = marker(dead_pid(), false);
write_marker(home.path(), &stale);
let report = diagnose_home(home.path());
assert_eq!(
status_of(&report),
DaemonTmpdirStatus::Stale { marker: stale }
);
assert!(report.is_healthy(), "a stale failure is not failing");
}
#[cfg(unix)]
#[test]
fn a_running_daemons_failed_probe_is_unhealthy() {
let home = tempfile::tempdir().unwrap();
let recorded = marker(std::process::id(), false);
write_marker(home.path(), &recorded);
let report = diagnose_home(home.path());
let check = report
.daemon_tmpdir
.clone()
.expect("the record is reported");
assert_eq!(
check.marker_path,
daemon_tmpdir_marker_path(home.path()).display().to_string()
);
assert_eq!(
check.status,
DaemonTmpdirStatus::Current { marker: recorded }
);
assert!(check.is_failing());
assert!(!report.is_healthy(), "{report:?}");
}
#[cfg(unix)]
#[test]
fn a_record_from_a_daemon_that_is_gone_is_stale_not_failing() {
let home = tempfile::tempdir().unwrap();
let recorded = marker(dead_pid(), false);
write_marker(home.path(), &recorded);
let report = diagnose_home(home.path());
let check = report
.daemon_tmpdir
.clone()
.expect("the record is reported");
assert_eq!(check.status, DaemonTmpdirStatus::Stale { marker: recorded });
assert!(!check.is_failing());
assert!(report.is_healthy(), "{report:?}");
}
#[cfg(unix)]
#[test]
fn a_current_ok_record_whose_checked_path_disappeared_is_failing() {
let home = tempfile::tempdir().unwrap();
let checked_path = healthy_tmpdir(home.path());
let mut recorded = marker(std::process::id(), true);
recorded.checked_path = checked_path.display().to_string();
recorded.checked_path_absolute = Some(recorded.checked_path.clone());
write_marker(home.path(), &recorded);
std::fs::remove_dir(&checked_path).unwrap();
let report = diagnose_home(home.path());
assert_eq!(
status_of(&report),
DaemonTmpdirStatus::CurrentPathGone {
marker: recorded.clone()
}
);
assert!(report.daemon_tmpdir.as_ref().unwrap().is_failing());
assert!(
!report.is_healthy(),
"the running daemon's vanished TMPDIR must produce exit 1"
);
}
#[test]
fn a_non_not_found_recheck_error_is_distinct_and_failing() {
let recorded = marker(std::process::id(), true);
let status = recheck_current_tmpdir_with(
DaemonTmpdirStatus::Current {
marker: recorded.clone(),
},
|_| {
Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"injected permission denied",
))
},
);
assert_eq!(
status,
DaemonTmpdirStatus::CurrentPathRecheckFailed {
marker: recorded,
error: "injected permission denied".to_string(),
}
);
let check = DaemonTmpdirCheck {
marker_path: "doctor/daemon-tmpdir.json".to_string(),
status,
};
assert!(check.is_failing());
}
#[cfg(unix)]
#[test]
fn a_relative_probe_path_survives_doctor_running_from_a_different_cwd() {
let runner_temp_root = std::env::temp_dir();
let fixture_root = if runner_temp_root.to_str().is_some() {
runner_temp_root.as_path()
} else {
Path::new(env!("CARGO_MANIFEST_DIR"))
};
assert!(
fixture_root.to_str().is_some(),
"the fixture root must be valid UTF-8"
);
let daemon_cwd = tempfile::Builder::new()
.prefix("car-daemon-cwd-")
.tempdir_in(fixture_root)
.unwrap();
let doctor_cwd = tempfile::Builder::new()
.prefix("car-doctor-cwd-")
.tempdir_in(fixture_root)
.unwrap();
assert!(daemon_cwd.path().to_str().is_some());
assert!(doctor_cwd.path().to_str().is_some());
let relative = Path::new("relative-tmpdir");
let actual = daemon_cwd.path().join(relative);
std::fs::create_dir(&actual).unwrap();
assert!(
matches!(
std::fs::metadata(doctor_cwd.path().join(relative)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound
),
"the old display-path recheck would report this path as gone"
);
let (exact, note) =
exact_absolute_tmpdir_path(relative, Ok(daemon_cwd.path().to_path_buf()));
assert_eq!(note, None);
assert_eq!(exact.as_deref(), actual.to_str());
let mut recorded = marker(std::process::id(), true);
recorded.checked_path = relative.display().to_string();
recorded.checked_path_absolute = exact;
let mut received_path = None;
let status = recheck_current_tmpdir_with(
DaemonTmpdirStatus::Current {
marker: recorded.clone(),
},
|path| {
received_path = Some(path.to_path_buf());
std::fs::metadata(path).map(|_| ())
},
);
assert_eq!(received_path.as_deref(), Some(actual.as_path()));
assert!(received_path
.as_ref()
.is_some_and(|path| path.is_absolute()));
assert_eq!(status, DaemonTmpdirStatus::Current { marker: recorded });
}
#[cfg(unix)]
#[test]
fn an_old_record_without_an_exact_path_skips_recheck_with_a_note() {
let home = tempfile::tempdir().unwrap();
let mut recorded = marker(std::process::id(), true);
recorded.checked_path_absolute = None;
write_marker(home.path(), &recorded);
let report = diagnose_home(home.path());
let DaemonTmpdirStatus::Current { marker } = status_of(&report) else {
panic!("old record must remain current: {:?}", report.daemon_tmpdir);
};
assert!(marker
.note
.as_deref()
.is_some_and(|note| note.contains("startup record has no exact absolute probe path")));
assert!(
report.is_healthy(),
"skipping an unsafe guess is informational"
);
}
#[cfg(unix)]
#[test]
fn a_non_utf8_probe_path_skips_future_rechecks_with_a_note() {
use std::os::unix::ffi::OsStringExt;
let path = PathBuf::from(std::ffi::OsString::from_vec(vec![
b'/', b't', b'm', b'p', b'/', 0xff,
]));
let (exact, note) = exact_absolute_tmpdir_path(&path, Ok(PathBuf::from("/ignored")));
assert_eq!(exact, None);
assert!(note
.as_deref()
.is_some_and(|note| note.contains("not valid UTF-8")));
}
#[test]
fn an_ok_record_is_healthy() {
let home = tempfile::tempdir().unwrap();
let checked_path = healthy_tmpdir(home.path());
let mut recorded = marker(std::process::id(), true);
recorded.checked_path = checked_path.display().to_string();
recorded.checked_path_absolute = Some(recorded.checked_path.clone());
write_marker(home.path(), &recorded);
let report = diagnose_home(home.path());
#[cfg(any(unix, windows))]
assert_eq!(
status_of(&report),
DaemonTmpdirStatus::Current { marker: recorded }
);
#[cfg(not(any(unix, windows)))]
assert_eq!(
status_of(&report),
DaemonTmpdirStatus::Unverified { marker: recorded }
);
assert!(report.is_healthy(), "{report:?}");
}
#[cfg(any(unix, windows))]
#[test]
fn the_liveness_check_separates_this_process_from_a_dead_one() {
assert_eq!(daemon_writer_alive(std::process::id()), Some(true));
assert_eq!(daemon_writer_alive(0), Some(false));
}
#[test]
fn without_a_liveness_check_a_failed_record_is_unverified_not_failing() {
let recorded = marker(std::process::id(), false);
let status = classify_daemon_tmpdir_marker(recorded.clone(), None);
assert_eq!(status, DaemonTmpdirStatus::Unverified { marker: recorded });
let check = DaemonTmpdirCheck {
marker_path: "doctor/daemon-tmpdir.json".to_string(),
status,
};
assert!(!check.is_failing());
}
#[test]
fn an_unreadable_record_is_reported_not_failing() {
let home = tempfile::tempdir().unwrap();
let path = daemon_tmpdir_marker_path(home.path());
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, "{ not json").unwrap();
let report = diagnose_home(home.path());
assert!(
matches!(status_of(&report), DaemonTmpdirStatus::Unreadable { .. }),
"{:?}",
report.daemon_tmpdir
);
assert!(report.is_healthy(), "{report:?}");
}
#[test]
fn a_record_serializes_additively_and_round_trips() {
let home = tempfile::tempdir().unwrap();
write_marker(home.path(), &marker(std::process::id(), true));
let json = serde_json::to_value(diagnose_home(home.path())).unwrap();
let record = json
.get("daemon_tmpdir")
.expect("the record serializes under `daemon_tmpdir`");
assert!(
record.get("status").is_some() && record.get("marker_path").is_some(),
"{record}"
);
assert_eq!(record["marker"]["ok"], serde_json::Value::Bool(true));
let back: DoctorReport = serde_json::from_value(json).unwrap();
assert!(back.daemon_tmpdir.is_some());
}
#[cfg(unix)]
#[test]
fn the_record_is_never_written_through_a_symlinked_doctor_dir() {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path().join("car-home");
std::fs::create_dir(&home).unwrap();
let elsewhere = tmp.path().join("elsewhere");
std::fs::create_dir(&elsewhere).unwrap();
std::os::unix::fs::symlink(&elsewhere, home.join(DOCTOR_DIR)).unwrap();
let healthy = healthy_tmpdir(tmp.path());
assert!(record_daemon_tmpdir_probe(&home, &healthy).is_err());
assert!(
std::fs::read_dir(&elsewhere).unwrap().next().is_none(),
"nothing may land where the symlink points"
);
}
#[cfg(unix)]
#[test]
fn a_symlink_at_the_record_path_is_replaced_not_followed() {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path().join("car-home");
std::fs::create_dir_all(home.join(DOCTOR_DIR)).unwrap();
let victim = tmp.path().join("victim");
std::fs::write(&victim, b"victim").unwrap();
std::os::unix::fs::symlink(&victim, daemon_tmpdir_marker_path(&home)).unwrap();
let healthy = healthy_tmpdir(tmp.path());
let recorded = record_daemon_tmpdir_probe(&home, &healthy).unwrap();
assert_eq!(std::fs::read(&victim).unwrap(), b"victim");
assert!(std::fs::symlink_metadata(daemon_tmpdir_marker_path(&home))
.unwrap()
.file_type()
.is_file());
assert_eq!(read_daemon_tmpdir_marker(&home), Some(Ok(recorded)));
}
}