@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,508 +1,526 @@
1
1
  use super::*;
2
2
 
3
- // =========================================================================
4
- // STEP-14 DIVERGENCE RED LANE — golden-pinned tests that FAIL against the
5
- // current Rust port. Golden = team-agent-public @ v0.2.11 (439bef8),
6
- // probed via PYTHONPATH=.../src python3 /tmp/probe_cli_all.py.
7
- // Each test encodes the EXACT golden value; the porters green these next.
8
- // =========================================================================
3
+ // =========================================================================
4
+ // STEP-14 DIVERGENCE RED LANE — golden-pinned tests that FAIL against the
5
+ // current Rust port. Golden = team-agent-public @ v0.2.11 (439bef8),
6
+ // probed via PYTHONPATH=.../src python3 /tmp/probe_cli_all.py.
7
+ // Each test encodes the EXACT golden value; the porters green these next.
8
+ // =========================================================================
9
9
 
10
- // ---- #1 / #15: classify_agent_bucket — raw "idle" with NO health => Unknown ----
11
- // golden _agent_summary_counts has NO `raw == "idle"` arm (commands.py:320):
12
- // idle is gated SOLELY on health.status=="idle". A raw "idle" with empty health
13
- // falls through every branch to the final `else: unknown += 1`.
14
- // golden probe: _agent_summary_counts({"a":{"status":"idle"}},{}) -> unknown=1.
15
- // Rust cli.rs:601 adds `|| raw == "idle"` => Idle (WRONG).
16
- #[test]
17
- fn red_classify_raw_idle_no_health_is_unknown_not_idle() {
18
- // golden: classify("idle","") lands in Unknown (the §11 bug-071/077/085 rule).
19
- assert_eq!(
20
- classify_agent_bucket("idle", ""),
21
- SummaryBucket::Unknown,
22
- "raw 'idle' with empty health MUST be Unknown (golden gates idle on health only)"
23
- );
24
- // and the uppercase variant (str.lower() in golden) also Unknown.
25
- assert_eq!(classify_agent_bucket("IDLE", ""), SummaryBucket::Unknown);
26
- // full-path golden: agent_summary_counts({"a":{"status":"idle"}},{}) -> unknown=1, idle=0.
27
- let got = agent_summary_counts(&json!({"a": {"status": "idle"}}), &json!({}));
28
- assert_eq!(
29
- got,
30
- SummaryCounts { unknown: 1, ..Default::default() },
31
- "golden: raw idle agent with no health => unknown=1 (not idle=1)"
32
- );
33
- // health=="idle" is still Idle (the only legitimate idle trigger).
34
- assert_eq!(classify_agent_bucket("", "idle"), SummaryBucket::Idle);
35
- }
10
+ // ---- #1 / #15: classify_agent_bucket — raw "idle" with NO health => Unknown ----
11
+ // golden _agent_summary_counts has NO `raw == "idle"` arm (commands.py:320):
12
+ // idle is gated SOLELY on health.status=="idle". A raw "idle" with empty health
13
+ // falls through every branch to the final `else: unknown += 1`.
14
+ // golden probe: _agent_summary_counts({"a":{"status":"idle"}},{}) -> unknown=1.
15
+ // Rust cli.rs:601 adds `|| raw == "idle"` => Idle (WRONG).
16
+ #[test]
17
+ fn red_classify_raw_idle_no_health_is_unknown_not_idle() {
18
+ // golden: classify("idle","") lands in Unknown (the §11 bug-071/077/085 rule).
19
+ assert_eq!(
20
+ classify_agent_bucket("idle", ""),
21
+ SummaryBucket::Unknown,
22
+ "raw 'idle' with empty health MUST be Unknown (golden gates idle on health only)"
23
+ );
24
+ // and the uppercase variant (str.lower() in golden) also Unknown.
25
+ assert_eq!(classify_agent_bucket("IDLE", ""), SummaryBucket::Unknown);
26
+ // full-path golden: agent_summary_counts({"a":{"status":"idle"}},{}) -> unknown=1, idle=0.
27
+ let got = agent_summary_counts(&json!({"a": {"status": "idle"}}), &json!({}));
28
+ assert_eq!(
29
+ got,
30
+ SummaryCounts {
31
+ unknown: 1,
32
+ ..Default::default()
33
+ },
34
+ "golden: raw idle agent with no health => unknown=1 (not idle=1)"
35
+ );
36
+ // health=="idle" is still Idle (the only legitimate idle trigger).
37
+ assert_eq!(classify_agent_bucket("", "idle"), SummaryBucket::Idle);
38
+ }
36
39
 
37
- // ---- #2 / #21 / #24: format_latest_result faithfulness ----
38
- // golden _latest_result_line (commands.py:333-337):
39
- // summary = str(summary or "").replace("\n"," ")[:80]; printed as `{summary or '-'}`;
40
- // agent_id printed as `{agent_id or '-'}`;
41
- // created_at rendered through runtime._age_text ('-' for None/'' /invalid ISO; 'Nh ago' for valid).
42
- // Rust format_latest_result passes summary verbatim, uses unwrap_or("-") (empty stays empty),
43
- // and prints created_at raw (no age_text).
44
- #[test]
45
- fn red_format_latest_result_empty_summary_and_agent_map_to_dash() {
46
- // golden: summary='' + created_at invalid -> 'latest result: a1 -> - @ -'
47
- let line = format_status_summary(&json!({
48
- "latest_results": [{"agent_id": "a1", "summary": "", "created_at": "bad-date"}]
49
- }));
50
- let latest = line.lines().nth(4).unwrap();
51
- assert_eq!(
52
- latest, "latest result: a1 -> - @ -",
53
- "empty summary -> '-', invalid created_at -> '-' (age_text); golden commands.py:337"
54
- );
55
- // golden: empty agent_id -> '-'
56
- let line2 = format_status_summary(&json!({
57
- "latest_results": [{"agent_id": "", "summary": "hi", "created_at": Value::Null}]
58
- }));
59
- assert_eq!(
60
- line2.lines().nth(4).unwrap(),
61
- "latest result: - -> hi @ -",
62
- "empty agent_id -> '-' (golden `agent_id or '-'`)"
63
- );
64
- }
40
+ // ---- #2 / #21 / #24: format_latest_result faithfulness ----
41
+ // golden _latest_result_line (commands.py:333-337):
42
+ // summary = str(summary or "").replace("\n"," ")[:80]; printed as `{summary or '-'}`;
43
+ // agent_id printed as `{agent_id or '-'}`;
44
+ // created_at rendered through runtime._age_text ('-' for None/'' /invalid ISO; 'Nh ago' for valid).
45
+ // Rust format_latest_result passes summary verbatim, uses unwrap_or("-") (empty stays empty),
46
+ // and prints created_at raw (no age_text).
47
+ #[test]
48
+ fn red_format_latest_result_empty_summary_and_agent_map_to_dash() {
49
+ // golden: summary='' + created_at invalid -> 'latest result: a1 -> - @ -'
50
+ let line = format_status_summary(&json!({
51
+ "latest_results": [{"agent_id": "a1", "summary": "", "created_at": "bad-date"}]
52
+ }));
53
+ let latest = line.lines().nth(4).unwrap();
54
+ assert_eq!(
55
+ latest, "latest result: a1 -> - @ -",
56
+ "empty summary -> '-', invalid created_at -> '-' (age_text); golden commands.py:337"
57
+ );
58
+ // golden: empty agent_id -> '-'
59
+ let line2 = format_status_summary(&json!({
60
+ "latest_results": [{"agent_id": "", "summary": "hi", "created_at": Value::Null}]
61
+ }));
62
+ assert_eq!(
63
+ line2.lines().nth(4).unwrap(),
64
+ "latest result: - -> hi @ -",
65
+ "empty agent_id -> '-' (golden `agent_id or '-'`)"
66
+ );
67
+ }
65
68
 
