- 1
#![allow( - 2
unused_imports, - 3
unused_variables, - 4
clippy::unwrap_used, - 5
clippy::expect_used, - 6
clippy::panic, - 7
clippy::indexing_slicing, - 8
clippy::redundant_closure, - 9
clippy::useless_conversion, - 10
clippy::bool_assert_comparison, - 11
clippy::collapsible_if, - 12
clippy::len_zero, - 13
clippy::needless_borrow, - 14
suspicious_double_ref_op - 15
)] - 16
- 17
//! Deep audit harness for `vak-delivery`: 500+ scenarios covering the render - 18
//! pipeline, markup conversion, templates, skill registry, recipes, signals, - 19
//! outbox lifecycle, posture disposition matrix, and worker subprocess - 20
//! protocol. - 21
//! - 22
//! Each scenario is a self-contained closure that panics on invariant - 23
//! violation. The harness counts pass/fail so the test report proves breadth. - 24
- 25
use serde_json::json; - 26
use std::collections::BTreeMap; - 27
use vak_delivery::Block; - 28
use vak_delivery::DeliveryPacket; - 29
use vak_delivery::Markup; - 30
use vak_delivery::ProgressPayload; - 31
use vak_delivery::ToolResultPayload; - 32
use vak_delivery::discord; - 33
use vak_delivery::outbox::{Outbox, OutboxError, OutboxRecord, OutboxState}; - 34
use vak_delivery::slack; - 35
use vak_delivery::telegram; - 36
use vak_delivery::worker::{WORKER_PROTOCOL_VERSION, WorkerRequest, WorkerResponse, process_line}; - 37
use vak_delivery::{ - 38
AnswerDraft, AnswerResult, ApprovalPayload, ArtifactRef, Cadence, CalloutTone, Citation, - 39
DELIVERY_SCHEMA_VERSION, DeliveryAction, DeliveryContent, DeliveryError, DeliveryJob, - 40
DeliveryKind, DeliveryPayload, DeliveryPosture, DeliveryProfile, Disposition, DocumentBlock, - 41
DocumentCoverage, DocumentCoverageDisposition, InlineNode, OutputContent, OutputItem, - 42
OutputKind, OutputProvenance, OutputRole, OutputStatus, OutputStreamEvent, OutputStreamFrame, - 43
OutputTimeline, PRESENTATION_SCHEMA_VERSION, PresentationDocument, ResultOutcome, - 44
SurfaceCapabilities, TemplateActivation, TemplateNode, TemplateOrigin, TemplateRegistry, - 45
TemplateSlot, TemplateSpec, Urgency, - 46
}; - 47
use vak_delivery::{ - 48
adapters::{built_in_adapters, structured_outputs_from_tool_result}, - 49
skills::{ - 50
PRESENTATION_SKILL_API, PresentationSkillManifest, RendererBinding, SignalContext, - 51
SkillError, SkillRegistry, StructuredOutput, built_in_recipes, built_in_skill_registry, - 52
link_previews_from_text, parse_fragment, project_structured_fences, signals_from_context, - 53
signals_from_text, structured_outputs_from_text, - 54
}, - 55
}; - 56
use vak_delivery::{compile_markdown, render}; - 57
- 58
type Scenario = (String, Box<dyn FnOnce()>); - 59
- 60
fn tc<F: FnOnce() + 'static>(name: &str, f: F) -> Scenario { - 61
(name.to_string(), Box::new(f)) - 62
} - 63
- 64
fn run(label: &str, scenarios: Vec<Scenario>) { - 65
let total = scenarios.len(); - 66
let mut passed = 0usize; - 67
let mut failures: Vec<String> = Vec::new(); - 68
for (name, test) in scenarios { - 69
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| test())) { - 70
Ok(()) => passed += 1, - 71
Err(_) => failures.push(name), - 72
} - 73
} - 74
if !failures.is_empty() { - 75
for f in &failures { - 76
eprintln!("FAIL: {label} :: {f}"); - 77
} - 78
} - 79
assert_eq!( - 80
passed, total, - 81
"{label}: {passed}/{total} passed — failures: {failures:?}" - 82
); - 83
println!(" ✓ {label}: {total} scenarios passed"); - 84
} - 85
- 86
fn answer_draft(source: &str) -> AnswerDraft { - 87
AnswerDraft::from_markdown(source) - 88
} - 89
- 90
fn answer_blocks(source: &str) -> Vec<Block> { - 91
answer_draft(source).blocks - 92
} - 93
- 94
fn answer_job( - 95
source: &str, - 96
markup: Markup, - 97
max_chars: Option<usize>, - 98
kind: DeliveryKind, - 99
surface: &str, - 100
) -> DeliveryJob { - 101
DeliveryJob { - 102
job_id: "test-job".into(), - 103
target: format!("{surface}:one"), - 104
kind, - 105
content: DeliveryContent::Answer(answer_draft(source)), - 106
profile: DeliveryProfile { - 107
surface: surface.into(), - 108
markup, - 109
max_chars, - 110
supports_tables: true, - 111
supports_code_blocks: true, - 112
supports_links: true, - 113
supports_actions: false, - 114
template: None, - 115
posture: DeliveryPosture::default(), - 116
}, - 117
skill_registry: None, - 118
} - 119
} - 120
- 121
fn plain_profile(surface: &str) -> DeliveryProfile { - 122
DeliveryProfile::plain(surface) - 123
} - 124
- 125
fn approval_job(_markup: Markup, surface: &str) -> DeliveryJob { - 126
DeliveryJob { - 127
job_id: "approval-job".into(), - 128
target: format!("{surface}:one"), - 129
kind: DeliveryKind::Approval, - 130
content: DeliveryContent::Approval(ApprovalPayload { - 131
request_id: "req-1".into(), - 132
title: "Confirm".into(), - 133
detail: "Do you want to proceed?".into(), - 134
expires_at: Some("2026-12-31T23:59:59Z".into()), - 135
actions: vec![DeliveryAction { - 136
id: "approve".into(), - 137
label: "Approve".into(), - 138
verb: "approve".into(), - 139
data: BTreeMap::new(), - 140
}], - 141
}), - 142
profile: plain_profile(surface), - 143
skill_registry: None, - 144
} - 145
} - 146
- 147
fn progress_job(_markup: Markup, surface: &str) -> DeliveryJob { - 148
DeliveryJob { - 149
job_id: "progress-job".into(), - 150
target: format!("{surface}:one"), - 151
kind: DeliveryKind::Progress, - 152
content: DeliveryContent::Progress(ProgressPayload { - 153
label: "Build".into(), - 154
state: "running".into(), - 155
percent: Some(75), - 156
}), - 157
profile: plain_profile(surface), - 158
skill_registry: None, - 159
} - 160
} - 161
- 162
fn tool_result_job(_markup: Markup, surface: &str, is_error: bool) -> DeliveryJob { - 163
DeliveryJob { - 164
job_id: "tool-result-job".into(), - 165
target: format!("{surface}:one"), - 166
kind: DeliveryKind::ToolResult, - 167
content: DeliveryContent::ToolResult(ToolResultPayload { - 168
tool: "bash".into(), - 169
output: if is_error { - 170
"command not found".into() - 171
} else { - 172
"ok\noutput".into() - 173
}, - 174
is_error, - 175
}), - 176
profile: plain_profile(surface), - 177
skill_registry: None, - 178
} - 179
} - 180
- 181
fn text_job(markdown: &str, _markup: Markup, surface: &str) -> DeliveryJob { - 182
DeliveryJob { - 183
job_id: "text-job".into(), - 184
target: format!("{surface}:one"), - 185
kind: DeliveryKind::Assistant, - 186
content: DeliveryContent::Text { - 187
markdown: markdown.into(), - 188
}, - 189
profile: plain_profile(surface), - 190
skill_registry: None, - 191
} - 192
} - 193
- 194
// ════════════════════════════════════════════════════════════════════ - 195
// 1. RENDER PIPELINE — Markdown variants × Markup - 196
// ════════════════════════════════════════════════════════════════════ - 197
- 198
#[test] - 199
fn audit_render_pipeline() { - 200
let mut scenarios: Vec<Scenario> = vec![]; - 201
- 202
let markdown_variants: &[&str] = &[ - 203
"# Title\n\nBody text.", - 204
"Plain paragraph.", - 205
"# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6", - 206
"- item one\n- item two\n- item three", - 207
"1. first\n2. second\n3. third", - 208
"- outer\n - inner", - 209
"> quoted text", - 210
"```rust\nlet x = 1;\n```", - 211
"```python\nprint('hello')\n```", - 212
"```\nplain code\n```", - 213
"| A | B | C |\n|---|---|---|\n| 1 | 2 | 3 |\n| 4 | 5 | 6 |", - 214
"**bold** text", - 215
"*italic* text", - 216
"~~strikethrough~~", - 217
"`inline code`", - 218
"[link text](https://example.com)", - 219
"", - 220
"---", - 221
"***", - 222
"Line 1\n\nLine 2\n\nLine 3", - 223
"Text with `code` and **bold** together.", - 224
"# Title with [link](https://x.com)", - 225
"", - 226
" ", - 227
"Special chars: < > & \" '", - 228
"Emoji: 🎉🚀✅", - 229
"Unicode: Hello 世界", - 230
"Tabs\tand\nnewlines", - 231
"```\nline1\nline2\nline3\n```\n\nText after code.", - 232
"**a** *b* ~~c~~ `d`", - 233
"# Title\n\n> Quote\n\n- List\n- Items\n\n```js\ncode()\n```\n\nText end.", - 234
"", - 235
" leading spaces\n\n## Sub\n\n> > nested quote", - 236
"\n\n# Title", - 237
"[text](https://safe.com)\n\n[bad](javascript:alert(1))", - 238
"| Col1 | Col2 |\n| --- | --- |\n| val1 | val2 |", - 239
"```diff\n- old\n+ new\n```\n\nMore text.", - 240
"# Title\n\n```vak\n{\"semantic_type\":\"metric\",\"payload\":{\"label\":\"X\",\"value\":42}}\n```", - 241
"1. Nested\n 1. Deep\n 2. Deep2\n2. Back", - 242
"- [ ] todo\n- [x] done", - 243
"Text with a footnote[^1] and more.\n\n[^1]: Note here.", - 244
"Entity-like: AT&T and <tags>", - 245
"Nested emphasis: **bold *italic***", - 246
"Multiple\n\n\n\n\n\n\n\nblanks", - 247
]; - 248
- 249
let markups: [(Markup, &str); 6] = [ - 250
(Markup::Plain, "plain"), - 251
(Markup::Markdown, "markdown"), - 252
(Markup::TelegramHtml, "telegram"), - 253
(Markup::SlackMrkdwn, "slack"), - 254
(Markup::DiscordMarkdown, "discord"), - 255
(Markup::Json, "json"), - 256
]; - 257
- 258
for (i, md) in markdown_variants.iter().enumerate() { - 259
let m = md.to_string(); - 260
for (markup, label) in markups { - 261
let mk = markup; - 262
let ml = label; - 263
let m_clone = m.clone(); - 264
scenarios.push(tc(&format!("render_{i}_{ml}"), move || { - 265
let job = answer_job(&m_clone, mk, None, DeliveryKind::Assistant, "test"); - 266
let packet = render(&job).expect("render should succeed"); - 267
assert_eq!(packet.schema_version, DELIVERY_SCHEMA_VERSION); - 268
assert_eq!(packet.kind, DeliveryKind::Assistant); - 269
assert_eq!(packet.surface, "test"); - 270
assert_eq!(packet.job_id, "test-job"); - 271
assert_eq!(packet.fallback_markdown, m_clone); - 272
match mk { - 273
Markup::Json => { - 274
assert!(matches!(packet.payload, DeliveryPayload::Structured(_))); - 275
} - 276
_ => { - 277
assert!(matches!(packet.payload, DeliveryPayload::Text(_))); - 278
} - 279
} - 280
assert!(!packet.coverage.is_empty() || m_clone.trim().is_empty()); - 281
})); - 282
} - 283
} - 284
- 285
// External content kinds render successfully - 286
let external_kinds = [ - 287
DeliveryKind::Assistant, - 288
DeliveryKind::TaskSummary, - 289
DeliveryKind::Alert, - 290
DeliveryKind::User, - 291
]; - 292
for (i, kind) in external_kinds.iter().enumerate() { - 293
let k = *kind; - 294
scenarios.push(tc(&format!("external_kind_{i}_{k:?}"), move || { - 295
let source = "# Test\n\nContent."; - 296
let job = answer_job(source, Markup::Markdown, None, k, "test"); - 297
let packet = render(&job).expect("external content should render"); - 298
assert_eq!(packet.kind, k); - 299
assert_eq!(packet.fallback_markdown, source); - 300
})); - 301
} - 302
- 303
run("render_pipeline", scenarios); - 304
} - 305
- 306
// ════════════════════════════════════════════════════════════════════ - 307
// 2. DELIVERY KIND × CONTENT MATCHING - 308
// ════════════════════════════════════════════════════════════════════ - 309
- 310
#[test] - 311
fn audit_kind_content_matching() { - 312
let mut scenarios: Vec<Scenario> = vec![]; - 313
- 314
let external_kinds = [ - 315
DeliveryKind::Assistant, - 316
DeliveryKind::TaskSummary, - 317
DeliveryKind::Alert, - 318
DeliveryKind::User, - 319
]; - 320
let control_kinds = [ - 321
DeliveryKind::System, - 322
DeliveryKind::Developer, - 323
DeliveryKind::Internal, - 324
]; - 325
- 326
// External kinds with Answer content → OK - 327
for (i, kind) in external_kinds.iter().enumerate() { - 328
let k = *kind; - 329
scenarios.push(tc(&format!("ext_answer_ok_{i}"), move || { - 330
let job = answer_job("# T\n\nB", Markup::Plain, None, k, "test"); - 331
assert!(render(&job).is_ok()); - 332
})); - 333
} - 334
- 335
// Control kinds always rejected - 336
for (i, kind) in control_kinds.iter().enumerate() { - 337
let k = *kind; - 338
scenarios.push(tc(&format!("control_rejected_{i}"), move || { - 339
let job = DeliveryJob { - 340
job_id: "c".into(), - 341
target: "t".into(), - 342
kind: k, - 343
content: DeliveryContent::Text { - 344
markdown: "sys".into(), - 345
}, - 346
profile: plain_profile("test"), - 347
skill_registry: None, - 348
}; - 349
assert!(render(&job).is_err()); - 350
})); - 351
} - 352
- 353
// Approval content with non-Approval kind → rejected - 354
for (i, kind) in external_kinds.iter().enumerate() { - 355
if *kind == DeliveryKind::Approval { - 356
continue; - 357
} - 358
let k = *kind; - 359
scenarios.push(tc(&format!("approval_mismatch_{i}"), move || { - 360
let job = DeliveryJob { - 361
job_id: "m".into(), - 362
target: "t".into(), - 363
kind: k, - 364
content: DeliveryContent::Approval(ApprovalPayload { - 365
request_id: "r".into(), - 366
title: "t".into(), - 367
detail: "d".into(), - 368
expires_at: None, - 369
actions: vec![], - 370
}), - 371
profile: plain_profile("test"), - 372
skill_registry: None, - 373
}; - 374
assert!(render(&job).is_err()); - 375
})); - 376
} - 377
- 378
// Text content accepted by all kinds - 379
for (i, kind) in [ - 380
DeliveryKind::Assistant, - 381
DeliveryKind::Approval, - 382
DeliveryKind::Progress, - 383
DeliveryKind::ToolResult, - 384
] - 385
.iter() - 386
.enumerate() - 387
{ - 388
let k = *kind; - 389
scenarios.push(tc(&format!("text_content_ok_{i}"), move || { - 390
let job = text_job("hello", Markup::Plain, "test"); - 391
let job = DeliveryJob { kind: k, ..job }; - 392
assert!(render(&job).is_ok()); - 393
})); - 394
} - 395
- 396
// Progress content only matches Progress kind - 397
scenarios.push(tc("progress_content_for_assistant_rejected", || { - 398
let job = DeliveryJob { - 399
job_id: "p".into(), - 400
target: "t".into(), - 401
kind: DeliveryKind::Assistant, - 402
content: DeliveryContent::Progress(ProgressPayload { - 403
label: "l".into(), - 404
state: "s".into(), - 405
percent: None, - 406
}), - 407
profile: plain_profile("test"), - 408
skill_registry: None, - 409
}; - 410
assert!(render(&job).is_err()); - 411
})); - 412
- 413
// ToolResult content only matches ToolResult kind - 414
scenarios.push(tc("toolresult_content_for_user_rejected", || { - 415
let job = DeliveryJob { - 416
job_id: "tr".into(), - 417
target: "t".into(), - 418
kind: DeliveryKind::User, - 419
content: DeliveryContent::ToolResult(ToolResultPayload { - 420
tool: "bash".into(), - 421
output: "ok".into(), - 422
is_error: false, - 423
}), - 424
profile: plain_profile("test"), - 425
skill_registry: None, - 426
}; - 427
assert!(render(&job).is_err()); - 428
})); - 429
- 430
run("kind_content_matching", scenarios); - 431
} - 432
- 433
// ════════════════════════════════════════════════════════════════════ - 434
// 3. MARKUP CONVERSION (compile_markdown) - 435
// ════════════════════════════════════════════════════════════════════ - 436
- 437
#[test] - 438
fn audit_compile_markdown() { - 439
let mut scenarios: Vec<Scenario> = vec![]; - 440
- 441
let markdown_cases: &[(&str, &str)] = &[ - 442
("empty string", ""), - 443
("plain text only", "Hello world"), - 444
("H1", "# Title"), - 445
("H2", "## Subtitle"), - 446
("H6 (max)", "###### Deepest"), - 447
("H7 (too deep)", "####### Too Deep"), - 448
("unordered list", "- a\n- b\n- c"), - 449
("ordered list", "1. first\n2. second"), - 450
("nested list", "- outer\n - inner"), - 451
("blockquote", "> quoted"), - 452
("nested blockquote", "> > deep"), - 453
("fenced code", "```rust\nlet x = 1;\n```"), - 454
("indented code", " indented code"), - 455
("inline code", "use `code` here"), - 456
("bold text", "**bold**"), - 457
("italic text", "*italic*"), - 458
("strikethrough", "~~struck~~"), - 459
("link", "[text](https://example.com)"), - 460
("image", ""), - 461
("horizontal rule", "---"), - 462
("table", "| A | B |\n|---|---|\n| 1 | 2 |"), - 463
("task list", "- [ ] todo\n- [x] done"), - 464
("nested blockquote + list", "> - item\n> - item2"), - 465
("code block then text", "```\ncode\n```\n\nText."), - 466
("heading then list", "# Title\n\n- a\n- b"), - 467
("multiple paragraphs", "First.\n\nSecond.\n\nThird."), - 468
("backslash escape", r"\*not bold\*"), - 469
("entity-like text", "AT&T and <company>"), - 470
("long paragraph", &"word ".repeat(500)), - 471
("unicode in heading", "# Tïtlé 标题"), - 472
("emoji heading", "# 🎉 Party"), - 473
( - 474
"mixed everything", - 475
"# Title\n\n**bold** *italic*\n\n- item\n\n```js\ncode()\n```\n\nText end.", - 476
), - 477
( - 478
"vak structured fence", - 479
"```vak\n{\"semantic_type\":\"metric\",\"payload\":{\"label\":\"x\",\"value\":1}}\n```", - 480
), - 481
("mermaid diagram", "```mermaid\ngraph TD\nA --> B\n```"), - 482
("diff fence", "```diff\n- old\n+ new\n```"), - 483
("raw HTML inline", "<b>bold</b>"), - 484
("raw HTML block", "<div>block</div>"), - 485
("footnote", "Text with a footnote[^1]\n\n[^1]: Note here."), - 486
("nested emphasis", "**bold *italic***"), - 487
("nested list depth", "- a\n - b\n - c\n - d"), - 488
("empty input", ""), - 489
("only whitespace", " \n\n "), - 490
("single heading", "# Only Title"), - 491
("deeply nested blockquotes", "> > > > deep"), - 492
]; - 493
- 494
for (label, md) in markdown_cases { - 495
let l = label.to_string(); - 496
let m = md.to_string(); - 497
scenarios.push(tc(&format!("compile_{l}"), move || { - 498
let doc = compile_markdown(&m); - 499
assert_eq!(doc.schema_version, PRESENTATION_SCHEMA_VERSION); - 500
assert_eq!(doc.source_markdown, m); - 501
})); - 502
} - 503
- 504
// Block type reachability - 505
let block_cases: &[(&str, &str)] = &[ - 506
("heading", "# Title"), - 507
("code", "```rust\nx\n```"), - 508
("list", "- a\n- b"), - 509
("quote", "> quoted"), - 510
("paragraph", "just text"), - 511
("rule", "---"), - 512
("table", "| A | B |\n|---|---|\n| 1 | 2 |"), - 513
( - 514
"structured", - 515
"```vak\n{\"semantic_type\":\"metric\",\"payload\":{\"label\":\"L\",\"value\":5}}\n```", - 516
), - 517
("diagram", "```mermaid\ngraph TD\nA-->B\n```"), - 518
("diff", "```diff\n- a\n+ b\n```"), - 519
("raw_html", "<div>x</div>"), - 520
]; - 521
for (label, md) in block_cases { - 522
let l = label.to_string(); - 523
let m = md.to_string(); - 524
scenarios.push(tc(&format!("block_{l}"), move || { - 525
let doc = compile_markdown(&m); - 526
assert!(!doc.blocks.is_empty() || m.is_empty()); - 527
for block in &doc.blocks { - 528
let _ = block; // DocumentBlock is a verified enum; variants carry id fields - 529
} - 530
})); - 531
} - 532
- 533
// Coverage disposition for each block type - 534
scenarios.push(tc("coverage_disp_all_native_or_fallback", || { - 535
let doc = compile_markdown( - 536
"# T\n\n> q\n\n```\ncode\n```\n\n<div>html</div>\n\n| a | b |\n|---|---|\n| 1 | 2 |", - 537
); - 538
for cov in &doc.coverage { - 539
assert!(matches!( - 540
cov.disposition, - 541
DocumentCoverageDisposition::Native | DocumentCoverageDisposition::Fallback - 542
)); - 543
} - 544
})); - 545
- 546
run("compile_markdown", scenarios); - 547
} - 548
- 549
// ════════════════════════════════════════════════════════════════════ - 550
// 4. DELIVERY PROFILE & CAPABILITIES - 551
// ════════════════════════════════════════════════════════════════════ - 552
- 553
#[test] - 554
fn audit_delivery_profiles() { - 555
let mut scenarios: Vec<Scenario> = vec![]; - 556
- 557
for (markup, label) in [ - 558
(Markup::Plain, "plain"), - 559
(Markup::Markdown, "markdown"), - 560
(Markup::TelegramHtml, "tg"), - 561
(Markup::SlackMrkdwn, "slack"), - 562
(Markup::DiscordMarkdown, "discord"), - 563
(Markup::Json, "json"), - 564
] { - 565
let m = markup; - 566
let l = label; - 567
scenarios.push(tc(&format!("profile_{l}"), move || { - 568
let p = DeliveryProfile { - 569
surface: "test".into(), - 570
markup: m, - 571
max_chars: None, - 572
supports_tables: true, - 573
supports_code_blocks: true, - 574
supports_links: true, - 575
supports_actions: false, - 576
template: None, - 577
posture: DeliveryPosture::default(), - 578
}; - 579
let caps = p.capabilities(); - 580
assert_eq!(caps.max_chars, None); - 581
assert_eq!(caps.code, true); - 582
assert_eq!(caps.links, true); - 583
assert_eq!(caps.tables, true); - 584
})); - 585
} - 586
- 587
// Native surfaces: all capabilities enabled - 588
for surface in &["desktop", "tui", "admin"] { - 589
let s = surface.to_string(); - 590
scenarios.push(tc(&format!("native_{s}"), move || { - 591
let p = DeliveryProfile { - 592
surface: s.clone(), - 593
markup: Markup::Json, - 594
max_chars: None, - 595
supports_tables: true, - 596
supports_code_blocks: true, - 597
supports_links: true, - 598
supports_actions: true, - 599
template: None, - 600
posture: DeliveryPosture::default(), - 601
}; - 602
let caps = p.capabilities(); - 603
assert!(caps.structured_blocks); - 604
assert!(caps.media); - 605
assert!(caps.file_references); - 606
assert!(caps.color); - 607
assert!(caps.interactive); - 608
})); - 609
} - 610
- 611
// Non-native surfaces with Plain markup - 612
for surface in &["telegram", "slack", "discord", "web", "voice"] { - 613
let s = surface.to_string(); - 614
scenarios.push(tc(&format!("nonnative_{s}"), move || { - 615
let p = DeliveryProfile { - 616
surface: s.clone(), - 617
markup: Markup::Plain, - 618
max_chars: Some(4096), - 619
supports_tables: false, - 620
supports_code_blocks: false, - 621
supports_links: true, - 622
supports_actions: false, - 623
template: None, - 624
posture: DeliveryPosture::default(), - 625
}; - 626
let caps = p.capabilities(); - 627
assert!(!caps.structured_blocks); - 628
assert!(!caps.media); - 629
assert!(!caps.color); - 630
assert!(caps.accessible_plain); - 631
assert_eq!(caps.max_chars, Some(4096)); - 632
})); - 633
} - 634
- 635
// Plain constructor defaults - 636
scenarios.push(tc("plain_constructor", || { - 637
let p = DeliveryProfile::plain("test"); - 638
assert_eq!(p.markup, Markup::Plain); - 639
assert_eq!(p.max_chars, None); - 640
assert!(!p.supports_tables); - 641
assert!(p.supports_code_blocks); - 642
assert!(p.supports_links); - 643
assert!(!p.supports_actions); - 644
assert!(p.template.is_none()); - 645
})); - 646
- 647
// Render with empty surface fails - 648
scenarios.push(tc("empty_surface_renders_fail", || { - 649
let job = answer_job("# T", Markup::Plain, None, DeliveryKind::Assistant, ""); - 650
assert!(render(&job).is_err()); - 651
})); - 652
- 653
// Render with max_chars=0 fails - 654
scenarios.push(tc("max_chars_zero_renders_fail", || { - 655
let job = answer_job( - 656
"# T", - 657
Markup::Plain, - 658
Some(0), - 659
DeliveryKind::Assistant, - 660
"test", - 661
); - 662
assert!(render(&job).is_err()); - 663
})); - 664
- 665
run("delivery_profiles", scenarios); - 666
} - 667
- 668
// ════════════════════════════════════════════════════════════════════ - 669
// 5. CHUNKING - 670
// ════════════════════════════════════════════════════════════════════ - 671
- 672
#[test] - 673
fn audit_chunking() { - 674
let mut scenarios: Vec<Scenario> = vec![]; - 675
- 676
let max_chars_table: &[(Option<usize>, usize)] = &[ - 677
(None, 1), - 678
(Some(100), 1), - 679
(Some(10), 5), - 680
(Some(5), 10), - 681
(Some(1), 50), - 682
]; - 683
- 684
for (i, (max, min_chunks)) in max_chars_table.iter().enumerate() { - 685
let mc = *max; - 686
let mn = *min_chunks; - 687
scenarios.push(tc(&format!("chunk_{i}"), move || { - 688
let source = &"word ".repeat(100); - 689
let job = answer_job(source, Markup::Plain, mc, DeliveryKind::Assistant, "test"); - 690
let packet = render(&job).expect("render"); - 691
assert!( - 692
packet.chunks.len() >= mn, - 693
"got {} chunks, expected >= {}", - 694
packet.chunks.len(), - 695
mn - 696
); - 697
})); - 698
} - 699
- 700
// Empty input - 701
scenarios.push(tc("chunk_empty_input", || { - 702
let job = answer_job("", Markup::Plain, Some(10), DeliveryKind::Assistant, "test"); - 703
let packet = render(&job).expect("render"); - 704
assert!(!packet.chunks.is_empty()); - 705
})); - 706
- 707
// Telegram HTML chunking - 708
for (i, max) in [None, Some(32), Some(50), Some(100)].iter().enumerate() { - 709
let mc = *max; - 710
scenarios.push(tc(&format!("tg_chunk_{i}"), move || { - 711
let source = "# Title\n\nSome **bold** text.\n\n| a | b |\n|---|---|\n| 1 | 2 |"; - 712
let job = answer_job( - 713
source, - 714
Markup::TelegramHtml, - 715
mc, - 716
DeliveryKind::Assistant, - 717
"tg", - 718
); - 719
let packet = render(&job).expect("render"); - 720
if let Some(m) = mc { - 721
for chunk in &packet.chunks { - 722
assert!( - 723
chunk.chars().count() <= m + 100, - 724
"chunk too long: {}", - 725
chunk.chars().count() - 726
); - 727
} - 728
} - 729
})); - 730
} - 731
- 732
// Slack/Discord markdown chunking - 733
for (i, surface) in ["slack", "discord"].iter().enumerate() { - 734
let surface = surface.to_string(); - 735
for markup in [Markup::SlackMrkdwn, Markup::DiscordMarkdown] { - 736
let mm = markup; - 737
let label = format!("md_chunk_{i}"); - 738
let surface = surface.clone(); - 739
scenarios.push(tc(&label, move || { - 740
let source = &"line.\n\n".repeat(20); - 741
let job = answer_job(source, mm, Some(50), DeliveryKind::Assistant, &surface); - 742
let packet = render(&job).expect("render"); - 743
assert!(packet.chunks.len() >= 2); - 744
})); - 745
} - 746
} - 747
- 748
// JSON payload has no chunks - 749
scenarios.push(tc("json_no_chunks", || { - 750
let job = answer_job( - 751
"source", - 752
Markup::Json, - 753
Some(10), - 754
DeliveryKind::Assistant, - 755
"test", - 756
); - 757
let packet = render(&job).expect("render"); - 758
assert!(packet.chunks.is_empty() || packet.chunks.iter().all(|c| c.is_empty())); - 759
})); - 760
- 761
run("chunking", scenarios); - 762
} - 763
- 764
// ════════════════════════════════════════════════════════════════════ - 765
// 6. TEMPLATES - 766
// ════════════════════════════════════════════════════════════════════ - 767
- 768
#[test] - 769
fn audit_templates() { - 770
let mut scenarios: Vec<Scenario> = vec![]; - 771
- 772
fn make_template( - 773
id: &str, - 774
origin: TemplateOrigin, - 775
activation: TemplateActivation, - 776
) -> TemplateSpec { - 777
TemplateSpec { - 778
id: id.into(), - 779
revision: 1, - 780
origin, - 781
activation, - 782
nodes: vec![ - 783
TemplateNode::Literal { - 784
text: "Header: ".into(), - 785
}, - 786
TemplateNode::Slot { - 787
slot: TemplateSlot::Body, - 788
}, - 789
TemplateNode::Literal { - 790
text: "\n---\n".into(), - 791
}, - 792
TemplateNode::Slot { - 793
slot: TemplateSlot::SourceMarkdown, - 794
}, - 795
], - 796
} - 797
} - 798
- 799
// Active template resolves - 800
scenarios.push(tc("active_resolves", || { - 801
let mut reg = TemplateRegistry::default(); - 802
reg.upsert(make_template( - 803
"t1", - 804
TemplateOrigin::User, - 805
TemplateActivation::Active, - 806
)) - 807
.unwrap(); - 808
assert!(reg.resolve("t1").is_some()); - 809
})); - 810
- 811
// Proposed template does not resolve - 812
scenarios.push(tc("proposed_not_resolved", || { - 813
let mut reg = TemplateRegistry::default(); - 814
reg.upsert(make_template( - 815
"t2", - 816
TemplateOrigin::AgentProposal, - 817
TemplateActivation::Proposed, - 818
)) - 819
.unwrap(); - 820
assert!(reg.resolve("t2").is_none()); - 821
})); - 822
- 823
// Activate proposal - 824
scenarios.push(tc("activate_proposal_success", || { - 825
let mut reg = TemplateRegistry::default(); - 826
reg.upsert(make_template( - 827
"t3", - 828
TemplateOrigin::AgentProposal, - 829
TemplateActivation::Proposed, - 830
)) - 831
.unwrap(); - 832
assert!(reg.activate_proposal("t3", 1)); - 833
assert!(reg.resolve("t3").is_some()); - 834
})); - 835
- 836
scenarios.push(tc("activate_proposal_wrong_origin_fails", || { - 837
let mut reg = TemplateRegistry::default(); - 838
reg.upsert(make_template( - 839
"t4", - 840
TemplateOrigin::User, - 841
TemplateActivation::Proposed, - 842
)) - 843
.unwrap(); - 844
assert!(!reg.activate_proposal("t4", 1)); - 845
assert!(reg.resolve("t4").is_none()); - 846
})); - 847
- 848
scenarios.push(tc("activate_proposal_wrong_revision_fails", || { - 849
let mut reg = TemplateRegistry::default(); - 850
reg.upsert(make_template( - 851
"t5", - 852
TemplateOrigin::AgentProposal, - 853
TemplateActivation::Proposed, - 854
)) - 855
.unwrap(); - 856
assert!(!reg.activate_proposal("t5", 999)); - 857
})); - 858
- 859
// Higher revision wins - 860
scenarios.push(tc("higher_revision_wins", || { - 861
let mut reg = TemplateRegistry::default(); - 862
reg.upsert(make_template( - 863
"t6", - 864
TemplateOrigin::User, - 865
TemplateActivation::Active, - 866
)) - 867
.unwrap(); - 868
let mut t2 = make_template("t6", TemplateOrigin::User, TemplateActivation::Active); - 869
t2.revision = 2; - 870
reg.upsert(t2).unwrap(); - 871
let resolved = reg.resolve("t6").unwrap(); - 872
assert_eq!(resolved.revision, 2); - 873
})); - 874
- 875
// Lower revision doesn't override - 876
scenarios.push(tc("lower_revision_does_not_downgrade", || { - 877
let mut reg = TemplateRegistry::default(); - 878
let mut t2 = make_template("t7", TemplateOrigin::User, TemplateActivation::Active); - 879
t2.revision = 2; - 880
reg.upsert(t2).unwrap(); - 881
let lower = make_template("t7", TemplateOrigin::User, TemplateActivation::Active); - 882
assert!(reg.upsert(lower).is_err()); - 883
let resolved = reg.resolve("t7").unwrap(); - 884
assert_eq!(resolved.revision, 2); - 885
})); - 886
- 887
// Upsert replaces by id + origin - 888
scenarios.push(tc("upsert_replaces_same_origin", || { - 889
let mut reg = TemplateRegistry::default(); - 890
reg.upsert(make_template( - 891
"t8", - 892
TemplateOrigin::User, - 893
TemplateActivation::Active, - 894
)) - 895
.unwrap(); - 896
let mut t2 = make_template("t8", TemplateOrigin::User, TemplateActivation::Active); - 897
t2.revision = 5; - 898
reg.upsert(t2).unwrap(); - 899
let count = reg.templates.iter().filter(|t| t.id == "t8").count(); - 900
assert_eq!(count, 1); - 901
assert_eq!(reg.resolve("t8").unwrap().revision, 5); - 902
})); - 903
- 904
// Different origins = separate entries - 905
scenarios.push(tc("different_origins_separate", || { - 906
let mut reg = TemplateRegistry::default(); - 907
reg.upsert(make_template( - 908
"t9", - 909
TemplateOrigin::User, - 910
TemplateActivation::Active, - 911
)) - 912
.unwrap(); - 913
reg.upsert(make_template( - 914
"t9", - 915
TemplateOrigin::BuiltIn, - 916
TemplateActivation::Active, - 917
)) - 918
.unwrap(); - 919
let count = reg.templates.iter().filter(|t| t.id == "t9").count(); - 920
assert_eq!(count, 2); - 921
})); - 922
- 923
// User origin overrides BuiltIn - 924
scenarios.push(tc("user_overrides_builtin", || { - 925
let mut reg = TemplateRegistry::default(); - 926
reg.upsert(make_template( - 927
"t10", - 928
TemplateOrigin::BuiltIn, - 929
TemplateActivation::Active, - 930
)) - 931
.unwrap(); - 932
reg.upsert(make_template( - 933
"t10", - 934
TemplateOrigin::User, - 935
TemplateActivation::Active, - 936
)) - 937
.unwrap(); - 938
let resolved = reg.resolve("t10").unwrap(); - 939
assert_eq!(resolved.origin, TemplateOrigin::User); - 940
})); - 941
- 942
// Project overrides BuiltIn - 943
scenarios.push(tc("project_overrides_builtin", || { - 944
let mut reg = TemplateRegistry::default(); - 945
reg.upsert(make_template( - 946
"t11", - 947
TemplateOrigin::BuiltIn, - 948
TemplateActivation::Active, - 949
)) - 950
.unwrap(); - 951
reg.upsert(make_template( - 952
"t11", - 953
TemplateOrigin::Project, - 954
TemplateActivation::Active, - 955
)) - 956
.unwrap(); - 957
let resolved = reg.resolve("t11").unwrap(); - 958
assert_eq!(resolved.origin, TemplateOrigin::Project); - 959
})); - 960
- 961
// Unknown template returns None - 962
scenarios.push(tc("resolve_unknown_returns_none", || { - 963
let reg = TemplateRegistry::default(); - 964
assert!(reg.resolve("nonexistent").is_none()); - 965
})); - 966
- 967
// All slot types are constructable - 968
scenarios.push(tc("slot_all_variants", || { - 969
let slots = [ - 970
TemplateSlot::Title, - 971
TemplateSlot::Body, - 972
TemplateSlot::SourceMarkdown, - 973
TemplateSlot::BlockCount, - 974
TemplateSlot::Metadata { - 975
key: "author".into(), - 976
}, - 977
]; - 978
for slot in slots { - 979
let template = TemplateSpec { - 980
id: "slot_test".into(), - 981
revision: 1, - 982
origin: TemplateOrigin::User, - 983
activation: TemplateActivation::Active, - 984
nodes: vec![TemplateNode::Slot { slot }], - 985
}; - 986
assert!(template.validate().is_ok()); - 987
} - 988
})); - 989
- 990
// IfPresent node - 991
scenarios.push(tc("ifpresent_constructs", || { - 992
let template = TemplateSpec { - 993
id: "if_test".into(), - 994
revision: 1, - 995
origin: TemplateOrigin::User, - 996
activation: TemplateActivation::Active, - 997
nodes: vec![TemplateNode::IfPresent { - 998
slot: TemplateSlot::Title, - 999
then_nodes: vec![TemplateNode::Literal { text: "yes".into() }], - 1000
else_nodes: vec![TemplateNode::Literal { text: "no".into() }],
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.