@team-agent/installer 0.5.42 → 0.5.44

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 (156) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +129 -54
  4. package/crates/team-agent/src/cli/diagnose.rs +1 -2
  5. package/crates/team-agent/src/cli/emit.rs +1 -7
  6. package/crates/team-agent/src/cli/helpers.rs +3 -1
  7. package/crates/team-agent/src/cli/leader.rs +2 -1
  8. package/crates/team-agent/src/cli/mod.rs +126 -16
  9. package/crates/team-agent/src/cli/named_address.rs +14 -5
  10. package/crates/team-agent/src/cli/profile.rs +19 -7
  11. package/crates/team-agent/src/cli/send.rs +9 -3
  12. package/crates/team-agent/src/cli/status.rs +8 -30
  13. package/crates/team-agent/src/cli/status_port.rs +1341 -1272
  14. package/crates/team-agent/src/cli/tests/base.rs +738 -660
  15. package/crates/team-agent/src/cli/tests/compile.rs +45 -18
  16. package/crates/team-agent/src/cli/tests/divergence.rs +462 -444
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +365 -282
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +356 -329
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +672 -564
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +284 -224
  21. package/crates/team-agent/src/cli/tests/mod.rs +17 -7
  22. package/crates/team-agent/src/cli/tests/named_address.rs +8 -2
  23. package/crates/team-agent/src/cli/tests/peer_allow.rs +10 -2
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +314 -273
  25. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +26 -15
  26. package/crates/team-agent/src/cli/tests/status_send.rs +707 -629
  27. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +20 -4
  28. package/crates/team-agent/src/cli/tests/verb_profile.rs +46 -17
  29. package/crates/team-agent/src/cli/tests/verb_validate.rs +15 -3
  30. package/crates/team-agent/src/codex_app_server.rs +2 -5
  31. package/crates/team-agent/src/compiler/tests.rs +139 -33
  32. package/crates/team-agent/src/compiler.rs +55 -22
  33. package/crates/team-agent/src/conpty/backend.rs +23 -33
  34. package/crates/team-agent/src/coordinator/backoff.rs +2 -7
  35. package/crates/team-agent/src/coordinator/conpty_shim.rs +55 -67
  36. package/crates/team-agent/src/coordinator/health.rs +46 -31
  37. package/crates/team-agent/src/coordinator/mod.rs +3 -3
  38. package/crates/team-agent/src/coordinator/orphan.rs +22 -10
  39. package/crates/team-agent/src/coordinator/steps/abnormal.rs +51 -56
  40. package/crates/team-agent/src/coordinator/tests/abnormal.rs +55 -19
  41. package/crates/team-agent/src/coordinator/tests/basics.rs +179 -41
  42. package/crates/team-agent/src/coordinator/tests/daemon.rs +53 -13
  43. package/crates/team-agent/src/coordinator/tests/health_sync.rs +78 -19
  44. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +61 -11
  45. package/crates/team-agent/src/coordinator/tests/mod.rs +33 -39
  46. package/crates/team-agent/src/coordinator/tests/spine.rs +52 -12
  47. package/crates/team-agent/src/coordinator/tests/takeover.rs +73 -15
  48. package/crates/team-agent/src/coordinator/tests/tick_core.rs +50 -15
  49. package/crates/team-agent/src/coordinator/tests/watch.rs +74 -20
  50. package/crates/team-agent/src/db/message_store.rs +138 -30
  51. package/crates/team-agent/src/db/migration.rs +249 -61
  52. package/crates/team-agent/src/db/schema.rs +303 -82
  53. package/crates/team-agent/src/diagnose/comms.rs +9 -2
  54. package/crates/team-agent/src/diagnose/mod.rs +1 -3
  55. package/crates/team-agent/src/diagnose/orphans.rs +79 -61
  56. package/crates/team-agent/src/event_log.rs +70 -16
  57. package/crates/team-agent/src/layout/manager.rs +15 -4
  58. package/crates/team-agent/src/layout/mod.rs +4 -4
  59. package/crates/team-agent/src/layout/overlay.rs +10 -3
  60. package/crates/team-agent/src/layout/placement.rs +5 -1
  61. package/crates/team-agent/src/layout/recovery.rs +4 -2
  62. package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
  63. package/crates/team-agent/src/layout/sessions.rs +17 -9
  64. package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
  65. package/crates/team-agent/src/layout/worker_env.rs +87 -19
  66. package/crates/team-agent/src/leader/helpers.rs +7 -1
  67. package/crates/team-agent/src/leader/lease.rs +199 -89
  68. package/crates/team-agent/src/leader/owner_bind.rs +55 -22
  69. package/crates/team-agent/src/leader/provider_attribution.rs +25 -6
  70. package/crates/team-agent/src/leader/rediscover/tests.rs +88 -24
  71. package/crates/team-agent/src/leader/rediscover.rs +74 -25
  72. package/crates/team-agent/src/leader/registry.rs +1 -1
  73. package/crates/team-agent/src/leader/start.rs +75 -54
  74. package/crates/team-agent/src/leader/takeover.rs +46 -11
  75. package/crates/team-agent/src/leader/tests/basics.rs +320 -167
  76. package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
  77. package/crates/team-agent/src/leader/tests/identity.rs +428 -356
  78. package/crates/team-agent/src/leader/tests/idle.rs +285 -254
  79. package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
  80. package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
  81. package/crates/team-agent/src/leader/tests/mod.rs +115 -99
  82. package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
  83. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
  84. package/crates/team-agent/src/lib.rs +4 -4
  85. package/crates/team-agent/src/lifecycle/display.rs +7 -3
  86. package/crates/team-agent/src/lifecycle/launch.rs +55 -15
  87. package/crates/team-agent/src/lifecycle/mod.rs +9 -1
  88. package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
  89. package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
  90. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
  91. package/crates/team-agent/src/lifecycle/restart/common.rs +6 -2
  92. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
  93. package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
  94. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
  95. package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
  96. package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
  97. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
  98. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
  99. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
  100. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
  101. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
  102. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +3 -0
  103. package/crates/team-agent/src/lifecycle/tests.rs +2 -2
  104. package/crates/team-agent/src/main.rs +4 -4
  105. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
  106. package/crates/team-agent/src/mcp_server/mod.rs +11 -2
  107. package/crates/team-agent/src/mcp_server/types.rs +14 -3
  108. package/crates/team-agent/src/mcp_server/wire.rs +230 -68
  109. package/crates/team-agent/src/messaging/delivery.rs +20 -18
  110. package/crates/team-agent/src/messaging/helpers.rs +25 -4
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
  112. package/crates/team-agent/src/messaging/mod.rs +2 -3
  113. package/crates/team-agent/src/messaging/selftest.rs +46 -26
  114. package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
  115. package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
  116. package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
  117. package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
  118. package/crates/team-agent/src/messaging/trust.rs +25 -2
  119. package/crates/team-agent/src/messaging/watchers.rs +37 -9
  120. package/crates/team-agent/src/model/enums.rs +65 -16
  121. package/crates/team-agent/src/model/ids.rs +12 -3
  122. package/crates/team-agent/src/model/paths.rs +28 -7
  123. package/crates/team-agent/src/model/permissions.rs +176 -33
  124. package/crates/team-agent/src/model/routing.rs +66 -20
  125. package/crates/team-agent/src/model/spec.rs +365 -69
  126. package/crates/team-agent/src/model/task_graph.rs +36 -9
  127. package/crates/team-agent/src/model/yaml/tests.rs +24 -6
  128. package/crates/team-agent/src/model/yaml.rs +7 -6
  129. package/crates/team-agent/src/packaging/install.rs +23 -9
  130. package/crates/team-agent/src/packaging/migrate.rs +5 -7
  131. package/crates/team-agent/src/packaging/mod.rs +9 -1
  132. package/crates/team-agent/src/packaging/repair.rs +13 -6
  133. package/crates/team-agent/src/packaging/tests.rs +63 -16
  134. package/crates/team-agent/src/packaging/types.rs +22 -7
  135. package/crates/team-agent/src/platform/argv.rs +4 -1
  136. package/crates/team-agent/src/platform/file_lock.rs +22 -8
  137. package/crates/team-agent/src/platform/process.rs +54 -24
  138. package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
  139. package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
  140. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
  141. package/crates/team-agent/src/provider/classify.rs +127 -42
  142. package/crates/team-agent/src/provider/faults.rs +17 -5
  143. package/crates/team-agent/src/provider/helpers.rs +6 -5
  144. package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
  145. package/crates/team-agent/src/state/persist.rs +28 -0
  146. package/crates/team-agent/src/state/repository.rs +8 -3
  147. package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
  148. package/crates/team-agent/src/tmux_backend.rs +80 -44
  149. package/crates/team-agent/src/topology.rs +40 -20
  150. package/crates/team-agent/src/transport/test_support.rs +20 -23
  151. package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
  152. package/crates/team-agent/src/transport/tests/mod.rs +178 -187
  153. package/crates/team-agent/src/transport/tests/wire.rs +561 -525
  154. package/crates/team-agent/src/transport.rs +12 -17
  155. package/crates/team-agent/src/transport_factory.rs +29 -14
  156. package/package.json +4 -4