66
- #[test]
67
- fn red_format_latest_result_newline_flattened_and_truncated_80() {
68
- // golden: summary 'line1\nline2' -> '\n'->' ' -> 'line1 line2'
69
- let line = format_status_summary(&json!({
70
- "latest_results": [{"agent_id": "a1", "summary": "line1\nline2", "created_at": Value::Null}]
71
- }));
72
- assert_eq!(
73
- line.lines().nth(4).unwrap(),
74
- "latest result: a1 -> line1 line2 @ -",
75
- "newline in summary MUST flatten to a space (golden .replace('\\n',' '))"
76
- );
77
- // golden: 100-char summary truncated to exactly 80 chars.
78
- let line2 = format_status_summary(&json!({
79
- "latest_results": [{"agent_id": "a1", "summary": "Z".repeat(100), "created_at": Value::Null}]
80
- }));
81
- let latest = line2.lines().nth(4).unwrap();
82
- let kept = latest
83
- .strip_prefix("latest result: a1 -> ")
84
- .unwrap()
85
- .strip_suffix(" @ -")
86
- .unwrap();
87
- assert_eq!(kept.chars().count(), 80, "summary MUST cap at 80 chars (golden [:80])");
88
- assert_eq!(kept, "Z".repeat(80));
89
- }
69
+ #[test]
70
+ fn red_format_latest_result_newline_flattened_and_truncated_80() {
71
+ // golden: summary 'line1\nline2' -> '\n'->' ' -> 'line1 line2'
72
+ let line = format_status_summary(&json!({
73
+ "latest_results": [{"agent_id": "a1", "summary": "line1\nline2", "created_at": Value::Null}]
74
+ }));
75
+ assert_eq!(
76
+ line.lines().nth(4).unwrap(),
77
+ "latest result: a1 -> line1 line2 @ -",
78
+ "newline in summary MUST flatten to a space (golden .replace('\\n',' '))"
79
+ );
80
+ // golden: 100-char summary truncated to exactly 80 chars.
81
+ let line2 = format_status_summary(&json!({
82
+ "latest_results": [{"agent_id": "a1", "summary": "Z".repeat(100), "created_at": Value::Null}]
83
+ }));
84
+ let latest = line2.lines().nth(4).unwrap();
85
+ let kept = latest
86
+ .strip_prefix("latest result: a1 -> ")
87
+ .unwrap()
88
+ .strip_suffix(" @ -")
89
+ .unwrap();
90
+ assert_eq!(
91
+ kept.chars().count(),
92
+ 80,
93
+ "summary MUST cap at 80 chars (golden [:80])"
94
+ );
95
+ assert_eq!(kept, "Z".repeat(80));
96
+ }
90
97
 
91
- #[test]
92
- fn red_format_latest_result_created_at_is_age_text_not_raw_iso() {
93
- // golden: a valid ISO created_at renders as relative age ('Nh ago'), NEVER the raw string.
94
- // (We avoid asserting the exact age — time-dependent — and assert it is NOT verbatim.)
95
- let line = format_status_summary(&json!({
96
- "latest_results": [{"agent_id": "a1", "summary": "done", "created_at": "2020-01-01T00:00:00Z"}]
97
- }));
98
- let latest = line.lines().nth(4).unwrap();
99
- let tail = latest.rsplit(" @ ").next().unwrap();
100
- assert_ne!(
101
- tail, "2020-01-01T00:00:00Z",
102
- "created_at MUST be rendered as age_text (e.g. 'Nh ago'), not the raw ISO string"
103
- );
104
- assert!(
105
- tail.ends_with(" ago"),
106
- "valid ISO created_at renders as a relative age ('... ago'); got: {tail:?}"
107
- );
108
- }
98
+ #[test]
99
+ fn red_format_latest_result_created_at_is_age_text_not_raw_iso() {
100
+ // golden: a valid ISO created_at renders as relative age ('Nh ago'), NEVER the raw string.
101
+ // (We avoid asserting the exact age — time-dependent — and assert it is NOT verbatim.)
102
+ let line = format_status_summary(&json!({
103
+ "latest_results": [{"agent_id": "a1", "summary": "done", "created_at": "2020-01-01T00:00:00Z"}]
104
+ }));
105
+ let latest = line.lines().nth(4).unwrap();
106
+ let tail = latest.rsplit(" @ ").next().unwrap();
107
+ assert_ne!(
108
+ tail, "2020-01-01T00:00:00Z",
109
+ "created_at MUST be rendered as age_text (e.g. 'Nh ago'), not the raw ISO string"
110
+ );
111
+ assert!(
112
+ tail.ends_with(" ago"),
113
+ "valid ISO created_at renders as a relative age ('... ago'); got: {tail:?}"
114
+ );
115
+ }
109
116
 
110
- // ---- #3: format_status_summary — falsy first latest_results element -> "none" ----
111
- // golden (commands.py:268,333-335): latest = (latest_results or [{}])[0] if latest_results else None;
112
- // _latest_result_line returns 'latest result: none' when the first element is falsy (None or {}).
113
- // Rust .first() returns Some for [Null]/[{}] -> renders '- -> - @ -'.
114
- #[test]
115
- fn red_format_status_summary_falsy_first_latest_is_none() {
116
- // golden: latest_results=[None] -> 'latest result: none'
117
- let line_null = format_status_summary(&json!({"latest_results": [Value::Null]}));
118
- assert_eq!(
119
- line_null.lines().nth(4).unwrap(),
120
- "latest result: none",
121
- "a Null first element renders 'latest result: none' (golden falsy guard)"
122
- );
123
- // golden: latest_results=[{}] -> 'latest result: none'
124
- let line_empty = format_status_summary(&json!({"latest_results": [{}]}));
125
- assert_eq!(
126
- line_empty.lines().nth(4).unwrap(),
127
- "latest result: none",
128
- "an empty-object first element renders 'latest result: none' (golden falsy guard)"
129
- );
130
- }
117
+ // ---- #3: format_status_summary — falsy first latest_results element -> "none" ----
118
+ // golden (commands.py:268,333-335): latest = (latest_results or [{}])[0] if latest_results else None;
119
+ // _latest_result_line returns 'latest result: none' when the first element is falsy (None or {}).
120
+ // Rust .first() returns Some for [Null]/[{}] -> renders '- -> - @ -'.
121
+ #[test]
122
+ fn red_format_status_summary_falsy_first_latest_is_none() {
123
+ // golden: latest_results=[None] -> 'latest result: none'
124
+ let line_null = format_status_summary(&json!({"latest_results": [Value::Null]}));
125
+ assert_eq!(
126
+ line_null.lines().nth(4).unwrap(),
127
+ "latest result: none",
128
+ "a Null first element renders 'latest result: none' (golden falsy guard)"
129
+ );
130
+ // golden: latest_results=[{}] -> 'latest result: none'
131
+ let line_empty = format_status_summary(&json!({"latest_results": [{}]}));
132
+ assert_eq!(
133
+ line_empty.lines().nth(4).unwrap(),
134
+ "latest result: none",
135
+ "an empty-object first element renders 'latest result: none' (golden falsy guard)"
136
+ );
137
+ }
131
138
 
