- 1001
each: None, - 1002
item: None, - 1003
}; - 1004
let s = make_spec("str_pl", 1, &["detail"], root); - 1005
let result = compile( - 1006
&s, - 1007
&ci("detail", serde_json::Value::String("not obj".into()), "fb"), - 1008
); - 1009
assert!(matches!(result, CompiledPresentation::Fallback { .. })); - 1010
})); - 1011
- 1012
scenarios.push(tc("hostile_null_bytes_in_text", || { - 1013
let root = SpecNode { - 1014
primitive: Primitive::Section, - 1015
props: BTreeMap::from([("title".into(), binding_val("$.title", false))]), - 1016
children: vec![], - 1017
each: None, - 1018
item: None, - 1019
}; - 1020
let s = make_spec("nullb", 1, &["detail"], root); - 1021
let result = compile(&s, &ci("detail", json!({"title": "hello\0world"}), "fb")); - 1022
assert!(matches!(result, CompiledPresentation::Rich(_))); - 1023
})); - 1024
- 1025
scenarios.push(tc("hostile_emoji_in_text", || { - 1026
let root = SpecNode { - 1027
primitive: Primitive::Section, - 1028
props: BTreeMap::from([("title".into(), binding_val("$.title", false))]), - 1029
children: vec![], - 1030
each: None, - 1031
item: None, - 1032
}; - 1033
let s = make_spec("emoji", 1, &["detail"], root); - 1034
let result = compile(&s, &ci("detail", json!({"title": "ππβ "}), "fb")); - 1035
assert!(matches!(result, CompiledPresentation::Rich(_))); - 1036
})); - 1037
- 1038
scenarios.push(tc("hostile_control_chars_in_text", || { - 1039
let root = SpecNode { - 1040
primitive: Primitive::Section, - 1041
props: BTreeMap::from([("title".into(), binding_val("$.title", false))]), - 1042
children: vec![], - 1043
each: None, - 1044
item: None, - 1045
}; - 1046
let s = make_spec("ctl", 1, &["detail"], root); - 1047
let result = compile(&s, &ci("detail", json!({"title": "\t\n\r"}), "fb")); - 1048
assert!(matches!(result, CompiledPresentation::Rich(_))); - 1049
})); - 1050
- 1051
scenarios.push(tc("hostile_array_index_out_of_range", || { - 1052
let root = SpecNode { - 1053
primitive: Primitive::Text, - 1054
props: BTreeMap::from([("text".into(), binding_val("$.items[999]", false))]), - 1055
children: vec![], - 1056
each: None, - 1057
item: None, - 1058
}; - 1059
let s = make_spec("oor", 1, &["detail"], root); - 1060
let result = compile(&s, &ci("detail", json!({"items": [1, 2, 3]}), "fb")); - 1061
assert!(matches!( - 1062
result, - 1063
CompiledPresentation::Rich(_) | CompiledPresentation::Fallback { .. } - 1064
)); - 1065
})); - 1066
- 1067
scenarios.push(tc("hostile_empty_array_each", || { - 1068
let root = SpecNode { - 1069
primitive: Primitive::Stack, - 1070
props: BTreeMap::new(), - 1071
children: vec![], - 1072
each: Some(Binding { - 1073
path: "$.items".into(), - 1074
required: false, - 1075
empty: EmptyValue::EmptyList, - 1076
}), - 1077
item: Some(Box::new(text_binding_node())), - 1078
}; - 1079
let s = make_spec("ea", 1, &["detail"], root); - 1080
let result = compile(&s, &ci("detail", json!({"items": []}), "fb")); - 1081
assert!(matches!(result, CompiledPresentation::Rich(_))); - 1082
})); - 1083
- 1084
run("compilation", scenarios); - 1085
} - 1086
- 1087
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ - 1088
// 4. DIGEST CONSISTENCY - 1089
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ - 1090
- 1091
#[test] - 1092
fn audit_digests() { - 1093
let mut scenarios: Vec<Scenario> = vec![]; - 1094
- 1095
scenarios.push(tc("identical_specs_same_digest", || { - 1096
let s1 = make_spec("d1", 1, &["detail"], section_with_title()); - 1097
let s2 = make_spec("d1", 1, &["detail"], section_with_title()); - 1098
assert_eq!(digest(&s1).unwrap(), digest(&s2).unwrap()); - 1099
})); - 1100
- 1101
scenarios.push(tc("different_title_different_digest", || { - 1102
let root1 = SpecNode { - 1103
primitive: Primitive::Section, - 1104
props: BTreeMap::from([("title".into(), binding_val("$.a", false))]), - 1105
children: vec![], - 1106
each: None, - 1107
item: None, - 1108
}; - 1109
let root2 = SpecNode { - 1110
primitive: Primitive::Section, - 1111
props: BTreeMap::from([("title".into(), binding_val("$.b", false))]), - 1112
children: vec![], - 1113
each: None, - 1114
item: None, - 1115
}; - 1116
let s1 = make_spec("d2", 1, &["detail"], root1); - 1117
let s2 = make_spec("d2", 1, &["detail"], root2); - 1118
assert_ne!(digest(&s1).unwrap(), digest(&s2).unwrap()); - 1119
})); - 1120
- 1121
scenarios.push(tc("different_id_different_digest", || { - 1122
let s1 = make_spec("id_a", 1, &["detail"], section_with_title()); - 1123
let s2 = make_spec("id_b", 1, &["detail"], section_with_title()); - 1124
assert_ne!(digest(&s1).unwrap(), digest(&s2).unwrap()); - 1125
})); - 1126
- 1127
scenarios.push(tc("different_revision_different_digest", || { - 1128
let s1 = make_spec("rev", 1, &["detail"], section_with_title()); - 1129
let s2 = make_spec("rev", 2, &["detail"], section_with_title()); - 1130
assert_ne!(digest(&s1).unwrap(), digest(&s2).unwrap()); - 1131
})); - 1132
- 1133
scenarios.push(tc("different_primitive_different_digest", || { - 1134
let s1 = make_spec("prim", 1, &["detail"], spec_node(Primitive::Stack)); - 1135
let s2 = make_spec("prim", 1, &["detail"], spec_node(Primitive::Row)); - 1136
assert_ne!(digest(&s1).unwrap(), digest(&s2).unwrap()); - 1137
})); - 1138
- 1139
scenarios.push(tc("json_roundtrip_preserves_digest", || { - 1140
let s = make_spec("rt", 1, &["detail"], section_with_title()); - 1141
let d1 = digest(&s).unwrap(); - 1142
let bytes = serde_json::to_vec(&s).unwrap(); - 1143
let parsed: PresentationSpec = serde_json::from_slice(&bytes).unwrap(); - 1144
let d2 = digest(&parsed).unwrap(); - 1145
assert_eq!(d1, d2); - 1146
})); - 1147
- 1148
scenarios.push(tc("digest_is_stable_across_calls", || { - 1149
let s = make_spec("stable", 1, &["detail"], section_with_title()); - 1150
let d1 = digest(&s).unwrap(); - 1151
let d2 = digest(&s).unwrap(); - 1152
let d3 = digest(&s).unwrap(); - 1153
assert_eq!(d1, d2); - 1154
assert_eq!(d2, d3); - 1155
})); - 1156
- 1157
scenarios.push(tc("digest_is_hex_64_chars", || { - 1158
let s = make_spec("hex", 1, &["detail"], section_with_title()); - 1159
let d = digest(&s).unwrap(); - 1160
assert_eq!(d.len(), 64); - 1161
assert!(d.chars().all(|c| c.is_ascii_hexdigit())); - 1162
})); - 1163
- 1164
scenarios.push(tc("digest_complex_spec_ok", || { - 1165
let mut props = BTreeMap::new(); - 1166
props.insert("title".into(), binding_val("$.title", false)); - 1167
props.insert( - 1168
"items".into(), - 1169
SpecValue::Binding(Binding { - 1170
path: "$.items".into(), - 1171
required: false, - 1172
empty: EmptyValue::EmptyList, - 1173
}), - 1174
); - 1175
let root = SpecNode { - 1176
primitive: Primitive::Section, - 1177
props, - 1178
children: vec![SpecNode { - 1179
primitive: Primitive::List, - 1180
props: BTreeMap::new(), - 1181
children: vec![], - 1182
each: Some(Binding { - 1183
path: "$.items".into(), - 1184
required: false, - 1185
empty: EmptyValue::Omit, - 1186
}), - 1187
item: Some(Box::new(text_binding_node())), - 1188
}], - 1189
each: None, - 1190
item: None, - 1191
}; - 1192
let s = make_spec("complex", 1, &["detail", "comparison"], root); - 1193
assert!(digest(&s).is_ok()); - 1194
})); - 1195
- 1196
scenarios.push(tc("static_spec_values_round_trip", || { - 1197
let root = SpecNode { - 1198
primitive: Primitive::Text, - 1199
props: BTreeMap::from([ - 1200
( - 1201
"text".into(), - 1202
SpecValue::Text { - 1203
value: "hello".into(), - 1204
}, - 1205
), - 1206
("count".into(), SpecValue::Number { value: 2.0 }), - 1207
("ready".into(), SpecValue::Boolean { value: true }), - 1208
( - 1209
"meta".into(), - 1210
SpecValue::Literal { - 1211
value: serde_json::json!({"source": "pack"}), - 1212
}, - 1213
), - 1214
]), - 1215
children: vec![], - 1216
each: None, - 1217
item: None, - 1218
}; - 1219
let s = make_spec("static_values", 1, &["detail"], root); - 1220
let encoded = serde_json::to_vec(&s).expect("serialize static pack values"); - 1221
let decoded = parse_spec(&encoded).expect("parse static pack values"); - 1222
assert_eq!(decoded, s); - 1223
assert!(digest(&decoded).is_ok()); - 1224
})); - 1225
- 1226
run("digests", scenarios); - 1227
} - 1228
- 1229
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ - 1230
// 5. PARSE SPEC - 1231
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ - 1232
- 1233
#[test] - 1234
fn audit_parse_spec() { - 1235
let mut scenarios: Vec<Scenario> = vec![]; - 1236
- 1237
let valid_json = r#"{"schema_version":1,"id":"test","revision":1,"accepts":["detail"],"root":{"primitive":"text","props":{"text":{"kind":"binding","path":"$.text","required":false,"empty":"empty_text"}}}}"#; - 1238
- 1239
scenarios.push(tc("parse_valid_minimal_json", || { - 1240
assert!(parse_spec(valid_json.as_bytes()).is_ok()); - 1241
})); - 1242
- 1243
scenarios.push(tc("parse_not_json", || { - 1244
assert!(matches!( - 1245
parse_spec(b"not json"), - 1246
Err(PresentationError::InvalidJson(_)) - 1247
)); - 1248
})); - 1249
- 1250
scenarios.push(tc("parse_empty_bytes", || { - 1251
assert!(matches!( - 1252
parse_spec(b""), - 1253
Err(PresentationError::InvalidJson(_)) - 1254
)); - 1255
})); - 1256
- 1257
scenarios.push(tc("parse_empty_object", || { - 1258
assert!(parse_spec(b"{}").is_err()); - 1259
})); - 1260
- 1261
scenarios.push(tc("parse_array_not_object", || { - 1262
assert!(matches!( - 1263
parse_spec(b"[1,2,3]"), - 1264
Err(PresentationError::InvalidJson(_)) - 1265
)); - 1266
})); - 1267
- 1268
scenarios.push(tc("parse_null", || { - 1269
assert!(matches!( - 1270
parse_spec(b"null"), - 1271
Err(PresentationError::InvalidJson(_)) - 1272
)); - 1273
})); - 1274
- 1275
scenarios.push(tc("parse_scalar_int", || { - 1276
assert!(parse_spec(b"42").is_err()); - 1277
})); - 1278
- 1279
scenarios.push(tc("parse_wrong_schema_version", || { - 1280
let bad = r#"{"schema_version":2,"id":"x","revision":1,"accepts":["detail"],"root":{"primitive":"text","props":{"text":{"kind":"binding","path":"$.text","required":false,"empty":"empty_text"}}}}"#; - 1281
assert!(matches!(parse_spec(bad.as_bytes()), Err(PresentationError::UnsupportedSchema(2)))); - 1282
})); - 1283
- 1284
scenarios.push(tc("parse_empty_id", || { - 1285
let bad = r#"{"schema_version":1,"id":"","revision":1,"accepts":["detail"],"root":{"primitive":"text","props":{"text":{"kind":"binding","path":"$.text","required":false,"empty":"empty_text"}}}}"#; - 1286
assert!(parse_spec(bad.as_bytes()).is_err()); - 1287
})); - 1288
- 1289
scenarios.push(tc("parse_oversized_spec_boundary", || { - 1290
// Construct a spec just over MAX_SPEC_BYTES when serialized - 1291
let long_val = "x".repeat(MAX_SPEC_BYTES); - 1292
let j = format!( - 1293
r#"{{"schema_version":1,"id":"big","revision":1,"accepts":["detail"],"root":{{"primitive":"section","props":{{"title":{{"kind":"literal","value":{{"t":"{long_val}"}}}}}}}}}}"# - 1294
); - 1295
assert!(matches!(parse_spec(j.as_bytes()), Err(PresentationError::SpecTooLarge))); - 1296
})); - 1297
- 1298
scenarios.push(tc("parse_extra_unknown_fields_ignored", || { - 1299
let j = r#"{"schema_version":1,"id":"x","revision":1,"accepts":["detail"],"root":{"primitive":"text","props":{"text":{"kind":"binding","path":"$.text","required":false,"empty":"empty_text"}}},"unknown_field":"value","extra":123}"#; - 1300
assert!(parse_spec(j.as_bytes()).is_ok()); - 1301
})); - 1302
- 1303
scenarios.push(tc("parse_valid_with_all_optional_fields", || { - 1304
let j = r#"{"schema_version":1,"id":"full","revision":1,"accepts":["detail"],"root":{"primitive":"text","props":{"text":{"kind":"binding","path":"$.text","required":false,"empty":"empty_text"}}},"fallback":{"kind":"plain"},"accessibility":{"summary":{"path":"$.title","required":false,"empty":"omit"}},"metadata":{"key":"value"}}"#; - 1305
assert!(parse_spec(j.as_bytes()).is_ok()); - 1306
})); - 1307
- 1308
scenarios.push(tc("parse_valid_with_string_prop", || { - 1309
let j = r#"{"schema_version":1,"id":"full2","revision":1,"accepts":["detail"],"root":{"primitive":"text","props":{"text":{"kind":"binding","path":"$.text","required":false,"empty":"empty_text"}}},"fallback":{"kind":"document"},"accessibility":{"summary":null},"metadata":{}}"#; - 1310
assert!(parse_spec(j.as_bytes()).is_ok()); - 1311
})); - 1312
- 1313
scenarios.push(tc("parse_valid_divider_root", || { - 1314
let j = r#"{"schema_version":1,"id":"d","revision":1,"accepts":["detail"],"root":{"primitive":"divider"}}"#; - 1315
assert!(parse_spec(j.as_bytes()).is_ok()); - 1316
})); - 1317
- 1318
scenarios.push(tc("parse_valid_with_children", || { - 1319
let j = r#"{"schema_version":1,"id":"c","revision":1,"accepts":["detail"],"root":{"primitive":"stack","props":{},"children":[{"primitive":"text","props":{"text":{"kind":"binding","path":"$.a","required":false,"empty":"empty_text"}}},{"primitive":"divider"}]}}"#; - 1320
assert!(parse_spec(j.as_bytes()).is_ok()); - 1321
})); - 1322
- 1323
scenarios.push(tc("parse_valid_with_each_item", || { - 1324
let j = r#"{"schema_version":1,"id":"e","revision":1,"accepts":["detail"],"root":{"primitive":"stack","props":{},"each":{"path":"$.items","required":false,"empty":"omit"},"item":{"primitive":"text","props":{"text":{"kind":"binding","path":"$.text","required":false,"empty":"empty_text"}}}}}"#; - 1325
assert!(parse_spec(j.as_bytes()).is_ok()); - 1326
})); - 1327
- 1328
scenarios.push(tc("parse_invalid_json_malformed", || { - 1329
assert!(matches!( - 1330
parse_spec(b"{bad json}"), - 1331
Err(PresentationError::InvalidJson(_)) - 1332
)); - 1333
})); - 1334
- 1335
scenarios.push(tc("parse_missing_root", || { - 1336
assert!(parse_spec(b"{\"schema_version\":1,\"id\":\"x\",\"revision\":1}").is_err()); - 1337
})); - 1338
- 1339
scenarios.push(tc("parse_zero_revision", || { - 1340
assert!(parse_spec(b"{\"schema_version\":1,\"id\":\"x\",\"revision\":0,\"root\":{\"primitive\":\"divider\"}}").is_err()); - 1341
})); - 1342
- 1343
scenarios.push(tc("parse_valid_multiple_semantic_types", || { - 1344
let j = r#"{"schema_version":1,"id":"m","revision":1,"accepts":["detail","comparison"],"root":{"primitive":"text","props":{"text":{"kind":"binding","path":"$.text","required":false,"empty":"empty_text"}}}}"#; - 1345
assert!(parse_spec(j.as_bytes()).is_ok()); - 1346
})); - 1347
- 1348
run("parse_spec", scenarios); - 1349
} - 1350
- 1351
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ - 1352
// 6. INFER SEMANTIC TYPE - 1353
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ - 1354
- 1355
#[test] - 1356
fn audit_infer_semantic_type() { - 1357
let cases: Vec<(serde_json::Value, Option<&'static str>, String)> = vec![ - 1358
( - 1359
json!({"left": "a", "right": "b"}), - 1360
Some("comparison"), - 1361
"comparison_left_right".into(), - 1362
), - 1363
( - 1364
json!({"alternatives": [1, 2]}), - 1365
Some("comparison"), - 1366
"comparison_alts".into(), - 1367
), - 1368
( - 1369
json!({"checks": [1]}), - 1370
Some("checklist"), - 1371
"checklist_checks".into(), - 1372
), - 1373
( - 1374
json!({"tasks": [1]}), - 1375
Some("checklist"), - 1376
"checklist_tasks".into(), - 1377
), - 1378
( - 1379
json!({"events": [1]}), - 1380
Some("schedule"), - 1381
"schedule_events".into(), - 1382
), - 1383
( - 1384
json!({"slots": [1]}), - 1385
Some("schedule"), - 1386
"schedule_slots".into(), - 1387
), - 1388
( - 1389
json!({"agenda": [1], "attendees": [1]}), - 1390
Some("meeting"), - 1391
"meeting".into(), - 1392
), - 1393
( - 1394
json!({"lessons": [1]}), - 1395
Some("lesson"), - 1396
"lesson_lessons".into(), - 1397
), - 1398
( - 1399
json!({"sections": [1]}), - 1400
Some("lesson"), - 1401
"lesson_sections".into(), - 1402
), - 1403
( - 1404
json!({"decision": "x"}), - 1405
Some("decision"), - 1406
"decision".into(), - 1407
), - 1408
( - 1409
json!({"choice": "x"}), - 1410
Some("decision"), - 1411
"decision_choice".into(), - 1412
), - 1413
( - 1414
json!({"income": [1], "expenses": [1]}), - 1415
Some("budget"), - 1416
"budget_ie".into(), - 1417
), - 1418
( - 1419
json!({"credits": [1], "debits": [1]}), - 1420
Some("budget"), - 1421
"budget_cd".into(), - 1422
), - 1423
( - 1424
json!({"items": [1]}), - 1425
Some("collection"), - 1426
"collection_items".into(), - 1427
), - 1428
( - 1429
json!({"entries": [1]}), - 1430
Some("collection"), - 1431
"collection_entries".into(), - 1432
), - 1433
(json!({"steps": [1]}), Some("steps"), "steps".into()), - 1434
(json!({"status": "active"}), Some("status"), "status".into()), - 1435
( - 1436
json!({"state": "active"}), - 1437
Some("status"), - 1438
"status_state".into(), - 1439
), - 1440
(json!({"value": 42}), Some("metric"), "metric_value".into()), - 1441
( - 1442
json!({"amount": 42}), - 1443
Some("metric"), - 1444
"metric_amount".into(), - 1445
), - 1446
(json!({"count": 42}), Some("metric"), "metric_count".into()), - 1447
(json!({"total": 42}), Some("metric"), "metric_total".into()), - 1448
(json!({"value": 0}), Some("metric"), "metric_zero".into()), - 1449
( - 1450
json!({"value": -1}), - 1451
Some("metric"), - 1452
"metric_negative".into(), - 1453
), - 1454
(json!({"count": true}), None, "count_not_number".into()), - 1455
( - 1456
json!({"title": "hi"}), - 1457
Some("detail"), - 1458
"detail_title".into(), - 1459
), - 1460
( - 1461
json!({"summary": "hi"}), - 1462
Some("detail"), - 1463
"detail_summary".into(), - 1464
), - 1465
(json!({"unknown": "field"}), None, "unknown_field".into()), - 1466
( - 1467
json!({"items": "not_array"}), - 1468
None, - 1469
"items_not_array".into(), - 1470
), - 1471
(json!({"left": 1}), None, "left_without_right".into()), - 1472
( - 1473
json!({"income": 1, "expenses": [1]}), - 1474
None, - 1475
"income_not_array".into(), - 1476
), - 1477
( - 1478
json!({"value": "not_a_number"}), - 1479
None, - 1480
"value_not_number".into(), - 1481
), - 1482
( - 1483
json!({"agenda": [1], "attendees": "not_array"}), - 1484
None, - 1485
"agenda_not_array".into(), - 1486
), - 1487
( - 1488
json!({"credits": 1, "debits": [1]}), - 1489
None, - 1490
"credits_not_array".into(), - 1491
), - 1492
(json!(42), None, "scalar_int".into()), - 1493
(json!("string"), None, "scalar_string".into()), - 1494
(json!([1, 2, 3]), None, "array_payload".into()), - 1495
(json!(null), None, "null_payload".into()), - 1496
(json!({}), None, "empty_object".into()), - 1497
( - 1498
json!({"items": [1], "title": "T"}), - 1499
Some("collection"), - 1500
"items_wins_over_title".into(), - 1501
), - 1502
( - 1503
json!({"left": "a", "right": "b", "title": "T"}), - 1504
Some("comparison"), - 1505
"comparison_wins_over_title".into(), - 1506
), - 1507
]; - 1508
- 1509
let mut scenarios: Vec<Scenario> = Vec::new(); - 1510
for (payload, expected, label) in cases { - 1511
let p = payload; - 1512
let e = expected; - 1513
scenarios.push(tc(&label.clone(), move || { - 1514
let result = infer_semantic_type(&p); - 1515
assert_eq!(result, e, "infer for {label}"); - 1516
})); - 1517
} - 1518
- 1519
run("infer_semantic_type", scenarios); - 1520
} - 1521
- 1522
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ - 1523
// 7. PRESENTATION LIBRARY LIFECYCLE - 1524
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ - 1525
- 1526
#[test] - 1527
fn audit_library_lifecycle() { - 1528
let mut scenarios: Vec<Scenario> = vec![]; - 1529
- 1530
// 7a. Register - 1531
scenarios.push(tc("register_correct_digest_ok", || { - 1532
let mut lib = PresentationLibrary::default(); - 1533
assert!( - 1534
lib.register(stored_simple( - 1535
"ok1", - 1536
1, - 1537
&["detail"], - 1538
text_binding_node(), - 1539
"o1" - 1540
)) - 1541
.is_ok() - 1542
); - 1543
})); - 1544
- 1545
scenarios.push(tc("register_wrong_digest_mismatch", || { - 1546
let mut lib = PresentationLibrary::default(); - 1547
let spec = make_spec("dmi", 1, &["detail"], text_binding_node()); - 1548
let dg = digest(&spec).unwrap(); - 1549
let bad = format!("{}00", &dg[..dg.len().saturating_sub(2)]); - 1550
let stored = StoredPresentation { - 1551
spec, - 1552
digest: bad, - 1553
origin: PresentationOrigin { - 1554
scope: LibraryScope::Workspace, - 1555
owner: "o1".into(), - 1556
plugin_id: None, - 1557
generation: Some("g1".into()), - 1558
}, - 1559
enabled: true, - 1560
}; - 1561
assert!(matches!( - 1562
lib.register(stored), - 1563
Err(PresentationError::DigestMismatch) - 1564
)); - 1565
})); - 1566
- 1567
scenarios.push(tc("register_empty_owner_invalid", || { - 1568
let mut lib = PresentationLibrary::default(); - 1569
let spec = make_spec("eoi", 1, &["detail"], text_binding_node()); - 1570
let dg = digest(&spec).unwrap(); - 1571
let stored = StoredPresentation { - 1572
spec, - 1573
digest: dg, - 1574
origin: PresentationOrigin { - 1575
scope: LibraryScope::Workspace, - 1576
owner: "".into(), - 1577
plugin_id: None, - 1578
generation: Some("g1".into()), - 1579
}, - 1580
enabled: true, - 1581
}; - 1582
assert!(matches!( - 1583
lib.register(stored), - 1584
Err(PresentationError::InvalidSpec(_)) - 1585
)); - 1586
})); - 1587
- 1588
scenarios.push(tc("register_duplicate_same_digest_ok", || { - 1589
let mut lib = PresentationLibrary::default(); - 1590
let s = stored_simple("dup1", 1, &["detail"], text_binding_node(), "o1"); - 1591
assert!(lib.register(s.clone()).is_ok()); - 1592
assert!(lib.register(s).is_ok()); - 1593
})); - 1594
- 1595
scenarios.push(tc("register_duplicate_different_digest_conflict", || { - 1596
let mut lib = PresentationLibrary::default(); - 1597
assert!( - 1598
lib.register(stored_simple( - 1599
"conf1", - 1600
1, - 1601
&["detail"], - 1602
text_binding_node(), - 1603
"o1" - 1604
)) - 1605
.is_ok() - 1606
); - 1607
let mut root2 = text_binding_node(); - 1608
root2.children = vec![]; // different text - 1609
let root2 = SpecNode { - 1610
primitive: Primitive::Divider, - 1611
props: BTreeMap::new(), - 1612
children: vec![], - 1613
each: None, - 1614
item: None, - 1615
}; - 1616
assert!(matches!( - 1617
lib.register(stored_simple("conf1", 1, &["detail"], root2, "o1")), - 1618
Err(PresentationError::RevisionConflict(_)) - 1619
)); - 1620
})); - 1621
- 1622
// 7b. Activate - 1623
scenarios.push(tc("activate_correct_scope_owner_ok", || { - 1624
let mut lib = PresentationLibrary::default(); - 1625
lib.register(stored_simple( - 1626
"ak1", - 1627
1, - 1628
&["detail"], - 1629
section_with_title(), - 1630
"o1", - 1631
)) - 1632
.unwrap(); - 1633
assert!( - 1634
lib.activate("ak1", 1, LibraryScope::Workspace, "o1") - 1635
.is_ok() - 1636
); - 1637
})); - 1638
- 1639
scenarios.push(tc("activate_wrong_scope_denied", || { - 1640
let mut lib = PresentationLibrary::default(); - 1641
let mut s = stored_simple("ak2", 1, &["detail"], section_with_title(), "o1"); - 1642
s.origin.scope = LibraryScope::User; - 1643
lib.register(s).unwrap(); - 1644
assert!(matches!( - 1645
lib.activate("ak2", 1, LibraryScope::Workspace, "o1"), - 1646
Err(PresentationError::ActivationDenied) - 1647
)); - 1648
})); - 1649
- 1650
scenarios.push(tc("activate_wrong_owner_denied", || { - 1651
let mut lib = PresentationLibrary::default(); - 1652
lib.register(stored_simple( - 1653
"ak3", - 1654
1, - 1655
&["detail"], - 1656
section_with_title(), - 1657
"o1", - 1658
)) - 1659
.unwrap(); - 1660
assert!(matches!( - 1661
lib.activate("ak3", 1, LibraryScope::Workspace, "wrong"), - 1662
Err(PresentationError::ActivationDenied) - 1663
)); - 1664
})); - 1665
- 1666
scenarios.push(tc("activate_unknown_presentation_error", || { - 1667
let mut lib = PresentationLibrary::default(); - 1668
assert!(matches!( - 1669
lib.activate("nope", 1, LibraryScope::Workspace, "o1"), - 1670
Err(PresentationError::UnknownPresentation(_)) - 1671
)); - 1672
})); - 1673
- 1674
// 7c. Activate built-in from any owner (built-in exemption is owner-only) - 1675
for owner in ["user1", "user2", "any_owner", "builtin"] { - 1676
let o = owner; - 1677
scenarios.push(tc(&format!("activate_builtin_any_owner_{o}"), move || { - 1678
let mut lib = PresentationLibrary::default(); - 1679
lib.register(stored_builtin("b1", 1, &["detail"], section_with_title())) - 1680
.unwrap(); - 1681
let result = lib.activate("b1", 1, LibraryScope::Workspace, o); - 1682
assert!(result.is_ok(), "builtin from owner {o:?}: {result:?}"); - 1683
})); - 1684
} - 1685
- 1686
// 7d. Select - 1687
scenarios.push(tc("select_returns_highest_revision", || { - 1688
let mut lib = PresentationLibrary::default(); - 1689
lib.register(stored_builtin("sel1", 1, &["detail"], section_with_title())) - 1690
.unwrap(); - 1691
lib.register(stored_simple( - 1692
"sel1", - 1693
2, - 1694
&["detail"], - 1695
section_with_title(), - 1696
"o1", - 1697
)) - 1698
.unwrap(); - 1699
lib.activate("sel1", 1, LibraryScope::Workspace, "builtin") - 1700
.unwrap(); - 1701
lib.activate("sel1", 2, LibraryScope::Workspace, "o1") - 1702
.unwrap(); - 1703
let selected = lib.select("detail", LibraryScope::Workspace, "o1"); - 1704
assert!(selected.is_some()); - 1705
assert_eq!(selected.unwrap().spec.revision, 2); - 1706
})); - 1707
- 1708
scenarios.push(tc("select_filters_by_scope", || { - 1709
let mut lib = PresentationLibrary::default(); - 1710
let mut s = stored_simple("sf1", 1, &["detail"], section_with_title(), "o1"); - 1711
s.origin.scope = LibraryScope::User; - 1712
lib.register(s).unwrap(); - 1713
lib.activate("sf1", 1, LibraryScope::User, "o1").unwrap(); - 1714
assert!( - 1715
lib.select("detail", LibraryScope::Workspace, "o1") - 1716
.is_none() - 1717
); - 1718
})); - 1719
- 1720
scenarios.push(tc("select_filters_by_owner", || { - 1721
let mut lib = PresentationLibrary::default(); - 1722
lib.register(stored_simple( - 1723
"sf2", - 1724
1, - 1725
&["detail"], - 1726
section_with_title(), - 1727
"owner_a", - 1728
)) - 1729
.unwrap(); - 1730
lib.activate("sf2", 1, LibraryScope::Workspace, "owner_a") - 1731
.unwrap(); - 1732
assert!( - 1733
lib.select("detail", LibraryScope::Workspace, "owner_b") - 1734
.is_none() - 1735
); - 1736
})); - 1737
- 1738
scenarios.push(tc("select_filters_by_semantic_type", || { - 1739
let mut lib = PresentationLibrary::default(); - 1740
lib.register(stored_simple( - 1741
"sf3", - 1742
1, - 1743
&["comparison"], - 1744
section_with_title(), - 1745
"o1", - 1746
)) - 1747
.unwrap(); - 1748
lib.activate("sf3", 1, LibraryScope::Workspace, "o1") - 1749
.unwrap(); - 1750
assert!( - 1751
lib.select("detail", LibraryScope::Workspace, "o1") - 1752
.is_none() - 1753
); - 1754
})); - 1755
- 1756
scenarios.push(tc("select_no_activation_returns_none", || { - 1757
let mut lib = PresentationLibrary::default(); - 1758
lib.register(stored_simple( - 1759
"na1", - 1760
1, - 1761
&["detail"], - 1762
section_with_title(), - 1763
"o1", - 1764
)) - 1765
.unwrap(); - 1766
assert!( - 1767
lib.select("detail", LibraryScope::Workspace, "o1") - 1768
.is_none() - 1769
); - 1770
})); - 1771
- 1772
// select_preferred matrix - 1773
for (u_has, w_has) in [(true, true), (true, false), (false, true), (false, false)] { - 1774
let (uh, wh) = (u_has, w_has); - 1775
scenarios.push(tc(&format!("select_preferred_u_{uh}_w_{wh}"), move || { - 1776
let mut lib = PresentationLibrary::default(); - 1777
if uh { - 1778
let mut s = stored_simple("u_spec", 1, &["detail"], section_with_title(), "user1"); - 1779
s.origin.scope = LibraryScope::User; - 1780
lib.register(s).unwrap(); - 1781
lib.activate("u_spec", 1, LibraryScope::User, "user1") - 1782
.unwrap(); - 1783
} - 1784
if wh { - 1785
let s = stored_simple("w_spec", 1, &["detail"], section_with_title(), "ws1"); - 1786
lib.register(s).unwrap(); - 1787
lib.activate("w_spec", 1, LibraryScope::Workspace, "ws1") - 1788
.unwrap(); - 1789
} - 1790
let selected = lib.select_preferred("detail", "user1", "ws1"); - 1791
match (uh, wh) { - 1792
(true, _) => assert!(selected.is_some()), - 1793
(false, true) => assert!(selected.is_some()), - 1794
(false, false) => assert!(selected.is_none()), - 1795
} - 1796
})); - 1797
} - 1798
- 1799
// 7e. Reset - 1800
scenarios.push(tc("reset_restores_revision_1", || { - 1801
let mut lib = PresentationLibrary::default(); - 1802
lib.register(stored_simple( - 1803
"r1", - 1804
1, - 1805
&["detail"], - 1806
section_with_title(), - 1807
"o1", - 1808
)) - 1809
.unwrap(); - 1810
let r2_root = section_with_title(); - 1811
lib.register(stored_simple("r1", 2, &["detail"], r2_root, "o1")) - 1812
.unwrap(); - 1813
lib.activate("r1", 2, LibraryScope::Workspace, "o1") - 1814
.unwrap(); - 1815
assert_eq!( - 1816
lib.select("detail", LibraryScope::Workspace, "o1") - 1817
.unwrap() - 1818
.spec - 1819
.revision, - 1820
2 - 1821
); - 1822
assert!(lib.reset("r1", LibraryScope::Workspace, "o1")); - 1823
assert_eq!( - 1824
lib.select("detail", LibraryScope::Workspace, "o1") - 1825
.unwrap() - 1826
.spec - 1827
.revision, - 1828
1 - 1829
); - 1830
})); - 1831
- 1832
scenarios.push(tc("reset_no_original_returns_false", || { - 1833
let mut lib = PresentationLibrary::default(); - 1834
lib.register(stored_simple( - 1835
"r2", - 1836
2, - 1837
&["detail"], - 1838
section_with_title(), - 1839
"o1", - 1840
)) - 1841
.unwrap(); - 1842
assert!(!lib.reset("r2", LibraryScope::Workspace, "o1")); - 1843
})); - 1844
- 1845
// 7f. Deactivate - 1846
scenarios.push(tc("deactivate_removes_activation", || { - 1847
let mut lib = PresentationLibrary::default(); - 1848
lib.register(stored_simple( - 1849
"da1", - 1850
1, - 1851
&["detail"], - 1852
section_with_title(), - 1853
"o1", - 1854
)) - 1855
.unwrap(); - 1856
lib.activate("da1", 1, LibraryScope::Workspace, "o1") - 1857
.unwrap(); - 1858
assert!( - 1859
lib.select("detail", LibraryScope::Workspace, "o1") - 1860
.is_some() - 1861
); - 1862
lib.deactivate("da1", LibraryScope::Workspace, "o1"); - 1863
assert!( - 1864
lib.select("detail", LibraryScope::Workspace, "o1") - 1865
.is_none() - 1866
); - 1867
})); - 1868
- 1869
// 7g. Revoke plugin - 1870
for pc in [1usize, 2, 5, 10] { - 1871
scenarios.push(tc(&format!("revoke_plugin_removes_{pc}"), move || { - 1872
let mut lib = PresentationLibrary::default(); - 1873
let pid = "plugin.test"; - 1874
for i in 0..pc { - 1875
let s = stored_plugin(&format!("p{i}"), 1, &["detail"], text_binding_node(), pid); - 1876
lib.register(s).unwrap(); - 1877
lib.activate(&format!("p{i}"), 1, LibraryScope::Workspace, "plugin_owner") - 1878
.unwrap(); - 1879
} - 1880
let before = lib.definitions().count(); - 1881
let removed = lib.revoke_plugin(pid); - 1882
assert_eq!(removed, pc); - 1883
assert_eq!(lib.definitions().count(), before - pc); - 1884
assert!(lib.activations().is_empty()); - 1885
})); - 1886
} - 1887
- 1888
// 7h. from_parts - 1889
scenarios.push(tc("from_parts_validates_specs", || { - 1890
let s = stored_simple("fp1", 1, &["detail"], section_with_title(), "o1"); - 1891
assert!(PresentationLibrary::from_parts(vec![s], vec![]).is_ok()); - 1892
})); - 1893
- 1894
scenarios.push(tc("from_parts_rejects_bad_digest", || { - 1895
let spec = make_spec("fp2", 1, &["detail"], section_with_title()); - 1896
let stored = StoredPresentation { - 1897
spec, - 1898
digest: "wrong".into(), - 1899
origin: PresentationOrigin { - 1900
scope: LibraryScope::Workspace, - 1901
owner: "o1".into(), - 1902
plugin_id: None, - 1903
generation: Some("g1".into()), - 1904
}, - 1905
enabled: true, - 1906
}; - 1907
assert!(matches!( - 1908
PresentationLibrary::from_parts(vec![stored], vec![]), - 1909
Err(PresentationError::DigestMismatch) - 1910
)); - 1911
})); - 1912
- 1913
// 7i. pack_manifest - 1914
scenarios.push(tc("pack_manifest_generates_digest", || { - 1915
let mut lib = PresentationLibrary::default(); - 1916
lib.register(stored_simple( - 1917
"pm1", - 1918
1, - 1919
&["detail"], - 1920
section_with_title(), - 1921
"o1", - 1922
)) - 1923
.unwrap(); - 1924
let manifest = lib.pack_manifest( - 1925
"my_pack", - 1926
"1.0.0", - 1927
vec!["Text".into()], - 1928
Some("publisher".into()), - 1929
); - 1930
assert!(manifest.is_ok()); - 1931
let m = manifest.unwrap(); - 1932
assert_eq!(m.pack_id, "my_pack"); - 1933
assert_eq!(m.version, "1.0.0"); - 1934
assert!(!m.digest.is_empty()); - 1935
})); - 1936
- 1937
// 7j. Cross-scope activation denied - 1938
scenarios.push(tc("cross_scope_activation_denied", || { - 1939
let mut lib = PresentationLibrary::default(); - 1940
let mut s = stored_simple("cs1", 1, &["detail"], section_with_title(), "user1"); - 1941
s.origin.scope = LibraryScope::User; - 1942
lib.register(s).unwrap(); - 1943
assert!(matches!( - 1944
lib.activate("cs1", 1, LibraryScope::Workspace, "user1"), - 1945
Err(PresentationError::ActivationDenied) - 1946
)); - 1947
})); - 1948
- 1949
// 7k. Plugin scope activation ok - 1950
scenarios.push(tc("plugin_scope_activation_ok", || { - 1951
let mut lib = PresentationLibrary::default(); - 1952
lib.register(stored_plugin( - 1953
"ps1", - 1954
1, - 1955
&["detail"], - 1956
section_with_title(), - 1957
"plugin.test", - 1958
)) - 1959
.unwrap(); - 1960
assert!( - 1961
lib.activate("ps1", 1, LibraryScope::Workspace, "plugin_owner") - 1962
.is_ok() - 1963
); - 1964
})); - 1965
- 1966
// 7l. get returns registered spec - 1967
scenarios.push(tc("get_after_register", || { - 1968
let mut lib = PresentationLibrary::default(); - 1969
lib.register(stored_simple( - 1970
"get1", - 1971
1, - 1972
&["detail"], - 1973
section_with_title(), - 1974
"o1", - 1975
)) - 1976
.unwrap(); - 1977
assert!(lib.get("get1", 1).is_some()); - 1978
assert!(lib.get("get1", 2).is_none()); - 1979
})); - 1980
- 1981
// 7m. Activate sets enabled - 1982
scenarios.push(tc("activate_sets_enabled", || { - 1983
let mut lib = PresentationLibrary::default(); - 1984
let mut s = stored_simple("ae1", 1, &["detail"], section_with_title(), "o1"); - 1985
s.enabled = false; - 1986
lib.register(s).unwrap(); - 1987
assert!(!lib.get("ae1", 1).unwrap().enabled); - 1988
lib.activate("ae1", 1, LibraryScope::Workspace, "o1") - 1989
.unwrap(); - 1990
assert!(lib.get("ae1", 1).unwrap().enabled); - 1991
})); - 1992
- 1993
// 7n. Deactivate keeps spec - 1994
scenarios.push(tc("deactivate_keeps_spec", || { - 1995
let mut lib = PresentationLibrary::default(); - 1996
lib.register(stored_simple( - 1997
"dk1", - 1998
1, - 1999
&["detail"], - 2000
section_with_title(),
Indexing the workspaceβ¦
Vakyartha documentation is discovering safe artifacts, anchors, and source references.