@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,217 +1,243 @@
1
1
  use super::*;
2
2
 
3
- // =====================================================================
4
- // 4. wake 层纯函数(unimplemented → RED)。golden:probe_leader.py 5 分支 + boundary。
5
- // =====================================================================
6
-
7
- #[test]
8
- fn should_reread_no_file_when_current_mtime_none() {
9
- // current_mtime is None → {reread:false, no_file}(wake.py:31-32)。
10
- let d = should_reread(None, None, None, 100.0, 60.0);
11
- assert!(!d.reread);
12
- assert_eq!(d.reason, RereadReason::NoFile);
3
+ // =====================================================================
4
+ // 4. wake 层纯函数(unimplemented → RED)。golden:probe_leader.py 5 分支 + boundary。
5
+ // =====================================================================
6
+
7
+ #[test]
8
+ fn should_reread_no_file_when_current_mtime_none() {
9
+ // current_mtime is None → {reread:false, no_file}(wake.py:31-32)。
10
+ let d = should_reread(None, None, None, 100.0, 60.0);
11
+ assert!(!d.reread);
12
+ assert_eq!(d.reason, RereadReason::NoFile);
13
+ }
14
+
15
+ #[test]
16
+ fn should_reread_never_classified() {
17
+ // last_classified_mtime is None → {reread:true, never_classified}。
18
+ let d = should_reread(None, Some(10.0), None, 100.0, 60.0);
19
+ assert!(d.reread);
20
+ assert_eq!(d.reason, RereadReason::NeverClassified);
21
+ }
22
+
23
+ #[test]
24
+ fn should_reread_file_changed() {
25
+ // current != last_classified → {reread:true, file_changed}。
26
+ let d = should_reread(Some(5.0), Some(20.0), Some(10.0), 100.0, 60.0);
27
+ assert!(d.reread);
28
+ assert_eq!(d.reason, RereadReason::FileChanged);
29
+ }
30
+
31
+ #[test]
32
+ fn should_reread_quiescent_already_classified_at_and_past_debounce() {
33
+ // current==last_classified 且 silent_for >= debounce → quiescent(不再重读)。
34
+ // silent_for = max(0, now-current_mtime) = 100-10 = 90 >= 60。
35
+ let d = should_reread(Some(10.0), Some(10.0), Some(10.0), 100.0, 60.0);
36
+ assert!(!d.reread);
37
+ assert_eq!(d.reason, RereadReason::QuiescentAlreadyClassified);
38
+ // 边界:silent_for 恰 == debounce(now-current=60)→ 仍 quiescent(>= 比较)。
39
+ let b = should_reread(Some(40.0), Some(40.0), Some(40.0), 100.0, 60.0);
40
+ assert!(!b.reread);
41
+ assert_eq!(b.reason, RereadReason::QuiescentAlreadyClassified);
42
+ }
43
+
44
+ #[test]
45
+ fn should_reread_unchanged_within_debounce() {
46
+ // current==last_classified 且 silent_for < debounce → unchanged。
47
+ // now-current = 100-95 = 5 < 60。注:last_mtime 在 Python body 中未被使用。
48
+ let d = should_reread(Some(10.0), Some(95.0), Some(95.0), 100.0, 60.0);
49
+ assert!(!d.reread);
50
+ assert_eq!(d.reason, RereadReason::Unchanged);
51
+ }
52
+
53
+ #[test]
54
+ fn on_file_changed_adds_node_sorted_and_records_mtime() {
55
+ // golden probe_leader.py:add b → pending ["b"];add a → sorted ["a","b"]。
56
+ let s0 = on_file_changed(None, "b", 1.0);
57
+ assert_eq!(s0.pending, vec!["b".to_string()]);
58
+ assert_eq!(s0.mtimes.get("b"), Some(&1.0));
59
+ let s1 = on_file_changed(Some(&s0), "a", 2.0);
60
+ assert_eq!(
61
+ s1.pending,
62
+ vec!["a".to_string(), "b".to_string()],
63
+ "pending 必须 sorted"
64
+ );
65
+ assert_eq!(s1.mtimes.get("a"), Some(&2.0));
66
+ // 重复 add b:pending 仍是 set(不重复),mtime 被更新到 3.0。
67
+ let s2 = on_file_changed(Some(&s1), "b", 3.0);
68
+ assert_eq!(s2.pending, vec!["a".to_string(), "b".to_string()]);
69
+ assert_eq!(s2.mtimes.get("b"), Some(&3.0));
70
+ }
71
+
72
+ #[test]
73
+ fn take_pending_drains_sorted_and_clears_pending_but_keeps_mtimes() {
74
+ // golden probe_leader.py:drain → (["a","b"], state{pending:[], mtimes 保留})。
75
+ let mut st = on_file_changed(None, "b", 3.0);
76
+ st = on_file_changed(Some(&st), "a", 2.0);
77
+ let (drained, after) = take_pending(Some(&st));
78
+ assert_eq!(drained, vec!["a".to_string(), "b".to_string()]);
79
+ assert!(after.pending.is_empty());
80
+ // mtimes 不被 drain 清空。
81
+ assert_eq!(after.mtimes.get("a"), Some(&2.0));
82
+ assert_eq!(after.mtimes.get("b"), Some(&3.0));
83
+ // 再 drain → 空。
84
+ let (drained2, _after2) = take_pending(Some(&after));
85
+ assert!(drained2.is_empty());
86
+ // drain None → ([], default state)。
87
+ let (none_drained, none_state) = take_pending(None);
88
+ assert!(none_drained.is_empty());
89
+ assert!(none_state.pending.is_empty());
90
+ }
91
+
92
+ // =====================================================================
93
+ // 5. leader_session_name — sha1 派生 + 文件夹消毒(unimplemented → RED)
94
+ // =====================================================================
95
+
96
+ // 公式:team-agent-leader-<provider>-<sanitized folder[:48]>-<sha1(resolve(ws))[:8]>。
97
+ // 用真实 temp 目录,sha1/sanitize 在测试内复算后断言函数输出与之一致(probe 已验证公式)。
98
+ #[test]
99
+ fn leader_session_name_formula_and_sanitization() {
100
+ // 公式 = team-agent-leader-<provider>-<sanitized folder>-<sha1(resolve(ws))[:8]>。
101
+ // sha1 复算需 sha1 crate(本测试不引);改为断言格式不变量(provider/消毒/8-hex 后缀),
102
+ // 字节级 sha1 由 golden probe_leader.py 已验证公式正确。
103
+ let base = std::env::temp_dir().join(format!("ta_rs_lsn_{}", std::process::id()));
104
+ let weird = base.join("My Proj!@#name");
105
+ std::fs::create_dir_all(&weird).unwrap();
106
+ let got = leader_session_name(Provider::Codex, &weird);
107
+ // 前缀 + provider + 消毒后的 folder(非字母数字_.- → '_')。
108
+ let s = got.as_str();
109
+ assert!(
110
+ s.starts_with("team-agent-leader-codex-My_Proj___name-"),
111
+ "got {s}"
112
+ );
113
+ // sha1[:8] 后缀:8 个 hex。
114
+ let suffix = s.rsplit('-').next().unwrap();
115
+ assert_eq!(suffix.len(), 8, "sha1 前缀须 8 hex,got {suffix}");
116
+ assert!(suffix.chars().all(|c| c.is_ascii_hexdigit()));
117
+ }
118
+
119
+ // folder 名消毒成空 → 回退 "workspace"(probe_leader.py allsym 用例)。
120
+ #[test]
121
+ fn leader_session_name_empty_sanitized_folder_falls_back_to_workspace() {
122
+ let base = std::env::temp_dir().join(format!("ta_rs_lsn2_{}", std::process::id()));
123
+ // 全符号目录名 → 消毒后 strip('._-') 为空 → "workspace"。
124
+ let allsym = base.join("@@@");
125
+ std::fs::create_dir_all(&allsym).unwrap();
126
+ let got = leader_session_name(Provider::Codex, &allsym);
127
+ assert!(
128
+ got.as_str().contains("-workspace-"),
129
+ "全符号 folder 应回退 'workspace',got {}",
130
+ got.as_str()
131
+ );
132
+ }
133
+
134
+ // claude_code provider 出现在 session 名里(probe:team-agent-leader-claude_code-...)。
135
+ #[test]
136
+ fn leader_session_name_uses_claude_code_provider_string() {
137
+ let base = std::env::temp_dir().join(format!("ta_rs_lsn3_{}", std::process::id()));
138
+ let dir = base.join("proj");
139
+ std::fs::create_dir_all(&dir).unwrap();
140
+ let got = leader_session_name(Provider::ClaudeCode, &dir);
141
+ assert!(
142
+ got.as_str()
143
+ .starts_with("team-agent-leader-claude_code-proj-"),
144
+ "got {}",
145
+ got.as_str()
146
+ );
147
+ }
148
+
149
+ #[test]
150
+ fn leader_session_name_uses_copilot_provider_string() {
151
+ let base = std::env::temp_dir().join(format!("ta_rs_lsn_copilot_{}", std::process::id()));
152
+ let dir = base.join("proj");
153
+ std::fs::create_dir_all(&dir).unwrap();
154
+ let got = leader_session_name(Provider::Copilot, &dir);
155
+ assert!(
156
+ got.as_str().starts_with("team-agent-leader-copilot-proj-"),
157
+ "got {}",
158
+ got.as_str()
159
+ );
160
+ }
161
+
162
+ // =====================================================================
163
+ // 6. Family A 正源 owner 绑定 — bind_owner_from_caller_pane(unimplemented → RED)
164
+ // =====================================================================
165
+
166
+ // $TMUX_PANE 缺 → refuse + reason=caller_pane_missing(leader_binding.py:79-95)。
167
+ // 此处只能在 $TMUX_PANE 缺失环境下断言(测试进程通常无 TMUX_PANE)。
168
+ #[test]
169
+ fn bind_owner_refuses_when_caller_pane_missing() {
170
+ // 防御:确保本测试看到的环境无 TMUX_PANE(若 CI 在 tmux 内跑,跳过断言形态)。
171
+ if std::env::var_os("TMUX_PANE").is_some() {
172
+ // 在 tmux 内:正源存在,不该走 refuse 分支;此用例只验缺失分支,直接返回。
173
+ return;
13
174
  }
14
-
15
- #[test]
16
- fn should_reread_never_classified() {
17
- // last_classified_mtime is None → {reread:true, never_classified}。
18
- let d = should_reread(None, Some(10.0), None, 100.0, 60.0);
19
- assert!(d.reread);
20
- assert_eq!(d.reason, RereadReason::NeverClassified);
21
- }
22
-
23
- #[test]
24
- fn should_reread_file_changed() {
25
- // current != last_classified → {reread:true, file_changed}。
26
- let d = should_reread(Some(5.0), Some(20.0), Some(10.0), 100.0, 60.0);
27
- assert!(d.reread);
28
- assert_eq!(d.reason, RereadReason::FileChanged);
29
- }
30
-
31
- #[test]
32
- fn should_reread_quiescent_already_classified_at_and_past_debounce() {
33
- // current==last_classified 且 silent_for >= debounce → quiescent(不再重读)。
34
- // silent_for = max(0, now-current_mtime) = 100-10 = 90 >= 60。
35
- let d = should_reread(Some(10.0), Some(10.0), Some(10.0), 100.0, 60.0);
36
- assert!(!d.reread);
37
- assert_eq!(d.reason, RereadReason::QuiescentAlreadyClassified);
38
- // 边界:silent_for 恰 == debounce(now-current=60)→ 仍 quiescent(>= 比较)。
39
- let b = should_reread(Some(40.0), Some(40.0), Some(40.0), 100.0, 60.0);
40
- assert!(!b.reread);
41
- assert_eq!(b.reason, RereadReason::QuiescentAlreadyClassified);
42
- }
43
-
44
- #[test]
45
- fn should_reread_unchanged_within_debounce() {
46
- // current==last_classified 且 silent_for < debounce → unchanged。
47
- // now-current = 100-95 = 5 < 60。注:last_mtime 在 Python body 中未被使用。
48
- let d = should_reread(Some(10.0), Some(95.0), Some(95.0), 100.0, 60.0);
49
- assert!(!d.reread);
50
- assert_eq!(d.reason, RereadReason::Unchanged);
51
- }
52
-
53
- #[test]
54
- fn on_file_changed_adds_node_sorted_and_records_mtime() {
55
- // golden probe_leader.py:add b → pending ["b"];add a → sorted ["a","b"]。
56
- let s0 = on_file_changed(None, "b", 1.0);
57
- assert_eq!(s0.pending, vec!["b".to_string()]);
58
- assert_eq!(s0.mtimes.get("b"), Some(&1.0));
59
- let s1 = on_file_changed(Some(&s0), "a", 2.0);
60
- assert_eq!(s1.pending, vec!["a".to_string(), "b".to_string()], "pending 必须 sorted");
61
- assert_eq!(s1.mtimes.get("a"), Some(&2.0));
62
- // 重复 add b:pending 仍是 set(不重复),mtime 被更新到 3.0。
63
- let s2 = on_file_changed(Some(&s1), "b", 3.0);
64
- assert_eq!(s2.pending, vec!["a".to_string(), "b".to_string()]);
65
- assert_eq!(s2.mtimes.get("b"), Some(&3.0));
66
- }
67
-
68
- #[test]
69
- fn take_pending_drains_sorted_and_clears_pending_but_keeps_mtimes() {
70
- // golden probe_leader.py:drain → (["a","b"], state{pending:[], mtimes 保留})。
71
- let mut st = on_file_changed(None, "b", 3.0);
72
- st = on_file_changed(Some(&st), "a", 2.0);
73
- let (drained, after) = take_pending(Some(&st));
74
- assert_eq!(drained, vec!["a".to_string(), "b".to_string()]);
75
- assert!(after.pending.is_empty());
76
- // mtimes 不被 drain 清空。
77
- assert_eq!(after.mtimes.get("a"), Some(&2.0));
78
- assert_eq!(after.mtimes.get("b"), Some(&3.0));
79
- // 再 drain → 空。
80
- let (drained2, _after2) = take_pending(Some(&after));
81
- assert!(drained2.is_empty());
82
- // drain None → ([], default state)。
83
- let (none_drained, none_state) = take_pending(None);
84
- assert!(none_drained.is_empty());
85
- assert!(none_state.pending.is_empty());
86
- }
87
-
88
- // =====================================================================
89
- // 5. leader_session_name — sha1 派生 + 文件夹消毒(unimplemented → RED)
90
- // =====================================================================
91
-
92
- // 公式:team-agent-leader-<provider>-<sanitized folder[:48]>-<sha1(resolve(ws))[:8]>。
93
- // 用真实 temp 目录,sha1/sanitize 在测试内复算后断言函数输出与之一致(probe 已验证公式)。
94
- #[test]
95
- fn leader_session_name_formula_and_sanitization() {
96
- // 公式 = team-agent-leader-<provider>-<sanitized folder>-<sha1(resolve(ws))[:8]>。
97
- // sha1 复算需 sha1 crate(本测试不引);改为断言格式不变量(provider/消毒/8-hex 后缀),
98
- // 字节级 sha1 由 golden probe_leader.py 已验证公式正确。
99
- let base = std::env::temp_dir().join(format!("ta_rs_lsn_{}", std::process::id()));
100
- let weird = base.join("My Proj!@#name");
101
- std::fs::create_dir_all(&weird).unwrap();
102
- let got = leader_session_name(Provider::Codex, &weird);
103
- // 前缀 + provider + 消毒后的 folder(非字母数字_.- → '_')。
104
- let s = got.as_str();
105
- assert!(s.starts_with("team-agent-leader-codex-My_Proj___name-"), "got {s}");
106
- // sha1[:8] 后缀:8 个 hex。
107
- let suffix = s.rsplit('-').next().unwrap();
108
- assert_eq!(suffix.len(), 8, "sha1 前缀须 8 hex,got {suffix}");
109
- assert!(suffix.chars().all(|c| c.is_ascii_hexdigit()));
110
- }
111
-
112
- // folder 名消毒成空 → 回退 "workspace"(probe_leader.py allsym 用例)。
113
- #[test]
114
- fn leader_session_name_empty_sanitized_folder_falls_back_to_workspace() {
115
- let base = std::env::temp_dir().join(format!("ta_rs_lsn2_{}", std::process::id()));
116
- // 全符号目录名 → 消毒后 strip('._-') 为空 → "workspace"。
117
- let allsym = base.join("@@@");
118
- std::fs::create_dir_all(&allsym).unwrap();
119
- let got = leader_session_name(Provider::Codex, &allsym);
120
- assert!(
121
- got.as_str().contains("-workspace-"),
122
- "全符号 folder 应回退 'workspace',got {}",
123
- got.as_str()
124
- );
125
- }
126
-
127
- // claude_code provider 出现在 session 名里(probe:team-agent-leader-claude_code-...)。
128
- #[test]
129
- fn leader_session_name_uses_claude_code_provider_string() {
130
- let base = std::env::temp_dir().join(format!("ta_rs_lsn3_{}", std::process::id()));
131
- let dir = base.join("proj");
132
- std::fs::create_dir_all(&dir).unwrap();
133
- let got = leader_session_name(Provider::ClaudeCode, &dir);
134
- assert!(got.as_str().starts_with("team-agent-leader-claude_code-proj-"), "got {}", got.as_str());
135
- }
136
-
137
- #[test]
138
- fn leader_session_name_uses_copilot_provider_string() {
139
- let base = std::env::temp_dir().join(format!("ta_rs_lsn_copilot_{}", std::process::id()));
140
- let dir = base.join("proj");
141
- std::fs::create_dir_all(&dir).unwrap();
142
- let got = leader_session_name(Provider::Copilot, &dir);
143
- assert!(
144
- got.as_str().starts_with("team-agent-leader-copilot-proj-"),
145
- "got {}",
146
- got.as_str()
147
- );
148
- }
149
-
150
- // =====================================================================
151
- // 6. Family A 正源 owner 绑定 — bind_owner_from_caller_pane(unimplemented → RED)
152
- // =====================================================================
153
-
154
- // $TMUX_PANE 缺 → refuse + reason=caller_pane_missing(leader_binding.py:79-95)。
155
- // 此处只能在 $TMUX_PANE 缺失环境下断言(测试进程通常无 TMUX_PANE)。
156
- #[test]
157
- fn bind_owner_refuses_when_caller_pane_missing() {
158
- // 防御:确保本测试看到的环境无 TMUX_PANE(若 CI 在 tmux 内跑,跳过断言形态)。
159
- if std::env::var_os("TMUX_PANE").is_some() {
160
- // 在 tmux 内:正源存在,不该走 refuse 分支;此用例只验缺失分支,直接返回。
161
- return;
162
- }
163
- let ws = std::env::temp_dir().join(format!("ta_rs_bind_{}", std::process::id()));
164
- std::fs::create_dir_all(&ws).unwrap();
165
- let team = TeamKey::new("default");
166
- let res = bind_owner_from_caller_pane(&ws, &team, None).unwrap();
167
- assert!(!res.ok);
168
- assert_eq!(res.reason, Some(LeaseReason::CallerPaneMissing));
169
- // caller_pane_id 为空(probe:""),hint 为 _HINT_RUN_FROM_LEADER_PANE。
170
- assert_eq!(res.caller_pane_id, PaneId::new(""));
171
- assert_eq!(res.caller_current_command, "");
172
- assert_eq!(
175
+ let ws = std::env::temp_dir().join(format!("ta_rs_bind_{}", std::process::id()));
176
+ std::fs::create_dir_all(&ws).unwrap();
177
+ let team = TeamKey::new("default");
178
+ let res = bind_owner_from_caller_pane(&ws, &team, None).unwrap();
179
+ assert!(!res.ok);
180
+ assert_eq!(res.reason, Some(LeaseReason::CallerPaneMissing));
181
+ // caller_pane_id 为空(probe:""),hint 为 _HINT_RUN_FROM_LEADER_PANE。
182
+ assert_eq!(res.caller_pane_id, PaneId::new(""));
183
+ assert_eq!(res.caller_current_command, "");
184
+ assert_eq!(
173
185
  res.hint.as_deref(),
174
186
  Some("run team-agent from inside your leader pane (the tmux pane you want to own this team).")
175
187
  );
176
- assert_eq!(res.team_id, team);
177
- }
178
-
179
- // owner.bind_refused 事件名字节锁(LeaderEvent::name unimplemented → RED;与 #5 重叠但锁 binding 路径)。
180
- #[test]
181
- fn owner_bind_refused_event_name_is_owner_bind_refused() {
182
- assert_eq!(LeaderEvent::OwnerBindRefused.name(), "owner.bind_refused");
183
- }
184
-
185
- // emit_owner_bound_event:成功绑定 hook(owner.bound_from_caller_pane;leader_binding.py:162-183)。
186
- // 强化(no-full-uuid-leak 命门):事件只写 derived_uuid_prefix == derived[:12](12 hex),
187
- // old uuid 为 None → old_uuid_prefix == ""(空串,非缺省);全 32 hex uuid 绝不出现在任何字段。
188
- // unimplemented → RED。
189
- #[test]
190
- fn emit_owner_bound_event_logs_prefix_only_never_full_uuid() {
191
- let ws = std::env::temp_dir().join(format!("ta_rs_emit_{}", std::process::id()));
192
- std::fs::create_dir_all(&ws).unwrap();
193
- let caller = PaneId::new("%7");
194
- let derived = uuid("fp", "/ws", "u", "default");
195
- let full = derived.as_str().to_string();
196
- assert_eq!(full.len(), 32, "derive 产 32 hex");
197
- let prefix12 = full[..12].to_string();
198
- emit_owner_bound_event(&ws, &caller, "codex", &derived, &TeamKey::new("default"), None).unwrap();
199
- // 读回审计事件:恰一条 owner.bound_from_caller_pane。
200
- let events = crate::event_log::EventLog::new(&ws).tail(50).unwrap();
201
- let ev = events
202
- .iter()
203
- .find(|e| e["event"] == serde_json::json!("owner.bound_from_caller_pane"))
204
- .expect("必写 owner.bound_from_caller_pane");
205
- assert_eq!(ev["caller_pane_id"], serde_json::json!("%7"));
206
- assert_eq!(ev["caller_current_command"], serde_json::json!("codex"));
207
- assert_eq!(ev["team_id"], serde_json::json!("default"));
208
- // derived_uuid_prefix == derived[:12](只前缀,12 hex)
209
- assert_eq!(ev["derived_uuid_prefix"], serde_json::json!(prefix12));
210
- // old uuid=None → old_uuid_prefix == ""(空串,非 null/缺省;golden probe 已验)
211
- assert_eq!(ev["old_uuid_prefix"], serde_json::json!(""));
212
- // no-full-uuid-leak:整条事件序列化文本里绝不出现完整 32-hex uuid。
213
- let raw = serde_json::to_string(ev).unwrap();
214
- assert!(!raw.contains(&full), "审计事件绝不泄露完整 leader_session_uuid");
215
- // 审计事件名字节锁。
216
- assert_eq!(LeaderEvent::OwnerBoundFromCallerPane.name(), "owner.bound_from_caller_pane");
217
- }
188
+ assert_eq!(res.team_id, team);
189
+ }
190
+
191
+ // owner.bind_refused 事件名字节锁(LeaderEvent::name unimplemented → RED;与 #5 重叠但锁 binding 路径)。
192
+ #[test]
193
+ fn owner_bind_refused_event_name_is_owner_bind_refused() {
194
+ assert_eq!(LeaderEvent::OwnerBindRefused.name(), "owner.bind_refused");
195
+ }
196
+
197
+ // emit_owner_bound_event:成功绑定 hook(owner.bound_from_caller_pane;leader_binding.py:162-183)。
198
+ // 强化(no-full-uuid-leak 命门):事件只写 derived_uuid_prefix == derived[:12](12 hex),
199
+ // old uuid 为 None → old_uuid_prefix == ""(空串,非缺省);全 32 hex uuid 绝不出现在任何字段。
200
+ // unimplemented → RED。
201
+ #[test]
202
+ fn emit_owner_bound_event_logs_prefix_only_never_full_uuid() {
203
+ let ws = std::env::temp_dir().join(format!("ta_rs_emit_{}", std::process::id()));
204
+ std::fs::create_dir_all(&ws).unwrap();
205
+ let caller = PaneId::new("%7");
206
+ let derived = uuid("fp", "/ws", "u", "default");
207
+ let full = derived.as_str().to_string();
208
+ assert_eq!(full.len(), 32, "derive 产 32 hex");
209
+ let prefix12 = full[..12].to_string();
210
+ emit_owner_bound_event(
211
+ &ws,
212
+ &caller,
213
+ "codex",
214
+ &derived,
215
+ &TeamKey::new("default"),
216
+ None,
217
+ )
218
+ .unwrap();
219
+ // 读回审计事件:恰一条 owner.bound_from_caller_pane。
220
+ let events = crate::event_log::EventLog::new(&ws).tail(50).unwrap();
221
+ let ev = events
222
+ .iter()
223
+ .find(|e| e["event"] == serde_json::json!("owner.bound_from_caller_pane"))
224
+ .expect("必写 owner.bound_from_caller_pane");
225
+ assert_eq!(ev["caller_pane_id"], serde_json::json!("%7"));
226
+ assert_eq!(ev["caller_current_command"], serde_json::json!("codex"));
227
+ assert_eq!(ev["team_id"], serde_json::json!("default"));
228
+ // derived_uuid_prefix == derived[:12](只前缀,12 hex)
229
+ assert_eq!(ev["derived_uuid_prefix"], serde_json::json!(prefix12));
230
+ // old uuid=None → old_uuid_prefix == ""(空串,非 null/缺省;golden probe 已验)。
231
+ assert_eq!(ev["old_uuid_prefix"], serde_json::json!(""));
232
+ // no-full-uuid-leak:整条事件序列化文本里绝不出现完整 32-hex uuid。
233
+ let raw = serde_json::to_string(ev).unwrap();
234
+ assert!(
235
+ !raw.contains(&full),
236
+ "审计事件绝不泄露完整 leader_session_uuid"
237
+ );
238
+ // 审计事件名字节锁。
239
+ assert_eq!(
240
+ LeaderEvent::OwnerBoundFromCallerPane.name(),
241
+ "owner.bound_from_caller_pane"
242
+ );
243
+ }
@@ -69,10 +69,10 @@ pub mod transport;
69
69
  // step 10-12 (leader/messaging/coordinator) — ROUND-0.5 类型+fn surface 骨架(fn body unimplemented!(),
70
70
  // P2 porter 落实现)。leader=lease/owner-bind/idle-takeover;messaging=send/deliver/retry/watchers;
71
71
  // coordinator=daemon tick(§10 no-panic,Result<TickReport,TickError>)。
72
- pub mod leader;
73
- pub mod messaging;
74
72
  pub mod coordinator;
75
73
  pub mod diagnose;
74
+ pub mod leader;
75
+ pub mod messaging;
76
76
 
77
77
  // step 13-15 (lifecycle/mcp_server/cli/packaging) — ROUND-0.5b behavioral-rich 骨架(entry-fn 签名 +
78
78
  // 富返回类型,fn body unimplemented!(),P2 porter 落实现)。lifecycle=quick-start/restart/display;
@@ -87,11 +87,11 @@ pub mod lifecycle;
87
87
  pub mod platform;
88
88
  // 0.3.28 — unified adaptive layout manager (single source of truth for tmux
89
89
  // topology decisions). See `.team/artifacts/adaptive-layout-full-architecture-locate.md`.
90
+ pub mod cli;
90
91
  pub mod layout;
91
- pub mod topology;
92
92
  pub mod mcp_server;
93
- pub mod cli;
94
93
  pub mod packaging;
94
+ pub mod topology;
95
95
 
96
96
  // fake-worker — subscription-free backing program for Provider::Fake; lets the real spawn path
97
97
  // (launch dry_run=false → tmux window) be exercised with no real provider (port of fake_worker.py).
@@ -217,7 +217,10 @@ fn open_adaptive_worker_displays(
217
217
  workspace_window: target.window.clone(),
218
218
  pane_id: target.pane_id.clone(),
219
219
  pane_title: Some(agent_id),
220
- target: target.pane_id.as_ref().map(|pane| pane.as_str().to_string()),
220
+ target: target
221
+ .pane_id
222
+ .as_ref()
223
+ .map(|pane| pane.as_str().to_string()),
221
224
  target_worker_session: Some(session_name.as_str().to_string()),
222
225
  linked_session: None,
223
226
  leader_session: Some(session_name.clone()),
@@ -279,7 +282,9 @@ fn close_adaptive_displays(
279
282
  }
280
283
  }
281
284
  if let Some(leader_session) = adaptive_leader_session(&state) {
282
- for target in close_adaptive_windows(&transport, leader_session.as_str(), session_name.as_str()) {
285
+ for target in
286
+ close_adaptive_windows(&transport, leader_session.as_str(), session_name.as_str())
287
+ {
283
288
  orphans_cleaned.push(target.clone());
284
289
  if seen.insert(target.clone()) {
285
290
  closed.push(target);
@@ -481,4 +486,3 @@ fn string_field(display: &serde_json::Map<String, serde_json::Value>, key: &str)
481
486
  .filter(|s| !s.is_empty())
482
487
  .map(str::to_string)
483
488
  }
484
-
@@ -139,7 +139,12 @@ pub fn launch_with_transport_in_workspace(
139
139
  .map(|id| id == agent.agent_id.as_str())
140
140
  .unwrap_or(false)
141
141
  })
142
- .and_then(|entry| entry.get("provider").and_then(Value::as_str).map(str::to_string))
142
+ .and_then(|entry| {
143
+ entry
144
+ .get("provider")
145
+ .and_then(Value::as_str)
146
+ .map(str::to_string)
147
+ })
143
148
  .unwrap_or_else(|| "fake".to_string());
144
149
  crate::lifecycle::restart::write_worker_spawn_timing_event(
145
150
  workspace,
@@ -28,7 +28,15 @@
28
28
  //! contracts blitz 可 NAME 这些类型并 CALL 这些 fn、断言其 rich return。
29
29
 
30
30
  // ROUND-0 skeleton:fn body 全 unimplemented!() → import/field/param/大 Err 暂未落地;P2 porter 实现时移除。
31
- #![allow(dead_code, unused_imports, unused_variables, clippy::result_large_err, clippy::doc_overindented_list_items, clippy::doc_lazy_continuation, clippy::io_other_error)]
31
+ #![allow(
32
+ dead_code,
33
+ unused_imports,
34
+ unused_variables,
35
+ clippy::result_large_err,
36
+ clippy::doc_overindented_list_items,
37
+ clippy::doc_lazy_continuation,
38
+ clippy::io_other_error
39
+ )]
32
40
  // §10:lifecycle 写路径(save_runtime_state/snapshot/plan_state,bug-084 高危)实现层禁 unwrap/expect/panic
33
41
  // (unimplemented!() stub 不被拦);tests 子模块各自 allow。
34
42
  #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]