- 1
//! Personal-OS server surfaces (docs/design/29-personal-os.md P1–P4): - 2
//! tiered memory CRUD, cross-project search, markdown transcript export, - 3
//! doctor, backup export/import, and the usage digest. - 4
- 5
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 6
- 7
use std::path::{Path, PathBuf}; - 8
use std::sync::{ - 9
Arc, - 10
atomic::{AtomicUsize, Ordering}, - 11
}; - 12
- 13
use tokio_util::sync::CancellationToken; - 14
- 15
use vak_core::Core; - 16
use vak_llm::stream; - 17
use vak_llm::types::{AssistantMessage, ChatRequest, ContentBlock, Usage}; - 18
use vak_llm::{EventStream, LlmError, Provider}; - 19
use vak_session::types::{FrozenContract, SessionHeader}; - 20
use vak_session::{SessionLog, SessionPath}; - 21
- 22
struct Counting { - 23
dispatches: Arc<AtomicUsize>, - 24
} - 25
- 26
#[async_trait::async_trait] - 27
impl Provider for Counting { - 28
fn name(&self) -> &str { - 29
"counting" - 30
} - 31
- 32
async fn stream( - 33
&self, - 34
_request: ChatRequest, - 35
_cancel: CancellationToken, - 36
) -> Result<EventStream, LlmError> { - 37
self.dispatches.fetch_add(1, Ordering::SeqCst); - 38
let (mut sink, rx) = stream::channel(8); - 39
let done = AssistantMessage { - 40
content: vec![ContentBlock::text("done")], - 41
stop_reason: vak_llm::types::StopReason::EndTurn, - 42
usage: Usage { - 43
input_tokens: 5, - 44
output_tokens: 1, - 45
..Default::default() - 46
}, - 47
model: "counted-model".into(), - 48
response_id: None, - 49
}; - 50
sink.push(stream::StreamEvent::Start { - 51
partial: done.clone(), - 52
}); - 53
sink.close_message(done).await; - 54
Ok(rx) - 55
} - 56
} - 57
- 58
struct Server { - 59
base: String, - 60
home: PathBuf, - 61
cwd: PathBuf, - 62
_dir: Arc<tempfile::TempDir>, - 63
client: reqwest::Client, - 64
} - 65
- 66
/// Spawn a plain (scheduler-free) server over a hermetic workspace. - 67
async fn spawn_server(config_toml: &str) -> Server { - 68
let dir = Arc::new(tempfile::tempdir().unwrap()); - 69
let cwd = dir.path().to_path_buf(); - 70
let project = cwd.join(".vak"); - 71
std::fs::create_dir_all(&project).unwrap(); - 72
std::fs::write( - 73
project.join("config.toml"), - 74
format!("[memory]\nreflection = false\n{config_toml}"), - 75
) - 76
.unwrap(); - 77
- 78
vak_config::paths::isolate_home_for_tests(); - 79
let core = Core::new_with_trust(cwd.clone(), true).unwrap(); - 80
core.set_sessions_home(dir.path().join("home")); - 81
core.set_permission_mode(vak_config::PermissionMode::FullAccess); - 82
// A REAL worker executable: the test harness itself cannot speak the - 83
// broker protocol. - 84
core.set_tool_worker_exe(PathBuf::from(env!("CARGO_BIN_EXE_vak-tool-worker"))); - 85
core.set_provider_instance(Arc::new(Counting { - 86
dispatches: Arc::new(AtomicUsize::new(0)), - 87
})); - 88
- 89
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - 90
let addr = listener.local_addr().unwrap(); - 91
let home = core.sessions_home(); - 92
let app = vak_server::router(core); - 93
tokio::spawn(async move { - 94
axum::serve(listener, app).await.unwrap(); - 95
}); - 96
Server { - 97
base: format!("http://{addr}"), - 98
home, - 99
cwd, - 100
_dir: dir, - 101
client: reqwest::Client::new(), - 102
} - 103
} - 104
- 105
fn header_for(id: &str, cwd: &Path) -> SessionHeader { - 106
SessionHeader { - 107
agent: None, - 108
session_id: id.to_string(), - 109
created_at: chrono::Utc::now(), - 110
cwd: cwd.to_path_buf(), - 111
parent_session_id: None, - 112
contract_id: None, - 113
work_item_id: None, - 114
conversation: None, - 115
contract: FrozenContract { - 116
app_version: "test".into(), - 117
provider: "counting".into(), - 118
model: "fixture-model".into(), - 119
route_ladder: Vec::new(), - 120
route_objective: String::new(), - 121
route_annotations: Vec::new(), - 122
system_prompt: String::new(), - 123
permission_mode: "workspace-write".into(), - 124
capabilities: Vec::new(), - 125
prompt_layers: Vec::new(), - 126
}, - 127
} - 128
} - 129
- 130
// ---- Memory endpoints -------------------------------------------------------- - 131
- 132
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] - 133
async fn memory_list_forget_amend_across_scopes() { - 134
let srv = spawn_server("").await; - 135
let ws = vak_core::memory::append_note( - 136
&srv.home, - 137
&srv.cwd, - 138
"decision", - 139
"deploy", - 140
"s1", - 141
"pause before rollbacks", - 142
) - 143
.unwrap(); - 144
let profile = vak_core::memory::append_profile_note( - 145
&srv.home, - 146
"preference", - 147
"editor", - 148
"vim bindings", - 149
"s2", - 150
) - 151
.unwrap(); - 152
- 153
// GET lists both tiers with scope annotations. - 154
let res = srv - 155
.client - 156
.get(format!("{}/memory", srv.base)) - 157
.send() - 158
.await - 159
.unwrap(); - 160
assert_eq!(res.status(), 200); - 161
let body: serde_json::Value = res.json().await.unwrap(); - 162
let notes = body["notes"].as_array().unwrap(); - 163
assert_eq!(notes.len(), 2); - 164
let scopes: Vec<&str> = notes.iter().map(|n| n["scope"].as_str().unwrap()).collect(); - 165
assert!(scopes.contains(&"workspace") && scopes.contains(&"profile")); - 166
assert!(notes.iter().all(|n| n["id"].is_string())); - 167
- 168
// Amend keeps provenance and swaps the body. - 169
let res = srv - 170
.client - 171
.patch(format!("{}/memory/{}", srv.base, profile.id)) - 172
.json(&serde_json::json!({"text": "prefers helix now", "scope": "profile"})) - 173
.send() - 174
.await - 175
.unwrap(); - 176
assert_eq!(res.status(), 200); - 177
let amended = vak_core::memory::list_profile_notes(&srv.home); - 178
assert_eq!(amended[0].text, "prefers helix now"); - 179
assert_eq!(amended[0].ts, profile.ts); - 180
- 181
// Default scope is workspace. - 182
let res = srv - 183
.client - 184
.patch(format!("{}/memory/{}", srv.base, ws.id)) - 185
.json(&serde_json::json!({"text": "always dry-run first"})) - 186
.send() - 187
.await - 188
.unwrap(); - 189
assert_eq!(res.status(), 200); - 190
- 191
// Forget removes exactly one block from the right store. - 192
let res = srv - 193
.client - 194
.delete(format!("{}/memory/{}?scope=workspace", srv.base, ws.id)) - 195
.send() - 196
.await - 197
.unwrap(); - 198
assert_eq!(res.status(), 200); - 199
let body: serde_json::Value = res.json().await.unwrap(); - 200
assert_eq!(body["forgotten"], serde_json::json!(ws.id)); - 201
assert!(vak_core::memory::list_notes(&srv.home, &srv.cwd).is_empty()); - 202
assert_eq!(vak_core::memory::list_profile_notes(&srv.home).len(), 1); - 203
- 204
// Unknown ids are typed 404s, in both tiers and both verbs. - 205
for uri in [ - 206
format!("{}/memory/deadbeef?scope=workspace", srv.base), - 207
format!("{}/memory/deadbeef?scope=profile", srv.base), - 208
] { - 209
let res = srv.client.delete(&uri).send().await.unwrap(); - 210
assert_eq!(res.status(), 404, "{uri}"); - 211
let body: serde_json::Value = res.json().await.unwrap(); - 212
assert!( - 213
!body["error"].as_str().unwrap_or_default().is_empty(), - 214
"typed error required: {body}" - 215
); - 216
} - 217
let res = srv - 218
.client - 219
.patch(format!("{}/memory/deadbeef", srv.base)) - 220
.json(&serde_json::json!({"text": "x"})) - 221
.send() - 222
.await - 223
.unwrap(); - 224
assert_eq!(res.status(), 404); - 225
- 226
// Empty amend text is rejected before touching any file. - 227
let res = srv - 228
.client - 229
.patch(format!("{}/memory/{}", srv.base, profile.id)) - 230
.json(&serde_json::json!({"text": " ", "scope": "profile"})) - 231
.send() - 232
.await - 233
.unwrap(); - 234
assert_eq!(res.status(), 400); - 235
- 236
// Cleanup is explicit and safe: it removes an empty orphan directory but - 237
// never removes the profile note that remains above. - 238
std::fs::create_dir_all(srv.home.join("memory/orphan-empty")).unwrap(); - 239
let res = srv - 240
.client - 241
.post(format!("{}/memory/cleanup", srv.base)) - 242
.send() - 243
.await - 244
.unwrap(); - 245
assert_eq!(res.status(), 200); - 246
let cleanup: serde_json::Value = res.json().await.unwrap(); - 247
assert_eq!(cleanup["removed_empty_dirs"], 1); - 248
assert!(!srv.home.join("memory/orphan-empty").exists()); - 249
assert_eq!(vak_core::memory::list_profile_notes(&srv.home).len(), 1); - 250
} - 251
- 252
// ---- Cross-project search ---------------------------------------------------- - 253
- 254
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] - 255
async fn search_all_spans_projects_and_flags_the_scope() { - 256
let srv = spawn_server("").await; - 257
// One ledger in THIS project's hash dir… - 258
let here = SessionPath::new_session_file(&srv.home, &srv.cwd, "22222222-here"); - 259
let mut log = SessionLog::create(here, header_for("22222222-here", &srv.cwd)).unwrap(); - 260
log.append_message(vak_session::types::MessageRecord { - 261
message: vak_llm::Message::user_text("local deploy checklist lives in ops"), - 262
meta: None, - 263
}) - 264
.unwrap(); - 265
drop(log); - 266
// …and one under a DIFFERENT project hash dir. - 267
let other_cwd = srv.cwd.join("other-project"); - 268
let there = SessionPath::new_session_file(&srv.home, &other_cwd, "33333333-there"); - 269
let mut log = SessionLog::create(there, header_for("33333333-there", &other_cwd)).unwrap(); - 270
log.append_message(vak_session::types::MessageRecord { - 271
message: vak_llm::Message::user_text("the rollout checklist lives elsewhere"), - 272
meta: None, - 273
}) - 274
.unwrap(); - 275
drop(log); - 276
- 277
// Workspace-scoped search sees only this project's ledger. - 278
let res = srv - 279
.client - 280
.get(format!("{}/search", srv.base)) - 281
.query(&[("q", "checklist")]) - 282
.send() - 283
.await - 284
.unwrap(); - 285
let body: serde_json::Value = res.json().await.unwrap(); - 286
assert_eq!(body["all"], false); - 287
let ids: Vec<&str> = body["hits"] - 288
.as_array() - 289
.unwrap() - 290
.iter() - 291
.map(|h| h["session_id"].as_str().unwrap()) - 292
.collect(); - 293
assert_eq!(ids, vec!["22222222-here"]); - 294
- 295
// all=true spans every project dir and annotates the origin. - 296
let res = srv - 297
.client - 298
.get(format!("{}/search", srv.base)) - 299
.query(&[("q", "checklist"), ("all", "true")]) - 300
.send() - 301
.await - 302
.unwrap(); - 303
let body: serde_json::Value = res.json().await.unwrap(); - 304
assert_eq!(body["all"], true); - 305
let hits = body["hits"].as_array().unwrap(); - 306
assert_eq!(hits.len(), 2, "{body}"); - 307
assert!(hits.iter().all(|h| h["project_hash"].is_string())); - 308
} - 309
- 310
// ---- Markdown transcript export ---------------------------------------------- - 311
- 312
// Regression: a fresh server process starts with an empty in-memory handle - 313
// map; historical sessions must still export from disk (found by live - 314
// dogfooding — transcript.md 404'd for every non-attached session, and the - 315
// JSON endpoint masked the same failure as a 200-wrapped error body). - 316
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] - 317
async fn historical_sessions_serve_from_disk_without_attach() { - 318
let srv = spawn_server("").await; - 319
let id = "55555555-disk"; - 320
let path = SessionPath::new_session_file(&srv.home, &srv.cwd, id); - 321
let mut log = SessionLog::create(path, header_for(id, &srv.cwd)).unwrap(); - 322
log.append_message(vak_session::types::MessageRecord { - 323
message: vak_llm::Message::user_text("historical question"), - 324
meta: None, - 325
}) - 326
.unwrap(); - 327
log.append_message(vak_session::types::MessageRecord { - 328
message: vak_llm::Message::assistant(vec![ContentBlock::text("historical answer")]), - 329
meta: None, - 330
}) - 331
.unwrap(); - 332
drop(log); - 333
- 334
let res = srv - 335
.client - 336
.get(format!("{}/sessions/{id}/transcript.md", srv.base)) - 337
.send() - 338
.await - 339
.unwrap(); - 340
assert_eq!(res.status(), 200); - 341
let body = res.text().await.unwrap(); - 342
assert!(body.contains("historical answer")); - 343
- 344
let res = srv - 345
.client - 346
.get(format!("{}/sessions/{id}/transcript", srv.base)) - 347
.send() - 348
.await - 349
.unwrap(); - 350
assert_eq!(res.status(), 200); - 351
let body: serde_json::Value = res.json().await.unwrap(); - 352
assert_eq!(body["count"], 2); - 353
- 354
let res = srv - 355
.client - 356
.get(format!("{}/sessions/unknown/transcript.md", srv.base)) - 357
.send() - 358
.await - 359
.unwrap(); - 360
assert_eq!(res.status(), 404); - 361
} - 362
- 363
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] - 364
async fn transcript_md_equals_shared_renderer_byte_for_byte() { - 365
let srv = spawn_server("").await; - 366
let id = "44444444-md"; - 367
let path = SessionPath::new_session_file(&srv.home, &srv.cwd, id); - 368
let mut log = SessionLog::create(path, header_for(id, &srv.cwd)).unwrap(); - 369
let user_msg = vak_llm::Message::user_text("fix the flaky test"); - 370
log.append_message(vak_session::types::MessageRecord { - 371
message: user_msg.clone(), - 372
meta: None, - 373
}) - 374
.unwrap(); - 375
log.append_message(vak_session::types::MessageRecord { - 376
message: vak_llm::Message::assistant(vec![ContentBlock::text("all done")]), - 377
meta: None, - 378
}) - 379
.unwrap(); - 380
drop(log); - 381
// The transcript endpoints serve live handles; attach like a client. - 382
srv.client - 383
.post(format!("{}/sessions/{id}/attach", srv.base)) - 384
.json(&serde_json::json!({"session_id": id})) - 385
.send() - 386
.await - 387
.unwrap(); - 388
- 389
let res = srv - 390
.client - 391
.get(format!("{}/sessions/{id}/transcript.md", srv.base)) - 392
.send() - 393
.await - 394
.unwrap(); - 395
assert_eq!(res.status(), 200); - 396
assert!( - 397
res.headers() - 398
.get(reqwest::header::CONTENT_TYPE) - 399
.and_then(|v| v.to_str().ok()) - 400
.unwrap_or("") - 401
.starts_with("text/markdown") - 402
); - 403
let body = res.text().await.unwrap(); - 404
- 405
// The exact projection the JSON transcript serves, through the one - 406
// shared renderer — byte parity by construction. - 407
let expected = vak_core::transcript_md::render_markdown(&[ - 408
user_msg, - 409
vak_llm::Message::assistant(vec![ContentBlock::text("all done")]), - 410
]); - 411
assert_eq!(body, expected); - 412
- 413
let res = srv - 414
.client - 415
.get(format!("{}/sessions/unknown/transcript.md", srv.base)) - 416
.send() - 417
.await - 418
.unwrap(); - 419
assert_eq!(res.status(), 404); - 420
} - 421
- 422
// ---- Doctor ------------------------------------------------------------------- - 423
- 424
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] - 425
async fn doctor_reports_checks_facts_and_optional_ladder() { - 426
let srv = spawn_server("").await; - 427
let res = srv - 428
.client - 429
.get(format!("{}/doctor", srv.base)) - 430
.send() - 431
.await - 432
.unwrap(); - 433
assert_eq!(res.status(), 200); - 434
let body: serde_json::Value = res.json().await.unwrap(); - 435
let labels: Vec<&str> = body["checks"] - 436
.as_array() - 437
.unwrap() - 438
.iter() - 439
.map(|c| c["label"].as_str().unwrap()) - 440
.collect(); - 441
// Order mirrors the pushes in vak_core::health::collect. "install - 442
// layout" (canonical-layout conformance, doc 32) joined the ladder in - 443
// f6131a5 and this expectation was never updated; the failure stayed - 444
// hidden because a deadlock in gateway.rs stopped this binary from - 445
// ever running. - 446
assert_eq!( - 447
labels, - 448
vec![ - 449
"provider", - 450
"sessions home", - 451
"config warnings", - 452
// Configured integrations the composed policy would refuse. - 453
// Belongs beside the config checks: "you set this up and it - 454
// does not work" is a health fact, not a transcript detail. - 455
"capability reach", - 456
"voice configuration", - 457
"local transcriber", - 458
"local TTS backend", - 459
// Configured capabilities that are currently unusable, with the - 460
// reason and the fix. Previously these were rendered only into - 461
// the system prompt, so the model was told a server was down and - 462
// the operator who could repair it was not. - 463
"capability health", - 464
// docs/design/34: channel state is part of the health surface, - 465
// not a config detail. - 466
"gateway channels", - 467
"install layout", - 468
"self version parity", - 469
"retired plugins", - 470
"agent roster", - 471
] - 472
); - 473
assert!( - 474
body["facts"] - 475
.as_array() - 476
.unwrap() - 477
.iter() - 478
.any(|f| f.as_str().unwrap().starts_with("model ")) - 479
); - 480
// Every check must pass except "self version parity", which compares - 481
// this build against whatever release is installed on the machine - 482
// running the suite. Mid-release — built 0.11.12, installed 0.11.11 — - 483
// that check legitimately fails, and asserting a bare zero here made - 484
// the suite a function of host state rather than of this code. - 485
let failed: Vec<&str> = body["checks"] - 486
.as_array() - 487
.unwrap() - 488
.iter() - 489
.filter(|c| c["ok"] == serde_json::Value::Bool(false)) - 490
.map(|c| c["label"].as_str().unwrap()) - 491
.collect(); - 492
assert!( - 493
failed.iter().all(|l| *l == "self version parity"), - 494
"unexpected doctor failures: {failed:?}" - 495
); - 496
assert!(body["ladder"].is_null(), "no session requested"); - 497
- 498
// With a session, the frozen-ladder section appears. - 499
let id = "55555555-doctor"; - 500
let path = SessionPath::new_session_file(&srv.home, &srv.cwd, id); - 501
let mut log = SessionLog::create(path, header_for(id, &srv.cwd)).unwrap(); - 502
log.append_message(vak_session::types::MessageRecord { - 503
message: vak_llm::Message::user_text("hi"), - 504
meta: None, - 505
}) - 506
.unwrap(); - 507
drop(log); - 508
srv.client - 509
.post(format!("{}/sessions/{id}/attach", srv.base)) - 510
.json(&serde_json::json!({"session_id": id})) - 511
.send() - 512
.await - 513
.unwrap(); - 514
let res = srv - 515
.client - 516
.get(format!("{}/doctor", srv.base)) - 517
.query(&[("session", id)]) - 518
.send() - 519
.await - 520
.unwrap(); - 521
let body: serde_json::Value = res.json().await.unwrap(); - 522
let ladder = &body["ladder"]; - 523
assert!(!ladder.is_null()); - 524
assert_eq!( - 525
ladder["rendered"], "fixture-model", - 526
"legacy header falls back to the model id" - 527
); - 528
assert_eq!(ladder["fallback_legs"], 0); - 529
} - 530
- 531
// ---- Backup export/import ------------------------------------------------------- - 532
- 533
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] - 534
async fn backup_roundtrip_preserves_ledgers_and_rejects_self_backup() { - 535
let srv = spawn_server("").await; - 536
std::fs::create_dir_all(srv.home.join("sessions/x")).unwrap(); - 537
std::fs::write(srv.home.join("cost-log.jsonl"), "{\"kind\":\"cost\"}\n").unwrap(); - 538
std::fs::write(srv.home.join("sessions/x/a.jsonl"), "ledger-bytes").unwrap(); - 539
let dest = tempfile::tempdir().unwrap(); - 540
- 541
let res = srv - 542
.client - 543
.post(format!("{}/backup/export", srv.base)) - 544
.json(&serde_json::json!({ - 545
"dest_dir": dest.path().display().to_string(), - 546
"include_secrets": false - 547
})) - 548
.send() - 549
.await - 550
.unwrap(); - 551
assert_eq!(res.status(), 200); - 552
let body: serde_json::Value = res.json().await.unwrap(); - 553
assert_eq!(body["manifest"]["file_count"], 2); - 554
assert_eq!(body["included_secrets"], false); - 555
assert_eq!( - 556
std::fs::read_to_string(dest.path().join("sessions/x/a.jsonl")).unwrap(), - 557
"ledger-bytes" - 558
); - 559
- 560
// Wipe the live data, then restore: skip-conflict report counts copies. - 561
std::fs::remove_file(srv.home.join("cost-log.jsonl")).unwrap(); - 562
std::fs::remove_file(srv.home.join("sessions/x/a.jsonl")).unwrap(); - 563
let res = srv - 564
.client - 565
.post(format!("{}/backup/import", srv.base)) - 566
.json(&serde_json::json!({ - 567
"src_dir": dest.path().display().to_string(), - 568
"conflict": "skip" - 569
})) - 570
.send() - 571
.await - 572
.unwrap(); - 573
assert_eq!(res.status(), 200); - 574
let body: serde_json::Value = res.json().await.unwrap(); - 575
assert_eq!(body["copied"], 2); - 576
assert_eq!(body["skipped"], 0); - 577
assert_eq!( - 578
std::fs::read_to_string(srv.home.join("cost-log.jsonl")).unwrap(), - 579
"{\"kind\":\"cost\"}\n", - 580
"roundtrip must be byte-identical" - 581
); - 582
- 583
// Re-import with rename preserves BOTH copies. - 584
let res = srv - 585
.client - 586
.post(format!("{}/backup/import", srv.base)) - 587
.json(&serde_json::json!({ - 588
"src_dir": dest.path().display().to_string(), - 589
"conflict": "rename" - 590
})) - 591
.send() - 592
.await - 593
.unwrap(); - 594
let body: serde_json::Value = res.json().await.unwrap(); - 595
assert_eq!(body["renamed"], 2); - 596
assert!(srv.home.join("cost-log.import1.jsonl").is_file()); - 597
- 598
// Self-backup is rejected on both directions, typed 400. - 599
for (uri, field) in [ - 600
("/backup/export", "dest_dir"), - 601
("/backup/import", "src_dir"), - 602
] { - 603
let res = srv - 604
.client - 605
.post(format!("{}{uri}", srv.base)) - 606
.json(&serde_json::json!({field: srv.home.display().to_string()})) - 607
.send() - 608
.await - 609
.unwrap(); - 610
assert_eq!(res.status(), 400, "{uri}"); - 611
let body: serde_json::Value = res.json().await.unwrap(); - 612
assert!( - 613
body["error"].as_str().unwrap().contains("home itself"), - 614
"{body}" - 615
); - 616
} - 617
- 618
// Unknown conflict policy is a typed 400 too. - 619
let res = srv - 620
.client - 621
.post(format!("{}/backup/import", srv.base)) - 622
.json(&serde_json::json!({ - 623
"src_dir": dest.path().display().to_string(), - 624
"conflict": "overwrite" - 625
})) - 626
.send() - 627
.await - 628
.unwrap(); - 629
assert_eq!(res.status(), 400); - 630
} - 631
- 632
// ---- Digest --------------------------------------------------------------------- - 633
- 634
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] - 635
async fn digest_reports_window_math_and_clamps_days() { - 636
let srv = spawn_server("").await; - 637
// Where every run writes it: the shared data home, not the Agent's. - 638
let ledger = vak_core::finops::FinOpsLedger::new(&srv._dir.path().join("home")); - 639
ledger - 640
.append(&vak_core::finops::CostRow { - 641
ts: chrono::Utc::now() - chrono::Duration::hours(2), - 642
model: "claude-sonnet".into(), - 643
provider: "anthropic".into(), - 644
input_tokens: 100, - 645
output_tokens: 50, - 646
cache_read_input_tokens: None, - 647
usd: Some(0.25), - 648
source: "estimated".into(), - 649
session_id: "s-digest".into(), - 650
}) - 651
.unwrap(); - 652
- 653
let res = srv - 654
.client - 655
.get(format!("{}/digest", srv.base)) - 656
.send() - 657
.await - 658
.unwrap(); - 659
assert_eq!(res.status(), 200); - 660
let body: serde_json::Value = res.json().await.unwrap(); - 661
assert_eq!(body["days"], 7, "default window"); - 662
assert_eq!(body["dispatches"], 1); - 663
assert!((body["total_usd"].as_f64().unwrap() - 0.25).abs() < 1e-9); - 664
assert_eq!(body["by_model"]["claude-sonnet"]["rows"], 1); - 665
assert_eq!(body["distinct_sessions"], serde_json::json!(["s-digest"])); - 666
- 667
let res = srv - 668
.client - 669
.get(format!("{}/digest", srv.base)) - 670
.query(&[("days", "500")]) - 671
.send() - 672
.await - 673
.unwrap(); - 674
let body: serde_json::Value = res.json().await.unwrap(); - 675
assert_eq!(body["days"], 90, "clamped high"); - 676
- 677
let res = srv - 678
.client - 679
.get(format!("{}/digest", srv.base)) - 680
.query(&[("days", "0")]) - 681
.send() - 682
.await - 683
.unwrap(); - 684
let body: serde_json::Value = res.json().await.unwrap(); - 685
assert_eq!(body["days"], 1, "clamped low"); - 686
} - 687
- 688
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] - 689
async fn finops_projects_observed_tokens_and_activity_without_zeroing_unknown_cost() { - 690
let srv = spawn_server("").await; - 691
let cost = vak_core::finops::FinOpsLedger::new(&srv.home); - 692
cost.append(&vak_core::finops::CostRow { - 693
ts: chrono::Utc::now(), - 694
model: "unpriced-model".into(), - 695
provider: "counting".into(), - 696
input_tokens: 12, - 697
output_tokens: 7, - 698
cache_read_input_tokens: Some(3), - 699
usd: None, - 700
source: "estimated".into(), - 701
session_id: "s-finops".into(), - 702
}) - 703
.unwrap(); - 704
let activity = vak_core::finops::ActivityLedger::new(&srv.home); - 705
activity - 706
.append(&vak_core::finops::ActivityRow { - 707
ts: chrono::Utc::now(), - 708
kind: "mcp".into(), - 709
name: "plugin.demo/search".into(), - 710
success: true, - 711
duration_ms: Some(19), - 712
session_id: Some("s-finops".into()), - 713
plugin: Some("demo".into()), - 714
}) - 715
.unwrap(); - 716
- 717
let body: serde_json::Value = srv - 718
.client - 719
.get(format!("{}/finops", srv.base)) - 720
.send() - 721
.await - 722
.unwrap() - 723
.json() - 724
.await - 725
.unwrap(); - 726
assert_eq!(body["day_input_tokens"], 12); - 727
assert_eq!(body["day_output_tokens"], 7); - 728
assert_eq!(body["unknown_rows"], 1); - 729
assert_eq!(body["day_usd"], 0.0); - 730
assert_eq!(body["activity"][0]["kind"], "mcp"); - 731
assert_eq!(body["activity"][0]["plugin"], "demo"); - 732
assert_eq!(body["activity"][0]["duration_ms"], 19); - 733
} - 734
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.