- 3001
#[test] - 3002
fn read_tools_do_not_claim_artifacts() { - 3003
assert!(artifact_from_tool("read", &serde_json::json!({ "path": "a" })).is_none()); - 3004
} - 3005
- 3006
#[test] - 3007
fn repeated_writes_keep_one_latest_artifact_per_turn_and_path() { - 3008
let artifact = |id: &str, turn: &str, path: &str| OutputItem { - 3009
id: id.into(), - 3010
timestamp: "2026-09-23T00:00:00Z".into(), - 3011
turn_id: turn.into(), - 3012
role: OutputRole::Tool, - 3013
kind: OutputKind::Artifact, - 3014
status: OutputStatus::Succeeded, - 3015
outcome: None, - 3016
content: OutputContent::Artifact { - 3017
artifact: vak_delivery::ArtifactRef { - 3018
name: path.into(), - 3019
path: Some(path.into()), - 3020
media_type: Some("text/csv".into()), - 3021
description: None, - 3022
size_bytes: None, - 3023
status: None, - 3024
}, - 3025
}, - 3026
provenance: None, - 3027
actions: Vec::new(), - 3028
fallback_text: path.into(), - 3029
}; - 3030
let mut items = vec![ - 3031
artifact("first", "turn-1", "report.csv"), - 3032
artifact("other-dir", "turn-1", "sub/report.csv"), - 3033
artifact("other-turn", "turn-2", "report.csv"), - 3034
artifact("last", "turn-1", "report.csv"), - 3035
]; - 3036
super::deduplicate_file_artifacts(&mut items); - 3037
assert_eq!( - 3038
items - 3039
.iter() - 3040
.map(|item| item.id.as_str()) - 3041
.collect::<Vec<_>>(), - 3042
vec!["other-dir", "other-turn", "last"] - 3043
); - 3044
} - 3045
- 3046
#[test] - 3047
fn presentation_envelope_is_not_an_answer() { - 3048
assert!(super::is_presentation_envelope( - 3049
"Outcome: produced\nSurface: desktop app." - 3050
)); - 3051
assert!(!super::is_presentation_envelope( - 3052
"Outcome: produced\n\nHere is the detailed answer." - 3053
)); - 3054
} - 3055
- 3056
#[test] - 3057
fn snapshot_is_outcome_first_and_deduplicates_activity_transitions() { - 3058
let dir = tempfile::tempdir().expect("temporary directory"); - 3059
let mut log = SessionLog::create( - 3060
dir.path().join("presentation.jsonl"), - 3061
SessionHeader { - 3062
agent: None, - 3063
session_id: "session-1".into(), - 3064
created_at: chrono::Utc::now(), - 3065
cwd: PathBuf::from("/tmp/project"), - 3066
parent_session_id: None, - 3067
contract_id: None, - 3068
work_item_id: None, - 3069
conversation: None, - 3070
contract: FrozenContract { - 3071
app_version: "test".into(), - 3072
provider: "test".into(), - 3073
model: "test".into(), - 3074
route_ladder: Vec::new(), - 3075
route_objective: String::new(), - 3076
route_annotations: Vec::new(), - 3077
system_prompt: String::new(), - 3078
permission_mode: "read-only".into(), - 3079
capabilities: Vec::new(), - 3080
prompt_layers: Vec::new(), - 3081
}, - 3082
}, - 3083
) - 3084
.expect("create session"); - 3085
log.append_message(MessageRecord { - 3086
message: Message::user_text("Build the report"), - 3087
meta: None, - 3088
}) - 3089
.expect("append user"); - 3090
log.append_message(MessageRecord { - 3091
message: Message::assistant(vec![ - 3092
ContentBlock::ToolUse { - 3093
id: "tool-1".into(), - 3094
name: "write".into(), - 3095
input: serde_json::json!({"path": "/tmp/report.md"}), - 3096
}, - 3097
ContentBlock::text( - 3098
"## Done\n\nThe report is ready. See https://example.com/report", - 3099
), - 3100
]), - 3101
meta: None, - 3102
}) - 3103
.expect("append assistant"); - 3104
for status in [ActivityStatus::Pending, ActivityStatus::Succeeded] { - 3105
log.append_activity(ActivityRecord { - 3106
activity_id: "approval-1".into(), - 3107
turn: Some(1), - 3108
kind: ActivityKind::Approval, - 3109
status, - 3110
label: "Approval".into(), - 3111
detail: Some("Write report".into()), - 3112
data: BTreeMap::from([ - 3113
("request_id".into(), "request-1".into()), - 3114
("tool".into(), "write".into()), - 3115
("args_json".into(), "{}".into()), - 3116
]), - 3117
}) - 3118
.expect("append approval state"); - 3119
} - 3120
log.append_activity(ActivityRecord { - 3121
activity_id: "evaluation-1".into(), - 3122
turn: Some(1), - 3123
kind: ActivityKind::Diagnostic, - 3124
status: ActivityStatus::Succeeded, - 3125
label: "Outcome evaluation".into(), - 3126
detail: Some("primary deliverable: produced".into()), - 3127
data: BTreeMap::from([ - 3128
("status".into(), "produced".into()), - 3129
("completion".into(), "unknown".into()), - 3130
("evaluation".into(), "[]".into()), - 3131
("evidence_receipts".into(), String::new()), - 3132
]), - 3133
}) - 3134
.expect("append outcome evaluation"); - 3135
log.append_activity(ActivityRecord { - 3136
activity_id: "review-1".into(), - 3137
turn: Some(1), - 3138
kind: ActivityKind::Diagnostic, - 3139
status: ActivityStatus::Succeeded, - 3140
label: "Outcome review".into(), - 3141
detail: None, - 3142
data: BTreeMap::from([(String::from("verdict"), String::from("accepted"))]), - 3143
}) - 3144
.expect("append outcome review"); - 3145
log.append_activity(ActivityRecord { - 3146
activity_id: "review-2".into(), - 3147
turn: Some(1), - 3148
kind: ActivityKind::Diagnostic, - 3149
status: ActivityStatus::Succeeded, - 3150
label: "Outcome review".into(), - 3151
detail: Some("superseding review".into()), - 3152
data: BTreeMap::from([(String::from("verdict"), String::from("needs_work"))]), - 3153
}) - 3154
.expect("append superseding outcome review"); - 3155
- 3156
let first = snapshot("session-1", &log); - 3157
let second = snapshot("session-1", &log); - 3158
assert_eq!(first, second); - 3159
assert_eq!( - 3160
first - 3161
.items - 3162
.iter() - 3163
.filter(|item| item.id == "approval-1") - 3164
.count(), - 3165
1 - 3166
); - 3167
let approval = first - 3168
.items - 3169
.iter() - 3170
.find(|item| item.id == "approval-1") - 3171
.expect("approval item"); - 3172
assert_eq!(approval.status, OutputStatus::Succeeded); - 3173
assert!( - 3174
first - 3175
.items - 3176
.iter() - 3177
.any(|item| item.kind == OutputKind::Artifact) - 3178
); - 3179
let outcome = first - 3180
.items - 3181
.iter() - 3182
.find(|item| item.kind == OutputKind::Outcome) - 3183
.expect("assistant outcome"); - 3184
let OutputContent::Document { document } = &outcome.content else { - 3185
panic!("assistant outcome must contain its document"); - 3186
}; - 3187
assert_eq!( - 3188
document.source_markdown, - 3189
"## Done\n\nThe report is ready. See https://example.com/report" - 3190
); - 3191
assert_eq!(document.metadata.get("recipe_id").map(String::as_str), None); - 3192
assert_eq!(document.metadata.get("renderer").map(String::as_str), None); - 3193
assert_eq!( - 3194
document - 3195
.metadata - 3196
.get("outcome_review_verdict") - 3197
.map(String::as_str), - 3198
Some("needs_work") - 3199
); - 3200
assert!(first.items.iter().any(|item| matches!( - 3201
item.content, - 3202
OutputContent::Structured { ref output } if output.semantic_type == "link.preview" - 3203
))); - 3204
} - 3205
- 3206
#[test] - 3207
fn recovered_tool_failure_stays_in_activity_without_attention_banner() { - 3208
let dir = tempfile::tempdir().expect("temporary directory"); - 3209
let mut log = SessionLog::create( - 3210
dir.path().join("presentation.jsonl"), - 3211
SessionHeader { - 3212
agent: None, - 3213
session_id: "session-2".into(), - 3214
created_at: chrono::Utc::now(), - 3215
cwd: PathBuf::from("/tmp/project"), - 3216
parent_session_id: None, - 3217
contract_id: None, - 3218
work_item_id: None, - 3219
conversation: None, - 3220
contract: FrozenContract { - 3221
app_version: "test".into(), - 3222
provider: "test".into(), - 3223
model: "test".into(), - 3224
route_ladder: Vec::new(), - 3225
route_objective: String::new(), - 3226
route_annotations: Vec::new(), - 3227
system_prompt: String::new(), - 3228
permission_mode: "read-only".into(), - 3229
capabilities: Vec::new(), - 3230
prompt_layers: Vec::new(), - 3231
}, - 3232
}, - 3233
) - 3234
.expect("create session"); - 3235
log.append_message(MessageRecord { - 3236
message: Message::user_text("Search the web"), - 3237
meta: None, - 3238
}) - 3239
.expect("append user"); - 3240
log.append_message(MessageRecord { - 3241
message: Message::assistant(vec![ContentBlock::ToolUse { - 3242
id: "tool-failed".into(), - 3243
name: "mcp".into(), - 3244
input: serde_json::json!({"tool": "search"}), - 3245
}]), - 3246
meta: None, - 3247
}) - 3248
.expect("append failed call"); - 3249
log.append_message(MessageRecord { - 3250
message: Message { - 3251
role: Role::User, - 3252
content: vec![ContentBlock::ToolResult { - 3253
tool_use_id: "tool-failed".into(), - 3254
content: "Unknown tool: search".into(), - 3255
is_error: true, - 3256
}], - 3257
}, - 3258
meta: None, - 3259
}) - 3260
.expect("append failed result"); - 3261
log.append_message(MessageRecord { - 3262
message: Message::assistant(vec![ContentBlock::text( - 3263
"Search completed after recovery", - 3264
)]), - 3265
meta: None, - 3266
}) - 3267
.expect("append outcome"); - 3268
log.append_activity(ActivityRecord { - 3269
activity_id: "run-2".into(), - 3270
turn: Some(1), - 3271
kind: ActivityKind::Run, - 3272
status: ActivityStatus::Succeeded, - 3273
label: "Run finished".into(), - 3274
detail: Some("completed".into()), - 3275
data: BTreeMap::new(), - 3276
}) - 3277
.expect("append run status"); - 3278
- 3279
let timeline = snapshot("session-2", &log); - 3280
assert!( - 3281
!timeline - 3282
.items - 3283
.iter() - 3284
.any(|item| item.kind == OutputKind::Error) - 3285
); - 3286
let recovered = timeline - 3287
.items - 3288
.iter() - 3289
.find(|item| item.id == "tool-failed") - 3290
.expect("recovered tool item"); - 3291
assert_eq!(recovered.kind, OutputKind::Progress); - 3292
assert_eq!(recovered.status, OutputStatus::Failed); - 3293
assert!(recovered.fallback_text.contains("Unknown tool")); - 3294
let outcome = timeline - 3295
.items - 3296
.iter() - 3297
.find(|item| item.kind == OutputKind::Outcome) - 3298
.expect("assistant outcome"); - 3299
assert_eq!( - 3300
outcome - 3301
.provenance - 3302
.as_ref() - 3303
.and_then(|provenance| provenance.tool_call_id.as_deref()), - 3304
Some("tool-failed") - 3305
); - 3306
} - 3307
- 3308
#[test] - 3309
fn tool_domain_refs_reads_declared_domains_by_tool_name() { - 3310
let mut domains = HashMap::new(); - 3311
domains.insert( - 3312
"tavily_search".to_string(), - 3313
vec!["web".to_string(), "live-data".to_string()], - 3314
); - 3315
assert_eq!( - 3316
super::tool_domain_refs(&domains, Some("tavily_search")), - 3317
vec!["web", "live-data"] - 3318
); - 3319
assert!(super::tool_domain_refs(&domains, Some("bash")).is_empty()); - 3320
assert!(super::tool_domain_refs(&domains, None).is_empty()); - 3321
} - 3322
- 3323
/// `snapshot` must read back a `TurnCapabilitiesBound` entry's declared - 3324
/// `tool_domains` without disturbing the rest of the projection — the - 3325
/// ledger-round-trip half of docs/design/68-context-engine.md §9's - 3326
/// `SignalContext.domains` note. The signal/recipe consequence of a - 3327
/// non-empty `domains` slice is covered directly against - 3328
/// `signals_from_context` in `vak-delivery`. - 3329
#[test] - 3330
fn snapshot_reads_declared_tool_domains_from_the_ledger_without_disrupting_projection() { - 3331
let dir = tempfile::tempdir().expect("temporary directory"); - 3332
let mut log = SessionLog::create( - 3333
dir.path().join("presentation.jsonl"), - 3334
SessionHeader { - 3335
agent: None, - 3336
session_id: "session-domains".into(), - 3337
created_at: chrono::Utc::now(), - 3338
cwd: PathBuf::from("/tmp/project"), - 3339
parent_session_id: None, - 3340
contract_id: None, - 3341
work_item_id: None, - 3342
conversation: None, - 3343
contract: FrozenContract { - 3344
app_version: "test".into(), - 3345
provider: "test".into(), - 3346
model: "test".into(), - 3347
route_ladder: Vec::new(), - 3348
route_objective: String::new(), - 3349
route_annotations: Vec::new(), - 3350
system_prompt: String::new(), - 3351
permission_mode: "read-only".into(), - 3352
capabilities: Vec::new(), - 3353
prompt_layers: Vec::new(), - 3354
}, - 3355
}, - 3356
) - 3357
.expect("create session"); - 3358
log.append_turn_capabilities(vak_session::types::TurnCapabilitiesBound { - 3359
epoch: 1, - 3360
capability_ids: Vec::new(), - 3361
excluded_ids: Vec::new(), - 3362
system_prompt: String::new(), - 3363
tool_schemas: Vec::new(), - 3364
core_tool_names: Vec::new(), - 3365
deferred_tool_names: Vec::new(), - 3366
tool_index: String::new(), - 3367
tool_domains: BTreeMap::from([( - 3368
"some_search_tool".to_string(), - 3369
vec!["web".to_string(), "live-data".to_string()], - 3370
)]), - 3371
}) - 3372
.expect("append turn capabilities"); - 3373
log.append_message(MessageRecord { - 3374
message: Message::user_text("What is happening in the market today?"), - 3375
meta: None, - 3376
}) - 3377
.expect("append user"); - 3378
log.append_message(MessageRecord { - 3379
message: Message::assistant(vec![ContentBlock::ToolUse { - 3380
id: "call-1".into(), - 3381
name: "some_search_tool".into(), - 3382
input: serde_json::json!({"query": "market news"}), - 3383
}]), - 3384
meta: None, - 3385
}) - 3386
.expect("append call"); - 3387
log.append_message(MessageRecord { - 3388
message: Message { - 3389
role: Role::User, - 3390
content: vec![ContentBlock::tool_result("call-1", "found three articles")], - 3391
}, - 3392
meta: None, - 3393
}) - 3394
.expect("append result"); - 3395
log.append_message(MessageRecord { - 3396
message: Message::assistant(vec![ContentBlock::text("Markets are up today.")]), - 3397
meta: None, - 3398
}) - 3399
.expect("append narration"); - 3400
- 3401
// Must not panic, and the ordinary answer must still project. - 3402
let timeline = snapshot("session-domains", &log); - 3403
assert!( - 3404
timeline - 3405
.items - 3406
.iter() - 3407
.any(|item| item.kind == OutputKind::Outcome), - 3408
"the turn's answer must still project with a declared-domains entry in the chain" - 3409
); - 3410
assert!( - 3411
timeline.items.iter().any(|item| { - 3412
item.role == OutputRole::Assistant - 3413
&& item.kind == OutputKind::Outcome - 3414
&& item.outcome.as_ref().is_some_and(|outcome| { - 3415
!outcome.result_id.is_empty() && outcome.status == OutputStatus::Succeeded - 3416
}) - 3417
}), - 3418
"every ordinary assistant result needs a stable address in an unbounded timeline" - 3419
); - 3420
} - 3421
- 3422
#[test] - 3423
fn a_tool_self_declaring_its_own_result_renders_structured_with_no_special_casing() { - 3424
// Any tool — not just ones this file knows by name — gets a rich - 3425
// render for free by tagging its own result with a semantic_type the - 3426
// registry recognizes. This models an arbitrary MCP tool doing that; - 3427
// nothing here mentions "weather" and nothing should have to. - 3428
let dir = tempfile::tempdir().expect("temporary directory"); - 3429
let mut log = SessionLog::create( - 3430
dir.path().join("presentation.jsonl"), - 3431
SessionHeader { - 3432
agent: None, - 3433
session_id: "session-3".into(), - 3434
created_at: chrono::Utc::now(), - 3435
cwd: PathBuf::from("/tmp/project"), - 3436
parent_session_id: None, - 3437
contract_id: None, - 3438
work_item_id: None, - 3439
conversation: None, - 3440
contract: FrozenContract { - 3441
app_version: "test".into(), - 3442
provider: "test".into(), - 3443
model: "test".into(), - 3444
route_ladder: Vec::new(), - 3445
route_objective: String::new(), - 3446
route_annotations: Vec::new(), - 3447
system_prompt: String::new(), - 3448
permission_mode: "read-only".into(), - 3449
capabilities: Vec::new(), - 3450
prompt_layers: Vec::new(), - 3451
}, - 3452
}, - 3453
) - 3454
.expect("create session"); - 3455
log.append_message(MessageRecord { - 3456
message: Message::user_text("What's the weather?"), - 3457
meta: None, - 3458
}) - 3459
.expect("append user"); - 3460
log.append_message(MessageRecord { - 3461
message: Message::assistant(vec![ContentBlock::ToolUse { - 3462
id: "tool-any".into(), - 3463
name: "some_third_party_mcp_tool".into(), - 3464
input: serde_json::json!({}), - 3465
}]), - 3466
meta: None, - 3467
}) - 3468
.expect("append call"); - 3469
log.append_message(MessageRecord { - 3470
message: Message { - 3471
role: Role::User, - 3472
content: vec![ContentBlock::ToolResult { - 3473
tool_use_id: "tool-any".into(), - 3474
content: "```vak\n{\"semantic_type\":\"metric\",\"payload\":{\"label\":\"Temperature\",\"value\":25,\"unit\":\"C\"}}\n```".into(), - 3475
is_error: false, - 3476
}], - 3477
}, - 3478
meta: None, - 3479
}) - 3480
.expect("append result"); - 3481
- 3482
let timeline = snapshot("session-3", &log); - 3483
let structured = timeline - 3484
.items - 3485
.iter() - 3486
.find(|item| matches!(item.content, OutputContent::Structured { .. })) - 3487
.expect("tool result should have produced a structured item"); - 3488
let OutputContent::Structured { output } = &structured.content else { - 3489
unreachable!() - 3490
}; - 3491
assert_eq!(output.semantic_type, "metric"); - 3492
assert_eq!( - 3493
structured - 3494
.provenance - 3495
.as_ref() - 3496
.and_then(|p| p.tool_call_id.as_deref()), - 3497
Some("tool-any") - 3498
); - 3499
} - 3500
- 3501
/// One end-to-end pass across four unrelated personas' tools, each - 3502
/// returning bare JSON with no Markdown fence — the shape a real tool - 3503
/// implementation naturally produces. No persona, tool name, or domain - 3504
/// is known to `snapshot`; each renders solely because its own result - 3505
/// declared a `semantic_type` the registry recognizes. - 3506
#[test] - 3507
fn different_personas_tools_all_render_through_the_same_unnamed_path() { - 3508
let dir = tempfile::tempdir().expect("temporary directory"); - 3509
let mut log = SessionLog::create( - 3510
dir.path().join("presentation.jsonl"), - 3511
SessionHeader { - 3512
agent: None, - 3513
session_id: "session-personas".into(), - 3514
created_at: chrono::Utc::now(), - 3515
cwd: PathBuf::from("/tmp/project"), - 3516
parent_session_id: None, - 3517
contract_id: None, - 3518
work_item_id: None, - 3519
conversation: None, - 3520
contract: FrozenContract { - 3521
app_version: "test".into(), - 3522
provider: "test".into(), - 3523
model: "test".into(), - 3524
route_ladder: Vec::new(), - 3525
route_objective: String::new(), - 3526
route_annotations: Vec::new(), - 3527
system_prompt: String::new(), - 3528
permission_mode: "read-only".into(), - 3529
capabilities: Vec::new(), - 3530
prompt_layers: Vec::new(), - 3531
}, - 3532
}, - 3533
) - 3534
.expect("create session"); - 3535
log.append_message(MessageRecord { - 3536
message: Message::user_text("Kick off a bunch of unrelated tools"), - 3537
meta: None, - 3538
}) - 3539
.expect("append user"); - 3540
- 3541
// (tool name, persona it stands in for, bare-JSON result, expected semantic_type) - 3542
let calls: [(&str, &str, &str, &str); 4] = [ - 3543
( - 3544
"get_local_weather", - 3545
"general user", - 3546
r#"{"semantic_type":"metric","payload":{"label":"Temperature","value":25,"unit":"C"}}"#, - 3547
"metric", - 3548
), - 3549
( - 3550
"run_test_suite", - 3551
"developer", - 3552
r#"{"semantic_type":"test.report","payload":{"tests":[{"name":"it_compiles","status":"passed"}],"total":1,"passed":1,"failed":0,"skipped":0}}"#, - 3553
"test.report", - 3554
), - 3555
( - 3556
"aggregate_research", - 3557
"knowledge worker", - 3558
r#"{"semantic_type":"research.synthesis","payload":{"sources":[{"title":"Report","url":"https://example.com"}],"takeaways":[{"text":"Adoption is rising","citation_indices":[1]}]}}"#, - 3559
"research.synthesis", - 3560
), - 3561
( - 3562
"query_warehouse", - 3563
"data analyst", - 3564
r#"{"semantic_type":"data.grid","payload":{"columns":[{"key":"region","label":"Region"}],"rows":[{"region":"APAC"}]}}"#, - 3565
"data.grid", - 3566
), - 3567
]; - 3568
- 3569
for (index, (tool_name, _persona, result_json, _expected_type)) in calls.iter().enumerate() - 3570
{ - 3571
let call_id = format!("call-{index}"); - 3572
log.append_message(MessageRecord { - 3573
message: Message::assistant(vec![ContentBlock::ToolUse { - 3574
id: call_id.clone(), - 3575
name: (*tool_name).into(), - 3576
input: serde_json::json!({}), - 3577
}]), - 3578
meta: None, - 3579
}) - 3580
.expect("append call"); - 3581
log.append_message(MessageRecord { - 3582
message: Message { - 3583
role: Role::User, - 3584
content: vec![ContentBlock::ToolResult { - 3585
tool_use_id: call_id, - 3586
content: (*result_json).into(), - 3587
is_error: false, - 3588
}], - 3589
}, - 3590
meta: None, - 3591
}) - 3592
.expect("append result"); - 3593
} - 3594
- 3595
let timeline = snapshot("session-personas", &log); - 3596
for (index, (_tool_name, persona, _result_json, expected_type)) in calls.iter().enumerate() - 3597
{ - 3598
let call_id = format!("call-{index}"); - 3599
let found = timeline.items.iter().any(|item| { - 3600
matches!(&item.content, OutputContent::Structured { output } - 3601
if output.semantic_type == *expected_type) - 3602
&& item - 3603
.provenance - 3604
.as_ref() - 3605
.and_then(|p| p.tool_call_id.as_deref()) - 3606
== Some(call_id.as_str()) - 3607
}); - 3608
assert!( - 3609
found, - 3610
"expected a rendered {expected_type} item for the {persona} scenario" - 3611
); - 3612
} - 3613
} - 3614
- 3615
#[test] - 3616
fn completion_status_requires_exact_complete_verdict() { - 3617
assert_eq!(super::status_for_completion(None), OutputStatus::Succeeded); - 3618
assert_eq!( - 3619
super::status_for_completion(Some("complete")), - 3620
OutputStatus::Succeeded - 3621
); - 3622
assert_eq!( - 3623
super::status_for_completion(Some("incomplete")), - 3624
OutputStatus::Partial - 3625
); - 3626
assert_eq!( - 3627
super::status_for_completion(Some("unknown")), - 3628
OutputStatus::Partial - 3629
); - 3630
} - 3631
- 3632
#[test] - 3633
fn clean_scaffolding_strips_inline_hints_and_scaffolding() { - 3634
let text = "Answer body.\n[recovery] retry the failing call"; - 3635
assert_eq!(super::clean_scaffolding(text), "Answer body."); - 3636
- 3637
let text2 = "Surface: desktop app\nDone."; - 3638
assert_eq!(super::clean_scaffolding(text2), "Done."); - 3639
- 3640
let text3 = "<intent>select</intent><context_packet>data</context_packet>Final result."; - 3641
assert_eq!(super::clean_scaffolding(text3), "Final result."); - 3642
} - 3643
- 3644
#[test] - 3645
fn synthetic_stop_messages_do_not_increment_turn_or_project() { - 3646
let dir = tempfile::tempdir().expect("tempdir"); - 3647
let mut log = SessionLog::create( - 3648
dir.path().join("synthetic-turns.jsonl"), - 3649
SessionHeader { - 3650
agent: None, - 3651
session_id: "synthetic-turns".into(), - 3652
created_at: chrono::Utc::now(), - 3653
cwd: PathBuf::from("/tmp/project"), - 3654
parent_session_id: None, - 3655
contract_id: None, - 3656
work_item_id: None, - 3657
conversation: None, - 3658
contract: FrozenContract { - 3659
app_version: "test".into(), - 3660
provider: "test".into(), - 3661
model: "test".into(), - 3662
route_ladder: Vec::new(), - 3663
route_objective: String::new(), - 3664
route_annotations: Vec::new(), - 3665
system_prompt: String::new(), - 3666
permission_mode: "read-only".into(), - 3667
capabilities: Vec::new(), - 3668
prompt_layers: Vec::new(), - 3669
}, - 3670
}, - 3671
) - 3672
.expect("create session"); - 3673
- 3674
// Real user turn 1 - 3675
log.append_message(MessageRecord { - 3676
message: Message::user_text("User query 1"), - 3677
meta: None, - 3678
}) - 3679
.expect("append user message"); - 3680
- 3681
log.append_message(MessageRecord { - 3682
message: Message { - 3683
role: Role::Assistant, - 3684
content: vec![ContentBlock::Text { - 3685
text: "Assistant answer 1".into(), - 3686
}], - 3687
}, - 3688
meta: None, - 3689
}) - 3690
.expect("append assistant message"); - 3691
- 3692
// Synthetic stop-hook nudge (should NOT increment turn count) - 3693
log.append_message(MessageRecord { - 3694
message: Message::user_text("[stop-hook]: hook said continue\nPlease continue."), - 3695
meta: Some(vak_session::MessageMeta { - 3696
control: Some(vak_intent::control::ControlKind::StopHook), - 3697
..Default::default() - 3698
}), - 3699
}) - 3700
.expect("append stop-hook message"); - 3701
- 3702
log.append_message(MessageRecord { - 3703
message: Message { - 3704
role: Role::Assistant, - 3705
content: vec![ContentBlock::Text { - 3706
text: "Assistant answer 2".into(), - 3707
}], - 3708
}, - 3709
meta: None, - 3710
}) - 3711
.expect("append assistant message 2"); - 3712
- 3713
// Synthetic repair directive nudge (should NOT increment turn count) - 3714
log.append_message(MessageRecord { - 3715
message: Message::user_text("The run is stuck on correctable tool failures..."), - 3716
meta: Some(vak_session::MessageMeta { - 3717
control: Some(vak_intent::control::ControlKind::RepairDirective), - 3718
..Default::default() - 3719
}), - 3720
}) - 3721
.expect("append repair directive message"); - 3722
- 3723
log.append_message(MessageRecord { - 3724
message: Message { - 3725
role: Role::Assistant, - 3726
content: vec![ContentBlock::Text { - 3727
text: "Assistant answer 3".into(), - 3728
}], - 3729
}, - 3730
meta: None, - 3731
}) - 3732
.expect("append assistant message 3"); - 3733
- 3734
let timeline = snapshot("synthetic-turns", &log); - 3735
// There should only be 1 user message projected, not 3 - 3736
let user_items: Vec<_> = timeline - 3737
.items - 3738
.iter() - 3739
.filter(|i| i.role == vak_delivery::OutputRole::User) - 3740
.collect(); - 3741
assert_eq!( - 3742
user_items.len(), - 3743
1, - 3744
"synthetic stop/repair messages must not be projected as user item" - 3745
); - 3746
assert_eq!(user_items[0].turn_id, "turn-1"); - 3747
} - 3748
- 3749
/// Regression coverage for a verified real bug: `gemma4:e2b-mlx` - 3750
/// (via Ollama) called `emit_chart_card` successfully, then vak-agent's - 3751
/// fence-check repair asked it to fix a malformed fence in its - 3752
/// following text, and instead of just editing the text it called - 3753
/// `emit_chart_card` AGAIN — leaving two separate `Structured` tool - 3754
/// results for what the user experiences as one card (confirmed via - 3755
/// `vak export`/direct `snapshot()` against the real session). A - 3756
/// `[fence-check]` (or `[duplicate-card-check]`) nudge marks a retry of - 3757
/// the SAME answer, not a new user request, so a second same-type card - 3758
/// after one of these markers must supersede the earlier one rather - 3759
/// than both surviving into the timeline. - 3760
#[test] - 3761
fn a_retried_tool_card_after_a_repair_nudge_supersedes_the_earlier_one() { - 3762
let dir = tempfile::tempdir().expect("tempdir"); - 3763
let mut log = SessionLog::create( - 3764
dir.path().join("duplicate-card.jsonl"), - 3765
SessionHeader { - 3766
agent: None, - 3767
session_id: "duplicate-card".into(), - 3768
created_at: chrono::Utc::now(), - 3769
cwd: PathBuf::from("/tmp/project"), - 3770
parent_session_id: None, - 3771
contract_id: None, - 3772
work_item_id: None, - 3773
conversation: None, - 3774
contract: FrozenContract { - 3775
app_version: "test".into(), - 3776
provider: "test".into(), - 3777
model: "test".into(), - 3778
route_ladder: Vec::new(), - 3779
route_objective: String::new(), - 3780
route_annotations: Vec::new(), - 3781
system_prompt: String::new(), - 3782
permission_mode: "read-only".into(), - 3783
capabilities: Vec::new(), - 3784
prompt_layers: Vec::new(), - 3785
}, - 3786
}, - 3787
) - 3788
.expect("create session"); - 3789
- 3790
log.append_message(MessageRecord { - 3791
message: Message::user_text("show me a chart"), - 3792
meta: None, - 3793
}) - 3794
.expect("append user message"); - 3795
- 3796
let chart_input = |summary: &str| serde_json::json!({"semantic_type":"chart","payload":{"chart_type":"line","series":[],"accessible_summary":summary}}); - 3797
let ack = || "Card displayed to the user (chart).".to_string(); - 3798
- 3799
log.append_message(MessageRecord { - 3800
message: Message::assistant(vec![ContentBlock::ToolUse { - 3801
id: "call-1".into(), - 3802
name: "emit_chart_card".into(), - 3803
input: chart_input("first attempt"), - 3804
}]), - 3805
meta: None, - 3806
}) - 3807
.expect("append call 1"); - 3808
log.append_message(MessageRecord { - 3809
message: Message { - 3810
role: Role::User, - 3811
content: vec![ContentBlock::ToolResult { - 3812
tool_use_id: "call-1".into(), - 3813
content: ack(), - 3814
is_error: false, - 3815
}], - 3816
}, - 3817
meta: None, - 3818
}) - 3819
.expect("append result 1"); - 3820
append_presentation_for_call( - 3821
&mut log, - 3822
"emit_chart_card", - 3823
"call-1", - 3824
&chart_input("first attempt"), - 3825
); - 3826
log.append_message(MessageRecord { - 3827
message: Message { - 3828
role: Role::Assistant, - 3829
content: vec![ContentBlock::Text { - 3830
text: "```vak\n{broken json\n```".into(), - 3831
}], - 3832
}, - 3833
meta: None, - 3834
}) - 3835
.expect("append malformed fence"); - 3836
- 3837
// The repair nudge: same logical answer, not a new user request. - 3838
log.append_message(MessageRecord { - 3839
message: Message::user_text( - 3840
"[fence-check]: The vak-fence in your last answer has invalid JSON and failed to parse. Resend it.", - 3841
), - 3842
meta: Some(vak_session::MessageMeta { - 3843
control: Some(vak_intent::control::ControlKind::FenceCheck), - 3844
..Default::default() - 3845
}), - 3846
}) - 3847
.expect("append fence-check nudge"); - 3848
- 3849
log.append_message(MessageRecord { - 3850
message: Message::assistant(vec![ContentBlock::ToolUse { - 3851
id: "call-2".into(), - 3852
name: "emit_chart_card".into(), - 3853
input: chart_input("retried attempt"), - 3854
}]), - 3855
meta: None, - 3856
}) - 3857
.expect("append call 2"); - 3858
log.append_message(MessageRecord { - 3859
message: Message { - 3860
role: Role::User, - 3861
content: vec![ContentBlock::ToolResult { - 3862
tool_use_id: "call-2".into(), - 3863
content: ack(), - 3864
is_error: false, - 3865
}], - 3866
}, - 3867
meta: None, - 3868
}) - 3869
.expect("append result 2"); - 3870
append_presentation_for_call( - 3871
&mut log, - 3872
"emit_chart_card", - 3873
"call-2", - 3874
&chart_input("retried attempt"), - 3875
); - 3876
log.append_message(MessageRecord { - 3877
message: Message { - 3878
role: Role::Assistant, - 3879
content: vec![ContentBlock::Text { - 3880
text: "Chart is displayed above.".into(), - 3881
}], - 3882
}, - 3883
meta: None, - 3884
}) - 3885
.expect("append final prose"); - 3886
- 3887
let timeline = snapshot("duplicate-card", &log); - 3888
let chart_items: Vec<_> = timeline - 3889
.items - 3890
.iter() - 3891
.filter(|item| { - 3892
matches!(&item.content, OutputContent::Structured { output } if output.semantic_type == "chart") - 3893
}) - 3894
.collect(); - 3895
assert_eq!( - 3896
chart_items.len(), - 3897
1, - 3898
"the earlier attempt's card must be superseded, not left duplicated: {chart_items:?}" - 3899
); - 3900
assert_eq!( - 3901
chart_items[0] - 3902
.provenance - 3903
.as_ref() - 3904
.and_then(|p| p.tool_call_id.as_deref()), - 3905
Some("call-2"), - 3906
"the SURVIVING card must be the retried (latest) attempt, not the first" - 3907
); - 3908
} - 3909
- 3910
/// A second same-type card with NO repair nudge in between is a - 3911
/// legitimate distinct card (e.g. "chart A, then chart B") and must - 3912
/// NOT be collapsed. - 3913
#[test] - 3914
fn two_same_type_cards_with_no_repair_nudge_both_survive() { - 3915
let dir = tempfile::tempdir().expect("tempdir"); - 3916
let mut log = SessionLog::create( - 3917
dir.path().join("two-charts.jsonl"), - 3918
SessionHeader { - 3919
agent: None, - 3920
session_id: "two-charts".into(), - 3921
created_at: chrono::Utc::now(), - 3922
cwd: PathBuf::from("/tmp/project"), - 3923
parent_session_id: None, - 3924
contract_id: None, - 3925
work_item_id: None, - 3926
conversation: None, - 3927
contract: FrozenContract { - 3928
app_version: "test".into(), - 3929
provider: "test".into(), - 3930
model: "test".into(), - 3931
route_ladder: Vec::new(), - 3932
route_objective: String::new(), - 3933
route_annotations: Vec::new(), - 3934
system_prompt: String::new(), - 3935
permission_mode: "read-only".into(), - 3936
capabilities: Vec::new(), - 3937
prompt_layers: Vec::new(), - 3938
}, - 3939
}, - 3940
) - 3941
.expect("create session"); - 3942
- 3943
log.append_message(MessageRecord { - 3944
message: Message::user_text("show me revenue and cost charts"), - 3945
meta: None, - 3946
}) - 3947
.expect("append user message"); - 3948
log.append_message(MessageRecord { - 3949
message: Message::assistant(vec![ContentBlock::ToolUse { - 3950
id: "call-1".into(), - 3951
name: "emit_chart_card".into(), - 3952
input: serde_json::json!({"semantic_type":"chart","payload":{"chart_type":"line","series":[],"accessible_summary":"revenue"}}), - 3953
}]), - 3954
meta: None, - 3955
}) - 3956
.expect("append call 1"); - 3957
log.append_message(MessageRecord { - 3958
message: Message { - 3959
role: Role::User, - 3960
content: vec![ContentBlock::ToolResult { - 3961
tool_use_id: "call-1".into(), - 3962
content: "Card displayed to the user (chart).".into(), - 3963
is_error: false, - 3964
}], - 3965
}, - 3966
meta: None, - 3967
}) - 3968
.expect("append result 1"); - 3969
append_presentation_for_call( - 3970
&mut log, - 3971
"emit_chart_card", - 3972
"call-1", - 3973
&serde_json::json!({"semantic_type":"chart","payload":{"chart_type":"line","series":[],"accessible_summary":"revenue"}}), - 3974
); - 3975
log.append_message(MessageRecord { - 3976
message: Message::assistant(vec![ContentBlock::ToolUse { - 3977
id: "call-2".into(), - 3978
name: "emit_chart_card".into(), - 3979
input: serde_json::json!({"semantic_type":"chart","payload":{"chart_type":"line","series":[],"accessible_summary":"cost"}}), - 3980
}]), - 3981
meta: None, - 3982
}) - 3983
.expect("append call 2"); - 3984
log.append_message(MessageRecord { - 3985
message: Message { - 3986
role: Role::User, - 3987
content: vec![ContentBlock::ToolResult { - 3988
tool_use_id: "call-2".into(), - 3989
content: "Card displayed to the user (chart).".into(), - 3990
is_error: false, - 3991
}], - 3992
}, - 3993
meta: None, - 3994
}) - 3995
.expect("append result 2"); - 3996
append_presentation_for_call( - 3997
&mut log, - 3998
"emit_chart_card", - 3999
"call-2", - 4000
&serde_json::json!({"semantic_type":"chart","payload":{"chart_type":"line","series":[],"accessible_summary":"cost"}}),
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.