@team-agent/installer 0.3.9 → 0.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/send.rs +9 -2
  4. package/crates/team-agent/src/coordinator/backoff.rs +83 -2
  5. package/crates/team-agent/src/coordinator/tests/spine.rs +6 -0
  6. package/crates/team-agent/src/coordinator/tick.rs +410 -168
  7. package/crates/team-agent/src/leader/lease.rs +19 -0
  8. package/crates/team-agent/src/leader/rediscover/tests.rs +12 -0
  9. package/crates/team-agent/src/leader/rediscover.rs +2 -0
  10. package/crates/team-agent/src/lifecycle/launch.rs +35 -0
  11. package/crates/team-agent/src/lifecycle/restart/agent.rs +17 -3
  12. package/crates/team-agent/src/lifecycle/restart/common.rs +75 -0
  13. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +201 -3
  14. package/crates/team-agent/src/lifecycle/restart/selection.rs +51 -14
  15. package/crates/team-agent/src/lifecycle/restart.rs +1 -1
  16. package/crates/team-agent/src/lifecycle/tests/core.rs +89 -15
  17. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +68 -3
  18. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +3 -1
  19. package/crates/team-agent/src/mcp_server/helpers.rs +24 -5
  20. package/crates/team-agent/src/mcp_server/normalize.rs +13 -6
  21. package/crates/team-agent/src/mcp_server/tests/send.rs +310 -212
  22. package/crates/team-agent/src/messaging/delivery.rs +83 -2
  23. package/crates/team-agent/src/messaging/helpers.rs +30 -10
  24. package/crates/team-agent/src/messaging/send.rs +71 -14
  25. package/crates/team-agent/src/messaging/tests/basic.rs +25 -7
  26. package/crates/team-agent/src/messaging/tests/runtime.rs +565 -111
  27. package/crates/team-agent/src/messaging/types.rs +19 -4
  28. package/crates/team-agent/src/provider/approvals/parsing.rs +43 -14
  29. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +12 -9
  30. package/crates/team-agent/src/transport/test_support.rs +12 -1
  31. package/package.json +4 -4
@@ -43,9 +43,11 @@ fn session_drift_refusal_none_for_no_target_leader_or_broadcast() {
43
43
  .is_none()
44
44
  );
45
45
  // target == "*" (broadcast) → None.
46
- assert!(session_drift_refusal(&state, "*", "leader", "s", None, &log)
47
- .unwrap()
48
- .is_none());
46
+ assert!(
47
+ session_drift_refusal(&state, "*", "leader", "s", None, &log)
48
+ .unwrap()
49
+ .is_none()
50
+ );
49
51
  }
50
52
 
51
53
  #[test]
@@ -53,9 +55,11 @@ fn session_drift_refusal_none_when_status_not_drift() {
53
55
  let ws = tmp_ws("driftok");
54
56
  let log = EventLog::new(&ws);
55
57
  let state = json(serde_json::json!({"agents": {"w1": {"status": "idle"}}}));
56
- assert!(session_drift_refusal(&state, "w1", "leader", "s", None, &log)
57
- .unwrap()
58
- .is_none());
58
+ assert!(
59
+ session_drift_refusal(&state, "w1", "leader", "s", None, &log)
60
+ .unwrap()
61
+ .is_none()
62
+ );
59
63
  }
60
64
 
61
65
  #[test]
@@ -149,7 +153,13 @@ fn classify_recent_provider_output_is_working_low_confidence() {
149
153
  fn classify_idle_prompt_beats_recent_output_for_just_launched_agent() {
150
154
  let state = json(serde_json::json!({}));
151
155
  let recent = chrono::Utc::now().to_rfc3339();
152
- let a = classify_agent_activity(&state, "codex ready\n❯ \n", false, Some("codex"), Some(&recent));
156
+ let a = classify_agent_activity(
157
+ &state,
158
+ "codex ready\n❯ \n",
159
+ false,
160
+ Some("codex"),
161
+ Some(&recent),
162
+ );
153
163
  assert_eq!(
154
164
  a.status,
155
165
  ActivityStatus::Idle,
@@ -158,7 +168,10 @@ fn classify_idle_prompt_beats_recent_output_for_just_launched_agent() {
158
168
  the startup banner is recent (activity.rs:192 recent-output fires before latest_prompt_signal:200) \
159
169
  — the Stage B dispatch deferred_busy regression. got {a:?}"
160
170
  );
161
- assert_eq!(a.confidence, 0.9, "golden idle-prompt confidence is 0.9; got {a:?}");
171
+ assert_eq!(
172
+ a.confidence, 0.9,
173
+ "golden idle-prompt confidence is 0.9; got {a:?}"
174
+ );
162
175
  }
163
176
 
164
177
  // ════════════════════════════════════════════════════════════════════════
@@ -227,7 +240,10 @@ fn trust_auto_answer_own_workspace_realpath_equal_answers() {
227
240
  &log,
228
241
  )
229
242
  .unwrap();
230
- assert!(out.answered, "own-workspace realpath-equal prompt must auto-answer");
243
+ assert!(
244
+ out.answered,
245
+ "own-workspace realpath-equal prompt must auto-answer"
246
+ );
231
247
  assert_eq!(out.reason, "trust_auto_answered");
232
248
  }
233
249
 
@@ -269,7 +285,10 @@ fn pane_width_failed_forces_exact_match_never_default() {
269
285
  )
270
286
  .unwrap();
271
287
  // fail-safe: no width → exact-equality only → truncated prefix refused.
272
- assert!(!out.answered, "Failed pane-width must NOT enable prefix/truncation matching");
288
+ assert!(
289
+ !out.answered,
290
+ "Failed pane-width must NOT enable prefix/truncation matching"
291
+ );
273
292
  assert_eq!(out.reason, "workspace_dir_mismatch");
274
293
  assert_eq!(out.action.as_deref(), Some("prompt_leader"));
275
294
  }
@@ -352,6 +371,80 @@ fn send_message_target_not_in_team_is_refused() {
352
371
  assert_eq!(out.reason, Some(DeliveryRefusal::TargetNotInTeam));
353
372
  }
354
373
 
