- 1
#![allow( - 2
clippy::unwrap_used, - 3
clippy::expect_used, - 4
clippy::panic, - 5
clippy::indexing_slicing - 6
)] - 7
- 8
//! Deep audit harness for `vak-presentation`: 500+ scenarios covering spec - 9
//! validation, binding paths, compilation, the `PresentationLibrary` - 10
//! register/activate/select/revoke lifecycle, revision proposals, semantic - 11
//! type inference, digest consistency, and the seed pack. - 12
//! - 13
//! Each scenario is a self-contained closure that panics on invariant - 14
//! violation. The harness counts pass/fail so the test report proves breadth. - 15
- 16
use serde_json::json; - 17
use std::collections::BTreeMap; - 18
use vak_presentation::{ - 19
AccessibilitySpec, Binding, CompileInput, CompiledPresentation, EmptyValue, FallbackKind, - 20
FallbackSpec, LibraryScope, MAX_DEPTH, MAX_EXPANSION_ITEMS, MAX_NODES, MAX_SPEC_BYTES, - 21
PresentationError, PresentationLibrary, PresentationOrigin, PresentationRevisionRequest, - 22
PresentationSpec, Primitive, SPEC_SCHEMA_VERSION, SpecNode, SpecValue, StoredPresentation, - 23
compile, digest, infer_semantic_type, parse_spec, propose_revision, seeds, validate_spec, - 24
}; - 25
- 26
type Scenario = (String, Box<dyn FnOnce()>); - 27
- 28
fn tc<F: FnOnce() + 'static>(name: &str, f: F) -> Scenario { - 29
(name.to_string(), Box::new(f)) - 30
} - 31
- 32
fn run(label: &str, scenarios: Vec<Scenario>) { - 33
let total = scenarios.len(); - 34
let mut passed = 0usize; - 35
let mut failures: Vec<String> = Vec::new(); - 36
for (name, test) in scenarios { - 37
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(test)) { - 38
Ok(()) => passed += 1, - 39
Err(_) => failures.push(name), - 40
} - 41
} - 42
if !failures.is_empty() { - 43
for f in &failures { - 44
eprintln!("FAIL: {label} :: {f}"); - 45
} - 46
} - 47
assert_eq!( - 48
passed, total, - 49
"{label}: {passed}/{total} passed — failures: {failures:?}" - 50
); - 51
println!(" ✓ {label}: {total} scenarios passed"); - 52
} - 53
- 54
// ── Data helpers ─────────────────────────────────────────────────── - 55
- 56
fn spec_node(prim: Primitive) -> SpecNode { - 57
SpecNode { - 58
primitive: prim, - 59
props: BTreeMap::new(), - 60
children: vec![], - 61
each: None, - 62
item: None, - 63
} - 64
} - 65
- 66
fn text_binding_node() -> SpecNode { - 67
SpecNode { - 68
primitive: Primitive::Text, - 69
props: BTreeMap::from([( - 70
"text".into(), - 71
SpecValue::Binding(Binding { - 72
path: "$.text".into(), - 73
required: false, - 74
empty: EmptyValue::EmptyText, - 75
}), - 76
)]), - 77
children: vec![], - 78
each: None, - 79
item: None, - 80
} - 81
} - 82
- 83
fn section_with_title() -> SpecNode { - 84
SpecNode { - 85
primitive: Primitive::Section, - 86
props: BTreeMap::from([( - 87
"title".into(), - 88
SpecValue::Binding(Binding { - 89
path: "$.title".into(), - 90
required: false, - 91
empty: EmptyValue::EmptyText, - 92
}), - 93
)]), - 94
children: vec![text_binding_node()], - 95
each: None, - 96
item: None, - 97
} - 98
} - 99
- 100
fn make_spec(id: &str, rev: u64, accepts: &[&str], root: SpecNode) -> PresentationSpec { - 101
PresentationSpec { - 102
schema_version: SPEC_SCHEMA_VERSION, - 103
id: id.into(), - 104
revision: rev, - 105
accepts: accepts.iter().map(|s| s.to_string()).collect(), - 106
root, - 107
fallback: FallbackSpec::default(), - 108
accessibility: AccessibilitySpec::default(), - 109
metadata: BTreeMap::new(), - 110
} - 111
} - 112
- 113
fn binding_val(path: &str, required: bool) -> SpecValue { - 114
SpecValue::Binding(Binding { - 115
path: path.into(), - 116
required, - 117
empty: EmptyValue::Omit, - 118
}) - 119
} - 120
- 121
fn ci(sem: &str, payload: serde_json::Value, fb: &str) -> CompileInput { - 122
let payload = match payload { - 123
serde_json::Value::Object(mut map) => { - 124
if !map.contains_key("text") { - 125
map.insert("text".into(), serde_json::Value::String("hello".into())); - 126
} - 127
serde_json::Value::Object(map) - 128
} - 129
other => other, - 130
}; - 131
CompileInput { - 132
semantic_type: sem.into(), - 133
payload, - 134
fallback_text: fb.into(), - 135
} - 136
} - 137
- 138
fn stored_simple( - 139
id: &str, - 140
rev: u64, - 141
accepts: &[&str], - 142
root: SpecNode, - 143
owner: &str, - 144
) -> StoredPresentation { - 145
let spec = make_spec(id, rev, accepts, root); - 146
let dg = digest(&spec).unwrap(); - 147
StoredPresentation { - 148
spec, - 149
digest: dg, - 150
origin: PresentationOrigin { - 151
scope: LibraryScope::Workspace, - 152
owner: owner.into(), - 153
plugin_id: None, - 154
generation: Some("test-1".into()), - 155
}, - 156
enabled: true, - 157
} - 158
} - 159
- 160
fn stored_builtin(id: &str, rev: u64, accepts: &[&str], root: SpecNode) -> StoredPresentation { - 161
let spec = make_spec(id, rev, accepts, root); - 162
let dg = digest(&spec).unwrap(); - 163
StoredPresentation { - 164
spec, - 165
digest: dg, - 166
origin: PresentationOrigin { - 167
scope: LibraryScope::Workspace, - 168
owner: "builtin".into(), - 169
plugin_id: None, - 170
generation: Some("seed-2".into()), - 171
}, - 172
enabled: true, - 173
} - 174
} - 175
- 176
fn stored_plugin( - 177
id: &str, - 178
rev: u64, - 179
accepts: &[&str], - 180
root: SpecNode, - 181
plugin_id: &str, - 182
) -> StoredPresentation { - 183
let spec = make_spec(id, rev, accepts, root); - 184
let dg = digest(&spec).unwrap(); - 185
StoredPresentation { - 186
spec, - 187
digest: dg, - 188
origin: PresentationOrigin { - 189
scope: LibraryScope::Workspace, - 190
owner: "plugin_owner".into(), - 191
plugin_id: Some(plugin_id.into()), - 192
generation: Some("gen-1".into()), - 193
}, - 194
enabled: true, - 195
} - 196
} - 197
- 198
const PRIMITIVES: &[Primitive] = &[ - 199
Primitive::Stack, - 200
Primitive::Row, - 201
Primitive::Group, - 202
Primitive::Section, - 203
Primitive::Divider, - 204
Primitive::Title, - 205
Primitive::Text, - 206
Primitive::RichText, - 207
Primitive::Label, - 208
Primitive::Badge, - 209
Primitive::Callout, - 210
Primitive::Quote, - 211
Primitive::List, - 212
Primitive::Checklist, - 213
Primitive::Timeline, - 214
Primitive::Steps, - 215
Primitive::KeyValue, - 216
Primitive::Table, - 217
Primitive::Metric, - 218
Primitive::Progress, - 219
Primitive::Chart, - 220
Primitive::DataGrid, - 221
Primitive::Comparison, - 222
Primitive::Image, - 223
Primitive::Audio, - 224
Primitive::Video, - 225
Primitive::File, - 226
Primitive::LinkPreview, - 227
Primitive::Gallery, - 228
Primitive::Diff, - 229
Primitive::TestMatrix, - 230
Primitive::Terminal, - 231
Primitive::Artifact, - 232
Primitive::CitationList, - 233
Primitive::Disclosure, - 234
Primitive::Filter, - 235
Primitive::Sort, - 236
Primitive::Search, - 237
Primitive::Stepper, - 238
Primitive::Timer, - 239
Primitive::Loading, - 240
Primitive::Empty, - 241
Primitive::Partial, - 242
Primitive::Error, - 243
Primitive::Unavailable, - 244
Primitive::Stale, - 245
]; - 246
- 247
// ════════════════════════════════════════════════════════════════════ - 248
// 1. SPEC VALIDATION - 249
// ════════════════════════════════════════════════════════════════════ - 250
- 251
#[test] - 252
fn audit_spec_validation() { - 253
let mut scenarios: Vec<Scenario> = vec![]; - 254
- 255
// 1a. Valid spec for each primitive (47 variants) - 256
for (i, prim) in PRIMITIVES.iter().enumerate() { - 257
let p = *prim; - 258
scenarios.push(tc(&format!("valid_spec_{i}_{p:?}"), move || { - 259
let s = make_spec(&format!("v{i}"), 1, &["detail"], spec_node(p)); - 260
assert!(validate_spec(&s).is_ok(), "primitive {p:?} should validate"); - 261
})); - 262
} - 263
- 264
// 1b. Valid complex specs - 265
scenarios.push(tc("valid_section_node", || { - 266
let s = make_spec("cs1", 1, &["detail"], section_with_title()); - 267
assert!(validate_spec(&s).is_ok()); - 268
})); - 269
- 270
scenarios.push(tc("valid_divider_node", || { - 271
let s = make_spec("cs2", 1, &["detail"], spec_node(Primitive::Divider)); - 272
assert!(validate_spec(&s).is_ok()); - 273
})); - 274
- 275
scenarios.push(tc("valid_stale_node", || { - 276
let s = make_spec("cs3", 1, &["detail"], spec_node(Primitive::Stale)); - 277
assert!(validate_spec(&s).is_ok()); - 278
})); - 279
- 280
scenarios.push(tc("valid_error_node", || { - 281
let s = make_spec("cs4", 1, &["detail"], spec_node(Primitive::Error)); - 282
assert!(validate_spec(&s).is_ok()); - 283
})); - 284
- 285
scenarios.push(tc("valid_loading_node", || { - 286
let s = make_spec("cs5", 1, &["detail"], spec_node(Primitive::Loading)); - 287
assert!(validate_spec(&s).is_ok()); - 288
})); - 289
- 290
scenarios.push(tc("valid_empty_node", || { - 291
let s = make_spec("cs6", 1, &["detail"], spec_node(Primitive::Empty)); - 292
assert!(validate_spec(&s).is_ok()); - 293
})); - 294
- 295
scenarios.push(tc("valid_partial_node", || { - 296
let s = make_spec("cs7", 1, &["detail"], spec_node(Primitive::Partial)); - 297
assert!(validate_spec(&s).is_ok()); - 298
})); - 299
- 300
scenarios.push(tc("valid_richtext_node", || { - 301
let s = make_spec("cs8", 1, &["detail"], spec_node(Primitive::RichText)); - 302
assert!(validate_spec(&s).is_ok()); - 303
})); - 304
- 305
scenarios.push(tc("valid_nested_stack", || { - 306
let root = SpecNode { - 307
primitive: Primitive::Stack, - 308
props: BTreeMap::new(), - 309
children: vec![spec_node(Primitive::Text), spec_node(Primitive::Divider)], - 310
each: None, - 311
item: None, - 312
}; - 313
let s = make_spec("cs9", 1, &["detail"], root); - 314
assert!(validate_spec(&s).is_ok()); - 315
})); - 316
- 317
scenarios.push(tc("valid_section_no_title", || { - 318
let root = SpecNode { - 319
primitive: Primitive::Section, - 320
props: BTreeMap::new(), - 321
children: vec![text_binding_node()], - 322
each: None, - 323
item: None, - 324
}; - 325
let s = make_spec("cs10", 1, &["detail"], root); - 326
assert!(validate_spec(&s).is_ok()); - 327
})); - 328
- 329
// 1c. Invalid schema versions - 330
for bad_sv in [0u16, 2, 3, 65535] { - 331
scenarios.push(tc(&format!("invalid_schema_{bad_sv}"), move || { - 332
let mut s = make_spec("bad_sv", 1, &["detail"], text_binding_node()); - 333
s.schema_version = bad_sv; - 334
assert!(matches!(validate_spec(&s), Err(PresentationError::UnsupportedSchema(v)) if v == bad_sv)); - 335
})); - 336
} - 337
- 338
// 1d. Empty / whitespace / too-long id - 339
scenarios.push(tc("empty_id_invalid", || { - 340
let s = make_spec("", 1, &["detail"], text_binding_node()); - 341
assert!(matches!( - 342
validate_spec(&s), - 343
Err(PresentationError::InvalidSpec(_)) - 344
)); - 345
})); - 346
- 347
scenarios.push(tc("whitespace_id_invalid", || { - 348
let s = make_spec(" ", 1, &["detail"], text_binding_node()); - 349
assert!(matches!( - 350
validate_spec(&s), - 351
Err(PresentationError::InvalidSpec(_)) - 352
)); - 353
})); - 354
- 355
scenarios.push(tc("too_long_id_invalid", || { - 356
let long_id = "x".repeat(257); - 357
let s = make_spec(&long_id, 1, &["detail"], text_binding_node()); - 358
assert!(matches!( - 359
validate_spec(&s), - 360
Err(PresentationError::InvalidSpec(_)) - 361
)); - 362
})); - 363
- 364
// 1e. Zero revision - 365
scenarios.push(tc("zero_revision_invalid", || { - 366
let s = make_spec("zr", 0, &["detail"], text_binding_node()); - 367
assert!(matches!( - 368
validate_spec(&s), - 369
Err(PresentationError::InvalidSpec(_)) - 370
)); - 371
})); - 372
- 373
// 1f. Empty accepts entries - 374
scenarios.push(tc("empty_accept_entry_invalid", || { - 375
let s = make_spec("bad_a", 1, &[""], text_binding_node()); - 376
assert!(matches!( - 377
validate_spec(&s), - 378
Err(PresentationError::InvalidSpec(_)) - 379
)); - 380
})); - 381
- 382
scenarios.push(tc("whitespace_accept_entry_invalid", || { - 383
let s = make_spec("bad_a2", 1, &[" "], text_binding_node()); - 384
assert!(matches!( - 385
validate_spec(&s), - 386
Err(PresentationError::InvalidSpec(_)) - 387
)); - 388
})); - 389
- 390
// 1g. Too many nodes - 391
for extra in [0usize, 1, 5, 50] { - 392
let target = MAX_NODES + extra; - 393
scenarios.push(tc(&format!("too_many_nodes_{target}"), move || { - 394
let mut children = Vec::new(); - 395
for i in 0..target { - 396
children.push(SpecNode { - 397
primitive: Primitive::Text, - 398
props: BTreeMap::from([( - 399
"text".into(), - 400
SpecValue::Text { - 401
value: format!("n{i}"), - 402
}, - 403
)]), - 404
children: vec![], - 405
each: None, - 406
item: None, - 407
}); - 408
} - 409
let root = SpecNode { - 410
primitive: Primitive::Stack, - 411
props: BTreeMap::new(), - 412
children, - 413
each: None, - 414
item: None, - 415
}; - 416
let s = make_spec("too_many", 1, &["detail"], root); - 417
assert!(matches!( - 418
validate_spec(&s), - 419
Err(PresentationError::Limit(_)) - 420
)); - 421
})); - 422
} - 423
- 424
// 1h. Too deep nesting - 425
for extra_depth in [0usize, 1, 5] { - 426
let depth = MAX_DEPTH + 1 + extra_depth; - 427
scenarios.push(tc(&format!("too_deep_{depth}"), move || { - 428
let mut node = text_binding_node(); - 429
for _ in 0..depth { - 430
node = SpecNode { - 431
primitive: Primitive::Stack, - 432
props: BTreeMap::new(), - 433
children: vec![node], - 434
each: None, - 435
item: None, - 436
}; - 437
} - 438
let s = make_spec("too_deep", 1, &["detail"], node); - 439
assert!(matches!( - 440
validate_spec(&s), - 441
Err(PresentationError::Limit(_)) - 442
)); - 443
})); - 444
} - 445
- 446
// 1i. Each without item - 447
scenarios.push(tc("each_without_item_invalid", || { - 448
let root = SpecNode { - 449
primitive: Primitive::Stack, - 450
props: BTreeMap::new(), - 451
children: vec![], - 452
each: Some(Binding { - 453
path: "$.items".into(), - 454
required: false, - 455
empty: EmptyValue::Omit, - 456
}), - 457
item: None, - 458
}; - 459
let s = make_spec("no_item", 1, &["detail"], root); - 460
assert!(matches!( - 461
validate_spec(&s), - 462
Err(PresentationError::InvalidSpec(_)) - 463
)); - 464
})); - 465
- 466
// 1j. Each with item is valid - 467
scenarios.push(tc("each_with_item_valid", || { - 468
let root = SpecNode { - 469
primitive: Primitive::Stack, - 470
props: BTreeMap::new(), - 471
children: vec![], - 472
each: Some(Binding { - 473
path: "$.items".into(), - 474
required: false, - 475
empty: EmptyValue::Omit, - 476
}), - 477
item: Some(Box::new(text_binding_node())), - 478
}; - 479
let s = make_spec("with_item", 1, &["detail"], root); - 480
assert!(validate_spec(&s).is_ok()); - 481
})); - 482
- 483
// 1k. Non-finite number (validation doesn't serialize, so should pass) - 484
for n_val in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] { - 485
let n = n_val; - 486
scenarios.push(tc(&format!("non_finite_number_{n:?}"), move || { - 487
let mut props = BTreeMap::new(); - 488
props.insert("value".into(), SpecValue::Number { value: n }); - 489
let root = SpecNode { - 490
primitive: Primitive::Metric, - 491
props, - 492
children: vec![], - 493
each: None, - 494
item: None, - 495
}; - 496
let s = make_spec("non_finite", 1, &["metric"], root); - 497
let result = validate_spec(&s); - 498
// validate_spec doesn't serialize, but compile does (via digest) - 499
assert!(result.is_ok()); - 500
})); - 501
} - 502
- 503
run("spec_validation", scenarios); - 504
} - 505
- 506
// ════════════════════════════════════════════════════════════════════ - 507
// 2. BINDING PATHS - 508
// ════════════════════════════════════════════════════════════════════ - 509
- 510
#[test] - 511
fn audit_binding_paths() { - 512
let mut scenarios: Vec<Scenario> = vec![]; - 513
- 514
let valid_paths: &[&str] = &[ - 515
"$", - 516
"$.title", - 517
"$.title.subtitle", - 518
"$.items[0]", - 519
"$.items[0].name", - 520
"$.items[10].name", - 521
"$.data[999]", - 522
"$.a.b.c.d.e.f.g", - 523
"$.a-b_c", - 524
"$.a1.b2.c3", - 525
"$[0]", - 526
"$[0][1]", - 527
"$.items[0][1]", - 528
"$.very.long.path.with.many.segments[0][1][2]", - 529
]; - 530
- 531
for path in valid_paths { - 532
let p = path.to_string(); - 533
scenarios.push(tc(&format!("valid_path_{p}"), move || { - 534
let root = SpecNode { - 535
primitive: Primitive::Text, - 536
props: BTreeMap::from([("text".into(), binding_val(&p, false))]), - 537
children: vec![], - 538
each: None, - 539
item: None, - 540
}; - 541
let s = make_spec("vb", 1, &["detail"], root); - 542
assert!(validate_spec(&s).is_ok(), "path {p:?} should be valid"); - 543
})); - 544
} - 545
- 546
let invalid_paths: &[&str] = &[ - 547
"title", - 548
"$.items[-1]", - 549
"$.items[abc]", - 550
"$.items[]", - 551
"$['title']", - 552
"$.items[", - 553
"$.items]", - 554
"$.items[0", - 555
"$..title", - 556
"$.title + 1", - 557
"$.items[0].", - 558
"$.items[0]..name", - 559
"./title", - 560
"$#comment", - 561
"$.items[1.5]", - 562
"$.items[0x]", - 563
"$.items[+1]", - 564
"$.a..b", - 565
"$.a.b.", - 566
"$.a[0]b", - 567
"$/.[0]", - 568
"$.[items]", - 569
"$/", - 570
"$.items[0]extra", - 571
"$.items]", - 572
"$a", - 573
]; - 574
- 575
for path in invalid_paths { - 576
let p = path.to_string(); - 577
scenarios.push(tc(&format!("invalid_path_{p:?}"), move || { - 578
let root = SpecNode { - 579
primitive: Primitive::Text, - 580
props: BTreeMap::from([("text".into(), binding_val(&p, false))]), - 581
children: vec![], - 582
each: None, - 583
item: None, - 584
}; - 585
let s = make_spec("ib", 1, &["detail"], root); - 586
let result = validate_spec(&s); - 587
assert!( - 588
matches!(result, Err(PresentationError::InvalidBinding(_))), - 589
"path {p:?}: {result:?}" - 590
); - 591
})); - 592
} - 593
- 594
// Path too long (>512 chars) - 595
scenarios.push(tc("path_513_chars_invalid", || { - 596
let long_path = format!("$.{}", "a.".repeat(300)); - 597
let root = SpecNode { - 598
primitive: Primitive::Text, - 599
props: BTreeMap::from([("text".into(), binding_val(&long_path, false))]), - 600
children: vec![], - 601
each: None, - 602
item: None, - 603
}; - 604
let s = make_spec("pl", 1, &["detail"], root); - 605
assert!(matches!( - 606
validate_spec(&s), - 607
Err(PresentationError::InvalidBinding(_)) - 608
)); - 609
})); - 610
- 611
// Empty path - 612
scenarios.push(tc("empty_binding_path_invalid", || { - 613
let root = SpecNode { - 614
primitive: Primitive::Text, - 615
props: BTreeMap::from([("text".into(), binding_val("", false))]), - 616
children: vec![], - 617
each: None, - 618
item: None, - 619
}; - 620
let s = make_spec("ep", 1, &["detail"], root); - 621
assert!(matches!( - 622
validate_spec(&s), - 623
Err(PresentationError::InvalidBinding(_)) - 624
)); - 625
})); - 626
- 627
// Required binding absent → Fallback (via compile) - 628
scenarios.push(tc("required_binding_absent_fallback", || { - 629
let root = SpecNode { - 630
primitive: Primitive::Text, - 631
props: BTreeMap::from([("text".into(), binding_val("$.missing", true))]), - 632
children: vec![], - 633
each: None, - 634
item: None, - 635
}; - 636
let s = make_spec("req_absent", 1, &["detail"], root); - 637
let result = compile(&s, &ci("detail", json!({}), "fb")); - 638
assert!(matches!(result, CompiledPresentation::Fallback { .. })); - 639
})); - 640
- 641
// Optional binding absent with Omit → resolved as None, node still Rich - 642
scenarios.push(tc("optional_binding_missing_omit", || { - 643
let root = SpecNode { - 644
primitive: Primitive::Text, - 645
props: BTreeMap::from([( - 646
"text".into(), - 647
SpecValue::Binding(Binding { - 648
path: "$.missing".into(), - 649
required: false, - 650
empty: EmptyValue::Omit, - 651
}), - 652
)]), - 653
children: vec![], - 654
each: None, - 655
item: None, - 656
}; - 657
let s = make_spec("opt_omit", 1, &["detail"], root); - 658
assert!(validate_spec(&s).is_ok()); - 659
let result = compile(&s, &ci("detail", json!({}), "fb")); - 660
assert!(matches!(result, CompiledPresentation::Rich(_))); - 661
})); - 662
- 663
// Optional binding absent with EmptyText → resolved as empty string - 664
scenarios.push(tc("optional_binding_missing_empty_text", || { - 665
let root = SpecNode { - 666
primitive: Primitive::Text, - 667
props: BTreeMap::from([( - 668
"text".into(), - 669
SpecValue::Binding(Binding { - 670
path: "$.missing".into(), - 671
required: false, - 672
empty: EmptyValue::EmptyText, - 673
}), - 674
)]), - 675
children: vec![], - 676
each: None, - 677
item: None, - 678
}; - 679
let s = make_spec("opt_et", 1, &["detail"], root); - 680
assert!(validate_spec(&s).is_ok()); - 681
})); - 682
- 683
// Optional binding absent with EmptyList → resolves as empty array - 684
scenarios.push(tc("optional_binding_missing_empty_list", || { - 685
let root = SpecNode { - 686
primitive: Primitive::Stack, - 687
props: BTreeMap::new(), - 688
children: vec![], - 689
each: Some(Binding { - 690
path: "$.missing".into(), - 691
required: false, - 692
empty: EmptyValue::EmptyList, - 693
}), - 694
item: Some(Box::new(text_binding_node())), - 695
}; - 696
let s = make_spec("opt_el", 1, &["detail"], root); - 697
assert!(validate_spec(&s).is_ok()); - 698
})); - 699
- 700
// Each with non-array data → Fallback - 701
scenarios.push(tc("each_non_array_fallback", || { - 702
let root = SpecNode { - 703
primitive: Primitive::Stack, - 704
props: BTreeMap::new(), - 705
children: vec![], - 706
each: Some(Binding { - 707
path: "$.items".into(), - 708
required: false, - 709
empty: EmptyValue::Omit, - 710
}), - 711
item: Some(Box::new(text_binding_node())), - 712
}; - 713
let s = make_spec("each_na", 1, &["detail"], root); - 714
let result = compile(&s, &ci("detail", json!({"items": "not_an_array"}), "fb")); - 715
assert!(matches!(result, CompiledPresentation::Fallback { .. })); - 716
})); - 717
- 718
// Each expansion exceeding MAX_EXPANSION_ITEMS - 719
scenarios.push(tc("each_exceeds_max_expansion_fallback", || { - 720
let root = SpecNode { - 721
primitive: Primitive::Stack, - 722
props: BTreeMap::new(), - 723
children: vec![], - 724
each: Some(Binding { - 725
path: "$.items".into(), - 726
required: false, - 727
empty: EmptyValue::Omit, - 728
}), - 729
item: Some(Box::new(text_binding_node())), - 730
}; - 731
let s = make_spec("each_over", 1, &["detail"], root); - 732
let items: Vec<String> = (0..(MAX_EXPANSION_ITEMS + 10)) - 733
.map(|i| format!("i{i}")) - 734
.collect(); - 735
let result = compile(&s, &ci("detail", json!({"items": items}), "fb")); - 736
assert!(matches!(result, CompiledPresentation::Fallback { .. })); - 737
})); - 738
- 739
// Nested each valid - 740
scenarios.push(tc("nested_each_valid", || { - 741
let inner = SpecNode { - 742
primitive: Primitive::Stack, - 743
props: BTreeMap::new(), - 744
children: vec![], - 745
each: Some(Binding { - 746
path: "$.items".into(), - 747
required: false, - 748
empty: EmptyValue::Omit, - 749
}), - 750
item: Some(Box::new(text_binding_node())), - 751
}; - 752
let root = SpecNode { - 753
primitive: Primitive::Stack, - 754
props: BTreeMap::new(), - 755
children: vec![inner], - 756
each: None, - 757
item: None, - 758
}; - 759
let s = make_spec("nested_each", 1, &["detail"], root); - 760
assert!(validate_spec(&s).is_ok()); - 761
})); - 762
- 763
run("binding_paths", scenarios); - 764
} - 765
- 766
// ════════════════════════════════════════════════════════════════════ - 767
// 3. COMPILATION - 768
// ════════════════════════════════════════════════════════════════════ - 769
- 770
#[test] - 771
fn audit_compilation() { - 772
let mut scenarios: Vec<Scenario> = vec![]; - 773
- 774
// 3a. Valid compilation for each primitive - 775
for (i, prim) in PRIMITIVES.iter().enumerate() { - 776
let p = *prim; - 777
scenarios.push(tc(&format!("compile_{i}_{p:?}"), move || { - 778
let s = make_spec(&format!("c{i}"), 1, &["detail"], spec_node(p)); - 779
let result = compile(&s, &ci("detail", json!({"title":"T","items":[1]}), "fb")); - 780
assert!( - 781
matches!(result, CompiledPresentation::Rich(_)), - 782
"compile {prim:?}: {result:?}" - 783
); - 784
})); - 785
} - 786
- 787
// 3b. Semantic type mismatch → Fallback - 788
for (i, prim) in PRIMITIVES.iter().enumerate() { - 789
let p = *prim; - 790
scenarios.push(tc(&format!("mismatch_{i}_{p:?}"), move || { - 791
let s = make_spec(&format!("m{i}"), 1, &["comparison"], spec_node(p)); - 792
let result = compile(&s, &ci("detail", json!({}), "fb")); - 793
assert!( - 794
matches!(result, CompiledPresentation::Fallback { ref reason, .. } if reason.contains("does not match")), - 795
"mismatch {prim:?}: {result:?}" - 796
); - 797
})); - 798
} - 799
- 800
// 3c. Each with collection expansion at various sizes - 801
for size in [0usize, 1, 2, 5, 10, 50, 100, 255, 256] { - 802
let sz = size; - 803
scenarios.push(tc(&format!("each_size_{sz}"), move || { - 804
let root = SpecNode { - 805
primitive: Primitive::Stack, - 806
props: BTreeMap::new(), - 807
children: vec![], - 808
each: Some(Binding { - 809
path: "$.items".into(), - 810
required: false, - 811
empty: EmptyValue::Omit, - 812
}), - 813
item: Some(Box::new(text_binding_node())), - 814
}; - 815
let s = make_spec("each", 1, &["detail"], root); - 816
let payload = - 817
json!({"items": (0..sz).map(|j| json!(format!("i{j}"))).collect::<Vec<_>>()}); - 818
let result = compile(&s, &ci("detail", payload, "fb")); - 819
assert!( - 820
matches!(result, CompiledPresentation::Rich(_)), - 821
"each size {sz}: {result:?}" - 822
); - 823
})); - 824
} - 825
- 826
// 3d. FallbackKind Document vs Plain - 827
for (fk, label) in [ - 828
(FallbackKind::Document, "doc"), - 829
(FallbackKind::Plain, "plain"), - 830
] { - 831
let lf = label.to_string(); - 832
let fk_val = fk; - 833
scenarios.push(tc(&format!("fallback_kind_{lf}"), move || { - 834
let root = SpecNode { - 835
primitive: Primitive::Title, - 836
props: BTreeMap::from([("title".into(), binding_val("$.missing", true))]), - 837
children: vec![], - 838
each: None, - 839
item: None, - 840
}; - 841
let s = PresentationSpec { - 842
schema_version: SPEC_SCHEMA_VERSION, - 843
id: format!("fb_{lf}"), - 844
revision: 1, - 845
accepts: vec!["detail".into()], - 846
root, - 847
fallback: FallbackSpec { kind: fk_val }, - 848
accessibility: AccessibilitySpec::default(), - 849
metadata: BTreeMap::new(), - 850
}; - 851
let result = compile(&s, &ci("detail", json!({}), "fallback text")); - 852
assert!(matches!(result, CompiledPresentation::Fallback { .. })); - 853
})); - 854
} - 855
- 856
// 3e. Deep nesting within MAX_DEPTH compiles - 857
scenarios.push(tc("deep_nesting_within_depth_compiles", || { - 858
let mut node = text_binding_node(); - 859
for _ in 0..(MAX_DEPTH - 2) { - 860
node = SpecNode { - 861
primitive: Primitive::Stack, - 862
props: BTreeMap::new(), - 863
children: vec![node], - 864
each: None, - 865
item: None, - 866
}; - 867
} - 868
let s = make_spec("deep_ok", 1, &["detail"], node); - 869
let result = compile(&s, &ci("detail", json!({}), "fb")); - 870
assert!(matches!(result, CompiledPresentation::Rich(_))); - 871
})); - 872
- 873
// 3f. Deep nesting exceeding depth at compile → Fallback - 874
scenarios.push(tc("deep_nesting_exceeds_depth_fallback", || { - 875
let mut node = text_binding_node(); - 876
for _ in 0..(MAX_DEPTH + 5) { - 877
node = SpecNode { - 878
primitive: Primitive::Stack, - 879
props: BTreeMap::new(), - 880
children: vec![node], - 881
each: None, - 882
item: None, - 883
}; - 884
} - 885
let s = make_spec("deep_bad", 1, &["detail"], node); - 886
let result = compile(&s, &ci("detail", json!({}), "fb")); - 887
assert!(matches!(result, CompiledPresentation::Fallback { .. })); - 888
})); - 889
- 890
// 3g. Accessibility summary resolved - 891
scenarios.push(tc("accessibility_summary_resolved", || { - 892
let root = section_with_title(); - 893
let mut s = make_spec("acc1", 1, &["detail"], root); - 894
s.accessibility.summary = Some(Binding { - 895
path: "$.title".into(), - 896
required: false, - 897
empty: EmptyValue::EmptyText, - 898
}); - 899
let result = compile(&s, &ci("detail", json!({"title": "Hello World"}), "fb")); - 900
assert!(matches!(result, CompiledPresentation::Rich(_))); - 901
})); - 902
- 903
// 3h. Accessibility summary missing → ok - 904
scenarios.push(tc("accessibility_summary_missing_ok", || { - 905
let s = make_spec("acc2", 1, &["detail"], section_with_title()); - 906
let result = compile(&s, &ci("detail", json!({}), "fb")); - 907
assert!(matches!(result, CompiledPresentation::Rich(_))); - 908
})); - 909
- 910
// 3i. Edge payloads - 911
scenarios.push(tc("empty_payload_omits_optional_bindings", || { - 912
let root = SpecNode { - 913
primitive: Primitive::Text, - 914
props: BTreeMap::from([("text".into(), binding_val("$.missing", false))]), - 915
children: vec![], - 916
each: None, - 917
item: None, - 918
}; - 919
let s = make_spec("empty", 1, &["detail"], root); - 920
let result = compile(&s, &ci("detail", json!({}), "fb")); - 921
assert!(matches!(result, CompiledPresentation::Rich(_))); - 922
})); - 923
- 924
scenarios.push(tc("unicode_in_text", || { - 925
let root = SpecNode { - 926
primitive: Primitive::Section, - 927
props: BTreeMap::from([("title".into(), binding_val("$.title", false))]), - 928
children: vec![], - 929
each: None, - 930
item: None, - 931
}; - 932
let s = make_spec("unicode", 1, &["detail"], root); - 933
let result = compile(&s, &ci("detail", json!({"title": "Hello 世界 🌍"}), "fb")); - 934
assert!(matches!(result, CompiledPresentation::Rich(_))); - 935
})); - 936
- 937
scenarios.push(tc("special_chars_in_text", || { - 938
let root = SpecNode { - 939
primitive: Primitive::Section, - 940
props: BTreeMap::from([("title".into(), binding_val("$.title", false))]), - 941
children: vec![], - 942
each: None, - 943
item: None, - 944
}; - 945
let s = make_spec("specialchars", 1, &["detail"], root); - 946
let result = compile(&s, &ci("detail", json!({"title": "<>&\"'`"}), "fb")); - 947
assert!(matches!(result, CompiledPresentation::Rich(_))); - 948
})); - 949
- 950
scenarios.push(tc("hostile_massive_payload_no_panic", || { - 951
let big_array: Vec<String> = (0..10000).map(|i| format!("x{i}")).collect(); - 952
let s = make_spec("huge", 1, &["detail"], section_with_title()); - 953
let result = compile( - 954
&s, - 955
&ci("detail", json!({"title": "Huge", "items": big_array}), "fb"), - 956
); - 957
assert!(matches!(result, CompiledPresentation::Rich(_))); - 958
})); - 959
- 960
scenarios.push(tc("hostile_deeply_nested_json_no_panic", || { - 961
let nested = serde_json::Value::Array((0..100).map(|i| json!({"v": i})).collect()); - 962
let s = make_spec("deep_json", 1, &["detail"], section_with_title()); - 963
let result = compile( - 964
&s, - 965
&ci("detail", json!({"title": "Deep", "items": nested}), "fb"), - 966
); - 967
assert!(matches!(result, CompiledPresentation::Rich(_))); - 968
})); - 969
- 970
scenarios.push(tc("hostile_null_payload_fallback", || { - 971
let root = SpecNode { - 972
primitive: Primitive::Text, - 973
props: BTreeMap::from([("text".into(), binding_val("$.title", true))]), - 974
children: vec![], - 975
each: None, - 976
item: None, - 977
}; - 978
let s = make_spec("null_pl", 1, &["detail"], root); - 979
let result = compile(&s, &ci("detail", serde_json::Value::Null, "fb")); - 980
assert!(matches!(result, CompiledPresentation::Fallback { .. })); - 981
})); - 982
- 983
scenarios.push(tc("hostile_array_payload_fallback", || { - 984
let root = SpecNode { - 985
primitive: Primitive::Section, - 986
props: BTreeMap::from([("title".into(), binding_val("$.title", true))]), - 987
children: vec![], - 988
each: None, - 989
item: None, - 990
}; - 991
let s = make_spec("arr_pl", 1, &["detail"], root); - 992
let result = compile(&s, &ci("detail", serde_json::Value::Array(vec![]), "fb")); - 993
assert!(matches!(result, CompiledPresentation::Fallback { .. })); - 994
})); - 995
- 996
scenarios.push(tc("hostile_string_payload_fallback", || { - 997
let root = SpecNode { - 998
primitive: Primitive::Section, - 999
props: BTreeMap::from([("title".into(), binding_val("$.title", true))]), - 1000
children: vec![],
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.