- 1001
..Reading::general() - 1002
}, - 1003
Vec::new(), - 1004
cwd.to_path_buf(), - 1005
Economics::default(), - 1006
); - 1007
spec.thread_id = Some(thread.into()); - 1008
ledger.open_commitment(spec).unwrap() - 1009
} - 1010
- 1011
#[test] - 1012
fn resuming_a_session_closes_its_orphaned_episode_before_starting_again() { - 1013
let dir = tempfile::tempdir().unwrap(); - 1014
let ledger = CommitmentLedger::new(dir.path()); - 1015
let id = open_on_thread(&ledger, dir.path(), "t0.0"); - 1016
ledger - 1017
.append(&Event::new( - 1018
&id, - 1019
EventKind::EpisodeStarted { - 1020
episode_id: "orphan".into(), - 1021
session_id: "s1".into(), - 1022
}, - 1023
)) - 1024
.unwrap(); - 1025
let intent = intent_on( - 1026
Evidence::None, - 1027
Horizon::Durable, - 1028
0.9, - 1029
"t1.0", - 1030
vak_intent::Lineage::Continues { - 1031
thread_id: "t0.0".into(), - 1032
}, - 1033
); - 1034
let next = begin_episode( - 1035
dir.path(), - 1036
&config(), - 1037
&intent, - 1038
"resume the migration", - 1039
"s1", - 1040
dir.path(), - 1041
) - 1042
.unwrap(); - 1043
assert_eq!(next.commitment_id, id); - 1044
let commitment = ledger.get(&id).unwrap().unwrap(); - 1045
assert!(commitment.episodes[0].ended_at.is_some()); - 1046
assert!(matches!( - 1047
commitment.episodes[0].advancement, - 1048
Some(Advancement::Blocked { .. }) - 1049
)); - 1050
assert_ne!(next.episode_id, "orphan"); - 1051
} - 1052
- 1053
/// A strand that continues a durable thread works on that thread's - 1054
/// commitment even when its own reading is a one-turn request: "now also - 1055
/// check staging" is part of the job it continues. - 1056
#[test] - 1057
fn a_continuation_works_on_its_threads_commitment_whatever_its_own_horizon() { - 1058
let dir = tempfile::tempdir().unwrap(); - 1059
let ledger = CommitmentLedger::new(dir.path()); - 1060
let id = open_on_thread(&ledger, dir.path(), "t0.0"); - 1061
let intent = intent_on( - 1062
Evidence::None, - 1063
Horizon::Turn, - 1064
0.9, - 1065
"t1.0", - 1066
vak_intent::Lineage::Continues { - 1067
thread_id: "t0.0".into(), - 1068
}, - 1069
); - 1070
let handle = begin_episode( - 1071
dir.path(), - 1072
&config(), - 1073
&intent, - 1074
"also staging", - 1075
"s2", - 1076
dir.path(), - 1077
) - 1078
.expect("the continuation joins the open commitment"); - 1079
assert_eq!(handle.commitment_id, id); - 1080
assert_eq!(ledger.all().len(), 1, "no twin was opened"); - 1081
} - 1082
- 1083
/// `/goal replace` on durable work opens its successor and supersedes the - 1084
/// original, keyed by the replacement's own thread. - 1085
#[test] - 1086
fn a_replacement_supersedes_the_replaced_threads_commitment() { - 1087
let dir = tempfile::tempdir().unwrap(); - 1088
let ledger = CommitmentLedger::new(dir.path()); - 1089
let old = open_on_thread(&ledger, dir.path(), "t0.0"); - 1090
let intent = intent_on( - 1091
Evidence::None, - 1092
Horizon::Turn, - 1093
0.9, - 1094
"t1.0", - 1095
vak_intent::Lineage::Replaces { - 1096
thread_id: "t0.0".into(), - 1097
}, - 1098
); - 1099
let handle = begin_episode( - 1100
dir.path(), - 1101
&config(), - 1102
&intent, - 1103
"do this instead", - 1104
"s2", - 1105
dir.path(), - 1106
) - 1107
.expect("the replacement opens a successor"); - 1108
assert_ne!(handle.commitment_id, old); - 1109
let old = ledger.get(&old).unwrap().unwrap(); - 1110
assert_eq!( - 1111
old.superseded_by.as_deref(), - 1112
Some(handle.commitment_id.as_str()) - 1113
); - 1114
let new = ledger.get(&handle.commitment_id).unwrap().unwrap(); - 1115
assert_eq!(new.spec.thread_id.as_deref(), Some("t1.0")); - 1116
assert_eq!(new.spec.audience_id.as_deref(), Some("local")); - 1117
} - 1118
- 1119
/// Two turns that each ask for durable work open two commitments: strand - 1120
/// ids carry the turn id, so the second can never mistake the first's - 1121
/// thread for its own. - 1122
#[test] - 1123
fn separate_durable_requests_open_separate_commitments() { - 1124
let dir = tempfile::tempdir().unwrap(); - 1125
for (turn, prompt) in [ - 1126
("t1.0", "watch the bill daily"), - 1127
("t2.0", "watch the logs daily"), - 1128
] { - 1129
let intent = intent_on( - 1130
Evidence::None, - 1131
Horizon::Durable, - 1132
0.9, - 1133
turn, - 1134
vak_intent::Lineage::New, - 1135
); - 1136
begin_episode(dir.path(), &config(), &intent, prompt, "s1", dir.path()).unwrap(); - 1137
} - 1138
assert_eq!(CommitmentLedger::new(dir.path()).open().len(), 2); - 1139
} - 1140
- 1141
/// Only an existing commitment can carry a grant, and only a live one is - 1142
/// offered to the turn. - 1143
#[test] - 1144
fn the_plan_offers_only_live_envelopes_of_existing_commitments() { - 1145
let dir = tempfile::tempdir().unwrap(); - 1146
let ledger = CommitmentLedger::new(dir.path()); - 1147
let id = open_on_thread(&ledger, dir.path(), "t0.0"); - 1148
let now = chrono::Utc::now(); - 1149
let envelope = vak_intent::Envelope { - 1150
envelope_id: "env-1".into(), - 1151
granted_by: "owner".into(), - 1152
granted_at: now, - 1153
expires_at: None, - 1154
spend_limit_usd: Some(5.0), - 1155
path_scope: vec!["docs/**".into()], - 1156
tool_scope: Vec::new(), - 1157
permission_ceiling: vak_intent::PermissionCeiling::WorkspaceWrite, - 1158
escalation: vak_intent::Escalation::WaitIndefinitely, - 1159
revoked_at: None, - 1160
}; - 1161
ledger - 1162
.append(&Event::new( - 1163
&id, - 1164
EventKind::EnvelopeGranted { - 1165
envelope: Box::new(envelope), - 1166
}, - 1167
)) - 1168
.unwrap(); - 1169
let intent = intent_on( - 1170
Evidence::None, - 1171
Horizon::Turn, - 1172
0.9, - 1173
"t1.0", - 1174
vak_intent::Lineage::Continues { - 1175
thread_id: "t0.0".into(), - 1176
}, - 1177
); - 1178
let plan = plan_episodes(dir.path(), &config(), &intent, now); - 1179
assert!(plan.envelopes().contains_key("t1.0")); - 1180
assert_eq!(plan.enveloped_commitments(), vec![id.clone()]); - 1181
- 1182
ledger - 1183
.append(&Event::new( - 1184
&id, - 1185
EventKind::EnvelopeRevoked { - 1186
envelope_id: "env-1".into(), - 1187
by: "owner".into(), - 1188
}, - 1189
)) - 1190
.unwrap(); - 1191
let plan = plan_episodes(dir.path(), &config(), &intent, now); - 1192
assert!( - 1193
plan.envelopes().is_empty(), - 1194
"a revoked grant is not offered" - 1195
); - 1196
} - 1197
- 1198
/// The upkeep evaluator runs outside any sandbox, so a criterion naming a - 1199
/// path outside the workspace is not checked at all. - 1200
#[tokio::test] - 1201
async fn the_workspace_evaluator_refuses_paths_outside_the_workspace() { - 1202
let outer = tempfile::tempdir().unwrap(); - 1203
let workspace = outer.path().join("ws"); - 1204
std::fs::create_dir_all(&workspace).unwrap(); - 1205
std::fs::write(outer.path().join("secret.txt"), "hunter2").unwrap(); - 1206
#[cfg(unix)] - 1207
std::os::unix::fs::symlink(outer.path(), workspace.join("up")).unwrap(); - 1208
let evaluator = WorkspaceEvaluator { cwd: &workspace }; - 1209
let mut paths = vec![ - 1210
"../secret.txt".to_string(), - 1211
outer.path().join("secret.txt").display().to_string(), - 1212
]; - 1213
if cfg!(unix) { - 1214
paths.push("up/secret.txt".into()); - 1215
} - 1216
for path in paths { - 1217
let evaluation = evaluator - 1218
.evaluate(&WorkCriterion { - 1219
criterion_id: "leak".into(), - 1220
statement: "probe".into(), - 1221
kind: CriterionKind::FileContains { - 1222
path: path.clone().into(), - 1223
pattern: "hunter2".into(), - 1224
}, - 1225
required: true, - 1226
}) - 1227
.await; - 1228
assert!( - 1229
matches!(evaluation.result, CriterionResult::Unknown { .. }), - 1230
"{path} was checked: {:?}", - 1231
evaluation.result - 1232
); - 1233
} - 1234
} - 1235
- 1236
/// Upkeep reaches every Agent's portfolio, not only the server's own. - 1237
#[tokio::test] - 1238
async fn upkeep_covers_every_agents_ledger() { - 1239
let data = tempfile::tempdir().unwrap(); - 1240
let agent_home = data.path().join("agents").join("helper"); - 1241
std::fs::create_dir_all(&agent_home).unwrap(); - 1242
let ledger = CommitmentLedger::new(&agent_home); - 1243
let id = open_on_thread(&ledger, data.path(), "t0.0"); - 1244
ledger - 1245
.append(&Event::new( - 1246
&id, - 1247
EventKind::Suspended { - 1248
suspension: vak_commit::Suspension::Schedule { - 1249
at: Some(chrono::Utc::now() - chrono::Duration::minutes(1)), - 1250
cron: None, - 1251
}, - 1252
}, - 1253
)) - 1254
.unwrap(); - 1255
assert_eq!(maintain_all(data.path()).await.resumed, vec![id]); - 1256
} - 1257
- 1258
/// A stray recurrence-ish word must not leave a month-long obligation - 1259
/// behind. Weak horizon evidence declines to open one. - 1260
#[test] - 1261
fn a_weak_horizon_reading_opens_nothing() { - 1262
let dir = tempfile::tempdir().unwrap(); - 1263
let handle = begin_episode( - 1264
dir.path(), - 1265
&config(), - 1266
&intent_with(Evidence::None, Horizon::Durable, 0.3), - 1267
"maybe keep an eye on things", - 1268
"s1", - 1269
dir.path(), - 1270
); - 1271
assert!(handle.is_none()); - 1272
} - 1273
- 1274
#[test] - 1275
fn disabling_commitments_opens_nothing() { - 1276
let dir = tempfile::tempdir().unwrap(); - 1277
let mut config = config(); - 1278
config.commitment.enabled = false; - 1279
assert!( - 1280
begin_episode( - 1281
dir.path(), - 1282
&config, - 1283
&intent_with(Evidence::None, Horizon::Durable, 0.9), - 1284
"watch the bill every day", - 1285
"s1", - 1286
dir.path(), - 1287
) - 1288
.is_none() - 1289
); - 1290
} - 1291
- 1292
/// The runtime may only propose criteria it could also check. Guessing a - 1293
/// shell command would manufacture `Observed` evidence from a guess. - 1294
#[test] - 1295
fn seeded_criteria_are_never_stronger_than_asserted() { - 1296
for evidence in [Evidence::Verified, Evidence::Audited] { - 1297
let intent = intent_with(evidence, Horizon::Durable, 0.9); - 1298
for criterion in seed_criteria(&intent, "do the thing") { - 1299
assert_eq!( - 1300
vak_commit::strength_of(&criterion.kind), - 1301
Satisfaction::Asserted - 1302
); - 1303
} - 1304
} - 1305
// And nothing at all is seeded when no proof is owed. - 1306
assert!(seed_criteria(&intent_with(Evidence::None, Horizon::Durable, 0.9), "x").is_empty()); - 1307
} - 1308
- 1309
/// Which means a seeded commitment cannot close itself: it stays visibly - 1310
/// open until a checkable criterion or a human attestation arrives. - 1311
#[test] - 1312
fn a_seeded_verified_commitment_cannot_be_closed_by_the_seed_alone() { - 1313
let dir = tempfile::tempdir().unwrap(); - 1314
let handle = begin_episode( - 1315
dir.path(), - 1316
&config(), - 1317
&intent_with(Evidence::Verified, Horizon::Durable, 0.9), - 1318
"migrate the schema and prove it works", - 1319
"s1", - 1320
dir.path(), - 1321
) - 1322
.unwrap(); - 1323
let ledger = CommitmentLedger::new(dir.path()); - 1324
ledger - 1325
.append(&Event::new( - 1326
&handle.commitment_id, - 1327
EventKind::CriterionEvaluated { - 1328
criterion_id: "objective".into(), - 1329
result: CriterionResult::Passed { - 1330
evidence: "I believe this is done".into(), - 1331
}, - 1332
strength: Satisfaction::Asserted, - 1333
}, - 1334
)) - 1335
.unwrap(); - 1336
let refused = ledger.append(&Event::new( - 1337
&handle.commitment_id, - 1338
EventKind::Closed { - 1339
verdict: Verdict::Fulfilled, - 1340
strength: Satisfaction::Asserted, - 1341
evidence: Vec::new(), - 1342
note: "done".into(), - 1343
}, - 1344
)); - 1345
assert!(refused.is_err()); - 1346
} - 1347
- 1348
#[test] - 1349
fn exhausting_the_turn_budget_counts_as_a_stall_but_answering_does_not() { - 1350
let stalled = classify(&vak_agent::TurnOutcome::MaxTurnsReached, 12, Vec::new()); - 1351
assert!(stalled.is_stall()); - 1352
- 1353
let answered = classify( - 1354
&vak_agent::TurnOutcome::Completed { - 1355
response: vak_llm::AssistantMessage { - 1356
content: vec![vak_llm::ContentBlock::Text { - 1357
text: "Here is a substantive answer that reduced uncertainty a lot.".into(), - 1358
}], - 1359
..vak_llm::AssistantMessage::empty("test-model") - 1360
}, - 1361
}, - 1362
0, - 1363
Vec::new(), - 1364
); - 1365
assert!(!answered.is_stall()); - 1366
assert!(matches!(answered, Advancement::Learned { .. })); - 1367
- 1368
let tool_only = classify( - 1369
&vak_agent::TurnOutcome::Completed { - 1370
response: vak_llm::AssistantMessage::empty("test-model"), - 1371
}, - 1372
12, - 1373
Vec::new(), - 1374
); - 1375
assert!(tool_only.is_stall()); - 1376
} - 1377
- 1378
#[test] - 1379
fn moving_a_criterion_is_advancement_whatever_else_happened() { - 1380
let advancement = classify( - 1381
&vak_agent::TurnOutcome::MaxTurnsReached, - 1382
0, - 1383
vec!["tests-pass".into()], - 1384
); - 1385
assert!(matches!(advancement, Advancement::Advanced { .. })); - 1386
assert!(!advancement.is_stall()); - 1387
} - 1388
- 1389
#[tokio::test] - 1390
async fn the_workspace_evaluator_observes_files_and_abstains_on_the_rest() { - 1391
let dir = tempfile::tempdir().unwrap(); - 1392
std::fs::write(dir.path().join("out.txt"), "all good").unwrap(); - 1393
let evaluator = WorkspaceEvaluator { cwd: dir.path() }; - 1394
- 1395
let exists = evaluator - 1396
.evaluate(&WorkCriterion { - 1397
criterion_id: "c1".into(), - 1398
statement: "output exists".into(), - 1399
kind: CriterionKind::FileExists { - 1400
path: "out.txt".into(), - 1401
}, - 1402
required: true, - 1403
}) - 1404
.await; - 1405
assert!(exists.passed()); - 1406
assert_eq!(exists.strength, Satisfaction::Observed); - 1407
- 1408
// A permissioned check abstains rather than guessing. - 1409
let shell = evaluator - 1410
.evaluate(&WorkCriterion { - 1411
criterion_id: "c2".into(), - 1412
statement: "tests pass".into(), - 1413
kind: CriterionKind::Shell { - 1414
command: "cargo test".into(), - 1415
}, - 1416
required: true, - 1417
}) - 1418
.await; - 1419
assert!(!shell.passed()); - 1420
assert!(matches!(shell.result, CriterionResult::Unknown { .. })); - 1421
} - 1422
- 1423
#[tokio::test] - 1424
async fn a_predicate_suspension_wakes_when_the_condition_becomes_true() { - 1425
let dir = tempfile::tempdir().unwrap(); - 1426
let ledger = CommitmentLedger::new(dir.path()); - 1427
let handle = begin_episode( - 1428
dir.path(), - 1429
&config(), - 1430
&intent_with(Evidence::None, Horizon::Durable, 0.9), - 1431
"tell me when the report lands, every day", - 1432
"s1", - 1433
dir.path(), - 1434
) - 1435
.unwrap(); - 1436
let criterion = WorkCriterion { - 1437
criterion_id: "landed".into(), - 1438
statement: "report.csv exists".into(), - 1439
kind: CriterionKind::FileExists { - 1440
path: "report.csv".into(), - 1441
}, - 1442
required: true, - 1443
}; - 1444
ledger - 1445
.append(&Event::new( - 1446
&handle.commitment_id, - 1447
EventKind::Suspended { - 1448
suspension: vak_commit::Suspension::Predicate { - 1449
criterion: criterion.clone(), - 1450
}, - 1451
}, - 1452
)) - 1453
.unwrap(); - 1454
- 1455
// While the condition is false the pass costs nothing and changes - 1456
// nothing — this is the zero-token watch path. - 1457
let report = maintain(dir.path()).await; - 1458
assert!(report.satisfied.is_empty()); - 1459
assert_eq!( - 1460
ledger.get(&handle.commitment_id).unwrap().unwrap().phase, - 1461
vak_commit::Phase::Suspended - 1462
); - 1463
- 1464
std::fs::write(dir.path().join("report.csv"), "done").unwrap(); - 1465
let report = maintain(dir.path()).await; - 1466
assert_eq!(report.satisfied, vec![handle.commitment_id.clone()]); - 1467
assert_ne!( - 1468
ledger.get(&handle.commitment_id).unwrap().unwrap().phase, - 1469
vak_commit::Phase::Suspended - 1470
); - 1471
} - 1472
- 1473
#[tokio::test] - 1474
async fn a_commitment_dependency_wakes_after_fulfillment() { - 1475
let dir = tempfile::tempdir().unwrap(); - 1476
let ledger = CommitmentLedger::new(dir.path()); - 1477
let dependency = ledger - 1478
.open_commitment(vak_commit::spec_from_reading( - 1479
"dependency", - 1480
Reading::general(), - 1481
Vec::new(), - 1482
dir.path().to_path_buf(), - 1483
Economics::default(), - 1484
)) - 1485
.unwrap(); - 1486
ledger - 1487
.append(&Event::new( - 1488
&dependency, - 1489
EventKind::Closed { - 1490
verdict: Verdict::Fulfilled, - 1491
strength: Satisfaction::Asserted, - 1492
evidence: Vec::new(), - 1493
note: "done".into(), - 1494
}, - 1495
)) - 1496
.unwrap(); - 1497
let waiting = ledger - 1498
.open_commitment(vak_commit::spec_from_reading( - 1499
"waiting", - 1500
Reading::general(), - 1501
Vec::new(), - 1502
dir.path().to_path_buf(), - 1503
Economics::default(), - 1504
)) - 1505
.unwrap(); - 1506
ledger - 1507
.append(&Event::new( - 1508
&waiting, - 1509
EventKind::Suspended { - 1510
suspension: vak_commit::Suspension::Commitment { - 1511
commitment_id: dependency, - 1512
}, - 1513
}, - 1514
)) - 1515
.unwrap(); - 1516
let report = maintain(dir.path()).await; - 1517
assert_eq!(report.resumed, vec![waiting]); - 1518
} - 1519
- 1520
#[tokio::test] - 1521
async fn a_scheduled_suspension_wakes_once_its_time_arrives() { - 1522
let dir = tempfile::tempdir().unwrap(); - 1523
let ledger = CommitmentLedger::new(dir.path()); - 1524
let handle = begin_episode( - 1525
dir.path(), - 1526
&config(), - 1527
&intent_with(Evidence::None, Horizon::Durable, 0.9), - 1528
"check the bill every day", - 1529
"s1", - 1530
dir.path(), - 1531
) - 1532
.unwrap(); - 1533
ledger - 1534
.append(&Event::new( - 1535
&handle.commitment_id, - 1536
EventKind::Suspended { - 1537
suspension: vak_commit::Suspension::Schedule { - 1538
at: Some(chrono::Utc::now() + chrono::Duration::hours(2)), - 1539
cron: None, - 1540
}, - 1541
}, - 1542
)) - 1543
.unwrap(); - 1544
assert!(maintain(dir.path()).await.resumed.is_empty()); - 1545
- 1546
ledger - 1547
.append(&Event::new( - 1548
&handle.commitment_id, - 1549
EventKind::Suspended { - 1550
suspension: vak_commit::Suspension::Schedule { - 1551
at: Some(chrono::Utc::now() - chrono::Duration::minutes(1)), - 1552
cron: None, - 1553
}, - 1554
}, - 1555
)) - 1556
.unwrap(); - 1557
assert_eq!( - 1558
maintain(dir.path()).await.resumed, - 1559
vec![handle.commitment_id] - 1560
); - 1561
} - 1562
- 1563
/// A question with no policy waits forever by design. That is a decision, - 1564
/// not a leak — and it must not quietly become an assumption. - 1565
#[tokio::test] - 1566
async fn an_unanswered_question_waits_unless_a_policy_says_otherwise() { - 1567
let dir = tempfile::tempdir().unwrap(); - 1568
let ledger = CommitmentLedger::new(dir.path()); - 1569
let handle = begin_episode( - 1570
dir.path(), - 1571
&config(), - 1572
&intent_with(Evidence::None, Horizon::Durable, 0.9), - 1573
"migrate the schema every night", - 1574
"s1", - 1575
dir.path(), - 1576
) - 1577
.unwrap(); - 1578
defer_for_human( - 1579
dir.path(), - 1580
&handle.commitment_id, - 1581
"which database?", - 1582
None, - 1583
vak_intent::Escalation::WaitIndefinitely, - 1584
) - 1585
.unwrap(); - 1586
assert!(maintain(dir.path()).await.escalated.is_empty()); - 1587
assert_eq!( - 1588
ledger.get(&handle.commitment_id).unwrap().unwrap().phase, - 1589
vak_commit::Phase::Suspended - 1590
); - 1591
} - 1592
- 1593
#[test] - 1594
fn expiry_closes_explicitly_and_budget_exhaustion_does_not() { - 1595
let dir = tempfile::tempdir().unwrap(); - 1596
let ledger = CommitmentLedger::new(dir.path()); - 1597
let mut config = config(); - 1598
config.commitment.default_ttl_days = Some(1); - 1599
- 1600
let handle = begin_episode( - 1601
dir.path(), - 1602
&config, - 1603
&intent_with(Evidence::None, Horizon::Durable, 0.9), - 1604
"watch it every day", - 1605
"s1", - 1606
dir.path(), - 1607
) - 1608
.unwrap(); - 1609
- 1610
// Not yet expired. - 1611
assert!(sweep_expired(dir.path()).is_empty()); - 1612
- 1613
// An over-budget commitment is held by the scheduler, never closed. - 1614
ledger - 1615
.append(&Event::new( - 1616
&handle.commitment_id, - 1617
EventKind::EpisodeEnded { - 1618
episode_id: handle.episode_id.clone(), - 1619
advancement: Advancement::Learned { fact: "x".into() }, - 1620
spend_usd: 9_999.0, - 1621
}, - 1622
)) - 1623
.unwrap(); - 1624
assert!(sweep_expired(dir.path()).is_empty()); - 1625
let commitment = ledger.get(&handle.commitment_id).unwrap().unwrap(); - 1626
assert!( - 1627
commitment.is_over_budget() || commitment.spec.economics.lifetime_budget_usd.is_none() - 1628
); - 1629
assert!(!commitment.phase.is_terminal()); - 1630
} - 1631
} - 1632
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.