@team-agent/installer 0.3.10 → 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.
@@ -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,11 @@ 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
+ );
1069
1239
  }
1070
1240
 
1071
1241
  struct UnverifiedInjectTransport;
@@ -1073,17 +1243,38 @@ impl Transport for UnverifiedInjectTransport {
1073
1243
  fn kind(&self) -> BackendKind {
1074
1244
  BackendKind::Tmux
1075
1245
  }
1076
- fn spawn_first(&self, _s: &SessionName, _w: &WindowName, _a: &[String], _c: &Path, _e: &BTreeMap<String, String>) -> Result<SpawnResult, TransportError> {
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> {
1077
1254
  unimplemented!("not reached in delivery")
1078
1255
  }
1079
- fn spawn_into(&self, _s: &SessionName, _w: &WindowName, _a: &[String], _c: &Path, _e: &BTreeMap<String, String>) -> Result<SpawnResult, TransportError> {
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> {
1080
1264
  unimplemented!("not reached in delivery")
1081
1265
  }
1082
- fn inject(&self, _t: &Target, _p: &InjectPayload, _s: Key, _b: bool) -> Result<InjectReport, TransportError> {
1266
+ fn inject(
1267
+ &self,
1268
+ _t: &Target,
1269
+ _p: &InjectPayload,
1270
+ _s: Key,
1271
+ _b: bool,
1272
+ ) -> Result<InjectReport, TransportError> {
1083
1273
  Ok(InjectReport {
1084
1274
  stage_reached: crate::transport::InjectStage::Submit,
1085
1275
  inject_verification: crate::transport::InjectVerification::CaptureContainsToken,
1086
- submit_verification: crate::transport::SubmitVerification::PastedContentPromptStillPresentAfterSubmit,
1276
+ submit_verification:
1277
+ crate::transport::SubmitVerification::PastedContentPromptStillPresentAfterSubmit,
1087
1278
  turn_verification: crate::transport::TurnVerification::NotYetObserved,
1088
1279
  attempts: u32::from(SEND_RETRY_MAX_ATTEMPTS),
1089
1280
  })
@@ -1092,7 +1283,10 @@ impl Transport for UnverifiedInjectTransport {
1092
1283
  Ok(())
1093
1284
  }
1094
1285
  fn capture(&self, _t: &Target, range: CaptureRange) -> Result<CapturedText, TransportError> {
1095
- Ok(CapturedText { text: String::new(), range })
1286
+ Ok(CapturedText {
1287
+ text: String::new(),
1288
+ range,
1289
+ })
1096
1290
  }
1097
1291
  fn query(&self, _t: &Target, _f: PaneField) -> Result<Option<String>, TransportError> {
1098
1292
  Ok(None)
@@ -1109,7 +1303,12 @@ impl Transport for UnverifiedInjectTransport {
1109
1303
  fn list_windows(&self, _s: &SessionName) -> Result<Vec<WindowName>, TransportError> {
1110
1304
  Ok(Vec::new())
1111
1305
  }
1112
- fn set_session_env(&self, _s: &SessionName, _k: &str, _v: &str) -> Result<SetEnvOutcome, TransportError> {
1306
+ fn set_session_env(
1307
+ &self,
1308
+ _s: &SessionName,
1309
+ _k: &str,
1310
+ _v: &str,
1311
+ ) -> Result<SetEnvOutcome, TransportError> {
1113
1312
  Ok(SetEnvOutcome::Applied)
1114
1313
  }
1115
1314
  fn kill_session(&self, _s: &SessionName) -> Result<(), TransportError> {
@@ -1138,22 +1337,30 @@ fn deliver_pending_exhausted_unverified_send_emits_failed_event() {
1138
1337
  .create_message(None, "leader", "w1", "ping", None, false, None)
1139
1338
  .unwrap();
1140
1339
 
1141
- let out = deliver_pending_message(&ws, &store, &UnverifiedInjectTransport, &message_id, &log, &state)
1142
- .unwrap();
1340
+ let out = deliver_pending_message(
1341
+ &ws,
1342
+ &store,
1343
+ &UnverifiedInjectTransport,
1344
+ &message_id,
1345
+ &log,
1346
+ &state,
1347
+ )
1348
+ .unwrap();
1143
1349
 
1144
1350
  assert!(!out.ok);
1145
1351
  assert_eq!(out.message_status.0, "failed");
1146
1352
  let events = log.tail(0).unwrap();
1147
1353
  assert!(
1148
- events
1149
- .iter()
1150
- .any(|event| event.get("event").and_then(serde_json::Value::as_str) == Some("send.failed")),
1354
+ events.iter().any(
1355
+ |event| event.get("event").and_then(serde_json::Value::as_str) == Some("send.failed")
1356
+ ),
1151
1357
  "exhausted unverified send must emit send.failed; got {events:?}"
1152
1358
  );
1153
1359
  assert!(
1154
- events
1155
- .iter()
1156
- .any(|event| event.get("event").and_then(serde_json::Value::as_str) == Some("send.failed_notification")),
1360
+ events.iter().any(
1361
+ |event| event.get("event").and_then(serde_json::Value::as_str)
1362
+ == Some("send.failed_notification")
1363
+ ),
1157
1364
  "exhausted unverified send must queue a leader-visible notification; got {events:?}"
1158
1365
  );
1159
1366
  }
@@ -1179,14 +1386,28 @@ fn retry_result_deliveries_retries_notify_failed_watcher() {
1179
1386
 
1180
1387
  let rid = seed_result(&store, "res_r1", "t1", "alice", "success");
1181
1388
  let w = seed_watcher(
1182
- &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,
1183
1397
  );
1184
1398
 
1185
1399
  let notices = retry_result_deliveries(&ws, &log).unwrap();
1186
1400
 
1187
- 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
+ );
1188
1406
  let notice = &notices[0];
1189
- 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
+ );
1190
1411
  assert_eq!(
1191
1412
  notice.result_id.as_deref(),
1192
1413
  Some(rid.as_str()),
@@ -1214,18 +1435,31 @@ fn collect_results_and_notify_watchers_returns_concrete_ok_shape() {
1214
1435
  let log = EventLog::new(&ws);
1215
1436
 
1216
1437
  seed_watcher(
1217
- &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,
1218
1446
  );
1219
1447
 
1220
1448
  let out = collect_results_and_notify_watchers(&ws, &log).unwrap();
1221
- 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
+ );
1222
1454
  assert_eq!(
1223
1455
  out.get("collected").and_then(|v| v.as_i64()),
1224
1456
  Some(0),
1225
1457
  "no uncollected results → collected==0"
1226
1458
  );
1227
1459
  assert_eq!(
1228
- 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()),
1229
1463
  Some(0),
1230
1464
  "orphan watcher (missing result) is skipped → notified empty"
1231
1465
  );
@@ -1271,26 +1505,52 @@ fn collect_task_scoped_result_collects_and_marks_task_done() {
1271
1505
  "agents": { "w1": { "provider": "codex" } },
1272
1506
  "tasks": [ { "id": "t2", "assignee": "w1", "title": "t2", "status": "pending" } ]
1273
1507
  }),
1274
- ).unwrap();
1508
+ )
1509
+ .unwrap();
1275
1510
  let store = store_for(&ws);
1276
1511
  seed_result(&store, "res_t2", "t2", "w1", "success");
1277
1512
 
1278
1513
  let out = collect(&ws, None, false).unwrap();
1279
- assert_eq!(out.get("ok").and_then(|v| v.as_bool()), Some(true), "no invalid → ok:true");
1280
- 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");
1281
1523
  assert_eq!(cr.len(), 1, "the seeded t2 result must collect");
1282
- 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
+ );
1283
1529
  assert_eq!(cr[0].get("task_id").and_then(|v| v.as_str()), Some("t2"));
1284
1530
  assert_eq!(cr[0].get("agent_id").and_then(|v| v.as_str()), Some("w1"));
1285
1531
  assert!(
1286
- 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,
1287
1537
  "results.collected must be ≥ 1"
1288
1538
  );
1289
1539
  let st = crate::state::persist::load_runtime_state(&ws).unwrap();
1290
- let t2_status = st.get("tasks").and_then(|v| v.as_array())
1291
- .and_then(|ts| ts.iter().find(|t| t.get("id").and_then(|v| v.as_str()) == Some("t2")))
1292
- .and_then(|t| t.get("status")).and_then(|v| v.as_str());
1293
- 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
+ );
1294
1554
  }
1295
1555
 
1296
1556
  // (c-C1) collect OUTPUT shape: collected_results entries are the 8-KEY SUMMARY (NO inlined
@@ -1308,30 +1568,46 @@ fn collect_output_matches_golden_collected_shape() {
1308
1568
  "agents": { "w1": { "provider": "codex" } },
1309
1569
  "tasks": [ { "id": "t2", "assignee": "w1", "title": "t2", "status": "pending" } ]
1310
1570
  }),
1311
- ).unwrap();
1571
+ )
1572
+ .unwrap();
1312
1573
  let store = store_for(&ws);