@@ -1,670 +1,710 @@
1
1
  use super::*;
2
2
 
3
- // =========================================================================
4
- // status_compact_flag (commands.py:99): compact = !detail — CLI 独占不变量
5
- // gate: 'detail=false => compact=true mapping, the one byte-level invariant CLI owns'.
6
- // =========================================================================
7
-
8
- #[test]
9
- fn status_compact_flag_default_is_compact() {
10
- // golden: cmd_status without --detail -> runtime.status(compact=not(False)) == compact=True.
11
- assert!(status_compact_flag(false), "detail=false MUST map to compact=true (commands.py:99)");
12
- }
3
+ // =========================================================================
4
+ // status_compact_flag (commands.py:99): compact = !detail — CLI 独占不变量
5
+ // gate: 'detail=false => compact=true mapping, the one byte-level invariant CLI owns'.
6
+ // =========================================================================
13
7
 
14
- #[test]
15
- fn status_compact_flag_detail_is_full() {
16
- // golden: cmd_status --detail -> runtime.status(compact=not(True)) == compact=False.
17
- assert!(!status_compact_flag(true), "detail=true MUST map to compact=false (full projection)");
18
- }
8
+ #[test]
9
+ fn status_compact_flag_default_is_compact() {
10
+ // golden: cmd_status without --detail -> runtime.status(compact=not(False)) == compact=True.
11
+ assert!(
12
+ status_compact_flag(false),
13
+ "detail=false MUST map to compact=true (commands.py:99)"
14
+ );
15
+ }
19
16
 
20
- // =========================================================================
21
- // status_port::status — REAL caller against SEEDED fixture (gate: 'zero callers').
22
- // Asserts the --json projection shape that the compact-vs-detail wiring selects.
23
- // RED: status_port::status is unimplemented!() so the call panics until ported.
24
- // =========================================================================
25
-
26
- // =========================================================================
27
- // RM-039-STAT-001 regression guard (real-machine evidence 2026-06-22).
28
- //
29
- // Architect verdict (bugs-stat001-sess001-architecture-analysis.md §root-cause):
30
- // the coordinator-tick activity classifier writes
31
- // `activity {status, confidence, rationale}` to the top-level
32
- // `agents.<id>` slot of state.json (T1 invariant 60/61); the compact
33
- // `status --json` projection MUST preserve it, last_output_at, and
34
- // the enrich_agents-injected `interacted` marker. Lifecycle `status`
35
- // and turn `activity.status` are separate fields per T1; the
36
- // projection MUST NOT collapse them.
37
- // =========================================================================
38
- #[test]
39
- fn rm039_stat001_compact_status_preserves_activity_and_last_output() {
40
- let ws = seed_status_workspace();
41
- let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
42
- if let Some(agents) = state
43
- .pointer_mut("/agents")
17
+ #[test]
18
+ fn status_compact_flag_detail_is_full() {
19
+ // golden: cmd_status --detail -> runtime.status(compact=not(True)) == compact=False.
20
+ assert!(
21
+ !status_compact_flag(true),
22
+ "detail=true MUST map to compact=false (full projection)"
23
+ );
24
+ }
25
+
26
+ // =========================================================================
27
+ // status_port::status REAL caller against SEEDED fixture (gate: 'zero callers').
28
+ // Asserts the --json projection shape that the compact-vs-detail wiring selects.
29
+ // RED: status_port::status is unimplemented!() so the call panics until ported.
30
+ // =========================================================================
31
+
32
+ // =========================================================================
33
+ // RM-039-STAT-001 regression guard (real-machine evidence 2026-06-22).
34
+ //
35
+ // Architect verdict (bugs-stat001-sess001-architecture-analysis.md §root-cause):
36
+ // the coordinator-tick activity classifier writes
37
+ // `activity {status, confidence, rationale}` to the top-level
38
+ // `agents.<id>` slot of state.json (T1 invariant 60/61); the compact
39
+ // `status --json` projection MUST preserve it, last_output_at, and
40
+ // the enrich_agents-injected `interacted` marker. Lifecycle `status`
41
+ // and turn `activity.status` are separate fields per T1; the
42
+ // projection MUST NOT collapse them.
43
+ // =========================================================================
44
+ #[test]
45
+ fn rm039_stat001_compact_status_preserves_activity_and_last_output() {
46
+ let ws = seed_status_workspace();
47
+ let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
48
+ if let Some(agents) = state
49
+ .pointer_mut("/agents")
50
+ .and_then(serde_json::Value::as_object_mut)
51
+ {
52
+ if let Some(agent) = agents
53
+ .get_mut("a1")
44
54
  .and_then(serde_json::Value::as_object_mut)
45
55
  {
46
- if let Some(agent) = agents
47
- .get_mut("a1")
48
- .and_then(serde_json::Value::as_object_mut)
49
- {
50
- agent.insert(
51
- "activity".to_string(),
52
- json!({
53
- "status": "working",
54
- "confidence": 0.95,
55
- "rationale": "provider_jsonl:open_turn",
56
- }),
57
- );
58
- agent.insert(
59
- "last_output_at".to_string(),
60
- json!("2026-06-22T02:52:30+00:00"),
61
- );
62
- // first_send_at is already set by seed_status_workspace so
63
- // enrich_agents will inject `interacted` with the same ISO value.
64
- }
56
+ agent.insert(
57
+ "activity".to_string(),
58
+ json!({
59
+ "status": "working",
60
+ "confidence": 0.95,
61
+ "rationale": "provider_jsonl:open_turn",
62
+ }),
63
+ );
64
+ agent.insert(
65
+ "last_output_at".to_string(),
66
+ json!("2026-06-22T02:52:30+00:00"),
67
+ );
68
+ // first_send_at is already set by seed_status_workspace so
69
+ // enrich_agents will inject `interacted` with the same ISO value.
65
70
  }
66
- crate::state::persist::save_runtime_state(&ws, &state).unwrap();
67
-
68
- let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false)
69
- .expect("compact status should project a value");
70
- let agent = v
71
- .pointer("/agents/a1")
72
- .and_then(serde_json::Value::as_object)
73
- .expect("seeded agent a1 must appear in compact projection");
74
-
75
- // T1 split: lifecycle `status` stays unchanged.
76
- assert_eq!(
77
- agent.get("status").and_then(serde_json::Value::as_str),
78
- Some("running"),
79
- "RM-039-STAT-001: compact projection must NOT collapse `status` into `activity.status`"
80
- );
81
- // Turn activity preserved (the field that was historically dropped).
82
- let activity = agent
83
- .get("activity")
84
- .expect("RM-039-STAT-001: compact projection must preserve `activity`");
85
- assert_eq!(
86
- activity.pointer("/status").and_then(serde_json::Value::as_str),
87
- Some("working"),
88
- "compact activity.status must survive the projection"
89
- );
90
- assert_eq!(
91
- activity.pointer("/rationale").and_then(serde_json::Value::as_str),
92
- Some("provider_jsonl:open_turn"),
93
- "compact activity.rationale must survive the projection"
94
- );
95
- // last_output_at is the timestamp the classifier advances when
96
- // scrollback digest changes; operators read it alongside activity.
97
- assert_eq!(
98
- agent.get("last_output_at").and_then(serde_json::Value::as_str),
99
- Some("2026-06-22T02:52:30+00:00"),
100
- "compact projection must preserve `last_output_at` for the \"is something moving\" view"
101
- );
102
- // 0.4.x compact slim: `interacted` moves to --detail; the 4-field
103
- // compact agent row keeps only status/provider/activity/last_output_at.
104
- assert!(
105
- agent.get("interacted").is_none(),
106
- "0.4.x: compact projection drops `interacted` (moved to --detail)"
107
- );
108
- let _ = std::fs::remove_dir_all(&ws);
109
71
  }
72
+ crate::state::persist::save_runtime_state(&ws, &state).unwrap();
73
+
74
+ let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false)
75
+ .expect("compact status should project a value");
76
+ let agent = v
77
+ .pointer("/agents/a1")
78
+ .and_then(serde_json::Value::as_object)
79
+ .expect("seeded agent a1 must appear in compact projection");
80
+
81
+ // T1 split: lifecycle `status` stays unchanged.
82
+ assert_eq!(
83
+ agent.get("status").and_then(serde_json::Value::as_str),
84
+ Some("running"),
85
+ "RM-039-STAT-001: compact projection must NOT collapse `status` into `activity.status`"
86
+ );
87
+ // Turn activity preserved (the field that was historically dropped).
88
+ let activity = agent
89
+ .get("activity")
90
+ .expect("RM-039-STAT-001: compact projection must preserve `activity`");
91
+ assert_eq!(
92
+ activity
93
+ .pointer("/status")
94
+ .and_then(serde_json::Value::as_str),
95
+ Some("working"),
96
+ "compact activity.status must survive the projection"
97
+ );
98
+ assert_eq!(
99
+ activity
100
+ .pointer("/rationale")
101
+ .and_then(serde_json::Value::as_str),
102
+ Some("provider_jsonl:open_turn"),
103
+ "compact activity.rationale must survive the projection"
104
+ );
105
+ // last_output_at is the timestamp the classifier advances when
106
+ // scrollback digest changes; operators read it alongside activity.
107
+ assert_eq!(
108
+ agent
109
+ .get("last_output_at")
110
+ .and_then(serde_json::Value::as_str),
111
+ Some("2026-06-22T02:52:30+00:00"),
112
+ "compact projection must preserve `last_output_at` for the \"is something moving\" view"
113
+ );
114
+ // 0.4.x compact slim: `interacted` moves to --detail; the 4-field
115
+ // compact agent row keeps only status/provider/activity/last_output_at.
116
+ assert!(
117
+ agent.get("interacted").is_none(),
118
+ "0.4.x: compact projection drops `interacted` (moved to --detail)"
119
+ );
120
+ let _ = std::fs::remove_dir_all(&ws);
121
+ }
110
122
 
