@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,532 +1,568 @@
1
- // ════════════════════════════════════════════════════════════════════════
2
- // GROUP A — serde 字节锁:verification / stage / backend 审计字符串
3
- // (tmux_io.py + tmux_prompt.py 散布字符串穷尽枚举;09-transport.md 表 §38-42)。
4
- // 这些是审计/事件 wire 值,改一个字节 = 下游 recognizer/事件断流。
5
- // ════════════════════════════════════════════════════════════════════════
6
- use super::*;
7
-
8
- #[test]
9
- fn backend_kind_serde_snake_case() {
10
- // BackendKind 诊断/事件用(transport-backend-design.md §1.3)。
11
- assert_eq!(serde_json::to_string(&BackendKind::Tmux).unwrap(), "\"tmux\"");
12
- assert_eq!(
13
- serde_json::to_string(&BackendKind::WezTerm).unwrap(),
14
- "\"wez_term\""
15
- );
16
- assert_eq!(
17
- serde_json::to_string(&BackendKind::ConPty).unwrap(),
18
- "\"con_pty\""
19
- );
1
+ // ════════════════════════════════════════════════════════════════════════
2
+ // GROUP A — serde 字节锁:verification / stage / backend 审计字符串
3
+ // (tmux_io.py + tmux_prompt.py 散布字符串穷尽枚举;09-transport.md 表 §38-42)。
4
+ // 这些是审计/事件 wire 值,改一个字节 = 下游 recognizer/事件断流。
5
+ // ════════════════════════════════════════════════════════════════════════
6
+ use super::*;
7
+
8
+ #[test]
9
+ fn backend_kind_serde_snake_case() {
10
+ // BackendKind 诊断/事件用(transport-backend-design.md §1.3)。
11
+ assert_eq!(
12
+ serde_json::to_string(&BackendKind::Tmux).unwrap(),
13
+ "\"tmux\""
14
+ );
15
+ assert_eq!(
16
+ serde_json::to_string(&BackendKind::WezTerm).unwrap(),
17
+ "\"wez_term\""
18
+ );
19
+ assert_eq!(
20
+ serde_json::to_string(&BackendKind::ConPty).unwrap(),
21
+ "\"con_pty\""
22
+ );
23
+ }
24
+
25
+ #[test]
26
+ fn inject_stage_serde_kebab_case_byte_locked() {
27
+ // 09-transport.md 表 §41:失败定位阶段,kebab-case 显式 rename。
28
+ let pairs = [
29
+ (InjectStage::SendKeys, "\"send-keys\""),
30
+ (InjectStage::PrePasteCapture, "\"pre-paste-capture\""),
31
+ (InjectStage::SetBuffer, "\"set-buffer\""),
32
+ (InjectStage::LoadBuffer, "\"load-buffer\""),
33
+ (InjectStage::PasteBuffer, "\"paste-buffer\""),
34
+ (InjectStage::DeleteBuffer, "\"delete-buffer\""),
35
+ (InjectStage::PrePastePaneState, "\"pre-paste-pane-state\""),
36
+ (InjectStage::PaneModeCheck, "\"pane-mode-check\""),
37
+ (InjectStage::Submit, "\"submit\""),
38
+ (InjectStage::VisibleCheck, "\"visible-check\""),
39
+ ];
40
+ for (stage, wire) in pairs {
41
+ assert_eq!(serde_json::to_string(&stage).unwrap(), wire, "{stage:?}");
20
42
  }
21
-
22
- #[test]
23
- fn inject_stage_serde_kebab_case_byte_locked() {
24
- // 09-transport.md 表 §41:失败定位阶段,kebab-case 显式 rename。
25
- let pairs = [
26
- (InjectStage::SendKeys, "\"send-keys\""),
27
- (InjectStage::PrePasteCapture, "\"pre-paste-capture\""),
28
- (InjectStage::SetBuffer, "\"set-buffer\""),
29
- (InjectStage::LoadBuffer, "\"load-buffer\""),
30
- (InjectStage::PasteBuffer, "\"paste-buffer\""),
31
- (InjectStage::DeleteBuffer, "\"delete-buffer\""),
32
- (InjectStage::PrePastePaneState, "\"pre-paste-pane-state\""),
33
- (InjectStage::PaneModeCheck, "\"pane-mode-check\""),
34
- (InjectStage::Submit, "\"submit\""),
35
- (InjectStage::VisibleCheck, "\"visible-check\""),
36
- ];
37
- for (stage, wire) in pairs {
38
- assert_eq!(serde_json::to_string(&stage).unwrap(), wire, "{stage:?}");
39
- }
40
- }
41
-
42
- #[test]
43
- fn inject_verification_serde_snake_case_byte_locked() {
44
- // 09-transport.md 表 §38。注意 EmptyTextSendKeys = 空文本走纯 send-keys 的验证。
45
- let pairs = [
46
- (
47
- InjectVerification::CaptureContainsToken,
48
- "\"capture_contains_token\"",
49
- ),
50
- (
51
- InjectVerification::CaptureContainsMessageFragment,
52
- "\"capture_contains_message_fragment\"",
53
- ),
54
- (
55
- InjectVerification::CaptureContainsNewPastedContentPrompt,
56
- "\"capture_contains_new_pasted_content_prompt\"",
57
- ),
58
- (InjectVerification::NoToken, "\"no_token\""),
59
- (
60
- InjectVerification::CaptureMissingToken,
61
- "\"capture_missing_token\"",
62
- ),
63
- (
64
- InjectVerification::EmptyTextSendKeys,
65
- "\"empty_text_send_keys\"",
66
- ),
67
- ];
68
- for (v, wire) in pairs {
69
- assert_eq!(serde_json::to_string(&v).unwrap(), wire, "{v:?}");
70
- }
71
- }
72
-
73
- #[test]
74
- fn turn_verification_serde_snake_case_byte_locked() {
75
- // 09-transport.md 表 §40;Gap42:not_yet_observed 也算成功,绝非投递闸门。
76
- let pairs = [
77
- (
78
- TurnVerification::LeaderNewTurnBoundaryVerified,
79
- "\"leader_new_turn_boundary_verified\"",
80
- ),
81
- (
82
- TurnVerification::LeaderNewTurnBoundaryMissing,
83
- "\"leader_new_turn_boundary_missing\"",
84
- ),
85
- (TurnVerification::NotYetObserved, "\"not_yet_observed\""),
86
- (TurnVerification::NotRequired, "\"not_required\""),
87
- ];
88
- for (v, wire) in pairs {
89
- assert_eq!(serde_json::to_string(&v).unwrap(), wire, "{v:?}");
90
- }
91
- }
92
-
93
- #[test]
94
- fn submit_verification_wire_strings_byte_locked() {
95
- // 09-transport.md 表 §39 + tmux_io.py:64/215-221 + tmux_prompt.py:304/313。
96
- // SubmitVerification 携 Key 模板,Python 是散字符串 → 这里钉死 to-wire 映射。
97
- // RED via stub:submit_verification_wire unimplemented!()。
98
- assert_eq!(
99
- submit_verification_wire(SubmitVerification::EnterSentWithoutPlaceholderCheck),
100
- "enter_sent_without_placeholder_check"
101
- );
102
- assert_eq!(
103
- submit_verification_wire(SubmitVerification::PastedContentPromptAbsentAfterSubmit),
104
- "pasted_content_prompt_absent_after_submit"
105
- );
106
- assert_eq!(
107
- submit_verification_wire(SubmitVerification::PastedContentPromptStillPresentAfterSubmit),
108
- "pasted_content_prompt_still_present_after_submit"
109
- );
110
- assert_eq!(
111
- submit_verification_wire(SubmitVerification::SendKeysFailed),
112
- "send_keys_failed"
113
- );
114
- // 模板 `{key}_sent_after_visible_token`:key 取 tmux 字面键名(submit_key)。
115
- // golden:Enter → "Enter_sent_after_visible_token"(tmux_io.py:218,submit_key 字面)。
116
- assert_eq!(
117
- submit_verification_wire(SubmitVerification::KeySentAfterVisibleToken {
118
- key: Key::Enter
119
- }),
120
- "Enter_sent_after_visible_token"
121
- );
122
- }
123
-
124
- #[test]
125
- fn pane_liveness_three_valued_reused_from_model() {
126
- // 裁决:Liveness{Live/Dead/Unknown} == model::enums::PaneLiveness(bug-085 穷尽三态)。
127
- // unknown ≠ dead ≠ live;serde 小写(与 state.py 对齐)。
128
- assert_eq!(
129
- serde_json::to_string(&PaneLiveness::Live).unwrap(),
130
- "\"live\""
131
- );
132
- assert_eq!(
133
- serde_json::to_string(&PaneLiveness::Dead).unwrap(),
134
- "\"dead\""
135
- );
136
- assert_eq!(
137
- serde_json::to_string(&PaneLiveness::Unknown).unwrap(),
138
- "\"unknown\""
139
- );
140
- }
141
-
142
- // ════════════════════════════════════════════════════════════════════════
143
- // GROUP B — id newtype 字节透明(== 裸字符串;沿用 model::ids 风格)
144
- // ════════════════════════════════════════════════════════════════════════
145
-
146
- #[test]
147
- fn pane_id_transparent_bytes_equal_raw_string() {
148
- // tmux `%7` 字节级 == 裸字符串(serde transparent)。
149
- assert_eq!(serde_json::to_string(&PaneId::new("%7")).unwrap(), "\"%7\"");
150
- assert_eq!(PaneId::from("%12").as_str(), "%12");
151
- assert_eq!(PaneId::new("%3").to_string(), "%3");
152
- // 反序列化往返。
153
- let back: PaneId = serde_json::from_str("\"%99\"").unwrap();
154
- assert_eq!(back, PaneId::new("%99"));
155
- }
156
-
157
- #[test]
158
- fn session_window_name_transparent_bytes() {
159
- assert_eq!(
160
- serde_json::to_string(&SessionName::new("team-sess")).unwrap(),
161
- "\"team-sess\""
162
- );
163
- assert_eq!(
164
- serde_json::to_string(&WindowName::new("win-1")).unwrap(),
165
- "\"win-1\""
166
- );
167
- assert_eq!(SessionName::from("s").as_str(), "s");
168
- assert_eq!(WindowName::from("w").to_string(), "w");
169
- }
170
-
171
- // ════════════════════════════════════════════════════════════════════════
172
- // GROUP C — Target 寻址稳定性(禁混传:Pane vs SessionWindow 类型上区分)
173
- // contracts-rust-native §2: spawn 返回的稳定 Target,后续 inject/capture 命中同进程。
174
- // ════════════════════════════════════════════════════════════════════════
175
-
176
- #[test]
177
- fn target_pane_and_session_window_are_distinct_addressings() {
178
- // 两种合法寻址不可混传;同名 PaneId vs SessionWindow 不相等。
179
- let p = Target::Pane(PaneId::new("%7"));
180
- let sw = Target::SessionWindow {
181
- session: SessionName::new("team-sess"),
182
- window: WindowName::new("%7"),
183
- };
184
- assert_ne!(p, sw);
185
- // Pane 寻址按 PaneId 字节相等。
186
- assert_eq!(
187
- Target::Pane(PaneId::new("%7")),
188
- Target::Pane(PaneId::new("%7"))
189
- );
190
- // SessionWindow 按 (session,window) 对相等;任一不同即不等。
191
- let a = Target::SessionWindow {
192
- session: SessionName::new("s"),
193
- window: WindowName::new("w"),
194
- };
195
- let b = Target::SessionWindow {
196
- session: SessionName::new("s"),
197
- window: WindowName::new("w2"),
198
- };
199
- assert_ne!(a, b);
200
- }
201
-
202
- #[test]
203
- fn spawn_first_returns_stable_addressable_target_then_reachable() {
204
- // contracts-rust-native: spawn_first 返回稳定可寻址 Target,且交回的 pane_id 能
205
- // 寻址回同一进程。RED:stub.spawn_first/capture unimplemented!()。
206
- let t = tmux();
207
- let env = BTreeMap::new();
208
- let spawned = t
209
- .spawn_first(
210
- &SessionName::new("team-sess"),
211
- &WindowName::new("win-1"),
212
- &["sh".into(), "-lc".into(), "echo hi".into()],
213
- Path::new("/tmp/ws"),
214
- &env,
215
- )
216
- .expect("spawn_first ok");
217
- // 身份正向登记:返回的 session/window 必须 == 请求的(不可凭空捏造别的)。
218
- assert_eq!(spawned.session, SessionName::new("team-sess"));
219
- assert_eq!(spawned.window, WindowName::new("win-1"));
220
- // 交回的 pane_id 必须能寻址回同一进程:capture 命中并能取到文本。
221
- let target = Target::Pane(spawned.pane_id.clone());
222
- let cap = t.capture(&target, CaptureRange::Tail(40)).expect("capture ok");
223
- assert_eq!(cap.range, CaptureRange::Tail(40));
224
-
225
- // 命令构造 golden(STEP-9 头号目标):first-spawn 必须是 new-session,
226
- // 不是 new-window(terminal.py:44-45 / runtime.py:1019-1020)。
227
- assert_eq!(
228
- tmux_spawn_argv(
229
- &SessionName::new("team-sess"),
230
- &WindowName::new("win-1"),
231
- "echo hi",
232
- true,
233
- ),
234
- vec![
235
- "tmux", "new-session", "-d", "-s", "team-sess", "-n", "win-1", "sh", "-lc",
236
- "echo hi",
237
- ]
238
- );
239
- }
240
-
241
- #[test]
242
- fn spawn_into_then_list_targets_enumerates_it() {
243
- // contracts-rust-native test_spawn_into_existing_session_parity。RED via stub。
244
- let t = tmux();
245
- let env = BTreeMap::new();
246
- let spawned = t
247
- .spawn_into(
248
- &SessionName::new("team-sess"),
249
- &WindowName::new("worker-2"),
250
- &["sh".into()],
251
- Path::new("/tmp/ws"),
252
- &env,
253
- )
254
- .expect("spawn_into ok");
255
- let listed = t.list_targets().expect("list_targets ok");
256
- // 枚举出的那条必须 == spawn 请求(porter 不能用同一捏造 pane_id 让 spawn_into
257
- // 与 list_targets 串通假绿):session/window_name/active 都要对上。
258
- let info = listed
259
- .iter()
260
- .find(|p| p.pane_id == spawned.pane_id)
261
- .expect("spawned worker must be enumerated by list_targets");
262
- assert_eq!(info.session, SessionName::new("team-sess"));
263
- assert_eq!(info.window_name, Some(WindowName::new("worker-2")));
264
- assert!(info.active, "freshly spawned worker pane is active");
265
-
266
- // 命令构造 golden:spawn_into 是 new-window(不是 new-session),挂到既有 session。
267
- assert_eq!(
268
- tmux_spawn_argv(
269
- &SessionName::new("team-sess"),
270
- &WindowName::new("worker-2"),
271
- "sh",
272
- false,
273
- ),
274
- // E53 (0.3.26): new-window now carries `-d` (detached, no focus steal).
275
- vec!["tmux", "new-window", "-d", "-t", "team-sess", "-n", "worker-2", "sh", "-lc", "sh"]
276
- );
277
- }
278
-
279
- // ════════════════════════════════════════════════════════════════════════
280
- // GROUP D — InjectPayload 空文本分流(trust turn-integrity 契约 §3,bug)
281
- // text=="" 走纯 send submit-key,禁 set/load/paste-buffer 空串(tmux 拒空 buffer
282
- // 会卡 trust prompt)。golden /tmp/transport_golden.py: empty_inject_*。
283
- // ════════════════════════════════════════════════════════════════════════
284
-
285
- #[test]
286
- fn inject_payload_empty_is_typed_distinct_from_empty_text() {
287
- // 类型上把空文本与含字符文本分流(InjectPayload::Empty != Text("..."))。
288
- assert_ne!(InjectPayload::Empty, InjectPayload::Text(String::new()));
289
- assert_eq!(
290
- InjectPayload::Text("hi".into()),
291
- InjectPayload::Text("hi".into())
292
- );
293
- }
294
-
295
- #[test]
296
- fn inject_empty_payload_reports_empty_text_send_keys_and_turn_not_required() {
297
- // golden empty_inject_report:
298
- // verification=empty_text_send_keys, turn_verification=not_required,
299
- // stage=submitted(Rust:stage_reached=Submit), submitted=true, attempts=1。
300
- // 空文本禁走 buffer:只发一个 submit-key(golden empty_inject_calls 仅 1 条 send-keys)。
301
- // RED:stub.inject unimplemented!()。
302
- let t = tmux();
303
- let report = t
304
- .inject(
305
- &Target::Pane(PaneId::new("%7")),
306
- &InjectPayload::Empty,
307
- Key::Enter,
308
- false,
309
- )
310
- .expect("inject empty ok");
311
- assert_eq!(report.inject_verification, InjectVerification::EmptyTextSendKeys);
312
- assert_eq!(report.turn_verification, TurnVerification::NotRequired);
313
- assert_eq!(report.stage_reached, InjectStage::Submit);
314
- assert_eq!(report.attempts, 1);
43
+ }
44
+
45
+ #[test]
46
+ fn inject_verification_serde_snake_case_byte_locked() {
47
+ // 09-transport.md 表 §38。注意 EmptyTextSendKeys = 空文本走纯 send-keys 的验证。
48
+ let pairs = [
49
+ (
50
+ InjectVerification::CaptureContainsToken,
51
+ "\"capture_contains_token\"",
52
+ ),
53
+ (
54
+ InjectVerification::CaptureContainsMessageFragment,
55
+ "\"capture_contains_message_fragment\"",
56
+ ),
57
+ (
58
+ InjectVerification::CaptureContainsNewPastedContentPrompt,
59
+ "\"capture_contains_new_pasted_content_prompt\"",
60
+ ),
61
+ (InjectVerification::NoToken, "\"no_token\""),
62
+ (
63
+ InjectVerification::CaptureMissingToken,
64
+ "\"capture_missing_token\"",
65
+ ),
66
+ (
67
+ InjectVerification::EmptyTextSendKeys,
68
+ "\"empty_text_send_keys\"",
69
+ ),
70
+ ];
71
+ for (v, wire) in pairs {
72
+ assert_eq!(serde_json::to_string(&v).unwrap(), wire, "{v:?}");
315
73
  }
316
-
317
- #[test]
318
- fn empty_inject_is_single_direct_send_keys_never_buffer() {
319
- // 命令构造 golden(STEP-9):空文本禁走 buffer,只发一条 send-keys 提交键
320
- // (tmux_io.py:42 —— tmux 拒空 buffer 会卡 trust prompt)。
321
- // golden empty_inject_calls 仅 1 条:send-keys -t %7 Enter。RED via stub。
322
- assert_eq!(
323
- tmux_empty_inject_argv(&PaneId::new("%7"), Key::Enter),
324
- vec!["tmux", "send-keys", "-t", "%7", "Enter"]
325
- );
74
+ }
75
+
76
+ #[test]
77
+ fn turn_verification_serde_snake_case_byte_locked() {
78
+ // 09-transport.md §40;Gap42:not_yet_observed 也算成功,绝非投递闸门。
79
+ let pairs = [
80
+ (
81
+ TurnVerification::LeaderNewTurnBoundaryVerified,
82
+ "\"leader_new_turn_boundary_verified\"",
83
+ ),
84
+ (
85
+ TurnVerification::LeaderNewTurnBoundaryMissing,
86
+ "\"leader_new_turn_boundary_missing\"",
87
+ ),
88
+ (TurnVerification::NotYetObserved, "\"not_yet_observed\""),
89
+ (TurnVerification::NotRequired, "\"not_required\""),
90
+ ];
91
+ for (v, wire) in pairs {
92
+ assert_eq!(serde_json::to_string(&v).unwrap(), wire, "{v:?}");
326
93
  }
327
-
328
- #[test]
329
- fn inject_text_payload_bracketed_reaches_submit_with_token_verification() {
330
- // contracts-rust-native test_inject_text_visible_parity:
331
- // 非空文本走 set/load-buffer + paste-buffer(-p)+ submit;
332
- // token verification=capture_contains_token,turn metadata(Gap42)。
333
- // RED via stub。
334
- let t = tmux();
335
- let report = t
336
- .inject(
337
- &Target::Pane(PaneId::new("%7")),
338
- &InjectPayload::Text("hello [team-agent-token:abc]".into()),
339
- Key::Enter,
340
- true,
341
- )
342
- .expect("inject text ok");
343
- assert_eq!(report.stage_reached, InjectStage::Submit);
344
- assert_eq!(
345
- report.inject_verification,
346
- InjectVerification::CaptureContainsToken
347
- );
348
- // Gap42:此 fixture 无 leader-boundary 标记(provider 非 fake,2s 内未观测到新 turn
349
- // → tmux_io.py:192-193 `turn_verification = "not_yet_observed"`)。metadata-only,
350
- // 绝非投递闸门 —— golden 钉死为 NotYetObserved(不是 2-of-4 析取)。
351
- assert_eq!(report.turn_verification, TurnVerification::NotYetObserved);
352
-
353
- // 命令构造 golden(STEP-9 头号目标):非空文本走 set-buffer paste-buffer -p →
354
- // delete-buffer 序列(tmux_io.py:119/303/314)。-p == bracketed paste。
355
- let argv = tmux_inject_text_argv(
356
- &PaneId::new("%7"),
357
- "team-agent-buf",
358
- "hello [team-agent-token:abc]",
94
+ }
95
+
96
+ #[test]
97
+ fn submit_verification_wire_strings_byte_locked() {
98
+ // 09-transport.md 表 §39 + tmux_io.py:64/215-221 + tmux_prompt.py:304/313。
99
+ // SubmitVerification Key 模板,Python 是散字符串 → 这里钉死 to-wire 映射。
100
+ // RED via stub:submit_verification_wire unimplemented!()
101
+ assert_eq!(
102
+ submit_verification_wire(SubmitVerification::EnterSentWithoutPlaceholderCheck),
103
+ "enter_sent_without_placeholder_check"
104
+ );
105
+ assert_eq!(
106
+ submit_verification_wire(SubmitVerification::PastedContentPromptAbsentAfterSubmit),
107
+ "pasted_content_prompt_absent_after_submit"
108
+ );
109
+ assert_eq!(
110
+ submit_verification_wire(SubmitVerification::PastedContentPromptStillPresentAfterSubmit),
111
+ "pasted_content_prompt_still_present_after_submit"
112
+ );
113
+ assert_eq!(
114
+ submit_verification_wire(SubmitVerification::SendKeysFailed),
115
+ "send_keys_failed"
116
+ );
117
+ // 模板 `{key}_sent_after_visible_token`:key tmux 字面键名(submit_key)。
118
+ // golden:Enter → "Enter_sent_after_visible_token"(tmux_io.py:218,submit_key 字面)
119
+ assert_eq!(
120
+ submit_verification_wire(SubmitVerification::KeySentAfterVisibleToken { key: Key::Enter }),
121
+ "Enter_sent_after_visible_token"
122
+ );
123
+ }
124
+
125
+ #[test]
126
+ fn pane_liveness_three_valued_reused_from_model() {
127
+ // 裁决:Liveness{Live/Dead/Unknown} == model::enums::PaneLiveness(bug-085 穷尽三态)。
128
+ // unknown ≠ dead ≠ live;serde 小写(与 state.py 对齐)。
129
+ assert_eq!(
130
+ serde_json::to_string(&PaneLiveness::Live).unwrap(),
131
+ "\"live\""
132
+ );
133
+ assert_eq!(
134
+ serde_json::to_string(&PaneLiveness::Dead).unwrap(),
135
+ "\"dead\""
136
+ );
137
+ assert_eq!(
138
+ serde_json::to_string(&PaneLiveness::Unknown).unwrap(),
139
+ "\"unknown\""
140
+ );
141
+ }
142
+
143
+ // ════════════════════════════════════════════════════════════════════════
144
+ // GROUP B — id newtype 字节透明(== 裸字符串;沿用 model::ids 风格)
145
+ // ════════════════════════════════════════════════════════════════════════
146
+
147
+ #[test]
148
+ fn pane_id_transparent_bytes_equal_raw_string() {
149
+ // tmux `%7` 字节级 == 裸字符串(serde transparent)。
150
+ assert_eq!(serde_json::to_string(&PaneId::new("%7")).unwrap(), "\"%7\"");
151
+ assert_eq!(PaneId::from("%12").as_str(), "%12");
152
+ assert_eq!(PaneId::new("%3").to_string(), "%3");
153
+ // 反序列化往返。
154
+ let back: PaneId = serde_json::from_str("\"%99\"").unwrap();
155
+ assert_eq!(back, PaneId::new("%99"));
156
+ }
157
+
158
+ #[test]
159
+ fn session_window_name_transparent_bytes() {
160
+ assert_eq!(
161
+ serde_json::to_string(&SessionName::new("team-sess")).unwrap(),
162
+ "\"team-sess\""
163
+ );
164
+ assert_eq!(
165
+ serde_json::to_string(&WindowName::new("win-1")).unwrap(),
166
+ "\"win-1\""
167
+ );
168
+ assert_eq!(SessionName::from("s").as_str(), "s");
169
+ assert_eq!(WindowName::from("w").to_string(), "w");
170
+ }
171
+
172
+ // ════════════════════════════════════════════════════════════════════════
173
+ // GROUP C — Target 寻址稳定性(禁混传:Pane vs SessionWindow 类型上区分)
174
+ // contracts-rust-native §2: spawn 返回的稳定 Target,后续 inject/capture 命中同进程。
175
+ // ════════════════════════════════════════════════════════════════════════
176
+
177
+ #[test]
178
+ fn target_pane_and_session_window_are_distinct_addressings() {
179
+ // 两种合法寻址不可混传;同名 PaneId vs SessionWindow 不相等。
180
+ let p = Target::Pane(PaneId::new("%7"));
181
+ let sw = Target::SessionWindow {
182
+ session: SessionName::new("team-sess"),
183
+ window: WindowName::new("%7"),
184
+ };
185
+ assert_ne!(p, sw);
186
+ // Pane 寻址按 PaneId 字节相等。
187
+ assert_eq!(
188
+ Target::Pane(PaneId::new("%7")),
189
+ Target::Pane(PaneId::new("%7"))
190
+ );
191
+ // SessionWindow 按 (session,window) 对相等;任一不同即不等。
192
+ let a = Target::SessionWindow {
193
+ session: SessionName::new("s"),
194
+ window: WindowName::new("w"),
195
+ };
196
+ let b = Target::SessionWindow {
197
+ session: SessionName::new("s"),
198
+ window: WindowName::new("w2"),
199
+ };
200
+ assert_ne!(a, b);
201
+ }
202
+
203
+ #[test]
204
+ fn spawn_first_returns_stable_addressable_target_then_reachable() {
205
+ // contracts-rust-native: spawn_first 返回稳定可寻址 Target,且交回的 pane_id 能
206
+ // 寻址回同一进程。RED:stub.spawn_first/capture unimplemented!()。
207
+ let t = tmux();
208
+ let env = BTreeMap::new();
209
+ let spawned = t
210
+ .spawn_first(
211
+ &SessionName::new("team-sess"),
212
+ &WindowName::new("win-1"),
213
+ &["sh".into(), "-lc".into(), "echo hi".into()],
214
+ Path::new("/tmp/ws"),
215
+ &env,
216
+ )
217
+ .expect("spawn_first ok");
218
+ // 身份正向登记:返回的 session/window 必须 == 请求的(不可凭空捏造别的)。
219
+ assert_eq!(spawned.session, SessionName::new("team-sess"));
220
+ assert_eq!(spawned.window, WindowName::new("win-1"));
221
+ // 交回的 pane_id 必须能寻址回同一进程:capture 命中并能取到文本。
222
+ let target = Target::Pane(spawned.pane_id.clone());
223
+ let cap = t
224
+ .capture(&target, CaptureRange::Tail(40))
225
+ .expect("capture ok");
226
+ assert_eq!(cap.range, CaptureRange::Tail(40));
227
+
228
+ // 命令构造 golden(STEP-9 头号目标):first-spawn 必须是 new-session,
229
+ // 不是 new-window(terminal.py:44-45 / runtime.py:1019-1020)。
230
+ assert_eq!(
231
+ tmux_spawn_argv(
232
+ &SessionName::new("team-sess"),
233
+ &WindowName::new("win-1"),
234
+ "echo hi",
235
+ true,
236
+ ),
237
+ vec![
238
+ "tmux",
239
+ "new-session",
240
+ "-d",
241
+ "-s",
242
+ "team-sess",
243
+ "-n",
244
+ "win-1",
245
+ "sh",
246
+ "-lc",
247
+ "echo hi",
248
+ ]
249
+ );
250
+ }
251
+
252
+ #[test]
253
+ fn spawn_into_then_list_targets_enumerates_it() {
254
+ // contracts-rust-native test_spawn_into_existing_session_parity。RED via stub。
255
+ let t = tmux();
256
+ let env = BTreeMap::new();
257
+ let spawned = t
258
+ .spawn_into(
259
+ &SessionName::new("team-sess"),
260
+ &WindowName::new("worker-2"),
261
+ &["sh".into()],
262
+ Path::new("/tmp/ws"),
263
+ &env,
264
+ )
265
+ .expect("spawn_into ok");
266
+ let listed = t.list_targets().expect("list_targets ok");
267
+ // 枚举出的那条必须 == spawn 请求(porter 不能用同一捏造 pane_id 让 spawn_into
268
+ // 与 list_targets 串通假绿):session/window_name/active 都要对上。
269
+ let info = listed
270
+ .iter()
271
+ .find(|p| p.pane_id == spawned.pane_id)
272
+ .expect("spawned worker must be enumerated by list_targets");
273
+ assert_eq!(info.session, SessionName::new("team-sess"));
274
+ assert_eq!(info.window_name, Some(WindowName::new("worker-2")));
275
+ assert!(info.active, "freshly spawned worker pane is active");
276
+
277
+ // 命令构造 golden:spawn_into 是 new-window(不是 new-session),挂到既有 session。
278
+ assert_eq!(
279
+ tmux_spawn_argv(
280
+ &SessionName::new("team-sess"),
281
+ &WindowName::new("worker-2"),
282
+ "sh",
283
+ false,
284
+ ),
285
+ // E53 (0.3.26): new-window now carries `-d` (detached, no focus steal).
286
+ vec![
287
+ "tmux",
288
+ "new-window",
289
+ "-d",
290
+ "-t",
291
+ "team-sess",
292
+ "-n",
293
+ "worker-2",
294
+ "sh",
295
+ "-lc",
296
+ "sh"
297
+ ]
298
+ );
299
+ }
300
+
301
+ // ════════════════════════════════════════════════════════════════════════
302
+ // GROUP D — InjectPayload 空文本分流(trust turn-integrity 契约 §3,bug)
303
+ // text=="" 走纯 send submit-key,禁 set/load/paste-buffer 空串(tmux 拒空 buffer
304
+ // 会卡 trust prompt)。golden /tmp/transport_golden.py: empty_inject_*。
305
+ // ════════════════════════════════════════════════════════════════════════
306
+
307
+ #[test]
308
+ fn inject_payload_empty_is_typed_distinct_from_empty_text() {
309
+ // 类型上把空文本与含字符文本分流(InjectPayload::Empty != Text("..."))。
310
+ assert_ne!(InjectPayload::Empty, InjectPayload::Text(String::new()));
311
+ assert_eq!(
312
+ InjectPayload::Text("hi".into()),
313
+ InjectPayload::Text("hi".into())
314
+ );
315
+ }
316
+
317
+ #[test]
318
+ fn inject_empty_payload_reports_empty_text_send_keys_and_turn_not_required() {
319
+ // golden empty_inject_report:
320
+ // verification=empty_text_send_keys, turn_verification=not_required,
321
+ // stage=submitted(Rust:stage_reached=Submit), submitted=true, attempts=1。
322
+ // 空文本禁走 buffer:只发一个 submit-key(golden empty_inject_calls 仅 1 条 send-keys)。
323
+ // RED:stub.inject unimplemented!()。
324
+ let t = tmux();
325
+ let report = t
326
+ .inject(
327
+ &Target::Pane(PaneId::new("%7")),
328
+ &InjectPayload::Empty,
329
+ Key::Enter,
330
+ false,
331
+ )
332
+ .expect("inject empty ok");
333
+ assert_eq!(
334
+ report.inject_verification,
335
+ InjectVerification::EmptyTextSendKeys
336
+ );
337
+ assert_eq!(report.turn_verification, TurnVerification::NotRequired);
338
+ assert_eq!(report.stage_reached, InjectStage::Submit);
339
+ assert_eq!(report.attempts, 1);
340
+ }
341
+
342
+ #[test]
343
+ fn empty_inject_is_single_direct_send_keys_never_buffer() {
344
+ // 命令构造 golden(STEP-9):空文本禁走 buffer,只发一条 send-keys 提交键
345
+ // (tmux_io.py:42 —— tmux 拒空 buffer 会卡 trust prompt)。
346
+ // golden empty_inject_calls 仅 1 条:send-keys -t %7 Enter。RED via stub。
347
+ assert_eq!(
348
+ tmux_empty_inject_argv(&PaneId::new("%7"), Key::Enter),
349
+ vec!["tmux", "send-keys", "-t", "%7", "Enter"]
350
+ );
351
+ }
352
+
353
+ #[test]
354
+ fn inject_text_payload_bracketed_reaches_submit_with_token_verification() {
355
+ // contracts-rust-native test_inject_text_visible_parity:
356
+ // 非空文本走 set/load-buffer + paste-buffer(-p)+ submit;
357
+ // 有 token 时 verification=capture_contains_token,turn 仅 metadata(Gap42)。
358
+ // RED via stub。
359
+ let t = tmux();
360
+ let report = t
361
+ .inject(
362
+ &Target::Pane(PaneId::new("%7")),
363
+ &InjectPayload::Text("hello [team-agent-token:abc]".into()),
364
+ Key::Enter,
359
365
  true,
360
- );
361
- assert_eq!(
362
- argv,
366
+ )
367
+ .expect("inject text ok");
368
+ assert_eq!(report.stage_reached, InjectStage::Submit);
369
+ assert_eq!(
370
+ report.inject_verification,
371
+ InjectVerification::CaptureContainsToken
372
+ );
373
+ // Gap42:此 fixture 无 leader-boundary 标记(provider 非 fake,2s 内未观测到新 turn
374
+ // → tmux_io.py:192-193 `turn_verification = "not_yet_observed"`)。metadata-only,
375
+ // 绝非投递闸门 —— golden 钉死为 NotYetObserved(不是 2-of-4 析取)。
376
+ assert_eq!(report.turn_verification, TurnVerification::NotYetObserved);
377
+
378
+ // 命令构造 golden(STEP-9 头号目标):非空文本走 set-buffer → paste-buffer -p →
379
+ // delete-buffer 序列(tmux_io.py:119/303/314)。-p == bracketed paste。
380
+ let argv = tmux_inject_text_argv(
381
+ &PaneId::new("%7"),
382
+ "team-agent-buf",
383
+ "hello [team-agent-token:abc]",
384
+ true,
385
+ );
386
+ assert_eq!(
387
+ argv,
388
+ vec![
363
389
  vec![
364
- vec![
365
- "tmux",
366
- "set-buffer",
367
- "-b",
368
- "team-agent-buf",
369
- "hello [team-agent-token:abc]"
370
- ],
371
- vec!["tmux", "paste-buffer", "-t", "%7", "-b", "team-agent-buf", "-p"],
372
- vec!["tmux", "delete-buffer", "-b", "team-agent-buf"],
373
- ]
374
- );
375
- }
376
-
377
- // ═══════════════ P2 FIX-LOOP RED (复绿即对抗 cross-model finding) ═══════════════
378
- // P1 — inject must switch to `load-buffer -` (stdin) at TMUX_STDIN_BUFFER_THRESHOLD
379
- // (16*1024 bytes); below it stays `set-buffer <text>`. The current seam has NO size
380
- // param → always set-buffer → 16KiB+ prompts hit ARG_MAX/E2BIG.
381
- // Golden: messaging/tmux_io.py:292-303 (`size >= TMUX_STDIN_BUFFER_THRESHOLD`),
382
- // _tmux_load_buffer_stdin argv `["tmux","load-buffer","-b",<buf>,"-"]`; runtime.py:464
383
- // TMUX_STDIN_BUFFER_THRESHOLD = 16 * 1024.
384
- #[test]
385
- fn p2_inject_large_text_switches_to_load_buffer_stdin_at_16k() {
386
- let pane = PaneId::new("%7");
387
- let buf = "team-agent-buf";
388
-
389
- // small set-buffer with the text inline (unchanged).
390
- let small = tmux_inject_text_argv(&pane, buf, "hello", true);
391
- assert_eq!(small[0], vec!["tmux", "set-buffer", "-b", buf, "hello"]);
392
-
393
- // just below threshold (16383 bytes) → still set-buffer.
394
- let below = "x".repeat(16383);
395
- let below_argv = tmux_inject_text_argv(&pane, buf, &below, true);
396
- assert_eq!(below_argv[0][1], "set-buffer", "16383 bytes < threshold → set-buffer");
397
-
398
- // at/above threshold (16384 bytes) → load-buffer - (text streamed via stdin).
399
- let big = "x".repeat(16384);
400
- let big_argv = tmux_inject_text_argv(&pane, buf, &big, true);
401
- assert_eq!(
402
- big_argv[0],
403
- vec!["tmux", "load-buffer", "-b", buf, "-"],
404
- ">=16384 bytes must stream via `load-buffer -` (stdin), not set-buffer argv"
405
- );
406
- // the payload must NOT ride the command line (ARG_MAX hazard).
407
- assert!(
408
- !big_argv[0].iter().any(|a| a.len() >= 16384),
409
- "the 16KiB text must not be passed as an argv argument"
410
- );
411
- }
412
-
413
- // ════════════════════════════════════════════════════════════════════════
414
- // GROUP E Key 枚举翻译(抽象 Key,各后端翻译,不透传 tmux 字面量)
415
- // contracts-rust-native test_key_enum_translation_parity / cancel_mode_noop。
416
- // ════════════════════════════════════════════════════════════════════════
417
-
418
- #[test]
419
- fn key_enum_variants_are_distinct_and_copy() {
420
- // Enter/Up/Down/Left/Right/Char/CtrlC/CancelMode 互不相等;Char 携字符。
421
- assert_ne!(Key::Enter, Key::CtrlC);
422
- assert_ne!(Key::Up, Key::Down);
423
- assert_ne!(Key::Char('1'), Key::Char('2'));
424
- assert_eq!(Key::Char('3'), Key::Char('3'));
425
- // Copy:可按值传两次。
426
- let k = Key::Enter;
427
- let _ = (k, k);
428
- }
429
-
430
- #[test]
431
- fn send_keys_sequence_routes_through_transport() {
432
- // send_keys 接受 &[Key];RED via stub。
433
- let t = tmux();
434
- t.send_keys(&Target::Pane(PaneId::new("%7")), &[Key::Down, Key::Enter])
435
- .expect("send_keys ok");
436
-
437
- // 命令构造 golden(STEP-9):抽象 Key 翻译成 tmux 字面键名,不透传(§gap-5)
438
- // [Down, Enter] → send-keys -t %7 Down Enter(codex.py:266)。
439
- assert_eq!(tmux_key_name(Key::Down), "Down");
440
- assert_eq!(tmux_key_name(Key::Enter), "Enter");
441
- assert_eq!(tmux_key_name(Key::CtrlC), "C-c");
442
- assert_eq!(
443
- tmux_send_keys_argv(&PaneId::new("%7"), &[Key::Down, Key::Enter]),
444
- vec!["tmux", "send-keys", "-t", "%7", "Down", "Enter"]
445
- );
446
- }
447
-
448
- #[test]
449
- fn cancel_mode_is_noop_on_non_tmux_backends() {
450
- // contracts-rust-native test_cancel_mode_noop_on_non_tmux_parity:
451
- // Key::CancelMode 在 WezTerm/ConPTY 上无害 no-op(无 copy-mode 概念),不报错不阻塞。
452
- // RED via stub(porter 让 wezterm/conpty 返回 Ok(()))。
453
- for be in [wezterm(), conpty()] {
454
- be.send_keys(&Target::Pane(PaneId::new("%1")), &[Key::CancelMode])
455
- .expect("CancelMode no-op must be Ok on non-tmux");
456
- }
457
-
458
- // tmux 侧:CancelMode 按 pane mode 分派退出键(命令构造 golden,tmux_io.py:419-426)。
459
- // copy→-X cancel,tree/view→q,client→d,unknown→-X cancel(+warn)
460
- assert_eq!(
461
- tmux_cancel_mode_argv(&PaneId::new("%7"), PaneMode::Copy),
462
- vec!["tmux", "send-keys", "-t", "%7", "-X", "cancel"]
463
- );
464
- assert_eq!(
465
- tmux_cancel_mode_argv(&PaneId::new("%7"), PaneMode::Tree),
466
- vec!["tmux", "send-keys", "-t", "%7", "q"]
467
- );
468
- assert_eq!(
469
- tmux_cancel_mode_argv(&PaneId::new("%7"), PaneMode::View),
470
- vec!["tmux", "send-keys", "-t", "%7", "q"]
471
- );
472
- assert_eq!(
473
- tmux_cancel_mode_argv(&PaneId::new("%7"), PaneMode::Client),
474
- vec!["tmux", "send-keys", "-t", "%7", "d"]
475
- );
476
- assert_eq!(
477
- tmux_cancel_mode_argv(&PaneId::new("%7"), PaneMode::Unknown),
478
- vec!["tmux", "send-keys", "-t", "%7", "-X", "cancel"]
479
- );
480
- }
481
-
482
- // ════════════════════════════════════════════════════════════════════════
483
- // GROUP F — CaptureRange tail/full 语义 + capture 出口规范化收口
484
- // tmux `-S -<N>`(Tail)/ `-S -`(Full);DELIVERY_CAPTURE_LINES=40 是默认 tail。
485
- // ════════════════════════════════════════════════════════════════════════
486
-
487
- #[test]
488
- fn capture_range_tail_and_full_distinct() {
489
- assert_ne!(CaptureRange::Tail(40), CaptureRange::Full);
490
- assert_ne!(CaptureRange::Tail(40), CaptureRange::Tail(30));
491
- assert_eq!(CaptureRange::Tail(40), CaptureRange::Tail(40));
492
- }
493
-
494
- #[test]
495
- fn capture_tail_records_range_in_captured_text() {
496
- // CapturedText 携带它抓取的 range + 出口规范化文本。
497
- // contracts-rust-native test_capture_normalized_text_parity。RED via stub。
498
- let t = tmux();
499
- let cap = t
500
- .capture(&Target::Pane(PaneId::new("%7")), CaptureRange::Tail(40))
501
- .expect("capture ok");
502
- assert_eq!(cap.range, CaptureRange::Tail(40));
503
-
504
- // 命令构造 golden(STEP-9):Tail(40) → capture-pane -p -S -40 -t %7
505
- // (tmux_prompt.py:149 / tmux_io.py:410)。范围回显不够 —— 钉死 argv 映射。
506
- assert_eq!(
507
- tmux_capture_argv(&PaneId::new("%7"), CaptureRange::Tail(40)),
508
- vec!["tmux", "capture-pane", "-p", "-S", "-40", "-t", "%7"]
509
- );
510
- // 出口文本规范化 golden(§4b design line 399-400):逐行 rstrip + \xa0→空格。
511
- assert_eq!(
512
- normalize_capture("line one \nbusy\u{a0}marker \n \n"),
513
- "line one\nbusy marker\n\n"
514
- );
390
+ "tmux",
391
+ "set-buffer",
392
+ "-b",
393
+ "team-agent-buf",
394
+ "hello [team-agent-token:abc]"
395
+ ],
396
+ vec![
397
+ "tmux",
398
+ "paste-buffer",
399
+ "-t",
400
+ "%7",
401
+ "-b",
402
+ "team-agent-buf",
403
+ "-p"
404
+ ],
405
+ vec!["tmux", "delete-buffer", "-b", "team-agent-buf"],
406
+ ]
407
+ );
408
+ }
409
+
410
+ // ═══════════════ P2 FIX-LOOP RED (复绿即对抗 cross-model finding) ═══════════════
411
+ // P1 — inject must switch to `load-buffer -` (stdin) at TMUX_STDIN_BUFFER_THRESHOLD
412
+ // (16*1024 bytes); below it stays `set-buffer <text>`. The current seam has NO size
413
+ // param always set-buffer → 16KiB+ prompts hit ARG_MAX/E2BIG.
414
+ // Golden: messaging/tmux_io.py:292-303 (`size >= TMUX_STDIN_BUFFER_THRESHOLD`),
415
+ // _tmux_load_buffer_stdin argv `["tmux","load-buffer","-b",<buf>,"-"]`; runtime.py:464
416
+ // TMUX_STDIN_BUFFER_THRESHOLD = 16 * 1024.
417
+ #[test]
418
+ fn p2_inject_large_text_switches_to_load_buffer_stdin_at_16k() {
419
+ let pane = PaneId::new("%7");
420
+ let buf = "team-agent-buf";
421
+
422
+ // small → set-buffer with the text inline (unchanged).
423
+ let small = tmux_inject_text_argv(&pane, buf, "hello", true);
424
+ assert_eq!(small[0], vec!["tmux", "set-buffer", "-b", buf, "hello"]);
425
+
426
+ // just below threshold (16383 bytes) still set-buffer.
427
+ let below = "x".repeat(16383);
428
+ let below_argv = tmux_inject_text_argv(&pane, buf, &below, true);
429
+ assert_eq!(
430
+ below_argv[0][1], "set-buffer",
431
+ "16383 bytes < threshold → set-buffer"
432
+ );
433
+
434
+ // at/above threshold (16384 bytes) → load-buffer - (text streamed via stdin).
435
+ let big = "x".repeat(16384);
436
+ let big_argv = tmux_inject_text_argv(&pane, buf, &big, true);
437
+ assert_eq!(
438
+ big_argv[0],
439
+ vec!["tmux", "load-buffer", "-b", buf, "-"],
440
+ ">=16384 bytes must stream via `load-buffer -` (stdin), not set-buffer argv"
441
+ );
442
+ // the payload must NOT ride the command line (ARG_MAX hazard).
443
+ assert!(
444
+ !big_argv[0].iter().any(|a| a.len() >= 16384),
445
+ "the 16KiB text must not be passed as an argv argument"
446
+ );
447
+ }
448
+
449
+ // ════════════════════════════════════════════════════════════════════════
450
+ // GROUP E — Key 枚举翻译(抽象 Key,各后端翻译,不透传 tmux 字面量)
451
+ // contracts-rust-native test_key_enum_translation_parity / cancel_mode_noop。
452
+ // ════════════════════════════════════════════════════════════════════════
453
+
454
+ #[test]
455
+ fn key_enum_variants_are_distinct_and_copy() {
456
+ // Enter/Up/Down/Left/Right/Char/CtrlC/CancelMode 互不相等;Char 携字符。
457
+ assert_ne!(Key::Enter, Key::CtrlC);
458
+ assert_ne!(Key::Up, Key::Down);
459
+ assert_ne!(Key::Char('1'), Key::Char('2'));
460
+ assert_eq!(Key::Char('3'), Key::Char('3'));
461
+ // Copy:可按值传两次。
462
+ let k = Key::Enter;
463
+ let _ = (k, k);
464
+ }
465
+
466
+ #[test]
467
+ fn send_keys_sequence_routes_through_transport() {
468
+ // send_keys 接受 &[Key];RED via stub。
469
+ let t = tmux();
470
+ t.send_keys(&Target::Pane(PaneId::new("%7")), &[Key::Down, Key::Enter])
471
+ .expect("send_keys ok");
472
+
473
+ // 命令构造 golden(STEP-9):抽象 Key 翻译成 tmux 字面键名,不透传(§gap-5)。
474
+ // [Down, Enter] → send-keys -t %7 Down Enter(codex.py:266)。
475
+ assert_eq!(tmux_key_name(Key::Down), "Down");
476
+ assert_eq!(tmux_key_name(Key::Enter), "Enter");
477
+ assert_eq!(tmux_key_name(Key::CtrlC), "C-c");
478
+ assert_eq!(
479
+ tmux_send_keys_argv(&PaneId::new("%7"), &[Key::Down, Key::Enter]),
480
+ vec!["tmux", "send-keys", "-t", "%7", "Down", "Enter"]
481
+ );
482
+ }
483
+
484
+ #[test]
485
+ fn cancel_mode_is_noop_on_non_tmux_backends() {
486
+ // contracts-rust-native test_cancel_mode_noop_on_non_tmux_parity:
487
+ // Key::CancelMode 在 WezTerm/ConPTY 上无害 no-op( copy-mode 概念),不报错不阻塞。
488
+ // RED via stub(porter wezterm/conpty 返回 Ok(()))。
489
+ for be in [wezterm(), conpty()] {
490
+ be.send_keys(&Target::Pane(PaneId::new("%1")), &[Key::CancelMode])
491
+ .expect("CancelMode no-op must be Ok on non-tmux");
515
492
  }
516
493
 
517
- #[test]
518
- fn capture_full_returns_complete_history() {
519
- // contracts-rust-native test_capture_full_scrollback_parity(tmux -S -)。RED via stub。
520
- let t = tmux();
521
- let cap = t
522
- .capture(&Target::Pane(PaneId::new("%7")), CaptureRange::Full)
523
- .expect("capture full ok");
524
- assert_eq!(cap.range, CaptureRange::Full);
525
-
526
- // 命令构造 golden:Full → capture-pane -p -S - -t %7(全量 scrollback,runtime.py:519)
527
- // 注意是 `-S -`(空 N)不是 `-S -0`。
528
- assert_eq!(
529
- tmux_capture_argv(&PaneId::new("%7"), CaptureRange::Full),
530
- vec!["tmux", "capture-pane", "-p", "-S", "-", "-t", "%7"]
531
- );
532
- }
494
+ // tmux 侧:CancelMode 按 pane mode 分派退出键(命令构造 golden,tmux_io.py:419-426)。
495
+ // copy→-X cancel,tree/view→q,client→d,unknown→-X cancel(+warn)
496
+ assert_eq!(
497
+ tmux_cancel_mode_argv(&PaneId::new("%7"), PaneMode::Copy),
498
+ vec!["tmux", "send-keys", "-t", "%7", "-X", "cancel"]
499
+ );
500
+ assert_eq!(
501
+ tmux_cancel_mode_argv(&PaneId::new("%7"), PaneMode::Tree),
502
+ vec!["tmux", "send-keys", "-t", "%7", "q"]
503
+ );
504
+ assert_eq!(
505
+ tmux_cancel_mode_argv(&PaneId::new("%7"), PaneMode::View),
506
+ vec!["tmux", "send-keys", "-t", "%7", "q"]
507
+ );
508
+ assert_eq!(
509
+ tmux_cancel_mode_argv(&PaneId::new("%7"), PaneMode::Client),
510
+ vec!["tmux", "send-keys", "-t", "%7", "d"]
511
+ );
512
+ assert_eq!(
513
+ tmux_cancel_mode_argv(&PaneId::new("%7"), PaneMode::Unknown),
514
+ vec!["tmux", "send-keys", "-t", "%7", "-X", "cancel"]
515
+ );
516
+ }
517
+
518
+ // ════════════════════════════════════════════════════════════════════════
519
+ // GROUP F — CaptureRange tail/full 语义 + capture 出口规范化收口
520
+ // tmux `-S -<N>`(Tail)/ `-S -`(Full);DELIVERY_CAPTURE_LINES=40 是默认 tail。
521
+ // ════════════════════════════════════════════════════════════════════════
522
+
523
+ #[test]
524
+ fn capture_range_tail_and_full_distinct() {
525
+ assert_ne!(CaptureRange::Tail(40), CaptureRange::Full);
526
+ assert_ne!(CaptureRange::Tail(40), CaptureRange::Tail(30));
527
+ assert_eq!(CaptureRange::Tail(40), CaptureRange::Tail(40));
528
+ }
529
+
530
+ #[test]
531
+ fn capture_tail_records_range_in_captured_text() {
532
+ // CapturedText 携带它抓取的 range + 出口规范化文本。
533
+ // contracts-rust-native test_capture_normalized_text_parity。RED via stub。
534
+ let t = tmux();
535
+ let cap = t
536
+ .capture(&Target::Pane(PaneId::new("%7")), CaptureRange::Tail(40))
537
+ .expect("capture ok");
538
+ assert_eq!(cap.range, CaptureRange::Tail(40));
539
+
540
+ // 命令构造 golden(STEP-9):Tail(40) → capture-pane -p -S -40 -t %7
541
+ // (tmux_prompt.py:149 / tmux_io.py:410)。范围回显不够 —— 钉死 argv 映射。
542
+ assert_eq!(
543
+ tmux_capture_argv(&PaneId::new("%7"), CaptureRange::Tail(40)),
544
+ vec!["tmux", "capture-pane", "-p", "-S", "-40", "-t", "%7"]
545
+ );
546
+ // 出口文本规范化 golden(§4b design line 399-400):逐行 rstrip + \xa0→空格。
547
+ assert_eq!(
548
+ normalize_capture("line one \nbusy\u{a0}marker \n \n"),
549
+ "line one\nbusy marker\n\n"
550
+ );
551
+ }
552
+
553
+ #[test]
554
+ fn capture_full_returns_complete_history() {
555
+ // contracts-rust-native test_capture_full_scrollback_parity(tmux -S -)。RED via stub。
556
+ let t = tmux();
557
+ let cap = t
558
+ .capture(&Target::Pane(PaneId::new("%7")), CaptureRange::Full)
559
+ .expect("capture full ok");
560
+ assert_eq!(cap.range, CaptureRange::Full);
561
+
562
+ // 命令构造 golden:Full → capture-pane -p -S - -t %7(全量 scrollback,runtime.py:519)。
563
+ // 注意是 `-S -`(空 N)不是 `-S -0`。
564
+ assert_eq!(
565
+ tmux_capture_argv(&PaneId::new("%7"), CaptureRange::Full),
566
+ vec!["tmux", "capture-pane", "-p", "-S", "-", "-t", "%7"]
567
+ );
568
+ }