374
+ #[test]
375
+ fn message_not_silently_stuck_accepted_when_coordinator_dead() {
376
+ let ws = tmp_ws("sendcoorddead");
377
+ crate::state::persist::save_runtime_state(
378
+ &ws,
379
+ &serde_json::json!({
380
+ "session_name": "team-x",
381
+ "agents": {
382
+ "worker-1": {"status": "running", "agent_id": "worker-1", "window": "worker-1"},
383
+ },
384
+ }),
385
+ )
386
+ .unwrap();
387
+ let coordinator_ws = crate::coordinator::WorkspacePath::new(ws.clone());
388
+ std::fs::create_dir_all(crate::model::paths::runtime_dir(&ws)).unwrap();
389
+ let _ = MessageStore::open(&ws).unwrap();
390
+ let stale_pid = crate::coordinator::Pid::new(99_999_999);
391
+ crate::coordinator::write_coordinator_metadata(
392
+ &coordinator_ws,
393
+ stale_pid,
394
+ crate::coordinator::MetadataSource::Boot,
395
+ )
396
+ .unwrap();
397
+ std::fs::write(
398
+ crate::coordinator::coordinator_pid_path(&coordinator_ws),
399
+ stale_pid.to_string(),
400
+ )
401
+ .unwrap();
402
+
403
+ let out = send_message(
404
+ &ws,
405
+ &MessageTarget::Single("worker-1".to_string()),
406
+ "hi",
407
+ &SendOptions::default(),
408
+ )
409
+ .unwrap();
410
+
411
+ assert!(!out.ok);
412
+ assert_eq!(out.status, DeliveryStatus::Degraded);
413
+ assert_eq!(out.message_status.0, "degraded");
414
+ assert_eq!(out.reason, Some(DeliveryRefusal::CoordinatorUnavailable));
415
+ assert!(
416
+ out.verification
417
+ .as_deref()
418
+ .is_some_and(|warning| warning.contains("coordinator is not running")),
419
+ "N38 warning must explain why the message was not queued; out={out:?}"
420
+ );
421
+ let store = MessageStore::open(&ws).unwrap();
422
+ let conn = crate::db::schema::open_db(store.db_path()).unwrap();
423
+ let accepted: i64 = conn
424
+ .query_row(
425
+ "select count(*) from messages where status = 'accepted'",
426
+ [],
427
+ |row| row.get(0),
428
+ )
429
+ .unwrap();
430
+ assert_eq!(
431
+ accepted, 0,
432
+ "dead coordinator send must not strand an accepted row"
433
+ );
434
+ let events = EventLog::new(&ws).tail(20).unwrap();
435
+ assert!(
436
+ events.iter().any(|event| {
437
+ event.get("event").and_then(serde_json::Value::as_str)
438
+ == Some("send.coordinator_unavailable")
439
+ && event
440
+ .get("message_queued")
441
+ .and_then(serde_json::Value::as_bool)
442
+ == Some(false)
443
+ }),
444
+ "send.coordinator_unavailable event must be durable; events={events:?}"
445
+ );
446
+ }
447
+
355
448
  #[test]
356
449
  fn send_message_broadcast_empty_team_skips_no_recipients() {
357
450
  // send.py:391-393 — "*" with no team recipients →
@@ -455,14 +548,8 @@ fn report_result_valid_envelope_returns_ok_with_result_id() {
455
548
  let out = report_result(&ws, &envelope).unwrap();
456
549
  // results.py:216-227 — ok True with result_id/task_id/agent_id echoed.
457
550
  assert_eq!(out.get("ok").and_then(|v| v.as_bool()), Some(true));
458
- assert_eq!(
459
- out.get("task_id").and_then(|v| v.as_str()),
460
- Some("t1")
461
- );
462
- assert_eq!(
463
- out.get("agent_id").and_then(|v| v.as_str()),
464
- Some("alice")
465
- );
551
+ assert_eq!(out.get("task_id").and_then(|v| v.as_str()), Some("t1"));
552
+ assert_eq!(out.get("agent_id").and_then(|v| v.as_str()), Some("alice"));
466
553
  assert!(out.get("result_id").and_then(|v| v.as_str()).is_some());
467
554
  }
468
555
 
@@ -523,7 +610,9 @@ fn report_result_funnels_into_leader_delivery_primitive_not_queued_scheduled_eve
523
610
  // No `scheduled_events` rows: the queued parallel path is gone.
524
611
  let conn = seed_conn(&store);
525
612
  let scheduled_count: i64 = conn
526
- .query_row("select count(*) from scheduled_events", [], |row| row.get(0))
613
+ .query_row("select count(*) from scheduled_events", [], |row| {
614
+ row.get(0)
615
+ })
527
616
  .unwrap();
528
617
  assert_eq!(
529
618
  scheduled_count, 0,
@@ -544,15 +633,16 @@ fn report_result_funnels_into_leader_delivery_primitive_not_queued_scheduled_eve
544
633
  "I-4: leader_notified=false when no leader pane is bound"
545
634
  );
546
635
  assert!(
547
- out.get("notification_event_id").is_some_and(|v| v.is_null()),
636
+ out.get("notification_event_id")
637
+ .is_some_and(|v| v.is_null()),
548
638
  "no scheduled_events row → notification_event_id is null"
549
639
  );
550
640
 
551
641
  // Audit events: the funnel emits leader_receiver.delivery_blocked (I-4 rebind),
552
642
  // and the legacy mcp.report_result_notify_queued audit is gone.
553
643
  let events_path = ws.join(".team").join("logs").join("events.jsonl");
554
- let event_lines = std::fs::read_to_string(events_path)
555
- .expect("report_result writes events.jsonl");
644
+ let event_lines =
645
+ std::fs::read_to_string(events_path).expect("report_result writes events.jsonl");
556
646
  assert!(
557
647
  event_lines.contains("\"leader_receiver.delivery_blocked\""),
558
648
  "I-4 rebind path must emit leader_receiver.delivery_blocked audit; got {event_lines}",
@@ -595,14 +685,7 @@ fn notify_result_watchers_no_match_returns_empty() {
595
685
  let watchers = vec![json(serde_json::json!({
596
686
  "watcher_id": "w-x", "task_id": "OTHER", "agent_id": "alice"
597
687
  }))];
598
- let notices = notify_result_watchers(
599
- &ws,
600
- &result,
601
- &log,
602
- Some(&watchers),
603
- None,
604
- )
605
- .unwrap();
688
+ let notices = notify_result_watchers(&ws, &result, &log, Some(&watchers), None).unwrap();
606
689
  assert!(notices.is_empty());
607
690
  }
608
691
 
@@ -625,14 +708,16 @@ fn notify_result_watchers_supersedes_duplicate_watchers() {
625
708
  "created_at": "2026-06-02T11:00:00+00:00"
626
709
  })),
627
710
  ];
628
- let notices =
629
- notify_result_watchers(&ws, &result, &log, Some(&watchers), None).unwrap();
711
+ let notices = notify_result_watchers(&ws, &result, &log, Some(&watchers), None).unwrap();
630
712
  // The late watcher must appear as a superseded (not-ok) notice — exactly-once.
631
713
  let superseded = notices
632
714
  .iter()
633
715
  .find(|n| n.watcher_id == "w-late")
634
716
  .expect("late watcher must be reported");
635
- assert!(!superseded.ok, "duplicate watcher must be superseded, not re-delivered");
717
+ assert!(
718
+ !superseded.ok,
719
+ "duplicate watcher must be superseded, not re-delivered"
720
+ );
636
721
  }
637
722
 
638
723
  // ════════════════════════════════════════════════════════════════════════