111
- // =========================================================================
112
- // RM-039-STAT-001 second-round regression (architect verdict 2026-06-22).
113
- //
114
- // Real failure shape: tick wrote `activity` to root .agents.coder and
115
- // .teams.current.agents.coder because `team_state_key` cascaded to the
116
- // `team_dir = "./.team/current"` basename. But status's selector reads
117
- // the `active_team_key = "rm039-status-working-891"` projection, which
118
- // was stale. The compact whitelist alone cannot fix this — by the time
119
- // compact_agent_state runs, the selected team slot already lacks activity.
120
- //
121
- // Fix expected at the projection/state-key layer: when load_runtime_state
122
- // sees `active_team_key` naming an existing teams entry and no root
123
- // `team_key`, it must promote `team_key = active_team_key` so subsequent
124
- // tick writes and status reads agree on which teams entry to use. The
125
- // assertion here drives the real CLI path through `status_port::status`.
126
- // =========================================================================
127
- #[test]
128
- fn rm039_stat001_status_resolves_active_team_when_root_team_key_missing() {
129
- let ws = seed_status_workspace();
130
- // Seed the exact dirty shape from the evidence: active_team_key
131
- // disagrees with team_dir basename; root team_key absent; the
132
- // teams.<active> entry is stale (no activity); root + teams.current
133
- // carry activity that the tick had already written there.
134
- let active = "rm039-status-working-891";
135
- let activity = json!({
136
- "status": "working",
137
- "confidence": 0.95,
138
- "rationale": "provider_jsonl:open_turn",
139
- });
140
- let state = json!({
141
- "session_name": "team-rm039-status-working",
142
- "team_dir": "./.team/current",
143
- "active_team_key": active,
144
- // intentionally no top-level "team_key" — that is the bug shape.
145
- "leader": {"id": "leader"},
146
- "leader_receiver": {"pane_id": "%3", "status": "running"},
147
- "agents": {
148
- "coder": {
149
- "status": "running",
150
- "first_send_at": "2026-01-01T00:00:00Z",
151
- "activity": activity.clone(),
152
- "last_output_at": "2026-06-22T02:52:30+00:00",
123
+ // =========================================================================
124
+ // RM-039-STAT-001 second-round regression (architect verdict 2026-06-22).
125
+ //
126
+ // Real failure shape: tick wrote `activity` to root .agents.coder and
127
+ // .teams.current.agents.coder because `team_state_key` cascaded to the
128
+ // `team_dir = "./.team/current"` basename. But status's selector reads
129
+ // the `active_team_key = "rm039-status-working-891"` projection, which
130
+ // was stale. The compact whitelist alone cannot fix this — by the time
131
+ // compact_agent_state runs, the selected team slot already lacks activity.
132
+ //
133
+ // Fix expected at the projection/state-key layer: when load_runtime_state
134
+ // sees `active_team_key` naming an existing teams entry and no root
135
+ // `team_key`, it must promote `team_key = active_team_key` so subsequent
136
+ // tick writes and status reads agree on which teams entry to use. The
137
+ // assertion here drives the real CLI path through `status_port::status`.
138
+ // =========================================================================
139
+ #[test]
140
+ fn rm039_stat001_status_resolves_active_team_when_root_team_key_missing() {
141
+ let ws = seed_status_workspace();
142
+ // Seed the exact dirty shape from the evidence: active_team_key
143
+ // disagrees with team_dir basename; root team_key absent; the
144
+ // teams.<active> entry is stale (no activity); root + teams.current
145
+ // carry activity that the tick had already written there.
146
+ let active = "rm039-status-working-891";
147
+ let activity = json!({
148
+ "status": "working",
149
+ "confidence": 0.95,
150
+ "rationale": "provider_jsonl:open_turn",
151
+ });
152
+ let state = json!({
153
+ "session_name": "team-rm039-status-working",
154
+ "team_dir": "./.team/current",
155
+ "active_team_key": active,
156
+ // intentionally no top-level "team_key" — that is the bug shape.
157
+ "leader": {"id": "leader"},
158
+ "leader_receiver": {"pane_id": "%3", "status": "running"},
159
+ "agents": {
160
+ "coder": {
161
+ "status": "running",
162
+ "first_send_at": "2026-01-01T00:00:00Z",
163
+ "activity": activity.clone(),
164
+ "last_output_at": "2026-06-22T02:52:30+00:00",
165
+ }
166
+ },
167
+ "teams": {
168
+ "current": {
169
+ "active_team_key": active,
170
+ "session_name": "team-rm039-status-working",
171
+ "agents": {
172
+ "coder": {
173
+ "status": "running",
174
+ "first_send_at": "2026-01-01T00:00:00Z",
175
+ "activity": activity.clone(),
176
+ }
153
177
  }
154
178
  },
155
- "teams": {
156
- "current": {
157
- "active_team_key": active,
158
- "session_name": "team-rm039-status-working",
159
- "agents": {
160
- "coder": {
161
- "status": "running",
162
- "first_send_at": "2026-01-01T00:00:00Z",
163
- "activity": activity.clone(),
164
- }
165
- }
166
- },
167
- active: {
168
- "active_team_key": active,
169
- "session_name": "team-rm039-status-working",
170
- "agents": {
171
- "coder": {
172
- "status": "running",
173
- "first_send_at": "2026-01-01T00:00:00Z",
174
- // NO `activity` here — this is the stale entry
175
- // that the selector landed on pre-fix.
176
- }
179
+ active: {
180
+ "active_team_key": active,
181
+ "session_name": "team-rm039-status-working",
182
+ "agents": {
183
+ "coder": {
184
+ "status": "running",
185
+ "first_send_at": "2026-01-01T00:00:00Z",
186
+ // NO `activity` here — this is the stale entry
187
+ // that the selector landed on pre-fix.
177
188
  }
178
189
  }
179
190
  }
180
- });
181
- std::fs::write(
182
- ws.join(".team").join("runtime").join("state.json"),
183
- serde_json::to_vec_pretty(&state).unwrap(),
184
- )
185
- .unwrap();
186
-
187
- let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false)
188
- .expect("compact status should project a value");
189
- let agent = v
190
- .pointer("/agents/coder")
191
- .and_then(serde_json::Value::as_object)
192
- .expect("coder agent must appear in compact projection");
193
- let got_activity = agent
194
- .get("activity")
195
- .expect("RM-039-STAT-001 second-round: activity must reach the compact projection \
196
- even when active_team_key disagrees with team_dir basename");
197
- assert_eq!(
198
- got_activity.pointer("/status").and_then(serde_json::Value::as_str),
199
- Some("working"),
200
- "RM-039-STAT-001 second-round: compact status.agents.coder.activity.status must \
191
+ }
192
+ });
193
+ std::fs::write(
194
+ ws.join(".team").join("runtime").join("state.json"),
195
+ serde_json::to_vec_pretty(&state).unwrap(),
196
+ )
197
+ .unwrap();
198
+
199
+ let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false)
200
+ .expect("compact status should project a value");
201
+ let agent = v
202
+ .pointer("/agents/coder")
203
+ .and_then(serde_json::Value::as_object)
204
+ .expect("coder agent must appear in compact projection");
205
+ let got_activity = agent.get("activity").expect(
206
+ "RM-039-STAT-001 second-round: activity must reach the compact projection \
207
+ even when active_team_key disagrees with team_dir basename",
208
+ );
209
+ assert_eq!(
210
+ got_activity
211
+ .pointer("/status")
212
+ .and_then(serde_json::Value::as_str),
213
+ Some("working"),
214
+ "RM-039-STAT-001 second-round: compact status.agents.coder.activity.status must \
201
215
  be `working` when state.active_team_key names an existing teams entry, \
202
216
  regardless of team_dir basename"
203
- );
204
- // Lifecycle status unchanged — T1 split invariant.
205
- assert_eq!(
206
- agent.get("status").and_then(serde_json::Value::as_str),
207
- Some("running"),
208
- "lifecycle status must NOT collapse into activity.status"
209
- );
210
- let _ = std::fs::remove_dir_all(&ws);
211
- }
217
+ );
218
+ // Lifecycle status unchanged — T1 split invariant.
219
+ assert_eq!(
220
+ agent.get("status").and_then(serde_json::Value::as_str),
221
+ Some("running"),
222
+ "lifecycle status must NOT collapse into activity.status"
223
+ );
224
+ let _ = std::fs::remove_dir_all(&ws);
225
+ }
212
226
 
213
- #[test]
214
- fn status_port_status_compact_json_shape_against_seeded_fixture() {
215
- // cmd_status json branch (detail=false) delegates status_port::status(compact=true).
216
- // 0.4.x compact slim: exactly 7 top-level fields; diagnostics moved
217
- // to --detail. Plan: .team/artifacts/status-compact-plan.md.
218
- let ws = seed_status_workspace();
219
- let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false)
220
- .expect("seeded fixture status should project a value");
221
- let obj = v.as_object().expect("--json status is a dict");
222
- // Exactly these 7 keys, no more.
223
- let expected: std::collections::BTreeSet<&str> = [
224
- "ok",
225
- "team",
226
- "session_name",
227
- "leader_attach_command",
228
- "ready",
229
- "not_ready",
230
- "agents",
231
- ]
232
- .iter()
233
- .copied()
234
- .collect();
235
- let actual: std::collections::BTreeSet<&str> = obj.keys().map(String::as_str).collect();
236
- assert_eq!(
237
- actual, expected,
238
- "0.4.x compact must expose exactly 7 keys; got {actual:?}"
239
- );
240
- // Diagnostic keys must NOT leak into the default compact payload.
241
- for forbidden in [
242
- "leader_topology",
243
- "is_external_leader",
244
- "leader_client",
245
- "tmux_session_present",
246
- "leader_receiver",
247
- "agent_health",
248
- "tasks",
249
- "messages",
250
- "queued_messages",
251
- "results",
252
- "latest_results",
253
- "coordinator",
254
- "readiness",
255
- "reminder",
256
- "last_events",
257
- ] {
258
- assert!(
259
- !obj.contains_key(forbidden),
260
- "0.4.x: compact must NOT contain diagnostic key `{forbidden}` (--detail only)"
261
- );
262
- }
263
- // seeded agent surfaces through the projection.
227
+ #[test]
228
+ fn status_port_status_compact_json_shape_against_seeded_fixture() {
229
+ // cmd_status json branch (detail=false) delegates status_port::status(compact=true).
230
+ // 0.4.x compact slim: exactly 7 top-level fields; diagnostics moved
231
+ // to --detail. Plan: .team/artifacts/status-compact-plan.md.
232
+ let ws = seed_status_workspace();
233
+ let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false)
234
+ .expect("seeded fixture status should project a value");
235
+ let obj = v.as_object().expect("--json status is a dict");
236
+ // Exactly these 7 keys, no more.
237
+ let expected: std::collections::BTreeSet<&str> = [
238
+ "ok",
239
+ "team",
240
+ "session_name",
241
+ "leader_attach_command",
242
+ "ready",
243
+ "not_ready",
244
+ "agents",
245
+ ]
246
+ .iter()
247
+ .copied()
248
+ .collect();
249
+ let actual: std::collections::BTreeSet<&str> = obj.keys().map(String::as_str).collect();
250
+ assert_eq!(
251
+ actual, expected,
252
+ "0.4.x compact must expose exactly 7 keys; got {actual:?}"
253
+ );
254
+ // Diagnostic keys must NOT leak into the default compact payload.
255
+ for forbidden in [
256
+ "leader_topology",
257
+ "is_external_leader",
258
+ "leader_client",
259
+ "tmux_session_present",
260
+ "leader_receiver",
261
+ "agent_health",
262
+ "tasks",
263
+ "messages",
264
+ "queued_messages",
265
+ "results",
266
+ "latest_results",
267
+ "coordinator",
268
+ "readiness",
269
+ "reminder",
270
+ "last_events",
271
+ ] {
264
272
  assert!(
265
- obj["agents"].as_object().unwrap().contains_key("a1"),
266
- "seeded agent a1 must appear in compact agents projection"
273
+ !obj.contains_key(forbidden),
274
+ "0.4.x: compact must NOT contain diagnostic key `{forbidden}` (--detail only)"
267
275
  );
268
- let _ = std::fs::remove_dir_all(&ws);
269
276
  }
277
+ // seeded agent surfaces through the projection.
278
+ assert!(
279
+ obj["agents"].as_object().unwrap().contains_key("a1"),
280
+ "seeded agent a1 must appear in compact agents projection"
281
+ );
282
+ let _ = std::fs::remove_dir_all(&ws);
283
+ }
270
284
 
271
- #[test]
272
- fn cmd_status_human_appends_harness_reminder() {
273
- let ws = seed_status_workspace();
274
- let args = StatusArgs {
275
- agent: None,
276
- workspace: ws.clone(),
277
- detail: false,
278
- summary: false,
279
- json: false,
280
- team: None,
281
- };
282
-
283
- let r = cmd_status_for_team(&args, None).expect("status");
284
- let text = match r.output {
285
- CmdOutput::Human(text) => text,
286
- other => panic!("expected human status output, got {other:?}"),
287
- };
288
-
289
- assert!(text.ends_with(crate::cli::STATUS_REMINDER), "{text}");
290
- let _ = std::fs::remove_dir_all(&ws);
285
+ #[test]
286
+ fn cmd_status_human_appends_harness_reminder() {
287
+ let ws = seed_status_workspace();
288
+ let args = StatusArgs {
289
+ agent: None,
290
+ workspace: ws.clone(),
291
+ detail: false,
292
+ summary: false,
293
+ json: false,
294
+ team: None,
295
+ };
296
+
297
+ let r = cmd_status_for_team(&args, None).expect("status");
298
+ let text = match r.output {
299
+ CmdOutput::Human(text) => text,
300
+ other => panic!("expected human status output, got {other:?}"),
301
+ };
302
+
303
+ assert!(text.ends_with(crate::cli::STATUS_REMINDER), "{text}");
304
+ let _ = std::fs::remove_dir_all(&ws);
305
+ }
306
+
307
+ #[test]
308
+ fn status_port_status_reports_managed_leader_topology_and_attach_command() {
309
+ let ws = seed_status_workspace();
310
+ let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
311
+ if let Some(obj) = state.as_object_mut() {
312
+ obj.insert("is_external_leader".to_string(), json!(false));
313
+ obj.insert("session_name".to_string(), json!("team-current"));
291
314
  }
315
+ crate::state::persist::save_runtime_state(&ws, &state).unwrap();
316
+
317
+ // 0.4.x: leader_topology / is_external_leader moved to --detail.
318
+ // leader_attach_command stays in the slim compact payload.
319
+ let slim = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false).expect("status");
320
+ let attach = slim["leader_attach_command"]
321
+ .as_str()
322
+ .expect("compact still includes leader_attach_command");
323
+ assert!(attach.contains("attach -t team-current:leader"), "{attach}");
324
+
325
+ let detail =
326
+ status_port::status(&ws, /*compact=*/ false, /*detail=*/ true).expect("status detail");
327
+ assert_eq!(detail["leader_topology"], json!("managed"));
328
+ assert_eq!(detail["is_external_leader"], json!(false));
329
+ let _ = std::fs::remove_dir_all(&ws);
330
+ }
292
331
 
293
- #[test]
294
- fn status_port_status_reports_managed_leader_topology_and_attach_command() {
295
- let ws = seed_status_workspace();
296
- let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
297
- if let Some(obj) = state.as_object_mut() {
298
- obj.insert("is_external_leader".to_string(), json!(false));
299
- obj.insert("session_name".to_string(), json!("team-current"));
300
- }
301
- crate::state::persist::save_runtime_state(&ws, &state).unwrap();
302
-
303
- // 0.4.x: leader_topology / is_external_leader moved to --detail.
304
- // leader_attach_command stays in the slim compact payload.
305
- let slim = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false).expect("status");
306
- let attach = slim["leader_attach_command"]
307
- .as_str()
308
- .expect("compact still includes leader_attach_command");
309
- assert!(attach.contains("attach -t team-current:leader"), "{attach}");
310
-
311
- let detail = status_port::status(&ws, /*compact=*/ false, /*detail=*/ true).expect("status detail");
312
- assert_eq!(detail["leader_topology"], json!("managed"));
313
- assert_eq!(detail["is_external_leader"], json!(false));
314
- let _ = std::fs::remove_dir_all(&ws);
332
+ #[test]
333
+ fn status_port_managed_attach_command_uses_receiver_window_name() {
334
+ let ws = seed_status_workspace();
335
+ let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
336
+ if let Some(obj) = state.as_object_mut() {
337
+ obj.insert("is_external_leader".to_string(), json!(false));
338
+ obj.insert("session_name".to_string(), json!("team-current"));
339
+ obj.insert(
340
+ "leader_receiver".to_string(),
341
+ json!({"pane_id": "%3", "window_name": "claude_code", "status": "attached"}),
342
+ );
315
343
  }
344
+ crate::state::persist::save_runtime_state(&ws, &state).unwrap();
316
345
 
317
- #[test]
318
- fn status_port_managed_attach_command_uses_receiver_window_name() {
319
- let ws = seed_status_workspace();
320
- let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
321
- if let Some(obj) = state.as_object_mut() {
322
- obj.insert("is_external_leader".to_string(), json!(false));
323
- obj.insert("session_name".to_string(), json!("team-current"));
324
- obj.insert(
325
- "leader_receiver".to_string(),
326
- json!({"pane_id": "%3", "window_name": "claude_code", "status": "attached"}),
327
- );
328
- }
329
- crate::state::persist::save_runtime_state(&ws, &state).unwrap();
346
+ let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false).expect("status");
330
347
 
331
- let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false).expect("status");
348
+ let attach = v["leader_attach_command"]
349
+ .as_str()
350
+ .expect("managed status includes attach command");
351
+ assert!(
352
+ attach.contains("attach -t team-current:claude_code"),
353
+ "{attach}"
354
+ );
355
+ let _ = std::fs::remove_dir_all(&ws);
356
+ }
332
357
 
333
- let attach = v["leader_attach_command"]
334
- .as_str()
335
- .expect("managed status includes attach command");
336
- assert!(attach.contains("attach -t team-current:claude_code"), "{attach}");
337
- let _ = std::fs::remove_dir_all(&ws);
338
- }
358
+ #[test]
359
+ fn leader_attach_command_for_plan_uses_plan_leader_window() {
360
+ let ws = tmp_workspace();
361
+ let plan = crate::leader::LeaderStartPlan {
362
+ mode: crate::leader::LeaderStartMode::ManagedTmuxClient,
363
+ provider: crate::provider::Provider::ClaudeCode,
364
+ workspace: ws.clone(),
365
+ socket: crate::leader::LeaderLaunchSocket::Workspace,
366
+ session_name: Some(crate::transport::SessionName::new(
367
+ "team-agent-leader-claude_code-demo".to_string(),
368
+ )),
369
+ argv: Vec::new(),
370
+ provider_argv: Vec::new(),
371
+ leader_window: Some(crate::transport::WindowName::new("claude_code")),
372
+ is_external_leader: false,
373
+ leader_env: std::collections::BTreeMap::new(),
374
+ identity: None,
375
+ detached: false,
376
+ };
339
377
 
340
- #[test]
341
- fn leader_attach_command_for_plan_uses_plan_leader_window() {
342
- let ws = tmp_workspace();
343
- let plan = crate::leader::LeaderStartPlan {
344
- mode: crate::leader::LeaderStartMode::ManagedTmuxClient,
345
- provider: crate::provider::Provider::ClaudeCode,
346
- workspace: ws.clone(),
347
- socket: crate::leader::LeaderLaunchSocket::Workspace,
348
- session_name: Some(crate::transport::SessionName::new(
349
- "team-agent-leader-claude_code-demo".to_string(),
350
- )),
351
- argv: Vec::new(),
352
- provider_argv: Vec::new(),
353
- leader_window: Some(crate::transport::WindowName::new("claude_code")),
354
- is_external_leader: false,
355
- leader_env: std::collections::BTreeMap::new(),
356
- identity: None,
357
- detached: false,
358
- };
359
-
360
- let attach = lifecycle_port::leader_attach_command_for_plan(&ws, &plan)
361
- .expect("managed plan has attach command");
378
+ let attach = lifecycle_port::leader_attach_command_for_plan(&ws, &plan)
379
+ .expect("managed plan has attach command");
362
380
 
363
- assert!(
364
- attach.contains("attach -t team-agent-leader-claude_code-demo:claude_code"),
365
- "{attach}"
366
- );
367
- let _ = std::fs::remove_dir_all(&ws);
368
- }
381
+ assert!(
382
+ attach.contains("attach -t team-agent-leader-claude_code-demo:claude_code"),
383
+ "{attach}"
384
+ );
385
+ let _ = std::fs::remove_dir_all(&ws);
386
+ }
369
387
 
370
- #[test]
371
- fn status_port_missing_topology_marker_defaults_to_managed() {
372
- let ws = seed_status_workspace();
373
- let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
374
- if let Some(obj) = state.as_object_mut() {
375
- obj.remove("is_external_leader");
376
- obj.insert("session_name".to_string(), json!("team-current"));
377
- }
378
- crate::state::persist::save_runtime_state(&ws, &state).unwrap();
379
-
380
- // 0.4.x: topology/external markers moved to --detail; compact keeps attach.
381
- let slim = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false).expect("status");
382
- let attach = slim["leader_attach_command"]
383
- .as_str()
384
- .expect("missing marker defaults to managed attach command");
385
- assert!(attach.contains("attach -t team-current:leader"), "{attach}");
386
-
387
- let detail = status_port::status(&ws, /*compact=*/ false, /*detail=*/ true).expect("status detail");
388
- assert_eq!(detail["leader_topology"], json!("managed"));
389
- assert_eq!(detail["is_external_leader"], json!(false));
390
- let _ = std::fs::remove_dir_all(&ws);
388
+ #[test]
389
+ fn status_port_missing_topology_marker_defaults_to_managed() {
390
+ let ws = seed_status_workspace();
391
+ let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
392
+ if let Some(obj) = state.as_object_mut() {
393
+ obj.remove("is_external_leader");
394
+ obj.insert("session_name".to_string(), json!("team-current"));
391
395
  }
396
+ crate::state::persist::save_runtime_state(&ws, &state).unwrap();
397
+
398
+ // 0.4.x: topology/external markers moved to --detail; compact keeps attach.
399
+ let slim = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false).expect("status");
400
+ let attach = slim["leader_attach_command"]
401
+ .as_str()
402
+ .expect("missing marker defaults to managed attach command");
403
+ assert!(attach.contains("attach -t team-current:leader"), "{attach}");
404
+
405
+ let detail =
406
+ status_port::status(&ws, /*compact=*/ false, /*detail=*/ true).expect("status detail");
407
+ assert_eq!(detail["leader_topology"], json!("managed"));
408
+ assert_eq!(detail["is_external_leader"], json!(false));
409
+ let _ = std::fs::remove_dir_all(&ws);
410
+ }
392
411
 
