- 1
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 2
- 3
//! Integration coverage for docs/design/34-channel-onboarding.md Phase 2: - 4
//! an allowlist entry approved for a workspace other than the gateway's - 5
//! own default actually runs a session rooted at that other workspace, - 6
//! not just its provider/model. - 7
//! - 8
//! `VAK_HOME` is set for the whole process below so pooled Cores started - 9
//! for the non-default workspace resolve their sessions_home the normal - 10
//! way (`Core::new_with_trust`, unmodified) inside a hermetic tempdir - 11
//! instead of the real user's data home. This file has exactly one test - 12
//! so that process-wide env mutation cannot race a sibling test. - 13
- 14
use std::sync::Arc; - 15
- 16
use vak_core::Core; - 17
use vak_llm::stream; - 18
use vak_llm::types::{AssistantMessage, ChatRequest, ContentBlock, StopReason, Usage}; - 19
use vak_llm::{EventStream, LlmError, Provider}; - 20
- 21
struct NoCred; - 22
- 23
#[async_trait::async_trait] - 24
impl Provider for NoCred { - 25
fn name(&self) -> &str { - 26
"scripted" - 27
} - 28
async fn stream( - 29
&self, - 30
_request: ChatRequest, - 31
_cancel: tokio_util::sync::CancellationToken, - 32
) -> Result<EventStream, LlmError> { - 33
let (mut sink, rx) = stream::channel(64); - 34
let m = AssistantMessage { - 35
content: vec![ContentBlock::text("unused")], - 36
stop_reason: StopReason::EndTurn, - 37
usage: Usage::default(), - 38
model: "test-model".into(), - 39
response_id: None, - 40
}; - 41
sink.close_message(m).await; - 42
Ok(rx) - 43
} - 44
} - 45
- 46
#[tokio::test(flavor = "multi_thread", worker_threads = 4)] - 47
async fn approved_entry_routes_to_its_own_workspace_core() { - 48
let vak_home = tempfile::tempdir().unwrap(); - 49
vak_config::paths::set_home_override(vak_home.path()); - 50
- 51
let default_dir = tempfile::tempdir().unwrap(); - 52
let other_dir = tempfile::tempdir().unwrap(); - 53
let _ = std::fs::create_dir_all(other_dir.path()); - 54
- 55
let default_cwd = default_dir.path().to_path_buf(); - 56
let _ = std::fs::create_dir_all(default_cwd.join(".vak")); - 57
let _ = std::fs::write( - 58
default_cwd.join(".vak/config.toml"), - 59
"[memory]\nreflection = false\n[gateway]\nchat_allowlist_open = false\n", - 60
); - 61
let core = Core::new_with_trust(default_cwd.clone(), true).unwrap(); - 62
core.set_sessions_home(vak_home.path().join("default-home")); - 63
core.set_provider_instance(Arc::new(NoCred)); - 64
- 65
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - 66
let addr = listener.local_addr().unwrap(); - 67
let (app, token) = vak_server::secured_router_with(core, true); - 68
let _server = tokio::spawn(async move { - 69
axum::serve(listener, app).await.unwrap(); - 70
}); - 71
let base = format!("http://{addr}"); - 72
- 73
let client = reqwest::ClientBuilder::new() - 74
.default_headers({ - 75
let mut h = reqwest::header::HeaderMap::new(); - 76
h.insert( - 77
reqwest::header::AUTHORIZATION, - 78
format!("Bearer {token}").parse().unwrap(), - 79
); - 80
h - 81
}) - 82
.build() - 83
.unwrap(); - 84
- 85
// First message: unknown key becomes pending, not silently allowed. - 86
let msg = serde_json::json!({ - 87
"surface": "webhook", "chat": "ci-other-ws", "sender": "bot", - 88
"text": "hello", "wait": false, - 89
}); - 90
let res = client - 91
.post(format!("{base}/gateway/inbound")) - 92
.json(&msg) - 93
.send() - 94
.await - 95
.unwrap(); - 96
assert_eq!(res.status(), 403); - 97
let body: serde_json::Value = res.json().await.unwrap(); - 98
assert_eq!(body["state"], "pending"); - 99
- 100
// Operator approves, explicitly naming a workspace other than the - 101
// gateway's own default — the Phase 1 fix for silent inheritance. - 102
let approve_res = client - 103
.post(format!( - 104
"{base}/admin/api/gateway/allowlist/webhook%3Aci-other-ws/approve" - 105
)) - 106
.json(&serde_json::json!({ "workspace": other_dir.path() })) - 107
.send() - 108
.await - 109
.unwrap(); - 110
assert_eq!(approve_res.status(), 200, "approve must succeed"); - 111
- 112
// Second message: now allowed. Provider auth still fails (no credential - 113
// configured / injected for the pooled Core), so the turn itself can't - 114
// finish — but session creation happens *before* that check, so the - 115
// pooled Core for `other_dir` must already have started and minted a - 116
// session there by the time we inspect it. - 117
let res = client - 118
.post(format!("{base}/gateway/inbound")) - 119
.json(&msg) - 120
.send() - 121
.await - 122
.unwrap(); - 123
assert_eq!( - 124
res.status(), - 125
503, - 126
"session should have been created against the other workspace's pooled Core \ - 127
before the provider-credential check fails" - 128
); - 129
- 130
// Confirm: the pool now reports the other workspace as warm. - 131
let status: serde_json::Value = client - 132
.get(format!("{base}/admin/api/gateway/status")) - 133
.send() - 134
.await - 135
.unwrap() - 136
.json() - 137
.await - 138
.unwrap(); - 139
let pool_entries = status["core_pool"]["entries"].as_array().unwrap(); - 140
let other_canonical = other_dir.path().canonicalize().unwrap(); - 141
assert!( - 142
pool_entries - 143
.iter() - 144
.any(|e| e["workspace"].as_str() == Some(other_canonical.to_str().unwrap())), - 145
"expected {other_canonical:?} to appear warm in the core pool, got {pool_entries:?}" - 146
); - 147
- 148
// Confirm: the session actually minted lives under the OTHER - 149
// workspace's sessions_home (the normal, unmodified `Core::new_with_trust` - 150
// resolution under VAK_HOME), with a ledger header whose cwd is that - 151
// workspace — not the gateway's own default cwd. - 152
let mut found_header_cwd: Option<String> = None; - 153
for entry in walkdir::WalkDir::new(vak_home.path()).into_iter().flatten() { - 154
let path = entry.path(); - 155
if path.extension().and_then(|e| e.to_str()) != Some("jsonl") { - 156
continue; - 157
} - 158
let Ok(raw) = std::fs::read_to_string(path) else { - 159
continue; - 160
}; - 161
if let Some(first_line) = raw.lines().next() - 162
&& let Ok(v) = serde_json::from_str::<serde_json::Value>(first_line) - 163
&& let Some(cwd) = v.get("cwd").and_then(|c| c.as_str()) - 164
{ - 165
found_header_cwd = Some(cwd.to_string()); - 166
} - 167
} - 168
assert_eq!( - 169
found_header_cwd.as_deref(), - 170
Some(other_canonical.to_str().unwrap()), - 171
"session ledger header cwd must be the approved entry's own workspace, \ - 172
not the gateway's default" - 173
); - 174
- 175
// Drop the pin so a later test in this binary resolves normally. - 176
vak_config::clear_override("VAK_HOME"); - 177
} - 178
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.