- 1
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 2
//! `--purge` leaves nothing of ours, and nothing of yours - 3
//! (`docs/design/46-stabilization-install-and-onboarding.md` D3, S9). - 4
//! - 5
//! The assertion that matters is **what survived**, not what was removed. - 6
//! A delete-list can be tested by checking the list ran; a preserve rule - 7
//! can only be tested by looking at the filesystem afterwards and finding - 8
//! the user's own files still there. - 9
- 10
use std::path::{Path, PathBuf}; - 11
use std::process::Command; - 12
- 13
fn vak_binary() -> PathBuf { - 14
// The integration binary sits beside the test binary's target dir. - 15
let mut path = std::env::current_exe().expect("test exe"); - 16
path.pop(); - 17
if path.ends_with("deps") { - 18
path.pop(); - 19
} - 20
path.join("vak") - 21
} - 22
- 23
fn write(path: &Path, contents: &str) { - 24
if let Some(parent) = path.parent() { - 25
std::fs::create_dir_all(parent).unwrap(); - 26
} - 27
std::fs::write(path, contents).unwrap(); - 28
} - 29
- 30
/// A purge removes every declared root and leaves a project's own `.vak` - 31
/// alone, because that is the user's file in the user's repository. - 32
#[test] - 33
fn purge_removes_declared_state_and_preserves_the_users_own_projects() { - 34
let binary = vak_binary(); - 35
if !binary.exists() { - 36
// `cargo test` without a prior build of the binary target. Skip - 37
// rather than fail on something that is not this test's subject. - 38
eprintln!("skipping: no vak binary at {}", binary.display()); - 39
return; - 40
} - 41
- 42
let home = tempfile::tempdir().unwrap(); - 43
let project = tempfile::tempdir().unwrap(); - 44
- 45
// State the registry declares, across both roots. - 46
let data = home.path(); - 47
write(&data.join("sessions/w/a.jsonl"), "{\"kind\":\"header\"}\n"); - 48
write(&data.join("security-events.jsonl"), "{}\n"); - 49
write( - 50
&data.join("gateway/bots.json"), - 51
"{\"schema\":1,\"bots\":[]}", - 52
); - 53
let shared = data.join("vak-home"); - 54
write(&shared.join(".env"), "SECRET=value\n"); - 55
write(&shared.join(".vak/config.toml"), "provider = \"ollama\"\n"); - 56
write( - 57
&shared.join(".vak/skills/demo/SKILL.md"), - 58
"---\nname: demo\n---\n", - 59
); - 60
- 61
// The user's own project, which a purge must never touch. - 62
let project_config = project.path().join(".vak/config.toml"); - 63
write(&project_config, "model = \"theirs\"\n"); - 64
let project_source = project.path().join("src/main.rs"); - 65
write(&project_source, "fn main() {}\n"); - 66
- 67
let output = Command::new(&binary) - 68
.args(["self", "uninstall", "--yes", "--purge", "--prefix"]) - 69
.arg(home.path().join("prefix")) - 70
.env("VAK_HOME", home.path()) - 71
.output() - 72
.expect("run uninstall"); - 73
assert!( - 74
output.status.success(), - 75
"purge failed: {}", - 76
String::from_utf8_lossy(&output.stderr) - 77
); - 78
- 79
// Ours is gone. - 80
for gone in [ - 81
data.join("sessions"), - 82
data.join("security-events.jsonl"), - 83
data.join("gateway"), - 84
shared.join(".env"), - 85
shared.join(".vak/config.toml"), - 86
shared.join(".vak/skills"), - 87
] { - 88
assert!( - 89
!gone.exists(), - 90
"{} survived a purge; the next install would not be a first run", - 91
gone.display() - 92
); - 93
} - 94
- 95
// Theirs is not. - 96
assert!( - 97
project_config.exists(), - 98
"a purge deleted a project's own .vak — that is the user's file in the user's repository" - 99
); - 100
assert!(project_source.exists(), "a purge touched project source"); - 101
} - 102
- 103
/// Logs are Vak's state too: a purge that left them made the next install - 104
/// read a previous version's service logs as its own. - 105
#[test] - 106
fn purge_includes_logs() { - 107
let binary = vak_binary(); - 108
if !binary.exists() { - 109
eprintln!("skipping: no vak binary at {}", binary.display()); - 110
return; - 111
} - 112
let home = tempfile::tempdir().unwrap(); - 113
let log = home.path().join("logs/gateway.log"); - 114
write(&log, "old service output\n"); - 115
let output = Command::new(&binary) - 116
.args(["self", "uninstall", "--yes", "--purge", "--prefix"]) - 117
.arg(home.path().join("prefix")) - 118
.env("VAK_HOME", home.path()) - 119
.output() - 120
.expect("run uninstall"); - 121
assert!( - 122
output.status.success(), - 123
"purge failed: {}", - 124
String::from_utf8_lossy(&output.stderr) - 125
); - 126
assert!(!log.exists(), "a purge left the logs behind"); - 127
} - 128
- 129
/// A symlinked root is refused rather than followed. - 130
/// - 131
/// `remove_dir_all` through a symlink deletes whatever it points at, which - 132
/// on a machine where someone has redirected their vak home is somebody - 133
/// else's directory entirely. - 134
#[test] - 135
fn a_symlinked_root_is_refused_not_followed() { - 136
let binary = vak_binary(); - 137
if !binary.exists() { - 138
eprintln!("skipping: no vak binary"); - 139
return; - 140
} - 141
#[cfg(unix)] - 142
{ - 143
let home = tempfile::tempdir().unwrap(); - 144
let elsewhere = tempfile::tempdir().unwrap(); - 145
let precious = elsewhere.path().join("precious.txt"); - 146
write(&precious, "do not delete me\n"); - 147
- 148
// `sessions` is a symlink into a directory we do not own. - 149
std::fs::create_dir_all(home.path()).unwrap(); - 150
std::os::unix::fs::symlink(elsewhere.path(), home.path().join("sessions")).unwrap(); - 151
- 152
let output = Command::new(&binary) - 153
.args(["self", "uninstall", "--yes", "--purge", "--prefix"]) - 154
.arg(home.path().join("prefix")) - 155
.env("VAK_HOME", home.path()) - 156
.output() - 157
.expect("run uninstall"); - 158
- 159
assert!( - 160
precious.exists(), - 161
"a purge followed a symlink and deleted a directory it does not own" - 162
); - 163
let combined = format!( - 164
"{}{}", - 165
String::from_utf8_lossy(&output.stdout), - 166
String::from_utf8_lossy(&output.stderr) - 167
); - 168
assert!( - 169
combined.contains("symlink"), - 170
"the refusal must say why: {combined}" - 171
); - 172
} - 173
} - 174
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.