132
- // ---- #4: format_status_summary — empty-string falsy fallbacks + current_command ----
133
- // golden: '' is falsy via Python `or`:
134
- // coordinator status '' -> 'stopped' (commands.py:284)
135
- // pane_id '' -> '-' (line 285)
136
- // cmd = pane_current_command or current_command or '-' (line 285) — note the current_command fallback.
137
- // Rust serde unwrap_or keeps '' verbatim and has NO current_command read.
138
- #[test]
139
- fn red_format_status_summary_empty_string_coordinator_status_is_stopped() {
140
- // golden: coordinator.status='' -> 'coordinator: stopped schema_ok=False tmux=False'
141
- let line = format_status_summary(&json!({"coordinator": {"status": ""}}));
142
- assert_eq!(
139
+ // ---- #4: format_status_summary — empty-string falsy fallbacks + current_command ----
140
+ // golden: '' is falsy via Python `or`:
141
+ // coordinator status '' -> 'stopped' (commands.py:284)
142
+ // pane_id '' -> '-' (line 285)
143
+ // cmd = pane_current_command or current_command or '-' (line 285) — note the current_command fallback.
144
+ // Rust serde unwrap_or keeps '' verbatim and has NO current_command read.
145
+ #[test]
146
+ fn red_format_status_summary_empty_string_coordinator_status_is_stopped() {
147
+ // golden: coordinator.status='' -> 'coordinator: stopped schema_ok=False tmux=False'
148
+ let line = format_status_summary(&json!({"coordinator": {"status": ""}}));
149
+ assert_eq!(
143
150
  line.lines().next().unwrap(),
144
151
  "coordinator: stopped schema_ok=false tmux=false",
145
152
  "empty-string coordinator status MUST fall back to 'stopped' (golden `status or 'stopped'`)"
146
153
  );
147
- }
154
+ }
148
155
 
149
- #[test]
150
- fn red_format_status_summary_empty_pane_id_is_dash() {
151
- // golden: pane_id='' -> 'receiver: - cmd=x topology=external'
152
- let line = format_status_summary(&json!({
153
- "leader_receiver": {"pane_id": "", "pane_current_command": "x"}
154
- }));
155
- assert_eq!(
156
- line.lines().nth(1).unwrap(),
157
- "receiver: - cmd=x topology=external",
158
- "empty-string pane_id MUST fall back to '-' (golden `pane_id or '-'`)"
159
- );
160
- }
156
+ #[test]
157
+ fn red_format_status_summary_empty_pane_id_is_dash() {
158
+ // golden: pane_id='' -> 'receiver: - cmd=x topology=external'
159
+ let line = format_status_summary(&json!({
160
+ "leader_receiver": {"pane_id": "", "pane_current_command": "x"}
161
+ }));
162
+ assert_eq!(
163
+ line.lines().nth(1).unwrap(),
164
+ "receiver: - cmd=x topology=external",
165
+ "empty-string pane_id MUST fall back to '-' (golden `pane_id or '-'`)"
166
+ );
167
+ }
161
168
 
162
- #[test]
163
- fn red_format_status_summary_cmd_falls_back_to_current_command() {
164
- // golden: missing pane_current_command + current_command='claude' -> 'receiver: %3 cmd=claude topology=external'
165
- let line_missing = format_status_summary(&json!({
166
- "leader_receiver": {"pane_id": "%3", "current_command": "claude"}
167
- }));
168
- assert_eq!(
169
+ #[test]
170
+ fn red_format_status_summary_cmd_falls_back_to_current_command() {
171
+ // golden: missing pane_current_command + current_command='claude' -> 'receiver: %3 cmd=claude topology=external'
172
+ let line_missing = format_status_summary(&json!({
173
+ "leader_receiver": {"pane_id": "%3", "current_command": "claude"}
174
+ }));
175
+ assert_eq!(
169
176
  line_missing.lines().nth(1).unwrap(),
170
177
  "receiver: %3 cmd=claude topology=external",
171
178
  "cmd MUST fall back to current_command when pane_current_command is absent (golden line 285)"
172
179
  );
173
- // golden: empty pane_current_command + current_command='claude' -> 'receiver: %3 cmd=claude topology=external'
174
- let line_empty = format_status_summary(&json!({
175
- "leader_receiver": {"pane_id": "%3", "pane_current_command": "", "current_command": "claude"}
176
- }));
177
- assert_eq!(
178
- line_empty.lines().nth(1).unwrap(),
179
- "receiver: %3 cmd=claude topology=external",
180
- "empty pane_current_command MUST fall through to current_command (golden falsy `or`)"
181
- );
182
- }
180
+ // golden: empty pane_current_command + current_command='claude' -> 'receiver: %3 cmd=claude topology=external'
181
+ let line_empty = format_status_summary(&json!({
182
+ "leader_receiver": {"pane_id": "%3", "pane_current_command": "", "current_command": "claude"}
183
+ }));
184
+ assert_eq!(
185
+ line_empty.lines().nth(1).unwrap(),
186
+ "receiver: %3 cmd=claude topology=external",
187
+ "empty pane_current_command MUST fall through to current_command (golden falsy `or`)"
188
+ );
189
+ }
183
190
 
184
- // ---- #5 (P2): format_status_summary — Python bool() truthiness coercion ----
185
- // golden: schema_ok / tmux via bool(...) — int 1 -> True, string 'yes' -> True (commands.py:284).
186
- // Rust as_bool() returns None for int/str -> false.
187
- #[test]
188
- fn red_format_status_summary_bool_coercion_truthy_nonbool() {
189
- // golden: schema_ok=1 (int) -> schema_ok=True (printed lowercase 'true' per the upstream re-spell).
190
- let line_int = format_status_summary(&json!({
191
- "coordinator": {"status": "running", "schema_ok": 1}
192
- }));
193
- assert_eq!(
194
- line_int.lines().next().unwrap(),
195
- "coordinator: running schema_ok=true tmux=false",
196
- "int 1 MUST coerce to truthy schema_ok (golden bool(1)==True)"
197
- );
198
- // golden: tmux_session_present='yes' (non-empty string) -> tmux=True.
199
- let line_str = format_status_summary(&json!({"tmux_session_present": "yes"}));
200
- assert_eq!(
201
- line_str.lines().next().unwrap(),
202
- "coordinator: stopped schema_ok=false tmux=true",
203
- "non-empty string 'yes' MUST coerce to truthy tmux (golden bool('yes')==True)"
204
- );
205
- }
191
+ // ---- #5 (P2): format_status_summary — Python bool() truthiness coercion ----
192
+ // golden: schema_ok / tmux via bool(...) — int 1 -> True, string 'yes' -> True (commands.py:284).
193
+ // Rust as_bool() returns None for int/str -> false.
194
+ #[test]
195
+ fn red_format_status_summary_bool_coercion_truthy_nonbool() {
196
+ // golden: schema_ok=1 (int) -> schema_ok=True (printed lowercase 'true' per the upstream re-spell).
197
+ let line_int = format_status_summary(&json!({
198
+ "coordinator": {"status": "running", "schema_ok": 1}
199
+ }));
200
+ assert_eq!(
201
+ line_int.lines().next().unwrap(),
202
+ "coordinator: running schema_ok=true tmux=false",
203
+ "int 1 MUST coerce to truthy schema_ok (golden bool(1)==True)"
204
+ );
205
+ // golden: tmux_session_present='yes' (non-empty string) -> tmux=True.
206
+ let line_str = format_status_summary(&json!({"tmux_session_present": "yes"}));
207
+ assert_eq!(
208
+ line_str.lines().next().unwrap(),
209
+ "coordinator: stopped schema_ok=false tmux=true",
210
+ "non-empty string 'yes' MUST coerce to truthy tmux (golden bool('yes')==True)"
211
+ );
212
+ }
206
213
 
207
- // ---- #6 / #18 / #26: parse_inbox_entries block grouping + title strip + [:80] cap ----
208
- // golden _leader_inbox_entries groups blocks on `[` + 'fallback'; _leader_inbox_entry_title
209
- // strips bracket/'Team Agent'/'Message id:'/'Task id:'/'From:'/'To:'/'Requires ack:'/'Artifacts:'
210
- // lines, joins remaining content with single spaces, [:80] cap.
211
- #[test]
212
- fn red_inbox_realistic_two_message_grouping_and_metadata_strip() {
213
- // golden full summary on a realistic 2-message fallback inbox (metadata + bodies):
214
- let ws = tmp_workspace();
215
- let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
216
- let raw = "[m1 fallback]\nTeam Agent\nMessage id: m1\nFrom: worker-1\nTo: leader\n\
214
+ // ---- #6 / #18 / #26: parse_inbox_entries block grouping + title strip + [:80] cap ----
215
+ // golden _leader_inbox_entries groups blocks on `[` + 'fallback'; _leader_inbox_entry_title
216
+ // strips bracket/'Team Agent'/'Message id:'/'Task id:'/'From:'/'To:'/'Requires ack:'/'Artifacts:'
217
+ // lines, joins remaining content with single spaces, [:80] cap.
218
+ #[test]
219
+ fn red_inbox_realistic_two_message_grouping_and_metadata_strip() {
220
+ // golden full summary on a realistic 2-message fallback inbox (metadata + bodies):
221
+ let ws = tmp_workspace();
222
+ let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
223
+ let raw = "[m1 fallback]\nTeam Agent\nMessage id: m1\nFrom: worker-1\nTo: leader\n\
217
224
  Requires ack: yes\nPlease review the PR and approve the deploy. It is blocking the release.\n\
218
225
  [m2 fallback]\nTeam Agent\nFrom: worker-2\nBuild failed on CI, see logs.";
219
- std::fs::write(&inbox, raw).unwrap();
220
- let summary = consume_leader_inbox_summary(&ws, 500).expect("Some");
221
- let expected = "Leader inbox: 2 new fallback entries\n\
226
+ std::fs::write(&inbox, raw).unwrap();
227
+ let summary = consume_leader_inbox_summary(&ws, 500).expect("Some");
228
+ let expected = "Leader inbox: 2 new fallback entries\n\
222
229
  - Please review the PR and approve the deploy. It is blocking the release.\n\
223
230
  - Build failed on CI, see logs.\n\
224
231
  Hint: team-agent inbox leader";
225
- assert_eq!(
226
- summary, expected,
227
- "golden groups into 2 entries, strips Team Agent/Message id/From/To/Requires ack metadata"
228
- );
229
- let _ = std::fs::remove_dir_all(&ws);
230
- }
232
+ assert_eq!(
233
+ summary, expected,
234
+ "golden groups into 2 entries, strips Team Agent/Message id/From/To/Requires ack metadata"
235
+ );
236
+ let _ = std::fs::remove_dir_all(&ws);
237
+ }
231
238
 
232
- #[test]
233
- fn red_inbox_non_fallback_bracket_stays_in_entry() {
234
- // golden: a `[...]` WITHOUT 'fallback' is content, not a header:
235
- // '[only bracket no kw]\nbody line' -> ONE entry titled '[only bracket no kw] body line'.
236
- let ws = tmp_workspace();
237
- let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
238
- std::fs::write(&inbox, "[only bracket no kw]\nbody line").unwrap();
239
- let summary = consume_leader_inbox_summary(&ws, 500).expect("Some");
240
- assert_eq!(
239
+ #[test]
240
+ fn red_inbox_non_fallback_bracket_stays_in_entry() {
241
+ // golden: a `[...]` WITHOUT 'fallback' is content, not a header:
242
+ // '[only bracket no kw]\nbody line' -> ONE entry titled '[only bracket no kw] body line'.
243
+ let ws = tmp_workspace();
244
+ let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
245
+ std::fs::write(&inbox, "[only bracket no kw]\nbody line").unwrap();
246
+ let summary = consume_leader_inbox_summary(&ws, 500).expect("Some");
247
+ assert_eq!(
241
248
  summary,
242
249
  "Leader inbox: 1 new fallback entry\n- [only bracket no kw] body line\nHint: team-agent inbox leader",
243
250
  "a non-'fallback' bracket line stays part of the entry body (golden grouping gate)"
244
251
  );
245
- let _ = std::fs::remove_dir_all(&ws);
246
- }
252
+ let _ = std::fs::remove_dir_all(&ws);
253
+ }
247
254
 
248
- #[test]
249
- fn red_inbox_plain_lines_join_into_single_entry() {
250
- // golden: no fallback header at all -> the whole text is ONE entry, lines joined w/ spaces.
251
- // 'alpha\nbeta\ngamma' -> 1 entry 'alpha beta gamma'.
252
- let ws = tmp_workspace();
253
- let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
254
- std::fs::write(&inbox, "alpha\nbeta\ngamma").unwrap();
255
- let summary = consume_leader_inbox_summary(&ws, 500).expect("Some");
256
- assert_eq!(
257
- summary,
258
- "Leader inbox: 1 new fallback entry\n- alpha beta gamma\nHint: team-agent inbox leader",
259
- "with no fallback header golden collapses ALL lines into one space-joined entry"
260
- );
261
- let _ = std::fs::remove_dir_all(&ws);
262
- }
255
+ #[test]
256
+ fn red_inbox_plain_lines_join_into_single_entry() {
257
+ // golden: no fallback header at all -> the whole text is ONE entry, lines joined w/ spaces.
258
+ // 'alpha\nbeta\ngamma' -> 1 entry 'alpha beta gamma'.
259
+ let ws = tmp_workspace();
260
+ let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
261
+ std::fs::write(&inbox, "alpha\nbeta\ngamma").unwrap();
262
+ let summary = consume_leader_inbox_summary(&ws, 500).expect("Some");
263
+ assert_eq!(
264
+ summary,
265
+ "Leader inbox: 1 new fallback entry\n- alpha beta gamma\nHint: team-agent inbox leader",
266
+ "with no fallback header golden collapses ALL lines into one space-joined entry"
267
+ );
268
+ let _ = std::fs::remove_dir_all(&ws);
269
+ }
263
270
 
264
- #[test]
265
- fn red_inbox_title_capped_at_80_chars() {
266
- // golden: a 200-char body title is capped to exactly 80 chars ([:80] per entry).
267
- let ws = tmp_workspace();
268
- let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
269
- let body = "X".repeat(200);
270
- std::fs::write(&inbox, format!("[x fallback]\n{body}")).unwrap();
271
- let summary = consume_leader_inbox_summary(&ws, 500).expect("Some");
272
- assert_eq!(
273
- summary,
274
- format!(
275
- "Leader inbox: 1 new fallback entry\n- {}\nHint: team-agent inbox leader",
276
- "X".repeat(80)
277
- ),
278
- "each entry title MUST cap at 80 chars (golden [:80]); Rust applies no per-entry cap"
279
- );
280
- let _ = std::fs::remove_dir_all(&ws);
281
- }
271
+ #[test]
272
+ fn red_inbox_title_capped_at_80_chars() {
273
+ // golden: a 200-char body title is capped to exactly 80 chars ([:80] per entry).
274
+ let ws = tmp_workspace();
275
+ let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
276
+ let body = "X".repeat(200);
277
+ std::fs::write(&inbox, format!("[x fallback]\n{body}")).unwrap();
278
+ let summary = consume_leader_inbox_summary(&ws, 500).expect("Some");
279
+ assert_eq!(
280
+ summary,
281
+ format!(
282
+ "Leader inbox: 1 new fallback entry\n- {}\nHint: team-agent inbox leader",
283
+ "X".repeat(80)
284
+ ),
285
+ "each entry title MUST cap at 80 chars (golden [:80]); Rust applies no per-entry cap"
286
+ );
287
+ let _ = std::fs::remove_dir_all(&ws);
288
+ }
282
289
 
283
- // ---- #7 / #16: consume_leader_inbox_summary — mid-codepoint cursor must NOT panic ----
284
- // golden seeks to a BYTE offset and decodes errors='replace': a cursor inside a multibyte
285
- // char yields U+FFFD replacement chars and NEVER crashes (bug-084).
286
- // Rust slices &text[offset..] which PANICS on a non-char-boundary. The panic IS the red.
287
- #[test]
288
- fn red_inbox_mid_codepoint_cursor_decodes_replacement_no_panic() {
289
- // golden probe (/tmp/probe_cli_all.py): inbox '[a fallback]\n世界 message', cursor='14'
290
- // (byte 14 is inside '世', whose bytes are 13..16) ->
291
- // 'Leader inbox: 1 new fallback entry\n- ��界 message\nHint: team-agent inbox leader'
292
- let ws = tmp_workspace();
293
- let runtime = ws.join(".team").join("runtime");
294
- std::fs::write(runtime.join("leader-inbox.log"), "[a fallback]\n世界 message").unwrap();
295
- std::fs::write(runtime.join("leader-inbox.cursor"), "14").unwrap();
296
- let summary = consume_leader_inbox_summary(&ws, 500)
297
- .expect("mid-codepoint cursor MUST degrade gracefully, not crash");
298
- assert_eq!(
290
+ // ---- #7 / #16: consume_leader_inbox_summary — mid-codepoint cursor must NOT panic ----
291
+ // golden seeks to a BYTE offset and decodes errors='replace': a cursor inside a multibyte
292
+ // char yields U+FFFD replacement chars and NEVER crashes (bug-084).
293
+ // Rust slices &text[offset..] which PANICS on a non-char-boundary. The panic IS the red.
294
+ #[test]
295
+ fn red_inbox_mid_codepoint_cursor_decodes_replacement_no_panic() {
296
+ // golden probe (/tmp/probe_cli_all.py): inbox '[a fallback]\n世界 message', cursor='14'
297
+ // (byte 14 is inside '世', whose bytes are 13..16) ->
298
+ // 'Leader inbox: 1 new fallback entry\n- ��界 message\nHint: team-agent inbox leader'
299
+ let ws = tmp_workspace();
300
+ let runtime = ws.join(".team").join("runtime");
301
+ std::fs::write(
302
+ runtime.join("leader-inbox.log"),
303
+ "[a fallback]\n世界 message",
304
+ )
305
+ .unwrap();
306
+ std::fs::write(runtime.join("leader-inbox.cursor"), "14").unwrap();
307
+ let summary = consume_leader_inbox_summary(&ws, 500)
308
+ .expect("mid-codepoint cursor MUST degrade gracefully, not crash");
309
+ assert_eq!(
299
310
  summary,
300
311
  "Leader inbox: 1 new fallback entry\n- \u{FFFD}\u{FFFD}界 message\nHint: team-agent inbox leader",
301
312
  "mid-codepoint byte offset MUST yield U+FFFD replacement chars (golden errors='replace')"
302
313
  );
303
- let _ = std::fs::remove_dir_all(&ws);
304
- }
314
+ let _ = std::fs::remove_dir_all(&ws);
315
+ }
305
316
 
306
- // ---- #8 / #17: consume_leader_inbox_summary — offset>size resets to 0; garbage cursor ----
307
- // golden helpers.py:38-40: offset<0 or offset>size -> offset=0 (re-read whole file);
308
- // a ValueError cursor ('abc') -> offset=0 AND size=0 -> offset==size -> None WITHOUT advancing.
309
- #[test]
310
- fn red_inbox_beyond_size_cursor_resets_to_zero_and_resummarizes() {
311
- // golden: file 'hello' inbox, cursor='99999' (> size) -> re-read from 0 -> summary; cursor advances.
312
- let ws = tmp_workspace();
313
- let runtime = ws.join(".team").join("runtime");
314
- std::fs::write(runtime.join("leader-inbox.log"), "[a fallback]\nhello").unwrap();
315
- std::fs::write(runtime.join("leader-inbox.cursor"), "99999").unwrap();
316
- let summary = consume_leader_inbox_summary(&ws, 500)
317
- .expect("over-size cursor MUST reset to 0 and re-summarize (golden offset>size => 0)");
318
- assert_eq!(
319
- summary,
320
- "Leader inbox: 1 new fallback entry\n- hello\nHint: team-agent inbox leader",
321
- "cursor beyond file size MUST re-read the whole inbox (NOT clamp-to-len-then-None)"
322
- );
323
- let _ = std::fs::remove_dir_all(&ws);
324
- }
317
+ // ---- #8 / #17: consume_leader_inbox_summary — offset>size resets to 0; garbage cursor ----
318
+ // golden helpers.py:38-40: offset<0 or offset>size -> offset=0 (re-read whole file);
319
+ // a ValueError cursor ('abc') -> offset=0 AND size=0 -> offset==size -> None WITHOUT advancing.
320
+ #[test]
321
+ fn red_inbox_beyond_size_cursor_resets_to_zero_and_resummarizes() {
322
+ // golden: file 'hello' inbox, cursor='99999' (> size) -> re-read from 0 -> summary; cursor advances.
323
+ let ws = tmp_workspace();
324
+ let runtime = ws.join(".team").join("runtime");
325
+ std::fs::write(runtime.join("leader-inbox.log"), "[a fallback]\nhello").unwrap();
326
+ std::fs::write(runtime.join("leader-inbox.cursor"), "99999").unwrap();
327
+ let summary = consume_leader_inbox_summary(&ws, 500)
328
+ .expect("over-size cursor MUST reset to 0 and re-summarize (golden offset>size => 0)");
329
+ assert_eq!(
330
+ summary, "Leader inbox: 1 new fallback entry\n- hello\nHint: team-agent inbox leader",
331
+ "cursor beyond file size MUST re-read the whole inbox (NOT clamp-to-len-then-None)"
332
+ );
333
+ let _ = std::fs::remove_dir_all(&ws);
334
+ }
325
335
 
326
- #[test]
327
- fn red_inbox_garbage_cursor_returns_none_without_advancing() {
328
- // golden: cursor='abc' (ValueError) -> offset=0,size=0 -> offset==size -> None; cursor LEFT 'abc'.
329
- let ws = tmp_workspace();
330
- let runtime = ws.join(".team").join("runtime");
331
- std::fs::write(runtime.join("leader-inbox.log"), "[a fallback]\nhello").unwrap();
332
- let cursor_path = runtime.join("leader-inbox.cursor");
333
- std::fs::write(&cursor_path, "abc").unwrap();
334
- let result = consume_leader_inbox_summary(&ws, 500);
335
- assert_eq!(
336
+ #[test]
337
+ fn red_inbox_garbage_cursor_returns_none_without_advancing() {
338
+ // golden: cursor='abc' (ValueError) -> offset=0,size=0 -> offset==size -> None; cursor LEFT 'abc'.
339
+ let ws = tmp_workspace();
340
+ let runtime = ws.join(".team").join("runtime");
341
+ std::fs::write(runtime.join("leader-inbox.log"), "[a fallback]\nhello").unwrap();
342
+ let cursor_path = runtime.join("leader-inbox.cursor");
343
+ std::fs::write(&cursor_path, "abc").unwrap();
344
+ let result = consume_leader_inbox_summary(&ws, 500);
345
+ assert_eq!(
336
346
  result, None,
337
347
  "an unparseable cursor MUST treat size as 0 (offset==size==0) and return None (golden ValueError)"
338
348
  );
339
- let cursor_after = std::fs::read_to_string(&cursor_path).unwrap();
340
- assert_eq!(
349
+ let cursor_after = std::fs::read_to_string(&cursor_path).unwrap();
350
+ assert_eq!(
341
351
  cursor_after, "abc",
342
352
  "a garbage cursor MUST be left untouched (golden never advances it); Rust overwrites to len"
343
353
  );
344
- let _ = std::fs::remove_dir_all(&ws);
345
- }
354
+ let _ = std::fs::remove_dir_all(&ws);
355
+ }
346
356
 
347
- // ---- #9: render_inbox_summary — budget measured in CHARS (code points), not bytes ----
348
- // golden uses Python str length (code points). Rust compares byte lengths.
349
- #[test]
350
- fn red_inbox_budget_is_char_count_not_bytes() {
351
- // golden probe: 30 CJK chars, budget=100 -> char-len 97 <= 100 so golden KEEPS the full title.
352
- let ws = tmp_workspace();
353
- let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
354
- std::fs::write(&inbox, format!("[x fallback]\n{}", "漢".repeat(30))).unwrap();
355
- let summary = consume_leader_inbox_summary(&ws, 100).expect("Some");
356
- assert_eq!(
357
- summary,
358
- format!(
359
- "Leader inbox: 1 new fallback entry\n- {}\nHint: team-agent inbox leader",
360
- "漢".repeat(30)
361
- ),
362
- "budget MUST count code points (97 chars <= 100), not bytes; Rust byte-len drops it"
363
- );
364
- assert!(
365
- !summary.contains("Truncated"),
366
- "the CJK title fits the char budget and MUST NOT be truncated (golden char-len semantics)"
367
- );
368
- let _ = std::fs::remove_dir_all(&ws);
369
- }
357
+ // ---- #9: render_inbox_summary — budget measured in CHARS (code points), not bytes ----
358
+ // golden uses Python str length (code points). Rust compares byte lengths.
359
+ #[test]
360
+ fn red_inbox_budget_is_char_count_not_bytes() {
361
+ // golden probe: 30 CJK chars, budget=100 -> char-len 97 <= 100 so golden KEEPS the full title.
362
+ let ws = tmp_workspace();
363
+ let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
364
+ std::fs::write(&inbox, format!("[x fallback]\n{}", "漢".repeat(30))).unwrap();
365
+ let summary = consume_leader_inbox_summary(&ws, 100).expect("Some");
366
+ assert_eq!(
367
+ summary,
368
+ format!(
369
+ "Leader inbox: 1 new fallback entry\n- {}\nHint: team-agent inbox leader",
370
+ "漢".repeat(30)
371
+ ),
372
+ "budget MUST count code points (97 chars <= 100), not bytes; Rust byte-len drops it"
373
+ );
374
+ assert!(
375
+ !summary.contains("Truncated"),
376
+ "the CJK title fits the char budget and MUST NOT be truncated (golden char-len semantics)"
377
+ );
378
+ let _ = std::fs::remove_dir_all(&ws);
379
+ }
370
380
 
371
- // ---- #10: render_inbox_summary — hard-trim when header+footer already exceed budget ----
372
- // golden post-assembly: if len(summary) > budget, body='\n'.join(lines)[:keep].rstrip()
373
- // with keep=max(0,budget-len(footer)-6), then `{body} ...\n{footer}` — even the HEADER is trimmed.
374
- #[test]
375
- fn red_inbox_small_budget_hard_trims_header() {
376
- // golden probe: budget=80 on 10 entries -> 'Lea ...\nTruncated: ...'
377
- let ws = tmp_workspace();
378
- let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
379
- let many = (0..10)
380
- .map(|i| format!("[e{i} fallback]\nMessage number {i} with some text padding here"))
381
- .collect::<Vec<_>>()
382
- .join("\n");
383
- std::fs::write(&inbox, &many).unwrap();
384
- let summary = consume_leader_inbox_summary(&ws, 80).expect("Some");
385
- assert_eq!(
386
- summary,
387
- "Lea ...\nTruncated: more fallback entries available; run team-agent inbox leader",
388
- "when header+footer alone exceed budget the body (incl header) MUST hard-trim (golden)"
389
- );
390
- let _ = std::fs::remove_dir_all(&ws);
391
- }
381
+ // ---- #10: render_inbox_summary — hard-trim when header+footer already exceed budget ----
382
+ // golden post-assembly: if len(summary) > budget, body='\n'.join(lines)[:keep].rstrip()
383
+ // with keep=max(0,budget-len(footer)-6), then `{body} ...\n{footer}` — even the HEADER is trimmed.
384
+ #[test]
385
+ fn red_inbox_small_budget_hard_trims_header() {
386
+ // golden probe: budget=80 on 10 entries -> 'Lea ...\nTruncated: ...'
387
+ let ws = tmp_workspace();
388
+ let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
389
+ let many = (0..10)
390
+ .map(|i| format!("[e{i} fallback]\nMessage number {i} with some text padding here"))
391
+ .collect::<Vec<_>>()
392
+ .join("\n");
393
+ std::fs::write(&inbox, &many).unwrap();
394
+ let summary = consume_leader_inbox_summary(&ws, 80).expect("Some");
395
+ assert_eq!(
396
+ summary, "Lea ...\nTruncated: more fallback entries available; run team-agent inbox leader",
397
+ "when header+footer alone exceed budget the body (incl header) MUST hard-trim (golden)"
398
+ );
399
+ let _ = std::fs::remove_dir_all(&ws);
400
+ }
392
401
 
393
- // ---- #19 / #25: emit human-dict scalar formatting (None/True/False) + nested string quotes ----
394
- // golden emit (helpers.py:16-21): scalar via Python str() -> None->'None', True->'True', False->'False';
395
- // dict/list via json.dumps(ensure_ascii=False) -> string ELEMENTS keep their double quotes.
396
- #[test]
397
- fn red_emit_human_dict_scalar_none_true_false_and_quoted_strings() {
398
- // golden probe: emit({"k":{"a":1,"b":[1,2]},"s":"hi","u":"世界","n":None,"f":False,"t":True}, False)
399
- let out = emit(
400
- &CmdOutput::Json(json!({
401
- "k": {"a": 1, "b": [1, 2]},
402
- "s": "hi",
403
- "u": "世界",
404
- "n": Value::Null,
405
- "f": false,
406
- "t": true,
407
- })),
408
- false,
409
- )
410
- .expect("dict human emit returns Some");
411
- assert_eq!(
412
- out,
413
- "k: {\"a\": 1, \"b\": [1, 2]}\ns: hi\nu: 世界\nn: None\nf: False\nt: True",
414
- "top-level scalar Null/false/true MUST render Python-str 'None'/'False'/'True' (golden)"
415
- );
416
- }
402
+ // ---- #19 / #25: emit human-dict scalar formatting (None/True/False) + nested string quotes ----
403
+ // golden emit (helpers.py:16-21): scalar via Python str() -> None->'None', True->'True', False->'False';
404
+ // dict/list via json.dumps(ensure_ascii=False) -> string ELEMENTS keep their double quotes.
405
+ #[test]
406
+ fn red_emit_human_dict_scalar_none_true_false_and_quoted_strings() {
407
+ // golden probe: emit({"k":{"a":1,"b":[1,2]},"s":"hi","u":"世界","n":None,"f":False,"t":True}, False)
408
+ let out = emit(
409
+ &CmdOutput::Json(json!({
410
+ "k": {"a": 1, "b": [1, 2]},
411
+ "s": "hi",
412
+ "u": "世界",
413
+ "n": Value::Null,
414
+ "f": false,
415
+ "t": true,
416
+ })),
417
+ false,
418
+ )
419
+ .expect("dict human emit returns Some");
420
+ assert_eq!(
421
+ out, "k: {\"a\": 1, \"b\": [1, 2]}\ns: hi\nu: 世界\nn: None\nf: False\nt: True",
422
+ "top-level scalar Null/false/true MUST render Python-str 'None'/'False'/'True' (golden)"
423
+ );
424
+ }
417
425
 
418
- #[test]
419
- fn red_emit_human_dict_string_elements_keep_quotes_in_collections() {
420
- // golden probe: emit({"items":["a","b"],"mixed":["x",1,True]}, False)
421
- // -> 'items: ["a", "b"]\nmixed: ["x", 1, true]' (string elements KEEP double quotes;
422
- // bool lowercased to json 'true' INSIDE the json.dumps collection).
423
- let out = emit(
424
- &CmdOutput::Json(json!({"items": ["a", "b"], "mixed": ["x", 1, true]})),
425
- false,
426
- )
427
- .expect("dict human emit returns Some");
428
- assert_eq!(
429
- out,
430
- "items: [\"a\", \"b\"]\nmixed: [\"x\", 1, true]",
431
- "string elements nested in a list MUST keep their double quotes (golden json.dumps)"
432
- );
433
- }
426
+ #[test]
427
+ fn red_emit_human_dict_string_elements_keep_quotes_in_collections() {
428
+ // golden probe: emit({"items":["a","b"],"mixed":["x",1,True]}, False)
429
+ // -> 'items: ["a", "b"]\nmixed: ["x", 1, true]' (string elements KEEP double quotes;
430
+ // bool lowercased to json 'true' INSIDE the json.dumps collection).
431
+ let out = emit(
432
+ &CmdOutput::Json(json!({"items": ["a", "b"], "mixed": ["x", 1, true]})),
433
+ false,
434
+ )
435
+ .expect("dict human emit returns Some");
436
+ assert_eq!(
437
+ out, "items: [\"a\", \"b\"]\nmixed: [\"x\", 1, true]",
438
+ "string elements nested in a list MUST keep their double quotes (golden json.dumps)"
439
+ );
440
+ }
434
441
 
435
- // ---- #20: send_target None routes to assignee, NEVER broadcast ----
436
- // golden _send_target returns None for a no-target send; send_message(target=None) routes to
437
- // the task assignee / leader receiver — '*' is the ONLY broadcast trigger.
438
- // Rust maps None => MessageTarget::Broadcast (WRONG recipient set).
439
- #[test]
440
- fn red_send_target_none_is_not_broadcast() {
441
- // golden: _send_target(targets=None, target=None) => None (single/assignee routing, NOT broadcast).
442
- let got = send_target(None, None);
443
- assert_ne!(
442
+ // ---- #20: send_target None routes to assignee, NEVER broadcast ----
443
+ // golden _send_target returns None for a no-target send; send_message(target=None) routes to
444
+ // the task assignee / leader receiver — '*' is the ONLY broadcast trigger.
445
+ // Rust maps None => MessageTarget::Broadcast (WRONG recipient set).
446
+ #[test]
447
+ fn red_send_target_none_is_not_broadcast() {
448
+ // golden: _send_target(targets=None, target=None) => None (single/assignee routing, NOT broadcast).
449
+ let got = send_target(None, None);
450
+ assert_ne!(
444
451
  got,
445
452
  MessageTarget::Broadcast,
446
453
  "a no-target send MUST NOT broadcast to the whole team; golden routes to the assignee/leader. \
447
454
  '*' is the only broadcast trigger."
448
455
  );
449
- // '*' remains the broadcast trigger (unchanged invariant).
450
- assert_eq!(send_target(None, Some("*")), MessageTarget::Broadcast);
451
- }
456
+ // '*' remains the broadcast trigger (unchanged invariant).
457
+ assert_eq!(send_target(None, Some("*")), MessageTarget::Broadcast);
458
+ }
452
459
 
453
- // ---- #23: cmd_doctor comms (human) returns COMMS_BOUNDARY_TEXT + sorted indented JSON ----
454
- // golden: for --comms WITHOUT --json, cmd_doctor returns the STRING
455
- // f"{COMMS_BOUNDARY_TEXT}\n{json.dumps(result, indent=2, ensure_ascii=False, sort_keys=True)}".
456
- // Rust always does CmdResult::from_json -> CmdOutput::Json (wrong shape).
457
- #[test]
458
- fn red_cmd_doctor_comms_human_is_boundary_text_plus_sorted_json() {
459
- const COMMS_BOUNDARY_TEXT: &str = "validates live pane binding consistency and zero-token comms contracts. Does NOT perform live runtime message round-trip. (zero token, zero pollution)";
460
- let args = DoctorArgs {
461
- spec: None,
462
- workspace: PathBuf::from("."),
463
- gate: None,
464
- comms: true,
465
- team: None,
466
- fix: false,
467
- fix_schema: false,
468
- cleanup_orphans: false,
469
- confirm: false,
470
- json: false,
471
- };
472
- let result = cmd_doctor(&args).expect("comms doctor returns CmdResult");
473
- let text = match result.output {
474
- CmdOutput::Human(s) => s,
475
- other => panic!(
476
- "comms WITHOUT --json MUST be a Human boundary-text + JSON string, got {other:?}"
477
- ),
478
- };
479
- assert!(
460
+ // ---- #23: cmd_doctor comms (human) returns COMMS_BOUNDARY_TEXT + sorted indented JSON ----
461
+ // golden: for --comms WITHOUT --json, cmd_doctor returns the STRING
462
+ // f"{COMMS_BOUNDARY_TEXT}\n{json.dumps(result, indent=2, ensure_ascii=False, sort_keys=True)}".
463
+ // Rust always does CmdResult::from_json -> CmdOutput::Json (wrong shape).
464
+ #[test]
465
+ fn red_cmd_doctor_comms_human_is_boundary_text_plus_sorted_json() {
466
+ const COMMS_BOUNDARY_TEXT: &str = "validates live pane binding consistency and zero-token comms contracts. Does NOT perform live runtime message round-trip. (zero token, zero pollution)";
467
+ let args = DoctorArgs {
468
+ spec: None,
469
+ workspace: PathBuf::from("."),
470
+ gate: None,
471
+ comms: true,
472
+ team: None,
473
+ fix: false,
474
+ fix_schema: false,
475
+ cleanup_orphans: false,
476
+ confirm: false,
477
+ json: false,
478
+ };
479
+ let result = cmd_doctor(&args).expect("comms doctor returns CmdResult");
480
+ let text = match result.output {
481
+ CmdOutput::Human(s) => s,
482
+ other => panic!(
483
+ "comms WITHOUT --json MUST be a Human boundary-text + JSON string, got {other:?}"
484
+ ),
485
+ };
486
+ assert!(
480
487
  text.starts_with(&format!("{COMMS_BOUNDARY_TEXT}\n")),
481
488
  "comms human output MUST start with COMMS_BOUNDARY_TEXT then a newline (golden commands.py:231); got: {text:?}"
482
489
  );
483
- // the tail is the selftest result rendered as sort_keys+indent=2 JSON (parseable, sorted).
484
- let json_tail = text.strip_prefix(&format!("{COMMS_BOUNDARY_TEXT}\n")).unwrap();
485
- let parsed: Value = serde_json::from_str(json_tail)
486
- .expect("comms human tail MUST be indent=2 sort_keys JSON of the selftest result");
487
- assert!(parsed.is_object(), "comms selftest JSON tail is an object");
488
- }
490
+ // the tail is the selftest result rendered as sort_keys+indent=2 JSON (parseable, sorted).
491
+ let json_tail = text
492
+ .strip_prefix(&format!("{COMMS_BOUNDARY_TEXT}\n"))
493
+ .unwrap();
494
+ let parsed: Value = serde_json::from_str(json_tail)
495
+ .expect("comms human tail MUST be indent=2 sort_keys JSON of the selftest result");
496
+ assert!(parsed.is_object(), "comms selftest JSON tail is an object");
497
+ }
489
498
 
490
- // ---- #13 / #27 (P2): run() must NOT treat 'claude_code' as a passthrough trigger ----
491
- // golden parser.py:86: only raw_argv[0] in {'codex','claude'} triggers leader passthrough.
492
- // 'claude_code' is the internal provider name, NOT a CLI subcommand (argparse would reject it).
493
- // Rust run() (cli.rs:1305) adds a 'claude_code' arm.
494
- #[test]
495
- fn red_run_claude_code_is_not_a_passthrough_trigger() {
496
- // golden: `team-agent claude_code -h` is an invalid choice (NOT a clean passthrough exit).
497
- // Rust currently routes it to cmd_leader_passthrough("claude_code",["-h"]) -> CmdResult::none()
498
- // -> ExitCode::Ok. Golden would NOT treat it as a valid leader passthrough.
499
- let exit = run(&["claude_code".to_string(), "-h".to_string()], Path::new("."));
500
- assert_ne!(
499
+ // ---- #13 / #27 (P2): run() must NOT treat 'claude_code' as a passthrough trigger ----
500
+ // golden parser.py:86: only raw_argv[0] in {'codex','claude'} triggers leader passthrough.
501
+ // 'claude_code' is the internal provider name, NOT a CLI subcommand (argparse would reject it).
502
+ // Rust run() (cli.rs:1305) adds a 'claude_code' arm.
503
+ #[test]
504
+ fn red_run_claude_code_is_not_a_passthrough_trigger() {
505
+ // golden: `team-agent claude_code -h` is an invalid choice (NOT a clean passthrough exit).
506
+ // Rust currently routes it to cmd_leader_passthrough("claude_code",["-h"]) -> CmdResult::none()
507
+ // -> ExitCode::Ok. Golden would NOT treat it as a valid leader passthrough.
508
+ let exit = run(
509
+ &["claude_code".to_string(), "-h".to_string()],
510
+ Path::new("."),
511
+ );
512
+ assert_ne!(
501
513
  exit,
502
514
  ExitCode::Ok,
503
515
  "'claude_code' MUST NOT be a leader passthrough trigger (golden gate is {{codex,claude}} only)"
504
516
  );
505
- // codex/claude REMAIN valid passthrough triggers (the -h fast path returns Ok).
506
- assert_eq!(run(&["codex".to_string(), "-h".to_string()], Path::new(".")), ExitCode::Ok);
507
- assert_eq!(run(&["claude".to_string(), "-h".to_string()], Path::new(".")), ExitCode::Ok);
508
- }
517
+ // codex/claude REMAIN valid passthrough triggers (the -h fast path returns Ok).
518
+ assert_eq!(
519
+ run(&["codex".to_string(), "-h".to_string()], Path::new(".")),
520
+ ExitCode::Ok
521
+ );
522
+ assert_eq!(
523
+ run(&["claude".to_string(), "-h".to_string()], Path::new(".")),
524
+ ExitCode::Ok
525
+ );
526
+ }