- 1
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 2
- 3
use std::collections::HashSet; - 4
use std::path::PathBuf; - 5
- 6
use tempfile::tempdir; - 7
- 8
use vak_llm::{ContentBlock, Message, Role, Usage}; - 9
use vak_session::types::{ - 10
EntryPayload, FrozenContract, MessageMeta, MessageRecord, SessionHeader, TurnCapabilitiesBound, - 11
WorkContract, WorkEvent, WorkEventKind, WorkItemDefinition, WorkOwner, - 12
}; - 13
use vak_session::{ - 14
ActivityKind, ActivityRecord, ActivityStatus, Fidelity, SessionLog, SessionPath, WorkingSetPlan, - 15
}; - 16
- 17
fn header() -> SessionHeader { - 18
SessionHeader { - 19
agent: None, - 20
session_id: "s-test".into(), - 21
created_at: chrono::Utc::now(), - 22
cwd: PathBuf::from("/tmp/proj"), - 23
parent_session_id: None, - 24
contract_id: None, - 25
work_item_id: None, - 26
conversation: None, - 27
contract: FrozenContract { - 28
app_version: "0.1.0".into(), - 29
provider: "anthropic".into(), - 30
model: "claude-sonnet-4-5".into(), - 31
route_ladder: Vec::new(), - 32
route_objective: String::new(), - 33
route_annotations: Vec::new(), - 34
system_prompt: "system prompt v1".into(), - 35
permission_mode: "workspace-write".into(), - 36
capabilities: Vec::new(), - 37
prompt_layers: Vec::new(), - 38
}, - 39
} - 40
} - 41
- 42
#[test] - 43
fn successful_tool_receipts_retain_replay_timestamp() { - 44
let dir = tempdir().unwrap(); - 45
let path = dir.path().join("receipts.jsonl"); - 46
let mut log = SessionLog::create(path.clone(), header()).unwrap(); - 47
log.append_message(MessageRecord { - 48
message: Message { - 49
role: Role::Assistant, - 50
content: vec![ContentBlock::ToolUse { - 51
id: "tool-1".into(), - 52
name: "read".into(), - 53
input: serde_json::json!({"path":"notes.txt"}), - 54
}], - 55
}, - 56
meta: None, - 57
}) - 58
.unwrap(); - 59
log.append_message(MessageRecord { - 60
message: Message { - 61
role: Role::User, - 62
content: vec![ContentBlock::tool_result("tool-1", "notes")], - 63
}, - 64
meta: None, - 65
}) - 66
.unwrap(); - 67
drop(log); - 68
let reopened = SessionLog::open(path).unwrap(); - 69
let receipts = reopened.successful_tool_receipts(); - 70
assert_eq!(receipts.len(), 1); - 71
assert_eq!(receipts[0].0, "tool-1"); - 72
assert!(receipts[0].1 <= chrono::Utc::now()); - 73
} - 74
- 75
fn user_msg(text: &str) -> MessageRecord { - 76
MessageRecord { - 77
message: Message::user_text(text), - 78
meta: None, - 79
} - 80
} - 81
- 82
fn assistant_msg(text: &str) -> MessageRecord { - 83
MessageRecord { - 84
message: Message::assistant(vec![ContentBlock::text(text)]), - 85
meta: None, - 86
} - 87
} - 88
- 89
fn work_contract(id: &str) -> WorkContract { - 90
WorkContract { - 91
contract_id: id.into(), - 92
revision: 0, - 93
source_entry_id: "prompt".into(), - 94
objective: "test work".into(), - 95
constraints: Vec::new(), - 96
assumptions: Vec::new(), - 97
criteria: Vec::new(), - 98
items: vec![WorkItemDefinition { - 99
item_id: "one".into(), - 100
title: "one".into(), - 101
instructions: "one".into(), - 102
dependencies: Vec::new(), - 103
owner: WorkOwner::ParentAgent, - 104
required: true, - 105
readonly: false, - 106
path_claims: Vec::new(), - 107
criterion_ids: Vec::new(), - 108
}], - 109
} - 110
} - 111
- 112
#[test] - 113
fn invalid_work_event_is_rejected_without_poisoning_the_ledger() { - 114
let dir = tempdir().unwrap(); - 115
let mut log = SessionLog::create(dir.path().join("work.jsonl"), header()).unwrap(); - 116
log.append_work(WorkEvent { - 117
contract_id: "work-atomic".into(), - 118
revision: 0, - 119
kind: WorkEventKind::ContractCreated { - 120
contract: work_contract("work-atomic"), - 121
}, - 122
}) - 123
.unwrap(); - 124
log.append_work(WorkEvent { - 125
contract_id: "work-atomic".into(), - 126
revision: 0, - 127
kind: WorkEventKind::ContractStatusChanged { - 128
from: vak_session::types::WorkContractStatus::Draft, - 129
to: vak_session::types::WorkContractStatus::Active, - 130
reason: "test".into(), - 131
}, - 132
}) - 133
.unwrap(); - 134
let before = log.len(); - 135
assert!( - 136
log.append_work(WorkEvent { - 137
contract_id: "work-atomic".into(), - 138
revision: 0, - 139
kind: WorkEventKind::ItemStatusChanged { - 140
item_id: "one".into(), - 141
from: vak_session::types::WorkItemStatus::Proposed, - 142
to: vak_session::types::WorkItemStatus::Ready, - 143
attempt: 0, - 144
reason: "duplicate".into(), - 145
}, - 146
}) - 147
.is_err() - 148
); - 149
assert_eq!(log.len(), before); - 150
assert!(log.work_projection().unwrap().is_some()); - 151
} - 152
- 153
#[test] - 154
fn append_and_derive_roundtrip() { - 155
let dir = tempdir().unwrap(); - 156
let path = dir.path().join("s.jsonl"); - 157
let mut log = SessionLog::create(path.clone(), header()).unwrap(); - 158
- 159
log.append_message(user_msg("first")).unwrap(); - 160
log.append_message(MessageRecord { - 161
message: Message::assistant(vec![ContentBlock::text("second")]), - 162
meta: Some(MessageMeta { - 163
model: Some("claude-sonnet-4-5".into()), - 164
stop_reason: Some("end_turn".into()), - 165
usage: Some(Usage { - 166
input_tokens: 10, - 167
output_tokens: 5, - 168
..Default::default() - 169
}), - 170
..Default::default() - 171
}), - 172
}) - 173
.unwrap(); - 174
- 175
// The create handle holds the ledger's exclusive lock; release it - 176
// before reopening. - 177
drop(log); - 178
let reopened = SessionLog::open(path).unwrap(); - 179
assert_eq!(reopened.len(), 3); - 180
let msgs = reopened.derive_messages(); - 181
assert_eq!(msgs.len(), 2); - 182
assert_eq!(msgs[0].text_content(), "first"); - 183
assert_eq!(msgs[1].role, Role::Assistant); - 184
assert_eq!(reopened.total_usage().output_tokens, 5); - 185
assert_eq!( - 186
reopened.header().unwrap().contract.model, - 187
"claude-sonnet-4-5" - 188
); - 189
} - 190
- 191
#[test] - 192
fn activity_roundtrips_without_entering_model_context() { - 193
let dir = tempdir().unwrap(); - 194
let path = dir.path().join("activity.jsonl"); - 195
let mut log = SessionLog::create(path.clone(), header()).unwrap(); - 196
log.append_message(user_msg("keep me visible")).unwrap(); - 197
log.append_activity(ActivityRecord { - 198
activity_id: "retry-1".into(), - 199
turn: Some(1), - 200
kind: ActivityKind::Retry, - 201
status: ActivityStatus::Succeeded, - 202
label: "Recovered after retry".into(), - 203
detail: Some("provider timeout".into()), - 204
data: [("attempt".into(), "1".into())].into(), - 205
}) - 206
.unwrap(); - 207
assert_eq!(log.derive_messages().len(), 1); - 208
drop(log); - 209
- 210
let reopened = SessionLog::open(path).unwrap(); - 211
assert_eq!(reopened.derive_messages().len(), 1); - 212
let activities = reopened.activities(); - 213
assert_eq!(activities.len(), 1); - 214
assert_eq!(activities[0].2.activity_id, "retry-1"); - 215
} - 216
- 217
#[test] - 218
fn voice_activity_helpers_record_transcript_and_playback() { - 219
let dir = tempfile::tempdir().unwrap(); - 220
let path = dir.path().join("voice.jsonl"); - 221
let mut log = SessionLog::create(path, header()).unwrap(); - 222
let transcript = log - 223
.append_voice_transcript("u1", "open the build log", true) - 224
.unwrap(); - 225
let playback = log.append_voice_playback("p1", 640, true).unwrap(); - 226
let EntryPayload::Activity(a) = transcript.payload else { - 227
panic!("expected transcript activity") - 228
}; - 229
assert_eq!(a.kind, ActivityKind::VoiceTranscript); - 230
assert_eq!(a.data.get("finalized"), Some(&"true".to_string())); - 231
let EntryPayload::Activity(a) = playback.payload else { - 232
panic!("expected playback activity") - 233
}; - 234
assert_eq!(a.kind, ActivityKind::VoicePlayback); - 235
assert_eq!(a.status, ActivityStatus::Partial); - 236
} - 237
- 238
/// Active work state reaches the model through the request tail - 239
/// (docs/design/68-context-engine.md §6/§10), not spliced into the - 240
/// projection. - 241
#[test] - 242
fn active_work_is_reconstructed_into_the_tail() { - 243
let dir = tempdir().unwrap(); - 244
let mut log = SessionLog::create(dir.path().join("work.jsonl"), header()).unwrap(); - 245
let contract = WorkContract { - 246
contract_id: "work-ctx".into(), - 247
revision: 0, - 248
source_entry_id: "user".into(), - 249
objective: "preserve active work".into(), - 250
constraints: Vec::new(), - 251
assumptions: Vec::new(), - 252
criteria: Vec::new(), - 253
items: vec![WorkItemDefinition { - 254
item_id: "inspect".into(), - 255
title: "Inspect".into(), - 256
instructions: "inspect".into(), - 257
dependencies: Vec::new(), - 258
owner: WorkOwner::ParentAgent, - 259
required: true, - 260
readonly: true, - 261
path_claims: Vec::new(), - 262
criterion_ids: Vec::new(), - 263
}], - 264
}; - 265
log.append_work(WorkEvent { - 266
contract_id: "work-ctx".into(), - 267
revision: 0, - 268
kind: WorkEventKind::ContractCreated { contract }, - 269
}) - 270
.unwrap(); - 271
assert!( - 272
log.derive_messages().is_empty(), - 273
"work state must not be spliced into the projection" - 274
); - 275
let work_contract = log - 276
.tail_sections(None) - 277
.work_contract - 278
.expect("active work contract present in the tail"); - 279
assert!(work_contract.contains("preserve active work")); - 280
assert!(work_contract.contains("inspect")); - 281
} - 282
- 283
#[test] - 284
fn active_work_survives_compaction() { - 285
let dir = tempdir().unwrap(); - 286
let mut log = SessionLog::create(dir.path().join("work-compaction.jsonl"), header()).unwrap(); - 287
log.append_message(user_msg("old context")).unwrap(); - 288
let contract = WorkContract { - 289
contract_id: "work-compaction".into(), - 290
revision: 0, - 291
source_entry_id: "user".into(), - 292
objective: "survive compaction".into(), - 293
constraints: Vec::new(), - 294
assumptions: Vec::new(), - 295
criteria: Vec::new(), - 296
items: vec![WorkItemDefinition { - 297
item_id: "verify".into(), - 298
title: "Verify evidence".into(), - 299
instructions: "verify".into(), - 300
dependencies: Vec::new(), - 301
owner: WorkOwner::ParentAgent, - 302
required: true, - 303
readonly: true, - 304
path_claims: Vec::new(), - 305
criterion_ids: Vec::new(), - 306
}], - 307
}; - 308
log.append_work(WorkEvent { - 309
contract_id: "work-compaction".into(), - 310
revision: 0, - 311
kind: WorkEventKind::ContractCreated { contract }, - 312
}) - 313
.unwrap(); - 314
log.append_message(user_msg("keep this")).unwrap(); - 315
log.append_handoff_reset("old summary".into(), 9000) - 316
.unwrap(); - 317
assert!( - 318
log.tail_sections(None) - 319
.work_contract - 320
.expect("active work contract present in the tail") - 321
.contains("survive compaction") - 322
); - 323
} - 324
- 325
#[test] - 326
fn restart_reconciles_orphaned_running_work_without_replaying_it() { - 327
let dir = tempdir().unwrap(); - 328
let mut log = SessionLog::create(dir.path().join("work-recovery.jsonl"), header()).unwrap(); - 329
let contract = WorkContract { - 330
contract_id: "work-recovery".into(), - 331
revision: 0, - 332
source_entry_id: "user".into(), - 333
objective: "recover safely".into(), - 334
constraints: Vec::new(), - 335
assumptions: Vec::new(), - 336
criteria: Vec::new(), - 337
items: vec![WorkItemDefinition { - 338
item_id: "mutate".into(), - 339
title: "Mutate safely".into(), - 340
instructions: "perform one operation".into(), - 341
dependencies: Vec::new(), - 342
owner: WorkOwner::ParentAgent, - 343
required: true, - 344
readonly: false, - 345
path_claims: Vec::new(), - 346
criterion_ids: Vec::new(), - 347
}], - 348
}; - 349
log.append_work(WorkEvent { - 350
contract_id: "work-recovery".into(), - 351
revision: 0, - 352
kind: WorkEventKind::ContractCreated { contract }, - 353
}) - 354
.unwrap(); - 355
log.append_work(WorkEvent { - 356
contract_id: "work-recovery".into(), - 357
revision: 0, - 358
kind: WorkEventKind::ContractStatusChanged { - 359
from: vak_session::types::WorkContractStatus::Draft, - 360
to: vak_session::types::WorkContractStatus::Active, - 361
reason: "test".into(), - 362
}, - 363
}) - 364
.unwrap(); - 365
log.append_work(WorkEvent { - 366
contract_id: "work-recovery".into(), - 367
revision: 0, - 368
kind: WorkEventKind::ItemStatusChanged { - 369
item_id: "mutate".into(), - 370
from: vak_session::types::WorkItemStatus::Ready, - 371
to: vak_session::types::WorkItemStatus::Running, - 372
attempt: 1, - 373
reason: "test".into(), - 374
}, - 375
}) - 376
.unwrap(); - 377
- 378
assert_eq!(log.reconcile_running_work(&HashSet::new()).unwrap(), 1); - 379
let projection = log.work_projection().unwrap().unwrap(); - 380
assert_eq!( - 381
projection.items["mutate"].status, - 382
vak_session::types::WorkItemStatus::Interrupted - 383
); - 384
assert!(projection.items["mutate"].blocker.is_some()); - 385
} - 386
- 387
#[test] - 388
fn restart_attaches_completed_child_for_verification_without_marking_it_succeeded() { - 389
let dir = tempdir().unwrap(); - 390
let child_path = - 391
SessionPath::new_session_file(dir.path(), PathBuf::from("/tmp/proj").as_path(), "child-1"); - 392
let mut child = SessionLog::create( - 393
child_path, - 394
SessionHeader { - 395
agent: None, - 396
session_id: "child-1".into(), - 397
..header() - 398
}, - 399
) - 400
.unwrap(); - 401
child - 402
.append_child_run_status(vak_session::types::ChildRunStatus::Completed) - 403
.unwrap(); - 404
drop(child); - 405
- 406
let mut log = SessionLog::create(dir.path().join("parent.jsonl"), header()).unwrap(); - 407
let mut contract = work_contract("child-recovery"); - 408
contract.items[0].owner = WorkOwner::Worker; - 409
log.append_work(WorkEvent { - 410
contract_id: "child-recovery".into(), - 411
revision: 0, - 412
kind: WorkEventKind::ContractCreated { contract }, - 413
}) - 414
.unwrap(); - 415
log.append_work(WorkEvent { - 416
contract_id: "child-recovery".into(), - 417
revision: 0, - 418
kind: WorkEventKind::ContractStatusChanged { - 419
from: vak_session::types::WorkContractStatus::Draft, - 420
to: vak_session::types::WorkContractStatus::Active, - 421
reason: "delegating".into(), - 422
}, - 423
}) - 424
.unwrap(); - 425
log.append_work(WorkEvent { - 426
contract_id: "child-recovery".into(), - 427
revision: 0, - 428
kind: WorkEventKind::ItemStatusChanged { - 429
item_id: "one".into(), - 430
from: vak_session::types::WorkItemStatus::Ready, - 431
to: vak_session::types::WorkItemStatus::Running, - 432
attempt: 1, - 433
reason: "delegated".into(), - 434
}, - 435
}) - 436
.unwrap(); - 437
log.append_work(WorkEvent { - 438
contract_id: "child-recovery".into(), - 439
revision: 0, - 440
kind: WorkEventKind::ItemAssigned { - 441
item_id: "one".into(), - 442
owner: WorkOwner::Worker, - 443
child_session_id: Some("child-1".into()), - 444
}, - 445
}) - 446
.unwrap(); - 447
assert_eq!( - 448
log.reconcile_running_work_with_child_ledgers(&HashSet::new(), Some(dir.path())) - 449
.unwrap(), - 450
1 - 451
); - 452
let state = log.work_projection().unwrap().unwrap().items["one"].clone(); - 453
assert_eq!( - 454
state.status, - 455
vak_session::types::WorkItemStatus::ReadyForVerification - 456
); - 457
assert_eq!(state.evidence.len(), 1); - 458
} - 459
- 460
#[test] - 461
fn branching_derives_only_active_path() { - 462
// Each user/assistant pair is a whole turn (docs/design/68-context- - 463
// engine.md principle 3); the projection now renders a closed turn as - 464
// its two-message full record, so the fixture needs a reply per turn - 465
// to exercise that rather than the old bare-message-per-turn shape. - 466
let dir = tempdir().unwrap(); - 467
let mut log = SessionLog::create(dir.path().join("s.jsonl"), header()).unwrap(); - 468
log.append_message(user_msg("a")).unwrap(); - 469
log.append_message(assistant_msg("a-reply")).unwrap(); - 470
log.append_message(user_msg("b")).unwrap(); - 471
let fork_point = log.append_message(assistant_msg("b-reply")).unwrap(); - 472
log.append_message(user_msg("c")).unwrap(); - 473
log.append_message(assistant_msg("c-reply")).unwrap(); - 474
- 475
log.branch_at(&fork_point.id).unwrap(); - 476
log.append_message(user_msg("d")).unwrap(); - 477
log.append_message(assistant_msg("d-reply")).unwrap(); - 478
- 479
let texts: Vec<String> = log - 480
.derive_messages() - 481
.iter() - 482
.map(|m| m.text_content()) - 483
.collect(); - 484
assert_eq!(texts, vec!["a", "a-reply", "b", "b-reply", "d", "d-reply"]); - 485
} - 486
- 487
/// A packet is rendered only when the plan's `packet_range` is exactly the - 488
/// range it covers (docs/design/68-context-engine.md §4): the packet is a - 489
/// cache of summariser work, never a boundary in the ledger. - 490
#[test] - 491
fn packet_is_rendered_only_for_the_exact_range_the_plan_asks_for() { - 492
let dir = tempdir().unwrap(); - 493
let mut log = SessionLog::create(dir.path().join("s.jsonl"), header()).unwrap(); - 494
let old1 = log.append_message(user_msg("old-1")).unwrap().id; - 495
log.append_message(assistant_msg("old-1-reply")).unwrap(); - 496
let old2 = log.append_message(user_msg("old-2")).unwrap().id; - 497
log.append_message(assistant_msg("old-2-reply")).unwrap(); - 498
let kept = log.append_message(user_msg("kept")).unwrap().id; - 499
log.append_message(assistant_msg("kept-reply")).unwrap(); - 500
log.append_packet( - 501
&old1, - 502
&old2, - 503
"small-model", - 504
"summary of old turns".into(), - 505
9000, - 506
) - 507
.unwrap(); - 508
let after = log.append_message(user_msg("after")).unwrap().id; - 509
log.append_message(assistant_msg("after-reply")).unwrap(); - 510
- 511
let texts = |messages: Vec<vak_llm::Message>| -> Vec<String> { - 512
messages.iter().map(|m| m.text_content()).collect() - 513
}; - 514
- 515
// The plan that asked for this packet sees it in place of the turns. - 516
let packeting = WorkingSetPlan { - 517
per_turn: vec![ - 518
(kept.clone(), Fidelity::Full), - 519
(after.clone(), Fidelity::Full), - 520
], - 521
packet_range: Some((old1.clone(), old2.clone())), - 522
..WorkingSetPlan::default() - 523
}; - 524
assert_eq!( - 525
texts(log.derive_with_plan(&packeting)), - 526
vec![ - 527
"<context_summary>\nsummary of old turns\n</context_summary>", - 528
"kept", - 529
"kept-reply", - 530
"after", - 531
"after-reply", - 532
] - 533
); - 534
- 535
// A plan that wants every turn at Full gets every turn at Full: the - 536
// packet hides nothing. - 537
let everything = WorkingSetPlan { - 538
per_turn: [&old1, &old2, &kept, &after] - 539
.into_iter() - 540
.map(|id| (id.clone(), Fidelity::Full)) - 541
.collect(), - 542
..WorkingSetPlan::default() - 543
}; - 544
assert_eq!( - 545
texts(log.derive_with_plan(&everything)), - 546
vec![ - 547
"old-1", - 548
"old-1-reply", - 549
"old-2", - 550
"old-2-reply", - 551
"kept", - 552
"kept-reply", - 553
"after", - 554
"after-reply", - 555
] - 556
); - 557
// ...and so does the plan-free projection. - 558
assert_eq!(texts(log.derive_messages()).len(), 8); - 559
- 560
// A plan that packets a different range finds no packet: those turns - 561
// render as cards rather than vanishing (no-cut), and no stale summary - 562
// is substituted. - 563
let narrower = WorkingSetPlan { - 564
per_turn: vec![ - 565
(old2.clone(), Fidelity::Full), - 566
(kept.clone(), Fidelity::Full), - 567
(after.clone(), Fidelity::Full), - 568
], - 569
packet_range: Some((old1.clone(), old1.clone())), - 570
..WorkingSetPlan::default() - 571
}; - 572
let rendered = texts(log.derive_with_plan(&narrower)); - 573
assert!( - 574
!rendered.iter().any(|t| t.contains("<context_summary>")), - 575
"{rendered:?}" - 576
); - 577
assert!( - 578
rendered[0].starts_with("<turns>") && rendered[0].contains("old-1"), - 579
"{rendered:?}" - 580
); - 581
assert!(log.packet_needs_compaction(&old1, &old1)); - 582
assert!(!log.packet_needs_compaction(&old1, &old2)); - 583
} - 584
- 585
#[test] - 586
fn unknown_parent_rejected() { - 587
let dir = tempdir().unwrap(); - 588
let mut log = SessionLog::create(dir.path().join("s.jsonl"), header()).unwrap(); - 589
let orphan = EntryPayload::Message(user_msg("x")); - 590
let mut entry = vak_session::Entry::new(Some("nope".into()), orphan); - 591
entry.parent_id = Some("nope".into()); - 592
assert!(log.append(entry).is_err()); - 593
} - 594
- 595
/// Growing a packet reuses the stored one as its seed (docs/design/68 - 596
/// §4): the summariser input for a wider range starts with the longest - 597
/// stored packet over the same first turn and continues with the cards of - 598
/// the turns after it — raw pre-packet history is never re-read — and the - 599
/// wider packet, once stored, is what the wider plan renders. - 600
#[test] - 601
fn a_wider_packet_seeds_from_the_stored_prefix_packet() { - 602
let dir = tempdir().unwrap(); - 603
let mut log = SessionLog::create(dir.path().join("s.jsonl"), header()).unwrap(); - 604
- 605
let mut ids = Vec::new(); - 606
for i in 0..8 { - 607
ids.push(log.append_message(user_msg(&format!("m{i}"))).unwrap().id); - 608
log.append_message(assistant_msg(&format!("m{i}-reply"))) - 609
.unwrap(); - 610
} - 611
log.append_packet(&ids[0], &ids[5], "m", "summary-one".into(), 9000) - 612
.unwrap(); - 613
- 614
// The plan now wants m0..=m6 packeted: the transcript is seeded from - 615
// summary-one and adds only m6. - 616
let (transcript, _) = log.packet_transcript(&ids[0], &ids[6]); - 617
assert!(transcript.starts_with("summary-one"), "{transcript}"); - 618
assert!(transcript.contains("m6"), "{transcript}"); - 619
assert!( - 620
!transcript.contains("m0-reply"), - 621
"raw history re-read: {transcript}" - 622
); - 623
assert!(log.packet_needs_compaction(&ids[0], &ids[6])); - 624
- 625
log.append_packet(&ids[0], &ids[6], "m", "summary-two".into(), 400) - 626
.unwrap(); - 627
let plan = WorkingSetPlan { - 628
per_turn: vec![(ids[7].clone(), Fidelity::Full)], - 629
packet_range: Some((ids[0].clone(), ids[6].clone())), - 630
..WorkingSetPlan::default() - 631
}; - 632
let final_msgs = log.derive_with_plan(&plan); - 633
assert!( - 634
final_msgs[0].text_content().contains("summary-two"), - 635
"the packet for the asked range wins" - 636
); - 637
assert!( - 638
!serde_like_contains(&final_msgs, "summary-one"), - 639
"the narrower packet is not rendered alongside" - 640
); - 641
// The narrower packet is still there for a plan that asks for it. - 642
assert!(!log.packet_needs_compaction(&ids[0], &ids[5])); - 643
} - 644
- 645
#[test] - 646
fn proactive_retrieval_finds_relevant_older_entries() { - 647
let dir = tempdir().unwrap(); - 648
let mut log = SessionLog::create(dir.path().join("s.jsonl"), header()).unwrap(); - 649
- 650
log.append_message(user_msg("discuss Kubernetes cluster deployment")) - 651
.unwrap(); - 652
log.append_message(MessageRecord { - 653
message: Message::assistant(vec![ContentBlock::text( - 654
"the Kubernetes cluster uses Docker containers", - 655
)]), - 656
meta: None, - 657
}) - 658
.unwrap(); - 659
log.append_message(user_msg("how to configure PostgreSQL database settings")) - 660
.unwrap(); - 661
log.append_message(MessageRecord { - 662
message: Message::assistant(vec![ContentBlock::text( - 663
"PostgreSQL needs shared_buffers tuning", - 664
)]), - 665
meta: None, - 666
}) - 667
.unwrap(); - 668
log.append_message(user_msg("tell me about network firewalls")) - 669
.unwrap(); - 670
- 671
// Retrieve the most relevant entries for a Kubernetes query. - 672
let retrieved = log.retrieve_relevant_entries( - 673
"Kubernetes deployment", - 674
5, - 675
0, // don't exclude any tail for this test - 676
); - 677
assert!( - 678
retrieved.len() >= 2, - 679
"should retrieve at least Kubernetes-related entries" - 680
); - 681
- 682
// The first retrieved entry should be about Kubernetes. - 683
let first_text = retrieved[0].1.text_content(); - 684
assert!( - 685
first_text.contains("Kubernetes") || first_text.contains("kubernetes"), - 686
"first retrieved should be Kubernetes-related, got: {first_text}" - 687
); - 688
} - 689
- 690
fn serde_like_contains(msgs: &[vak_llm::Message], needle: &str) -> bool { - 691
msgs.iter().any(|m| m.text_content().contains(needle)) - 692
} - 693
- 694
#[test] - 695
fn torn_trailing_line_is_skipped_not_fatal() { - 696
let dir = tempdir().unwrap(); - 697
let path = dir.path().join("s.jsonl"); - 698
let mut log = SessionLog::create(path.clone(), header()).unwrap(); - 699
log.append_message(user_msg("safe")).unwrap(); - 700
drop(log); - 701
- 702
// Simulate a crash mid-append: a partial JSON line at EOF. - 703
use std::io::Write as _; - 704
let mut f = std::fs::OpenOptions::new() - 705
.append(true) - 706
.open(&path) - 707
.unwrap(); - 708
write!(f, "{{\"id\":\"torn\", \"pay").unwrap(); - 709
drop(f); - 710
- 711
let reopened = SessionLog::open(path).unwrap(); - 712
assert_eq!(reopened.len(), 2); - 713
assert!(!reopened.warnings().is_empty(), "skip must be surfaced"); - 714
let msgs = reopened.derive_messages(); - 715
assert_eq!(msgs.len(), 1); - 716
assert_eq!(msgs[0].text_content(), "safe"); - 717
} - 718
- 719
#[test] - 720
fn second_handle_on_same_file_is_locked_out() { - 721
let dir = tempdir().unwrap(); - 722
let path = dir.path().join("s.jsonl"); - 723
let log = SessionLog::create(path.clone(), header()).unwrap(); - 724
assert!( - 725
SessionLog::open(path.clone()).is_err(), - 726
"concurrent open must fail while a handle is alive" - 727
); - 728
drop(log); - 729
assert!(SessionLog::open(path).is_ok()); - 730
} - 731
- 732
#[test] - 733
fn open_read_only_succeeds_while_handle_is_locked() { - 734
let dir = tempdir().unwrap(); - 735
let path = dir.path().join("s.jsonl"); - 736
let mut writer = SessionLog::create(path.clone(), header()).unwrap(); - 737
writer - 738
.append_message(user_msg("hello from writer")) - 739
.unwrap(); - 740
- 741
let mut reader = SessionLog::open_read_only(path.clone()).expect("read only open must succeed"); - 742
assert!(reader.is_read_only()); - 743
assert_eq!(reader.derive_messages().len(), 1); - 744
assert!( - 745
reader - 746
.append_message(user_msg("write should fail")) - 747
.is_err(), - 748
"appending to read-only session must fail" - 749
); - 750
- 751
drop(writer); - 752
} - 753
- 754
#[test] - 755
fn create_on_existing_nonempty_file_refuses() { - 756
let dir = tempdir().unwrap(); - 757
let path = dir.path().join("s.jsonl"); - 758
let log = SessionLog::create(path.clone(), header()).unwrap(); - 759
drop(log); - 760
assert!( - 761
SessionLog::create(path, header()).is_err(), - 762
"must not append a second header to an existing ledger" - 763
); - 764
} - 765
- 766
#[test] - 767
fn total_usage_counts_active_chain_only() { - 768
let dir = tempdir().unwrap(); - 769
let mut log = SessionLog::create(dir.path().join("s.jsonl"), header()).unwrap(); - 770
- 771
let usage_msg = |text: &str, out: u64| MessageRecord { - 772
message: Message::user_text(text), - 773
meta: Some(MessageMeta { - 774
usage: Some(Usage { - 775
input_tokens: 100, - 776
output_tokens: out, - 777
..Default::default() - 778
}), - 779
..Default::default() - 780
}), - 781
}; - 782
- 783
let fork = log.append_message(usage_msg("branch-a", 50)).unwrap().id; - 784
log.append_message(usage_msg("branch-a-2", 70)).unwrap(); - 785
- 786
// Abandon that branch: rewind to the first entry and grow elsewhere. - 787
log.branch_at(&fork).unwrap(); - 788
log.append_message(usage_msg("active", 10)).unwrap(); - 789
- 790
// Old code summed every entry (100/130 from the abandoned branch). - 791
let u = log.total_usage(); - 792
assert_eq!(u.input_tokens, 200); - 793
assert_eq!(u.output_tokens, 60); - 794
} - 795
- 796
#[test] - 797
fn turn_capability_binding_roundtrips_without_entering_context() { - 798
let dir = tempdir().unwrap(); - 799
let path = dir.path().join("s.jsonl"); - 800
let mut log = SessionLog::create(path.clone(), header()).unwrap(); - 801
log.append_turn_capabilities(TurnCapabilitiesBound { - 802
epoch: 7, - 803
capability_ids: vec!["Tool:read".into()], - 804
excluded_ids: vec!["Tool:bash".into()], - 805
system_prompt: "system".into(), - 806
tool_schemas: vec![serde_json::json!({"name":"read"})], - 807
core_tool_names: vec!["read".into()], - 808
deferred_tool_names: Vec::new(), - 809
tool_index: String::new(), - 810
tool_domains: Default::default(), - 811
}) - 812
.unwrap(); - 813
assert!(log.derive_messages().is_empty()); - 814
drop(log); - 815
let reopened = SessionLog::open(path).unwrap(); - 816
assert!( - 817
reopened - 818
.chain_to_root() - 819
.iter() - 820
.any(|entry| matches!(entry.payload, EntryPayload::TurnCapabilitiesBound(_))) - 821
); - 822
} - 823
- 824
/// A turn bound to the same interface as the last one records a reference, - 825
/// not the whole system prompt and schemas again; a changed interface is - 826
/// written in full. Prints the bytes a turn's binding costs either way. - 827
#[test] - 828
fn unchanged_capabilities_not_rewritten() { - 829
let dir = tempdir().unwrap(); - 830
let path = dir.path().join("s.jsonl"); - 831
let mut log = SessionLog::create(path.clone(), header()).unwrap(); - 832
let bound = |epoch: u64, prompt: &str| TurnCapabilitiesBound { - 833
epoch, - 834
capability_ids: vec!["Tool:read".into()], - 835
excluded_ids: Vec::new(), - 836
system_prompt: prompt.repeat(400), - 837
tool_schemas: vec![serde_json::json!({"name": "read", "description": "x".repeat(2000)})], - 838
core_tool_names: vec!["read".into()], - 839
deferred_tool_names: Vec::new(), - 840
tool_index: String::new(), - 841
tool_domains: Default::default(), - 842
}; - 843
let size = || std::fs::metadata(&path).unwrap().len(); - 844
- 845
let before = size(); - 846
let first = log.append_turn_capabilities(bound(1, "system ")).unwrap(); - 847
let full = size() - before; - 848
let before = size(); - 849
let second = log.append_turn_capabilities(bound(2, "system ")).unwrap(); - 850
let referenced = size() - before; - 851
println!("capability binding: {full} bytes in full, {referenced} bytes by reference"); - 852
- 853
match &second.payload { - 854
EntryPayload::TurnCapabilitiesRef(reference) => { - 855
assert_eq!(reference.entry, first.id); - 856
assert_eq!(reference.epoch, 2, "the epoch is still recorded"); - 857
} - 858
other => panic!("an unchanged binding was rewritten: {other:?}"), - 859
} - 860
assert!(referenced * 10 < full, "{referenced} vs {full}"); - 861
- 862
let third = log - 863
.append_turn_capabilities(bound(2, "a different system prompt ")) - 864
.unwrap(); - 865
assert!(matches!( - 866
third.payload, - 867
EntryPayload::TurnCapabilitiesBound(_) - 868
)); - 869
assert!(log.derive_messages().is_empty()); - 870
} - 871
- 872
/// The conversation thread is a tail section (docs/design/68-context-engine.md - 873
/// §6/§10), not spliced into the projection, and it lists only directives no - 874
/// longer verbatim among `derive_messages()` — one still present in the - 875
/// working set needs no restating (§6 "one source per fact"). - 876
#[test] - 877
fn conversation_thread_lists_only_directives_dropped_by_compaction() { - 878
let dir = tempdir().unwrap(); - 879
let mut log = SessionLog::create(dir.path().join("thread.jsonl"), header()).unwrap(); - 880
- 881
// Turn 1 - 882
log.append_goal_update(vak_intent::GoalUpdate { - 883
revision: 1, - 884
relation: vak_intent::GoalRelation::New, - 885
request: "initial research on WEF".into(), - 886
supersedes_revision: None, - 887
explicit: false, - 888
}) - 889
.unwrap(); - 890
log.append_message(user_msg("initial research on WEF")) - 891
.unwrap(); - 892
log.append_message(assistant_msg("here are findings")) - 893
.unwrap(); - 894
- 895
// Turn 2 - 896
log.append_goal_update(vak_intent::GoalUpdate { - 897
revision: 2, - 898
relation: vak_intent::GoalRelation::AddsTo, - 899
request: "use python sandbox".into(), - 900
supersedes_revision: None, - 901
explicit: false, - 902
}) - 903
.unwrap(); - 904
log.append_message(user_msg("use python sandbox")).unwrap(); - 905
log.append_message(assistant_msg("running in sandbox")) - 906
.unwrap(); - 907
- 908
// Turn 3: User drifts to GDP task - 909
log.append_goal_update(vak_intent::GoalUpdate { - 910
revision: 3, - 911
relation: vak_intent::GoalRelation::AddsTo, - 912
request: "now evaluate global GDP past 5 years".into(), - 913
supersedes_revision: None, - 914
explicit: false, - 915
}) - 916
.unwrap(); - 917
log.append_message(user_msg("now evaluate global GDP past 5 years")) - 918
.unwrap(); - 919
- 920
// While every directive is still verbatim in the working set, the - 921
// projection carries none of them (they moved to the tail) and the - 922
// tail itself has nothing to add — restating a directive already in - 923
// `derive_messages()` would duplicate a fact already sent. - 924
assert!( - 925
log.derive_messages() - 926
.iter() - 927
.all(|m| !m.text_content().contains("<conversation_thread")), - 928
"the thread must never be spliced into the projection" - 929
); - 930
assert!( - 931
log.tail_sections(None).thread.is_none(), - 932
"nothing is dropped from the working set yet, so the thread has nothing to add" - 933
); - 934
- 935
// Packet turns 1 and 2 away; only turn 3's directive stays verbatim. - 936
let index = vak_session::TurnIndex::from_log(&log); - 937
let (t1, t2) = (index.turns[0].id.clone(), index.turns[1].id.clone()); - 938
log.append_packet(&t1, &t2, "m", "summary of turns 1-2".into(), 999) - 939
.unwrap(); - 940
let plan = WorkingSetPlan { - 941
packet_range: Some((t1, t2)), - 942
..WorkingSetPlan::default() - 943
}; - 944
- 945
let joined = log - 946
.derive_with_plan(&plan) - 947
.iter() - 948
.map(|m| m.text_content()) - 949
.collect::<Vec<_>>() - 950
.join("\n"); - 951
assert!( - 952
!joined.contains("<conversation_thread"), - 953
"the thread must never be spliced into the projection" - 954
); - 955
- 956
let thread_text = log - 957
.tail_sections(Some(&plan)) - 958
.thread - 959
.expect("packeted-away directives surface in the thread"); - 960
assert!(thread_text.contains("revision=\"3\"")); - 961
assert!(thread_text.contains("initial research on WEF")); - 962
assert!(thread_text.contains("use python sandbox")); - 963
// Turn 3's directive is still verbatim in the working set (it was kept, - 964
// not compacted), so restating it in the thread would duplicate it. - 965
assert!(!thread_text.contains("now evaluate global GDP past 5 years")); - 966
assert!(thread_text.contains("Follow the user's intent across conversational drifts")); - 967
assert!(thread_text.contains("Conversational drift across turns is expected: follow along smoothly and adapt immediately.")); - 968
assert!(thread_text.contains("If genuinely confused, ask a brief clarification, but NEVER use asking clarification as an exception-handling escape hatch")); - 969
} - 970
- 971
/// Replaces the old character-count trim: a closed turn's historical tool - 972
/// result is never truncated in the ledger, and never appears as a raw - 973
/// `ToolResult` block in the projection at all — it becomes a trace line - 974
/// naming its evidence id, and the full content is recoverable via - 975
/// `SessionLog::evidence` (docs/design/68-context-engine.md §3, §10). - 976
#[test] - 977
fn historical_tool_result_projects_as_a_trace_line_and_evidence_returns_it_whole() { - 978
let dir = tempdir().unwrap(); - 979
let mut log = SessionLog::create(dir.path().join("prune.jsonl"), header()).unwrap(); - 980
- 981
// Turn 1: closed, with a 5,000-char search result. - 982
log.append_message(user_msg("search the web")).unwrap(); - 983
let call_id = "call-1".to_string(); - 984
log.append_message(MessageRecord { - 985
message: Message { - 986
role: vak_llm::Role::Assistant, - 987
content: vec![vak_llm::ContentBlock::ToolUse { - 988
id: call_id.clone(), - 989
name: "search".into(), - 990
input: serde_json::json!({}), - 991
}], - 992
}, - 993
meta: None, - 994
}) - 995
.unwrap(); - 996
let giant_output = "A".repeat(5_000); - 997
log.append_message(MessageRecord { - 998
message: Message { - 999
role: vak_llm::Role::User, - 1000
content: vec![vak_llm::ContentBlock::tool_result(
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.