use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::download::{
cache_file_usable, purge_corrupt_cache_files, 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",
];
const KNOWN_NON_JSON_FILES: &[&str] = &[
"env",
"peer-identity.key",
];
const KNOWN_DIRS: &[&str] = &[
"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",
];
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 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,
pub ok: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "status")]
pub enum DaemonTmpdirStatus {
Current { marker: DaemonTmpdirMarker },
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)
}
}
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> {
let error = probe_daemon_tmpdir(tmpdir).err();
record_daemon_tmpdir_probe_result(car_home, tmpdir, error)
}
pub fn probe_daemon_tmpdir(tmpdir: &Path) -> Result<(), String> {
probe_tmpdir(tmpdir, std::process::id())
}
pub fn record_daemon_tmpdir_probe_result(
car_home: &Path,
tmpdir: &Path,
error: Option<String>,
) -> std::io::Result<DaemonTmpdirMarker> {
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(),
ok: error.is_none(),
error,
};
write_daemon_tmpdir_marker(car_home, &marker)?;
Ok(marker)
}
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<(), String> {
use std::io::Write;
let path = tmpdir.join(format!(".car-tmpdir-probe.{}", unique_probe_suffix(pid)));
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.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(|_| ()).and(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),
Err(_) => Err(format!(
"cleanup skipped: probe file identity could not be proven; left {}",
path.display()
)),
}
}
fn combine_probe_results(
operation: Result<(), String>,
cleanup: Result<(), String>,
) -> Result<(), String> {
match (operation, cleanup) {
(Ok(()), Ok(())) => Ok(()),
(Err(error), Ok(())) | (Ok(()), Err(error)) => Err(error),
(Err(operation), Err(cleanup)) => Err(format!("{operation}; {cleanup}")),
}
}
#[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<()> {
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(marker)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
json.push(b'\n');
let tmp = dir.join(format!(
"{DAEMON_TMPDIR_MARKER_FILE}.{}.tmp",
unique_probe_suffix(marker.pid)
));
let mut file = car_secrets::create_private_file(&tmp)?;
file.write_all(&json)?;
car_secrets::atomic_replace_private_file(&tmp, &dir.join(DAEMON_TMPDIR_MARKER_FILE))
}
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> {
#[cfg(unix)]
{
Some(unix_pid_alive(pid))
}
#[cfg(not(unix))]
{
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)
}
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);
classify_daemon_tmpdir_marker(marker, writer_alive)
}
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 },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Leftover {
pub path: String,
pub bytes: u64,
}
#[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 runtimes: Vec<RuntimeCheck>,
pub unrecognized: Vec<String>,
pub repairs: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub daemon_tmpdir: Option<DaemonTmpdirCheck>,
}
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 = std::env::var("CAR_CATALOG_PUBKEY").ok();
let registry = crate::registry::UnifiedRegistry::new_with_session(
home.to_path_buf(),
models_dir.to_path_buf(),
catalog_public_key.as_deref(),
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 models = check_models(models_dir, opts, &mut repairs);
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 leftovers = match huggingface_hub_root {
Some(hub) => find_leftovers_in(hub),
None => find_leftovers(),
};
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);
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,
runtimes,
unrecognized,
repairs,
daemon_tmpdir,
}
}
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,
}
}
fn check_models(
models_dir: &Path,
opts: &DoctorOptions,
repairs: &mut Vec<String>,
) -> 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 let Some(status) = check_one_model(&dir, opts, &name, repairs) {
out.push(ModelCheck { name, status });
}
}
out.sort_by(|a, b| a.name.cmp(&b.name));
out
}
fn check_one_model(
dir: &Path,
opts: &DoctorOptions,
name: &str,
repairs: &mut Vec<String>,
) -> Option<ModelStatus> {
let weights = weight_files(dir);
if weights.is_empty() {
if is_interrupted_install(dir) {
return Some(ModelStatus::Incomplete {
detail: format!(
"manifest linked into the HuggingFace cache but no weights resolve — \
re-pull with `car models pull {name}`"
),
});
}
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(dir);
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 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 dir = home.join("journals");
let Ok(entries) = std::fs::read_dir(&dir) else {
return Vec::new();
};
let mut out: Vec<PathBuf> = entries
.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() -> Vec<Leftover> {
find_leftovers_in(&crate::registry::huggingface_cache_root())
}
fn find_leftovers_in(hub: &Path) -> Vec<Leftover> {
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 bytes = e.metadata().map(|m| m.len()).unwrap_or(0);
out.push(Leftover {
path: p.display().to_string(),
bytes,
});
}
}
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 prev = std::env::var_os("HF_HOME");
std::env::set_var("HF_HOME", hf.path());
let r = diagnose_in(home.path(), &opts(false, true));
match prev {
Some(v) => std::env::set_var("HF_HOME", v),
None => std::env::remove_var("HF_HOME"),
}
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");
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!(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
);
}
#[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());
}
#[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"] {
std::fs::write(tmp.path().join(name), b"{}").unwrap();
}
for name in ["proposal-completed-index", "selfheal"] {
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 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());
}
#[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(),
runtimes: Vec::new(),
unrecognized: Vec::new(),
repairs: Vec::new(),
daemon_tmpdir: 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(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 {
DaemonTmpdirMarker {
pid,
booted_at_unix: 1_789_000_000,
car_version: VersionStamp::current().car_version,
checked_path: "/private/tmp/PKInstallSandbox.test/tmp".to_string(),
ok,
error: (!ok)
.then(|| "create failed: No such file or directory (os error 2)".to_string()),
}
}
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!(read_daemon_tmpdir_marker(&home), Some(Ok(recorded)));
#[cfg(unix)]
assert!(
std::fs::read_dir(&healthy).unwrap().next().is_none(),
"the probe removes the one file it created after proving its identity"
);
#[cfg(not(unix))]
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_failed_handle_stat_records_that_the_probe_file_was_left() {
let tmp = tempfile::tempdir().unwrap();
let probe_path = tmp.path().join("probe");
std::fs::write(&probe_path, TMPDIR_PROBE_BYTES).unwrap();
let created = Err("stat failed: injected failure".to_string());
let cleanup = cleanup_probe_file(&probe_path, &created);
let error = combine_probe_results(created.map(|_| ()), cleanup).unwrap_err();
assert!(error.contains("stat failed: injected failure"), "{error}");
assert!(error.contains("identity could not be proven"), "{error}");
assert!(error.contains("left"), "{error}");
assert_eq!(std::fs::read(&probe_path).unwrap(), TMPDIR_PROBE_BYTES);
}
#[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 healthy = marker(live, true);
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:?}");
}
#[test]
fn an_ok_record_is_healthy() {
let home = tempfile::tempdir().unwrap();
let recorded = marker(std::process::id(), true);
write_marker(home.path(), &recorded);
let report = diagnose_home(home.path());
#[cfg(unix)]
assert_eq!(
status_of(&report),
DaemonTmpdirStatus::Current { marker: recorded }
);
#[cfg(not(unix))]
assert_eq!(
status_of(&report),
DaemonTmpdirStatus::Unverified { marker: recorded }
);
assert!(report.is_healthy(), "{report:?}");
}
#[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)));
}
}