- 1
#![allow(clippy::unwrap_used, clippy::expect_used)] - 2
- 3
//! Guards `paths.rs`'s claim to be THE source of truth for the data home. - 4
//! - 5
//! The 0.7 drift incident is why this module owns every home path and why - 6
//! every entry point reads from it. There is no migration step and no legacy - 7
//! dotdir: sessions, gateway state, config, and the shared workspace layer land - 8
//! only in the canonical `data_home()` / `cache_home()` / `logs_dir()` - 9
//! locations, so the tree never has a second home to drift from. The check - 10
//! below fails bluntly on any new hand-rolled `HOME`-relative or - 11
//! `~/.config/vak` path and points the author at `vak_config::paths`. - 12
//! - 13
//! This test reads the workspace's own source. It is deliberately blunt: any - 14
//! new hand-rolled home path fails here with the file and line, and the fix is - 15
//! to call `vak_config::paths`. - 16
- 17
use std::path::{Path, PathBuf}; - 18
- 19
/// Sites that legitimately need the operating-system account home rather than - 20
/// vak's data home, with the reason each is exempt. - 21
const ALLOWED: &[(&str, &str)] = &[ - 22
( - 23
"crates/vak-config/src/paths.rs", - 24
"the module that defines the layout", - 25
), - 26
( - 27
"crates/vak-core/src/install.rs", - 28
"macOS bundle prefix under ~/Applications", - 29
), - 30
( - 31
"crates/vak-ops/src/lib.rs", - 32
"launchd/systemd unit paths are account-home-relative by definition", - 33
), - 34
( - 35
"crates/vak-sandbox/src/backend.rs", - 36
"Seatbelt read-allowlists real toolchain dirs (~/.cargo, ~/.rustup)", - 37
), - 38
( - 39
"crates/vak-sandbox/src/landlock.rs", - 40
"Landlock read-allowlists real toolchain dirs (~/.cargo, ~/.rustup)", - 41
), - 42
]; - 43
- 44
fn workspace_root() -> PathBuf { - 45
Path::new(env!("CARGO_MANIFEST_DIR")) - 46
.ancestors() - 47
.nth(2) - 48
.expect("workspace root") - 49
.to_path_buf() - 50
} - 51
- 52
fn rust_sources(dir: &Path, out: &mut Vec<PathBuf>) { - 53
let Ok(entries) = std::fs::read_dir(dir) else { - 54
return; - 55
}; - 56
for entry in entries.flatten() { - 57
let path = entry.path(); - 58
if path.is_dir() { - 59
let name = entry.file_name(); - 60
if name == "target" || name == "node_modules" || name == "dist" || name == "tests" { - 61
continue; - 62
} - 63
rust_sources(&path, out); - 64
} else if path.extension().is_some_and(|e| e == "rs") { - 65
out.push(path); - 66
} - 67
} - 68
} - 69
- 70
fn is_exempt(relative: &str) -> bool { - 71
ALLOWED.iter().any(|(path, _)| *path == relative) - 72
} - 73
- 74
#[test] - 75
fn no_crate_rebuilds_the_data_home_by_hand() { - 76
let root = workspace_root(); - 77
let mut files = Vec::new(); - 78
rust_sources(&root.join("crates"), &mut files); - 79
assert!(!files.is_empty(), "found no sources to scan"); - 80
- 81
let mut offenders = Vec::new(); - 82
for file in files { - 83
let relative = file - 84
.strip_prefix(&root) - 85
.unwrap_or(&file) - 86
.to_string_lossy() - 87
.replace('\\', "/"); - 88
if is_exempt(&relative) { - 89
continue; - 90
} - 91
let Ok(text) = std::fs::read_to_string(&file) else { - 92
continue; - 93
}; - 94
let mut in_tests = false; - 95
for (index, line) in text.lines().enumerate() { - 96
if line.contains("#[cfg(test)]") { - 97
in_tests = true; - 98
} - 99
if in_tests { - 100
continue; - 101
} - 102
let reads_account_home = - 103
line.contains("var_os(\"HOME\")") || line.contains("var(\"HOME\")"); - 104
// Probing whether HOME exists is not layout: `Core::new` falls - 105
// back to a cwd-local home in environments that have none. - 106
let is_probe = line.contains(".is_none()") || line.contains(".is_some()"); - 107
let builds_a_path = - 108
line.contains("PathBuf") || line.contains(".join(") || line.contains("Path::new"); - 109
let invents_a_convention = line.contains("\".config/vak"); - 110
if (reads_account_home && builds_a_path && !is_probe) || invents_a_convention { - 111
offenders.push(format!( - 112
"{relative}:{}: {}", - 113
index + 1, - 114
line.trim().chars().take(90).collect::<String>() - 115
)); - 116
} - 117
} - 118
} - 119
- 120
assert!( - 121
offenders.is_empty(), - 122
"these sites resolve the home directory by hand instead of through \ - 123
vak_config::paths — route them through data_home()/cache_home()/\ - 124
logs_dir(), or add a justified exemption to ALLOWED:\n {}", - 125
offenders.join("\n ") - 126
); - 127
} - 128
- 129
/// Exemptions are load-bearing: a stale one silently re-opens the hole it - 130
/// was granted for. If a file moves or stops needing the account home, its - 131
/// entry must go with it. - 132
#[test] - 133
fn every_exemption_still_points_at_a_real_file_that_needs_it() { - 134
let root = workspace_root(); - 135
for (relative, reason) in ALLOWED { - 136
let path = root.join(relative); - 137
assert!( - 138
path.is_file(), - 139
"exemption for '{relative}' ({reason}) names a file that no longer exists" - 140
); - 141
let text = std::fs::read_to_string(&path).expect("read exempt file"); - 142
assert!( - 143
text.contains("var_os(\"HOME\")") - 144
|| text.contains("var(\"HOME\")") - 145
|| text.contains(".config/vak"), - 146
"'{relative}' no longer resolves the account home; drop its exemption" - 147
); - 148
} - 149
} - 150
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.