393
- #[test]
394
- fn status_port_status_detail_full_keeps_uncompacted_events() {
395
- // 0.4.x: --detail (compact=false) preserves ALL diagnostic fields the
396
- // compact slim payload drops. Pin the must-keep set so future
397
- // refactors can't accidentally strip detail diagnostics.
398
- let ws = seed_status_workspace();
399
- let full = status_port::status(&ws, /*compact=*/ false, /*detail=*/ true)
400
- .expect("seeded fixture full status should project a value");
401
- let compact = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false)
402
- .expect("seeded fixture compact status should project a value");
403
- assert_ne!(full, compact, "detail (full) and default (compact) projections must differ");
404
- let full_obj = full.as_object().expect("full status is a dict");
405
- for key in [
406
- "coordinator",
407
- "readiness",
408
- "leader_receiver",
409
- "agent_health",
410
- "tasks",
411
- "messages",
412
- "queued_messages",
413
- "results",
414
- "latest_results",
415
- "last_events",
416
- ] {
417
- assert!(
418
- full_obj.contains_key(key),
419
- "0.4.x: --detail must preserve `{key}` (compact slimming escape hatch)"
420
- );
421
- }
422
- assert_eq!(full_obj["agents"].as_object().unwrap().len(), 1);
423
- let _ = std::fs::remove_dir_all(&ws);
412
+ #[test]
413
+ fn status_port_status_detail_full_keeps_uncompacted_events() {
414
+ // 0.4.x: --detail (compact=false) preserves ALL diagnostic fields the
415
+ // compact slim payload drops. Pin the must-keep set so future
416
+ // refactors can't accidentally strip detail diagnostics.
417
+ let ws = seed_status_workspace();
418
+ let full = status_port::status(&ws, /*compact=*/ false, /*detail=*/ true)
419
+ .expect("seeded fixture full status should project a value");
420
+ let compact = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false)
421
+ .expect("seeded fixture compact status should project a value");
422
+ assert_ne!(
423
+ full, compact,
424
+ "detail (full) and default (compact) projections must differ"
425
+ );
426
+ let full_obj = full.as_object().expect("full status is a dict");
427
+ for key in [
428
+ "coordinator",
429
+ "readiness",
430
+ "leader_receiver",
431
+ "agent_health",
432
+ "tasks",
433
+ "messages",
434
+ "queued_messages",
435
+ "results",
436
+ "latest_results",
437
+ "last_events",
438
+ ] {
439
+ assert!(
440
+ full_obj.contains_key(key),
441
+ "0.4.x: --detail must preserve `{key}` (compact slimming escape hatch)"
442
+ );
424
443
  }
