- 1
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 2
- 3
//! The pool starts a server only on demand (AGENTS.md invariant 25). Each - 4
//! test wraps the server command so every spawn appends a line to a marker - 5
//! file, which makes "was a process started?" a count, not a guess. - 6
- 7
use std::collections::HashMap; - 8
use std::path::{Path, PathBuf}; - 9
use std::sync::Arc; - 10
use std::time::Duration; - 11
- 12
use serde_json::json; - 13
- 14
use vak_mcp::{McpManager, McpTool, ServerConfig}; - 15
use vak_tools::{Tool, ToolContext}; - 16
- 17
fn server_script() -> PathBuf { - 18
PathBuf::from(env!("CARGO_MANIFEST_DIR")) - 19
.join("../../scripts/fake_mcp_server.py") - 20
.canonicalize() - 21
.unwrap() - 22
} - 23
- 24
/// A server whose every start is recorded in `marker`; `then` is what the - 25
/// shell runs after recording. - 26
fn counted(marker: &Path, then: &str) -> ServerConfig { - 27
ServerConfig { - 28
command: "sh".into(), - 29
args: vec![ - 30
"-c".into(), - 31
format!("echo started >> '{}'; {then}", marker.display()), - 32
], - 33
env: Vec::new(), - 34
network: false, - 35
} - 36
} - 37
- 38
fn spawns(marker: &Path) -> usize { - 39
std::fs::read_to_string(marker) - 40
.map(|text| text.lines().count()) - 41
.unwrap_or(0) - 42
} - 43
- 44
fn pool(name: &str, config: ServerConfig, cwd: &Path) -> McpManager { - 45
McpManager::new( - 46
HashMap::from([(name.to_string(), config)]), - 47
cwd.to_path_buf(), - 48
) - 49
} - 50
- 51
fn ctx(cwd: &Path) -> ToolContext { - 52
ToolContext::new(cwd.to_path_buf()) - 53
} - 54
- 55
#[tokio::test] - 56
async fn nothing_starts_until_a_call_needs_the_server() { - 57
let dir = tempfile::tempdir().unwrap(); - 58
let marker = dir.path().join("spawns"); - 59
let script = format!("exec python3 '{}'", server_script().display()); - 60
let manager = Arc::new(pool("fake", counted(&marker, &script), dir.path())); - 61
let tool = McpTool::new(manager.clone()); - 62
- 63
assert!(manager.observations().is_empty()); - 64
let names = tool - 65
.execute(&json!({"action": "list"}), &ctx(dir.path())) - 66
.await; - 67
assert!(!names.is_error, "{}", names.content); - 68
assert!(names.content.contains("- fake"), "{}", names.content); - 69
assert_eq!(spawns(&marker), 0, "listing server names starts nothing"); - 70
- 71
let listed = tool - 72
.execute( - 73
&json!({"action": "list", "server": "fake"}), - 74
&ctx(dir.path()), - 75
) - 76
.await; - 77
assert!( - 78
listed.content.contains("echo — Echo back"), - 79
"{}", - 80
listed.content - 81
); - 82
assert_eq!( - 83
spawns(&marker), - 84
1, - 85
"demand starts exactly the server asked for" - 86
); - 87
- 88
let called = tool - 89
.execute( - 90
&json!({"action": "call", "server": "fake", "tool": "echo", "arguments": {"text": "hi"}}), - 91
&ctx(dir.path()), - 92
) - 93
.await; - 94
assert!(!called.is_error, "{}", called.content); - 95
assert_eq!(spawns(&marker), 1, "the pooled connection is reused"); - 96
- 97
let observed = manager.observations(); - 98
let tools = observed["fake"].tools.as_ref().expect("catalog observed"); - 99
assert!(tools.iter().any(|t| t.name == "echo")); - 100
} - 101
- 102
#[tokio::test] - 103
async fn an_evicted_server_is_not_restarted_without_demand() { - 104
let dir = tempfile::tempdir().unwrap(); - 105
let marker = dir.path().join("spawns"); - 106
let script = format!("exec python3 '{}'", server_script().display()); - 107
let manager = pool("fake", counted(&marker, &script), dir.path()); - 108
manager.list_tools("fake").await.unwrap(); - 109
assert_eq!(spawns(&marker), 1); - 110
- 111
assert_eq!(manager.evict_idle(Duration::ZERO).await, vec!["fake"]); - 112
tokio::time::sleep(Duration::from_millis(100)).await; - 113
assert_eq!(spawns(&marker), 1, "eviction never triggers a respawn"); - 114
assert!( - 115
manager.observations()["fake"].tools.is_some(), - 116
"the last catalog survives eviction, so the prompt still names its tools" - 117
); - 118
- 119
manager.list_tools("fake").await.unwrap(); - 120
assert_eq!(spawns(&marker), 2, "the next demand respawns it"); - 121
} - 122
- 123
#[tokio::test] - 124
async fn a_failing_server_backs_off_instead_of_respawning_on_every_call() { - 125
let dir = tempfile::tempdir().unwrap(); - 126
let marker = dir.path().join("spawns"); - 127
let manager = pool("broken", counted(&marker, "exit 1"), dir.path()); - 128
- 129
let first = manager.list_tools("broken").await.unwrap_err(); - 130
assert_eq!(spawns(&marker), 1); - 131
let observed = manager.observations(); - 132
assert!( - 133
observed["broken"].failure.is_some(), - 134
"the failure is recorded" - 135
); - 136
assert!(observed["broken"].tools.is_none()); - 137
- 138
let second = manager.list_tools("broken").await.unwrap_err(); - 139
assert_eq!( - 140
spawns(&marker), - 141
1, - 142
"inside the backoff window nothing is spawned" - 143
); - 144
assert!( - 145
second.contains("next attempt in"), - 146
"{second} (first: {first})" - 147
); - 148
} - 149
- 150
#[tokio::test] - 151
async fn the_observer_hears_what_demand_learned() { - 152
let dir = tempfile::tempdir().unwrap(); - 153
let marker = dir.path().join("spawns"); - 154
let script = format!("exec python3 '{}'", server_script().display()); - 155
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); - 156
let manager = pool("fake", counted(&marker, &script), dir.path()).with_observer(tx); - 157
- 158
manager.list_tools("fake").await.unwrap(); - 159
assert_eq!( - 160
rx.try_recv().unwrap(), - 161
"fake", - 162
"a learned catalog is announced" - 163
); - 164
manager.list_tools("fake").await.unwrap(); - 165
assert!(rx.try_recv().is_err(), "an unchanged catalog is not news"); - 166
- 167
manager.forget_catalog("fake"); - 168
assert_eq!(rx.try_recv().unwrap(), "fake"); - 169
assert!(manager.observations()["fake"].tools.is_none()); - 170
} - 171
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.