@@ -654,17 +739,36 @@ fn requeue_after_claim_leader_skips_already_notified() {
654
739
  let team = TeamKey::new("team-a");
655
740
  let pane = PaneId::new("%new-leader");
656
741
 
657
- let w_un = seed_watcher(&store, "w-unnotified", "team-a", "t1", "alice", "pending", None, None);
742
+ let w_un = seed_watcher(
743
+ &store,
744
+ "w-unnotified",
745
+ "team-a",
746
+ "t1",
747
+ "alice",
748
+ "pending",
749
+ None,
750
+ None,
751
+ );
658
752
  let w_notified = seed_watcher(
659
- &store, "w-notified", "team-a", "t2", "bob", "pending", None, Some("msg_already"),
753
+ &store,
754
+ "w-notified",
755
+ "team-a",
756
+ "t2",
757
+ "bob",
758
+ "pending",
759
+ None,
760
+ Some("msg_already"),
660
761
  );
661
762
 
662
- let requeued =
663
- requeue_after_claim_leader(&ws, &store, &log, &team, &pane, None).unwrap();
763
+ let requeued = requeue_after_claim_leader(&ws, &store, &log, &team, &pane, None).unwrap();
664
764
 
665
765
  // ONLY the un-notified watcher requeues (the notified one is the dedupe gate).
666
766
  let ids: Vec<&str> = requeued.iter().map(|n| n.watcher_id.as_str()).collect();
667
- assert_eq!(ids, vec![w_un.as_str()], "exactly the un-notified watcher requeues");
767
+ assert_eq!(
768
+ ids,
769
+ vec![w_un.as_str()],
770
+ "exactly the un-notified watcher requeues"
771
+ );
668
772
  assert!(
669
773
  !requeued.iter().any(|n| n.watcher_id == w_notified),
670
774
  "already-notified watcher must NOT be requeued (Gap 32)"
@@ -719,10 +823,13 @@ fn requeue_delivery_exhausted_watchers_reopens_only_exhausted() {
719
823
  None,
720
824
  );
721
825
 
722
- let requeued =
723
- requeue_delivery_exhausted_watchers(&ws, &store, &log, &team, &pane).unwrap();
826
+ let requeued = requeue_delivery_exhausted_watchers(&ws, &store, &log, &team, &pane).unwrap();
724
827
 
725
- assert_eq!(requeued.len(), 1, "only delivery_exhausted unnotified watchers requeue");
828
+ assert_eq!(
829
+ requeued.len(),
830
+ 1,
831
+ "only delivery_exhausted unnotified watchers requeue"
832
+ );
726
833
  let notice = &requeued[0];
727
834
  assert_eq!(notice.watcher_id, exhausted);
728
835
  assert_eq!(notice.result_id.as_deref(), Some(rid.as_str()));
@@ -734,26 +841,55 @@ fn requeue_delivery_exhausted_watchers_reopens_only_exhausted() {
734
841
  // R8 (golden): attach requeue leaves the watcher at notify_failed and DEFERS retry to the coordinator
735
842
  // tick — it does NOT immediately re-deliver (only the claim path retries). So the persisted status is
736
843
  // notify_failed, not 'notified'.
737
- assert_eq!(status, "notify_failed", "attach requeue flips to notify_failed and defers retry (golden)");
844
+ assert_eq!(
845
+ status, "notify_failed",
846
+ "attach requeue flips to notify_failed and defers retry (golden)"
847
+ );
738
848
  let (status, notified_id) = watcher_state(&store, &notified);
739
849
  assert_eq!(status, "delivery_exhausted");
740
850
  assert_eq!(notified_id.as_deref(), Some("msg_done"));
741
851
  let (status, _notified_id) = watcher_state(&store, &failed);
742
- assert_eq!(status, "notify_failed", "non-exhausted watcher is not selected");
852
+ assert_eq!(
853
+ status, "notify_failed",
854
+ "non-exhausted watcher is not selected"
855
+ );
743
856
 
744
857
  // R8 (golden leader/__init__.py:46-50): result_watcher.requeued is the ATTACH form
745
858
  // {watcher_id, trigger:"attach_leader", new_pane_id} — NOT the claim-style {prior_state,claimed_pane_id,team_id}.
746
859
  let events = log.tail(0).unwrap();
747
- let ev = events.iter().rev()
748
- .find(|event| event.get("event").and_then(|v| v.as_str()) == Some("result_watcher.requeued"))
860
+ let ev = events
861
+ .iter()
862
+ .rev()
863
+ .find(|event| {
864
+ event.get("event").and_then(|v| v.as_str()) == Some("result_watcher.requeued")
865
+ })
749
866
  .expect("result_watcher.requeued event");
750
- let keys: std::collections::BTreeSet<&str> = ev.as_object().unwrap().keys()
751
- .map(String::as_str).filter(|k| *k != "ts" && *k != "event").collect();
752
- let expected: std::collections::BTreeSet<&str> = ["watcher_id", "trigger", "new_pane_id"].into_iter().collect();
753
- assert_eq!(keys, expected, "result_watcher.requeued must be golden attach form {{watcher_id, trigger, new_pane_id}}");
754
- assert_eq!(ev.get("watcher_id").and_then(|v| v.as_str()), Some("w-exhausted"));
755
- assert_eq!(ev.get("trigger").and_then(|v| v.as_str()), Some("attach_leader"));
756
- assert_eq!(ev.get("new_pane_id").and_then(|v| v.as_str()), Some("%leader"));
867
+ let keys: std::collections::BTreeSet<&str> = ev
868
+ .as_object()
869
+ .unwrap()
870
+ .keys()
871
+ .map(String::as_str)
872
+ .filter(|k| *k != "ts" && *k != "event")
873
+ .collect();
874
+ let expected: std::collections::BTreeSet<&str> = ["watcher_id", "trigger", "new_pane_id"]
875
+ .into_iter()
876
+ .collect();
877
+ assert_eq!(
878
+ keys, expected,
879
+ "result_watcher.requeued must be golden attach form {{watcher_id, trigger, new_pane_id}}"
880
+ );
881
+ assert_eq!(
882
+ ev.get("watcher_id").and_then(|v| v.as_str()),
883
+ Some("w-exhausted")
884
+ );
885
+ assert_eq!(
886
+ ev.get("trigger").and_then(|v| v.as_str()),
887
+ Some("attach_leader")
888
+ );
889
+ assert_eq!(
890
+ ev.get("new_pane_id").and_then(|v| v.as_str()),
891
+ Some("%leader")
892
+ );
757
893
  }
758
894
 
759
895
  // ════════════════════════════════════════════════════════════════════════
@@ -767,10 +903,11 @@ fn stuck_cancel_none_alert_type_expands_to_all() {
767
903
  let ws = tmp_ws("stuckcancel");
768
904
  let out = stuck_cancel(&ws, "w1", None, "leader").unwrap();
769
905
  // The suppression result must enumerate all three alert types.
770
- let types = out
771
- .get("alert_types")
772
- .and_then(|v| v.as_array())
773
- .map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect::<Vec<_>>());
906
+ let types = out.get("alert_types").and_then(|v| v.as_array()).map(|a| {
907
+ a.iter()
908
+ .filter_map(|x| x.as_str().map(str::to_string))
909
+ .collect::<Vec<_>>()
910
+ });
774
911
  assert_eq!(
775
912
  types,
776
913
  Some(vec![
@@ -825,9 +962,18 @@ fn collect_accepts_message_scoped_result_for_matching_recipient() {
825
962
  .and_then(|v| v.as_array())
826
963
  .expect("collected_results");
827
964
  assert_eq!(collected.len(), 1);
828
- assert_eq!(collected[0].get("task_id").and_then(|v| v.as_str()), Some(message_id.as_str()));
829
- assert_eq!(collected[0].get("agent_id").and_then(|v| v.as_str()), Some("w1"));
830
- assert_eq!(collected[0].get("scope").and_then(|v| v.as_str()), Some("message"));
965
+ assert_eq!(
966
+ collected[0].get("task_id").and_then(|v| v.as_str()),
967
+ Some(message_id.as_str())
968
+ );
969
+ assert_eq!(
970
+ collected[0].get("agent_id").and_then(|v| v.as_str()),
971
+ Some("w1")
972
+ );
973
+ assert_eq!(
974
+ collected[0].get("scope").and_then(|v| v.as_str()),
975
+ Some("message")
976
+ );
831
977
  // D3 (leader-adjudicated): golden collected_results entry is EXACTLY the 8-key summary for BOTH
832
978
  // scopes; golden's task_status feeds only the `collect.result` EVENT, never the entry. So a
833
979
  // message-scope entry carries NO task_status key (the prior `Some("message_scoped")` lock encoded a
@@ -837,7 +983,12 @@ fn collect_accepts_message_scoped_result_for_matching_recipient() {
837
983
  "collected_results entry must NOT carry task_status (golden 8-key summary; event-only); got {:?}",
838
984
  collected[0]
839
985
  );
840
- let keys: Vec<&str> = collected[0].as_object().expect("entry is an object").keys().map(String::as_str).collect();
986
+ let keys: Vec<&str> = collected[0]
987
+ .as_object()
988
+ .expect("entry is an object")
989
+ .keys()
990
+ .map(String::as_str)
991
+ .collect();
841
992
  assert_eq!(
842
993
  keys,
843
994
  vec!["result_id", "task_id", "agent_id", "status", "summary", "tests", "created_at", "scope"],
@@ -868,7 +1019,10 @@ fn collect_rejects_message_scoped_result_without_matching_recipient() {
868
1019
  .and_then(|v| v.as_array())
869
1020
  .expect("invalid_results");
870
1021
  assert_eq!(invalid.len(), 1);
871
- assert_eq!(invalid[0].get("task_id").and_then(|v| v.as_str()), Some(message_id.as_str()));
1022
+ assert_eq!(
1023
+ invalid[0].get("task_id").and_then(|v| v.as_str()),
1024
+ Some(message_id.as_str())
1025
+ );
872
1026
  assert_eq!(
873
1027
  invalid[0].get("error").and_then(|v| v.as_str()),
874
1028
  Some(format!("unknown task id: {message_id}").as_str())
@@ -882,7 +1036,10 @@ fn allow_peer_talk_records_bidirectional_allowlist_and_event() {
882
1036
  assert_eq!(out.get("ok").and_then(|v| v.as_bool()), Some(true));
883
1037
  assert_eq!(out.get("a").and_then(|v| v.as_str()), Some("alice"));
884
1038
  assert_eq!(out.get("b").and_then(|v| v.as_str()), Some("bob"));
885
- assert_eq!(out.get("status").and_then(|v| v.as_str()), Some("compat_noop"));
1039
+ assert_eq!(
1040
+ out.get("status").and_then(|v| v.as_str()),
1041
+ Some("compat_noop")
1042
+ );
886
1043
  assert_eq!(
887
1044
  out.get("reason").and_then(|v| v.as_str()),
888
1045
  Some("team_scoped_peer_messages_enabled")
@@ -893,7 +1050,9 @@ fn allow_peer_talk_records_bidirectional_allowlist_and_event() {
893
1050
  let rows = conn
894
1051
  .prepare("select a, b from peer_allowlist order by a, b")
895
1052
  .unwrap()
896
- .query_map([], |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)))
1053
+ .query_map([], |row| {
1054
+ Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
1055
+ })
897
1056
  .unwrap()
898
1057
  .collect::<Result<Vec<_>, _>>()
899
1058
  .unwrap();
@@ -908,7 +1067,9 @@ fn allow_peer_talk_records_bidirectional_allowlist_and_event() {
908
1067
  let events = EventLog::new(&ws).tail(10).unwrap();
909
1068
  let event = events
910
1069
  .iter()
911
- .find(|event| event.get("event").and_then(|v| v.as_str()) == Some("communication.peer_allowed"))
1070
+ .find(|event| {
1071
+ event.get("event").and_then(|v| v.as_str()) == Some("communication.peer_allowed")
1072
+ })
912
1073
  .expect("communication.peer_allowed event");
913
1074
  assert_eq!(event.get("a").and_then(|v| v.as_str()), Some("alice"));
914
1075
  assert_eq!(event.get("b").and_then(|v| v.as_str()), Some("bob"));
@@ -1017,7 +1178,8 @@ fn deliver_pending_message_missing_message_fails() {
1017
1178
  let store = store_for(&ws);
1018
1179
  let log = EventLog::new(&ws);
1019
1180
  let t = NoopTransport;
1020
- let out = deliver_pending_message(&ws, &store, &t, "nope", &log, &serde_json::json!({})).unwrap();
1181
+ let out =
1182
+ deliver_pending_message(&ws, &store, &t, "nope", &log, &serde_json::json!({})).unwrap();
1021
1183
  assert!(!out.ok);
1022
1184
  assert_eq!(out.status, DeliveryStatus::Failed);
1023
1185
  }
@@ -1046,8 +1208,12 @@ fn fire_due_scheduled_events_fires_each_scheduled_kind() {
1046
1208
  "%w1",
1047
1209
  &serde_json::json!({"content": "ping", "attempt": 1, "max_attempts": 1}),
1048
1210
  );
1049
- let ping_id =
1050
- seed_scheduled_event(&store, ScheduledKind::HealthPing, "%w1", &serde_json::json!({}));
1211
+ let ping_id = seed_scheduled_event(
1212
+ &store,
1213
+ ScheduledKind::HealthPing,
1214
+ "%w1",
1215
+ &serde_json::json!({}),
1216
+ );
1051
1217
  let trust_id = seed_scheduled_event(
1052
1218
  &store,
1053
1219
  ScheduledKind::TrustRetry,
@@ -1065,7 +1231,138 @@ fn fire_due_scheduled_events_fires_each_scheduled_kind() {
1065
1231
  "scheduled event id {id} (each ScheduledKind) must fire; got {fired:?}"
1066
1232
  );
1067
1233
  }
1068
- assert_eq!(fired.len(), 3, "exactly the three seeded due events fire, no extras");
1234
+ assert_eq!(
1235
+ fired.len(),
1236
+ 3,
1237
+ "exactly the three seeded due events fire, no extras"
1238
+ );
1239
+ }
1240
+
1241
+ struct UnverifiedInjectTransport;
1242
+ impl Transport for UnverifiedInjectTransport {
1243
+ fn kind(&self) -> BackendKind {
1244
+ BackendKind::Tmux
1245
+ }
1246
+ fn spawn_first(
1247
+ &self,
1248
+ _s: &SessionName,
1249
+ _w: &WindowName,
1250
+ _a: &[String],
1251
+ _c: &Path,
1252
+ _e: &BTreeMap<String, String>,
1253
+ ) -> Result<SpawnResult, TransportError> {
1254
+ unimplemented!("not reached in delivery")
1255
+ }
1256
+ fn spawn_into(
1257
+ &self,
1258
+ _s: &SessionName,
1259
+ _w: &WindowName,
1260
+ _a: &[String],
1261
+ _c: &Path,
1262
+ _e: &BTreeMap<String, String>,
1263
+ ) -> Result<SpawnResult, TransportError> {
1264
+ unimplemented!("not reached in delivery")
1265
+ }
1266
+ fn inject(
1267
+ &self,
1268
+ _t: &Target,
1269
+ _p: &InjectPayload,
1270
+ _s: Key,
1271
+ _b: bool,
1272
+ ) -> Result<InjectReport, TransportError> {
1273
+ Ok(InjectReport {
1274
+ stage_reached: crate::transport::InjectStage::Submit,
1275
+ inject_verification: crate::transport::InjectVerification::CaptureContainsToken,
1276
+ submit_verification:
1277
+ crate::transport::SubmitVerification::PastedContentPromptStillPresentAfterSubmit,
1278
+ turn_verification: crate::transport::TurnVerification::NotYetObserved,
1279
+ attempts: u32::from(SEND_RETRY_MAX_ATTEMPTS),
1280
+ })
1281
+ }
1282
+ fn send_keys(&self, _t: &Target, _k: &[Key]) -> Result<(), TransportError> {
1283
+ Ok(())
1284
+ }
1285
+ fn capture(&self, _t: &Target, range: CaptureRange) -> Result<CapturedText, TransportError> {
1286
+ Ok(CapturedText {
1287
+ text: String::new(),
1288
+ range,
1289
+ })
1290
+ }
1291
+ fn query(&self, _t: &Target, _f: PaneField) -> Result<Option<String>, TransportError> {
1292
+ Ok(None)
1293
+ }
1294
+ fn liveness(&self, _p: &PaneId) -> Result<PaneLiveness, TransportError> {
1295
+ Ok(PaneLiveness::Unknown)
1296
+ }
1297
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
1298
+ Ok(Vec::new())
1299
+ }
1300
+ fn has_session(&self, _s: &SessionName) -> Result<bool, TransportError> {
1301
+ Ok(true)
1302
+ }
1303
+ fn list_windows(&self, _s: &SessionName) -> Result<Vec<WindowName>, TransportError> {
1304
+ Ok(Vec::new())
1305
+ }
1306
+ fn set_session_env(
1307
+ &self,
1308
+ _s: &SessionName,
1309
+ _k: &str,
1310
+ _v: &str,
1311
+ ) -> Result<SetEnvOutcome, TransportError> {
1312
+ Ok(SetEnvOutcome::Applied)
1313
+ }
1314
+ fn kill_session(&self, _s: &SessionName) -> Result<(), TransportError> {
1315
+ Ok(())
1316
+ }
1317
+ fn kill_window(&self, _t: &Target) -> Result<(), TransportError> {
1318
+ Ok(())
1319
+ }
1320
+ fn attach_session(&self, _s: &SessionName) -> Result<AttachOutcome, TransportError> {
1321
+ Ok(AttachOutcome::Attached)
1322
+ }
1323
+ }
1324
+
1325
+ #[test]
1326
+ fn deliver_pending_exhausted_unverified_send_emits_failed_event() {
1327
+ let ws = tmp_ws("sendfailed");
1328
+ let store = store_for(&ws);
1329
+ let log = EventLog::new(&ws);
1330
+ let state = serde_json::json!({
1331
+ "session_name": "team-sendfailed",
1332
+ "leader_receiver": {"pane_id": "%leader"},
1333
+ "agents": {"w1": {"provider": "fake", "pane_id": "%1"}}
1334
+ });
1335
+ crate::state::persist::save_runtime_state(&ws, &state).unwrap();
1336
+ let message_id = store
1337
+ .create_message(None, "leader", "w1", "ping", None, false, None)
1338
+ .unwrap();
1339
+
1340
+ let out = deliver_pending_message(
1341
+ &ws,
1342
+ &store,
1343
+ &UnverifiedInjectTransport,
1344
+ &message_id,
1345
+ &log,
1346
+ &state,
1347
+ )
1348
+ .unwrap();
1349
+
1350
+ assert!(!out.ok);
1351
+ assert_eq!(out.message_status.0, "failed");
1352
+ let events = log.tail(0).unwrap();
1353
+ assert!(
1354
+ events.iter().any(
1355
+ |event| event.get("event").and_then(serde_json::Value::as_str) == Some("send.failed")
1356
+ ),
1357
+ "exhausted unverified send must emit send.failed; got {events:?}"
1358
+ );
1359
+ assert!(
1360
+ events.iter().any(
1361
+ |event| event.get("event").and_then(serde_json::Value::as_str)
1362
+ == Some("send.failed_notification")
1363
+ ),
1364
+ "exhausted unverified send must queue a leader-visible notification; got {events:?}"
1365
+ );
1069
1366
  }
1070
1367
 
1071
1368
  // ════════════════════════════════════════════════════════════════════════
@@ -1089,14 +1386,28 @@ fn retry_result_deliveries_retries_notify_failed_watcher() {
1089
1386
 
1090
1387
  let rid = seed_result(&store, "res_r1", "t1", "alice", "success");
1091
1388
  let w = seed_watcher(
1092
- &store, "w-failed", "team-a", "t1", "alice", "notify_failed", Some(&rid), None,
1389
+ &store,
1390
+ "w-failed",
1391
+ "team-a",
1392
+ "t1",
1393
+ "alice",
1394
+ "notify_failed",
1395
+ Some(&rid),
1396
+ None,
1093
1397
  );
1094
1398
 
1095
1399
  let notices = retry_result_deliveries(&ws, &log).unwrap();
1096
1400
 
1097
- assert_eq!(notices.len(), 1, "the single notify_failed watcher must be retried");
1401
+ assert_eq!(
1402
+ notices.len(),
1403
+ 1,
1404
+ "the single notify_failed watcher must be retried"
1405
+ );
1098
1406
  let notice = &notices[0];
1099
- assert_eq!(notice.watcher_id, w, "the retried notice names the seeded watcher");
1407
+ assert_eq!(
1408
+ notice.watcher_id, w,
1409
+ "the retried notice names the seeded watcher"
1410
+ );
1100
1411
  assert_eq!(
1101
1412
  notice.result_id.as_deref(),
1102
1413
  Some(rid.as_str()),
@@ -1124,18 +1435,31 @@ fn collect_results_and_notify_watchers_returns_concrete_ok_shape() {
1124
1435
  let log = EventLog::new(&ws);
1125
1436
 
1126
1437
  seed_watcher(
1127
- &store, "w-orphan", "team-a", "t1", "alice", "notify_failed", Some("res_missing"), None,
1438
+ &store,
1439
+ "w-orphan",
1440
+ "team-a",
1441
+ "t1",
1442
+ "alice",
1443
+ "notify_failed",
1444
+ Some("res_missing"),
1445
+ None,
1128
1446
  );
1129
1447
 
1130
1448
  let out = collect_results_and_notify_watchers(&ws, &log).unwrap();
1131
- assert_eq!(out.get("ok").and_then(|v| v.as_bool()), Some(true), "ok==true");
1449
+ assert_eq!(
1450
+ out.get("ok").and_then(|v| v.as_bool()),
1451
+ Some(true),
1452
+ "ok==true"
1453
+ );
1132
1454
  assert_eq!(
1133
1455
  out.get("collected").and_then(|v| v.as_i64()),
1134
1456
  Some(0),
1135
1457
  "no uncollected results → collected==0"
1136
1458
  );
1137
1459
  assert_eq!(
1138
- out.get("notified").and_then(|v| v.as_array()).map(|a| a.len()),
1460
+ out.get("notified")
1461
+ .and_then(|v| v.as_array())
1462
+ .map(|a| a.len()),
1139
1463
  Some(0),
1140
1464
  "orphan watcher (missing result) is skipped → notified empty"
1141
1465
  );
@@ -1181,26 +1505,52 @@ fn collect_task_scoped_result_collects_and_marks_task_done() {
1181
1505
  "agents": { "w1": { "provider": "codex" } },
1182
1506
  "tasks": [ { "id": "t2", "assignee": "w1", "title": "t2", "status": "pending" } ]
1183
1507
  }),
1184
- ).unwrap();
1508
+ )
1509
+ .unwrap();
1185
1510
  let store = store_for(&ws);
1186
1511
  seed_result(&store, "res_t2", "t2", "w1", "success");
1187
1512
 
1188
1513
  let out = collect(&ws, None, false).unwrap();
1189
- assert_eq!(out.get("ok").and_then(|v| v.as_bool()), Some(true), "no invalid → ok:true");
1190
- let cr = out.get("collected_results").and_then(|v| v.as_array()).expect("collected_results");
1514
+ assert_eq!(
1515
+ out.get("ok").and_then(|v| v.as_bool()),
1516
+ Some(true),
1517
+ "no invalid → ok:true"
1518
+ );
1519
+ let cr = out
1520
+ .get("collected_results")
1521
+ .and_then(|v| v.as_array())
1522
+ .expect("collected_results");
1191
1523
  assert_eq!(cr.len(), 1, "the seeded t2 result must collect");
1192
- assert_eq!(cr[0].get("scope").and_then(|v| v.as_str()), Some("task"), "t2 ∈ state.tasks → scope:task");
1524
+ assert_eq!(
1525
+ cr[0].get("scope").and_then(|v| v.as_str()),
1526
+ Some("task"),
1527
+ "t2 ∈ state.tasks → scope:task"
1528
+ );
1193
1529
  assert_eq!(cr[0].get("task_id").and_then(|v| v.as_str()), Some("t2"));
1194
1530
  assert_eq!(cr[0].get("agent_id").and_then(|v| v.as_str()), Some("w1"));
1195
1531
  assert!(
1196
- out.get("results").and_then(|r| r.get("collected")).and_then(|v| v.as_i64()).unwrap_or(0) >= 1,
1532
+ out.get("results")
1533
+ .and_then(|r| r.get("collected"))
1534
+ .and_then(|v| v.as_i64())
1535
+ .unwrap_or(0)
1536
+ >= 1,
1197
1537
  "results.collected must be ≥ 1"
1198
1538
  );
1199
1539
  let st = crate::state::persist::load_runtime_state(&ws).unwrap();
1200
- let t2_status = st.get("tasks").and_then(|v| v.as_array())
1201
- .and_then(|ts| ts.iter().find(|t| t.get("id").and_then(|v| v.as_str()) == Some("t2")))
1202
- .and_then(|t| t.get("status")).and_then(|v| v.as_str());
1203
- assert_eq!(t2_status, Some("done"), "success result → task row status 'done' (runtime.py:1066)");
1540
+ let t2_status = st
1541
+ .get("tasks")
1542
+ .and_then(|v| v.as_array())
1543
+ .and_then(|ts| {
1544
+ ts.iter()
1545
+ .find(|t| t.get("id").and_then(|v| v.as_str()) == Some("t2"))
1546
+ })
1547
+ .and_then(|t| t.get("status"))
1548
+ .and_then(|v| v.as_str());
1549
+ assert_eq!(
1550
+ t2_status,
1551
+ Some("done"),
1552
+ "success result → task row status 'done' (runtime.py:1066)"
1553
+ );
1204
1554
  }
1205
1555
 
1206
1556
  // (c-C1) collect OUTPUT shape: collected_results entries are the 8-KEY SUMMARY (NO inlined
@@ -1218,30 +1568,46 @@ fn collect_output_matches_golden_collected_shape() {
1218
1568
  "agents": { "w1": { "provider": "codex" } },
1219
1569
  "tasks": [ { "id": "t2", "assignee": "w1", "title": "t2", "status": "pending" } ]
1220
1570
  }),
1221
- ).unwrap();
1571
+ )
1572
+ .unwrap();
1222
1573
  let store = store_for(&ws);
1223
1574
  seed_result(&store, "res_t2s", "t2", "w1", "success");
1224
1575
 
1225
1576
  let out = collect(&ws, None, false).unwrap();
1226
- let cr = out.get("collected_results").and_then(|v| v.as_array()).expect("collected_results");
1577
+ let cr = out
1578
+ .get("collected_results")
1579
+ .and_then(|v| v.as_array())
1580
+ .expect("collected_results");
1227
1581
  let e = &cr[0];
1228
1582
  // C1: collected_results entry is the 8-key SUMMARY — NO envelope inlined; carries summary+tests.
1229
1583
  assert!(e.get("envelope").is_none(),
1230
1584
  "collected_results entry must NOT inline `envelope` (golden 8-key summary); the full envelope belongs in `collected`. got {e:?}");
1231
- assert!(e.get("summary").is_some() && e.get("tests").is_some(),
1232
- "collected_results summary entry must carry `summary`+`tests` (golden results.py:131)");
1585
+ assert!(
1586
+ e.get("summary").is_some() && e.get("tests").is_some(),
1587
+ "collected_results summary entry must carry `summary`+`tests` (golden results.py:131)"
1588
+ );
1233
1589
  // C1: the full envelopes live in a separate top-level `collected` list.
1234
- let collected = out.get("collected").and_then(|v| v.as_array())
1590
+ let collected = out
1591
+ .get("collected")
1592
+ .and_then(|v| v.as_array())
1235
1593
  .expect("golden collect returns a top-level `collected` list of full envelopes");
1236
1594
  assert!(
1237
- collected.first().and_then(|env| env.get("schema_version")).and_then(|v| v.as_str())
1595
+ collected
1596
+ .first()
1597
+ .and_then(|env| env.get("schema_version"))
1598
+ .and_then(|v| v.as_str())
1238
1599
  == Some("result_envelope_v1"),
1239
1600
  "collected[0] must be the full result_envelope_v1 envelope; got {collected:?}"
1240
1601
  );
1241
1602
 
1242
1603
  // ── STRENGTHENED (option-B byte-parity, leader-adjudicated 0700cff review) ──
1243
1604
  // D3 — task-scope collected_results entry must be EXACTLY the golden 8 keys, in order, NO task_status.
1244
- let keys: Vec<&str> = e.as_object().expect("entry is an object").keys().map(String::as_str).collect();
1605
+ let keys: Vec<&str> = e
1606
+ .as_object()
1607
+ .expect("entry is an object")
1608
+ .keys()
1609
+ .map(String::as_str)
1610
+ .collect();
1245
1611
  assert_eq!(
1246
1612
  keys,
1247
1613
  vec!["result_id", "task_id", "agent_id", "status", "summary", "tests", "created_at", "scope"],
@@ -1249,10 +1615,24 @@ fn collect_output_matches_golden_collected_shape() {
1249
1615
  );
1250
1616
  // D1+D2 — collect RETURN top-level key order must match golden EXACTLY: delivered_messages BEFORE
1251
1617
  // invalid_results, AND a `coordinator` key (mirroring golden _ensure_coordinator_after_collect).
1252
- let top: Vec<&str> = out.as_object().expect("collect result is an object").keys().map(String::as_str).collect();
1618
+ let top: Vec<&str> = out
1619
+ .as_object()
1620
+ .expect("collect result is an object")
1621
+ .keys()
1622
+ .map(String::as_str)
1623
+ .collect();
1253
1624
  assert_eq!(
1254
1625
  top,
1255
- vec!["ok", "collected", "collected_results", "delivered_messages", "invalid_results", "results", "state_file", "coordinator"],
1626
+ vec![
1627
+ "ok",
1628
+ "collected",
1629
+ "collected_results",
1630
+ "delivered_messages",
1631
+ "invalid_results",
1632
+ "results",
1633
+ "state_file",
1634
+ "coordinator"
1635
+ ],
1256
1636
  "collect return top-level key order must match golden return shape; got {top:?}"
1257
1637
  );
1258
1638
  }
@@ -1270,7 +1650,8 @@ fn send_with_unknown_task_id_raises_unknown_task() {
1270
1650
  "agents": { "w1": { "provider": "codex" } },
1271
1651
  "tasks": []
1272
1652
  }),
1273
- ).unwrap();
1653
+ )
1654
+ .unwrap();
1274
1655
  let _ = store_for(&ws);
1275
1656
  let opts = SendOptions {
1276
1657
  task_id: Some(crate::model::ids::TaskId::new("t2-unknown")),
@@ -1345,7 +1726,8 @@ fn send_route_task_id_true_known_task_succeeds() {
1345
1726
  "agents": { "w1": { "provider": "codex" } },
1346
1727
  "tasks": [ { "id": "t-known", "assignee": "w1", "title": "t", "status": "pending" } ]
1347
1728
  }),
1348
- ).unwrap();
1729
+ )
1730
+ .unwrap();
1349
1731
  let _ = store_for(&ws);