444
+ assert_eq!(full_obj["agents"].as_object().unwrap().len(), 1);
445
+ let _ = std::fs::remove_dir_all(&ws);
446
+ }
425
447
 
426
- // =========================================================================
427
- // send_options_from_args (commands.py:170-177): SendArgs->SendOptions 旗标取反
428
- // gate: 'no_ack:true => requires_ack:false and no_wait:true => wait_visible:false';
429
- // watch_result flag maps into SendOptions.
430
- // RED: send_options_from_args is unimplemented!() until ported.
431
- // =========================================================================
432
-
433
- fn send_args_fixture() -> SendArgs {
434
- SendArgs {
435
- target: Some("alice".into()),
436
- message: vec!["hello".into(), "world".into(), "foo".into()],
437
- targets: None,
438
- workspace: PathBuf::from("."),
439
- team: Some("teamA".into()),
440
- task: Some("t-1".into()),
441
- sender: "leader".into(),
442
- no_ack: true,
443
- no_wait: true,
444
- watch_result: true,
445
- timeout: 12.5,
446
- confirm_human: false,
447
- json: false,
448
- message_id: None,
449
- pane: None,
450
- to_name: None,
448
+ // =========================================================================
449
+ // send_options_from_args (commands.py:170-177): SendArgs->SendOptions 旗标取反
450
+ // gate: 'no_ack:true => requires_ack:false and no_wait:true => wait_visible:false';
451
+ // watch_result flag maps into SendOptions.
452
+ // RED: send_options_from_args is unimplemented!() until ported.
453
+ // =========================================================================
454
+
455
+ fn send_args_fixture() -> SendArgs {
456
+ SendArgs {
457
+ target: Some("alice".into()),
458
+ message: vec!["hello".into(), "world".into(), "foo".into()],
459
+ targets: None,
460
+ workspace: PathBuf::from("."),
461
+ team: Some("teamA".into()),
462
+ task: Some("t-1".into()),
463
+ sender: "leader".into(),
464
+ no_ack: true,
465
+ no_wait: true,
466
+ watch_result: true,
467
+ timeout: 12.5,
468
+ confirm_human: false,
469
+ json: false,
470
+ message_id: None,
471
+ pane: None,
472
+ to_name: None,
451
473
  to_leader: None,
452
- }
453
474
  }
475
+ }
454
476
 
455
- fn queued_send_args_fixture(json: bool) -> SendArgs {
456
- let ws = deleg_uniq_dir("send-human");
457
- let _ = crate::message_store::MessageStore::open(&ws).unwrap();
458
- crate::state::persist::save_runtime_state(
459
- &ws,
460
- &json!({
461
- "active_team_key": "current",
462
- "teams": {"current": {"agents": {"w1": {"provider": "codex"}}}}
463
- }),
464
- )
465
- .unwrap();
466
- SendArgs {
467
- workspace: ws,
468
- target: Some("w1".into()),
469
- team: None,
470
- task: None,
471
- watch_result: false,
472
- json,
473
- ..send_args_fixture()
474
- }
477
+ fn queued_send_args_fixture(json: bool) -> SendArgs {
478
+ let ws = deleg_uniq_dir("send-human");
479
+ let _ = crate::message_store::MessageStore::open(&ws).unwrap();
480
+ crate::state::persist::save_runtime_state(
481
+ &ws,
482
+ &json!({
483
+ "active_team_key": "current",
484
+ "teams": {"current": {"agents": {"w1": {"provider": "codex"}}}}
485
+ }),
486
+ )
487
+ .unwrap();
488
+ SendArgs {
489
+ workspace: ws,
490
+ target: Some("w1".into()),
491
+ team: None,
492
+ task: None,
493
+ watch_result: false,
494
+ json,
495
+ ..send_args_fixture()
475
496
  }
497
+ }
476
498
 
477
- #[test]
478
- fn send_options_negates_no_ack_and_no_wait_and_carries_watch() {
479
- // golden (commands.py:172,174,176): requires_ack=not no_ack; wait_visible=not no_wait;
480
- // watch_result passthrough. With no_ack=true,no_wait=true,watch_result=true:
481
- // requires_ack=false, wait_visible=false, watch_result=true.
482
- let opts = send_options_from_args(&send_args_fixture());
483
- assert!(!opts.requires_ack, "no_ack:true MUST map to requires_ack:false (off-by-inversion guard)");
484
- assert!(!opts.wait_visible, "no_wait:true MUST map to wait_visible:false");
485
- assert!(opts.watch_result, "watch_result flag MUST pass through into SendOptions");
486
- assert!(!opts.confirm_human);
487
- assert_eq!(opts.sender, "leader");
488
- assert_eq!(opts.timeout, 12.5);
489
- }
499
+ #[test]
500
+ fn send_options_negates_no_ack_and_no_wait_and_carries_watch() {
501
+ // golden (commands.py:172,174,176): requires_ack=not no_ack; wait_visible=not no_wait;
502
+ // watch_result passthrough. With no_ack=true,no_wait=true,watch_result=true:
503
+ // requires_ack=false, wait_visible=false, watch_result=true.
504
+ let opts = send_options_from_args(&send_args_fixture());
505
+ assert!(
506
+ !opts.requires_ack,
507
+ "no_ack:true MUST map to requires_ack:false (off-by-inversion guard)"
508
+ );
509
+ assert!(
510
+ !opts.wait_visible,
511
+ "no_wait:true MUST map to wait_visible:false"
512
+ );
513
+ assert!(
514
+ opts.watch_result,
515
+ "watch_result flag MUST pass through into SendOptions"
516
+ );
517
+ assert!(!opts.confirm_human);
518
+ assert_eq!(opts.sender, "leader");
519
+ assert_eq!(opts.timeout, 12.5);
520
+ }
490
521
 
491
- #[test]
492
- fn send_options_default_flags_are_acked_and_waited() {
493
- // golden: no_ack=false,no_wait=false,watch_result=false ->
494
- // requires_ack=true, wait_visible=true, watch_result=false (Python defaults inverted back).
495
- let args = SendArgs {
496
- no_ack: false,
497
- no_wait: false,
498
- watch_result: false,
499
- ..send_args_fixture()
500
- };
501
- let opts = send_options_from_args(&args);
502
- assert!(opts.requires_ack, "no_ack:false MUST map to requires_ack:true");
503
- assert!(opts.wait_visible, "no_wait:false MUST map to wait_visible:true");
504
- assert!(!opts.watch_result);
505
- }
522
+ #[test]
523
+ fn send_options_default_flags_are_acked_and_waited() {
524
+ // golden: no_ack=false,no_wait=false,watch_result=false ->
525
+ // requires_ack=true, wait_visible=true, watch_result=false (Python defaults inverted back).
526
+ let args = SendArgs {
527
+ no_ack: false,
528
+ no_wait: false,
529
+ watch_result: false,
530
+ ..send_args_fixture()
531
+ };
532
+ let opts = send_options_from_args(&args);
533
+ assert!(
534
+ opts.requires_ack,
535
+ "no_ack:false MUST map to requires_ack:true"
536
+ );
537
+ assert!(
538
+ opts.wait_visible,
539
+ "no_wait:false MUST map to wait_visible:true"
540
+ );
541
+ assert!(!opts.watch_result);
542
+ }
506
543
 
507
- // =========================================================================
508
- // cmd_send — REAL caller (gate: 'cmd_send has NO test beyond send_target').
509
- // Asserts (1) message Vec joined by single space surfaces to send_message,
510
- // (2) the registered-watcher notice ({status:'registered',...} -> result['watch'],
511
- // send.py:326-337) survives into CmdResult Json output,
512
- // (3) DeliveryOutcome->exit-code derivation (ok=true -> ExitCode::Ok).
513
- // RED: cmd_send is unimplemented!() so it panics until ported.
514
- // =========================================================================
515
-
516
- #[test]
517
- fn cmd_send_joins_message_with_single_space() {
518
- // golden (commands.py:169): " ".join(["hello","world","foo"]) == "hello world foo".
519
- // Drive cmd_send; the joined content must reach send_message (RED until ported).
520
- let args = SendArgs {
521
- json: true,
522
- ..send_args_fixture()
523
- };
524
- let r = cmd_send(&args).expect("cmd_send returns CmdResult");
525
- // The delegate's DeliveryOutcome -> Json must carry an `ok` key feeding exit-code.
526
- match r.output {
527
- CmdOutput::Json(ref v) => {
528
- assert!(v.get("ok").is_some(), "send result Json must carry `ok`");
529
- if v.get("ok").and_then(|ok| ok.as_bool()) == Some(true) {
530
- assert_eq!(
531
- v.get("reminder").and_then(|reminder| reminder.as_str()),
532
- Some(crate::cli::SEND_REMINDER)
533
- );
534
- }
544
+ // =========================================================================
545
+ // cmd_send — REAL caller (gate: 'cmd_send has NO test beyond send_target').
546
+ // Asserts (1) message Vec joined by single space surfaces to send_message,
547
+ // (2) the registered-watcher notice ({status:'registered',...} -> result['watch'],
548
+ // send.py:326-337) survives into CmdResult Json output,
549
+ // (3) DeliveryOutcome->exit-code derivation (ok=true -> ExitCode::Ok).
550
+ // RED: cmd_send is unimplemented!() so it panics until ported.
551
+ // =========================================================================
552
+
553
+ #[test]
554
+ fn cmd_send_joins_message_with_single_space() {
555
+ // golden (commands.py:169): " ".join(["hello","world","foo"]) == "hello world foo".
556
+ // Drive cmd_send; the joined content must reach send_message (RED until ported).
557
+ let args = SendArgs {
558
+ json: true,
559
+ ..send_args_fixture()
560
+ };
561
+ let r = cmd_send(&args).expect("cmd_send returns CmdResult");
562
+ // The delegate's DeliveryOutcome -> Json must carry an `ok` key feeding exit-code.
563
+ match r.output {
564
+ CmdOutput::Json(ref v) => {
565
+ assert!(v.get("ok").is_some(), "send result Json must carry `ok`");
566
+ if v.get("ok").and_then(|ok| ok.as_bool()) == Some(true) {
567
+ assert_eq!(
568
+ v.get("reminder").and_then(|reminder| reminder.as_str()),
569
+ Some(crate::cli::SEND_REMINDER)
570
+ );
535
571
  }
536
- other => panic!("cmd_send must emit Json DeliveryOutcome, got {other:?}"),
537
572
  }
573
+ other => panic!("cmd_send must emit Json DeliveryOutcome, got {other:?}"),
538
574
  }
575
+ }
539
576
 
540
- #[test]
541
- fn cmd_send_default_human_output_is_one_line_without_false_delivered() {
542
- let r = cmd_send(&queued_send_args_fixture(false)).expect("cmd_send returns CmdResult");
543
- assert!(!r.as_json);
544
- let text = emit(&r.output, r.as_json).expect("send should render human text");
545
- let lines: Vec<_> = text.lines().collect();
546
- assert_eq!(lines.len(), 1, "default send output must be one line: {text}");
547
- assert!(
548
- lines[0].contains("ok:")
549
- && lines[0].contains("status:")
550
- && lines[0].contains("message_id:")
551
- && lines[0].contains("target:"),
552
- "default send output must keep only the core fields; got {text}"
553
- );
577
+ #[test]
578
+ fn cmd_send_default_human_output_is_one_line_without_false_delivered() {
579
+ let r = cmd_send(&queued_send_args_fixture(false)).expect("cmd_send returns CmdResult");
580
+ assert!(!r.as_json);
581
+ let text = emit(&r.output, r.as_json).expect("send should render human text");
582
+ let lines: Vec<_> = text.lines().collect();
583
+ assert_eq!(
584
+ lines.len(),
585
+ 1,
586
+ "default send output must be one line: {text}"
587
+ );
588
+ assert!(
589
+ lines[0].contains("ok:")
590
+ && lines[0].contains("status:")
591
+ && lines[0].contains("message_id:")
592
+ && lines[0].contains("target:"),
593
+ "default send output must keep only the core fields; got {text}"
594
+ );
595
+ assert!(
596
+ !text.contains("delivered"),
597
+ "queued send output must not claim or mention delivered; got {text}"
598
+ );
599
+ for hidden in [
600
+ "agent_id:",
601
+ "sender:",
602
+ "message_status:",
603
+ "verification:",
604
+ "stage:",
605
+ "reason:",
606
+ "channel:",
607
+ "reminder:",
608
+ ] {
554
609
  assert!(
555
- !text.contains("delivered"),
556
- "queued send output must not claim or mention delivered; got {text}"
610
+ !text.contains(hidden),
611
+ "default send output should hide {hidden} unless needed; got {text}"
557
612
  );
558
- for hidden in [
559
- "agent_id:",
560
- "sender:",
561
- "message_status:",
562
- "verification:",
563
- "stage:",
564
- "reason:",
565
- "channel:",
566
- "reminder:",
567
- ] {
568
- assert!(
569
- !text.contains(hidden),
570
- "default send output should hide {hidden} unless needed; got {text}"
571
- );
572
- }
573
613
  }
614
+ }
574
615
 
575
- #[test]
576
- fn cmd_send_json_shape_keeps_056_fields() {
577
- let args = queued_send_args_fixture(true);
578
- let r = cmd_send(&args).expect("cmd_send returns CmdResult");
579
- let v = match r.output {
580
- CmdOutput::Json(v) => v,
581
- other => panic!("--json send must emit Json, got {other:?}"),
582
- };
583
- let obj = v.as_object().expect("--json send output must be object");
584
- for key in [
585
- "ok",
586
- "status",
587
- "delivery_status",
588
- "delivered",
589
- "target",
590
- "agent_id",
591
- "content_length_bytes",
592
- "sender",
593
- "message_id",
594
- "message_status",
595
- "verification",
596
- "stage",
597
- "reason",
598
- "channel",
599
- "reminder",
600
- ] {
601
- assert!(obj.contains_key(key), "--json send shape lost {key}: {v}");
602
- }
603
- assert_eq!(v.get("verification"), Some(&serde_json::Value::Null));
604
- assert_eq!(v.get("stage"), Some(&serde_json::Value::Null));
605
- assert_eq!(v.get("reason"), Some(&serde_json::Value::Null));
606
- assert_eq!(v.get("channel"), Some(&serde_json::Value::Null));
607
- assert_eq!(v.get("delivered").and_then(|d| d.as_bool()), Some(false));
608
- assert!(
609
- !v.get("reminder")
610
- .and_then(|reminder| reminder.as_str())
611
- .unwrap_or_default()
612
- .contains("Message delivered."),
613
- "queued JSON reminder must not contradict delivered:false: {v}"
614
- );
616
+ #[test]
617
+ fn cmd_send_json_shape_keeps_056_fields() {
618
+ let args = queued_send_args_fixture(true);
619
+ let r = cmd_send(&args).expect("cmd_send returns CmdResult");
620
+ let v = match r.output {
621
+ CmdOutput::Json(v) => v,
622
+ other => panic!("--json send must emit Json, got {other:?}"),
623
+ };
624
+ let obj = v.as_object().expect("--json send output must be object");
625
+ for key in [
626
+ "ok",
627
+ "status",
628
+ "delivery_status",
629
+ "delivered",
630
+ "target",
631
+ "agent_id",
632
+ "content_length_bytes",
633
+ "sender",
634
+ "message_id",
635
+ "message_status",
636
+ "verification",
637
+ "stage",
638
+ "reason",
639
+ "channel",
640
+ "reminder",
641
+ ] {
642
+ assert!(obj.contains_key(key), "--json send shape lost {key}: {v}");
615
643
  }
644
+ assert_eq!(v.get("verification"), Some(&serde_json::Value::Null));
645
+ assert_eq!(v.get("stage"), Some(&serde_json::Value::Null));
646
+ assert_eq!(v.get("reason"), Some(&serde_json::Value::Null));
647
+ assert_eq!(v.get("channel"), Some(&serde_json::Value::Null));
648
+ assert_eq!(v.get("delivered").and_then(|d| d.as_bool()), Some(false));
649
+ assert!(
650
+ !v.get("reminder")
651
+ .and_then(|reminder| reminder.as_str())
652
+ .unwrap_or_default()
653
+ .contains("Message delivered."),
654
+ "queued JSON reminder must not contradict delivered:false: {v}"
655
+ );
656
+ }
616
657
 
617
- #[test]
618
- fn cmd_send_watch_result_does_not_register_before_delivery() {
619
- // 0.5.x send contract: --watch-result may only advertise a watcher after
620
- // initial worker delivery is physically proven.
621
- let args = SendArgs {
622
- json: true,
623
- ..send_args_fixture()
624
- };
625
- let r = cmd_send(&args).expect("cmd_send returns CmdResult");
626
- let v = match r.output {
627
- CmdOutput::Json(v) => v,
628
- other => panic!("expected Json, got {other:?}"),
629
- };
630
- assert!(
631
- v.get("delivery_status").and_then(|s| s.as_str()).is_some(),
632
- "send output must expose delivery_status; got {v}"
633
- );
634
- assert_eq!(
635
- v.get("delivered").and_then(|s| s.as_bool()),
636
- Some(false),
637
- "undelivered send outcome must not look delivered"
638
- );
639
- assert!(
640
- !v.as_object().unwrap().contains_key("watch"),
641
- "watch_result:true must not attach result['watch'] before delivery; got {v}"
642
- );
643
- }
658
+ #[test]
659
+ fn cmd_send_watch_result_does_not_register_before_delivery() {
660
+ // 0.5.x send contract: --watch-result may only advertise a watcher after
661
+ // initial worker delivery is physically proven.
662
+ let args = SendArgs {
663
+ json: true,
664
+ ..send_args_fixture()
665
+ };
666
+ let r = cmd_send(&args).expect("cmd_send returns CmdResult");
667
+ let v = match r.output {
668
+ CmdOutput::Json(v) => v,
669
+ other => panic!("expected Json, got {other:?}"),
670
+ };
671
+ assert!(
672
+ v.get("delivery_status").and_then(|s| s.as_str()).is_some(),
673
+ "send output must expose delivery_status; got {v}"
674
+ );
675
+ assert_eq!(
676
+ v.get("delivered").and_then(|s| s.as_bool()),
677
+ Some(false),
678
+ "undelivered send outcome must not look delivered"
679
+ );
680
+ assert!(
681
+ !v.as_object().unwrap().contains_key("watch"),
682
+ "watch_result:true must not attach result['watch'] before delivery; got {v}"
683
+ );
684
+ }
644
685
 
645
- #[test]
646
- fn cmd_send_failed_outcome_yields_error_exit() {
647
- // DeliveryOutcome ok=false (e.g. refused) -> from_json -> ExitCode::Error (parser.py:507).
648
- // A failed send to a target must propagate non-zero exit reporting through CmdResult.
649
- let args = SendArgs {
650
- target: Some("nonexistent".into()),
651
- no_ack: false,
652
- no_wait: false,
653
- watch_result: false,
654
- ..send_args_fixture()
655
- };
656
- let r = cmd_send(&args).expect("cmd_send returns CmdResult even on delivery failure");
657
- if let CmdOutput::Json(ref v) = r.output {
658
- if v.get("ok").and_then(|b| b.as_bool()) == Some(false) {
659
- assert_eq!(
660
- r.exit,
661
- ExitCode::Error,
662
- "ok:false DeliveryOutcome MUST derive ExitCode::Error (non-zero exit)"
663
- );
664
- }
686
+ #[test]
687
+ fn cmd_send_failed_outcome_yields_error_exit() {
688
+ // DeliveryOutcome ok=false (e.g. refused) -> from_json -> ExitCode::Error (parser.py:507).
689
+ // A failed send to a target must propagate non-zero exit reporting through CmdResult.
690
+ let args = SendArgs {
691
+ target: Some("nonexistent".into()),
692
+ no_ack: false,
693
+ no_wait: false,
694
+ watch_result: false,
695
+ ..send_args_fixture()
696
+ };
697
+ let r = cmd_send(&args).expect("cmd_send returns CmdResult even on delivery failure");
698
+ if let CmdOutput::Json(ref v) = r.output {
699
+ if v.get("ok").and_then(|b| b.as_bool()) == Some(false) {
700
+ assert_eq!(
701
+ r.exit,
702
+ ExitCode::Error,
703
+ "ok:false DeliveryOutcome MUST derive ExitCode::Error (non-zero exit)"
704
+ );
665
705
  }
666
706
  }
667
-
707
+ }
668
708
 
669
709
  // ═══════════════════════════════════════════════════════════════════════════
670
710
  // coordinator.ok — non-compact status carries the FULL coordinator_health (incl. `ok`); compact
@@ -676,14 +716,27 @@ use super::*;
676
716
  fn status_noncompact_coordinator_includes_ok() {
677
717
  let ws = seed_status_workspace();
678
718
  let v = status_port::status(&ws, /*compact=*/ false, /*detail=*/ true).expect("status");
679
- let coord = v.get("coordinator").and_then(|c| c.as_object()).expect("coordinator object");
719
+ let coord = v
720
+ .get("coordinator")
721
+ .and_then(|c| c.as_object())
722
+ .expect("coordinator object");
680
723
  assert!(
681
724
  coord.contains_key("ok"),
682
725
  "non-compact coordinator MUST carry `ok` (golden queries.py:77 full coordinator_health); got keys {:?}",
683
726
  coord.keys().collect::<Vec<_>>()
684
727
  );
685
- for key in ["ok", "status", "pid", "metadata", "metadata_ok", "schema_ok"] {
686
- assert!(coord.contains_key(key), "non-compact coordinator missing `{key}`");
728
+ for key in [
729
+ "ok",
730
+ "status",
731
+ "pid",
732
+ "metadata",
733
+ "metadata_ok",
734
+ "schema_ok",
735
+ ] {
736
+ assert!(
737
+ coord.contains_key(key),
738
+ "non-compact coordinator missing `{key}`"
739
+ );
687
740
  }
688
741
  assert_eq!(
689
742
  coord.get("ok").and_then(|v| v.as_bool()),
@@ -722,7 +775,10 @@ fn cmd_send_unknown_task_surfaces_golden_error_envelope_not_silent() {
722
775
  let ws = std::env::temp_dir().join(format!(
723
776
  "ta-cli-sendunk-{}-{}",
724
777
  std::process::id(),
725
- std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()
778
+ std::time::SystemTime::now()
779
+ .duration_since(std::time::UNIX_EPOCH)
780
+ .unwrap()
781
+ .as_nanos()
726
782
  ));
727
783
  std::fs::create_dir_all(ws.join(".team").join("runtime")).unwrap();
728
784
  std::fs::write(
@@ -735,8 +791,10 @@ fn cmd_send_unknown_task_surfaces_golden_error_envelope_not_silent() {
735
791
  "agents": { "w1": { "status": "running" } },
736
792
  "tasks": []
737
793
  }}
738
- })).unwrap(),
739
- ).unwrap();
794
+ }))
795
+ .unwrap(),
796
+ )
797
+ .unwrap();
740
798
  let _ = crate::message_store::MessageStore::open(&ws);
741
799
  let args = SendArgs {
742
800
  target: Some("w1".into()),
@@ -759,7 +817,10 @@ fn cmd_send_unknown_task_surfaces_golden_error_envelope_not_silent() {
759
817
  payload.error, "unknown task id: t-unknown",
760
818
  "CLI error field == golden bare message (golden runtime.py:1032 str(exc)); NO 'validation:' prefix"
761
819
  );
762
- assert_eq!(payload.action, "run `team-agent doctor` or inspect the log path shown here");
820
+ assert_eq!(
821
+ payload.action,
822
+ "run `team-agent doctor` or inspect the log path shown here"
823
+ );
763
824
  let _ = std::fs::remove_dir_all(&ws);
764
825
  }
765
826
 
@@ -775,7 +836,10 @@ fn run_send_unknown_task_renders_error_not_silent_swallow() {
775
836
  let ws = std::env::temp_dir().join(format!(
776
837
  "ta-run-sendunk-{}-{}",
777
838
  std::process::id(),
778
- std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()
839
+ std::time::SystemTime::now()
840
+ .duration_since(std::time::UNIX_EPOCH)
841
+ .unwrap()
842
+ .as_nanos()
779
843
  ));
780
844
  std::fs::create_dir_all(ws.join(".team").join("runtime")).unwrap();
781
845
  std::fs::write(
@@ -788,13 +852,21 @@ fn run_send_unknown_task_renders_error_not_silent_swallow() {
788
852
  "agents": { "w1": { "status": "running" } },
789
853
  "tasks": []
790
854
  }}
791
- })).unwrap(),
792
- ).unwrap();
855
+ }))
856
+ .unwrap(),
857
+ )
858
+ .unwrap();
793
859
  let _ = crate::message_store::MessageStore::open(&ws);