1313
1574
  seed_result(&store, "res_t2s", "t2", "w1", "success");
1314
1575
 
1315
1576
  let out = collect(&ws, None, false).unwrap();
1316
- 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");
1317
1581
  let e = &cr[0];
1318
1582
  // C1: collected_results entry is the 8-key SUMMARY — NO envelope inlined; carries summary+tests.
1319
1583
  assert!(e.get("envelope").is_none(),
1320
1584
  "collected_results entry must NOT inline `envelope` (golden 8-key summary); the full envelope belongs in `collected`. got {e:?}");
1321
- assert!(e.get("summary").is_some() && e.get("tests").is_some(),
1322
- "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
+ );
1323
1589
  // C1: the full envelopes live in a separate top-level `collected` list.
1324
- 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())
1325
1593
  .expect("golden collect returns a top-level `collected` list of full envelopes");
1326
1594
  assert!(
1327
- 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())
1328
1599
  == Some("result_envelope_v1"),
1329
1600
  "collected[0] must be the full result_envelope_v1 envelope; got {collected:?}"
1330
1601
  );
1331
1602
 
1332
1603
  // ── STRENGTHENED (option-B byte-parity, leader-adjudicated 0700cff review) ──
1333
1604
  // D3 — task-scope collected_results entry must be EXACTLY the golden 8 keys, in order, NO task_status.
