- 1
//! Derived setup readiness (`docs/design/46-stabilization-install-and-onboarding.md` - 2
//! Part III). - 3
//! - 4
//! Readiness is **derived on every read** from the same authorities the - 5
//! rest of the system uses. There is deliberately no marker file saying - 6
//! "setup is done": deleting a key, moving a workspace, or revoking a - 7
//! permission has to make setup incomplete again on the next read, with - 8
//! no cached state to invalidate and no way for a stale file to grant a - 9
//! capability nothing else agrees exists. - 10
//! - 11
//! One projection, three surfaces: `vak setup status`, `GET /onboarding`, - 12
//! and the desktop shell all render this. A surface that computed its own - 13
//! answer is how two of them come to disagree. - 14
- 15
use std::path::Path; - 16
- 17
use serde::{Deserialize, Serialize}; - 18
- 19
use crate::Core; - 20
- 21
/// Why a step is not satisfied, in the four fields every setup failure - 22
/// owes the reader (doc 46, "Error design"): what failed, what is still - 23
/// safe, the one repair, and the underlying detail for whoever wants it. - 24
/// - 25
/// Collapsing these into one "setup failed" string is the specific - 26
/// failure this shape exists to prevent — provider auth, model discovery, - 27
/// sandbox availability, and backend boot are four different problems - 28
/// with four different remedies. - 29
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 30
pub struct StepFailure { - 31
/// What failed, in a sentence a person can act on. - 32
pub what: String, - 33
/// What is still safe or preserved, so the reader knows the blast radius. - 34
pub preserved: String, - 35
/// The single next action that resolves it. - 36
pub repair: String, - 37
/// The original typed error, for disclosure rather than display. - 38
pub detail: Option<String>, - 39
} - 40
- 41
impl StepFailure { - 42
pub fn new( - 43
what: impl Into<String>, - 44
preserved: impl Into<String>, - 45
repair: impl Into<String>, - 46
) -> Self { - 47
Self { - 48
what: what.into(), - 49
preserved: preserved.into(), - 50
repair: repair.into(), - 51
detail: None, - 52
} - 53
} - 54
- 55
pub fn with_detail(mut self, detail: impl Into<String>) -> Self { - 56
self.detail = Some(detail.into()); - 57
self - 58
} - 59
} - 60
- 61
/// One step of setup. - 62
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 63
#[serde(tag = "state", rename_all = "snake_case")] - 64
pub enum StepState { - 65
/// Done. `provenance` names the layer the value came from where the - 66
/// step reads a layered setting, because the single most confusing - 67
/// thing about a deep inheritance chain is a value you cannot - 68
/// attribute (doc 46 Part VI). - 69
Satisfied { - 70
detail: String, - 71
#[serde(skip_serializing_if = "Option::is_none")] - 72
provenance: Option<String>, - 73
}, - 74
/// Not done, with the four-field reason. - 75
Incomplete(StepFailure), - 76
/// Nothing to do here for this installation — a headless box has no - 77
/// desktop step, a CLI-only user has no channels. Distinct from - 78
/// `Satisfied` so a surface can render it as "not needed" rather - 79
/// than claiming an achievement nobody earned. - 80
NotApplicable { reason: String }, - 81
} - 82
- 83
impl StepState { - 84
pub fn ok(detail: impl Into<String>) -> Self { - 85
StepState::Satisfied { - 86
detail: detail.into(), - 87
provenance: None, - 88
} - 89
} - 90
- 91
pub fn ok_from(detail: impl Into<String>, provenance: impl Into<String>) -> Self { - 92
StepState::Satisfied { - 93
detail: detail.into(), - 94
provenance: Some(provenance.into()), - 95
} - 96
} - 97
- 98
pub fn is_satisfied(&self) -> bool { - 99
matches!(self, StepState::Satisfied { .. }) - 100
} - 101
- 102
/// Satisfied or deliberately not applicable — i.e. nothing is owed - 103
/// here. Readiness sums this, not `is_satisfied`, so an inapplicable - 104
/// step never blocks a machine it was never meant to apply to. - 105
pub fn settled(&self) -> bool { - 106
!matches!(self, StepState::Incomplete(_)) - 107
} - 108
- 109
pub fn failure(&self) -> Option<&StepFailure> { - 110
match self { - 111
StepState::Incomplete(f) => Some(f), - 112
_ => None, - 113
} - 114
} - 115
} - 116
- 117
/// Optional facts a caller probes on our behalf. - 118
/// - 119
/// The service manager is reachable from the CLI and the server but is - 120
/// not something a library should shell out to on every status read, and - 121
/// the caller already knows whether this installation asked for durable - 122
/// services at all. `None` means "not probed", which renders as - 123
/// `NotApplicable` rather than as a failure. - 124
#[derive(Debug, Clone, Default)] - 125
pub struct ProbedFacts { - 126
/// (service name, running) for each unit this installation expects. - 127
pub services: Option<Vec<(String, bool)>>, - 128
/// Units that configuration calls for but the service manager has not - 129
/// been told about yet — bots created without being activated. - 130
/// - 131
/// Reported separately from `services` because it is a different - 132
/// state: not "broken", but "you configured this and have not - 133
/// activated it". Before this was surfaced, a bot created in the - 134
/// console looked identical to one that was live. - 135
pub awaiting_activation: Vec<String>, - 136
/// The managed install: `Ok(detail)` when it verifies, `Err(failure)` - 137
/// with the *right* four fields when it does not. Typed rather than a - 138
/// string, because the caller knows whether this is drift (repair: - 139
/// reinstall) or pre-baseline state (repair: purge) — and offering - 140
/// the wrong remedy is worse than offering none. - 141
pub install: Option<Result<String, StepFailure>>, - 142
} - 143
- 144
/// The whole projection. - 145
#[derive(Debug, Clone, Serialize, Deserialize)] - 146
pub struct OnboardingState { - 147
pub install: StepState, - 148
pub dependencies: StepState, - 149
pub workspace: StepState, - 150
pub trust: StepState, - 151
pub provider: StepState, - 152
pub route: StepState, - 153
pub permission: StepState, - 154
pub sandbox: StepState, - 155
pub capabilities: StepState, - 156
pub integrations: StepState, - 157
pub channels: StepState, - 158
pub services: StepState, - 159
pub first_result: StepState, - 160
/// Enough to run a task in this workspace. - 161
pub core_ready: bool, - 162
/// Enough to run unattended: core, plus services and a bound channel. - 163
pub unattended_ready: bool, - 164
} - 165
- 166
impl OnboardingState { - 167
/// Every step, in presentation order, paired with its label. - 168
pub fn steps(&self) -> Vec<(&'static str, &StepState)> { - 169
vec![ - 170
("install", &self.install), - 171
("dependencies", &self.dependencies), - 172
("workspace", &self.workspace), - 173
("trust", &self.trust), - 174
("provider", &self.provider), - 175
("route", &self.route), - 176
("permission", &self.permission), - 177
("sandbox", &self.sandbox), - 178
("capabilities", &self.capabilities), - 179
("integrations", &self.integrations), - 180
("channels", &self.channels), - 181
("services", &self.services), - 182
("first result", &self.first_result), - 183
] - 184
} - 185
- 186
/// Steps still owed, for a surface that wants to resume where the - 187
/// operator left off rather than replay what is already done. - 188
pub fn incomplete(&self) -> Vec<(&'static str, &StepFailure)> { - 189
self.steps() - 190
.into_iter() - 191
.filter_map(|(label, step)| step.failure().map(|f| (label, f))) - 192
.collect() - 193
} - 194
} - 195
- 196
/// Derive the whole projection from live state. - 197
pub fn derive(core: &Core, probed: &ProbedFacts) -> OnboardingState { - 198
let install = install_step(probed); - 199
let dependencies = dependencies_step(); - 200
let workspace = workspace_step(core); - 201
let trust = trust_step(core); - 202
let (provider, route) = provider_and_route_steps(core); - 203
let permission = permission_step(core); - 204
let sandbox = sandbox_step(core); - 205
let capabilities = capabilities_step(); - 206
let integrations = integrations_step(core); - 207
let channels = channels_step(core); - 208
let services = services_step(probed); - 209
let first_result = first_result_step(core); - 210
- 211
// A route is only meaningful once the provider authenticates, so - 212
// `core_ready` deliberately does not double-count it. - 213
// - 214
// The permission step is `settled()`, not `is_satisfied()`. An unchosen - 215
// posture is a decision still owed — worth showing, and worth an action - 216
// in the wizard — but it does not stop a task from running: the default - 217
// is `workspace-write` and the sandbox applies either way. Requiring it - 218
// here would have declared every working install "not ready" the moment - 219
// this step learned to be honest. - 220
let core_ready = install.settled() - 221
&& workspace.is_satisfied() - 222
&& trust.settled() - 223
&& provider.is_satisfied() - 224
&& route.is_satisfied(); - 225
let unattended_ready = core_ready && services.is_satisfied() && channels.is_satisfied(); - 226
- 227
OnboardingState { - 228
install, - 229
dependencies, - 230
workspace, - 231
trust, - 232
provider, - 233
route, - 234
permission, - 235
sandbox, - 236
capabilities, - 237
integrations, - 238
channels, - 239
services, - 240
first_result, - 241
core_ready, - 242
unattended_ready, - 243
} - 244
} - 245
- 246
fn install_step(probed: &ProbedFacts) -> StepState { - 247
match &probed.install { - 248
Some(Ok(detail)) => StepState::ok(detail.clone()), - 249
Some(Err(failure)) => StepState::Incomplete(failure.clone()), - 250
// Running from a source tree or an unmanaged copy. That is a - 251
// normal way to develop, not a defect to report. - 252
None => StepState::NotApplicable { - 253
reason: "not a managed install".into(), - 254
}, - 255
} - 256
} - 257
- 258
/// Probe the external commands optional features need, and say so once - 259
/// here rather than letting a missing one surface as a confusing failure - 260
/// three screens later (doc 46, Step 1). - 261
fn dependencies_step() -> StepState { - 262
let mut missing = Vec::new(); - 263
if which("npx").is_none() { - 264
missing.push("npx (Node) — needed by the curated MCP integrations"); - 265
} - 266
if which("git").is_none() { - 267
missing.push("git — needed by worktrees, diff review, and the PR flow"); - 268
} - 269
if missing.is_empty() { - 270
return StepState::ok("npx and git available"); - 271
} - 272
// Optional by definition: never blocks core readiness. - 273
StepState::Incomplete(StepFailure::new( - 274
format!("Optional dependencies are missing: {}.", missing.join("; ")), - 275
"Everything that does not need them works normally.", - 276
"Install them with your platform package manager, then re-run setup.", - 277
)) - 278
} - 279
- 280
/// Minimal PATH lookup. Deliberately not a crate: one read of `PATH` and - 281
/// an executable-bit check is the whole contract, and a dependency here - 282
/// would ride into every surface that renders setup status. - 283
fn which(program: &str) -> Option<std::path::PathBuf> { - 284
let path = std::env::var_os("PATH")?; - 285
std::env::split_paths(&path).find_map(|dir| { - 286
let candidate = dir.join(program); - 287
candidate.is_file().then_some(candidate) - 288
}) - 289
} - 290
- 291
fn workspace_step(core: &Core) -> StepState { - 292
let cwd = core.cwd(); - 293
if std::fs::read_dir(cwd).is_err() { - 294
return StepState::Incomplete(StepFailure::new( - 295
format!("The workspace at {} cannot be read.", cwd.display()), - 296
"Nothing was changed.", - 297
"Choose a different workspace, or restore that directory.", - 298
)); - 299
} - 300
let home = core.sessions_home(); - 301
if std::fs::create_dir_all(&home).is_err() { - 302
return StepState::Incomplete(StepFailure::new( - 303
format!("The session store at {} is not writable.", home.display()), - 304
"Your workspace files are untouched.", - 305
"Fix the directory's permissions, then re-run setup.", - 306
)); - 307
} - 308
StepState::ok(cwd.display().to_string()) - 309
} - 310
- 311
fn trust_step(core: &Core) -> StepState { - 312
let cwd = core.cwd(); - 313
if !crate::trust::requests_privilege(cwd) { - 314
return StepState::NotApplicable { - 315
reason: "this workspace requests no privileged configuration".into(), - 316
}; - 317
} - 318
if core.project_config_trusted() { - 319
return StepState::ok("privileged project configuration is trusted here"); - 320
} - 321
// Open-safely is a complete, deliberate outcome, not a half-done - 322
// step: the workspace runs, and the project's privileged keys stay - 323
// demoted. Reporting it as a failure would push people to grant - 324
// trust to clear a warning, which is exactly backwards. - 325
StepState::ok("opened safely — privileged project configuration stays demoted") - 326
} - 327
- 328
fn provider_and_route_steps(core: &Core) -> (StepState, StepState) { - 329
let provider_name = core.effective_provider(); - 330
let model = core.effective_model(); - 331
match core.provider() { - 332
Ok(p) => { - 333
// Report the provider the operator configured, not the client - 334
// implementation that serves it: Ollama speaks the - 335
// OpenAI-compatible wire format, so `p.name()` says - 336
// "openai-completions" for a route the user chose as "ollama". - 337
// Naming the implementation there reads as though their choice - 338
// was ignored. The implementation is disclosure, not headline. - 339
let implementation = p.name().to_string(); - 340
let detail = if implementation.eq_ignore_ascii_case(&provider_name) { - 341
format!("{provider_name} authenticated") - 342
} else { - 343
format!("{provider_name} authenticated (via the {implementation} API)") - 344
}; - 345
let provider = StepState::ok(detail); - 346
// Provider and model are one atomic route (invariant 17); a - 347
// configured provider with no model is not a usable route. - 348
let route = if model.trim().is_empty() { - 349
StepState::Incomplete(StepFailure::new( - 350
format!("No model is selected for {provider_name}."), - 351
"Your credential is stored and the provider authenticates.", - 352
"Choose a model in setup; the list comes from your key.", - 353
)) - 354
} else { - 355
StepState::ok_from(format!("{provider_name}/{model}"), route_provenance(core)) - 356
}; - 357
(provider, route) - 358
} - 359
Err(e) => ( - 360
StepState::Incomplete( - 361
StepFailure::new( - 362
format!("{provider_name} is not connected."), - 363
"No route was activated and nothing else was changed.", - 364
"Add a credential for this provider in setup, or choose another.", - 365
) - 366
.with_detail(e.to_string()), - 367
), - 368
StepState::Incomplete(StepFailure::new( - 369
"No verified route.", - 370
"Existing workspace settings are unchanged.", - 371
"Connect a provider first; the route is saved with the model.", - 372
)), - 373
), - 374
} - 375
} - 376
- 377
/// Which layer supplied the effective route. Reported rather than - 378
/// inferred, because "why is this the model?" is the question a layered - 379
/// configuration makes hardest to answer. - 380
fn route_provenance(core: &Core) -> &'static str { - 381
if vak_config::project_path(core.cwd()).is_file() { - 382
"project or Shared layer" - 383
} else { - 384
"Shared layer" - 385
} - 386
} - 387
- 388
/// True when some config layer actually names a permission mode, as - 389
/// opposed to the effective value being the compiled default. - 390
/// - 391
/// Read as **text**, the same way `trust::requested_privileges` reads a - 392
/// project config: this runs during setup, before an operator has decided - 393
/// anything, and loading a layer to ask a question about it is the mistake - 394
/// that module exists to avoid. - 395
fn permission_mode_is_chosen(core: &Core) -> bool { - 396
let layers = [ - 397
vak_config::project_path(core.cwd()), - 398
vak_config::global_path().unwrap_or_default(), - 399
]; - 400
layers.iter().any(|path| { - 401
std::fs::read_to_string(path).is_ok_and(|text| { - 402
text.lines() - 403
.map(str::trim_start) - 404
.any(|line| line.starts_with("permission_mode")) - 405
}) - 406
}) - 407
} - 408
- 409
/// How much vak may do on its own. - 410
/// - 411
/// This step used to report `Satisfied` unconditionally, which made it - 412
/// invisible in exactly the way that matters: the wizard renders a step's - 413
/// actions only while it is unsatisfied, so the three-posture chooser - 414
/// behind it could never appear and a first-run operator was never asked. - 415
/// They inherited `workspace-write` — a reasonable default, and still not a - 416
/// decision anybody made. - 417
/// - 418
/// An explicit mode in either layer settles it. The remedy names all three - 419
/// postures rather than recommending one, because this is an access - 420
/// decision and the wizard must not make it on someone's behalf. - 421
fn permission_step(core: &Core) -> StepState { - 422
if permission_mode_is_chosen(core) { - 423
return StepState::ok_from( - 424
format!("{:?}", core.effective_permission_mode()), - 425
route_provenance(core), - 426
); - 427
} - 428
StepState::Incomplete( - 429
StepFailure::new( - 430
"No safety posture has been chosen for this workspace.", - 431
format!( - 432
"Nothing is unguarded: until you choose, vak runs at the \ - 433
default ({:?}) and the sandbox still applies.", - 434
core.effective_permission_mode() - 435
), - 436
"Pick one: inspect only, work with approval, or unrestricted.", - 437
) - 438
.with_detail( - 439
"No config layer sets `permission_mode`, so the effective value is \ - 440
the compiled default rather than a decision. Choosing writes it to \ - 441
the layer you pick.", - 442
), - 443
) - 444
} - 445
- 446
fn sandbox_step(core: &Core) -> StepState { - 447
let name = core.effective_sandbox_name(); - 448
let absent = name.eq_ignore_ascii_case("none") || name.eq_ignore_ascii_case("off"); - 449
let restricted = !matches!( - 450
core.effective_permission_mode(), - 451
vak_config::PermissionMode::FullAccess - 452
); - 453
if absent && restricted { - 454
// A restricted mode with no containment backend fails closed at - 455
// the first tool call. Saying so here is what keeps that - 456
// discovery out of the middle of someone's first task. - 457
return StepState::Incomplete(StepFailure::new( - 458
"No sandbox backend is available for the selected safety posture.", - 459
"Restricted modes still refuse unsandboxed work, so nothing escaped.", - 460
"Install a supported backend, or choose full access deliberately.", - 461
)); - 462
} - 463
if absent { - 464
return StepState::NotApplicable { - 465
reason: "full access is unsandboxed by design".into(), - 466
}; - 467
} - 468
StepState::ok(name) - 469
} - 470
- 471
fn capabilities_step() -> StepState { - 472
let root = vak_config::paths::default_workspace().join(".vak"); - 473
let skills = root.join("skills"); - 474
let count = std::fs::read_dir(&skills) - 475
.map(|entries| entries.flatten().filter(|e| e.path().is_dir()).count()) - 476
.unwrap_or(0); - 477
if count == 0 { - 478
return StepState::Incomplete(StepFailure::new( - 479
"No shared skills are installed yet.", - 480
"Nothing was written; the agent runs without them.", - 481
"Install the starter skills (`vak setup seed`, or the button in setup).", - 482
)); - 483
} - 484
StepState::ok_from(format!("{count} shared skills"), "Shared layer") - 485
} - 486
- 487
fn integrations_step(core: &Core) -> StepState { - 488
let servers = core.effective_mcp().servers; - 489
if servers.is_empty() { - 490
// Nothing is seeded on our initiative (doc 46 D4), so an empty - 491
// set is the expected fresh state rather than an omission. - 492
return StepState::NotApplicable { - 493
reason: "no integrations enabled".into(), - 494
}; - 495
} - 496
let mut names: Vec<&str> = servers.keys().map(String::as_str).collect(); - 497
names.sort_unstable(); - 498
StepState::ok(names.join(", ")) - 499
} - 500
- 501
fn channels_step(core: &Core) -> StepState { - 502
let entries = crate::health::read_channel_entries(&core.sessions_home()); - 503
if entries.is_empty() { - 504
return StepState::NotApplicable { - 505
reason: "no channels onboarded".into(), - 506
}; - 507
} - 508
let allowed = entries.iter().filter(|e| e.status == "allowed").count(); - 509
let pending = entries.iter().filter(|e| e.status == "pending").count(); - 510
if allowed == 0 { - 511
return StepState::Incomplete(StepFailure::new( - 512
format!("No channel is approved yet ({pending} waiting for review)."), - 513
"Every unapproved chat is refused, as designed.", - 514
"Approve a chat in setup or the admin console.", - 515
)); - 516
} - 517
StepState::ok(format!("{allowed} approved · {pending} pending")) - 518
} - 519
- 520
fn services_step(probed: &ProbedFacts) -> StepState { - 521
if !probed.awaiting_activation.is_empty() { - 522
let count = probed.awaiting_activation.len(); - 523
return StepState::Incomplete(StepFailure::new( - 524
format!("{count} configured bridge(s) are not activated yet."), - 525
"Their configuration and credentials are saved; nothing is running for them.", - 526
"Activate services to register and start them.", - 527
)); - 528
} - 529
let Some(services) = &probed.services else { - 530
return StepState::NotApplicable { - 531
reason: "no durable services requested".into(), - 532
}; - 533
}; - 534
if services.is_empty() { - 535
return StepState::NotApplicable { - 536
reason: "no durable services requested".into(), - 537
}; - 538
} - 539
let down: Vec<&str> = services - 540
.iter() - 541
.filter(|(_, running)| !running) - 542
.map(|(name, _)| name.as_str()) - 543
.collect(); - 544
if down.is_empty() { - 545
return StepState::ok(format!("{} running", services.len())); - 546
} - 547
StepState::Incomplete(StepFailure::new( - 548
format!("Not running: {}.", down.join(", ")), - 549
"Configuration and credentials are unchanged.", - 550
"Run `vak self services-sync`, then check the service log.", - 551
)) - 552
} - 553
- 554
fn first_result_step(core: &Core) -> StepState { - 555
let home = core.sessions_home(); - 556
if has_any_session(&home) { - 557
return StepState::ok("this workspace has run at least one session"); - 558
} - 559
StepState::Incomplete(StepFailure::new( - 560
"No task has been run here yet.", - 561
"Setup is otherwise complete.", - 562
"Run the read-only starter task to see a result and its receipt.", - 563
)) - 564
} - 565
- 566
/// Cheap existence probe: one directory walk, no ledger parsing. Whether - 567
/// *a* session exists is all this step asserts. - 568
fn has_any_session(sessions_home: &Path) -> bool { - 569
let root = sessions_home.join("sessions"); - 570
let Ok(workspaces) = std::fs::read_dir(&root) else { - 571
return false; - 572
}; - 573
workspaces.flatten().any(|workspace| { - 574
std::fs::read_dir(workspace.path()).is_ok_and(|mut files| { - 575
files.any(|f| f.is_ok_and(|f| f.path().extension().is_some_and(|e| e == "jsonl"))) - 576
}) - 577
}) - 578
} - 579
- 580
#[cfg(test)] - 581
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 582
mod permission_step_tests { - 583
use super::*; - 584
- 585
fn workspace(mode: Option<&str>) -> tempfile::TempDir { - 586
// Otherwise `global_path()` is the developer's own ~/vak-home - 587
// config, and whether this test passes depends on whose machine it - 588
// runs on. - 589
vak_config::paths::isolate_home_for_tests(); - 590
let dir = tempfile::tempdir().unwrap(); - 591
if let Some(mode) = mode { - 592
std::fs::create_dir_all(dir.path().join(".vak")).unwrap(); - 593
std::fs::write( - 594
dir.path().join(".vak/config.toml"), - 595
format!("permission_mode = \"{mode}\"\n"), - 596
) - 597
.unwrap(); - 598
} - 599
dir - 600
} - 601
- 602
/// The regression this step exists to prevent: it reported `Satisfied` - 603
/// unconditionally, the wizard only renders actions for an unsatisfied - 604
/// step, and so the three-posture chooser could never appear. - 605
#[test] - 606
fn an_unchosen_posture_is_incomplete_so_the_wizard_can_offer_one() { - 607
let dir = workspace(None); - 608
let core = Core::new_with_trust(dir.path().to_path_buf(), true).unwrap(); - 609
let step = permission_step(&core); - 610
let failure = step.failure().expect("must be incomplete"); - 611
// All four fields, per doc 46's error design. - 612
assert!(failure.what.contains("safety posture")); - 613
assert!(!failure.preserved.is_empty()); - 614
assert!(!failure.repair.is_empty()); - 615
assert!(failure.detail.is_some()); - 616
} - 617
- 618
#[test] - 619
fn an_explicit_mode_settles_it() { - 620
let dir = workspace(Some("read-only")); - 621
let core = Core::new_with_trust(dir.path().to_path_buf(), true).unwrap(); - 622
assert!(permission_step(&core).is_satisfied()); - 623
} - 624
- 625
/// An unchosen posture is a decision still owed, not a broken install: - 626
/// the default is safe and the sandbox applies either way. Gating - 627
/// readiness on it would declare every working install "not ready". - 628
#[test] - 629
fn an_unchosen_posture_does_not_block_readiness() { - 630
let dir = workspace(None); - 631
let core = Core::new_with_trust(dir.path().to_path_buf(), true).unwrap(); - 632
let state = derive(&core, &ProbedFacts::default()); - 633
assert!(!state.permission.is_satisfied()); - 634
// `core_ready` still turns on provider/route, which this fixture has - 635
// not set — the point is only that permission is not one of its terms. - 636
assert!( - 637
!state - 638
.steps() - 639
.iter() - 640
.any(|(name, _)| *name == "permission" && state.core_ready), - 641
"permission must not be a term of core_ready" - 642
); - 643
} - 644
} - 645
- 646
#[cfg(test)] - 647
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 648
mod tests { - 649
use super::*; - 650
- 651
/// A Core whose every path is temporary, so no assertion here can - 652
/// read or write the operator's real state. - 653
fn core_in(dir: &Path) -> Core { - 654
crate::isolate_global_config(); - 655
let core = Core::new(dir.to_path_buf()).unwrap(); - 656
core.set_sessions_home(dir.join("home")); - 657
core - 658
} - 659
- 660
#[test] - 661
fn a_clean_machine_is_not_core_ready_and_says_why() { - 662
let dir = tempfile::tempdir().unwrap(); - 663
let core = core_in(dir.path()); - 664
let state = derive(&core, &ProbedFacts::default()); - 665
- 666
assert!(!state.core_ready, "no provider is configured"); - 667
let provider = state.provider.failure().expect("provider is incomplete"); - 668
assert!(provider.repair.contains("setup"), "and names one repair"); - 669
assert!( - 670
!provider.preserved.is_empty(), - 671
"and says what is still safe" - 672
); - 673
} - 674
- 675
#[test] - 676
fn an_unprobed_install_is_not_applicable_rather_than_broken() { - 677
// Running from a source tree is normal. Reporting it as a defect - 678
// trains people to ignore the report. - 679
let dir = tempfile::tempdir().unwrap(); - 680
let core = core_in(dir.path()); - 681
let state = derive(&core, &ProbedFacts::default()); - 682
assert!(matches!(state.install, StepState::NotApplicable { .. })); - 683
assert!(state.install.settled(), "and never blocks readiness"); - 684
} - 685
- 686
#[test] - 687
fn a_workspace_requesting_no_privilege_is_never_asked_about_trust() { - 688
let dir = tempfile::tempdir().unwrap(); - 689
let core = core_in(dir.path()); - 690
let state = derive(&core, &ProbedFacts::default()); - 691
assert!(matches!(state.trust, StepState::NotApplicable { .. })); - 692
} - 693
- 694
#[test] - 695
fn opened_safely_is_a_complete_outcome_not_a_failure() { - 696
// Reporting safe-open as incomplete would push people to grant - 697
// trust merely to clear a warning -- exactly backwards. - 698
let dir = tempfile::tempdir().unwrap(); - 699
std::fs::create_dir_all(dir.path().join(".vak")).unwrap(); - 700
std::fs::write(dir.path().join(".vak/config.toml"), "").unwrap(); - 701
crate::isolate_global_config(); - 702
let core = Core::new_with_trust(dir.path().to_path_buf(), false).unwrap(); - 703
core.set_sessions_home(dir.path().join("home")); - 704
let state = derive(&core, &ProbedFacts::default()); - 705
assert!(state.trust.is_satisfied()); - 706
assert!( - 707
matches!(&state.trust, StepState::Satisfied { detail, .. } if detail.contains("safely")) - 708
); - 709
} - 710
- 711
#[test] - 712
fn the_install_step_reports_the_probers_own_repair_verbatim() { - 713
// Pre-baseline state and ordinary drift need different remedies; - 714
// wrapping either in a generic "reinstall" is how a reader is - 715
// handed the wrong command. - 716
let dir = tempfile::tempdir().unwrap(); - 717
let core = core_in(dir.path()); - 718
let state = derive( - 719
&core, - 720
&ProbedFacts { - 721
services: None, - 722
awaiting_activation: Vec::new(), - 723
install: Some(Err(StepFailure::new( - 724
"predates the baseline", - 725
"project files untouched", - 726
"vak self uninstall --purge", - 727
))), - 728
}, - 729
); - 730
let failure = state.install.failure().expect("install is incomplete"); - 731
assert_eq!(failure.repair, "vak self uninstall --purge"); - 732
} - 733
- 734
#[test] - 735
fn a_down_service_is_incomplete_and_an_unrequested_one_is_not() { - 736
let dir = tempfile::tempdir().unwrap(); - 737
let core = core_in(dir.path()); - 738
- 739
let none = derive(&core, &ProbedFacts::default()); - 740
assert!(none.services.settled(), "nobody asked for services"); - 741
- 742
let down = derive( - 743
&core, - 744
&ProbedFacts { - 745
services: Some(vec![("com.vak.gateway".into(), false)]), - 746
install: None, - 747
awaiting_activation: Vec::new(), - 748
}, - 749
); - 750
let failure = down.services.failure().expect("a down service is a defect"); - 751
assert!(failure.what.contains("com.vak.gateway")); - 752
} - 753
- 754
#[test] - 755
fn the_provider_step_names_the_configured_provider_not_the_client() { - 756
// A route configured as "ollama" is served by the - 757
// OpenAI-compatible client; reporting "openai-completions - 758
// authenticated" reads as though the operator's choice was - 759
// ignored. - 760
let dir = tempfile::tempdir().unwrap(); - 761
let core = core_in(dir.path()); - 762
let configured = core.effective_provider(); - 763
if let StepState::Satisfied { detail, .. } = - 764
&derive(&core, &ProbedFacts::default()).provider - 765
{ - 766
assert!( - 767
detail.starts_with(&configured), - 768
"expected {detail:?} to lead with the configured provider {configured:?}" - 769
); - 770
} - 771
} - 772
- 773
/// Configured-but-not-activated is its own state, and it has to be - 774
/// visible: a bot created in the console must not look identical to - 775
/// one that is actually running. - 776
#[test] - 777
fn a_bot_awaiting_activation_is_reported_as_such() { - 778
let dir = tempfile::tempdir().unwrap(); - 779
let core = core_in(dir.path()); - 780
let state = derive( - 781
&core, - 782
&ProbedFacts { - 783
services: None, - 784
install: None, - 785
awaiting_activation: vec!["com.vak.discord-ops".into()], - 786
}, - 787
); - 788
let failure = state.services.failure().expect("activation is owed"); - 789
assert!(failure.what.contains("not activated")); - 790
assert!(failure.repair.contains("Activate")); - 791
} - 792
- 793
#[test] - 794
fn integrations_are_absent_by_default_not_missing() { - 795
// Nothing is seeded on our initiative, so an empty set is the - 796
// expected fresh state (doc 46 D4). - 797
let dir = tempfile::tempdir().unwrap(); - 798
let core = core_in(dir.path()); - 799
let state = derive(&core, &ProbedFacts::default()); - 800
assert!(matches!( - 801
state.integrations, - 802
StepState::NotApplicable { .. } - 803
)); - 804
} - 805
- 806
#[test] - 807
fn every_incomplete_step_carries_all_four_fields() { - 808
// "Setup failed" as one message is the failure this shape exists - 809
// to prevent. - 810
let dir = tempfile::tempdir().unwrap(); - 811
let core = core_in(dir.path()); - 812
let state = derive(&core, &ProbedFacts::default()); - 813
for (label, failure) in state.incomplete() { - 814
assert!(!failure.what.is_empty(), "{label} says what failed"); - 815
assert!( - 816
!failure.preserved.is_empty(), - 817
"{label} says what is preserved" - 818
); - 819
assert!(!failure.repair.is_empty(), "{label} names one repair"); - 820
} - 821
} - 822
- 823
#[test] - 824
fn the_projection_round_trips_through_json() { - 825
let dir = tempfile::tempdir().unwrap(); - 826
let core = core_in(dir.path()); - 827
let state = derive(&core, &ProbedFacts::default()); - 828
let encoded = serde_json::to_string(&state).unwrap(); - 829
let back: OnboardingState = serde_json::from_str(&encoded).unwrap(); - 830
assert_eq!(back.core_ready, state.core_ready); - 831
assert_eq!(back.steps().len(), state.steps().len()); - 832
} - 833
} - 834
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.