- 4001
); - 4002
- 4003
let timeline = snapshot("two-charts", &log); - 4004
let chart_items: Vec<_> = timeline - 4005
.items - 4006
.iter() - 4007
.filter(|item| { - 4008
matches!(&item.content, OutputContent::Structured { output } if output.semantic_type == "chart") - 4009
}) - 4010
.collect(); - 4011
assert_eq!( - 4012
chart_items.len(), - 4013
2, - 4014
"two intentional cards of the same type with no repair nudge between them must both survive: {chart_items:?}" - 4015
); - 4016
} - 4017
- 4018
/// The live bug: a research card over the tool framework's ~2000-char - 4019
/// line limit had its recorded RESULT truncated mid-JSON, so no card was - 4020
/// produced and the user saw prose. The card now comes from the call's - 4021
/// own (untruncated) arguments; the result is only an ack. - 4022
#[test] - 4023
fn a_card_larger_than_the_result_line_limit_still_renders_from_the_call() { - 4024
let dir = tempfile::tempdir().expect("tempdir"); - 4025
let mut log = SessionLog::create( - 4026
dir.path().join("big-card.jsonl"), - 4027
SessionHeader { - 4028
agent: None, - 4029
session_id: "big-card".into(), - 4030
created_at: chrono::Utc::now(), - 4031
cwd: PathBuf::from("/tmp/project"), - 4032
parent_session_id: None, - 4033
contract_id: None, - 4034
work_item_id: None, - 4035
conversation: None, - 4036
contract: FrozenContract { - 4037
app_version: "test".into(), - 4038
provider: "test".into(), - 4039
model: "test".into(), - 4040
route_ladder: Vec::new(), - 4041
route_objective: String::new(), - 4042
route_annotations: Vec::new(), - 4043
system_prompt: String::new(), - 4044
permission_mode: "read-only".into(), - 4045
capabilities: Vec::new(), - 4046
prompt_layers: Vec::new(), - 4047
}, - 4048
}, - 4049
) - 4050
.expect("create session"); - 4051
log.append_message(MessageRecord { - 4052
message: Message::user_text("summarize last week's market"), - 4053
meta: None, - 4054
}) - 4055
.expect("user"); - 4056
let takeaways: Vec<_> = (0..12) - 4057
.map(|i| serde_json::json!({"text": format!("Takeaway {i}: {}", "detail ".repeat(40)), "citation_indices": [1]})) - 4058
.collect(); - 4059
let input = serde_json::json!({ - 4060
"semantic_type": "research.synthesis", - 4061
"payload": {"sources": [{"title": "Reuters", "url": "https://example.com/a"}], "takeaways": takeaways} - 4062
}); - 4063
assert!( - 4064
input.to_string().len() > 3000, - 4065
"fixture must exceed the limit" - 4066
); - 4067
log.append_message(MessageRecord { - 4068
message: Message::assistant(vec![ContentBlock::ToolUse { - 4069
id: "call-r".into(), - 4070
name: "emit_research_card".into(), - 4071
input: input.clone(), - 4072
}]), - 4073
meta: None, - 4074
}) - 4075
.expect("call"); - 4076
log.append_message(MessageRecord { - 4077
message: Message { - 4078
role: Role::User, - 4079
content: vec![ContentBlock::ToolResult { - 4080
tool_use_id: "call-r".into(), - 4081
content: "Card displayed to the user (research.synthesis).".into(), - 4082
is_error: false, - 4083
}], - 4084
}, - 4085
meta: None, - 4086
}) - 4087
.expect("result"); - 4088
append_presentation_for_call(&mut log, "emit_research_card", "call-r", &input); - 4089
let timeline = snapshot("big-card", &log); - 4090
assert!( - 4091
timeline.items.iter().any(|item| matches!(&item.content, - 4092
OutputContent::Structured { output } if output.semantic_type == "research.synthesis")), - 4093
"the research card must render from the call arguments" - 4094
); - 4095
} - 4096
- 4097
/// A card's `provenance.presentation_id` is the `Presentation` ledger - 4098
/// entry's OWN id (docs/design/68-context-engine.md §10), not the id of - 4099
/// the message entry the `emit_*_card` tool call rode in on — those two - 4100
/// entries are different chain entries, and the client's - 4101
/// `/presentation/feedback` and `/presentation/select` calls key on the - 4102
/// former. - 4103
#[test] - 4104
fn card_provenance_carries_the_presentations_own_entry_id() { - 4105
let dir = tempfile::tempdir().expect("tempdir"); - 4106
let mut log = SessionLog::create( - 4107
dir.path().join("card-id.jsonl"), - 4108
SessionHeader { - 4109
agent: None, - 4110
session_id: "card-id".into(), - 4111
created_at: chrono::Utc::now(), - 4112
cwd: PathBuf::from("/tmp/project"), - 4113
parent_session_id: None, - 4114
contract_id: None, - 4115
work_item_id: None, - 4116
conversation: None, - 4117
contract: FrozenContract { - 4118
app_version: "test".into(), - 4119
provider: "test".into(), - 4120
model: "test".into(), - 4121
route_ladder: Vec::new(), - 4122
route_objective: String::new(), - 4123
route_annotations: Vec::new(), - 4124
system_prompt: String::new(), - 4125
permission_mode: "read-only".into(), - 4126
capabilities: Vec::new(), - 4127
prompt_layers: Vec::new(), - 4128
}, - 4129
}, - 4130
) - 4131
.expect("create session"); - 4132
log.append_message(MessageRecord { - 4133
message: Message::user_text("what's the temperature"), - 4134
meta: None, - 4135
}) - 4136
.expect("user"); - 4137
let input = serde_json::json!({ - 4138
"semantic_type": "metric", - 4139
"payload": {"label": "Temperature", "value": 25, "unit": "C"} - 4140
}); - 4141
let call_entry = log - 4142
.append_message(MessageRecord { - 4143
message: Message::assistant(vec![ContentBlock::ToolUse { - 4144
id: "call-m".into(), - 4145
name: "emit_metric_card".into(), - 4146
input: input.clone(), - 4147
}]), - 4148
meta: None, - 4149
}) - 4150
.expect("call"); - 4151
log.append_message(MessageRecord { - 4152
message: Message { - 4153
role: Role::User, - 4154
content: vec![ContentBlock::ToolResult { - 4155
tool_use_id: "call-m".into(), - 4156
content: "Card displayed to the user (metric).".into(), - 4157
is_error: false, - 4158
}], - 4159
}, - 4160
meta: None, - 4161
}) - 4162
.expect("result"); - 4163
append_presentation_for_call(&mut log, "emit_metric_card", "call-m", &input); - 4164
let (presentation_entry_id, _) = log - 4165
.presentations() - 4166
.into_iter() - 4167
.next() - 4168
.expect("one presentation entry was written"); - 4169
assert_ne!( - 4170
presentation_entry_id, call_entry.id, - 4171
"the Presentation entry must be its own chain entry, distinct \ - 4172
from the message carrying the tool_use block" - 4173
); - 4174
- 4175
let timeline = snapshot("card-id", &log); - 4176
let card = timeline - 4177
.items - 4178
.iter() - 4179
.find(|item| item.kind == OutputKind::Card) - 4180
.expect("a metric card must be projected"); - 4181
assert_eq!( - 4182
card.provenance - 4183
.as_ref() - 4184
.and_then(|p| p.presentation_id.clone()), - 4185
Some(presentation_entry_id), - 4186
"the card's presentation_id must be the Presentation entry's \ - 4187
own id, not the tool_use message's entry id" - 4188
); - 4189
} - 4190
- 4191
#[test] - 4192
fn a_runtime_nudge_is_never_projected_as_a_user_message_whatever_its_text() { - 4193
use vak_intent::control::ControlKind; - 4194
let dir = tempfile::tempdir().expect("tempdir"); - 4195
let mut log = SessionLog::create( - 4196
dir.path().join("nudge.jsonl"), - 4197
SessionHeader { - 4198
agent: None, - 4199
session_id: "nudge".into(), - 4200
created_at: chrono::Utc::now(), - 4201
cwd: PathBuf::from("/tmp/project"), - 4202
parent_session_id: None, - 4203
contract_id: None, - 4204
work_item_id: None, - 4205
conversation: None, - 4206
contract: FrozenContract { - 4207
app_version: "test".into(), - 4208
provider: "test".into(), - 4209
model: "test".into(), - 4210
route_ladder: Vec::new(), - 4211
route_objective: String::new(), - 4212
route_annotations: Vec::new(), - 4213
system_prompt: String::new(), - 4214
permission_mode: "read-only".into(), - 4215
capabilities: Vec::new(), - 4216
prompt_layers: Vec::new(), - 4217
}, - 4218
}, - 4219
) - 4220
.expect("create"); - 4221
log.append_message(MessageRecord { - 4222
message: Message::user_text("what is the weather"), - 4223
meta: None, - 4224
}) - 4225
.expect("user"); - 4226
for kind in ControlKind::ALL { - 4227
// No marker in the text at all: only the tag says it is runtime-authored. - 4228
log.append_message(MessageRecord::control(kind, "please redo that answer")) - 4229
.expect("nudge"); - 4230
} - 4231
// Text that merely LOOKS like a marker, from the user, stays the user's. - 4232
log.append_message(MessageRecord { - 4233
message: Message::user_text("[fence-check]: my own note"), - 4234
meta: None, - 4235
}) - 4236
.expect("look-alike"); - 4237
let timeline = snapshot("nudge", &log); - 4238
let users: Vec<_> = timeline - 4239
.items - 4240
.iter() - 4241
.filter(|i| i.role == vak_delivery::OutputRole::User) - 4242
.collect(); - 4243
assert_eq!( - 4244
users.len(), - 4245
2, - 4246
"the real question and the user's look-alike, no nudges: {:?}", - 4247
users.iter().map(|i| &i.fallback_text).collect::<Vec<_>>() - 4248
); - 4249
let turns: std::collections::BTreeSet<_> = - 4250
users.iter().map(|i| i.turn_id.clone()).collect(); - 4251
assert_eq!( - 4252
turns.len(), - 4253
2, - 4254
"nudges must not start turns; the look-alike does" - 4255
); - 4256
} - 4257
- 4258
/// The projection's user items carry the ledger entry id, which is what - 4259
/// the client pairs a chat turn with instead of counting turns. - 4260
#[test] - 4261
fn a_user_item_carries_the_ledger_entry_id_of_its_message() { - 4262
let dir = tempfile::tempdir().expect("tempdir"); - 4263
let mut log = SessionLog::create( - 4264
dir.path().join("ident.jsonl"), - 4265
SessionHeader { - 4266
agent: None, - 4267
session_id: "ident".into(), - 4268
created_at: chrono::Utc::now(), - 4269
cwd: PathBuf::from("/tmp/project"), - 4270
parent_session_id: None, - 4271
contract_id: None, - 4272
work_item_id: None, - 4273
conversation: None, - 4274
contract: FrozenContract { - 4275
app_version: "test".into(), - 4276
provider: "test".into(), - 4277
model: "test".into(), - 4278
route_ladder: Vec::new(), - 4279
route_objective: String::new(), - 4280
route_annotations: Vec::new(), - 4281
system_prompt: String::new(), - 4282
permission_mode: "read-only".into(), - 4283
capabilities: Vec::new(), - 4284
prompt_layers: Vec::new(), - 4285
}, - 4286
}, - 4287
) - 4288
.expect("create"); - 4289
log.append_message(MessageRecord { - 4290
message: Message::user_text("first"), - 4291
meta: None, - 4292
}) - 4293
.expect("u1"); - 4294
log.append_message(MessageRecord::control( - 4295
vak_intent::control::ControlKind::GroundingCheck, - 4296
"redo", - 4297
)) - 4298
.expect("n"); - 4299
log.append_message(MessageRecord { - 4300
message: Message::user_text("second"), - 4301
meta: None, - 4302
}) - 4303
.expect("u2"); - 4304
let transcript = log.derive_transcript(); - 4305
let real: Vec<_> = transcript.iter().filter(|t| t.control.is_none()).collect(); - 4306
let timeline = snapshot("ident", &log); - 4307
for item in &real { - 4308
let text = item.message.text_content(); - 4309
let user_item = timeline - 4310
.items - 4311
.iter() - 4312
.find(|i| i.role == vak_delivery::OutputRole::User && i.fallback_text == text) - 4313
.expect("projected"); - 4314
assert_eq!( - 4315
user_item - 4316
.provenance - 4317
.as_ref() - 4318
.and_then(|p| p.entry_id.as_deref()), - 4319
Some(item.entry_id.as_str()), - 4320
"pairing key must be the transcript's entry id for {text:?}" - 4321
); - 4322
} - 4323
} - 4324
- 4325
/// Not per-type and not per-size: every type the emit tools support, at a - 4326
/// normal size and padded far past the tool framework's result line - 4327
/// limit, must come out of the real `snapshot()` as exactly one card of - 4328
/// that type — from the call arguments alone, with only an ack as result. - 4329
#[test] - 4330
fn every_supported_type_renders_through_the_full_path_at_any_size() { - 4331
let cases = vak_core::presentation_tools::conformance_cases(); - 4332
assert!( - 4333
cases.len() >= 97, - 4334
"expected every registered type, got {}", - 4335
cases.len() - 4336
); - 4337
let mut failures = Vec::new(); - 4338
for (tool, semantic_type, payload) in cases { - 4339
for padded in [false, true] { - 4340
let mut payload = payload.clone(); - 4341
if padded { - 4342
payload["x_padding"] = serde_json::Value::String("p".repeat(6000)); - 4343
} - 4344
let dir = tempfile::tempdir().expect("tempdir"); - 4345
let mut log = SessionLog::create( - 4346
dir.path().join("t.jsonl"), - 4347
SessionHeader { - 4348
agent: None, - 4349
session_id: "conformance".into(), - 4350
created_at: chrono::Utc::now(), - 4351
cwd: PathBuf::from("/tmp/project"), - 4352
parent_session_id: None, - 4353
contract_id: None, - 4354
work_item_id: None, - 4355
conversation: None, - 4356
contract: FrozenContract { - 4357
app_version: "test".into(), - 4358
provider: "test".into(), - 4359
model: "test".into(), - 4360
route_ladder: Vec::new(), - 4361
route_objective: String::new(), - 4362
route_annotations: Vec::new(), - 4363
system_prompt: String::new(), - 4364
permission_mode: "read-only".into(), - 4365
capabilities: Vec::new(), - 4366
prompt_layers: Vec::new(), - 4367
}, - 4368
}, - 4369
) - 4370
.expect("create"); - 4371
log.append_message(MessageRecord { - 4372
message: Message::user_text("go"), - 4373
meta: None, - 4374
}) - 4375
.expect("user"); - 4376
let call_args = - 4377
serde_json::json!({"semantic_type": semantic_type, "payload": payload}); - 4378
log.append_message(MessageRecord { - 4379
message: Message::assistant(vec![ContentBlock::ToolUse { - 4380
id: "c1".into(), - 4381
name: tool.into(), - 4382
input: call_args.clone(), - 4383
}]), - 4384
meta: None, - 4385
}) - 4386
.expect("call"); - 4387
log.append_message(MessageRecord { - 4388
message: Message { - 4389
role: Role::User, - 4390
content: vec![ContentBlock::ToolResult { - 4391
tool_use_id: "c1".into(), - 4392
content: "Card displayed to the user.".into(), - 4393
is_error: false, - 4394
}], - 4395
}, - 4396
meta: None, - 4397
}) - 4398
.expect("result"); - 4399
append_presentation_for_call(&mut log, tool, "c1", &call_args); - 4400
let timeline = snapshot("conformance", &log); - 4401
let found: Vec<_> = timeline - 4402
.items - 4403
.iter() - 4404
.filter_map(|i| match &i.content { - 4405
OutputContent::Structured { output } => Some(output.semantic_type.clone()), - 4406
_ => None, - 4407
}) - 4408
.collect(); - 4409
// A card is answer content, never activity chatter: chat views - 4410
// fold away progress/retry/information items, so a card - 4411
// carrying one of those kinds is invisible to the user. - 4412
if let Some(bad) = timeline.items.iter().find(|i| { - 4413
matches!(i.content, OutputContent::Structured { .. }) - 4414
&& i.kind != OutputKind::Card - 4415
}) { - 4416
failures.push(format!( - 4417
"{semantic_type} via {tool}: card projected as {:?}, not Card", - 4418
bad.kind - 4419
)); - 4420
} - 4421
if found != [semantic_type.to_string()] { - 4422
failures.push(format!( - 4423
"{semantic_type} via {tool} padded={padded}: got {found:?}" - 4424
)); - 4425
} - 4426
} - 4427
} - 4428
assert!( - 4429
failures.is_empty(), - 4430
"{} failures:\n{}", - 4431
failures.len(), - 4432
failures.join("\n") - 4433
); - 4434
} - 4435
- 4436
#[test] - 4437
fn every_built_in_pack_compiles_its_real_emitter_shape() { - 4438
let cases = vak_core::presentation_tools::conformance_cases(); - 4439
let mut failures = Vec::new(); - 4440
for seed in vak_presentation::seeds::built_in_seed_pack() { - 4441
let Some((_, semantic_type, payload)) = cases - 4442
.iter() - 4443
.find(|(_, kind, _)| seed.spec.accepts.iter().any(|accepted| accepted == kind)) - 4444
else { - 4445
failures.push(format!("{} has no emitted semantic type", seed.spec.id)); - 4446
continue; - 4447
}; - 4448
let compiled = vak_presentation::compile( - 4449
&seed.spec, - 4450
&vak_presentation::CompileInput { - 4451
semantic_type: (*semantic_type).into(), - 4452
payload: payload.clone(), - 4453
fallback_text: "Fallback".into(), - 4454
}, - 4455
); - 4456
if !matches!(compiled, vak_presentation::CompiledPresentation::Rich(_)) { - 4457
failures.push(format!("{} ({semantic_type}): {compiled:?}", seed.spec.id)); - 4458
} else if vak_delivery::adaptive_presentation_markdown(&compiled) - 4459
.trim() - 4460
.is_empty() - 4461
{ - 4462
failures.push(format!( - 4463
"{} ({semantic_type}): empty text delivery", - 4464
seed.spec.id - 4465
)); - 4466
} - 4467
} - 4468
assert!( - 4469
failures.is_empty(), - 4470
"{} unusable packs:\n{}", - 4471
failures.len(), - 4472
failures.join("\n") - 4473
); - 4474
} - 4475
- 4476
#[test] - 4477
fn selected_metric_pack_keeps_every_reading_from_an_emitted_grid() { - 4478
let dir = tempfile::tempdir().expect("tempdir"); - 4479
let mut log = channel_log(&dir, "metric-grid"); - 4480
log.append_message(MessageRecord { - 4481
message: Message::user_text("How is the weather?"), - 4482
meta: None, - 4483
}) - 4484
.expect("user"); - 4485
let input = serde_json::json!({ - 4486
"semantic_type": "metric", - 4487
"payload": {"label": "Noida now", "condition": "Sunny", "temperature": "35.2°C", "humidity": "31%"} - 4488
}); - 4489
log.append_message(MessageRecord { - 4490
message: Message::assistant(vec![ContentBlock::ToolUse { - 4491
id: "grid-call".into(), - 4492
name: "emit_metric_card".into(), - 4493
input: input.clone(), - 4494
}]), - 4495
meta: None, - 4496
}) - 4497
.expect("call"); - 4498
log.append_message(MessageRecord { - 4499
message: Message { - 4500
role: Role::User, - 4501
content: vec![ContentBlock::ToolResult { - 4502
tool_use_id: "grid-call".into(), - 4503
content: "Card displayed to the user (metric).".into(), - 4504
is_error: false, - 4505
}], - 4506
}, - 4507
meta: None, - 4508
}) - 4509
.expect("result"); - 4510
append_presentation_for_call(&mut log, "emit_metric_card", "grid-call", &input); - 4511
let library = crate::effective_presentation_library( - 4512
&vak_presentation::PresentationLibrary::default(), - 4513
"/tmp/project", - 4514
); - 4515
let planner = vak_delivery::PresentationPlanner { - 4516
skills: vak_delivery::built_in_skill_registry(), - 4517
recipes: vak_delivery::built_in_recipes(), - 4518
}; - 4519
let timeline = - 4520
super::snapshot_with_planner_and_library("metric-grid", &log, &planner, &library); - 4521
let card = timeline - 4522
.items - 4523
.iter() - 4524
.find(|item| item.kind == OutputKind::Card) - 4525
.expect("card"); - 4526
let OutputContent::Adaptive { tree, .. } = &card.content else { - 4527
panic!("selected pack should produce adaptive content"); - 4528
}; - 4529
assert_eq!(tree.spec_id, "seed.metric"); - 4530
assert_eq!( - 4531
tree.root.props.get("condition"), - 4532
Some(&serde_json::json!("Sunny")) - 4533
); - 4534
assert_eq!( - 4535
tree.root.props.get("temperature"), - 4536
Some(&serde_json::json!("35.2°C")) - 4537
); - 4538
assert_eq!( - 4539
tree.root.props.get("humidity"), - 4540
Some(&serde_json::json!("31%")) - 4541
); - 4542
let lowered = vak_delivery::adaptive_presentation_markdown( - 4543
&vak_presentation::CompiledPresentation::Rich(tree.clone()), - 4544
); - 4545
for reading in ["Sunny", "35.2°C", "31%"] { - 4546
assert!( - 4547
lowered.contains(reading), - 4548
"constrained delivery lost {reading}: {lowered}" - 4549
); - 4550
} - 4551
} - 4552
- 4553
fn channel_log(dir: &tempfile::TempDir, name: &str) -> SessionLog { - 4554
SessionLog::create( - 4555
dir.path().join(format!("{name}.jsonl")), - 4556
SessionHeader { - 4557
agent: None, - 4558
session_id: name.into(), - 4559
created_at: chrono::Utc::now(), - 4560
cwd: PathBuf::from("/tmp/project"), - 4561
parent_session_id: None, - 4562
contract_id: None, - 4563
work_item_id: None, - 4564
conversation: None, - 4565
contract: FrozenContract { - 4566
app_version: "test".into(), - 4567
provider: "test".into(), - 4568
model: "test".into(), - 4569
route_ladder: Vec::new(), - 4570
route_objective: String::new(), - 4571
route_annotations: Vec::new(), - 4572
system_prompt: String::new(), - 4573
permission_mode: "read-only".into(), - 4574
capabilities: Vec::new(), - 4575
prompt_layers: Vec::new(), - 4576
}, - 4577
}, - 4578
) - 4579
.expect("create") - 4580
} - 4581
- 4582
fn append_card_call(log: &mut SessionLog, id: &str, summary: &str) { - 4583
let input = serde_json::json!({"semantic_type":"chart","payload":{"chart_type":"line","series":[],"accessible_summary":summary}}); - 4584
log.append_message(MessageRecord { - 4585
message: Message::assistant(vec![ContentBlock::ToolUse { - 4586
id: id.into(), - 4587
name: "emit_chart_card".into(), - 4588
input: input.clone(), - 4589
}]), - 4590
meta: None, - 4591
}) - 4592
.expect("call"); - 4593
log.append_message(MessageRecord { - 4594
message: Message { - 4595
role: Role::User, - 4596
content: vec![ContentBlock::ToolResult { - 4597
tool_use_id: id.into(), - 4598
content: "Card displayed to the user (chart).".into(), - 4599
is_error: false, - 4600
}], - 4601
}, - 4602
meta: None, - 4603
}) - 4604
.expect("result"); - 4605
append_presentation_for_call(log, "emit_chart_card", id, &input); - 4606
} - 4607
- 4608
/// Channels deliver the card as the answer and retain only explicitly - 4609
/// additional narration. - 4610
#[test] - 4611
fn a_channel_gets_the_cards_of_the_run_ahead_of_the_narration() { - 4612
let dir = tempfile::tempdir().expect("tempdir"); - 4613
let mut log = channel_log(&dir, "ch1"); - 4614
log.append_message(MessageRecord { - 4615
message: Message::user_text("chart it"), - 4616
meta: None, - 4617
}) - 4618
.expect("u"); - 4619
append_card_call(&mut log, "c1", "sales rise steadily"); - 4620
let text = super::text_with_run_cards(&log, "The chart is shown above.".into()); - 4621
assert!( - 4622
text.contains("sales rise steadily"), - 4623
"card content must reach the channel: {text}" - 4624
); - 4625
assert!(!text.contains("The chart is shown above."), "{text}"); - 4626
let with_note = super::text_with_run_cards(&log, "Note: Check the holiday dip.".into()); - 4627
assert!(with_note.contains("sales rise steadily"), "{with_note}"); - 4628
assert!(with_note.ends_with("Check the holiday dip."), "{with_note}"); - 4629
} - 4630
- 4631
#[test] - 4632
fn card_result_projects_only_an_explicit_additional_note() { - 4633
let dir = tempfile::tempdir().expect("tempdir"); - 4634
let mut log = channel_log(&dir, "card-note"); - 4635
log.append_message(MessageRecord { - 4636
message: Message::user_text("chart it"), - 4637
meta: None, - 4638
}) - 4639
.expect("user"); - 4640
append_card_call(&mut log, "c1", "sales rise steadily"); - 4641
log.append_message(MessageRecord { - 4642
message: Message::assistant(vec![ContentBlock::text( - 4643
"Note: The holiday dip needs review.", - 4644
)]), - 4645
meta: None, - 4646
}) - 4647
.expect("answer"); - 4648
let timeline = super::snapshot("card-note", &log); - 4649
let note = timeline.items.iter().find_map(|item| match &item.content { - 4650
OutputContent::Document { document } => document.metadata.get("card_note"), - 4651
_ => None, - 4652
}); - 4653
assert_eq!( - 4654
note.map(String::as_str), - 4655
Some("The holiday dip needs review.") - 4656
); - 4657
} - 4658
- 4659
#[test] - 4660
fn a_run_with_no_cards_is_delivered_unchanged() { - 4661
let dir = tempfile::tempdir().expect("tempdir"); - 4662
let mut log = channel_log(&dir, "ch2"); - 4663
log.append_message(MessageRecord { - 4664
message: Message::user_text("hi"), - 4665
meta: None, - 4666
}) - 4667
.expect("u"); - 4668
assert_eq!(super::text_with_run_cards(&log, "hello".into()), "hello"); - 4669
} - 4670
- 4671
#[test] - 4672
fn only_the_latest_turns_cards_are_delivered() { - 4673
let dir = tempfile::tempdir().expect("tempdir"); - 4674
let mut log = channel_log(&dir, "ch3"); - 4675
log.append_message(MessageRecord { - 4676
message: Message::user_text("first"), - 4677
meta: None, - 4678
}) - 4679
.expect("u1"); - 4680
append_card_call(&mut log, "c1", "OLD-TURN-CARD"); - 4681
log.append_message(MessageRecord { - 4682
message: Message::user_text("second"), - 4683
meta: None, - 4684
}) - 4685
.expect("u2"); - 4686
append_card_call(&mut log, "c2", "NEW-TURN-CARD"); - 4687
let text = super::text_with_run_cards(&log, "done".into()); - 4688
assert!( - 4689
text.contains("NEW-TURN-CARD") && !text.contains("OLD-TURN-CARD"), - 4690
"{text}" - 4691
); - 4692
} - 4693
- 4694
#[test] - 4695
fn a_retried_card_is_delivered_once_and_a_nudge_is_not_a_turn() { - 4696
let dir = tempfile::tempdir().expect("tempdir"); - 4697
let mut log = channel_log(&dir, "ch4"); - 4698
log.append_message(MessageRecord { - 4699
message: Message::user_text("chart it"), - 4700
meta: None, - 4701
}) - 4702
.expect("u"); - 4703
append_card_call(&mut log, "c1", "FIRST-ATTEMPT"); - 4704
log.append_message(MessageRecord::control( - 4705
vak_intent::control::ControlKind::FenceCheck, - 4706
"[fence-check]: resend", - 4707
)) - 4708
.expect("nudge"); - 4709
append_card_call(&mut log, "c2", "RETRY"); - 4710
let text = super::text_with_run_cards(&log, "(no text)".into()); - 4711
assert!( - 4712
text.contains("RETRY") && !text.contains("FIRST-ATTEMPT"), - 4713
"{text}" - 4714
); - 4715
assert!( - 4716
!text.contains("(no text)"), - 4717
"a card-only run has no placeholder: {text}" - 4718
); - 4719
} - 4720
- 4721
/// Real ledger (gemma, "weather in noida"): a prose draft, a presentation - 4722
/// nudge, the same card called three times, then the narration. The user - 4723
/// sees one card and one answer, in that order. - 4724
#[test] - 4725
fn identical_repeated_card_calls_and_a_rejected_draft_project_to_one_card_one_answer() { - 4726
let dir = tempfile::tempdir().expect("tempdir"); - 4727
let mut log = channel_log(&dir, "repeat"); - 4728
log.append_message(MessageRecord { - 4729
message: Message::user_text("weather in noida"), - 4730
meta: None, - 4731
}) - 4732
.expect("u"); - 4733
log.append_message(MessageRecord { - 4734
message: Message::assistant(vec![ContentBlock::text("DRAFT 28C")]), - 4735
meta: None, - 4736
}) - 4737
.expect("draft"); - 4738
log.append_message(MessageRecord::control( - 4739
vak_intent::control::ControlKind::PresentationCheck, - 4740
"[presentation-check]: use a card", - 4741
)) - 4742
.expect("nudge"); - 4743
for id in ["c1", "c2", "c3"] { - 4744
append_card_call(&mut log, id, "28C"); - 4745
} - 4746
log.append_message(MessageRecord { - 4747
message: Message::assistant(vec![ContentBlock::text("FINAL 28C")]), - 4748
meta: None, - 4749
}) - 4750
.expect("final"); - 4751
let timeline = snapshot("repeat", &log); - 4752
let cards = timeline - 4753
.items - 4754
.iter() - 4755
.filter(|i| i.kind == OutputKind::Card) - 4756
.count(); - 4757
let answers: Vec<&str> = timeline - 4758
.items - 4759
.iter() - 4760
.filter(|i| i.role == vak_delivery::OutputRole::Assistant) - 4761
.map(|i| i.fallback_text.as_str()) - 4762
.collect(); - 4763
assert_eq!( - 4764
cards, - 4765
1, - 4766
"{:?}", - 4767
timeline.items.iter().map(|i| &i.id).collect::<Vec<_>>() - 4768
); - 4769
assert_eq!(answers.len(), 1, "{answers:?}"); - 4770
assert!(answers[0].contains("FINAL"), "{answers:?}"); - 4771
let kinds: Vec<_> = timeline.items.iter().map(|i| i.kind).collect(); - 4772
let card_at = kinds - 4773
.iter() - 4774
.position(|k| *k == OutputKind::Card) - 4775
.expect("a card"); - 4776
let answer_at = timeline - 4777
.items - 4778
.iter() - 4779
.position(|i| i.role == vak_delivery::OutputRole::Assistant) - 4780
.expect("an answer"); - 4781
assert!(card_at < answer_at, "the card precedes its narration"); - 4782
} - 4783
- 4784
/// A draft the runtime sent back for a redo is internal: the user sees the - 4785
/// redone answer, never the rejected attempt as well. - 4786
#[test] - 4787
fn a_rejected_draft_is_never_projected_but_the_redo_is() { - 4788
let dir = tempfile::tempdir().expect("tempdir"); - 4789
let mut log = channel_log(&dir, "draft"); - 4790
log.append_message(MessageRecord { - 4791
message: Message::user_text("weather in delhi"), - 4792
meta: None, - 4793
}) - 4794
.expect("u"); - 4795
log.append_message(MessageRecord { - 4796
message: Message::assistant(vec![ContentBlock::text( - 4797
"DRAFT-PROSE answer with a broken fence", - 4798
)]), - 4799
meta: None, - 4800
}) - 4801
.expect("draft"); - 4802
log.append_message(MessageRecord::control( - 4803
vak_intent::control::ControlKind::PresentationCheck, - 4804
"[presentation-check]: use a card", - 4805
)) - 4806
.expect("nudge"); - 4807
append_card_call(&mut log, "c1", "29C and sunny"); - 4808
log.append_message(MessageRecord { - 4809
message: Message::assistant(vec![ContentBlock::text("FINAL-NARRATION")]), - 4810
meta: None, - 4811
}) - 4812
.expect("final"); - 4813
let timeline = snapshot("draft", &log); - 4814
let assistant_text: Vec<String> = timeline - 4815
.items - 4816
.iter() - 4817
.filter(|i| i.role == vak_delivery::OutputRole::Assistant) - 4818
.map(|i| i.fallback_text.clone()) - 4819
.collect(); - 4820
assert!( - 4821
!assistant_text.iter().any(|t| t.contains("DRAFT-PROSE")), - 4822
"{assistant_text:?}" - 4823
); - 4824
assert!( - 4825
assistant_text.iter().any(|t| t.contains("FINAL-NARRATION")), - 4826
"{assistant_text:?}" - 4827
); - 4828
assert!( - 4829
timeline - 4830
.items - 4831
.iter() - 4832
.any(|i| matches!(i.content, OutputContent::Structured { .. })) - 4833
); - 4834
} - 4835
- 4836
/// Only a redo-nudge rejects a draft. A stop hook or guard asks the model - 4837
/// to keep working, so the text before it is real interim narration. - 4838
#[test] - 4839
fn a_continue_nudge_does_not_reject_the_text_before_it() { - 4840
let dir = tempfile::tempdir().expect("tempdir"); - 4841
let mut log = channel_log(&dir, "cont"); - 4842
log.append_message(MessageRecord { - 4843
message: Message::user_text("do the thing"), - 4844
meta: None, - 4845
}) - 4846
.expect("u"); - 4847
log.append_message(MessageRecord { - 4848
message: Message::assistant(vec![ContentBlock::text("INTERIM-PROGRESS-NOTE")]), - 4849
meta: None, - 4850
}) - 4851
.expect("interim"); - 4852
log.append_message(MessageRecord::control( - 4853
vak_intent::control::ControlKind::StopGuard, - 4854
"[stop-guard]: not done yet\nPlease continue.", - 4855
)) - 4856
.expect("guard"); - 4857
log.append_message(MessageRecord { - 4858
message: Message::assistant(vec![ContentBlock::text("ALL-DONE")]), - 4859
meta: None, - 4860
}) - 4861
.expect("final"); - 4862
let timeline = snapshot("cont", &log); - 4863
let texts: Vec<String> = timeline - 4864
.items - 4865
.iter() - 4866
.filter(|i| i.role == vak_delivery::OutputRole::Assistant) - 4867
.map(|i| i.fallback_text.clone()) - 4868
.collect(); - 4869
assert!( - 4870
texts.iter().any(|t| t.contains("INTERIM-PROGRESS-NOTE")), - 4871
"{texts:?}" - 4872
); - 4873
assert!(texts.iter().any(|t| t.contains("ALL-DONE")), "{texts:?}"); - 4874
} - 4875
- 4876
/// The transcript sent to clients carries only what a person can see: - 4877
/// nudges and derived context blocks are not sent, and every message - 4878
/// arrives with the ledger entry id the chat pairs turns by. - 4879
#[test] - 4880
fn the_client_transcript_omits_runtime_traffic_and_carries_entry_ids() { - 4881
let dir = tempfile::tempdir().expect("tempdir"); - 4882
let mut log = channel_log(&dir, "wire"); - 4883
log.append_message(MessageRecord { - 4884
message: Message::user_text("what is the weather"), - 4885
meta: None, - 4886
}) - 4887
.expect("u"); - 4888
// A text-only draft the runtime itself rejected: the very next - 4889
// entry is a control nudge that asks for a redo. Neither the draft - 4890
// nor the nudge belongs on the wire (docs/audits Finding 3). - 4891
log.append_message(MessageRecord { - 4892
message: Message::assistant(vec![ContentBlock::text("sunny")]), - 4893
meta: None, - 4894
}) - 4895
.expect("draft"); - 4896
log.append_message(MessageRecord::control( - 4897
vak_intent::control::ControlKind::GroundingCheck, - 4898
"[grounding-check]: cite it", - 4899
)) - 4900
.expect("nudge"); - 4901
log.append_message(MessageRecord { - 4902
message: Message::assistant(vec![ContentBlock::text("Sunny, source: NWS.")]), - 4903
meta: None, - 4904
}) - 4905
.expect("final answer"); - 4906
let json = crate::transcript_json(&log); - 4907
let messages = json["messages"].as_array().expect("messages"); - 4908
let entries = json["entries"].as_array().expect("entries"); - 4909
assert_eq!( - 4910
messages.len(), - 4911
2, - 4912
"neither the nudge nor the rejected draft is on the wire: {messages:?}" - 4913
); - 4914
assert_eq!( - 4915
messages[1]["content"][0]["text"].as_str(), - 4916
Some("Sunny, source: NWS."), - 4917
"the redo's real answer is on the wire, not the discarded draft" - 4918
); - 4919
assert_eq!( - 4920
entries.len(), - 4921
messages.len(), - 4922
"entries run parallel to messages" - 4923
); - 4924
assert!( - 4925
entries - 4926
.iter() - 4927
.all(|e| e["entry_id"].as_str().is_some_and(|id| !id.is_empty())) - 4928
); - 4929
assert!( - 4930
json.get("contract").is_none(), - 4931
"the frozen system prompt is not sent to the client" - 4932
); - 4933
assert!( - 4934
json["count"].as_u64().expect("count") >= 4, - 4935
"count stays the model-visible total, draft and nudge included" - 4936
); - 4937
} - 4938
- 4939
#[test] - 4940
fn conversation_messages_drops_the_rejected_draft_but_keeps_the_redo() { - 4941
let dir = tempfile::tempdir().expect("tempdir"); - 4942
let mut log = channel_log(&dir, "wire-md"); - 4943
log.append_message(MessageRecord { - 4944
message: Message::user_text("what is the weather"), - 4945
meta: None, - 4946
}) - 4947
.expect("u"); - 4948
log.append_message(MessageRecord { - 4949
message: Message::assistant(vec![ContentBlock::text("sunny")]), - 4950
meta: None, - 4951
}) - 4952
.expect("draft"); - 4953
log.append_message(MessageRecord::control( - 4954
vak_intent::control::ControlKind::GroundingCheck, - 4955
"[grounding-check]: cite it", - 4956
)) - 4957
.expect("nudge"); - 4958
log.append_message(MessageRecord { - 4959
message: Message::assistant(vec![ContentBlock::text("Sunny, source: NWS.")]), - 4960
meta: None, - 4961
}) - 4962
.expect("final answer"); - 4963
let messages = crate::conversation_messages(&log); - 4964
let texts: Vec<String> = messages - 4965
.iter() - 4966
.flat_map(|m| m.content.iter()) - 4967
.filter_map(|block| match block { - 4968
ContentBlock::Text { text } => Some(text.clone()), - 4969
_ => None, - 4970
}) - 4971
.collect(); - 4972
assert_eq!(texts, vec!["what is the weather", "Sunny, source: NWS."]); - 4973
} - 4974
} - 4975
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.