@team-agent/installer 0.5.41 → 0.5.43

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 (152) 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 +27 -7
  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 +47 -52
  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/coordinator/tick.rs +19 -1
  51. package/crates/team-agent/src/db/message_store.rs +138 -30
  52. package/crates/team-agent/src/db/migration.rs +249 -61
  53. package/crates/team-agent/src/db/schema.rs +303 -82
  54. package/crates/team-agent/src/diagnose/comms.rs +9 -2
  55. package/crates/team-agent/src/diagnose/mod.rs +1 -3
  56. package/crates/team-agent/src/diagnose/orphans.rs +79 -61
  57. package/crates/team-agent/src/event_log.rs +70 -16
  58. package/crates/team-agent/src/layout/manager.rs +15 -4
  59. package/crates/team-agent/src/layout/mod.rs +4 -4
  60. package/crates/team-agent/src/layout/overlay.rs +10 -3
  61. package/crates/team-agent/src/layout/placement.rs +5 -1
  62. package/crates/team-agent/src/layout/recovery.rs +4 -2
  63. package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
  64. package/crates/team-agent/src/layout/sessions.rs +17 -9
  65. package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
  66. package/crates/team-agent/src/layout/worker_env.rs +52 -19
  67. package/crates/team-agent/src/leader/helpers.rs +7 -1
  68. package/crates/team-agent/src/leader/lease.rs +195 -82
  69. package/crates/team-agent/src/leader/owner_bind.rs +55 -22
  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/start.rs +75 -54
  73. package/crates/team-agent/src/leader/takeover.rs +46 -11
  74. package/crates/team-agent/src/leader/tests/basics.rs +320 -167
  75. package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
  76. package/crates/team-agent/src/leader/tests/identity.rs +428 -356
  77. package/crates/team-agent/src/leader/tests/idle.rs +285 -254
  78. package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
  79. package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
  80. package/crates/team-agent/src/leader/tests/mod.rs +115 -99
  81. package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
  82. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
  83. package/crates/team-agent/src/lib.rs +4 -4
  84. package/crates/team-agent/src/lifecycle/display.rs +7 -3
  85. package/crates/team-agent/src/lifecycle/launch.rs +6 -1
  86. package/crates/team-agent/src/lifecycle/mod.rs +9 -1
  87. package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
  88. package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
  89. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
  90. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
  91. package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
  92. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
  93. package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
  94. package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
  95. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
  96. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
  97. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
  98. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
  99. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
  100. package/crates/team-agent/src/lifecycle/tests.rs +2 -2
  101. package/crates/team-agent/src/main.rs +4 -4
  102. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
  103. package/crates/team-agent/src/mcp_server/mod.rs +11 -2
  104. package/crates/team-agent/src/mcp_server/types.rs +14 -3
  105. package/crates/team-agent/src/mcp_server/wire.rs +230 -68
  106. package/crates/team-agent/src/messaging/delivery.rs +20 -18
  107. package/crates/team-agent/src/messaging/helpers.rs +25 -4
  108. package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
  109. package/crates/team-agent/src/messaging/mod.rs +2 -3
  110. package/crates/team-agent/src/messaging/selftest.rs +46 -26
  111. package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
  112. package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
  113. package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
  114. package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
  115. package/crates/team-agent/src/messaging/trust.rs +25 -2
  116. package/crates/team-agent/src/messaging/watchers.rs +37 -9
  117. package/crates/team-agent/src/model/enums.rs +65 -16
  118. package/crates/team-agent/src/model/ids.rs +12 -3
  119. package/crates/team-agent/src/model/paths.rs +28 -7
  120. package/crates/team-agent/src/model/permissions.rs +176 -33
  121. package/crates/team-agent/src/model/routing.rs +66 -20
  122. package/crates/team-agent/src/model/spec.rs +365 -69
  123. package/crates/team-agent/src/model/task_graph.rs +36 -9
  124. package/crates/team-agent/src/model/yaml/tests.rs +24 -6
  125. package/crates/team-agent/src/model/yaml.rs +7 -6
  126. package/crates/team-agent/src/packaging/install.rs +23 -9
  127. package/crates/team-agent/src/packaging/migrate.rs +5 -7
  128. package/crates/team-agent/src/packaging/mod.rs +9 -1
  129. package/crates/team-agent/src/packaging/repair.rs +13 -6
  130. package/crates/team-agent/src/packaging/tests.rs +63 -16
  131. package/crates/team-agent/src/packaging/types.rs +22 -7
  132. package/crates/team-agent/src/platform/argv.rs +4 -1
  133. package/crates/team-agent/src/platform/file_lock.rs +22 -8
  134. package/crates/team-agent/src/platform/process.rs +21 -22
  135. package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
  136. package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
  137. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
  138. package/crates/team-agent/src/provider/classify.rs +127 -42
  139. package/crates/team-agent/src/provider/faults.rs +17 -5
  140. package/crates/team-agent/src/provider/helpers.rs +6 -5
  141. package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
  142. package/crates/team-agent/src/state/repository.rs +27 -14
  143. package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
  144. package/crates/team-agent/src/tmux_backend.rs +80 -44
  145. package/crates/team-agent/src/topology.rs +27 -20
  146. package/crates/team-agent/src/transport/test_support.rs +20 -23
  147. package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
  148. package/crates/team-agent/src/transport/tests/mod.rs +178 -187
  149. package/crates/team-agent/src/transport/tests/wire.rs +561 -525
  150. package/crates/team-agent/src/transport.rs +12 -17
  151. package/crates/team-agent/src/transport_factory.rs +29 -14
  152. package/package.json +4 -4
@@ -1,71 +1,71 @@
1
1
  use super::*;
2
2
 
