- 1001
}], - 1002
}; - 1003
assert!(template.validate().is_ok()); - 1004
})); - 1005
- 1006
// Template validation: empty id - 1007
scenarios.push(tc("empty_id_invalid", || { - 1008
let template = TemplateSpec { - 1009
id: "".into(), - 1010
revision: 1, - 1011
origin: TemplateOrigin::User, - 1012
activation: TemplateActivation::Active, - 1013
nodes: vec![TemplateNode::Literal { text: "x".into() }], - 1014
}; - 1015
assert!(template.validate().is_err()); - 1016
})); - 1017
- 1018
// Template validation: too many nodes - 1019
scenarios.push(tc("too_many_nodes_invalid", || { - 1020
let nodes: Vec<TemplateNode> = (0..129) - 1021
.map(|_| TemplateNode::Literal { text: "x".into() }) - 1022
.collect(); - 1023
let template = TemplateSpec { - 1024
id: "many".into(), - 1025
revision: 1, - 1026
origin: TemplateOrigin::User, - 1027
activation: TemplateActivation::Active, - 1028
nodes, - 1029
}; - 1030
assert!(template.validate().is_err()); - 1031
})); - 1032
- 1033
scenarios.push(tc("max_nodes_ok", || { - 1034
let nodes: Vec<TemplateNode> = (0..128) - 1035
.map(|_| TemplateNode::Literal { text: "x".into() }) - 1036
.collect(); - 1037
let template = TemplateSpec { - 1038
id: "max128".into(), - 1039
revision: 1, - 1040
origin: TemplateOrigin::User, - 1041
activation: TemplateActivation::Active, - 1042
nodes, - 1043
}; - 1044
assert!(template.validate().is_ok()); - 1045
})); - 1046
- 1047
// Deep nesting - 1048
scenarios.push(tc("deep_nesting_exceeds_max_invalid", || { - 1049
let mut inner = TemplateNode::Literal { text: "x".into() }; - 1050
for _ in 0..10 { - 1051
inner = TemplateNode::IfPresent { - 1052
slot: TemplateSlot::Body, - 1053
then_nodes: vec![inner], - 1054
else_nodes: vec![], - 1055
}; - 1056
} - 1057
let template = TemplateSpec { - 1058
id: "deep".into(), - 1059
revision: 1, - 1060
origin: TemplateOrigin::User, - 1061
activation: TemplateActivation::Active, - 1062
nodes: vec![inner], - 1063
}; - 1064
assert!(template.validate().is_err()); - 1065
})); - 1066
- 1067
scenarios.push(tc("max_depth_ok", || { - 1068
let mut inner = TemplateNode::Literal { text: "x".into() }; - 1069
for _ in 0..8 { - 1070
inner = TemplateNode::IfPresent { - 1071
slot: TemplateSlot::Body, - 1072
then_nodes: vec![inner], - 1073
else_nodes: vec![], - 1074
}; - 1075
} - 1076
let template = TemplateSpec { - 1077
id: "max_depth".into(), - 1078
revision: 1, - 1079
origin: TemplateOrigin::User, - 1080
activation: TemplateActivation::Active, - 1081
nodes: vec![inner], - 1082
}; - 1083
assert!(template.validate().is_ok()); - 1084
})); - 1085
- 1086
// Literal too long - 1087
scenarios.push(tc("literal_too_long_invalid", || { - 1088
let template = TemplateSpec { - 1089
id: "long".into(), - 1090
revision: 1, - 1091
origin: TemplateOrigin::User, - 1092
activation: TemplateActivation::Active, - 1093
nodes: vec![TemplateNode::Literal { - 1094
text: "x".repeat(16385), - 1095
}], - 1096
}; - 1097
assert!(template.validate().is_err()); - 1098
})); - 1099
- 1100
scenarios.push(tc("literal_max_len_ok", || { - 1101
let template = TemplateSpec { - 1102
id: "max_long".into(), - 1103
revision: 1, - 1104
origin: TemplateOrigin::User, - 1105
activation: TemplateActivation::Active, - 1106
nodes: vec![TemplateNode::Literal { - 1107
text: "x".repeat(16384), - 1108
}], - 1109
}; - 1110
assert!(template.validate().is_ok()); - 1111
})); - 1112
- 1113
// Template render with actual answer - 1114
scenarios.push(tc("template_render_replaces_slots", || { - 1115
let mut reg = TemplateRegistry::default(); - 1116
reg.upsert(make_template( - 1117
"rend1", - 1118
TemplateOrigin::User, - 1119
TemplateActivation::Active, - 1120
)) - 1121
.unwrap(); - 1122
let template = reg.resolve("rend1").unwrap(); - 1123
assert_eq!(template.nodes.len(), 4); - 1124
})); - 1125
- 1126
run("templates", scenarios); - 1127
} - 1128
- 1129
// ════════════════════════════════════════════════════════════════════ - 1130
// 7. POSTURE DISPOSITION MATRIX - 1131
// ════════════════════════════════════════════════════════════════════ - 1132
- 1133
#[test] - 1134
fn audit_posture_matrix() { - 1135
let mut scenarios: Vec<Scenario> = vec![]; - 1136
- 1137
let cadences = [Cadence::Live, Cadence::OnCompletion, Cadence::Digest]; - 1138
let urgencies = [Urgency::Interrupt, Urgency::Notify, Urgency::Quiet]; - 1139
let kinds = [ - 1140
DeliveryKind::Assistant, - 1141
DeliveryKind::TaskSummary, - 1142
DeliveryKind::Alert, - 1143
DeliveryKind::User, - 1144
DeliveryKind::Approval, - 1145
DeliveryKind::Progress, - 1146
DeliveryKind::ToolResult, - 1147
DeliveryKind::System, - 1148
DeliveryKind::Developer, - 1149
DeliveryKind::Internal, - 1150
DeliveryKind::ToolCall, - 1151
DeliveryKind::Steering, - 1152
]; - 1153
- 1154
for cadence in cadences { - 1155
for urgency in urgencies { - 1156
for kind in kinds { - 1157
let c = cadence; - 1158
let u = urgency; - 1159
let k = kind; - 1160
let label = format!("disposition_{:?}_{:?}_{:?}", c, u, k); - 1161
scenarios.push(tc(&label, move || { - 1162
let posture = DeliveryPosture { - 1163
cadence: c, - 1164
urgency: u, - 1165
}; - 1166
let disp = posture.disposition(k); - 1167
// Interrupt always sends - 1168
if u == Urgency::Interrupt { - 1169
assert_eq!(disp, Disposition::Send); - 1170
} - 1171
// Approval always sends - 1172
if k == DeliveryKind::Approval { - 1173
assert_eq!(disp, Disposition::Send); - 1174
} - 1175
// Alert under Digest always sends - 1176
if k == DeliveryKind::Alert && c == Cadence::Digest { - 1177
assert_eq!(disp, Disposition::Send); - 1178
} - 1179
// Live + non-interrupt sends everything - 1180
if c == Cadence::Live && u != Urgency::Interrupt { - 1181
if k != DeliveryKind::Approval { - 1182
assert_eq!(disp, Disposition::Send); - 1183
} - 1184
} - 1185
// Digest + Quiet + non-alert + non-approval → hold for digest - 1186
if c == Cadence::Digest - 1187
&& u == Urgency::Quiet - 1188
&& k != DeliveryKind::Alert - 1189
&& k != DeliveryKind::Approval - 1190
{ - 1191
assert_eq!(disp, Disposition::HoldForDigest); - 1192
} - 1193
// OnCompletion + Notify: progress/tool chatter held - 1194
if c == Cadence::OnCompletion && u == Urgency::Notify { - 1195
match k { - 1196
DeliveryKind::Progress - 1197
| DeliveryKind::ToolResult - 1198
| DeliveryKind::ToolCall => { - 1199
assert_eq!(disp, Disposition::HoldUntilComplete); - 1200
} - 1201
_ => { - 1202
assert_eq!(disp, Disposition::Send); - 1203
} - 1204
} - 1205
} - 1206
})); - 1207
} - 1208
} - 1209
} - 1210
- 1211
// Default posture sends everything - 1212
scenarios.push(tc("default_posture_sends_all", || { - 1213
let default = DeliveryPosture::default(); - 1214
for kind in [ - 1215
DeliveryKind::Assistant, - 1216
DeliveryKind::Progress, - 1217
DeliveryKind::Alert, - 1218
DeliveryKind::Approval, - 1219
DeliveryKind::ToolResult, - 1220
] { - 1221
assert_eq!(default.disposition(kind), Disposition::Send); - 1222
} - 1223
})); - 1224
- 1225
// Cadence variants serialize - 1226
for cadence in cadences { - 1227
scenarios.push(tc(&format!("serde_cadence_{cadence:?}"), move || { - 1228
let bytes = serde_json::to_vec(&cadence).expect("serialize"); - 1229
let deserialized: Cadence = serde_json::from_slice(&bytes).expect("deserialize"); - 1230
assert_eq!(deserialized, cadence); - 1231
})); - 1232
} - 1233
- 1234
run("posture_matrix", scenarios); - 1235
} - 1236
- 1237
// ════════════════════════════════════════════════════════════════════ - 1238
// 8. SKILL REGISTRY & VALIDATION - 1239
// ════════════════════════════════════════════════════════════════════ - 1240
- 1241
#[test] - 1242
fn audit_skill_registry() { - 1243
let mut scenarios: Vec<Scenario> = vec![]; - 1244
- 1245
let expected_types = [ - 1246
"link.preview", - 1247
"metric", - 1248
"chart", - 1249
"media.image", - 1250
"media.video", - 1251
"media.audio", - 1252
"research.synthesis", - 1253
"itinerary", - 1254
"coding.diff", - 1255
"test.report", - 1256
"terminal.view", - 1257
"data.grid", - 1258
"recipe.card", - 1259
"ui.preview", - 1260
"plan.timeline", - 1261
]; - 1262
for (i, st) in expected_types.iter().enumerate() { - 1263
let s = (*st).to_string(); - 1264
scenarios.push(tc(&format!("builtin_type_{i}_{s}"), move || { - 1265
let registry = built_in_skill_registry(); - 1266
assert!(registry.find_by_type(&s).is_some(), "missing {s}"); - 1267
})); - 1268
} - 1269
- 1270
// Register valid manifest - 1271
scenarios.push(tc("register_valid", || { - 1272
let mut registry = SkillRegistry::default(); - 1273
let manifest = PresentationSkillManifest { - 1274
id: "test.skill".into(), - 1275
version: "1.0.0".into(), - 1276
api: PRESENTATION_SKILL_API.into(), - 1277
provides: vec!["test.type".into()], - 1278
renderers: BTreeMap::from([( - 1279
"desktop".into(), - 1280
RendererBinding { - 1281
renderer: "native:structured".into(), - 1282
interactive: false, - 1283
requires: vec![], - 1284
}, - 1285
)]), - 1286
schema: None, - 1287
outcome_requirements: vec![], - 1288
}; - 1289
assert!(registry.register(manifest).is_ok()); - 1290
})); - 1291
- 1292
scenarios.push(tc("register_empty_id_invalid", || { - 1293
let mut registry = SkillRegistry::default(); - 1294
let manifest = PresentationSkillManifest { - 1295
id: "".into(), - 1296
version: "1.0.0".into(), - 1297
api: PRESENTATION_SKILL_API.into(), - 1298
provides: vec!["t".into()], - 1299
renderers: BTreeMap::new(), - 1300
schema: None, - 1301
outcome_requirements: vec![], - 1302
}; - 1303
assert!(matches!( - 1304
registry.register(manifest), - 1305
Err(SkillError::InvalidManifest(_)) - 1306
)); - 1307
})); - 1308
- 1309
scenarios.push(tc("register_empty_version_invalid", || { - 1310
let mut registry = SkillRegistry::default(); - 1311
let manifest = PresentationSkillManifest { - 1312
id: "x".into(), - 1313
version: "".into(), - 1314
api: PRESENTATION_SKILL_API.into(), - 1315
provides: vec!["t".into()], - 1316
renderers: BTreeMap::new(), - 1317
schema: None, - 1318
outcome_requirements: vec![], - 1319
}; - 1320
assert!(matches!( - 1321
registry.register(manifest), - 1322
Err(SkillError::InvalidManifest(_)) - 1323
)); - 1324
})); - 1325
- 1326
scenarios.push(tc("register_wrong_api_invalid", || { - 1327
let mut registry = SkillRegistry::default(); - 1328
let manifest = PresentationSkillManifest { - 1329
id: "x".into(), - 1330
version: "1.0.0".into(), - 1331
api: "wrong".into(), - 1332
provides: vec!["t".into()], - 1333
renderers: BTreeMap::new(), - 1334
schema: None, - 1335
outcome_requirements: vec![], - 1336
}; - 1337
assert!(matches!( - 1338
registry.register(manifest), - 1339
Err(SkillError::InvalidManifest(_)) - 1340
)); - 1341
})); - 1342
- 1343
scenarios.push(tc("register_empty_type_invalid", || { - 1344
let mut registry = SkillRegistry::default(); - 1345
let manifest = PresentationSkillManifest { - 1346
id: "x".into(), - 1347
version: "1.0.0".into(), - 1348
api: PRESENTATION_SKILL_API.into(), - 1349
provides: vec!["".into()], - 1350
renderers: BTreeMap::new(), - 1351
schema: None, - 1352
outcome_requirements: vec![], - 1353
}; - 1354
assert!(matches!( - 1355
registry.register(manifest), - 1356
Err(SkillError::InvalidManifest(_)) - 1357
)); - 1358
})); - 1359
- 1360
scenarios.push(tc("register_duplicate_owned_type_invalid", || { - 1361
let mut registry = built_in_skill_registry(); - 1362
let manifest = PresentationSkillManifest { - 1363
id: "x".into(), - 1364
version: "1.0.0".into(), - 1365
api: PRESENTATION_SKILL_API.into(), - 1366
provides: vec!["metric".into()], - 1367
renderers: BTreeMap::new(), - 1368
schema: None, - 1369
outcome_requirements: vec![], - 1370
}; - 1371
assert!(matches!( - 1372
registry.register(manifest), - 1373
Err(SkillError::InvalidManifest(_)) - 1374
)); - 1375
})); - 1376
- 1377
scenarios.push(tc("register_overwrite_same_id_ok", || { - 1378
let mut registry = SkillRegistry::default(); - 1379
let manifest = PresentationSkillManifest { - 1380
id: "x".into(), - 1381
version: "1.0.0".into(), - 1382
api: PRESENTATION_SKILL_API.into(), - 1383
provides: vec!["test.type".into()], - 1384
renderers: BTreeMap::new(), - 1385
schema: None, - 1386
outcome_requirements: vec![], - 1387
}; - 1388
assert!(registry.register(manifest.clone()).is_ok()); - 1389
assert!(registry.register(manifest).is_ok()); - 1390
})); - 1391
- 1392
// find_by_type - 1393
scenarios.push(tc("find_by_type_returns_core", || { - 1394
let registry = built_in_skill_registry(); - 1395
let result = registry.find_by_type("metric").expect("metric"); - 1396
assert_eq!(result.0, "core"); - 1397
assert_eq!(result.1, "1.0.0"); - 1398
})); - 1399
- 1400
scenarios.push(tc("find_by_type_unknown_none", || { - 1401
let registry = built_in_skill_registry(); - 1402
assert!(registry.find_by_type("nonexistent").is_none()); - 1403
})); - 1404
- 1405
// get - 1406
scenarios.push(tc("get_existing_skill", || { - 1407
let registry = built_in_skill_registry(); - 1408
assert!(registry.get("core").is_some()); - 1409
})); - 1410
- 1411
scenarios.push(tc("get_unknown_skill_none", || { - 1412
let registry = built_in_skill_registry(); - 1413
assert!(registry.get("unknown").is_none()); - 1414
})); - 1415
- 1416
// parse_fragment valid - 1417
let valid_fragments: &[&str] = &[ - 1418
r#"{"semantic_type":"metric","payload":{"label":"X","value":42,"unit":"%"}}"#, - 1419
r#"{"semantic_type":"link.preview","payload":{"url":"https://x.com","title":"T"}}"#, - 1420
r#"{"semantic_type":"collection","payload":{"items":["a","b"]}}"#, - 1421
r#"{"semantic_type":"detail","payload":{"title":"T"}}"#, - 1422
r#"{"semantic_type":"comparison","payload":{"left":"a","right":"b"}}"#, - 1423
r#"{"semantic_type":"checklist","payload":{"items":[{"label":"a","done":true}]}}"#, - 1424
r#"{"semantic_type":"timeline","payload":{"items":[{"label":"step"}]}}"#, - 1425
r#"{"semantic_type":"steps","payload":{"items":[{"label":"s"}]}}"#, - 1426
r#"{"semantic_type":"schedule","payload":{"slots":[{"label":"morning"}]}}"#, - 1427
r#"{"semantic_type":"meeting_notes","payload":{"agenda":[{"title":"A"}],"attendees":["x"]}}"#, - 1428
r#"{"semantic_type":"decision","payload":{"label":"Choose X","choices":["A","B"]}}"#, - 1429
r#"{"semantic_type":"budget","payload":{"label":"Income","income":100,"expenses":50}}"#, - 1430
]; - 1431
for (i, frag) in valid_fragments.iter().enumerate() { - 1432
let f = frag.to_string(); - 1433
scenarios.push(tc(&format!("parse_fragment_valid_{i}"), move || { - 1434
let result = parse_fragment(&f); - 1435
assert!(result.is_ok(), "fragment {i}: {result:?}"); - 1436
})); - 1437
} - 1438
- 1439
// parse_fragment invalid - 1440
let invalid_fragments: &[&str] = &[ - 1441
r#"not json"#, - 1442
r#"{"semantic_type":123}"#, - 1443
r#"{"semantic_type":"unknown.type","payload":{}}"#, - 1444
r#""{"missing": "quotes"}"#, - 1445
]; - 1446
for (i, frag) in invalid_fragments.iter().enumerate() { - 1447
let f = frag.to_string(); - 1448
scenarios.push(tc(&format!("parse_fragment_invalid_{i}"), move || { - 1449
assert!(parse_fragment(&f).is_err()); - 1450
})); - 1451
} - 1452
- 1453
// validate metric - 1454
scenarios.push(tc("validate_metric_ok", || { - 1455
let registry = built_in_skill_registry(); - 1456
let output = StructuredOutput { - 1457
semantic_type: "metric".into(), - 1458
schema_version: PRESENTATION_SCHEMA_VERSION, - 1459
skill_id: "core".into(), - 1460
skill_version: "1.0.0".into(), - 1461
payload: json!({"label": "X", "value": 42, "unit": "%"}), - 1462
}; - 1463
assert!(registry.validate(&output, "desktop", &[]).is_ok()); - 1464
})); - 1465
- 1466
scenarios.push(tc("validate_metric_missing_label_fails", || { - 1467
let registry = built_in_skill_registry(); - 1468
let output = StructuredOutput { - 1469
semantic_type: "metric".into(), - 1470
schema_version: PRESENTATION_SCHEMA_VERSION, - 1471
skill_id: "core".into(), - 1472
skill_version: "1.0.0".into(), - 1473
payload: json!({"value": 42}), - 1474
}; - 1475
assert!(registry.validate(&output, "desktop", &[]).is_err()); - 1476
})); - 1477
- 1478
scenarios.push(tc("validate_unknown_skill_fails", || { - 1479
let registry = built_in_skill_registry(); - 1480
let output = StructuredOutput { - 1481
semantic_type: "metric".into(), - 1482
schema_version: PRESENTATION_SCHEMA_VERSION, - 1483
skill_id: "unknown".into(), - 1484
skill_version: "1.0.0".into(), - 1485
payload: json!({"label": "X", "value": 42}), - 1486
}; - 1487
assert!(matches!( - 1488
registry.validate(&output, "desktop", &[]), - 1489
Err(SkillError::UnknownType(_)) - 1490
)); - 1491
})); - 1492
- 1493
scenarios.push(tc("validate_wrong_schema_fails", || { - 1494
let registry = built_in_skill_registry(); - 1495
let output = StructuredOutput { - 1496
semantic_type: "metric".into(), - 1497
schema_version: 999, - 1498
skill_id: "core".into(), - 1499
skill_version: "1.0.0".into(), - 1500
payload: json!({"label": "X", "value": 42}), - 1501
}; - 1502
assert!(matches!( - 1503
registry.validate(&output, "desktop", &[]), - 1504
Err(SkillError::InvalidPayload(_)) - 1505
)); - 1506
})); - 1507
- 1508
scenarios.push(tc("validate_wrong_version_fails", || { - 1509
let registry = built_in_skill_registry(); - 1510
let output = StructuredOutput { - 1511
semantic_type: "metric".into(), - 1512
schema_version: PRESENTATION_SCHEMA_VERSION, - 1513
skill_id: "core".into(), - 1514
skill_version: "2.0.0".into(), - 1515
payload: json!({"label": "X", "value": 42}), - 1516
}; - 1517
assert!(matches!( - 1518
registry.validate(&output, "desktop", &[]), - 1519
Err(SkillError::InvalidPayload(_)) - 1520
)); - 1521
})); - 1522
- 1523
scenarios.push(tc("validate_missing_surface_renderer_fails", || { - 1524
let registry = built_in_skill_registry(); - 1525
let output = StructuredOutput { - 1526
semantic_type: "metric".into(), - 1527
schema_version: PRESENTATION_SCHEMA_VERSION, - 1528
skill_id: "core".into(), - 1529
skill_version: "1.0.0".into(), - 1530
payload: json!({"label": "X", "value": 42}), - 1531
}; - 1532
assert!(matches!( - 1533
registry.validate(&output, "unknown_surface", &[]), - 1534
Err(SkillError::MissingCapability(_)) - 1535
)); - 1536
})); - 1537
- 1538
scenarios.push(tc("validate_unknown_type_fails", || { - 1539
let registry = built_in_skill_registry(); - 1540
let output = StructuredOutput { - 1541
semantic_type: "nonexistent".into(), - 1542
schema_version: PRESENTATION_SCHEMA_VERSION, - 1543
skill_id: "core".into(), - 1544
skill_version: "1.0.0".into(), - 1545
payload: json!({}), - 1546
}; - 1547
assert!(matches!( - 1548
registry.validate(&output, "desktop", &[]), - 1549
Err(SkillError::UnknownType(_)) - 1550
)); - 1551
})); - 1552
- 1553
// structured_outputs_from_text - 1554
scenarios.push(tc("outputs_bare_json", || { - 1555
let outputs = structured_outputs_from_text( - 1556
r#"{"semantic_type":"metric","payload":{"label":"X","value":42}}"#, - 1557
); - 1558
assert_eq!(outputs.len(), 1); - 1559
assert_eq!(outputs[0].semantic_type, "metric"); - 1560
})); - 1561
- 1562
scenarios.push(tc("outputs_fenced_vak", || { - 1563
let text = "Data\n```vak\n{\"semantic_type\":\"metric\",\"payload\":{\"label\":\"X\",\"value\":1}}\n```\n"; - 1564
let outputs = structured_outputs_from_text(text); - 1565
assert_eq!(outputs.len(), 1); - 1566
})); - 1567
- 1568
scenarios.push(tc("outputs_plain_no_output", || { - 1569
assert!(structured_outputs_from_text("just plain text").is_empty()); - 1570
})); - 1571
- 1572
scenarios.push(tc("outputs_multiple_fences", || { - 1573
let text = "```vak\n{\"semantic_type\":\"metric\",\"payload\":{\"label\":\"A\",\"value\":1}}\n```\n```vak\n{\"semantic_type\":\"collection\",\"payload\":{\"items\":[\"a\",\"b\"]}}\n```"; - 1574
assert_eq!(structured_outputs_from_text(text).len(), 2); - 1575
})); - 1576
- 1577
scenarios.push(tc("outputs_dedup_same", || { - 1578
let text = "```vak\n{\"semantic_type\":\"metric\",\"payload\":{\"label\":\"A\",\"value\":1}}\n```\n```vak\n{\"semantic_type\":\"metric\",\"payload\":{\"label\":\"A\",\"value\":1}}\n```"; - 1579
assert_eq!(structured_outputs_from_text(text).len(), 1); - 1580
})); - 1581
- 1582
run("skill_registry", scenarios); - 1583
} - 1584
- 1585
// ════════════════════════════════════════════════════════════════════ - 1586
// 9. SIGNALS - 1587
// ════════════════════════════════════════════════════════════════════ - 1588
- 1589
#[test] - 1590
fn audit_signals() { - 1591
let mut scenarios: Vec<Scenario> = vec![]; - 1592
- 1593
let signal_cases: &[(&str, &[&str])] = &[ - 1594
("https://example.com source: x", &["citations"]), - 1595
("according to references", &["multiple_sources"]), - 1596
( - 1597
"key takeaways findings", - 1598
&["research", "synthesis", "takeaways"], - 1599
), - 1600
("temperature is 25°c", &["temperature"]), - 1601
("forecast humidity wind speed", &["forecast"]), - 1602
("travel itinerary flight hotel", &["travel"]), - 1603
("latest headlines news", &["news"]), - 1604
("diff --git a b", &["diff"]), - 1605
("files changed modified: created:", &["files_changed"]), - 1606
("tests passed", &["tests", "pass_fail"]), - 1607
("test suite test report", &["tests"]), - 1608
( - 1609
"benchmark req/sec p99 throughput", - 1610
&["benchmark", "telemetry"], - 1611
), - 1612
("chart plot graph trend", &["chart"]), - 1613
("telemetry metrics kpi latency", &["telemetry"]), - 1614
("table dataset mrr", &["table_data"]), - 1615
("recipe servings cook time", &["recipe"]), - 1616
("ingredients tbsp tsp", &["ingredients"]), - 1617
("artifact download", &["artifact"]), - 1618
("approval approve permission", &["approval"]), - 1619
("allow once deny", &["action"]), - 1620
("docker ps container ports", &["docker", "terminal"]), - 1621
("exit 0", &["terminal", "command_exec"]), - 1622
("no signals here at all", &[]), - 1623
("plain text without keywords", &[]), - 1624
("", &[]), - 1625
("temperature forecast", &["temperature", "forecast"]), - 1626
("https://a.com https://b.com", &["citations"]), - 1627
( - 1628
"research synthesis verified sources", - 1629
&["research", "synthesis", "takeaways"], - 1630
), - 1631
("tests passed failed failures", &["tests", "pass_fail"]), - 1632
( - 1633
"chart telemetry kpi latency p99 throughput", - 1634
&["chart", "telemetry"], - 1635
), - 1636
("diff files_changed modified: created:", &["files_changed"]), - 1637
]; - 1638
- 1639
for (i, (text, expected)) in signal_cases.iter().enumerate() { - 1640
let t = text.to_string(); - 1641
let expected_vec: Vec<String> = expected.iter().map(|s| s.to_string()).collect(); - 1642
scenarios.push(tc(&format!("signals_{i}"), move || { - 1643
let signals = signals_from_text(&t); - 1644
for expected_sig in &expected_vec { - 1645
assert!( - 1646
signals.contains(expected_sig), - 1647
"missing {expected_sig} in {signals:?} for {t:?}" - 1648
); - 1649
} - 1650
})); - 1651
} - 1652
- 1653
// signals_from_context - 1654
scenarios.push(tc("context_no_tool", || { - 1655
let ctx = SignalContext { - 1656
text: "plain text", - 1657
tool_name: None, - 1658
tool_input: None, - 1659
tool_output: None, - 1660
is_error: false, - 1661
domains: &[], - 1662
}; - 1663
let signals = signals_from_context(&ctx); - 1664
assert!(!signals.contains(&"terminal".to_string())); - 1665
})); - 1666
- 1667
scenarios.push(tc("context_bash_adds_terminal", || { - 1668
let ctx = SignalContext { - 1669
text: "running command", - 1670
tool_name: Some("bash"), - 1671
tool_input: None, - 1672
tool_output: None, - 1673
is_error: false, - 1674
domains: &[], - 1675
}; - 1676
let signals = signals_from_context(&ctx); - 1677
assert!(signals.contains(&"terminal".to_string())); - 1678
assert!(signals.contains(&"command_exec".to_string())); - 1679
})); - 1680
- 1681
scenarios.push(tc("context_bash_test_output", || { - 1682
let input = json!({"command": "cargo test"}); - 1683
let ctx = SignalContext { - 1684
text: "running", - 1685
tool_name: Some("bash"), - 1686
tool_input: Some(&input), - 1687
tool_output: None, - 1688
is_error: false, - 1689
domains: &[], - 1690
}; - 1691
let signals = signals_from_context(&ctx); - 1692
assert!(signals.contains(&"tests".to_string())); - 1693
assert!(signals.contains(&"pass_fail".to_string())); - 1694
})); - 1695
- 1696
scenarios.push(tc("context_edit_adds_diff", || { - 1697
let ctx = SignalContext { - 1698
text: "editing", - 1699
tool_name: Some("edit"), - 1700
tool_input: None, - 1701
tool_output: None, - 1702
is_error: false, - 1703
domains: &[], - 1704
}; - 1705
let signals = signals_from_context(&ctx); - 1706
assert!(signals.contains(&"diff".to_string())); - 1707
assert!(signals.contains(&"files_changed".to_string())); - 1708
})); - 1709
- 1710
scenarios.push(tc("context_web_domain_adds_research", || { - 1711
let ctx = SignalContext { - 1712
text: "searching", - 1713
tool_name: Some("some_configured_mcp_tool"), - 1714
tool_input: None, - 1715
tool_output: None, - 1716
is_error: false, - 1717
domains: &["web"], - 1718
}; - 1719
let signals = signals_from_context(&ctx); - 1720
assert!(signals.contains(&"citations".to_string())); - 1721
assert!(signals.contains(&"research".to_string())); - 1722
})); - 1723
- 1724
scenarios.push(tc("context_live_data_domain_adds_research", || { - 1725
let ctx = SignalContext { - 1726
text: "searching", - 1727
tool_name: Some("some_configured_mcp_tool"), - 1728
tool_input: None, - 1729
tool_output: None, - 1730
is_error: false, - 1731
domains: &["live-data"], - 1732
}; - 1733
let signals = signals_from_context(&ctx); - 1734
assert!(signals.contains(&"research".to_string())); - 1735
assert!(signals.contains(&"synthesis".to_string())); - 1736
})); - 1737
- 1738
scenarios.push(tc("context_tool_name_alone_never_adds_research", || { - 1739
// The name of a tool must never drive this signal — only its - 1740
// capability's declared domains do. A tool named after a search - 1741
// vendor but with no declared domain gets nothing here. - 1742
let ctx = SignalContext { - 1743
text: "searching", - 1744
tool_name: Some("tavily_search"), - 1745
tool_input: None, - 1746
tool_output: None, - 1747
is_error: false, - 1748
domains: &[], - 1749
}; - 1750
let signals = signals_from_context(&ctx); - 1751
assert!(!signals.contains(&"research".to_string())); - 1752
})); - 1753
- 1754
scenarios.push(tc("context_output_test_result", || { - 1755
let ctx = SignalContext { - 1756
text: "done", - 1757
tool_name: None, - 1758
tool_input: None, - 1759
tool_output: Some("test result: 5 passed; 0 failed"), - 1760
is_error: false, - 1761
domains: &[], - 1762
}; - 1763
let signals = signals_from_context(&ctx); - 1764
assert!(signals.contains(&"tests".to_string())); - 1765
assert!(signals.contains(&"pass_fail".to_string())); - 1766
})); - 1767
- 1768
scenarios.push(tc("context_output_diff", || { - 1769
let ctx = SignalContext { - 1770
text: "done", - 1771
tool_name: None, - 1772
tool_input: None, - 1773
tool_output: Some("diff --git a/file b/file"), - 1774
is_error: false, - 1775
domains: &[], - 1776
}; - 1777
let signals = signals_from_context(&ctx); - 1778
assert!(signals.contains(&"diff".to_string())); - 1779
assert!(signals.contains(&"files_changed".to_string())); - 1780
})); - 1781
- 1782
scenarios.push(tc("context_is_error_doesnt_crash", || { - 1783
let ctx = SignalContext { - 1784
text: "error", - 1785
tool_name: Some("bash"), - 1786
tool_input: None, - 1787
tool_output: None, - 1788
is_error: true, - 1789
domains: &[], - 1790
}; - 1791
let signals = signals_from_context(&ctx); - 1792
assert!(signals.contains(&"terminal".to_string())); - 1793
})); - 1794
- 1795
run("signals", scenarios); - 1796
} - 1797
- 1798
// ════════════════════════════════════════════════════════════════════ - 1799
// 10. RECIPE SELECTION - 1800
// ════════════════════════════════════════════════════════════════════ - 1801
- 1802
#[test] - 1803
fn audit_recipe_selection() { - 1804
let mut scenarios: Vec<Scenario> = vec![]; - 1805
- 1806
// Cases that work with plain `choose` (no typed output required) - 1807
let choose_cases: &[(&str, &[&str], &str)] = &[ - 1808
("detail", &[], "answer.basic"), - 1809
( - 1810
"terminal.view", - 1811
&["terminal", "command_exec"], - 1812
"terminal.session", - 1813
), - 1814
("chart", &["chart", "telemetry"], "data.multi_chart"), - 1815
( - 1816
"data.grid", - 1817
&["table_data", "tabular"], - 1818
"data.spreadsheet_grid", - 1819
), - 1820
( - 1821
"recipe.card", - 1822
&["recipe", "ingredients"], - 1823
"lifestyle.culinary_recipe", - 1824
), - 1825
("plan.timeline", &["plan", "timeline"], "plan.timeline"), - 1826
("itinerary", &["travel", "itinerary"], "travel.itinerary"), - 1827
( - 1828
"research.synthesis", - 1829
&["research", "synthesis", "takeaways"], - 1830
"research.synthesis", - 1831
), - 1832
("metric", &["temperature", "forecast"], "weather.forecast"), - 1833
("collection", &["chart", "telemetry"], "data.multi_chart"), - 1834
]; - 1835
- 1836
for (i, (etype, signals, expected)) in choose_cases.iter().enumerate() { - 1837
let st = etype.to_string(); - 1838
let sigs: Vec<String> = signals.iter().map(|s| s.to_string()).collect(); - 1839
let er = expected.to_string(); - 1840
let cat = built_in_recipes(); - 1841
scenarios.push(tc(&format!("recipe_{i}"), move || { - 1842
let decision = cat.choose(&sigs, "desktop"); - 1843
assert!( - 1844
decision.is_some(), - 1845
"no recipe for {st} with signals {sigs:?}" - 1846
); - 1847
assert_eq!(decision.unwrap().recipe_id, er); - 1848
})); - 1849
} - 1850
- 1851
// Cases requiring typed output (use choose_for_types) - 1852
let typed_cases: &[(&str, &[&str], &[&str], &str)] = &[ - 1853
( - 1854
"coding.diff", - 1855
&["diff", "files_changed"], - 1856
&["coding.diff"], - 1857
"coding.diff_inspector", - 1858
), - 1859
( - 1860
"test.report", - 1861
&["tests", "pass_fail"], - 1862
&["test.report"], - 1863
"coding.test_report", - 1864
), - 1865
("ui.preview", &[], &["ui.preview"], "ui.preview"), - 1866
]; - 1867
- 1868
for (i, (etype, signals, types, expected)) in typed_cases.iter().enumerate() { - 1869
let st = etype.to_string(); - 1870
let sigs: Vec<String> = signals.iter().map(|s| s.to_string()).collect(); - 1871
let at: Vec<String> = types.iter().map(|s| s.to_string()).collect(); - 1872
let er = expected.to_string(); - 1873
let cat = built_in_recipes(); - 1874
scenarios.push(tc(&format!("typed_recipe_{i}"), move || { - 1875
let decision = cat.choose_for_types(&sigs, "desktop", &at); - 1876
assert!( - 1877
decision.is_some(), - 1878
"no recipe for {st} with signals {sigs:?} types {at:?}" - 1879
); - 1880
assert_eq!(decision.unwrap().recipe_id, er); - 1881
})); - 1882
} - 1883
- 1884
// Surface filtering: some recipes are desktop/terminal-only - 1885
let surface_filtered: &[(&str, &[&str], &str, &str)] = &[ - 1886
( - 1887
"coding.diff", - 1888
&["diff", "files_changed"], - 1889
"desktop", - 1890
"coding.diff_inspector", - 1891
), - 1892
( - 1893
"coding.diff", - 1894
&["diff", "files_changed"], - 1895
"terminal", - 1896
"coding.diff_inspector", - 1897
), - 1898
( - 1899
"coding.diff", - 1900
&["diff", "files_changed"], - 1901
"telegram", - 1902
"answer.basic", - 1903
), - 1904
( - 1905
"research.synthesis", - 1906
&["research", "synthesis", "takeaways"], - 1907
"telegram", - 1908
"answer.basic", - 1909
), - 1910
( - 1911
"research.synthesis", - 1912
&["research", "synthesis", "takeaways"], - 1913
"desktop", - 1914
"research.synthesis", - 1915
), - 1916
( - 1917
"lifestyle.culinary_recipe", - 1918
&["recipe", "ingredients"], - 1919
"desktop", - 1920
"lifestyle.culinary_recipe", - 1921
), - 1922
( - 1923
"lifestyle.culinary_recipe", - 1924
&["recipe", "ingredients"], - 1925
"telegram", - 1926
"answer.basic", - 1927
), - 1928
]; - 1929
for (i, (etype, signals, surface, expected)) in surface_filtered.iter().enumerate() { - 1930
let st = etype.to_string(); - 1931
let sigs: Vec<String> = signals.iter().map(|s| s.to_string()).collect(); - 1932
let sr = surface.to_string(); - 1933
let er = expected.to_string(); - 1934
let cat = built_in_recipes(); - 1935
scenarios.push(tc(&format!("surface_filter_{i}"), move || { - 1936
let decision = cat.choose(&sigs, &sr); - 1937
if let Some(d) = decision { - 1938
assert_eq!(d.recipe_id, er, "for {st} on {sr}"); - 1939
} else { - 1940
assert_eq!(er, "answer.basic", "expected {er} but got None on {sr}"); - 1941
} - 1942
})); - 1943
} - 1944
- 1945
// Default recipe with no signals - 1946
scenarios.push(tc("default_recipe_no_signals", || { - 1947
let catalog = built_in_recipes(); - 1948
let decision = catalog.choose(&[], "desktop"); - 1949
assert!(decision.is_some()); - 1950
assert_eq!(decision.unwrap().recipe_id, "answer.basic"); - 1951
})); - 1952
- 1953
// answer.basic filtered when signals present but no matching recipe - 1954
scenarios.push(tc("answer_basic_filtered_with_signals", || { - 1955
let catalog = built_in_recipes(); - 1956
let decision = catalog.choose_for_types(&["diff".to_string()], "desktop", &[]); - 1957
assert!(decision.is_none()); - 1958
})); - 1959
- 1960
// Partial signals do not match (answer.basic filtered by final filter) - 1961
scenarios.push(tc("partial_signal_mismatch_returns_none", || { - 1962
let catalog = built_in_recipes(); - 1963
let decision = catalog.choose(&["diff".to_string()], "desktop"); - 1964
assert!(decision.is_none()); - 1965
})); - 1966
- 1967
// Incomplete signals do not match - 1968
scenarios.push(tc("requires_all_signals_returns_none", || { - 1969
let catalog = built_in_recipes(); - 1970
let decision = catalog.choose(&["temperature".to_string()], "desktop"); - 1971
assert!(decision.is_none()); - 1972
})); - 1973
- 1974
// Unknown surface with no signals: answer.basic only supports desktop/terminal/telegram - 1975
scenarios.push(tc("unknown_surface_no_match", || { - 1976
let catalog = built_in_recipes(); - 1977
let decision = catalog.choose(&[], "unknown_surface"); - 1978
assert!(decision.is_none()); - 1979
})); - 1980
- 1981
// answer.basic with no signals on telegram - 1982
scenarios.push(tc("answer_basic_telegram_no_signals", || { - 1983
let catalog = built_in_recipes(); - 1984
let decision = catalog.choose(&[], "telegram"); - 1985
assert!(decision.is_some()); - 1986
assert_eq!(decision.unwrap().recipe_id, "answer.basic"); - 1987
})); - 1988
- 1989
// Multiple matching recipes: highest priority wins - 1990
scenarios.push(tc("highest_priority_wins", || { - 1991
let catalog = built_in_recipes(); - 1992
// Multiple recipes can match "diff" + "files_changed": - 1993
// coding.diff_inspector and coding.change_summary - 1994
// coding.diff_inspector has priority 0, coding.change_summary has priority 0 - 1995
// But coding.diff_inspector has more matched signals (2 vs 1) - 1996
let decision = catalog.choose( - 1997
&["diff".to_string(), "files_changed".to_string()], - 1998
"desktop", - 1999
); - 2000
assert!(decision.is_some());
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.