1334
- 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();
1335
1611
  assert_eq!(
1336
1612
  keys,
1337
1613
  vec!["result_id", "task_id", "agent_id", "status", "summary", "tests", "created_at", "scope"],
@@ -1339,10 +1615,24 @@ fn collect_output_matches_golden_collected_shape() {
1339
1615
  );
1340
1616
  // D1+D2 — collect RETURN top-level key order must match golden EXACTLY: delivered_messages BEFORE
1341
1617
  // invalid_results, AND a `coordinator` key (mirroring golden _ensure_coordinator_after_collect).
1342
- 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();
1343
1624
  assert_eq!(
1344
1625
  top,
1345
- 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
+ ],
1346
1636
  "collect return top-level key order must match golden return shape; got {top:?}"
1347
1637
  );
1348
1638
  }
@@ -1360,7 +1650,8 @@ fn send_with_unknown_task_id_raises_unknown_task() {
1360
1650
  "agents": { "w1": { "provider": "codex" } },
1361
1651
  "tasks": []
1362
1652
  }),
1363
- ).unwrap();
1653
+ )
1654
+ .unwrap();
1364
1655
  let _ = store_for(&ws);
1365
1656
  let opts = SendOptions {
1366
1657
  task_id: Some(crate::model::ids::TaskId::new("t2-unknown")),
@@ -1435,7 +1726,8 @@ fn send_route_task_id_true_known_task_succeeds() {
1435
1726
  "agents": { "w1": { "provider": "codex" } },
1436
1727
  "tasks": [ { "id": "t-known", "assignee": "w1", "title": "t", "status": "pending" } ]
1437
1728
  }),
1438
- ).unwrap();
1729
+ )
1730
+ .unwrap();
1439
1731
  let _ = store_for(&ws);
1440
1732
  let opts = SendOptions {
1441
1733
  task_id: Some(crate::model::ids::TaskId::new("t-known")),
@@ -1445,7 +1737,10 @@ fn send_route_task_id_true_known_task_succeeds() {
1445
1737
  };
1446
1738
  let out = send_message(&ws, &MessageTarget::Single("w1".to_string()), "go", &opts)
1447
1739
  .expect("route_task_id=true with a KNOWN task must succeed");
1448
- 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
+ );
1449
1744
  }
1450
1745
 
1451
1746
  // ════════════════════════════════════════════════════════════════════════
@@ -1468,7 +1763,16 @@ fn r8_attach_requeue_exhausted_to_notify_failed_golden_attach_event() {
1468
1763
 
1469
1764
  // --- Sub-A: DRIVE w-r8 (team-a) to delivery_exhausted via notify_result_watchers (attempts>=MAX) ---
1470
1765
  let rid = seed_result(&store, "res_r8", "t1", "alice", "success");
1471
- 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
+ );
1472
1776
  // attempts are EVENT-counted (result_watcher.notify_failed/retry_notified) — seed MAX prior failures.