3
- // =========================================================================
4
- // WAVE-2 LANE C — diagnose/status byte-parity DIVERGENCES + target-scan wiring.
5
- // The status/diagnose handlers are IMPLEMENTED (status) or STUBBED (approvals/inbox/doctor/
6
- // comms_selftest/orphan_gate/cleanup_orphans/fix_schema) — these REDs lock the EXACT golden JSON
7
- // shapes (keys/values/insertion-order) each one DIVERGES from. Golden bytes captured via
8
- // PYTHONPATH=.../src python3 /tmp/lanec_probe*.py on a fresh workspace.
9
- // =========================================================================
3
+ // =========================================================================
4
+ // WAVE-2 LANE C — diagnose/status byte-parity DIVERGENCES + target-scan wiring.
5
+ // The status/diagnose handlers are IMPLEMENTED (status) or STUBBED (approvals/inbox/doctor/
6
+ // comms_selftest/orphan_gate/cleanup_orphans/fix_schema) — these REDs lock the EXACT golden JSON
7
+ // shapes (keys/values/insertion-order) each one DIVERGES from. Golden bytes captured via
8
+ // PYTHONPATH=.../src python3 /tmp/lanec_probe*.py on a fresh workspace.
9
+ // =========================================================================
10
10
 
11
- /// Seed `.team/runtime/state.json` with `state` so `status_port::status` reads a real fixture.
12
- fn seed_status_state(state: serde_json::Value) -> std::path::PathBuf {
13
- let ws = tmp_workspace();
14
- std::fs::write(
15
- ws.join(".team").join("runtime").join("state.json"),
16
- serde_json::to_vec_pretty(&state).unwrap(),
17
- )
18
- .unwrap();
19
- ws
20
- }
11
+ /// Seed `.team/runtime/state.json` with `state` so `status_port::status` reads a real fixture.
12
+ fn seed_status_state(state: serde_json::Value) -> std::path::PathBuf {
13
+ let ws = tmp_workspace();
14
+ std::fs::write(
15
+ ws.join(".team").join("runtime").join("state.json"),
16
+ serde_json::to_vec_pretty(&state).unwrap(),
17
+ )
18
+ .unwrap();
19
+ ws
20
+ }
21
21
 
22
- // ── status #1: `team` = state["leader"]["id"] (default "leader"), NOT state["team"] ──────────────
23
- // GOLDEN queries.py:66 `state.get("leader", {}).get("id", "leader")`. RUST mod.rs:94 reads
24
- // `state.get("team")`. Seed BOTH distinct -> golden binds leader.id, Rust binds the team key. RED.
25
- #[test]
26
- fn status_team_is_leader_id_not_team_key() {
27
- let ws = seed_status_state(json!({"team": "TEAMKEY", "leader": {"id": "LEADERID"}}));
28
- let v = status_port::status(&ws, /*compact=*/ false, /*detail=*/ true).expect("status");
29
- assert_eq!(
30
- v["team"],
31
- json!("LEADERID"),
32
- "golden status.team = state['leader']['id'] (queries.py:66), NOT state['team']; got {:?}",
33
- v["team"]
34
- );
35
- let _ = std::fs::remove_dir_all(&ws);
36
- }
22
+ // ── status #1: `team` = state["leader"]["id"] (default "leader"), NOT state["team"] ──────────────
23
+ // GOLDEN queries.py:66 `state.get("leader", {}).get("id", "leader")`. RUST mod.rs:94 reads
24
+ // `state.get("team")`. Seed BOTH distinct -> golden binds leader.id, Rust binds the team key. RED.
25
+ #[test]
26
+ fn status_team_is_leader_id_not_team_key() {
27
+ let ws = seed_status_state(json!({"team": "TEAMKEY", "leader": {"id": "LEADERID"}}));
28
+ let v = status_port::status(&ws, /*compact=*/ false, /*detail=*/ true).expect("status");
29
+ assert_eq!(
30
+ v["team"],
31
+ json!("LEADERID"),
32
+ "golden status.team = state['leader']['id'] (queries.py:66), NOT state['team']; got {:?}",
33
+ v["team"]
34
+ );
35
+ let _ = std::fs::remove_dir_all(&ws);
36
+ }
37
37
 
38
- // ── status #2: messages = store.message_counts() ({} when empty), results = store.result_counts()
39
- // ({total,uncollected,collected,invalid,by_status}) — NOT the Rust {"count": N} placeholders. ──────
40
- // GOLDEN queries.py:73,75. RUST mod.rs:101,103 emit `{"count": N}`. RED.
41
- #[test]
42
- fn status_counts_are_store_count_maps_not_count_int() {
43
- let ws = tmp_workspace(); // empty store
44
- let v = status_port::status(&ws, false, true).expect("status");
45
- assert_eq!(
46
- v["messages"],
47
- json!({}),
48
- "golden empty store.message_counts() == {{}} (queries.py:73), NOT {{count:0}}; got {:?}",
49
- v["messages"]
50
- );
51
- assert_eq!(
52
- v["results"],
53
- json!({"total": 0, "uncollected": 0, "collected": 0, "invalid": 0, "by_status": {}}),
54
- "golden store.result_counts() full shape (queries.py:75), NOT {{count:0}}; got {:?}",
55
- v["results"]
56
- );
57
- let _ = std::fs::remove_dir_all(&ws);
58
- }
38
+ // ── status #2: messages = store.message_counts() ({} when empty), results = store.result_counts()
39
+ // ({total,uncollected,collected,invalid,by_status}) — NOT the Rust {"count": N} placeholders. ──────
40
+ // GOLDEN queries.py:73,75. RUST mod.rs:101,103 emit `{"count": N}`. RED.
41
+ #[test]
42
+ fn status_counts_are_store_count_maps_not_count_int() {
43
+ let ws = tmp_workspace(); // empty store
44
+ let v = status_port::status(&ws, false, true).expect("status");
45
+ assert_eq!(
46
+ v["messages"],
47
+ json!({}),
48
+ "golden empty store.message_counts() == {{}} (queries.py:73), NOT {{count:0}}; got {:?}",
49
+ v["messages"]
50
+ );
51
+ assert_eq!(
52
+ v["results"],
53
+ json!({"total": 0, "uncollected": 0, "collected": 0, "invalid": 0, "by_status": {}}),
54
+ "golden store.result_counts() full shape (queries.py:75), NOT {{count:0}}; got {:?}",
55
+ v["results"]
56
+ );
57
+ let _ = std::fs::remove_dir_all(&ws);
58
+ }
59
59
 
