返回 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_narrow_default_scope_and_reject_invalid_input() {
86 let request = parse_spawn_request(&json!({
87 "type": "implement", "prompt": "write outputs", "deliverables": ["tmp/a/report.md", "tmp/b/report.md"]
88 })).unwrap();
89 assert!(request.write_roots.is_empty());
90 assert_eq!(request.exact_files, ["tmp/a/report.md", "tmp/b/report.md"]);
91 for paths in [
92 json!([""]),
93 json!(["../report.md"]),
94 json!(["/tmp/report.md"]),
95 json!([".git/config"]),
96 json!(["."]),
97 json!([1]),
98 json!("report.md"),
99 ] {
100 assert!(
101 parse_spawn_request(
102 &json!({"type":"implement", "prompt":"report", "deliverables": paths})
103 )
104 .is_err()
105 );
106 }
107 assert!(
108 parse_spawn_request(
109 &json!({"type":"implement", "prompt":"report", "deliverables": vec!["x.md"; 17]})
110 )
111 .is_err()
112 );
113 assert!(
114 delivery::declared_paths(&[], Some("review findings"))
115 .unwrap()
116 .is_empty()
117 );
118 assert_eq!(
119 delivery::declared_paths(&[], Some("report.md")).unwrap(),
120 ["report.md"]
121 );
122 }
123
124 #[test]
125 fn deliverable_verdict_distinguishes_present_missing_empty_directory_and_scope() {
126 let tmp = tempdir().unwrap();
127 fs::write(tmp.path().join("present.md"), "report").unwrap();
128 fs::write(tmp.path().join("empty.md"), "").unwrap();
129 fs::create_dir(tmp.path().join("directory")).unwrap();
130 for (path, status) in [
131 ("present.md", "present"),
132 ("missing.md", "missing"),
133 ("empty.md", "empty"),
134 ("directory", "not_file"),
135 ] {
136 assert_eq!(
137 delivery::check_deliverable(tmp.path(), path, true).status,
138 status
139 );
140 }
141 assert_eq!(
142 delivery::check_deliverable(tmp.path(), "present.md", false).status,
143 "out_of_scope"
144 );
145 }
146
147 #[cfg(unix)]
148 #[test]
149 fn deliverables_refuse_leaf_and_parent_symlink_escape() {
150 let tmp = tempdir().unwrap();
151 let outside = tempdir().unwrap();
152 fs::write(outside.path().join("secret.md"), "outside").unwrap();
153 std::os::unix::fs::symlink(outside.path().join("secret.md"), tmp.path().join("leaf.md"))
154 .unwrap();
155 std::os::unix::fs::symlink(outside.path(), tmp.path().join("linked")).unwrap();
156 for path in ["leaf.md", "linked/secret.md"] {
157 assert_eq!(
158 delivery::check_deliverable(tmp.path(), path, true).status,
159 "invalid_path"
160 );
161 assert!(delivery::safe_deliverable_path(tmp.path(), path).is_err());
162 }
163 }
164
165 #[test]
166 fn missing_declared_deliverable_is_visible_in_terminal_sentinel() {
167 let tmp = tempdir().unwrap();
168 let (mut manager, id) = worker(tmp.path(), true, &["report.md"], &["."]);
169 let verification = complete(&mut manager, &id, "Finished the research.");
170 assert_eq!(verification.status, "deliverable_missing");
171 assert_eq!(verification.deliverables[0].status, "missing");
172 let mut result = manager.get_result(&id).unwrap();
173 result.status = SubAgentStatus::Completed;
174 let completion = subagent_completion_with_verification(
175 "workspace",
176 &result,
177 None,
178 Some(&verification),
179 None,
180 );
181 assert!(completion.payload.contains("deliverable_missing"));
182 assert!(completion.payload.contains("report.md"));
183 }
184
185 #[test]
186 fn declared_output_is_not_an_undeclared_edit_and_undeclared_worker_stays_self_reported() {
187 let tmp = tempdir().unwrap();
188 repository(tmp.path());
189 let (mut manager, id) = worker(tmp.path(), true, &["report.md"], &["."]);
190 fs::write(tmp.path().join("report.md"), "findings").unwrap();
191 let verification = complete(&mut manager, &id, "Report ready.");
192 assert_eq!(verification.status, "deliverables_present");
193 assert_eq!(verification.deliverables[0].bytes, Some(8));
194 let (mut manager, id) = worker(tmp.path(), false, &[], &[]);
195 assert_eq!(
196 complete(&mut manager, &id, "No changes.").status,
197 "self_report_only"
198 );
199 }
200
201 #[test]
202 fn change_like_prose_and_line_citations_are_never_edit_claims() {
203 for report in [
204 "Fixed behavior is documented in src/lib.rs:12-19.",
205 "CHANGES: None\nThe added guard is at src/lib.rs:12-19",
206 "CHANGES: src/lib.rs:12-19",
207 "CHANGES:\n- Reviewed src/lib.rs:12-19.",
208 "CHANGES:\n- Reviewed src/lib.rs:12-19!",
209 "CHANGES:\n- Reviewed src/lib.rs:12-19:",
210 "CHANGES: [src/lib.rs:12-19](src/lib.rs#L12-L19)",
211 "CHANGES: [source](src/lib.rs:12-19)",
212 ] {
213 assert!(
214 delivery::explicit_change_paths(report).is_empty(),
215 "{report}"
216 );
217 }
218 assert_eq!(
219 delivery::explicit_change_paths("CHANGES:\n- src/lib.rs\n- report.md"),
220 BTreeSet::from(["src/lib.rs".into(), "report.md".into()])
221 );
222 let tmp = tempdir().unwrap();
223 repository(tmp.path());
224 let (mut manager, id) = worker(tmp.path(), false, &[], &[]);
225 assert_eq!(
226 complete(&mut manager, &id, "CHANGES: src/lib.rs").status,
227 "self_report_only"
228 );
229 }
230
231 #[test]
232 fn unchanged_dirty_file_does_not_satisfy_a_new_edit_claim() {
233 let tmp = tempdir().unwrap();
234 repository(tmp.path());
235 fs::write(tmp.path().join("src/lib.rs"), "existing dirty work\n").unwrap();
236 let (mut manager, id) = worker(tmp.path(), true, &[], &["src"]);
237 let verification = complete(&mut manager, &id, "CHANGES: src/lib.rs");
238 assert_eq!(verification.status, "claim_mismatch");
239 assert!(verification.summary.contains("declared but unchanged"));
240 assert!(verification.summary.contains("src/lib.rs"));
241 }
242
243 #[test]
244 fn five_heading_output_declares_changed_files_without_claiming_evidence_or_risks() {
245 let tmp = tempdir().unwrap();
246 repository(tmp.path());
247 let (mut manager, id) = worker(tmp.path(), true, &[], &["src"]);
248 fs::write(tmp.path().join("src/lib.rs"), "updated\n").unwrap();
249 manager
250 .worker_records
251 .get_mut(&id)
252 .unwrap()
253 .delivery_evidence
254 .observed_writes
255 .insert("src/lib.rs".into());
256 let report = "### SUMMARY\n\nUpdated the parser.\n\n\
257 ### EVIDENCE\n\n- Reviewed src/reference.rs:12-19.\n\n\
258 ### CHANGES\n\n- `src/lib.rs` — adjusted the parser\n\n\
259 ### RISKS\n\n- src/consumer.rs still needs a separate review\n\n\
260 ### BLOCKERS\n\nNone.\n";
261 for report in [
262 report.to_string(),
263 report.replace("### CHANGES", "### changes"),
264 ] {
265 assert_eq!(
266 delivery::explicit_change_paths(&report),
267 BTreeSet::from(["src/lib.rs".into()])
268 );
269 assert_ne!(
270 complete(&mut manager, &id, &report).status,
271 "claim_mismatch"
272 );
273 }
274 }
275
276 #[test]
277 fn changes_bullet_descriptions_do_not_invent_file_claims() {
278 let tmp = tempdir().unwrap();
279 repository(tmp.path());
280 let (mut manager, id) = worker(tmp.path(), true, &[], &["src"]);
281 fs::write(tmp.path().join("src/lib.rs"), "updated\n").unwrap();
282 manager
283 .worker_records
284 .get_mut(&id)
285 .unwrap()
286 .delivery_evidence
287 .observed_writes
288 .insert("src/lib.rs".into());
289 for declaration in [
290 "- `src/lib.rs` — updated parsing.",
291 "- src/lib.rs: Updated parsing.",
292 "- src/lib.rs updated parsing; reviewed notes.md.",
293 "- [src/lib.rs](src/lib.rs) - Updated parsing! See notes.md.",
294 "- 'src/lib.rs' — matches src/reference.rs:12-19.",
295 ] {
296 let report = format!(
297 "### SUMMARY\n\nUpdated the parser.\n\n\
298 ### EVIDENCE\n\n- Reviewed src/reference.rs:12-19.\n\n\
299 ### CHANGES\n\n{declaration}\n\n\
300 ### RISKS\n\n- Check notes.md separately.\n\n\
301 ### BLOCKERS\n\nNone.\n"
302 );
303 assert_eq!(
304 delivery::explicit_change_paths(&report),
305 BTreeSet::from(["src/lib.rs".into()]),
306 "{declaration}"
307 );
308 assert_ne!(
309 complete(&mut manager, &id, &report).status,
310 "claim_mismatch",
311 "{declaration}"
312 );
313 }
314 }
315
316 #[test]
317 fn explicit_change_path_lists_preserve_quoted_spaces_and_punctuation() {
318 let expected = BTreeSet::from([
319 "src/lib.rs".into(),
320 "src/with spaces.rs".into(),
321 "src/another file.rs".into(),
322 "src/trailing.".into(),
323 ]);
324 for label in ["CHANGES:", "Changed files:", "Files changed:"] {
325 let report = format!(
326 "{label} src/lib.rs, `src/with spaces.rs`; \"src/another file.rs\" 'src/trailing.'"
327 );
328 assert_eq!(delivery::explicit_change_paths(&report), expected);
329 }
330 assert!(delivery::explicit_change_paths("CHANGES: Updated parsing.").is_empty());
331 assert!(
332 delivery::explicit_change_paths("CHANGES: src/reference.rs:12-19. — reviewed only.")
333 .is_empty()
334 );
335 }
336
337 #[test]
338 fn modification_of_already_dirty_file_is_measured_against_spawn_content() {
339 let tmp = tempdir().unwrap();
340 repository(tmp.path());
341 fs::write(tmp.path().join("src/lib.rs"), "existing dirty work\n").unwrap();
342 let (mut manager, id) = worker(tmp.path(), true, &[], &["src"]);
343 fs::write(
344 tmp.path().join("src/lib.rs"),
345 "worker changed this further\n",
346 )
347 .unwrap();
348 assert_eq!(
349 complete(&mut manager, &id, "CHANGES: src/lib.rs").status,
350 "self_report_only"
351 );
352 }
353
354 #[test]
355 fn committed_change_is_compared_with_exact_spawn_head_without_timestamp_guessing() {
356 let tmp = tempdir().unwrap();
357 repository(tmp.path());
358 let (mut manager, id) = worker(tmp.path(), true, &[], &["src"]);
359 fs::write(tmp.path().join("src/lib.rs"), "worker commit\n").unwrap();
360 git(tmp.path(), &["add", "--", "src/lib.rs"]);
361 git(tmp.path(), &["commit", "--quiet", "-m", "worker"]);
362 assert_eq!(
363 complete(&mut manager, &id, "CHANGES: src/lib.rs").status,
364 "self_report_only"
365 );
366 }
367
368 #[test]
369 fn an_observed_write_without_a_declaration_is_flagged_but_external_changes_are_not() {
370 let tmp = tempdir().unwrap();
371 repository(tmp.path());
372 let (mut manager, id) = worker(tmp.path(), true, &[], &["src"]);
373 fs::write(tmp.path().join("src/lib.rs"), "new change\n").unwrap();
374 manager
375 .worker_records
376 .get_mut(&id)
377 .unwrap()
378 .delivery_evidence
379 .observed_writes
380 .insert("src/lib.rs".into());
381 assert_eq!(
382 complete(&mut manager, &id, "CHANGES: None").status,
383 "claim_mismatch"
384 );
385 for scope in ["reports", "."] {
386 let (mut manager, id) = worker(tmp.path(), true, &[], &[scope]);
387 fs::write(
388 tmp.path().join("src/lib.rs"),
389 format!("external {scope} change\n"),
390 )
391 .unwrap();
392 assert!(
393 manager.worker_records[&id]
394 .delivery_evidence
395 .observed_writes
396 .is_empty()
397 );
398 assert_eq!(
399 complete(&mut manager, &id, "CHANGES: None").status,
400 "self_report_only"
401 );
402 }
403 }
404
405 #[test]
406 fn disjoint_sibling_write_paths_admit_and_ancestor_overlap_names_actual_remedy() {
407 let mut ledger = CoordinationLedger::default();
408 let claim = |owner: &str, path: &str| WriteScopeClaim {
409 owner: owner.into(),
410 roots: vec![path.into()],
411 exact_files: Vec::new(),
412 contracts: Vec::new(),
413 };
414 ledger
415 .register_claim(claim("a", "tmp/scan/a"), false, |_| true)
416 .unwrap();
417 ledger
418 .register_claim(claim("b", "tmp/scan/b"), false, |_| true)
419 .unwrap();
420 let error = ledger
421 .register_claim(claim("broad", "tmp/scan"), false, |_| true)
422 .unwrap_err();
423 for text in [
424 "tmp/scan/a",
425 "tmp/scan",
426 "disjoint sibling",
427 "exact_files",
428 "write_authority=read_only",
429 ] {
430 assert!(error.contains(text), "{error}");
431 }
432 }
433
434 #[cfg(unix)]
435 #[tokio::test]
436 #[allow(clippy::print_stderr)] // Test receipt must distinguish refused probes from exercised isolation.
437 async fn enforced_readonly_python_queries_sqlite_under_a_live_peer_write_claim() {
438 let tmp = tempdir().unwrap();
439 let database = rusqlite::Connection::open(tmp.path().join("fixture.sqlite")).unwrap();
440 database
441 .execute_batch(
442 "CREATE TABLE fixture(value TEXT); INSERT INTO fixture VALUES ('peer-read-receipt');",
443 )
444 .unwrap();
445 drop(database);
446 fs::write(tmp.path().join("peer.txt"), "preserve peer bytes").unwrap();
447 fs::write(tmp.path().join("own.txt"), "own bytes").unwrap();
448 let manager = new_shared_subagent_manager(tmp.path().to_path_buf(), 4);
449 {
450 let mut guard = manager.write().await;
451 guard.insert_test_running_agent("analysis", tmp.path());
452 guard.insert_test_running_agent("peer", tmp.path());
453 for (owner, file) in [("agent_analysis", "own.txt"), ("agent_peer", "peer.txt")] {
454 guard
455 .coordination
456 .register_claim(
457 WriteScopeClaim {
458 owner: owner.into(),
459 roots: Vec::new(),
460 exact_files: vec![file.into()],
461 contracts: Vec::new(),
462 },
463 false,
464 |_| true,
465 )
466 .unwrap();
467 }
468 assert_eq!(
469 guard.live_peer_shared_write_claim_owners("agent_analysis"),
470 ["agent_peer"]
471 );
472 }
473 let python = [
474 "/usr/bin/python3",
475 "/opt/homebrew/bin/python3",
476 "/usr/local/bin/python3",
477 ]
478 .into_iter()
479 .find(|path| Path::new(path).is_file())
480 .expect("Python fixture runtime");
481 for role in [FleetRole::Builder, FleetRole::Scout] {
482 let mut runtime = super::tests::stub_runtime();
483 runtime.manager = Arc::clone(&manager);
484 runtime.context = ToolContext::new(tmp.path());
485 runtime.context.auto_approve = true;
486 runtime.context.elevated_sandbox_policy =
487 Some(crate::sandbox::SandboxPolicy::DangerFullAccess);
488 #[cfg(target_os = "linux")]
489 runtime
490 .context
491 .shell_manager
492 .lock()
493 .unwrap()
494 .set_prefer_bwrap(true);
495 let available = runtime
496 .context
497 .shell_manager
498 .lock()
499 .unwrap()
500 .configured_sandbox_type()
501 .is_some();
502 runtime.worker_profile = WorkerRuntimeProfile::for_role(role.clone());
503 let registry = SubAgentToolRegistry::new_with_owner(
504 runtime,
505 role,
506 "agent_analysis".into(),
507 "analysis".into(),
508 Some(vec!["bash".into()]),
509 Arc::new(Mutex::new(TodoList::new())),
510 Arc::new(Mutex::new(PlanState::default())),
511 );
512 let script = "import sqlite3; c=sqlite3.connect('file:fixture.sqlite?mode=ro', uri=True); print(c.execute('SELECT value FROM fixture').fetchone()[0])";
513 let query = json!({"command": format!("{python} -I -B -c {}", shell_words::quote(script)), "read_only": true});
514 let read = registry.execute("agent_analysis", "bash", query).await;
515 if !available {
516 let error = read.unwrap_err().to_string();
517 assert!(error.contains("native read-only enforcement"), "{error}");
518 eprintln!("UNRUN: native child+peer Python probe; unavailable sandbox was refused");
519 continue;
520 }
521 let read = read.unwrap();
522 assert!(read.contains("peer-read-receipt"), "{read}");
523 let write = json!({"command": format!("{python} -I -B -c {}", shell_words::quote("open('peer.txt', 'w').write('corrupt')")), "read_only": true});
524 let error = registry
525 .execute("agent_analysis", "bash", write)
526 .await
527 .unwrap_err()
528 .to_string();
529 assert!(
530 !error.contains("blocking peers"),
531 "read-only execution must reach native enforcement: {error}"
532 );
533 assert_eq!(
534 fs::read_to_string(tmp.path().join("peer.txt")).unwrap(),
535 "preserve peer bytes"
536 );
537 assert_eq!(
538 manager
539 .read()
540 .await
541 .get_result("agent_peer")
542 .unwrap()
543 .status,
544 SubAgentStatus::Running
545 );
546 eprintln!(
547 "NATIVE_READONLY_ENFORCED: child SQLite query passed under a live peer claim; mutation denied"
548 );
549 }
550 }
551
552 /// Deferred-verification contract (#6210): the terminal commit stores the
553 /// worker projection but leaves verification pending for `ensure`.
554 #[test]
555 fn terminal_commit_leaves_delivery_verification_pending() {
556 let tmp = tempdir().unwrap();
557 repository(tmp.path());
558 let (mut manager, id) = worker(tmp.path(), true, &["report.md"], &["."]);
559 let mut result = manager.get_result(&id).unwrap();
560 result.status = SubAgentStatus::Completed;
561 result.result = Some("Finished the research.".into());
562 manager.complete_worker_from_result(&id, &result);
563 let record = manager.worker_records.get(&id).unwrap();
564 assert!(!record.delivery_evidence.checked);
565 assert_eq!(record.verification.status, "self_report_only");
566 assert_eq!(
567 record.result_summary.as_deref(),
568 Some("Finished the research.")
569 );
570 }
571
572 #[tokio::test]
573 async fn ensure_worker_delivery_verified_stores_verdicts_and_is_idempotent() {
574 let tmp = tempdir().unwrap();
575 repository(tmp.path());
576 let (manager, id) = worker(tmp.path(), true, &["report.md"], &["."]);
577 let manager = Arc::new(RwLock::new(manager));
578 let result = {
579 let mut guard = manager.write().await;
580 let mut result = guard.get_result(&id).unwrap();
581 result.status = SubAgentStatus::Completed;
582 result.result = Some("Finished the research.".into());
583 guard.complete_worker_from_result(&id, &result);
584 assert!(!guard.worker_records[&id].delivery_evidence.checked);
585 result
586 };
587 ensure_worker_delivery_verified(&manager, &id, &result).await;
588 let first = manager.read().await.worker_records[&id]
589 .verification
590 .clone();
591 assert_eq!(first.status, "deliverable_missing");
592 assert!(
593 manager.read().await.worker_records[&id]
594 .delivery_evidence
595 .checked
596 );
597 // A second call is a no-op even with a different report.
598 let mut other = result.clone();
599 other.result = Some("CHANGES: src/lib.rs".into());
600 ensure_worker_delivery_verified(&manager, &id, &other).await;
601 assert_eq!(manager.read().await.worker_records[&id].verification, first);
602 }
603
604 #[tokio::test]
605 async fn ensure_worker_delivery_verified_ignores_running_missing_and_checked() {
606 let tmp = tempdir().unwrap();
607 repository(tmp.path());
608 let (manager, id) = worker(tmp.path(), true, &[], &["src"]);
609 let manager = Arc::new(RwLock::new(manager));
610 let running = manager.read().await.get_result(&id).unwrap();
611 assert_eq!(running.status, SubAgentStatus::Running);
612 ensure_worker_delivery_verified(&manager, &id, &running).await;
613 assert!(
614 !manager.read().await.worker_records[&id]
615 .delivery_evidence
616 .checked
617 );
618 // A missing worker id is a silent no-op.
619 let mut missing = running.clone();
620 missing.agent_id = "agent_missing".to_string();
621 missing.status = SubAgentStatus::Completed;
622 ensure_worker_delivery_verified(&manager, "agent_missing", &missing).await;
623 // A checked record keeps its stored verdict.
624 let mut done = running.clone();
625 done.status = SubAgentStatus::Completed;
626 done.result = Some("CHANGES: src/lib.rs".into());
627 ensure_worker_delivery_verified(&manager, &id, &done).await;
628 assert_eq!(
629 manager.read().await.worker_records[&id].verification.status,
630 "claim_mismatch"
631 );
632 let mut changed_mind = done.clone();
633 changed_mind.result = Some("CHANGES: None".into());
634 ensure_worker_delivery_verified(&manager, &id, &changed_mind).await;
635 assert_eq!(
636 manager.read().await.worker_records[&id].verification.status,
637 "claim_mismatch"
638 );
639 }
640
641 /// Read-side backstop (#6210): a terminal detail projection heals a
642 /// verification left pending by a Stop/interrupt/close/stale commit.
643 #[tokio::test]
644 async fn detail_projection_heals_pending_delivery_verification() {
645 let tmp = tempdir().unwrap();
646 repository(tmp.path());
647 let (manager, id) = worker(tmp.path(), true, &["report.md"], &["."]);
648 let manager = Arc::new(RwLock::new(manager));
649 let result = {
650 let mut guard = manager.write().await;
651 let mut result = guard.get_result(&id).unwrap();
652 result.status = SubAgentStatus::Completed;
653 result.result = Some("Finished the research.".into());
654 guard.complete_worker_from_result(&id, &result);
655 result
656 };
657 let mut context = ToolContext::new(tmp.path());
658 context.state_namespace = "workspace".to_string();
659 let worker_record = manager
660 .read()
661 .await
662 .get_worker_record_for_session("workspace", &id);
663 assert!(
664 worker_record
665 .as_ref()
666 .is_some_and(|record| !record.delivery_evidence.checked)
667 );
668 let projection =
669 subagent_session_projection(&manager, result, false, &context, worker_record).await;
670 assert_eq!(projection.verification.status, "deliverable_missing");
671 assert!(
672 projection
673 .verification
674 .deliverables
675 .iter()
676 .any(|verdict| verdict.path == "report.md")
677 );
678 assert!(
679 manager.read().await.worker_records[&id]
680 .delivery_evidence
681 .checked
682 );
683 }
684
684 lines RUST