1350
1732
  let opts = SendOptions {
1351
1733
  task_id: Some(crate::model::ids::TaskId::new("t-known")),
@@ -1355,7 +1737,10 @@ fn send_route_task_id_true_known_task_succeeds() {
1355
1737
  };
1356
1738
  let out = send_message(&ws, &MessageTarget::Single("w1".to_string()), "go", &opts)
1357
1739
  .expect("route_task_id=true with a KNOWN task must succeed");
1358
- assert!(out.message_id.is_some(), "known-task routing send must create the message; got {out:?}");
1740
+ assert!(
1741
+ out.message_id.is_some(),
1742
+ "known-task routing send must create the message; got {out:?}"
1743
+ );
1359
1744
  }
1360
1745
 
1361
1746
  // ════════════════════════════════════════════════════════════════════════
@@ -1378,7 +1763,16 @@ fn r8_attach_requeue_exhausted_to_notify_failed_golden_attach_event() {
1378
1763
 
1379
1764
  // --- Sub-A: DRIVE w-r8 (team-a) to delivery_exhausted via notify_result_watchers (attempts>=MAX) ---
1380
1765
  let rid = seed_result(&store, "res_r8", "t1", "alice", "success");
1381
- seed_watcher(&store, "w-r8", "team-a", "t1", "alice", "pending", Some(&rid), None);
1766
+ seed_watcher(
1767
+ &store,
1768
+ "w-r8",
1769
+ "team-a",
1770
+ "t1",
1771
+ "alice",
1772
+ "pending",
1773
+ Some(&rid),
1774
+ None,
1775
+ );
1382
1776
  // attempts are EVENT-counted (result_watcher.notify_failed/retry_notified) — seed MAX prior failures.
1383
1777
  for n in 0..u64::from(RESULT_DELIVERY_MAX_ATTEMPTS) {
1384
1778
  log.write(
@@ -1386,7 +1780,8 @@ fn r8_attach_requeue_exhausted_to_notify_failed_golden_attach_event() {
1386
1780
  json(serde_json::json!({"watcher_id": "w-r8", "result_id": rid.as_str(), "status": "notify_failed", "error": "x", "n": n})),
1387
1781
  ).unwrap();
1388
1782
  }
1389
- let result_env = json(serde_json::json!({"result_id": rid.as_str(), "task_id": "t1", "agent_id": "alice"}));
1783
+ let result_env =
1784
+ json(serde_json::json!({"result_id": rid.as_str(), "task_id": "t1", "agent_id": "alice"}));
1390
1785
  let watcher_view = json(serde_json::json!({
1391
1786
  "watcher_id": "w-r8", "task_id": "t1", "agent_id": "alice",
1392
1787
  "created_at": "2026-01-01T00:00:00Z", "owner_team_id": "team-a",
@@ -1399,9 +1794,36 @@ fn r8_attach_requeue_exhausted_to_notify_failed_golden_attach_event() {
1399
1794
  proves the attach-requeue input is real, not 空过");
1400
1795
 
1401
1796
  // selection-lock fixtures: cross-team exhausted + notified exhausted (Gap-32) + pending.
1402
- let team_b = seed_watcher(&store, "w-teamb", "team-b", "t2", "bob", "delivery_exhausted", Some("res_b"), None);
1403
- let notif = seed_watcher(&store, "w-notified", "team-a", "t3", "carol", "delivery_exhausted", Some("res_c"), Some("msg_done"));
1404
- seed_watcher(&store, "w-pending", "team-a", "t4", "dave", "pending", Some("res_d"), None);
1797
+ let team_b = seed_watcher(
1798
+ &store,
1799
+ "w-teamb",
1800
+ "team-b",
1801
+ "t2",
1802
+ "bob",
1803
+ "delivery_exhausted",
1804
+ Some("res_b"),
1805
+ None,
1806
+ );
1807
+ let notif = seed_watcher(
1808
+ &store,
1809
+ "w-notified",
1810
+ "team-a",
1811
+ "t3",
1812
+ "carol",
1813
+ "delivery_exhausted",
1814
+ Some("res_c"),
1815
+ Some("msg_done"),
1816
+ );
1817
+ seed_watcher(
1818
+ &store,
1819
+ "w-pending",
1820
+ "team-a",
1821
+ "t4",
1822
+ "dave",
1823
+ "pending",
1824
+ Some("res_d"),
1825
+ None,
1826
+ );
1405
1827
 
1406
1828
  // --- Sub-B: attach requeue (golden contract) ---
1407
1829
  let requeued = requeue_delivery_exhausted_watchers(&ws, &store, &log, &team, &pane).unwrap();
@@ -1416,24 +1838,50 @@ fn r8_attach_requeue_exhausted_to_notify_failed_golden_attach_event() {
1416
1838
  "D1 ✦: team-scoped selection — a team-b exhausted watcher must NOT be requeued by a team-a attach (anti cross-team pollution / CP-1)");
1417
1839
  // Gap-32: a notified watcher is never requeued; its notified_message_id survives.
1418
1840
  let (st_n, nid) = watcher_state(&store, &notif);
1419
- assert_eq!(st_n, "delivery_exhausted", "Gap-32: notified watcher not requeued");
1420
- assert_eq!(nid.as_deref(), Some("msg_done"), "Gap-32: notified_message_id preserved");
1841
+ assert_eq!(
1842
+ st_n, "delivery_exhausted",
1843
+ "Gap-32: notified watcher not requeued"
1844
+ );
1845
+ assert_eq!(
1846
+ nid.as_deref(),
1847
+ Some("msg_done"),
1848
+ "Gap-32: notified_message_id preserved"
1849
+ );
1421
1850
  // only the team-a unnotified exhausted watcher requeues.
1422
1851
  let ids: Vec<&str> = requeued.iter().map(|n| n.watcher_id.as_str()).collect();
1423
- assert_eq!(ids, vec!["w-r8"], "only team-a unnotified delivery_exhausted watcher requeues");
1852
+ assert_eq!(
1853
+ ids,
1854
+ vec!["w-r8"],
1855
+ "only team-a unnotified delivery_exhausted watcher requeues"
1856
+ );
1424
1857
 
1425
1858
  // D3: result_watcher.requeued payload == golden ATTACH form {watcher_id, trigger, new_pane_id}.
1426
1859
  let events = log.tail(0).unwrap();
1427
- let ev = events.iter().rev()
1860
+ let ev = events
1861
+ .iter()
1862
+ .rev()
1428
1863
  .find(|e| e.get("event").and_then(|v| v.as_str()) == Some("result_watcher.requeued"))
1429
1864
  .expect("result_watcher.requeued event");
1430
- let keys: std::collections::BTreeSet<&str> = ev.as_object().unwrap().keys()
1431
- .map(String::as_str).filter(|k| *k != "ts" && *k != "event").collect();
1432
- let expected: std::collections::BTreeSet<&str> = ["watcher_id", "trigger", "new_pane_id"].into_iter().collect();
1865
+ let keys: std::collections::BTreeSet<&str> = ev
1866
+ .as_object()
1867
+ .unwrap()
1868
+ .keys()
1869
+ .map(String::as_str)
1870
+ .filter(|k| *k != "ts" && *k != "event")
1871
+ .collect();
1872
+ let expected: std::collections::BTreeSet<&str> = ["watcher_id", "trigger", "new_pane_id"]
1873
+ .into_iter()
1874
+ .collect();
1433
1875
  assert_eq!(keys, expected,
1434
1876
  "D3: result_watcher.requeued must be golden ATTACH form {{watcher_id, trigger, new_pane_id}} (leader/__init__.py:46-50), not claim-style; got {keys:?}");
1435
- assert_eq!(ev.get("trigger").and_then(|v| v.as_str()), Some("attach_leader"));
1436
- assert_eq!(ev.get("new_pane_id").and_then(|v| v.as_str()), Some("%leader-new"));
1877
+ assert_eq!(
1878
+ ev.get("trigger").and_then(|v| v.as_str()),
1879
+ Some("attach_leader")
1880
+ );
1881
+ assert_eq!(
1882
+ ev.get("new_pane_id").and_then(|v| v.as_str()),
1883
+ Some("%leader-new")
1884
+ );
1437
1885
  }
1438
1886
 
1439
1887
  // E15 (F4.4 双投修)·源码守卫:report_result 的 direct inject 必须被 `if !outcome.ok` 守为
@@ -1446,8 +1894,14 @@ fn e15_direct_inject_is_gated_by_deliver_failure_not_unconditional() {
1446
1894
  let inject_call = "match inject_leader_notification_direct(";
1447
1895
  let gate_pos = src.find(gate);
1448
1896
  let inject_pos = src.find(inject_call);
1449
- assert!(gate_pos.is_some(), "E15: direct inject must be gated by `if !outcome.ok` (deliver-fail fallback)");
1450
- assert!(inject_pos.is_some(), "inject_leader_notification_direct call site must exist (do NOT delete it; #230 fallback)");
1897
+ assert!(
1898
+ gate_pos.is_some(),
1899
+ "E15: direct inject must be gated by `if !outcome.ok` (deliver-fail fallback)"
1900
+ );
1901
+ assert!(
1902
+ inject_pos.is_some(),
1903
+ "inject_leader_notification_direct call site must exist (do NOT delete it; #230 fallback)"
1904
+ );
1451
1905
  assert!(
1452
1906
  gate_pos.unwrap() < inject_pos.unwrap(),
1453
1907
  "E15: the `if !outcome.ok` gate must precede the direct-inject call (deliver-success must skip inject → leader gets exactly one copy)"