794
860
  let argv: Vec<String> = ["send", "w1", "--task", "t-unknown", "go", "--json"]
795
- .iter().map(ToString::to_string).collect();
861
+ .iter()
862
+ .map(ToString::to_string)
863
+ .collect();
796
864
  let code = run(&argv, &ws);
797
- assert_eq!(code, ExitCode::Error, "run(send --task <unknown>) must exit Error, not Ok");
865
+ assert_eq!(
866
+ code,
867
+ ExitCode::Error,
868
+ "run(send --task <unknown>) must exit Error, not Ok"
869
+ );
798
870
  // run() must have RENDERED (emit_cli_error wrote the cli-error log); a swallow leaves none.
799
871
  let logs_dir = ws.join(".team").join("logs");
800
872
  let mut found = String::new();
@@ -867,11 +939,17 @@ fn run_leader_passthrough_flag_after_dashdash_renders_error_not_silent_swallow()
867
939
  #[test]
868
940
  fn r8_project_requeued_exhausted_watchers_golden_string_list() {
869
941
  // golden attach event shape (what D4 emits): {watcher_ids:[str], count, trigger}.
870
- let golden_event = serde_json::json!({"watcher_ids": ["w1", "w2"], "count": 2, "trigger": "attach_leader"});
942
+ let golden_event =
943
+ serde_json::json!({"watcher_ids": ["w1", "w2"], "count": 2, "trigger": "attach_leader"});
871
944
  let projected = crate::cli::leader_port::project_requeued_exhausted_watchers(&golden_event);
872
- let list = projected.as_array().expect("requeued_exhausted_watchers must be a JSON array");
945
+ let list = projected
946
+ .as_array()
947
+ .expect("requeued_exhausted_watchers must be a JSON array");
873
948
  let ids: Vec<&str> = list.iter().filter_map(|v| v.as_str()).collect();
874
- assert_eq!(ids, vec!["w1", "w2"],
949
+ assert_eq!(
950
+ ids,
951
+ vec!["w1", "w2"],
875
952
  "D6: CLI requeued_exhausted_watchers must project the golden watcher_ids STRING list \
876
- (leader/__init__.py:56), not the `requeued` Vec<WatcherNotice> objects; got {projected:?}");
953
+ (leader/__init__.py:56), not the `requeued` Vec<WatcherNotice> objects; got {projected:?}"
954
+ );
877
955
  }