1473
1777
  for n in 0..u64::from(RESULT_DELIVERY_MAX_ATTEMPTS) {
1474
1778
  log.write(
@@ -1476,7 +1780,8 @@ fn r8_attach_requeue_exhausted_to_notify_failed_golden_attach_event() {
1476
1780
  json(serde_json::json!({"watcher_id": "w-r8", "result_id": rid.as_str(), "status": "notify_failed", "error": "x", "n": n})),
1477
1781
  ).unwrap();
1478
1782
  }
1479
- 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"}));
1480
1785
  let watcher_view = json(serde_json::json!({
1481
1786
  "watcher_id": "w-r8", "task_id": "t1", "agent_id": "alice",
1482
1787
  "created_at": "2026-01-01T00:00:00Z", "owner_team_id": "team-a",
@@ -1489,9 +1794,36 @@ fn r8_attach_requeue_exhausted_to_notify_failed_golden_attach_event() {
1489
1794
  proves the attach-requeue input is real, not 空过");
1490
1795
 
1491
1796
  // selection-lock fixtures: cross-team exhausted + notified exhausted (Gap-32) + pending.
1492
- let team_b = seed_watcher(&store, "w-teamb", "team-b", "t2", "bob", "delivery_exhausted", Some("res_b"), None);
1493
- let notif = seed_watcher(&store, "w-notified", "team-a", "t3", "carol", "delivery_exhausted", Some("res_c"), Some("msg_done"));
1494
- 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
+ );
1495
1827
 
1496
1828
  // --- Sub-B: attach requeue (golden contract) ---
1497
1829
  let requeued = requeue_delivery_exhausted_watchers(&ws, &store, &log, &team, &pane).unwrap();
@@ -1506,24 +1838,50 @@ fn r8_attach_requeue_exhausted_to_notify_failed_golden_attach_event() {
1506
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)");
1507
1839
  // Gap-32: a notified watcher is never requeued; its notified_message_id survives.
1508
1840
  let (st_n, nid) = watcher_state(&store, &notif);
1509
- assert_eq!(st_n, "delivery_exhausted", "Gap-32: notified watcher not requeued");
1510
- 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
+ );
1511
1850
  // only the team-a unnotified exhausted watcher requeues.
1512
1851
  let ids: Vec<&str> = requeued.iter().map(|n| n.watcher_id.as_str()).collect();
1513
- 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
+ );
1514
1857
 
1515
1858
  // D3: result_watcher.requeued payload == golden ATTACH form {watcher_id, trigger, new_pane_id}.
1516
1859
  let events = log.tail(0).unwrap();
1517
- let ev = events.iter().rev()
1860
+ let ev = events
1861
+ .iter()
1862
+ .rev()
1518
1863
  .find(|e| e.get("event").and_then(|v| v.as_str()) == Some("result_watcher.requeued"))
1519
1864
  .expect("result_watcher.requeued event");
1520
- let keys: std::collections::BTreeSet<&str> = ev.as_object().unwrap().keys()
1521
- .map(String::as_str).filter(|k| *k != "ts" && *k != "event").collect();
1522
- 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();
1523
1875
  assert_eq!(keys, expected,
1524
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:?}");
1525
- assert_eq!(ev.get("trigger").and_then(|v| v.as_str()), Some("attach_leader"));
1526
- 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
+ );
1527
1885
  }
1528
1886
 
1529
1887
  // E15 (F4.4 双投修)·源码守卫:report_result 的 direct inject 必须被 `if !outcome.ok` 守为
@@ -1536,8 +1894,14 @@ fn e15_direct_inject_is_gated_by_deliver_failure_not_unconditional() {
1536
1894
  let inject_call = "match inject_leader_notification_direct(";
1537
1895
  let gate_pos = src.find(gate);
1538
1896
  let inject_pos = src.find(inject_call);
1539
- assert!(gate_pos.is_some(), "E15: direct inject must be gated by `if !outcome.ok` (deliver-fail fallback)");
1540
- 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
+ );
1541
1905
  assert!(
1542
1906
  gate_pos.unwrap() < inject_pos.unwrap(),
1543
1907
  "E15: the `if !outcome.ok` gate must precede the direct-inject call (deliver-success must skip inject → leader gets exactly one copy)"