@team-agent/installer 0.2.11 → 0.3.0

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 (326) hide show
  1. package/Cargo.lock +744 -0
  2. package/Cargo.toml +34 -0
  3. package/crates/team-agent/Cargo.toml +33 -0
  4. package/crates/team-agent/src/cli/adapters.rs +1343 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +554 -0
  6. package/crates/team-agent/src/cli/emit.rs +1077 -0
  7. package/crates/team-agent/src/cli/helpers.rs +88 -0
  8. package/crates/team-agent/src/cli/leader.rs +216 -0
  9. package/crates/team-agent/src/cli/mod.rs +1141 -0
  10. package/crates/team-agent/src/cli/profile.rs +306 -0
  11. package/crates/team-agent/src/cli/send.rs +215 -0
  12. package/crates/team-agent/src/cli/status.rs +179 -0
  13. package/crates/team-agent/src/cli/status_port.rs +502 -0
  14. package/crates/team-agent/src/cli/tests/base.rs +616 -0
  15. package/crates/team-agent/src/cli/tests/compile.rs +96 -0
  16. package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
  21. package/crates/team-agent/src/cli/tests/mod.rs +97 -0
  22. package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
  23. package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
  25. package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
  27. package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
  28. package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
  29. package/crates/team-agent/src/cli/types.rs +605 -0
  30. package/crates/team-agent/src/compiler/tests.rs +701 -0
  31. package/crates/team-agent/src/compiler.rs +489 -0
  32. package/crates/team-agent/src/coordinator/backoff.rs +153 -0
  33. package/crates/team-agent/src/coordinator/health.rs +436 -0
  34. package/crates/team-agent/src/coordinator/mod.rs +80 -0
  35. package/crates/team-agent/src/coordinator/orphan.rs +179 -0
  36. package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
  37. package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
  38. package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
  39. package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
  40. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
  41. package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
  42. package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
  43. package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
  44. package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
  45. package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
  46. package/crates/team-agent/src/coordinator/tick.rs +2032 -0
  47. package/crates/team-agent/src/coordinator/types.rs +584 -0
  48. package/crates/team-agent/src/db/migration.rs +716 -0
  49. package/crates/team-agent/src/db/mod.rs +23 -0
  50. package/crates/team-agent/src/db/schema.rs +378 -0
  51. package/crates/team-agent/src/event_log.rs +375 -0
  52. package/crates/team-agent/src/fake_worker.rs +253 -0
  53. package/crates/team-agent/src/leader/helpers.rs +190 -0
  54. package/crates/team-agent/src/leader/inject.rs +33 -0
  55. package/crates/team-agent/src/leader/lease.rs +1063 -0
  56. package/crates/team-agent/src/leader/mod.rs +99 -0
  57. package/crates/team-agent/src/leader/owner_bind.rs +292 -0
  58. package/crates/team-agent/src/leader/rediscover/tests.rs +525 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1099 -0
  60. package/crates/team-agent/src/leader/start.rs +273 -0
  61. package/crates/team-agent/src/leader/takeover.rs +235 -0
  62. package/crates/team-agent/src/leader/tests/basics.rs +183 -0
  63. package/crates/team-agent/src/leader/tests/byte_findings.rs +234 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +271 -0
  66. package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
  67. package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -0
  68. package/crates/team-agent/src/leader/tests/mod.rs +125 -0
  69. package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
  70. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
  71. package/crates/team-agent/src/leader/types.rs +487 -0
  72. package/crates/team-agent/src/lib.rs +85 -0
  73. package/crates/team-agent/src/lifecycle/display.rs +228 -0
  74. package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
  75. package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
  76. package/crates/team-agent/src/lifecycle/launch.rs +1833 -0
  77. package/crates/team-agent/src/lifecycle/mod.rs +62 -0
  78. package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
  79. package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
  80. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
  81. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
  82. package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
  83. package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
  84. package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
  85. package/crates/team-agent/src/lifecycle/restart.rs +76 -0
  86. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
  88. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
  89. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +933 -0
  90. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
  91. package/crates/team-agent/src/lifecycle/tests.rs +27 -0
  92. package/crates/team-agent/src/lifecycle/types.rs +685 -0
  93. package/crates/team-agent/src/main.rs +41 -0
  94. package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
  95. package/crates/team-agent/src/mcp_server/mod.rs +183 -0
  96. package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
  97. package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
  98. package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
  99. package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
  100. package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
  101. package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
  102. package/crates/team-agent/src/mcp_server/tests/wire.rs +159 -0
  103. package/crates/team-agent/src/mcp_server/tests.rs +38 -0
  104. package/crates/team-agent/src/mcp_server/tools.rs +603 -0
  105. package/crates/team-agent/src/mcp_server/types.rs +421 -0
  106. package/crates/team-agent/src/mcp_server/wire.rs +388 -0
  107. package/crates/team-agent/src/message_store.rs +767 -0
  108. package/crates/team-agent/src/messaging/activity.rs +433 -0
  109. package/crates/team-agent/src/messaging/delivery.rs +542 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +340 -0
  112. package/crates/team-agent/src/messaging/mod.rs +147 -0
  113. package/crates/team-agent/src/messaging/peers.rs +32 -0
  114. package/crates/team-agent/src/messaging/results.rs +537 -0
  115. package/crates/team-agent/src/messaging/scheduler.rs +344 -0
  116. package/crates/team-agent/src/messaging/selftest.rs +100 -0
  117. package/crates/team-agent/src/messaging/send.rs +582 -0
  118. package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
  119. package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
  121. package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
  122. package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
  123. package/crates/team-agent/src/messaging/trust.rs +192 -0
  124. package/crates/team-agent/src/messaging/types.rs +355 -0
  125. package/crates/team-agent/src/messaging/watchers.rs +591 -0
  126. package/crates/team-agent/src/model/enums.rs +311 -0
  127. package/crates/team-agent/src/model/errors.rs +17 -0
  128. package/crates/team-agent/src/model/ids.rs +155 -0
  129. package/crates/team-agent/src/model/mod.rs +22 -0
  130. package/crates/team-agent/src/model/paths.rs +228 -0
  131. package/crates/team-agent/src/model/permissions.rs +567 -0
  132. package/crates/team-agent/src/model/routing.rs +340 -0
  133. package/crates/team-agent/src/model/spec.rs +680 -0
  134. package/crates/team-agent/src/model/task_graph.rs +380 -0
  135. package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
  136. package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
  137. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
  138. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
  139. package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
  140. package/crates/team-agent/src/model/yaml/tests.rs +288 -0
  141. package/crates/team-agent/src/model/yaml.rs +800 -0
  142. package/crates/team-agent/src/packaging/install.rs +305 -0
  143. package/crates/team-agent/src/packaging/migrate.rs +30 -0
  144. package/crates/team-agent/src/packaging/mod.rs +82 -0
  145. package/crates/team-agent/src/packaging/repair.rs +24 -0
  146. package/crates/team-agent/src/packaging/tests.rs +829 -0
  147. package/crates/team-agent/src/packaging/types.rs +369 -0
  148. package/crates/team-agent/src/provider/adapter.rs +801 -0
  149. package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
  150. package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
  151. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
  152. package/crates/team-agent/src/provider/classify.rs +456 -0
  153. package/crates/team-agent/src/provider/faults.rs +136 -0
  154. package/crates/team-agent/src/provider/helpers.rs +41 -0
  155. package/crates/team-agent/src/provider/mod.rs +53 -0
  156. package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
  157. package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
  158. package/crates/team-agent/src/provider/tests/classify.rs +240 -0
  159. package/crates/team-agent/src/provider/tests/faults.rs +120 -0
  160. package/crates/team-agent/src/provider/tests/idle.rs +208 -0
  161. package/crates/team-agent/src/provider/tests/wire.rs +213 -0
  162. package/crates/team-agent/src/provider/tests.rs +31 -0
  163. package/crates/team-agent/src/provider/types.rs +424 -0
  164. package/crates/team-agent/src/state/identity.rs +656 -0
  165. package/crates/team-agent/src/state/mod.rs +58 -0
  166. package/crates/team-agent/src/state/owner_gate.rs +423 -0
  167. package/crates/team-agent/src/state/persist.rs +712 -0
  168. package/crates/team-agent/src/state/projection.rs +657 -0
  169. package/crates/team-agent/src/state/selector.rs +105 -0
  170. package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
  171. package/crates/team-agent/src/tmux_backend/tests.rs +586 -0
  172. package/crates/team-agent/src/tmux_backend.rs +758 -0
  173. package/crates/team-agent/src/transport/test_support.rs +252 -0
  174. package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
  175. package/crates/team-agent/src/transport/tests/mod.rs +199 -0
  176. package/crates/team-agent/src/transport/tests/wire.rs +527 -0
  177. package/crates/team-agent/src/transport.rs +774 -0
  178. package/npm/install.mjs +90 -106
  179. package/package.json +15 -13
  180. package/crates/team-agent-core/Cargo.toml +0 -12
  181. package/crates/team-agent-core/src/lib.rs +0 -332
  182. package/crates/team-agent-core/src/main.rs +0 -152
  183. package/pyproject.toml +0 -18
  184. package/scripts/install.py +0 -88
  185. package/scripts/run_regression_tests.py +0 -83
  186. package/src/team_agent/__init__.py +0 -3
  187. package/src/team_agent/__main__.py +0 -5
  188. package/src/team_agent/_legacy_pane_discovery.py +0 -186
  189. package/src/team_agent/abnormal_track.py +0 -253
  190. package/src/team_agent/approvals/__init__.py +0 -65
  191. package/src/team_agent/approvals/constants.py +0 -6
  192. package/src/team_agent/approvals/parsing.py +0 -176
  193. package/src/team_agent/approvals/runtime_prompts.py +0 -171
  194. package/src/team_agent/approvals/status.py +0 -176
  195. package/src/team_agent/cli/__init__.py +0 -137
  196. package/src/team_agent/cli/commands.py +0 -481
  197. package/src/team_agent/cli/e2e.py +0 -202
  198. package/src/team_agent/cli/helpers.py +0 -226
  199. package/src/team_agent/cli/parser.py +0 -540
  200. package/src/team_agent/compiler.py +0 -334
  201. package/src/team_agent/coordinator/__init__.py +0 -53
  202. package/src/team_agent/coordinator/__main__.py +0 -119
  203. package/src/team_agent/coordinator/lifecycle.py +0 -411
  204. package/src/team_agent/coordinator/metadata.py +0 -61
  205. package/src/team_agent/coordinator/paths.py +0 -17
  206. package/src/team_agent/diagnose/__init__.py +0 -48
  207. package/src/team_agent/diagnose/checks.py +0 -101
  208. package/src/team_agent/diagnose/comms.py +0 -213
  209. package/src/team_agent/diagnose/health.py +0 -241
  210. package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
  211. package/src/team_agent/diagnose/preflight.py +0 -194
  212. package/src/team_agent/diagnose/quick_start.py +0 -324
  213. package/src/team_agent/display/__init__.py +0 -92
  214. package/src/team_agent/display/adaptive.py +0 -511
  215. package/src/team_agent/display/backend.py +0 -46
  216. package/src/team_agent/display/close.py +0 -154
  217. package/src/team_agent/display/ghostty.py +0 -77
  218. package/src/team_agent/display/rebuild.py +0 -102
  219. package/src/team_agent/display/tiling.py +0 -156
  220. package/src/team_agent/display/worker_window.py +0 -114
  221. package/src/team_agent/display/workspace.py +0 -382
  222. package/src/team_agent/errors.py +0 -10
  223. package/src/team_agent/events.py +0 -84
  224. package/src/team_agent/fake_worker.py +0 -80
  225. package/src/team_agent/idle_predicate.py +0 -218
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -114
  228. package/src/team_agent/launch/__init__.py +0 -41
  229. package/src/team_agent/launch/bootstrap.py +0 -85
  230. package/src/team_agent/launch/config.py +0 -106
  231. package/src/team_agent/launch/core.py +0 -301
  232. package/src/team_agent/launch/requirements.py +0 -57
  233. package/src/team_agent/leader/__init__.py +0 -926
  234. package/src/team_agent/leader_binding.py +0 -183
  235. package/src/team_agent/lifecycle/__init__.py +0 -5
  236. package/src/team_agent/lifecycle/agents.py +0 -278
  237. package/src/team_agent/lifecycle/operations.py +0 -411
  238. package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
  239. package/src/team_agent/lifecycle/start.py +0 -363
  240. package/src/team_agent/mcp_server/__init__.py +0 -42
  241. package/src/team_agent/mcp_server/__main__.py +0 -7
  242. package/src/team_agent/mcp_server/contracts.py +0 -148
  243. package/src/team_agent/mcp_server/normalize.py +0 -257
  244. package/src/team_agent/mcp_server/server.py +0 -150
  245. package/src/team_agent/mcp_server/tools.py +0 -352
  246. package/src/team_agent/message_store/__init__.py +0 -23
  247. package/src/team_agent/message_store/agent_health.py +0 -113
  248. package/src/team_agent/message_store/core.py +0 -497
  249. package/src/team_agent/message_store/leader_notification_log.py +0 -198
  250. package/src/team_agent/message_store/result_watchers.py +0 -251
  251. package/src/team_agent/message_store/schema.py +0 -308
  252. package/src/team_agent/message_store/schema_migration.py +0 -448
  253. package/src/team_agent/messaging/__init__.py +0 -1
  254. package/src/team_agent/messaging/activity_detector.py +0 -262
  255. package/src/team_agent/messaging/delivery.py +0 -504
  256. package/src/team_agent/messaging/deps.py +0 -247
  257. package/src/team_agent/messaging/idle_alerts.py +0 -423
  258. package/src/team_agent/messaging/internal_delivery.py +0 -46
  259. package/src/team_agent/messaging/leader.py +0 -497
  260. package/src/team_agent/messaging/leader_api_errors.py +0 -216
  261. package/src/team_agent/messaging/leader_panes.py +0 -673
  262. package/src/team_agent/messaging/owner_bypass.py +0 -29
  263. package/src/team_agent/messaging/result_delivery.py +0 -539
  264. package/src/team_agent/messaging/results.py +0 -447
  265. package/src/team_agent/messaging/scheduler.py +0 -450
  266. package/src/team_agent/messaging/send.py +0 -532
  267. package/src/team_agent/messaging/session_drift.py +0 -94
  268. package/src/team_agent/messaging/tmux_io.py +0 -506
  269. package/src/team_agent/messaging/tmux_prompt.py +0 -338
  270. package/src/team_agent/messaging/trust_auto_answer.py +0 -52
  271. package/src/team_agent/orchestrator/__init__.py +0 -376
  272. package/src/team_agent/orchestrator/plan.py +0 -122
  273. package/src/team_agent/orchestrator/state.py +0 -128
  274. package/src/team_agent/paths.py +0 -45
  275. package/src/team_agent/permissions.py +0 -123
  276. package/src/team_agent/profiles/__init__.py +0 -82
  277. package/src/team_agent/profiles/constants.py +0 -19
  278. package/src/team_agent/profiles/core.py +0 -407
  279. package/src/team_agent/profiles/helpers.py +0 -69
  280. package/src/team_agent/profiles/provider_env.py +0 -188
  281. package/src/team_agent/profiles/smoke.py +0 -201
  282. package/src/team_agent/provider_cli/__init__.py +0 -43
  283. package/src/team_agent/provider_cli/adapter.py +0 -172
  284. package/src/team_agent/provider_cli/base.py +0 -48
  285. package/src/team_agent/provider_cli/claude.py +0 -503
  286. package/src/team_agent/provider_cli/codex.py +0 -336
  287. package/src/team_agent/provider_cli/copilot.py +0 -8
  288. package/src/team_agent/provider_cli/fake.py +0 -39
  289. package/src/team_agent/provider_cli/gemini.py +0 -95
  290. package/src/team_agent/provider_cli/opencode.py +0 -8
  291. package/src/team_agent/provider_cli/prompt.py +0 -62
  292. package/src/team_agent/provider_cli/registry.py +0 -18
  293. package/src/team_agent/provider_cli/unsupported.py +0 -32
  294. package/src/team_agent/provider_state/README.md +0 -78
  295. package/src/team_agent/provider_state/__init__.py +0 -91
  296. package/src/team_agent/provider_state/claude.py +0 -86
  297. package/src/team_agent/provider_state/codex.py +0 -84
  298. package/src/team_agent/provider_state/common.py +0 -207
  299. package/src/team_agent/provider_state/registry.py +0 -118
  300. package/src/team_agent/providers.py +0 -163
  301. package/src/team_agent/quality_gates.py +0 -104
  302. package/src/team_agent/restart/__init__.py +0 -34
  303. package/src/team_agent/restart/orchestration.py +0 -554
  304. package/src/team_agent/restart/selection.py +0 -89
  305. package/src/team_agent/restart/snapshot.py +0 -70
  306. package/src/team_agent/routing.py +0 -84
  307. package/src/team_agent/runtime.py +0 -1243
  308. package/src/team_agent/rust_core.py +0 -327
  309. package/src/team_agent/sessions/__init__.py +0 -25
  310. package/src/team_agent/sessions/capture.py +0 -144
  311. package/src/team_agent/sessions/inventory.py +0 -44
  312. package/src/team_agent/sessions/resume.py +0 -135
  313. package/src/team_agent/simple_yaml.py +0 -236
  314. package/src/team_agent/spec.py +0 -370
  315. package/src/team_agent/state.py +0 -693
  316. package/src/team_agent/status/__init__.py +0 -63
  317. package/src/team_agent/status/approvals.py +0 -52
  318. package/src/team_agent/status/compact.py +0 -158
  319. package/src/team_agent/status/constants.py +0 -18
  320. package/src/team_agent/status/inbox.py +0 -58
  321. package/src/team_agent/status/peek.py +0 -117
  322. package/src/team_agent/status/queries.py +0 -199
  323. package/src/team_agent/task_graph.py +0 -80
  324. package/src/team_agent/terminal.py +0 -57
  325. package/src/team_agent/wake.py +0 -58
  326. package/src/team_agent/watch/__init__.py +0 -145
