返回 CodeWhale
delivery_tests.rs
根目录 / crates / tui / src / tools / subagent / delivery_tests.rs
1 use super::*;
2 use tempfile::tempdir;
3
4 fn git(root: &Path, args: &[&str]) {
5 let output = std::process::Command::new("git")
6 .arg("-C")
7 .arg(root)
8 .args(args)
9 .output()
10 .expect("git");
11 assert!(
12 output.status.success(),
13 "git {args:?}: {}",
14 String::from_utf8_lossy(&output.stderr)
15 );
16 }
17
18 fn repository(root: &Path) {
19 git(root, &["init", "--quiet"]);
20 git(root, &["config", "user.name", "Delivery test"]);
21 git(root, &["config", "user.email", "delivery@example.invalid"]);
22 fs::create_dir_all(root.join("src")).unwrap();
23 fs::write(root.join("src/lib.rs"), "baseline\n").unwrap();
24 git(root, &["add", "--", "src/lib.rs"]);
25 git(root, &["commit", "--quiet", "-m", "baseline"]);
26 }
27
28 fn worker(root: &Path, write: bool, paths: &[&str], scope: &[&str]) -> (SubAgentManager, String) {
29 let mut manager = SubAgentManager::new(root.to_path_buf(), 2);
30 let id = manager.insert_test_running_agent("delivery", root);
31 let record = manager.worker_records.get_mut(&id).unwrap();
32 record.spec.runtime_profile.permissions.write = write;
33 record.spec.launch_manifest = Some(ChildLaunchManifest {
34 owner_session: "workspace".into(),
35 child_id: id.clone(),
36 profile: record.spec.runtime_profile.clone(),
37 prompt: "produce report".into(),
38 cwd: Some(root.display().to_string()),
39 worktree: false,
40 writable_roots: scope.iter().map(|path| (*path).into()).collect(),
41 writable_files: Vec::new(),
42 coordination_contracts: Vec::new(),
43 expected_artifact: None,
44 deliverables: paths.iter().map(|path| (*path).into()).collect(),
45 resume_identity: None,
46 generation: 1,
47 resume_from_agent_id: None,
48 });
49 record.delivery_evidence = DeliveryEvidence::capture(&record.spec);
50 if write && !scope.is_empty() {
51 manager
52 .coordination
53 .register_claim(
54 WriteScopeClaim {
55 owner: id.clone(),
56 roots: scope.iter().map(|path| (*path).into()).collect(),
57 exact_files: Vec::new(),
58 contracts: Vec::new(),
59 },
60 false,
61 |_| false,
62 )
63 .unwrap();
64 }
65 (manager, id)
66 }
67
68 fn complete(manager: &mut SubAgentManager, id: &str, report: &str) -> AgentRunVerificationSummary {
69 let mut result = manager.get_result(id).unwrap();
70 result.status = SubAgentStatus::Completed;
71 result.result = Some(report.into());
72 manager.complete_worker_from_result(id, &result);
73 // Deferred verification (#6210): the commit leaves verification pending;
74 // run the same snapshot→compute→store halves `ensure` runs off the lock.
75 if !manager.worker_records[id].delivery_evidence.checked
76 && let Some(inputs) = manager.delivery_verification_inputs(id, &result)
77 {
78 let verification = delivery::compute_delivery_verification(&inputs);
79 manager.store_delivery_verification(id, verification);
80 }
81 manager.worker_records[id].verification.clone()
82 }
83
84 #[test]
85 fn declared_deliverables_exclude_git_metadata_by_the_shared_rule() {
86 let mut refused = vec![".GIT/config", "sub/.Git/HEAD"];
87 if cfg!(windows) {
88 refused.extend([".git./config", ".git /config", "GIT~1/config"]);
89 }
90 for path in refused {
91 let error = delivery::declared_paths(&[path.to_string()], None).expect_err(path);
92 assert!(error.contains("git metadata"), "{path}: {error}");
93 }
94 assert_eq!(
95 delivery::declared_paths(&[".github/report.md".to_string()], None).unwrap(),
96 [".github/report.md"]
97 );
98 }
99
100 #[test]
101 fn declared_deliverables_narrow_default_scope_and_reject_invalid_input() {
102 let request = parse_spawn_request(&json!({
103 "type": "implement", "prompt": "write outputs", "deliverables": ["tmp/a/report.md", "tmp/b/report.md"]
104 })).unwrap();
105 assert!(request.write_roots.is_empty());
106 assert_eq!(request.exact_files, ["tmp/a/report.md", "tmp/b/report.md"]);
107 for paths in [
108 json!([""]),
109 json!(["../report.md"]),
110 json!(["/tmp/report.md"]),
111 json!([".git/config"]),
112 json!(["."]),
113 json!([1]),
114 json!("report.md"),
115 ] {
116 assert!(
117 parse_spawn_request(
118 &json!({"type":"implement", "prompt":"report", "deliverables": paths})
119 )
120 .is_err()
121 );
122 }
123 assert!(
124 parse_spawn_request(
125 &json!({"type":"implement", "prompt":"report", "deliverables": vec!["x.md"; 17]})
126 )
127 .is_err()
128 );
129 assert!(
130 delivery::declared_paths(&[], Some("review findings"))
131 .unwrap()
132 .is_empty()
133 );
134 assert_eq!(
135 delivery::declared_paths(&[], Some("report.md")).unwrap(),
136 ["report.md"]
137 );
138 }
139
140 #[test]
141 fn deliverable_verdict_distinguishes_present_missing_empty_directory_and_scope() {
142 let tmp = tempdir().unwrap();
143 fs::write(tmp.path().join("present.md"), "report").unwrap();
144 fs::write(tmp.path().join("empty.md"), "").unwrap();
145 fs::create_dir(tmp.path().join("directory")).unwrap();
146 for (path, status) in [
147 ("present.md", "present"),
148 ("missing.md", "missing"),
149 ("empty.md", "empty"),
150 ("directory", "not_file"),
151 ] {
152 assert_eq!(
153 delivery::check_deliverable(tmp.path(), path, true).status,
154 status
155 );
156 }
157 assert_eq!(
158 delivery::check_deliverable(tmp.path(), "present.md", false).status,
159 "out_of_scope"
160 );
161 }
162
163 #[cfg(unix)]
164 #[test]
165 fn deliverables_refuse_leaf_and_parent_symlink_escape() {
166 let tmp = tempdir().unwrap();
167 let outside = tempdir().unwrap();
168 fs::write(outside.path().join("secret.md"), "outside").unwrap();
169 std::os::unix::fs::symlink(outside.path().join("secret.md"), tmp.path().join("leaf.md"))
170 .unwrap();
171 std::os::unix::fs::symlink(outside.path(), tmp.path().join("linked")).unwrap();
172 for path in ["leaf.md", "linked/secret.md"] {
173 assert_eq!(
174 delivery::check_deliverable(tmp.path(), path, true).status,
175 "invalid_path"
176 );
177 assert!(delivery::safe_deliverable_path(tmp.path(), path).is_err());
178 }
179 }
180
181 #[test]
182 fn missing_declared_deliverable_is_visible_in_terminal_sentinel() {
183 let tmp = tempdir().unwrap();
184 let (mut manager, id) = worker(tmp.path(), true, &["report.md"], &["."]);
185 let verification = complete(&mut manager, &id, "Finished the research.");
186 assert_eq!(verification.status, "deliverable_missing");
187 assert_eq!(verification.deliverables[0].status, "missing");
188 let mut result = manager.get_result(&id).unwrap();
189 result.status = SubAgentStatus::Completed;
190 let completion = subagent_completion_with_verification(
191 "workspace",
192 &result,
193 None,
194 Some(&verification),
195 None,
196 );
197 assert!(completion.payload.contains("deliverable_missing"));
198 assert!(completion.payload.contains("report.md"));
199 }
200
201 #[test]
202 fn declared_output_is_not_an_undeclared_edit_and_undeclared_worker_stays_self_reported() {
203 let tmp = tempdir().unwrap();
204 repository(tmp.path());
205 let (mut manager, id) = worker(tmp.path(), true, &["report.md"], &["."]);
206 fs::write(tmp.path().join("report.md"), "findings").unwrap();
207 let verification = complete(&mut manager, &id, "Report ready.");
208 assert_eq!(verification.status, "deliverables_present");
209 assert_eq!(verification.deliverables[0].bytes, Some(8));
210 let (mut manager, id) = worker(tmp.path(), false, &[], &[]);
211 assert_eq!(
212 complete(&mut manager, &id, "No changes.").status,
213 "self_report_only"
214 );
215 }
216
217 #[test]
218 fn change_like_prose_and_line_citations_are_never_edit_claims() {
219 for report in [
220 "Fixed behavior is documented in src/lib.rs:12-19.",
221 "CHANGES: None\nThe added guard is at src/lib.rs:12-19",
222 "CHANGES: src/lib.rs:12-19",
223 "CHANGES:\n- Reviewed src/lib.rs:12-19.",
224 "CHANGES:\n- Reviewed src/lib.rs:12-19!",
225 "CHANGES:\n- Reviewed src/lib.rs:12-19:",
226 "CHANGES: [src/lib.rs:12-19](src/lib.rs#L12-L19)",
227 "CHANGES: [source](src/lib.rs:12-19)",
228 ] {
229 assert!(
230 delivery::explicit_change_paths(report).is_empty(),
231 "{report}"
232 );
233 }
234 assert_eq!(
235 delivery::explicit_change_paths("CHANGES:\n- src/lib.rs\n- report.md"),
236 BTreeSet::from(["src/lib.rs".into(), "report.md".into()])
237 );
238 let tmp = tempdir().unwrap();
239 repository(tmp.path());
240 let (mut manager, id) = worker(tmp.path(), false, &[], &[]);
241 assert_eq!(
242 complete(&mut manager, &id, "CHANGES: src/lib.rs").status,
243 "self_report_only"
244 );
245 }
246
247 #[test]
248 fn unchanged_dirty_file_does_not_satisfy_a_new_edit_claim() {
249 let tmp = tempdir().unwrap();
250 repository(tmp.path());
251 fs::write(tmp.path().join("src/lib.rs"), "existing dirty work\n").unwrap();
252 let (mut manager, id) = worker(tmp.path(), true, &[], &["src"]);
253 let verification = complete(&mut manager, &id, "CHANGES: src/lib.rs");
254 assert_eq!(verification.status, "claim_mismatch");
255 assert!(verification.summary.contains("declared but unchanged"));
256 assert!(verification.summary.contains("src/lib.rs"));
257 }
258
259 #[test]
260 fn five_heading_output_declares_changed_files_without_claiming_evidence_or_risks() {
261 let tmp = tempdir().unwrap();
262 repository(tmp.path());
263 let (mut manager, id) = worker(tmp.path(), true, &[], &["src"]);
264 fs::write(tmp.path().join("src/lib.rs"), "updated\n").unwrap();
265 manager
266 .worker_records
267 .get_mut(&id)
268 .unwrap()
269 .delivery_evidence
270 .observed_writes
271 .insert("src/lib.rs".into());
272 let report = "### SUMMARY\n\nUpdated the parser.\n\n\
273 ### EVIDENCE\n\n- Reviewed src/reference.rs:12-19.\n\n\
274 ### CHANGES\n\n- `src/lib.rs` — adjusted the parser\n\n\
275 ### RISKS\n\n- src/consumer.rs still needs a separate review\n\n\
276 ### BLOCKERS\n\nNone.\n";
277 for report in [
278 report.to_string(),
279 report.replace("### CHANGES", "### changes"),
280 ] {
281 assert_eq!(
282 delivery::explicit_change_paths(&report),
283 BTreeSet::from(["src/lib.rs".into()])
284 );
285 assert_ne!(
286 complete(&mut manager, &id, &report).status,
287 "claim_mismatch"
288 );
289 }
290 }
291
292 #[test]
293 fn changes_bullet_descriptions_do_not_invent_file_claims() {
294 let tmp = tempdir().unwrap();
295 repository(tmp.path());
296 let (mut manager, id) = worker(tmp.path(), true, &[], &["src"]);
297 fs::write(tmp.path().join("src/lib.rs"), "updated\n").unwrap();
298 manager
299 .worker_records
300 .get_mut(&id)
301 .unwrap()
302 .delivery_evidence
303 .observed_writes
304 .insert("src/lib.rs".into());
305 for declaration in [
306 "- `src/lib.rs` — updated parsing.",
307 "- src/lib.rs: Updated parsing.",
308 "- src/lib.rs updated parsing; reviewed notes.md.",
309 "- [src/lib.rs](src/lib.rs) - Updated parsing! See notes.md.",
310 "- 'src/lib.rs' — matches src/reference.rs:12-19.",
311 ] {
312 let report = format!(
313 "### SUMMARY\n\nUpdated the parser.\n\n\
314 ### EVIDENCE\n\n- Reviewed src/reference.rs:12-19.\n\n\
315 ### CHANGES\n\n{declaration}\n\n\
316 ### RISKS\n\n- Check notes.md separately.\n\n\
317 ### BLOCKERS\n\nNone.\n"
318 );
319 assert_eq!(
320 delivery::explicit_change_paths(&report),
321 BTreeSet::from(["src/lib.rs".into()]),
322 "{declaration}"
323 );
324 assert_ne!(
325 complete(&mut manager, &id, &report).status,
326 "claim_mismatch",
327 "{declaration}"
328 );
329 }
330 }
331
332 #[test]
333 fn explicit_change_path_lists_preserve_quoted_spaces_and_punctuation() {
334 let expected = BTreeSet::from([
335 "src/lib.rs".into(),
336 "src/with spaces.rs".into(),
337 "src/another file.rs".into(),
338 "src/trailing.".into(),
339 ]);
340 for label in ["CHANGES:", "Changed files:", "Files changed:"] {
341 let report = format!(
342 "{label} src/lib.rs, `src/with spaces.rs`; \"src/another file.rs\" 'src/trailing.'"
343 );
344 assert_eq!(delivery::explicit_change_paths(&report), expected);
345 }
346 assert!(delivery::explicit_change_paths("CHANGES: Updated parsing.").is_empty());
347 assert!(
348 delivery::explicit_change_paths("CHANGES: src/reference.rs:12-19. — reviewed only.")
349 .is_empty()
350 );
351 }
352
353 #[test]
354 fn modification_of_already_dirty_file_is_measured_against_spawn_content() {
355 let tmp = tempdir().unwrap();
356 repository(tmp.path());
357 fs::write(tmp.path().join("src/lib.rs"), "existing dirty work\n").unwrap();
358 let (mut manager, id) = worker(tmp.path(), true, &[], &["src"]);
359 fs::write(
360 tmp.path().join("src/lib.rs"),
361 "worker changed this further\n",
362 )
363 .unwrap();
364 assert_eq!(
365 complete(&mut manager, &id, "CHANGES: src/lib.rs").status,
366 "self_report_only"
367 );
368 }
369
370 #[test]
371 fn committed_change_is_compared_with_exact_spawn_head_without_timestamp_guessing() {
372 let tmp = tempdir().unwrap();
373 repository(tmp.path());
374 let (mut manager, id) = worker(tmp.path(), true, &[], &["src"]);
375 fs::write(tmp.path().join("src/lib.rs"), "worker commit\n").unwrap();
376 git(tmp.path(), &["add", "--", "src/lib.rs"]);
377 git(tmp.path(), &["commit", "--quiet", "-m", "worker"]);
378 assert_eq!(
379 complete(&mut manager, &id, "CHANGES: src/lib.rs").status,
380 "self_report_only"
381 );
382 }
383
384 #[test]
385 fn an_observed_write_without_a_declaration_is_flagged_but_external_changes_are_not() {
386 let tmp = tempdir().unwrap();
387 repository(tmp.path());
388 let (mut manager, id) = worker(tmp.path(), true, &[], &["src"]);
389 fs::write(tmp.path().join("src/lib.rs"), "new change\n").unwrap();
390 manager
391 .worker_records
392 .get_mut(&id)
393 .unwrap()
394 .delivery_evidence
395 .observed_writes
396 .insert("src/lib.rs".into());
397 assert_eq!(
398 complete(&mut manager, &id, "CHANGES: None").status,
399 "claim_mismatch"
400 );
401 for scope in ["reports", "."] {
402 let (mut manager, id) = worker(tmp.path(), true, &[], &[scope]);
403 fs::write(
404 tmp.path().join("src/lib.rs"),
405 format!("external {scope} change\n"),
406 )
407 .unwrap();
408 assert!(
409 manager.worker_records[&id]
410 .delivery_evidence
411 .observed_writes
412 .is_empty()
413 );
414 assert_eq!(
415 complete(&mut manager, &id, "CHANGES: None").status,
416 "self_report_only"
417 );
418 }
419 }
420
421 #[test]
422 fn disjoint_sibling_write_paths_admit_and_ancestor_overlap_names_actual_remedy() {
423 let mut ledger = CoordinationLedger::default();
424 let claim = |owner: &str, path: &str| WriteScopeClaim {
425 owner: owner.into(),
426 roots: vec![path.into()],
427 exact_files: Vec::new(),
428 contracts: Vec::new(),
429 };
430 ledger
431 .register_claim(claim("a", "tmp/scan/a"), false, |_| true)
432 .unwrap();
433 ledger
434 .register_claim(claim("b", "tmp/scan/b"), false, |_| true)
435 .unwrap();
436 let error = ledger
437 .register_claim(claim("broad", "tmp/scan"), false, |_| true)
438 .unwrap_err();
439 for text in [
440 "tmp/scan/a",
441 "tmp/scan",
442 "disjoint sibling",
443 "exact_files",
444 "write_authority=read_only",
445 ] {
446 assert!(error.contains(text), "{error}");
447 }
448 }
449
450 #[cfg(unix)]
451 #[tokio::test]
452 #[allow(clippy::print_stderr)] // Test receipt must distinguish refused probes from exercised isolation.
453 async fn enforced_readonly_python_queries_sqlite_under_a_live_peer_write_claim() {
454 // #6305: the sandbox replaces /tmp with a fresh tmpfs, so a fixture
455 // rooted there vanishes before --chdir reaches it.
456 let tmp = crate::test_support::sandbox_visible_tempdir();
457 let database = rusqlite::Connection::open(tmp.path().join("fixture.sqlite")).unwrap();
458 database
459 .execute_batch(
460 "CREATE TABLE fixture(value TEXT); INSERT INTO fixture VALUES ('peer-read-receipt');",
461 )
462 .unwrap();
463 drop(database);
464 fs::write(tmp.path().join("peer.txt"), "preserve peer bytes").unwrap();
465 fs::write(tmp.path().join("own.txt"), "own bytes").unwrap();
466 let manager = new_shared_subagent_manager(tmp.path().to_path_buf(), 4);
467 {
468 let mut guard = manager.write().await;
469 guard.insert_test_running_agent("analysis", tmp.path());
470 guard.insert_test_running_agent("peer", tmp.path());
471 for (owner, file) in [("agent_analysis", "own.txt"), ("agent_peer", "peer.txt")] {
472 guard
473 .coordination
474 .register_claim(
475 WriteScopeClaim {
476 owner: owner.into(),
477 roots: Vec::new(),
478 exact_files: vec![file.into()],
479 contracts: Vec::new(),
480 },
481 false,
482 |_| true,
483 )
484 .unwrap();
485 }
486 assert_eq!(
487 guard.live_peer_shared_write_claim_owners("agent_analysis"),
488 ["agent_peer"]
489 );
490 }
491 let python = [
492 "/usr/bin/python3",
493 "/opt/homebrew/bin/python3",
494 "/usr/local/bin/python3",
495 ]
496 .into_iter()
497 .find(|path| Path::new(path).is_file())
498 .expect("Python fixture runtime");
499 for role in [FleetRole::Builder, FleetRole::Scout] {
500 let mut runtime = super::tests::stub_runtime();
501 runtime.manager = Arc::clone(&manager);
502 runtime.context = ToolContext::new(tmp.path());
503 runtime.context.auto_approve = true;
504 runtime.context.elevated_sandbox_policy =
505 Some(crate::sandbox::SandboxPolicy::DangerFullAccess);
506 #[cfg(target_os = "linux")]
507 runtime
508 .context
509 .shell_manager
510 .lock()
511 .unwrap()
512 .set_prefer_bwrap(true);
513 let available = runtime
514 .context
515 .shell_manager
516 .lock()
517 .unwrap()
518 .configured_sandbox_type()
519 .is_some();
520 runtime.worker_profile = WorkerRuntimeProfile::for_role(role.clone());
521 let registry = ChildCoreProbe::new_with_owner(
522 runtime,
523 role,
524 "agent_analysis".into(),
525 "analysis".into(),
526 Some(vec!["bash".into()]),
527 Arc::new(Mutex::new(TodoList::new())),
528 Arc::new(Mutex::new(PlanState::default())),
529 );
530 let script = "import sqlite3; c=sqlite3.connect('file:fixture.sqlite?mode=ro', uri=True); print(c.execute('SELECT value FROM fixture').fetchone()[0])";
531 let query = json!({"command": format!("{python} -I -B -c {}", shell_words::quote(script)), "read_only": true});
532 let read = registry.execute("agent_analysis", "bash", query).await;
533 if !available {
534 let error = read.unwrap_err().to_string();
535 assert!(error.contains("native read-only enforcement"), "{error}");
536 eprintln!("UNRUN: native child+peer Python probe; unavailable sandbox was refused");
537 continue;
538 }
539 let read = read.unwrap();
540 assert!(read.contains("peer-read-receipt"), "{read}");
541 let write = json!({"command": format!("{python} -I -B -c {}", shell_words::quote("open('peer.txt', 'w').write('corrupt')")), "read_only": true});
542 let error = registry
543 .execute("agent_analysis", "bash", write)
544 .await
545 .unwrap_err()
546 .to_string();
547 assert!(
548 !error.contains("blocking peers"),
549 "read-only execution must reach native enforcement: {error}"
550 );
551 assert_eq!(
552 fs::read_to_string(tmp.path().join("peer.txt")).unwrap(),
553 "preserve peer bytes"
554 );
555 assert_eq!(
556 manager
557 .read()
558 .await
559 .get_result("agent_peer")
560 .unwrap()
561 .status,
562 SubAgentStatus::Running
563 );
564 eprintln!(
565 "NATIVE_READONLY_ENFORCED: child SQLite query passed under a live peer claim; mutation denied"
566 );
567 }
568 }
569
570 /// Deferred-verification contract (#6210): the terminal commit stores the
571 /// worker projection but leaves verification pending for `ensure`.
572 #[test]
573 fn terminal_commit_leaves_delivery_verification_pending() {
574 let tmp = tempdir().unwrap();
575 repository(tmp.path());
576 let (mut manager, id) = worker(tmp.path(), true, &["report.md"], &["."]);
577 let mut result = manager.get_result(&id).unwrap();
578 result.status = SubAgentStatus::Completed;
579 result.result = Some("Finished the research.".into());
580 manager.complete_worker_from_result(&id, &result);
581 let record = manager.worker_records.get(&id).unwrap();
582 assert!(!record.delivery_evidence.checked);
583 assert_eq!(record.verification.status, "self_report_only");
584 assert_eq!(
585 record.result_summary.as_deref(),
586 Some("Finished the research.")
587 );
588 }
589
590 #[tokio::test]
591 async fn ensure_worker_delivery_verified_stores_verdicts_and_is_idempotent() {
592 let tmp = tempdir().unwrap();
593 repository(tmp.path());
594 let (manager, id) = worker(tmp.path(), true, &["report.md"], &["."]);
595 let manager = Arc::new(RwLock::new(manager));
596 let result = {
597 let mut guard = manager.write().await;
598 let mut result = guard.get_result(&id).unwrap();
599 result.status = SubAgentStatus::Completed;
600 result.result = Some("Finished the research.".into());
601 guard.complete_worker_from_result(&id, &result);
602 assert!(!guard.worker_records[&id].delivery_evidence.checked);
603 result
604 };
605 ensure_worker_delivery_verified(&manager, &id, &result).await;
606 let first = manager.read().await.worker_records[&id]
607 .verification
608 .clone();
609 assert_eq!(first.status, "deliverable_missing");
610 assert!(
611 manager.read().await.worker_records[&id]
612 .delivery_evidence
613 .checked
614 );
615 // A second call is a no-op even with a different report.
616 let mut other = result.clone();
617 other.result = Some("CHANGES: src/lib.rs".into());
618 ensure_worker_delivery_verified(&manager, &id, &other).await;
619 assert_eq!(manager.read().await.worker_records[&id].verification, first);
620 }
621
622 #[tokio::test]
623 async fn ensure_worker_delivery_verified_ignores_running_missing_and_checked() {
624 let tmp = tempdir().unwrap();
625 repository(tmp.path());
626 let (manager, id) = worker(tmp.path(), true, &[], &["src"]);
627 let manager = Arc::new(RwLock::new(manager));
628 let running = manager.read().await.get_result(&id).unwrap();
629 assert_eq!(running.status, SubAgentStatus::Running);
630 ensure_worker_delivery_verified(&manager, &id, &running).await;
631 assert!(
632 !manager.read().await.worker_records[&id]
633 .delivery_evidence
634 .checked
635 );
636 // A missing worker id is a silent no-op.
637 let mut missing = running.clone();
638 missing.agent_id = "agent_missing".to_string();
639 missing.status = SubAgentStatus::Completed;
640 ensure_worker_delivery_verified(&manager, "agent_missing", &missing).await;
641 // A checked record keeps its stored verdict.
642 let mut done = running.clone();
643 done.status = SubAgentStatus::Completed;
644 done.result = Some("CHANGES: src/lib.rs".into());
645 ensure_worker_delivery_verified(&manager, &id, &done).await;
646 assert_eq!(
647 manager.read().await.worker_records[&id].verification.status,
648 "claim_mismatch"
649 );
650 let mut changed_mind = done.clone();
651 changed_mind.result = Some("CHANGES: None".into());
652 ensure_worker_delivery_verified(&manager, &id, &changed_mind).await;
653 assert_eq!(
654 manager.read().await.worker_records[&id].verification.status,
655 "claim_mismatch"
656 );
657 }
658
659 /// Read-side backstop (#6210): a terminal detail projection heals a
660 /// verification left pending by a Stop/interrupt/close/stale commit.
661 #[tokio::test]
662 async fn detail_projection_heals_pending_delivery_verification() {
663 let tmp = tempdir().unwrap();
664 repository(tmp.path());
665 let (manager, id) = worker(tmp.path(), true, &["report.md"], &["."]);
666 let manager = Arc::new(RwLock::new(manager));
667 let result = {
668 let mut guard = manager.write().await;
669 let mut result = guard.get_result(&id).unwrap();
670 result.status = SubAgentStatus::Completed;
671 result.result = Some("Finished the research.".into());
672 guard.complete_worker_from_result(&id, &result);
673 result
674 };
675 let mut context = ToolContext::new(tmp.path());
676 context.state_namespace = "workspace".to_string();
677 let worker_record = manager
678 .read()
679 .await
680 .get_worker_record_for_session("workspace", &id);
681 assert!(
682 worker_record
683 .as_ref()
684 .is_some_and(|record| !record.delivery_evidence.checked)
685 );
686 let projection =
687 subagent_session_projection(&manager, result, false, &context, worker_record).await;
688 assert_eq!(projection.verification.status, "deliverable_missing");
689 assert!(
690 projection
691 .verification
692 .deliverables
693 .iter()
694 .any(|verdict| verdict.path == "report.md")
695 );
696 assert!(
697 manager.read().await.worker_records[&id]
698 .delivery_evidence
699 .checked
700 );
701 }
702
703 fn git_lines(root: &Path, args: &[&str]) -> BTreeSet<String> {
704 let output = std::process::Command::new("git")
705 .arg("-C")
706 .arg(root)
707 .args(args)
708 .output()
709 .expect("git");
710 assert!(
711 output.status.success(),
712 "git {args:?}: {}",
713 String::from_utf8_lossy(&output.stderr)
714 );
715 String::from_utf8_lossy(&output.stdout)
716 .lines()
717 .filter(|line| !line.is_empty())
718 .map(str::to_string)
719 .collect()
720 }
721
722 /// #6557 D02-11: the budget-death checkpoint owns exactly the worker's
723 /// inventory at every size. Past 1000 paths it used to widen to
724 /// `git add -A` and sweep a foreign edit into the salvage commit; below that
725 /// a plain `git commit` took whatever someone else had staged, a glob-shaped
726 /// name staged every matching file, and a `git rm` failed the whole add.
727 #[test]
728 fn checkpoint_commits_only_worker_paths_at_any_inventory_size() {
729 for worker_files in [1001_usize, 3] {
730 let tmp = tempdir().unwrap();
731 let root = tmp.path();
732 repository(root);
733 let literal_glob = "[foreign].rs";
734 for name in [
735 "foreign_edit.rs",
736 "foreign_staged.rs",
737 "f.rs",
738 "removed.rs",
739 "renamed.rs",
740 ] {
741 fs::write(root.join(name), "base\n").unwrap();
742 }
743 git(root, &["add", "--", "."]);
744 git(root, &["commit", "--quiet", "-m", "fixtures"]);
745 // Someone else's work, present before the worker starts.
746 fs::write(root.join("foreign_edit.rs"), "base\nforeign\n").unwrap();
747 // This platform-valid glob spelling would also match this foreign edit.
748 fs::write(root.join("f.rs"), "base\nforeign\n").unwrap();
749 fs::write(root.join("foreign_staged.rs"), "base\nstaged elsewhere\n").unwrap();
750 git(root, &["add", "--", "foreign_staged.rs"]);
751
752 let evidence = DeliveryEvidence::capture_for_handle(root, true);
753
754 fs::write(root.join("src/lib.rs"), "baseline\nworker\n").unwrap();
755 fs::write(root.join(literal_glob), "a literal name, not a glob\n").unwrap();
756 git(root, &["rm", "--quiet", "--", "removed.rs"]);
757 git(root, &["mv", "--", "renamed.rs", "moved.rs"]);
758 fs::create_dir_all(root.join("gen")).unwrap();
759 for index in 0..worker_files {
760 fs::write(root.join(format!("gen/f{index}.rs")), "x\n").unwrap();
761 }
762
763 let changed = evidence.changed_paths(root).expect("inventory");
764 assert!(changed.len() > worker_files, "{worker_files}: {changed:?}");
765 match evidence.checkpoint_uncommitted(&changed, "worker", "wall_time_budget", true) {
766 delivery::BudgetCheckpointOutcome::Committed { .. } => {}
767 delivery::BudgetCheckpointOutcome::Failed { reason } => {
768 panic!("{worker_files} files: checkpoint failed: {reason}")
769 }
770 _ => panic!("{worker_files} files: checkpoint did not commit"),
771 }
772
773 let committed = git_lines(
774 root,
775 &["show", "--no-renames", "--name-only", "--format=", "HEAD"],
776 );
777 let mut expected: BTreeSet<String> = [
778 "src/lib.rs",
779 literal_glob,
780 "removed.rs",
781 "renamed.rs",
782 "moved.rs",
783 ]
784 .into_iter()
785 .map(str::to_string)
786 .collect();
787 expected.extend((0..worker_files).map(|index| format!("gen/f{index}.rs")));
788 assert_eq!(committed, expected, "{worker_files} files");
789 assert_eq!(
790 git_lines(root, &["status", "--porcelain=v1"]),
791 BTreeSet::from([
792 " M f.rs".to_string(),
793 " M foreign_edit.rs".to_string(),
794 "M foreign_staged.rs".to_string(),
795 ]),
796 "{worker_files} files: foreign work stays where it was"
797 );
798 }
799 }
800
800 lines RUST