60
- // ── status #3: coordinator = coordinator_health(ws) FULL shape in golden order ──────────────────
61
- // 0.5.18 adds binary identity diagnostics to expose stale coordinator rotation reasons.
62
- #[test]
63
- fn status_coordinator_is_full_health_identity_shape() {
64
- let ws = tmp_workspace();
65
- let v = status_port::status(&ws, false, true).expect("status");
66
- let coord = v["coordinator"].as_object().expect("coordinator dict");
67
- let order: Vec<&str> = coord.keys().map(String::as_str).collect();
68
- assert_eq!(
60
+ // ── status #3: coordinator = coordinator_health(ws) FULL shape in golden order ──────────────────
61
+ // 0.5.18 adds binary identity diagnostics to expose stale coordinator rotation reasons.
62
+ #[test]
63
+ fn status_coordinator_is_full_health_identity_shape() {
64
+ let ws = tmp_workspace();
65
+ let v = status_port::status(&ws, false, true).expect("status");
66
+ let coord = v["coordinator"].as_object().expect("coordinator dict");
67
+ let order: Vec<&str> = coord.keys().map(String::as_str).collect();
68
+ assert_eq!(
69
69
  order,
70
70
  vec![
71
71
  "ok",
@@ -89,197 +89,220 @@ use super::*;
89
89
  ],
90
90
  "golden coordinator_health insertion order with 0.5.41 fault-invisibility fields; got {order:?}"
91
91
  );
92
- assert!(
92
+ assert!(
93
93
  coord.get("schema_version").is_none(),
94
94
  "golden coordinator has NO flat `schema_version` (it nests schema:{{message_store_schema_version}}); \
95
95
  Rust emits the flat key. got {coord:?}"
96
96
  );
97
- let _ = std::fs::remove_dir_all(&ws);
98
- }
97
+ let _ = std::fs::remove_dir_all(&ws);
98
+ }
99
99
 
100
- // ── status #4: last_events = EventLog.tail(10) ([] when no events), NOT the literal placeholder ──
101
- // GOLDEN queries.py:78 `EventLog(workspace).tail(10)`. RUST mod.rs:113 (non-compact) emits the
102
- // hardcoded placeholder `[{"event":"status.full"}]`. On a fresh log golden == []. RED. ────────────
103
- #[test]
104
- fn status_last_events_is_event_tail_not_status_full_placeholder() {
105
- let ws = tmp_workspace(); // no events.jsonl
106
- let v = status_port::status(&ws, false, true).expect("status");
107
- assert_ne!(
100
+ // ── status #4: last_events = EventLog.tail(10) ([] when no events), NOT the literal placeholder ──
101
+ // GOLDEN queries.py:78 `EventLog(workspace).tail(10)`. RUST mod.rs:113 (non-compact) emits the
102
+ // hardcoded placeholder `[{"event":"status.full"}]`. On a fresh log golden == []. RED. ────────────
103
+ #[test]
104
+ fn status_last_events_is_event_tail_not_status_full_placeholder() {
105
+ let ws = tmp_workspace(); // no events.jsonl
106
+ let v = status_port::status(&ws, false, true).expect("status");
107
+ assert_ne!(
108
108
  v["last_events"],
109
109
  json!([{"event": "status.full"}]),
110
110
  "golden last_events = EventLog.tail(10), NOT the literal [{{event:status.full}}] placeholder"
111
111
  );
112
- assert_eq!(
113
- v["last_events"],
114
- json!([]),
115
- "golden last_events on a fresh event log is [] (tail of empty); got {:?}",
116
- v["last_events"]
117
- );
118
- let _ = std::fs::remove_dir_all(&ws);
119
- }
112
+ assert_eq!(
113
+ v["last_events"],
114
+ json!([]),
115
+ "golden last_events on a fresh event log is [] (tail of empty); got {:?}",
116
+ v["last_events"]
117
+ );
118
+ let _ = std::fs::remove_dir_all(&ws);
119
+ }
120
120
 
121
- // ── status #5: the full (non-compact) status dict has NO `detail` key ────────────────────────────
122
- // GOLDEN queries.py:65-79 builds exactly 13 keys; there is no `detail`. RUST mod.rs:115-119 injects
123
- // `detail:true` whenever `!compact || detail`. The full --detail call MUST NOT carry detail. RED. ──
124
- #[test]
125
- fn status_noncompact_has_no_detail_key() {
126
- let ws = tmp_workspace();
127
- let v = status_port::status(&ws, /*compact=*/ false, /*detail=*/ true).expect("status");
128
- assert!(
121
+ // ── status #5: the full (non-compact) status dict has NO `detail` key ────────────────────────────
122
+ // GOLDEN queries.py:65-79 builds exactly 13 keys; there is no `detail`. RUST mod.rs:115-119 injects
123
+ // `detail:true` whenever `!compact || detail`. The full --detail call MUST NOT carry detail. RED. ──
124
+ #[test]
125
+ fn status_noncompact_has_no_detail_key() {
126
+ let ws = tmp_workspace();
127
+ let v = status_port::status(&ws, /*compact=*/ false, /*detail=*/ true).expect("status");
128
+ assert!(
129
129
  v.as_object().unwrap().get("detail").is_none(),
130
130
  "golden status dict has NO `detail` key (queries.py:65-79); Rust injects detail:true. got {v:?}"
131
131
  );
132
- let _ = std::fs::remove_dir_all(&ws);
133
- }
132
+ let _ = std::fs::remove_dir_all(&ws);
133
+ }
134
134
 
135
- // ── status #6: each agent entry is enriched with an `interacted` marker (C3, queries.py:53-64) ────
136
- // GOLDEN: valid ISO first_send_at passes through; any other shape (invalid str / missing) -> the
137
- // literal "never" (_interacted_marker, queries.py:15-30). RUST mod.rs:81-84 passes agents through
138
- // RAW with no enrichment -> no `interacted` key. RED. ────────────────────────────────────────────
139
- #[test]
140
- fn status_agents_enriched_with_interacted_marker() {
141
- let ws = seed_status_state(json!({
142
- "leader": {"id": "leader"},
143
- "agents": {
144
- "alpha": {"provider": "codex", "first_send_at": "2026-05-27T10:00:00+00:00"},
145
- "beta": {"provider": "claude", "first_send_at": "not-a-date"},
146
- "gamma": {"provider": "claude"},
147
- }
148
- }));
149
- let v = status_port::status(&ws, false, true).expect("status");
150
- let agents = &v["agents"];
151
- assert_eq!(
152
- agents["alpha"]["interacted"],
153
- json!("2026-05-27T10:00:00+00:00"),
154
- "valid ISO first_send_at must pass through as the interacted marker (queries.py:24-29)"
155
- );
156
- assert_eq!(
157
- agents["beta"]["interacted"],
158
- json!("never"),
159
- "an unparseable first_send_at must render as the literal 'never' (queries.py:27-28)"
160
- );
161
- assert_eq!(
162
- agents["gamma"]["interacted"],
163
- json!("never"),
164
- "a missing first_send_at must render as 'never' (queries.py:30)"
165
- );
166
- let _ = std::fs::remove_dir_all(&ws);
167
- }
135
+ // ── status #6: each agent entry is enriched with an `interacted` marker (C3, queries.py:53-64) ────
136
+ // GOLDEN: valid ISO first_send_at passes through; any other shape (invalid str / missing) -> the
137
+ // literal "never" (_interacted_marker, queries.py:15-30). RUST mod.rs:81-84 passes agents through
138
+ // RAW with no enrichment -> no `interacted` key. RED. ────────────────────────────────────────────
139
+ #[test]
140
+ fn status_agents_enriched_with_interacted_marker() {
141
+ let ws = seed_status_state(json!({
142
+ "leader": {"id": "leader"},
143
+ "agents": {
144
+ "alpha": {"provider": "codex", "first_send_at": "2026-05-27T10:00:00+00:00"},
145
+ "beta": {"provider": "claude", "first_send_at": "not-a-date"},
146
+ "gamma": {"provider": "claude"},
147
+ }
148
+ }));
149
+ let v = status_port::status(&ws, false, true).expect("status");
150
+ let agents = &v["agents"];
151
+ assert_eq!(
152
+ agents["alpha"]["interacted"],
153
+ json!("2026-05-27T10:00:00+00:00"),
154
+ "valid ISO first_send_at must pass through as the interacted marker (queries.py:24-29)"
155
+ );
156
+ assert_eq!(
157
+ agents["beta"]["interacted"],
158
+ json!("never"),
159
+ "an unparseable first_send_at must render as the literal 'never' (queries.py:27-28)"
160
+ );
161
+ assert_eq!(
162
+ agents["gamma"]["interacted"],
163
+ json!("never"),
164
+ "a missing first_send_at must render as 'never' (queries.py:30)"
165
+ );
166
+ let _ = std::fs::remove_dir_all(&ws);
167
+ }
168
168
 
169
- // ── status #7 (TARGET-SCAN WIRING, in-process): tmux_session_present uses a LIVE tmux probe ───────
170
- // GOLDEN queries.py:52 `_tmux_session_exists(session_name)` (real `tmux has-session`). RUST mod.rs:96
171
- // shortcuts to `session_name.is_some()`. Seed a present-but-nonexistent session -> golden False,
172
- // Rust True. This is the status-level pane-discovery wiring the dangling list_targets feeds. RED. ──
173
- #[test]
174
- fn status_tmux_session_present_uses_live_tmux_probe_not_is_some() {
175
- let absent = format!("ta-lanec-absent-{}", std::process::id());
176
- let ws = seed_status_state(json!({"leader": {"id": "leader"}, "session_name": absent}));
177
- let v = status_port::status(&ws, false, true).expect("status");
178
- assert_eq!(
169
+ // ── status #7 (TARGET-SCAN WIRING, in-process): tmux_session_present uses a LIVE tmux probe ───────
170
+ // GOLDEN queries.py:52 `_tmux_session_exists(session_name)` (real `tmux has-session`). RUST mod.rs:96
171
+ // shortcuts to `session_name.is_some()`. Seed a present-but-nonexistent session -> golden False,
172
+ // Rust True. This is the status-level pane-discovery wiring the dangling list_targets feeds. RED. ──
173
+ #[test]
174
+ fn status_tmux_session_present_uses_live_tmux_probe_not_is_some() {
175
+ let absent = format!("ta-lanec-absent-{}", std::process::id());
176
+ let ws = seed_status_state(json!({"leader": {"id": "leader"}, "session_name": absent}));
177
+ let v = status_port::status(&ws, false, true).expect("status");
178
+ assert_eq!(
179
179
  v["tmux_session_present"],
180
180
  json!(false),
181
181
  "golden tmux_session_present = _tmux_session_exists(name) (a live probe; queries.py:52); a \
182
182
  present-but-nonexistent session name must be False, not is_some()==true. got {:?}",
183
183
  v["tmux_session_present"]
184
184
  );
185
- let _ = std::fs::remove_dir_all(&ws);
186
- }
185
+ let _ = std::fs::remove_dir_all(&ws);
186
+ }
187
187
 
188
- // ── approvals: golden 5-key {ok,waiting,waiting_count,approvals,scan} (status/approvals.py:30-36) ──
189
- // RUST mod.rs:131-133 stub {ok,agent,approvals}. RED. ───────────────────────────────────────────
190
- #[test]
191
- fn approvals_golden_shape_has_waiting_count_and_scan_not_agent() {
192
- let ws = tmp_workspace();
193
- let v = status_port::approvals(&ws, None, true).expect("approvals");
194
- let obj = v.as_object().expect("approvals dict");
195
- let order: Vec<&str> = obj.keys().map(String::as_str).collect();
196
- assert_eq!(
188
+ // ── approvals: golden 5-key {ok,waiting,waiting_count,approvals,scan} (status/approvals.py:30-36) ──
189
+ // RUST mod.rs:131-133 stub {ok,agent,approvals}. RED. ───────────────────────────────────────────
190
+ #[test]
191
+ fn approvals_golden_shape_has_waiting_count_and_scan_not_agent() {
192
+ let ws = tmp_workspace();
193
+ let v = status_port::approvals(&ws, None, true).expect("approvals");
194
+ let obj = v.as_object().expect("approvals dict");
195
+ let order: Vec<&str> = obj.keys().map(String::as_str).collect();
196
+ assert_eq!(
197
197
  order,
198
198
  vec!["ok", "waiting", "waiting_count", "approvals", "scan"],
199
199
  "golden approvals 5-key order (approvals.py:30-36); Rust stub emits {{ok,agent,approvals}}. got {order:?}"
200
200
  );
201
- assert!(!obj.contains_key("agent"), "golden approvals has NO `agent` key");
202
- assert_eq!(
201
+ assert!(
202
+ !obj.contains_key("agent"),
203
+ "golden approvals has NO `agent` key"
204
+ );
205
+ assert_eq!(
203
206
  v["scan"],
204
207
  json!({"mode": "tail", "lines": 120, "raw_output": false}),
205
208
  "golden scan == {{mode:tail,lines:120,raw_output:false}} (APPROVAL_SCAN_LINES=120); got {:?}",
206
209
  v["scan"]
207
210
  );
208
- let _ = std::fs::remove_dir_all(&ws);
209
- }
211
+ let _ = std::fs::remove_dir_all(&ws);
212
+ }
210
213
 
211
- // ── inbox: golden {ok,agent_id,messages,since} (status/inbox.py:35-38) — key is `agent_id`, carries
212
- // `since` (not `agent`/`limit`). RUST mod.rs:136-144 stub {ok,agent,limit,messages}. RED. ──────────
213
- #[test]
214
- fn inbox_golden_shape_is_agent_id_and_since_not_agent_limit() {
215
- let ws = tmp_workspace();
216
- let v = status_port::inbox(&ws, "alpha", 20, None, true, None).expect("inbox");
217
- let obj = v.as_object().expect("inbox dict");
218
- let order: Vec<&str> = obj.keys().map(String::as_str).collect();
219
- assert_eq!(
214
+ // ── inbox: golden {ok,agent_id,messages,since} (status/inbox.py:35-38) — key is `agent_id`, carries
215
+ // `since` (not `agent`/`limit`). RUST mod.rs:136-144 stub {ok,agent,limit,messages}. RED. ──────────
216
+ #[test]
217
+ fn inbox_golden_shape_is_agent_id_and_since_not_agent_limit() {
218
+ let ws = tmp_workspace();
219
+ let v = status_port::inbox(&ws, "alpha", 20, None, true, None).expect("inbox");
220
+ let obj = v.as_object().expect("inbox dict");
221
+ let order: Vec<&str> = obj.keys().map(String::as_str).collect();
222
+ assert_eq!(
220
223
  order,
221
224
  vec!["ok", "agent_id", "messages", "since"],
222
225
  "golden inbox 4-key order {{ok,agent_id,messages,since}} (inbox.py:38); Rust stub uses agent/limit. got {order:?}"
223
226
  );
224
- assert_eq!(v["agent_id"], json!("alpha"), "golden key is `agent_id` (not `agent`)");
225
- assert!(obj.contains_key("since"), "golden inbox carries `since` (echoed, null here)");
226
- assert!(!obj.contains_key("agent"), "golden inbox has NO bare `agent` key");
227
- assert!(!obj.contains_key("limit"), "golden inbox has NO `limit` key in the result dict");
228
- let _ = std::fs::remove_dir_all(&ws);
229
- }
227
+ assert_eq!(
228
+ v["agent_id"],
229
+ json!("alpha"),
230
+ "golden key is `agent_id` (not `agent`)"
231
+ );
232
+ assert!(
233
+ obj.contains_key("since"),
234
+ "golden inbox carries `since` (echoed, null here)"
235
+ );
236
+ assert!(
237
+ !obj.contains_key("agent"),
238
+ "golden inbox has NO bare `agent` key"
239
+ );
240
+ assert!(
241
+ !obj.contains_key("limit"),
242
+ "golden inbox has NO `limit` key in the result dict"
243
+ );
244
+ let _ = std::fs::remove_dir_all(&ws);
245
+ }
230
246
 
231
- // ── comms_selftest: golden {ok,status,run_id,scope,boundary,checks} (diagnose/comms.py:40-47) ─────
232
- // RUST mod.rs:525-527 stub {ok,team,gate,provider_sdk_calls:int}. RED. Locks COMMS_BOUNDARY_TEXT +
233
- // the deterministic check sub-shapes (run_id is a random uuid; not value-locked). ────────────────
234
- #[test]
235
- fn comms_selftest_golden_boundary_scope_and_check_shapes() {
236
- let boundary = "validates live pane binding consistency and zero-token comms contracts. \
247
+ // ── comms_selftest: golden {ok,status,run_id,scope,boundary,checks} (diagnose/comms.py:40-47) ─────
248
+ // RUST mod.rs:525-527 stub {ok,team,gate,provider_sdk_calls:int}. RED. Locks COMMS_BOUNDARY_TEXT +
249
+ // the deterministic check sub-shapes (run_id is a random uuid; not value-locked). ────────────────
250
+ #[test]
251
+ fn comms_selftest_golden_boundary_scope_and_check_shapes() {
252
+ let boundary = "validates live pane binding consistency and zero-token comms contracts. \
237
253
  Does NOT perform live runtime message round-trip. (zero token, zero pollution)";
238
- let ws = tmp_workspace();
239
- let v = diagnose_port::comms_selftest(&ws, None, None).expect("comms_selftest");
240
- let obj = v.as_object().expect("comms dict");
241
- assert_eq!(v["boundary"], json!(boundary), "golden COMMS_BOUNDARY_TEXT prefix (comms.py:11-14)");
242
- assert_eq!(v["scope"], json!("binding_consistency"), "golden scope");
243
- assert_eq!(
244
- v["status"],
245
- json!("fail"),
246
- "empty workspace has no runtime receiver binding, so comms gate must not pass"
247
- );
248
- assert!(obj.contains_key("run_id"), "golden carries a run_id (uuid hex[:12])");
249
- assert!(!obj.contains_key("team"), "golden has NO `team` key");
250
- assert!(!obj.contains_key("gate"), "golden has NO `gate` key");
251
- assert_eq!(v["checks"]["contract_suite"]["status"], json!("pass"));
252
- assert_eq!(v["checks"]["contract_suite"]["failed"], json!([]));
253
- let suite_names: Vec<&str> = v["checks"]["contract_suite"]["checks"]
254
- .as_array()
255
- .expect("contract suite checks")
256
- .iter()
257
- .filter_map(|check| check.get("name").and_then(Value::as_str))
258
- .collect();
259
- assert!(
260
- suite_names.contains(&"message_store_schema")
261
- && suite_names.contains(&"message_token_shape")
262
- && suite_names.contains(&"result_notification_render")
263
- && suite_names.contains(&"leader_projection_owner_team"),
264
- "contract suite must be executable, not deferred: {suite_names:?}"
265
- );
266
- assert_eq!(
267
- v["checks"]["provider_sdk_calls"]["calls"],
268
- json!({"anthropic": 0, "openai": 0, "httpx": 0}),
269
- "golden provider_sdk_calls.calls (comms.py:142-151); Rust stub is a bare int 0"
270
- );
271
- let _ = std::fs::remove_dir_all(&ws);
272
- }
254
+ let ws = tmp_workspace();
255
+ let v = diagnose_port::comms_selftest(&ws, None, None).expect("comms_selftest");
256
+ let obj = v.as_object().expect("comms dict");
257
+ assert_eq!(
258
+ v["boundary"],
259
+ json!(boundary),
260
+ "golden COMMS_BOUNDARY_TEXT prefix (comms.py:11-14)"
261
+ );
262
+ assert_eq!(v["scope"], json!("binding_consistency"), "golden scope");
263
+ assert_eq!(
264
+ v["status"],
265
+ json!("fail"),
266
+ "empty workspace has no runtime receiver binding, so comms gate must not pass"
267
+ );
268
+ assert!(
269
+ obj.contains_key("run_id"),
270
+ "golden carries a run_id (uuid hex[:12])"
271
+ );
272
+ assert!(!obj.contains_key("team"), "golden has NO `team` key");
273
+ assert!(!obj.contains_key("gate"), "golden has NO `gate` key");
274
+ assert_eq!(v["checks"]["contract_suite"]["status"], json!("pass"));
275
+ assert_eq!(v["checks"]["contract_suite"]["failed"], json!([]));
276
+ let suite_names: Vec<&str> = v["checks"]["contract_suite"]["checks"]
277
+ .as_array()
278
+ .expect("contract suite checks")
279
+ .iter()
280
+ .filter_map(|check| check.get("name").and_then(Value::as_str))
281
+ .collect();
282
+ assert!(
283
+ suite_names.contains(&"message_store_schema")
284
+ && suite_names.contains(&"message_token_shape")
285
+ && suite_names.contains(&"result_notification_render")
286
+ && suite_names.contains(&"leader_projection_owner_team"),
287
+ "contract suite must be executable, not deferred: {suite_names:?}"
288
+ );
289
+ assert_eq!(
290
+ v["checks"]["provider_sdk_calls"]["calls"],
291
+ json!({"anthropic": 0, "openai": 0, "httpx": 0}),
292
+ "golden provider_sdk_calls.calls (comms.py:142-151); Rust stub is a bare int 0"
293
+ );
294
+ let _ = std::fs::remove_dir_all(&ws);
295
+ }
273
296
 
274
- // ── orphan_gate(fix=true,confirm=false): golden REFUSED envelope (orphan_cleanup.py:304-311) ──────
275
- // {ok:false,gate:"orphans",status:"refused",reason:"fix_requires_confirm",action:...}. RUST mod.rs:
276
- // 530-531 stub {ok:true,fix,confirm,orphans:[]}. Fully deterministic (no subprocess). RED. ─────────
277
- #[test]
278
- fn orphan_gate_fix_without_confirm_is_refused_envelope() {
279
- let ws = tmp_workspace();
280
- let v = diagnose_port::orphan_gate(&ws, /*fix=*/ true, /*confirm=*/ false)
281
- .expect("orphan_gate");
282
- assert_eq!(
297
+ // ── orphan_gate(fix=true,confirm=false): golden REFUSED envelope (orphan_cleanup.py:304-311) ──────
298
+ // {ok:false,gate:"orphans",status:"refused",reason:"fix_requires_confirm",action:...}. RUST mod.rs:
299
+ // 530-531 stub {ok:true,fix,confirm,orphans:[]}. Fully deterministic (no subprocess). RED. ─────────
300
+ #[test]
301
+ fn orphan_gate_fix_without_confirm_is_refused_envelope() {
302
+ let ws = tmp_workspace();
303
+ let v =
304
+ diagnose_port::orphan_gate(&ws, /*fix=*/ true, /*confirm=*/ false).expect("orphan_gate");
305
+ assert_eq!(
283
306
  v,
284
307
  json!({
285
308
  "ok": false,
@@ -290,74 +313,134 @@ use super::*;
290
313
  }),
291
314
  "golden orphan_gate fix-without-confirm refused envelope (orphan_cleanup.py:304-311); got {v:?}"
292
315
  );
293
- let order: Vec<&str> = v.as_object().unwrap().keys().map(String::as_str).collect();
294
- assert_eq!(order, vec!["ok", "gate", "status", "reason", "action"], "golden refused key order");
295
- }
316
+ let order: Vec<&str> = v.as_object().unwrap().keys().map(String::as_str).collect();
317
+ assert_eq!(
318
+ order,
319
+ vec!["ok", "gate", "status", "reason", "action"],
320
+ "golden refused key order"
321
+ );
322
+ }
296
323
 
297
- // ── cleanup_orphans(confirm=false): golden DRY-RUN envelope (orphan_cleanup.py cleanup_…) ─────────
298
- // {ok,scanned,orphans,dry_run:true,scanned_at,action_required}. RUST mod.rs:534-535 stub
299
- // {ok,confirm,cleaned}. scanned/scanned_at are machine/clock-derived; lock the deterministic ones. RED.
300
- #[test]
301
- #[ignore = "real-machine: cleanup_orphans scans machine-wide ta-* tmux/process residue"]
302
- #[serial_test::file_serial(tmux)]
303
- fn cleanup_orphans_dryrun_golden_envelope() {
304
- let ws = tmp_workspace();
305
- let v = diagnose_port::cleanup_orphans(&ws, /*confirm=*/ false).expect("cleanup_orphans");
306
- let obj = v.as_object().expect("cleanup dict");
307
- assert_eq!(v["dry_run"], json!(true), "no --confirm => dry_run:true (cleanup_orphan_coordinators)");
308
- assert_eq!(v["orphans"], json!([]), "golden lists orphans (empty when none)");
309
- assert_eq!(
310
- v["action_required"],
311
- json!("re-run with --confirm to send SIGTERM"),
312
- "golden dry-run action_required text"
313
- );
314
- assert!(obj.contains_key("scanned_at"), "golden carries scanned_at timestamp");
315
- assert!(obj.contains_key("scanned"), "golden carries scanned count");
316
- assert!(!obj.contains_key("confirm"), "golden has NO `confirm` key");
317
- assert!(!obj.contains_key("cleaned"), "golden has NO `cleaned` key (uses `orphans`)");
318
- }
324
+ // ── cleanup_orphans(confirm=false): golden DRY-RUN envelope (orphan_cleanup.py cleanup_…) ─────────
325
+ // {ok,scanned,orphans,dry_run:true,scanned_at,action_required}. RUST mod.rs:534-535 stub
326
+ // {ok,confirm,cleaned}. scanned/scanned_at are machine/clock-derived; lock the deterministic ones. RED.
327
+ #[test]
328
+ #[ignore = "real-machine: cleanup_orphans scans machine-wide ta-* tmux/process residue"]
329
+ #[serial_test::file_serial(tmux)]
330
+ fn cleanup_orphans_dryrun_golden_envelope() {
331
+ let ws = tmp_workspace();
332
+ let v = diagnose_port::cleanup_orphans(&ws, /*confirm=*/ false).expect("cleanup_orphans");
333
+ let obj = v.as_object().expect("cleanup dict");
334
+ assert_eq!(
335
+ v["dry_run"],
336
+ json!(true),
337
+ "no --confirm => dry_run:true (cleanup_orphan_coordinators)"
338
+ );
339
+ assert_eq!(
340
+ v["orphans"],
341
+ json!([]),
342
+ "golden lists orphans (empty when none)"
343
+ );
344
+ assert_eq!(
345
+ v["action_required"],
346
+ json!("re-run with --confirm to send SIGTERM"),
347
+ "golden dry-run action_required text"
348
+ );
349
+ assert!(
350
+ obj.contains_key("scanned_at"),
351
+ "golden carries scanned_at timestamp"
352
+ );
353
+ assert!(obj.contains_key("scanned"), "golden carries scanned count");
354
+ assert!(!obj.contains_key("confirm"), "golden has NO `confirm` key");
355
+ assert!(
356
+ !obj.contains_key("cleaned"),
357
+ "golden has NO `cleaned` key (uses `orphans`)"
358
+ );
359
+ }
319
360
 
320
- // ── fix_schema: golden fix_schema_layout diagnosis envelope (schema_migration.py:258) ─────────────
321
- // {ok,status,db_path,schema_version,user_version,layout_diffs,recommended_action,would_backup_path,
322
- // fixed,rebuilds}. RUST mod.rs:538-540 stub {ok,fixed:false}. db_path/would_backup_path are
323
- // path/clock-derived; lock the deterministic fields. SCHEMA_VERSION==3. RED. ─────────────────────
324
- #[test]
325
- fn fix_schema_golden_layout_diagnosis_envelope() {
326
- let ws = tmp_workspace();
327
- let v = diagnose_port::fix_schema(&ws).expect("fix_schema");
328
- let obj = v.as_object().expect("fix_schema dict");
329
- assert_eq!(v["status"], json!("missing"), "missing db: status missing");
330
- assert_eq!(v["schema_version"], json!(3), "golden schema_version == SCHEMA_VERSION (3)");
331
- assert_eq!(v["user_version"], json!(0), "missing db user_version == 0");
332
- assert_eq!(v["layout_diffs"], json!([]), "missing db => empty layout_diffs list");
333
- assert_eq!(v["recommended_action"], json!("none"), "no drift => recommended_action 'none'");
334
- assert_eq!(v["fixed"], json!(false), "missing db is diagnostic-only and does not create a db");
335
- assert_eq!(v["rebuilds"], json!([]), "no rebuilds on a fresh db");
336
- assert!(obj.contains_key("db_path"), "golden carries db_path");
337
- assert!(obj.contains_key("would_backup_path"), "golden carries would_backup_path");
338
- let _ = std::fs::remove_dir_all(&ws);
339
- }
361
+ // ── fix_schema: golden fix_schema_layout diagnosis envelope (schema_migration.py:258) ─────────────
362
+ // {ok,status,db_path,schema_version,user_version,layout_diffs,recommended_action,would_backup_path,
363
+ // fixed,rebuilds}. RUST mod.rs:538-540 stub {ok,fixed:false}. db_path/would_backup_path are
364
+ // path/clock-derived; lock the deterministic fields. SCHEMA_VERSION==3. RED. ─────────────────────
365
+ #[test]
366
+ fn fix_schema_golden_layout_diagnosis_envelope() {
367
+ let ws = tmp_workspace();
368
+ let v = diagnose_port::fix_schema(&ws).expect("fix_schema");
369
+ let obj = v.as_object().expect("fix_schema dict");
370
+ assert_eq!(v["status"], json!("missing"), "missing db: status missing");
371
+ assert_eq!(
372
+ v["schema_version"],
373
+ json!(3),
374
+ "golden schema_version == SCHEMA_VERSION (3)"
375
+ );
376
+ assert_eq!(v["user_version"], json!(0), "missing db user_version == 0");
377
+ assert_eq!(
378
+ v["layout_diffs"],
379
+ json!([]),
380
+ "missing db => empty layout_diffs list"
381
+ );
382
+ assert_eq!(
383
+ v["recommended_action"],
384
+ json!("none"),
385
+ "no drift => recommended_action 'none'"
386
+ );
387
+ assert_eq!(
388
+ v["fixed"],
389
+ json!(false),
390
+ "missing db is diagnostic-only and does not create a db"
391
+ );
392
+ assert_eq!(v["rebuilds"], json!([]), "no rebuilds on a fresh db");
393
+ assert!(obj.contains_key("db_path"), "golden carries db_path");
394
+ assert!(
395
+ obj.contains_key("would_backup_path"),
396
+ "golden carries would_backup_path"
397
+ );
398
+ let _ = std::fs::remove_dir_all(&ws);
399
+ }
340
400
 
341
- // ── doctor: golden checks dict (diagnose/health.py:179-241) — tmux/workspace/workspace_is_git_repo/
342
- // providers/mcp/coordinator/ok. RUST mod.rs:520-522 stub {ok,spec,blockers}. Lock the golden
343
- // top-level keys (values are machine-derived) + absence of the stub's spec/blockers. RED. ─────────
344
- #[test]
345
- fn doctor_golden_checks_shape_not_spec_blockers_stub() {
346
- let ws = tmp_workspace();
347
- let v = diagnose_port::doctor(&ws, None).expect("doctor");
348
- let obj = v.as_object().expect("doctor dict");
349
- let tmux = v["tmux"].as_object().expect("golden doctor.tmux is a dict {installed,path}");
350
- assert!(tmux.contains_key("installed") && tmux.contains_key("path"), "tmux check shape");
351
- assert!(v["workspace"].is_string(), "golden doctor.workspace is a string path");
352
- assert!(v["workspace_is_git_repo"].is_boolean(), "golden doctor.workspace_is_git_repo bool");
353
- assert!(v["providers"].is_object(), "golden doctor.providers is a dict");
354
- assert_eq!(
355
- v["mcp"]["local_module"],
356
- json!(true),
357
- "golden doctor.mcp.local_module == true (health.py:197-200)"
358
- );
359
- assert!(v["coordinator"].is_object(), "golden doctor.coordinator is the coordinator_health dict");
360
- assert!(!obj.contains_key("spec"), "golden doctor has NO `spec` key (Rust stub adds it)");
361
- assert!(!obj.contains_key("blockers"), "golden doctor has NO `blockers` key (Rust stub adds it)");
362
- let _ = std::fs::remove_dir_all(&ws);
363
- }
401
+ // ── doctor: golden checks dict (diagnose/health.py:179-241) — tmux/workspace/workspace_is_git_repo/
402
+ // providers/mcp/coordinator/ok. RUST mod.rs:520-522 stub {ok,spec,blockers}. Lock the golden
403
+ // top-level keys (values are machine-derived) + absence of the stub's spec/blockers. RED. ─────────
404
+ #[test]
405
+ fn doctor_golden_checks_shape_not_spec_blockers_stub() {
406
+ let ws = tmp_workspace();
407
+ let v = diagnose_port::doctor(&ws, None).expect("doctor");
408
+ let obj = v.as_object().expect("doctor dict");
409
+ let tmux = v["tmux"]
410
+ .as_object()
411
+ .expect("golden doctor.tmux is a dict {installed,path}");
412
+ assert!(
413
+ tmux.contains_key("installed") && tmux.contains_key("path"),
414
+ "tmux check shape"
415
+ );
416
+ assert!(
417
+ v["workspace"].is_string(),
418
+ "golden doctor.workspace is a string path"
419
+ );
420
+ assert!(
421
+ v["workspace_is_git_repo"].is_boolean(),
422
+ "golden doctor.workspace_is_git_repo bool"
423
+ );
424
+ assert!(
425
+ v["providers"].is_object(),
426
+ "golden doctor.providers is a dict"
427
+ );
428
+ assert_eq!(
429
+ v["mcp"]["local_module"],
430
+ json!(true),
431
+ "golden doctor.mcp.local_module == true (health.py:197-200)"
432
+ );
433
+ assert!(
434
+ v["coordinator"].is_object(),
435
+ "golden doctor.coordinator is the coordinator_health dict"
436
+ );
437
+ assert!(
438
+ !obj.contains_key("spec"),
439
+ "golden doctor has NO `spec` key (Rust stub adds it)"
440
+ );
441
+ assert!(
442
+ !obj.contains_key("blockers"),
443
+ "golden doctor has NO `blockers` key (Rust stub adds it)"
444
+ );
445
+ let _ = std::fs::remove_dir_all(&ws);
446
+ }