@@ -0,0 +1,527 @@
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
+ );
20
+ }
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::SendKeysFailed),
108
+ "send_keys_failed"
109
+ );
110
+ // 模板 `{key}_sent_after_visible_token`:key 取 tmux 字面键名(submit_key)。
111
+ // golden:Enter → "Enter_sent_after_visible_token"(tmux_io.py:218,submit_key 字面)。
112
+ assert_eq!(
113
+ submit_verification_wire(SubmitVerification::KeySentAfterVisibleToken {
114
+ key: Key::Enter
115
+ }),
116
+ "Enter_sent_after_visible_token"
117
+ );
118
+ }
119
+
120
+ #[test]
121
+ fn pane_liveness_three_valued_reused_from_model() {
122
+ // 裁决:Liveness{Live/Dead/Unknown} == model::enums::PaneLiveness(bug-085 穷尽三态)。
123
+ // unknown ≠ dead ≠ live;serde 小写(与 state.py 对齐)。
124
+ assert_eq!(
125
+ serde_json::to_string(&PaneLiveness::Live).unwrap(),
126
+ "\"live\""
127
+ );
128
+ assert_eq!(
129
+ serde_json::to_string(&PaneLiveness::Dead).unwrap(),
130
+ "\"dead\""
131
+ );
132
+ assert_eq!(
133
+ serde_json::to_string(&PaneLiveness::Unknown).unwrap(),
134
+ "\"unknown\""
135
+ );
136
+ }
137
+
138
+ // ════════════════════════════════════════════════════════════════════════
139
+ // GROUP B — id newtype 字节透明(== 裸字符串;沿用 model::ids 风格)
140
+ // ════════════════════════════════════════════════════════════════════════
141
+
142
+ #[test]
143
+ fn pane_id_transparent_bytes_equal_raw_string() {
144
+ // tmux `%7` 字节级 == 裸字符串(serde transparent)。
145
+ assert_eq!(serde_json::to_string(&PaneId::new("%7")).unwrap(), "\"%7\"");
146
+ assert_eq!(PaneId::from("%12").as_str(), "%12");
147
+ assert_eq!(PaneId::new("%3").to_string(), "%3");
148
+ // 反序列化往返。
149
+ let back: PaneId = serde_json::from_str("\"%99\"").unwrap();
150
+ assert_eq!(back, PaneId::new("%99"));
151
+ }
152
+
153
+ #[test]
154
+ fn session_window_name_transparent_bytes() {
155
+ assert_eq!(
156
+ serde_json::to_string(&SessionName::new("team-sess")).unwrap(),
157
+ "\"team-sess\""
158
+ );
159
+ assert_eq!(
160
+ serde_json::to_string(&WindowName::new("win-1")).unwrap(),
161
+ "\"win-1\""
162
+ );
163
+ assert_eq!(SessionName::from("s").as_str(), "s");
164
+ assert_eq!(WindowName::from("w").to_string(), "w");
165
+ }
166
+
167
+ // ════════════════════════════════════════════════════════════════════════
168
+ // GROUP C — Target 寻址稳定性(禁混传:Pane vs SessionWindow 类型上区分)
169
+ // contracts-rust-native §2: spawn 返回的稳定 Target,后续 inject/capture 命中同进程。
170
+ // ════════════════════════════════════════════════════════════════════════
171
+
172
+ #[test]
173
+ fn target_pane_and_session_window_are_distinct_addressings() {
174
+ // 两种合法寻址不可混传;同名 PaneId vs SessionWindow 不相等。
175
+ let p = Target::Pane(PaneId::new("%7"));
176
+ let sw = Target::SessionWindow {
177
+ session: SessionName::new("team-sess"),
178
+ window: WindowName::new("%7"),
179
+ };
180
+ assert_ne!(p, sw);
181
+ // Pane 寻址按 PaneId 字节相等。
182
+ assert_eq!(
183
+ Target::Pane(PaneId::new("%7")),
184
+ Target::Pane(PaneId::new("%7"))
185
+ );
186
+ // SessionWindow 按 (session,window) 对相等;任一不同即不等。
187
+ let a = Target::SessionWindow {
188
+ session: SessionName::new("s"),
189
+ window: WindowName::new("w"),
190
+ };
191
+ let b = Target::SessionWindow {
192
+ session: SessionName::new("s"),
193
+ window: WindowName::new("w2"),
194
+ };
195
+ assert_ne!(a, b);
196
+ }
197
+
198
+ #[test]
199
+ fn spawn_first_returns_stable_addressable_target_then_reachable() {
200
+ // contracts-rust-native: spawn_first 返回稳定可寻址 Target,且交回的 pane_id 能
201
+ // 寻址回同一进程。RED:stub.spawn_first/capture unimplemented!()。
202
+ let t = tmux();
203
+ let env = BTreeMap::new();
204
+ let spawned = t
205
+ .spawn_first(
206
+ &SessionName::new("team-sess"),
207
+ &WindowName::new("win-1"),
208
+ &["sh".into(), "-lc".into(), "echo hi".into()],
209
+ Path::new("/tmp/ws"),
210
+ &env,
211
+ )
212
+ .expect("spawn_first ok");
213
+ // 身份正向登记:返回的 session/window 必须 == 请求的(不可凭空捏造别的)。
214
+ assert_eq!(spawned.session, SessionName::new("team-sess"));
215
+ assert_eq!(spawned.window, WindowName::new("win-1"));
216
+ // 交回的 pane_id 必须能寻址回同一进程:capture 命中并能取到文本。
217
+ let target = Target::Pane(spawned.pane_id.clone());
218
+ let cap = t.capture(&target, CaptureRange::Tail(40)).expect("capture ok");
219
+ assert_eq!(cap.range, CaptureRange::Tail(40));
220
+
221
+ // 命令构造 golden(STEP-9 头号目标):first-spawn 必须是 new-session,
222
+ // 不是 new-window(terminal.py:44-45 / runtime.py:1019-1020)。
223
+ assert_eq!(
224
+ tmux_spawn_argv(
225
+ &SessionName::new("team-sess"),
226
+ &WindowName::new("win-1"),
227
+ "echo hi",
228
+ true,
229
+ ),
230
+ vec![
231
+ "tmux", "new-session", "-d", "-s", "team-sess", "-n", "win-1", "sh", "-lc",
232
+ "echo hi",
233
+ ]
234
+ );
235
+ }
236
+
237
+ #[test]
238
+ fn spawn_into_then_list_targets_enumerates_it() {
239
+ // contracts-rust-native test_spawn_into_existing_session_parity。RED via stub。
240
+ let t = tmux();
241
+ let env = BTreeMap::new();
242
+ let spawned = t
243
+ .spawn_into(
244
+ &SessionName::new("team-sess"),
245
+ &WindowName::new("worker-2"),
246
+ &["sh".into()],
247
+ Path::new("/tmp/ws"),
248
+ &env,
249
+ )
250
+ .expect("spawn_into ok");
251
+ let listed = t.list_targets().expect("list_targets ok");
252
+ // 枚举出的那条必须 == spawn 请求(porter 不能用同一捏造 pane_id 让 spawn_into
253
+ // 与 list_targets 串通假绿):session/window_name/active 都要对上。
254
+ let info = listed
255
+ .iter()
256
+ .find(|p| p.pane_id == spawned.pane_id)
257
+ .expect("spawned worker must be enumerated by list_targets");
258
+ assert_eq!(info.session, SessionName::new("team-sess"));
259
+ assert_eq!(info.window_name, Some(WindowName::new("worker-2")));
260
+ assert!(info.active, "freshly spawned worker pane is active");
261
+
262
+ // 命令构造 golden:spawn_into 是 new-window(不是 new-session),挂到既有 session。
263
+ assert_eq!(
264
+ tmux_spawn_argv(
265
+ &SessionName::new("team-sess"),
266
+ &WindowName::new("worker-2"),
267
+ "sh",
268
+ false,
269
+ ),
270
+ vec!["tmux", "new-window", "-t", "team-sess", "-n", "worker-2", "sh", "-lc", "sh"]
271
+ );
272
+ }
273
+
274
+ // ════════════════════════════════════════════════════════════════════════
275
+ // GROUP D — InjectPayload 空文本分流(trust turn-integrity 契约 §3,bug)
276
+ // text=="" 走纯 send submit-key,禁 set/load/paste-buffer 空串(tmux 拒空 buffer
277
+ // 会卡 trust prompt)。golden /tmp/transport_golden.py: empty_inject_*。
278
+ // ════════════════════════════════════════════════════════════════════════
279
+
280
+ #[test]
281
+ fn inject_payload_empty_is_typed_distinct_from_empty_text() {
282
+ // 类型上把空文本与含字符文本分流(InjectPayload::Empty != Text("..."))。
283
+ assert_ne!(InjectPayload::Empty, InjectPayload::Text(String::new()));
284
+ assert_eq!(
285
+ InjectPayload::Text("hi".into()),
286
+ InjectPayload::Text("hi".into())
287
+ );
288
+ }
289
+
290
+ #[test]
291
+ fn inject_empty_payload_reports_empty_text_send_keys_and_turn_not_required() {
292
+ // golden empty_inject_report:
293
+ // verification=empty_text_send_keys, turn_verification=not_required,
294
+ // stage=submitted(Rust:stage_reached=Submit), submitted=true, attempts=1。
295
+ // 空文本禁走 buffer:只发一个 submit-key(golden empty_inject_calls 仅 1 条 send-keys)。
296
+ // RED:stub.inject unimplemented!()。
297
+ let t = tmux();
298
+ let report = t
299
+ .inject(
300
+ &Target::Pane(PaneId::new("%7")),
301
+ &InjectPayload::Empty,
302
+ Key::Enter,
303
+ false,
304
+ )
305
+ .expect("inject empty ok");
306
+ assert_eq!(report.inject_verification, InjectVerification::EmptyTextSendKeys);
307
+ assert_eq!(report.turn_verification, TurnVerification::NotRequired);
308
+ assert_eq!(report.stage_reached, InjectStage::Submit);
309
+ assert_eq!(report.attempts, 1);
310
+ }
311
+
312
+ #[test]
313
+ fn empty_inject_is_single_direct_send_keys_never_buffer() {
314
+ // 命令构造 golden(STEP-9):空文本禁走 buffer,只发一条 send-keys 提交键
315
+ // (tmux_io.py:42 —— tmux 拒空 buffer 会卡 trust prompt)。
316
+ // golden empty_inject_calls 仅 1 条:send-keys -t %7 Enter。RED via stub。
317
+ assert_eq!(
318
+ tmux_empty_inject_argv(&PaneId::new("%7"), Key::Enter),
319
+ vec!["tmux", "send-keys", "-t", "%7", "Enter"]
320
+ );
321
+ }
322
+
323
+ #[test]
324
+ fn inject_text_payload_bracketed_reaches_submit_with_token_verification() {
325
+ // contracts-rust-native test_inject_text_visible_parity:
326
+ // 非空文本走 set/load-buffer + paste-buffer(-p)+ submit;
327
+ // 有 token 时 verification=capture_contains_token,turn 仅 metadata(Gap42)。
328
+ // RED via stub。
329
+ let t = tmux();
330
+ let report = t
331
+ .inject(
332
+ &Target::Pane(PaneId::new("%7")),
333
+ &InjectPayload::Text("hello [team-agent-token:abc]".into()),
334
+ Key::Enter,
335
+ true,
336
+ )
337
+ .expect("inject text ok");
338
+ assert_eq!(report.stage_reached, InjectStage::Submit);
339
+ assert_eq!(
340
+ report.inject_verification,
341
+ InjectVerification::CaptureContainsToken
342
+ );
343
+ // Gap42:此 fixture 无 leader-boundary 标记(provider 非 fake,2s 内未观测到新 turn
344
+ // → tmux_io.py:192-193 `turn_verification = "not_yet_observed"`)。metadata-only,
345
+ // 绝非投递闸门 —— golden 钉死为 NotYetObserved(不是 2-of-4 析取)。
346
+ assert_eq!(report.turn_verification, TurnVerification::NotYetObserved);
347
+
348
+ // 命令构造 golden(STEP-9 头号目标):非空文本走 set-buffer → paste-buffer -p →
349
+ // delete-buffer 序列(tmux_io.py:119/303/314)。-p == bracketed paste。
350
+ let argv = tmux_inject_text_argv(
351
+ &PaneId::new("%7"),
352
+ "team-agent-buf",
353
+ "hello [team-agent-token:abc]",
354
+ true,
355
+ );
356
+ assert_eq!(
357
+ argv,
358
+ vec![
359
+ vec![
360
+ "tmux",
361
+ "set-buffer",
362
+ "-b",
363
+ "team-agent-buf",
364
+ "hello [team-agent-token:abc]"
365
+ ],
366
+ vec!["tmux", "paste-buffer", "-t", "%7", "-b", "team-agent-buf", "-p"],
367
+ vec!["tmux", "delete-buffer", "-b", "team-agent-buf"],
368
+ ]
369
+ );
370
+ }
371
+
372
+ // ═══════════════ P2 FIX-LOOP RED (复绿即对抗 cross-model finding) ═══════════════
373
+ // P1 — inject must switch to `load-buffer -` (stdin) at TMUX_STDIN_BUFFER_THRESHOLD
374
+ // (16*1024 bytes); below it stays `set-buffer <text>`. The current seam has NO size
375
+ // param → always set-buffer → 16KiB+ prompts hit ARG_MAX/E2BIG.
376
+ // Golden: messaging/tmux_io.py:292-303 (`size >= TMUX_STDIN_BUFFER_THRESHOLD`),
377
+ // _tmux_load_buffer_stdin argv `["tmux","load-buffer","-b",<buf>,"-"]`; runtime.py:464
378
+ // TMUX_STDIN_BUFFER_THRESHOLD = 16 * 1024.
379
+ #[test]
380
+ fn p2_inject_large_text_switches_to_load_buffer_stdin_at_16k() {
381
+ let pane = PaneId::new("%7");
382
+ let buf = "team-agent-buf";
383
+
384
+ // small → set-buffer with the text inline (unchanged).
385
+ let small = tmux_inject_text_argv(&pane, buf, "hello", true);
386
+ assert_eq!(small[0], vec!["tmux", "set-buffer", "-b", buf, "hello"]);
387
+
388
+ // just below threshold (16383 bytes) → still set-buffer.
389
+ let below = "x".repeat(16383);
390
+ let below_argv = tmux_inject_text_argv(&pane, buf, &below, true);
391
+ assert_eq!(below_argv[0][1], "set-buffer", "16383 bytes < threshold → set-buffer");
392
+
393
+ // at/above threshold (16384 bytes) → load-buffer - (text streamed via stdin).
394
+ let big = "x".repeat(16384);
395
+ let big_argv = tmux_inject_text_argv(&pane, buf, &big, true);
396
+ assert_eq!(
397
+ big_argv[0],
398
+ vec!["tmux", "load-buffer", "-b", buf, "-"],
399
+ ">=16384 bytes must stream via `load-buffer -` (stdin), not set-buffer argv"
400
+ );
401
+ // the payload must NOT ride the command line (ARG_MAX hazard).
402
+ assert!(
403
+ !big_argv[0].iter().any(|a| a.len() >= 16384),
404
+ "the 16KiB text must not be passed as an argv argument"
405
+ );
406
+ }
407
+
408
+ // ════════════════════════════════════════════════════════════════════════
409
+ // GROUP E — Key 枚举翻译(抽象 Key,各后端翻译,不透传 tmux 字面量)
410
+ // contracts-rust-native test_key_enum_translation_parity / cancel_mode_noop。
411
+ // ════════════════════════════════════════════════════════════════════════
412
+
413
+ #[test]
414
+ fn key_enum_variants_are_distinct_and_copy() {
415
+ // Enter/Up/Down/Left/Right/Char/CtrlC/CancelMode 互不相等;Char 携字符。
416
+ assert_ne!(Key::Enter, Key::CtrlC);
417
+ assert_ne!(Key::Up, Key::Down);
418
+ assert_ne!(Key::Char('1'), Key::Char('2'));
419
+ assert_eq!(Key::Char('3'), Key::Char('3'));
420
+ // Copy:可按值传两次。
421
+ let k = Key::Enter;
422
+ let _ = (k, k);
423
+ }
424
+
425
+ #[test]
426
+ fn send_keys_sequence_routes_through_transport() {
427
+ // send_keys 接受 &[Key];RED via stub。
428
+ let t = tmux();
429
+ t.send_keys(&Target::Pane(PaneId::new("%7")), &[Key::Down, Key::Enter])
430
+ .expect("send_keys ok");
431
+
432
+ // 命令构造 golden(STEP-9):抽象 Key 翻译成 tmux 字面键名,不透传(§gap-5)。
433
+ // [Down, Enter] → send-keys -t %7 Down Enter(codex.py:266)。
434
+ assert_eq!(tmux_key_name(Key::Down), "Down");
435
+ assert_eq!(tmux_key_name(Key::Enter), "Enter");
436
+ assert_eq!(tmux_key_name(Key::CtrlC), "C-c");
437
+ assert_eq!(
438
+ tmux_send_keys_argv(&PaneId::new("%7"), &[Key::Down, Key::Enter]),
439
+ vec!["tmux", "send-keys", "-t", "%7", "Down", "Enter"]
440
+ );
441
+ }
442
+
443
+ #[test]
444
+ fn cancel_mode_is_noop_on_non_tmux_backends() {
445
+ // contracts-rust-native test_cancel_mode_noop_on_non_tmux_parity:
446
+ // Key::CancelMode 在 WezTerm/ConPTY 上无害 no-op(无 copy-mode 概念),不报错不阻塞。
447
+ // RED via stub(porter 让 wezterm/conpty 返回 Ok(()))。
448
+ for be in [wezterm(), conpty()] {
449
+ be.send_keys(&Target::Pane(PaneId::new("%1")), &[Key::CancelMode])
450
+ .expect("CancelMode no-op must be Ok on non-tmux");
451
+ }
452
+
453
+ // tmux 侧:CancelMode 按 pane mode 分派退出键(命令构造 golden,tmux_io.py:419-426)。
454
+ // copy→-X cancel,tree/view→q,client→d,unknown→-X cancel(+warn)。
455
+ assert_eq!(
456
+ tmux_cancel_mode_argv(&PaneId::new("%7"), PaneMode::Copy),
457
+ vec!["tmux", "send-keys", "-t", "%7", "-X", "cancel"]
458
+ );
459
+ assert_eq!(
460
+ tmux_cancel_mode_argv(&PaneId::new("%7"), PaneMode::Tree),
461
+ vec!["tmux", "send-keys", "-t", "%7", "q"]
462
+ );
463
+ assert_eq!(
464
+ tmux_cancel_mode_argv(&PaneId::new("%7"), PaneMode::View),
465
+ vec!["tmux", "send-keys", "-t", "%7", "q"]
466
+ );
467
+ assert_eq!(
468
+ tmux_cancel_mode_argv(&PaneId::new("%7"), PaneMode::Client),
469
+ vec!["tmux", "send-keys", "-t", "%7", "d"]
470
+ );
471
+ assert_eq!(
472
+ tmux_cancel_mode_argv(&PaneId::new("%7"), PaneMode::Unknown),
473
+ vec!["tmux", "send-keys", "-t", "%7", "-X", "cancel"]
474
+ );
475
+ }
476
+
477
+ // ════════════════════════════════════════════════════════════════════════
478
+ // GROUP F — CaptureRange tail/full 语义 + capture 出口规范化收口
479
+ // tmux `-S -<N>`(Tail)/ `-S -`(Full);DELIVERY_CAPTURE_LINES=40 是默认 tail。
480
+ // ════════════════════════════════════════════════════════════════════════
481
+
482
+ #[test]
483
+ fn capture_range_tail_and_full_distinct() {
484
+ assert_ne!(CaptureRange::Tail(40), CaptureRange::Full);
485
+ assert_ne!(CaptureRange::Tail(40), CaptureRange::Tail(30));
486
+ assert_eq!(CaptureRange::Tail(40), CaptureRange::Tail(40));
487
+ }
488
+
489
+ #[test]
490
+ fn capture_tail_records_range_in_captured_text() {
491
+ // CapturedText 携带它抓取的 range + 出口规范化文本。
492
+ // contracts-rust-native test_capture_normalized_text_parity。RED via stub。
493
+ let t = tmux();
494
+ let cap = t
495
+ .capture(&Target::Pane(PaneId::new("%7")), CaptureRange::Tail(40))
496
+ .expect("capture ok");
497
+ assert_eq!(cap.range, CaptureRange::Tail(40));
498
+
499
+ // 命令构造 golden(STEP-9):Tail(40) → capture-pane -p -S -40 -t %7
500
+ // (tmux_prompt.py:149 / tmux_io.py:410)。范围回显不够 —— 钉死 argv 映射。
501
+ assert_eq!(
502
+ tmux_capture_argv(&PaneId::new("%7"), CaptureRange::Tail(40)),
503
+ vec!["tmux", "capture-pane", "-p", "-S", "-40", "-t", "%7"]
504
+ );
505
+ // 出口文本规范化 golden(§4b design line 399-400):逐行 rstrip + \xa0→空格。
506
+ assert_eq!(
507
+ normalize_capture("line one \nbusy\u{a0}marker \n \n"),
508
+ "line one\nbusy marker\n\n"
509
+ );
510
+ }
511
+
512
+ #[test]
513
+ fn capture_full_returns_complete_history() {
514
+ // contracts-rust-native test_capture_full_scrollback_parity(tmux -S -)。RED via stub。
515
+ let t = tmux();
516
+ let cap = t
517
+ .capture(&Target::Pane(PaneId::new("%7")), CaptureRange::Full)
518
+ .expect("capture full ok");
519
+ assert_eq!(cap.range, CaptureRange::Full);
520
+
521
+ // 命令构造 golden:Full → capture-pane -p -S - -t %7(全量 scrollback,runtime.py:519)。
522
+ // 注意是 `-S -`(空 N)不是 `-S -0`。
523
+ assert_eq!(
524
+ tmux_capture_argv(&PaneId::new("%7"), CaptureRange::Full),
525
+ vec!["tmux", "capture-pane", "-p", "-S", "-", "-t", "%7"]
526
+ );
527
+ }