- 1
#![allow(clippy::unwrap_used, clippy::expect_used)] - 2
- 3
//! End-to-end contract for the five extension points across both scopes. - 4
//! - 5
//! vak advertises skills, custom commands, plugins, hooks, and MCP servers as - 6
//! working "global as well as specific to workspace" — and each is resolved by - 7
//! its own code path, so "it works for skills" has never implied anything - 8
//! about the other four. This asserts the whole matrix in one place: - 9
//! discovery at each scope, workspace-over-shared precedence, and that every - 10
//! `inherit_*` switch actually isolates the capability it names. - 11
//! - 12
//! `VAK_HOME` is process-global, so everything runs inside a single test. - 13
- 14
use std::path::{Path, PathBuf}; - 15
- 16
use vak_core::Core; - 17
- 18
struct Layout { - 19
_root: tempfile::TempDir, - 20
_shared: PathBuf, - 21
workspace: PathBuf, - 22
} - 23
- 24
fn write(path: &Path, body: &str) { - 25
std::fs::create_dir_all(path.parent().expect("parent")).unwrap(); - 26
std::fs::write(path, body).unwrap(); - 27
} - 28
- 29
fn skill(root: &Path, name: &str, description: &str) { - 30
write( - 31
&root.join("skills").join(name).join("SKILL.md"), - 32
&format!("---\nname: {name}\ndescription: {description}\n---\nBody for {name}.\n"), - 33
); - 34
} - 35
- 36
fn command(root: &Path, name: &str, description: &str) { - 37
write( - 38
&root.join("commands").join(format!("{name}.md")), - 39
&format!("---\ndescription: {description}\n---\nTemplate for {name}.\n"), - 40
); - 41
} - 42
- 43
/// Builds a data home containing a shared workspace and a separate project - 44
/// workspace, each carrying every capability kind, with one deliberate name - 45
/// collision per kind. - 46
fn layout() -> Layout { - 47
let root = tempfile::tempdir().unwrap(); - 48
// Safe: `get_var` consults the override map above the real - 49
// environment, so pinning the home needs no `std::env::set_var` and - 50
// therefore no `unsafe` (AGENTS.md invariant 6). - 51
vak_config::paths::set_home_override(root.path()); - 52
- 53
let shared = root.path().join("vak-home/.vak"); - 54
let workspace = root.path().join("project/.vak"); - 55
std::fs::create_dir_all(&shared).unwrap(); - 56
std::fs::create_dir_all(&workspace).unwrap(); - 57
- 58
skill(&shared, "shared-only", "lives only in the shared workspace"); - 59
skill(&shared, "collide", "the shared definition"); - 60
skill(&workspace, "workspace-only", "lives only in this workspace"); - 61
skill(&workspace, "collide", "the workspace definition"); - 62
- 63
command(&shared, "shared-cmd", "shared command"); - 64
command(&shared, "both-cmd", "shared version"); - 65
command(&workspace, "workspace-cmd", "workspace command"); - 66
command(&workspace, "both-cmd", "workspace version"); - 67
- 68
write( - 69
&shared.join("config.toml"), - 70
"[mcp.servers.shared-server]\ncommand = \"true\"\nargs = []\n\n\ - 71
[[hooks]]\nevent = \"pre-tool-use\"\ncommand = \"echo shared-hook\"\nenabled = true\n", - 72
); - 73
- 74
Layout { - 75
_root: root, - 76
_shared: shared, - 77
workspace, - 78
} - 79
} - 80
- 81
fn build(layout: &Layout, project_config: &str) -> Core { - 82
write(&layout.workspace.join("config.toml"), project_config); - 83
Core::new_with_trust( - 84
layout - 85
.workspace - 86
.parent() - 87
.expect("workspace parent") - 88
.to_path_buf(), - 89
true, - 90
) - 91
.unwrap() - 92
} - 93
- 94
fn skill_names(core: &Core) -> Vec<String> { - 95
let mut names: Vec<String> = core.skills().into_iter().map(|s| s.name).collect(); - 96
names.sort(); - 97
names - 98
} - 99
- 100
fn command_names(core: &Core) -> Vec<String> { - 101
let mut names: Vec<String> = core.custom_commands().into_iter().map(|c| c.name).collect(); - 102
names.sort(); - 103
names - 104
} - 105
- 106
#[test] - 107
fn every_capability_resolves_at_both_scopes_and_every_switch_isolates() { - 108
let layout = layout(); - 109
- 110
// ── default: both scopes contribute, workspace wins collisions ─────── - 111
let core = build(&layout, "provider = \"anthropic\"\n"); - 112
- 113
let skills = skill_names(&core); - 114
assert!( - 115
skills.contains(&"workspace-only".to_string()), - 116
"workspace skills must be discovered: {skills:?}" - 117
); - 118
assert!( - 119
skills.contains(&"shared-only".to_string()), - 120
"shared skills must be inherited by default: {skills:?}" - 121
); - 122
assert_eq!( - 123
skills.iter().filter(|n| *n == "collide").count(), - 124
1, - 125
"a shadowed skill must not appear twice: {skills:?}" - 126
); - 127
let winner = core - 128
.skills() - 129
.into_iter() - 130
.find(|s| s.name == "collide") - 131
.expect("collide"); - 132
assert_eq!( - 133
winner.description, "the workspace definition", - 134
"workspace scope must shadow shared scope" - 135
); - 136
- 137
let commands = command_names(&core); - 138
assert!( - 139
commands.contains(&"workspace-cmd".to_string()), - 140
"workspace commands must be discovered: {commands:?}" - 141
); - 142
assert!( - 143
commands.contains(&"shared-cmd".to_string()), - 144
"shared commands must be inherited by default: {commands:?}" - 145
); - 146
- 147
assert!( - 148
core.effective_mcp().servers.contains_key("shared-server"), - 149
"an MCP server defined in the shared config must reach the workspace" - 150
); - 151
assert!( - 152
core.effective_hooks() - 153
.iter() - 154
.any(|h| h.command.contains("shared-hook")), - 155
"a hook defined in the shared config must reach the workspace" - 156
); - 157
- 158
// ── inherit_skills = false ─────────────────────────────────────────── - 159
let isolated = build( - 160
&layout, - 161
"provider = \"anthropic\"\n[capabilities]\ninherit_skills = false\n", - 162
); - 163
let skills = skill_names(&isolated); - 164
assert!( - 165
skills.contains(&"workspace-only".to_string()), - 166
"isolation must not remove the workspace's own skills: {skills:?}" - 167
); - 168
assert!( - 169
!skills.contains(&"shared-only".to_string()), - 170
"inherit_skills = false must drop shared skills: {skills:?}" - 171
); - 172
assert!( - 173
command_names(&isolated).contains(&"shared-cmd".to_string()), - 174
"inherit_skills must not silently take commands with it" - 175
); - 176
- 177
// ── inherit_commands = false ───────────────────────────────────────── - 178
let isolated = build( - 179
&layout, - 180
"provider = \"anthropic\"\n[capabilities]\ninherit_commands = false\n", - 181
); - 182
let commands = command_names(&isolated); - 183
assert!( - 184
commands.contains(&"workspace-cmd".to_string()), - 185
"isolation must not remove the workspace's own commands: {commands:?}" - 186
); - 187
assert!( - 188
!commands.contains(&"shared-cmd".to_string()), - 189
"inherit_commands = false must drop shared commands: {commands:?}" - 190
); - 191
assert!( - 192
skill_names(&isolated).contains(&"shared-only".to_string()), - 193
"inherit_commands must not silently take skills with it" - 194
); - 195
- 196
// ── inherit_mcp = false ────────────────────────────────────────────── - 197
let isolated = build( - 198
&layout, - 199
"provider = \"anthropic\"\n[capabilities]\ninherit_mcp = false\n", - 200
); - 201
assert!( - 202
!isolated - 203
.effective_mcp() - 204
.servers - 205
.contains_key("shared-server"), - 206
"inherit_mcp = false must drop the shared MCP server" - 207
); - 208
- 209
// ── inherit_hooks = false ──────────────────────────────────────────── - 210
let isolated = build( - 211
&layout, - 212
"provider = \"anthropic\"\n[capabilities]\ninherit_hooks = false\n", - 213
); - 214
assert!( - 215
!isolated - 216
.effective_hooks() - 217
.iter() - 218
.any(|h| h.command.contains("shared-hook")), - 219
"inherit_hooks = false must drop the shared hook" - 220
); - 221
- 222
// ── every switch at once: a fully isolated workspace ───────────────── - 223
let sealed = build( - 224
&layout, - 225
"provider = \"anthropic\"\n[capabilities]\ninherit_mcp = false\n\ - 226
inherit_hooks = false\ninherit_skills = false\ninherit_commands = false\n\ - 227
inherit_plugins = false\n", - 228
); - 229
let skills = skill_names(&sealed); - 230
let commands = command_names(&sealed); - 231
assert!(!skills.contains(&"shared-only".to_string())); - 232
assert!(!commands.contains(&"shared-cmd".to_string())); - 233
assert!(!sealed.effective_mcp().servers.contains_key("shared-server")); - 234
assert!( - 235
!sealed - 236
.effective_hooks() - 237
.iter() - 238
.any(|h| h.command.contains("shared-hook")) - 239
); - 240
assert!( - 241
skills.contains(&"workspace-only".to_string()) - 242
&& commands.contains(&"workspace-cmd".to_string()), - 243
"a sealed workspace keeps everything it defines itself: {skills:?} {commands:?}" - 244
); - 245
} - 246
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.