- 1076
] { - 1077
let spec: PresentationSpec = serde_json::from_value(serde_json::json!({ - 1078
"schema_version": SPEC_SCHEMA_VERSION, - 1079
"id": format!("plugin.{semantic_type}"), - 1080
"revision": 1, - 1081
"accepts": [semantic_type], - 1082
"root": root, - 1083
})) - 1084
.expect("runtime pack parses"); - 1085
validate_spec(&spec).expect("runtime pack validates"); - 1086
let result = compile( - 1087
&spec, - 1088
&CompileInput { - 1089
semantic_type: semantic_type.into(), - 1090
payload: serde_json::json!({ - 1091
"title": "Card", - 1092
"label": "Latency", - 1093
"value": "12ms", - 1094
"src": "a.png", - 1095
"clip": "a.mp4", - 1096
"metrics": [{"label":"p50","value":"9ms"},{"label":"p99","value":"40ms"}], - 1097
}), - 1098
fallback_text: "Fallback".into(), - 1099
}, - 1100
); - 1101
assert!( - 1102
matches!(result, CompiledPresentation::Rich(_)), - 1103
"{semantic_type} should compile without any code change: {result:?}" - 1104
); - 1105
} - 1106
} - 1107
- 1108
fn spec() -> PresentationSpec { - 1109
PresentationSpec { - 1110
schema_version: SPEC_SCHEMA_VERSION, - 1111
id: "test.timeline".into(), - 1112
revision: 1, - 1113
accepts: vec!["trip".into()], - 1114
root: SpecNode { - 1115
primitive: Primitive::Timeline, - 1116
props: BTreeMap::from([( - 1117
String::from("title"), - 1118
SpecValue::Binding(Binding { - 1119
path: "$.title".into(), - 1120
required: true, - 1121
empty: EmptyValue::Omit, - 1122
}), - 1123
)]), - 1124
children: Vec::new(), - 1125
each: Some(Binding { - 1126
path: "$.days".into(), - 1127
required: true, - 1128
empty: EmptyValue::Omit, - 1129
}), - 1130
item: Some(Box::new(SpecNode { - 1131
primitive: Primitive::Section, - 1132
props: BTreeMap::from([( - 1133
String::from("label"), - 1134
SpecValue::Binding(Binding { - 1135
path: "$.label".into(), - 1136
required: true, - 1137
empty: EmptyValue::Omit, - 1138
}), - 1139
)]), - 1140
children: Vec::new(), - 1141
each: None, - 1142
item: None, - 1143
})), - 1144
}, - 1145
fallback: FallbackSpec::default(), - 1146
accessibility: AccessibilitySpec { - 1147
summary: Some(Binding { - 1148
path: "$.summary".into(), - 1149
required: false, - 1150
empty: EmptyValue::EmptyText, - 1151
}), - 1152
}, - 1153
metadata: BTreeMap::new(), - 1154
} - 1155
} - 1156
- 1157
#[test] - 1158
fn compiles_bounded_timeline_and_tracks_coverage() { - 1159
let result = compile( - 1160
&spec(), - 1161
&CompileInput { - 1162
semantic_type: "trip".into(), - 1163
payload: serde_json::json!({"title":"Jaipur", "summary":"Three days in Jaipur", "days":[{"label":"Day 1"},{"label":"Day 2"}]}), - 1164
fallback_text: "Jaipur\n\nDay 1\nDay 2".into(), - 1165
}, - 1166
); - 1167
let tree = match result { - 1168
CompiledPresentation::Rich(tree) => tree, - 1169
CompiledPresentation::Fallback { reason, .. } => { - 1170
assert!(reason.is_empty(), "expected rich result: {reason}"); - 1171
return; - 1172
} - 1173
}; - 1174
assert_eq!(tree.root.children.len(), 2); - 1175
assert!( - 1176
tree.coverage - 1177
.rendered_paths - 1178
.iter() - 1179
.any(|path| path == "$.days") - 1180
); - 1181
assert_eq!( - 1182
tree.accessibility_summary.as_deref(), - 1183
Some("Three days in Jaipur") - 1184
); - 1185
} - 1186
- 1187
#[test] - 1188
fn rejects_hostile_spec_depth_and_node_count() { - 1189
let mut deep = spec(); - 1190
let mut node = SpecNode { - 1191
primitive: Primitive::Group, - 1192
props: BTreeMap::new(), - 1193
children: Vec::new(), - 1194
each: None, - 1195
item: None, - 1196
}; - 1197
for _ in 0..=MAX_DEPTH { - 1198
node = SpecNode { - 1199
primitive: Primitive::Group, - 1200
props: BTreeMap::new(), - 1201
children: vec![node], - 1202
each: None, - 1203
item: None, - 1204
}; - 1205
} - 1206
deep.root = node; - 1207
assert!(matches!( - 1208
validate_spec(&deep), - 1209
Err(PresentationError::Limit(_)) - 1210
)); - 1211
- 1212
let mut wide = spec(); - 1213
wide.root.children = (0..=MAX_NODES) - 1214
.map(|_| SpecNode { - 1215
primitive: Primitive::Text, - 1216
props: BTreeMap::new(), - 1217
children: Vec::new(), - 1218
each: None, - 1219
item: None, - 1220
}) - 1221
.collect(); - 1222
assert!(matches!( - 1223
validate_spec(&wide), - 1224
Err(PresentationError::Limit(_)) - 1225
)); - 1226
} - 1227
- 1228
#[test] - 1229
fn bounds_untrusted_collection_expansion() { - 1230
let result = compile( - 1231
&spec(), - 1232
&CompileInput { - 1233
semantic_type: "trip".into(), - 1234
payload: serde_json::json!({ - 1235
"title": "large", - 1236
"days": (0..=MAX_EXPANSION_ITEMS) - 1237
.map(|index| serde_json::json!({"label": index})) - 1238
.collect::<Vec<_>>() - 1239
}), - 1240
fallback_text: "safe fallback".into(), - 1241
}, - 1242
); - 1243
assert!( - 1244
matches!(result, CompiledPresentation::Fallback { text, reason } - 1245
if text == "safe fallback" && reason.contains("runtime collection expansion")) - 1246
); - 1247
} - 1248
- 1249
#[test] - 1250
fn invalid_spec_degrades_without_panicking() { - 1251
let mut invalid = spec(); - 1252
invalid.root.each = Some(Binding { - 1253
path: "$.days[".into(), - 1254
required: false, - 1255
empty: EmptyValue::Omit, - 1256
}); - 1257
let result = compile( - 1258
&invalid, - 1259
&CompileInput { - 1260
semantic_type: "trip".into(), - 1261
payload: Value::Null, - 1262
fallback_text: "Original answer".into(), - 1263
}, - 1264
); - 1265
assert!( - 1266
matches!(result, CompiledPresentation::Fallback { text, .. } if text == "Original answer") - 1267
); - 1268
} - 1269
- 1270
#[test] - 1271
fn semantic_mismatch_uses_fallback() { - 1272
let result = compile( - 1273
&spec(), - 1274
&CompileInput { - 1275
semantic_type: "recipe".into(), - 1276
payload: Value::Null, - 1277
fallback_text: "Original".into(), - 1278
}, - 1279
); - 1280
assert!(matches!(result, CompiledPresentation::Fallback { .. })); - 1281
} - 1282
- 1283
#[test] - 1284
fn shape_inference_is_bounded_and_domain_neutral() { - 1285
assert_eq!( - 1286
infer_semantic_type(&serde_json::json!({"items": []})), - 1287
Some("collection") - 1288
); - 1289
assert_eq!( - 1290
infer_semantic_type(&serde_json::json!({"steps": []})), - 1291
Some("steps") - 1292
); - 1293
assert_eq!( - 1294
infer_semantic_type(&serde_json::json!({"status": "ready"})), - 1295
Some("status") - 1296
); - 1297
assert_eq!( - 1298
infer_semantic_type(&serde_json::json!({"label": "Score", "value": 42})), - 1299
Some("metric") - 1300
); - 1301
assert_eq!( - 1302
infer_semantic_type(&serde_json::json!({"title": "Anything"})), - 1303
Some("detail") - 1304
); - 1305
assert_eq!( - 1306
infer_semantic_type(&serde_json::json!({"trip": "Paris"})), - 1307
None - 1308
); - 1309
assert_eq!( - 1310
infer_semantic_type(&serde_json::json!({"left": {}, "right": {}})), - 1311
Some("comparison") - 1312
); - 1313
assert_eq!( - 1314
infer_semantic_type(&serde_json::json!({"tasks": []})), - 1315
Some("checklist") - 1316
); - 1317
assert_eq!( - 1318
infer_semantic_type(&serde_json::json!({"events": []})), - 1319
Some("schedule") - 1320
); - 1321
assert_eq!( - 1322
infer_semantic_type(&serde_json::json!({"lessons": []})), - 1323
Some("lesson") - 1324
); - 1325
assert_eq!( - 1326
infer_semantic_type(&serde_json::json!({"income": [], "expenses": []})), - 1327
Some("budget") - 1328
); - 1329
assert_eq!( - 1330
infer_semantic_type(&serde_json::json!({"decision": "keep"})), - 1331
Some("decision") - 1332
); - 1333
} - 1334
- 1335
#[test] - 1336
fn path_validator_rejects_expressions_and_parent_traversal() { - 1337
for path in ["$.title + evil", "$..title", "$.title.foo()", "$.items[-1]"] { - 1338
assert!( - 1339
validate_binding(&Binding { - 1340
path: path.into(), - 1341
required: false, - 1342
empty: EmptyValue::Omit - 1343
}) - 1344
.is_err() - 1345
); - 1346
} - 1347
} - 1348
- 1349
#[test] - 1350
fn hostile_specs_fail_closed_at_parse_boundary() { - 1351
let mut oversized = spec(); - 1352
oversized.root.props.insert( - 1353
"text".into(), - 1354
SpecValue::Text { - 1355
value: "x".repeat(MAX_TEXT + 1), - 1356
}, - 1357
); - 1358
assert!(matches!( - 1359
validate_spec(&oversized), - 1360
Err(PresentationError::Limit(message)) if message.contains("text") - 1361
)); - 1362
- 1363
let mut deep = spec(); - 1364
let mut node = SpecNode { - 1365
primitive: Primitive::Group, - 1366
props: BTreeMap::new(), - 1367
children: Vec::new(), - 1368
each: None, - 1369
item: None, - 1370
}; - 1371
for _ in 0..=MAX_DEPTH { - 1372
node = SpecNode { - 1373
primitive: Primitive::Group, - 1374
props: BTreeMap::new(), - 1375
children: vec![node], - 1376
each: None, - 1377
item: None, - 1378
}; - 1379
} - 1380
deep.root = node; - 1381
assert!(matches!( - 1382
validate_spec(&deep), - 1383
Err(PresentationError::Limit(message)) if message.contains("depth") - 1384
)); - 1385
} - 1386
- 1387
#[test] - 1388
fn hostile_binding_corpus_never_escapes_the_small_path_language() { - 1389
// Keep this deterministic and dependency-free: the corpus exercises - 1390
// the same boundary that an untrusted pack or model-authored JSON - 1391
// reaches, without relying on a particular fuzzing runner. - 1392
let valid = ["$", "$.title", "$.items[0].label", "$.a_b-c[12]"]; - 1393
for path in valid { - 1394
assert!( - 1395
validate_binding(&Binding { - 1396
path: path.into(), - 1397
required: false, - 1398
empty: EmptyValue::Omit, - 1399
}) - 1400
.is_ok(), - 1401
"valid path rejected: {path}" - 1402
); - 1403
} - 1404
let hostile = [ - 1405
"", - 1406
"title", - 1407
"$..title", - 1408
"$.title + 1", - 1409
"$.title.foo()", - 1410
"$.items[-1]", - 1411
"$.items[abc]", - 1412
"$.items[0].", - 1413
"$.items[]", - 1414
"$.items[0]/../../secret", - 1415
"$['title']", - 1416
&format!("$.{}", "x".repeat(513)), - 1417
]; - 1418
for path in hostile { - 1419
assert!( - 1420
validate_binding(&Binding { - 1421
path: path.into(), - 1422
required: false, - 1423
empty: EmptyValue::Omit, - 1424
}) - 1425
.is_err(), - 1426
"hostile path accepted: {path}" - 1427
); - 1428
} - 1429
} - 1430
- 1431
#[test] - 1432
fn hostile_payloads_keep_the_exact_fallback_and_do_not_become_actions() { - 1433
let mut presentation = spec(); - 1434
presentation.root.props.insert( - 1435
"text".into(), - 1436
SpecValue::Binding(Binding { - 1437
path: "$.title".into(), - 1438
required: true, - 1439
empty: EmptyValue::Omit, - 1440
}), - 1441
); - 1442
let fallback = "Keep this answer unchanged\n\nApprove manually."; - 1443
let payloads = [ - 1444
serde_json::json!({"title": "javascript:alert(1)"}), - 1445
serde_json::json!({"title": "https://example.invalid/?next=javascript:alert(1)"}), - 1446
serde_json::json!({"title": "<script>document.body.innerHTML='x'</script>"}), - 1447
serde_json::json!({"title": {"action": "approve", "args": "--unsafe"}}), - 1448
serde_json::json!({"title": ["\\u0000", "\n", "\r"]}), - 1449
serde_json::json!({"other": "missing required binding"}), - 1450
]; - 1451
for payload in payloads { - 1452
let compiled = compile( - 1453
&presentation, - 1454
&CompileInput { - 1455
semantic_type: "trip".into(), - 1456
payload, - 1457
fallback_text: fallback.into(), - 1458
}, - 1459
); - 1460
assert!( - 1461
matches!(compiled, CompiledPresentation::Fallback { text, .. } if text == fallback), - 1462
"hostile payload unexpectedly changed fallback" - 1463
); - 1464
} - 1465
} - 1466
- 1467
#[test] - 1468
fn generated_specs_hit_limits_without_panicking() { - 1469
for width in [MAX_NODES - 1, MAX_NODES, MAX_NODES + 1] { - 1470
let mut candidate = spec(); - 1471
candidate.root.children = (0..width) - 1472
.map(|index| SpecNode { - 1473
primitive: Primitive::Text, - 1474
props: BTreeMap::from([( - 1475
format!("p{index}"), - 1476
SpecValue::Text { value: "x".into() }, - 1477
)]), - 1478
children: Vec::new(), - 1479
each: None, - 1480
item: None, - 1481
}) - 1482
.collect(); - 1483
let result = validate_spec(&candidate); - 1484
// The root itself counts toward the node budget. - 1485
if width >= MAX_NODES { - 1486
assert!(matches!(result, Err(PresentationError::Limit(_)))); - 1487
} - 1488
} - 1489
} - 1490
- 1491
#[test] - 1492
fn tampered_pack_and_cross_scope_activation_are_denied() { - 1493
let mut library = PresentationLibrary::default(); - 1494
let original = spec(); - 1495
let mut tampered = original.clone(); - 1496
tampered.id = "tampered".into(); - 1497
assert!(matches!( - 1498
library.register(StoredPresentation { - 1499
spec: tampered, - 1500
digest: digest(&original).expect("digest"), - 1501
origin: PresentationOrigin { - 1502
scope: LibraryScope::Workspace, - 1503
owner: "workspace-a".into(), - 1504
plugin_id: Some("untrusted-pack".into()), - 1505
generation: Some("1".into()), - 1506
}, - 1507
enabled: false, - 1508
}), - 1509
Err(PresentationError::DigestMismatch) - 1510
)); - 1511
- 1512
let stored = StoredPresentation { - 1513
digest: digest(&original).expect("digest"), - 1514
spec: original, - 1515
origin: PresentationOrigin { - 1516
scope: LibraryScope::Workspace, - 1517
owner: "workspace-a".into(), - 1518
plugin_id: Some("pack-a".into()), - 1519
generation: Some("1".into()), - 1520
}, - 1521
enabled: false, - 1522
}; - 1523
library.register(stored).expect("register"); - 1524
assert!(matches!( - 1525
library.activate("test.timeline", 1, LibraryScope::Workspace, "workspace-b"), - 1526
Err(PresentationError::ActivationDenied) - 1527
)); - 1528
assert!(library.activations().is_empty()); - 1529
} - 1530
- 1531
#[test] - 1532
fn library_registers_activates_and_selects_by_scope() { - 1533
let spec = spec(); - 1534
let stored = StoredPresentation { - 1535
digest: digest(&spec).expect("digest"), - 1536
spec, - 1537
origin: PresentationOrigin { - 1538
scope: LibraryScope::Workspace, - 1539
owner: "ws-1".into(), - 1540
plugin_id: None, - 1541
generation: None, - 1542
}, - 1543
enabled: false, - 1544
}; - 1545
let mut library = PresentationLibrary::default(); - 1546
library.register(stored).expect("register"); - 1547
library - 1548
.activate("test.timeline", 1, LibraryScope::Workspace, "ws-1") - 1549
.expect("activate"); - 1550
assert!( - 1551
library - 1552
.select("trip", LibraryScope::Workspace, "ws-1") - 1553
.is_some() - 1554
); - 1555
library.deactivate("test.timeline", LibraryScope::Workspace, "ws-1"); - 1556
assert!( - 1557
library - 1558
.select("trip", LibraryScope::Workspace, "ws-1") - 1559
.is_none() - 1560
); - 1561
} - 1562
- 1563
#[test] - 1564
fn preferred_selection_uses_user_intent_before_workspace_intent() { - 1565
let mut workspace_spec = spec(); - 1566
workspace_spec.id = "workspace.layout".into(); - 1567
let mut user_spec = spec(); - 1568
user_spec.id = "user.layout".into(); - 1569
let mut library = PresentationLibrary::default(); - 1570
library - 1571
.register(StoredPresentation { - 1572
digest: digest(&workspace_spec).expect("digest"), - 1573
spec: workspace_spec, - 1574
origin: PresentationOrigin { - 1575
scope: LibraryScope::Workspace, - 1576
owner: "ws".into(), - 1577
plugin_id: None, - 1578
generation: None, - 1579
}, - 1580
enabled: false, - 1581
}) - 1582
.expect("workspace"); - 1583
library - 1584
.register(StoredPresentation { - 1585
digest: digest(&user_spec).expect("digest"), - 1586
spec: user_spec, - 1587
origin: PresentationOrigin { - 1588
scope: LibraryScope::User, - 1589
owner: "user".into(), - 1590
plugin_id: None, - 1591
generation: None, - 1592
}, - 1593
enabled: false, - 1594
}) - 1595
.expect("user"); - 1596
library - 1597
.activate("workspace.layout", 1, LibraryScope::Workspace, "ws") - 1598
.expect("workspace activation"); - 1599
assert_eq!( - 1600
library - 1601
.select_preferred("trip", "user", "ws") - 1602
.map(|s| s.spec.id.as_str()), - 1603
Some("workspace.layout") - 1604
); - 1605
library - 1606
.activate("user.layout", 1, LibraryScope::User, "user") - 1607
.expect("user activation"); - 1608
assert_eq!( - 1609
library - 1610
.select_preferred("trip", "user", "ws") - 1611
.map(|s| s.spec.id.as_str()), - 1612
Some("user.layout") - 1613
); - 1614
} - 1615
- 1616
#[test] - 1617
fn built_in_seed_can_be_explicitly_activated_for_a_workspace() { - 1618
let seed = seeds::built_in_seed_pack() - 1619
.into_iter() - 1620
.next() - 1621
.expect("seed pack is non-empty"); - 1622
let id = seed.spec.id.clone(); - 1623
let revision = seed.spec.revision; - 1624
let accepts = seed.spec.accepts[0].clone(); - 1625
let mut library = PresentationLibrary::default(); - 1626
library.register(seed).expect("register seed"); - 1627
library - 1628
.activate(&id, revision, LibraryScope::Workspace, "a-workspace") - 1629
.expect("activate built-in"); - 1630
assert!( - 1631
library - 1632
.select(&accepts, LibraryScope::Workspace, "a-workspace") - 1633
.is_some() - 1634
); - 1635
} - 1636
- 1637
#[test] - 1638
fn built_in_seed_can_be_explicitly_activated_for_user_scope() { - 1639
let seed = seeds::built_in_seed_pack() - 1640
.into_iter() - 1641
.next() - 1642
.expect("seed pack is non-empty"); - 1643
let id = seed.spec.id.clone(); - 1644
let revision = seed.spec.revision; - 1645
let accepts = seed.spec.accepts[0].clone(); - 1646
let mut library = PresentationLibrary::default(); - 1647
library.register(seed).expect("register seed"); - 1648
library - 1649
.activate(&id, revision, LibraryScope::User, "user") - 1650
.expect("activate built-in for user"); - 1651
assert!( - 1652
library - 1653
.select(&accepts, LibraryScope::User, "user") - 1654
.is_some() - 1655
); - 1656
} - 1657
- 1658
#[test] - 1659
fn library_scope_json_matches_client_contract() { - 1660
assert_eq!( - 1661
serde_json::to_string(&LibraryScope::User).unwrap(), - 1662
"\"user\"" - 1663
); - 1664
assert_eq!( - 1665
serde_json::from_str::<LibraryScope>("\"workspace\"").unwrap(), - 1666
LibraryScope::Workspace - 1667
); - 1668
assert_eq!( - 1669
serde_json::from_str::<LibraryScope>("\"Workspace\"").unwrap(), - 1670
LibraryScope::Workspace - 1671
); - 1672
} - 1673
- 1674
#[test] - 1675
fn reset_restores_original_revision_without_deleting_history() { - 1676
let mut original = spec(); - 1677
original.id = "reset.card".into(); - 1678
let mut revision = original.clone(); - 1679
revision.revision = 2; - 1680
let mut library = PresentationLibrary::default(); - 1681
for candidate in [original, revision] { - 1682
library - 1683
.register(StoredPresentation { - 1684
digest: digest(&candidate).expect("digest"), - 1685
spec: candidate, - 1686
origin: PresentationOrigin { - 1687
scope: LibraryScope::Workspace, - 1688
owner: "ws".into(), - 1689
plugin_id: None, - 1690
generation: None, - 1691
}, - 1692
enabled: false, - 1693
}) - 1694
.expect("register"); - 1695
} - 1696
library - 1697
.activate("reset.card", 2, LibraryScope::Workspace, "ws") - 1698
.expect("activate revision"); - 1699
assert!(library.reset("reset.card", LibraryScope::Workspace, "ws")); - 1700
assert_eq!( - 1701
library - 1702
.select("trip", LibraryScope::Workspace, "ws") - 1703
.map(|entry| entry.spec.revision), - 1704
Some(1) - 1705
); - 1706
assert!(library.get("reset.card", 2).is_some()); - 1707
} - 1708
- 1709
#[test] - 1710
fn library_rejects_digest_mismatch() { - 1711
let spec = spec(); - 1712
let result = PresentationLibrary::default().register(StoredPresentation { - 1713
digest: "bad".into(), - 1714
spec, - 1715
origin: PresentationOrigin { - 1716
scope: LibraryScope::User, - 1717
owner: "user-1".into(), - 1718
plugin_id: None, - 1719
generation: None, - 1720
}, - 1721
enabled: true, - 1722
}); - 1723
assert!(matches!(result, Err(PresentationError::DigestMismatch))); - 1724
} - 1725
- 1726
#[test] - 1727
fn revision_is_immutable_and_runtime_numbered() { - 1728
let mut proposed = spec(); - 1729
proposed.revision = 99; - 1730
let revision = propose_revision( - 1731
PresentationRevisionRequest { - 1732
base_id: "test.timeline".into(), - 1733
base_revision: 1, - 1734
feedback: "make it shorter".into(), - 1735
attempt: 1, - 1736
}, - 1737
proposed, - 1738
) - 1739
.expect("revision"); - 1740
assert_eq!(revision.proposed.revision, 2); - 1741
assert_eq!(revision.digest, digest(&revision.proposed).expect("digest")); - 1742
} - 1743
- 1744
#[test] - 1745
fn revision_rejects_identity_and_retry_budget() { - 1746
let mut proposed = spec(); - 1747
proposed.id = "other".into(); - 1748
let result = propose_revision( - 1749
PresentationRevisionRequest { - 1750
base_id: "test.timeline".into(), - 1751
base_revision: 1, - 1752
feedback: "change".into(), - 1753
attempt: 1, - 1754
}, - 1755
proposed, - 1756
); - 1757
assert!(matches!( - 1758
result, - 1759
Err(PresentationError::RevisionIdentityChanged) - 1760
)); - 1761
let result = propose_revision( - 1762
PresentationRevisionRequest { - 1763
base_id: "test.timeline".into(), - 1764
base_revision: 1, - 1765
feedback: "change".into(), - 1766
attempt: 3, - 1767
}, - 1768
spec(), - 1769
); - 1770
assert!(matches!( - 1771
result, - 1772
Err(PresentationError::RevisionBudgetExceeded) - 1773
)); - 1774
} - 1775
- 1776
#[test] - 1777
fn revoking_plugin_removes_definitions_and_activations() { - 1778
let spec = spec(); - 1779
let mut library = PresentationLibrary::default(); - 1780
library - 1781
.register(StoredPresentation { - 1782
digest: digest(&spec).expect("digest"), - 1783
spec, - 1784
origin: PresentationOrigin { - 1785
scope: LibraryScope::Workspace, - 1786
owner: "workspace".into(), - 1787
plugin_id: Some("pack.demo".into()), - 1788
generation: Some("1.0.0".into()), - 1789
}, - 1790
enabled: false, - 1791
}) - 1792
.expect("register"); - 1793
library - 1794
.activate("test.timeline", 1, LibraryScope::Workspace, "workspace") - 1795
.expect("activate"); - 1796
assert_eq!(library.revoke_plugin("pack.demo"), 1); - 1797
assert!(library.activations().is_empty()); - 1798
assert!(library.get("test.timeline", 1).is_none()); - 1799
} - 1800
} - 1801
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.