@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,327 +1,326 @@
1
- // ════════════════════════════════════════════════════════════════════════
2
- // GROUP G — PaneField 单字段查询映射(display-message -p -F → list json 字段)
3
- // 非 tmux 后端无对应概念的字段返回 Ok(None)(typed「不适用」)。
4
- // ════════════════════════════════════════════════════════════════════════
5
- use super::*;
1
+ // ════════════════════════════════════════════════════════════════════════
2
+ // GROUP G — PaneField 单字段查询映射(display-message -p -F → list json 字段)
3
+ // 非 tmux 后端无对应概念的字段返回 Ok(None)(typed「不适用」)。
4
+ // ════════════════════════════════════════════════════════════════════════
5
+ use super::*;
6
6
 
7
- #[test]
8
- fn pane_field_variants_distinct() {
9
- // 6 个查询字段互不相等(避免散字符串打地鼠)。
10
- let all = [
11
- PaneField::PaneId,
12
- PaneField::PaneMode,
13
- PaneField::PaneWidth,
14
- PaneField::PaneCurrentCommand,
15
- PaneField::PaneCurrentPath,
16
- PaneField::SessionName,
17
- ];
18
- for (i, a) in all.iter().enumerate() {
19
- for (j, b) in all.iter().enumerate() {
20
- assert_eq!(i == j, a == b, "{a:?} vs {b:?}");
21
- }
7
+ #[test]
8
+ fn pane_field_variants_distinct() {
9
+ // 6 个查询字段互不相等(避免散字符串打地鼠)。
10
+ let all = [
11
+ PaneField::PaneId,
12
+ PaneField::PaneMode,
13
+ PaneField::PaneWidth,
14
+ PaneField::PaneCurrentCommand,
15
+ PaneField::PaneCurrentPath,
16
+ PaneField::SessionName,
17
+ ];
18
+ for (i, a) in all.iter().enumerate() {
19
+ for (j, b) in all.iter().enumerate() {
20
+ assert_eq!(i == j, a == b, "{a:?} vs {b:?}");
22
21
  }
23
22
  }
23
+ }
24
24
 
25
- #[test]
26
- fn query_pane_width_returns_numeric_string() {
27
- // contracts-rust-native test_query_pane_width_parity:
28
- // tmux display-message '#{pane_width}' / wezterm list json size.cols。RED via stub。
29
- let t = tmux();
30
- let w = t
31
- .query(&Target::Pane(PaneId::new("%7")), PaneField::PaneWidth)
32
- .expect("query ok");
33
- assert!(w.is_some(), "tmux must report a pane width");
34
- assert!(
35
- w.unwrap().chars().all(|c| c.is_ascii_digit()),
36
- "width is numeric"
37
- );
25
+ #[test]
26
+ fn query_pane_width_returns_numeric_string() {
27
+ // contracts-rust-native test_query_pane_width_parity:
28
+ // tmux display-message '#{pane_width}' / wezterm list json size.cols。RED via stub。
29
+ let t = tmux();
30
+ let w = t
31
+ .query(&Target::Pane(PaneId::new("%7")), PaneField::PaneWidth)
32
+ .expect("query ok");
33
+ assert!(w.is_some(), "tmux must report a pane width");
34
+ assert!(
35
+ w.unwrap().chars().all(|c| c.is_ascii_digit()),
36
+ "width is numeric"
37
+ );
38
38
 
39
- // 命令构造 golden(STEP-9):PaneWidth → display-message -p -t %7 -F '#{pane_width}'
40
- // (delivery.py:34)。格式 check 不锁字段映射 —— 钉死 argv。
41
- assert_eq!(
42
- tmux_query_argv(&PaneId::new("%7"), PaneField::PaneWidth),
43
- vec![
44
- "tmux",
45
- "display-message",
46
- "-p",
47
- "-t",
48
- "%7",
49
- "-F",
50
- "#{pane_width}"
51
- ]
52
- );
53
- // PaneMode 用裸格式参数(无 -F),pane_mode 直传(tmux_io.py:403)。
54
- assert_eq!(
55
- tmux_query_argv(&PaneId::new("%7"), PaneField::PaneMode),
56
- vec!["tmux", "display-message", "-p", "-t", "%7", "#{pane_mode}"]
57
- );
58
- }
39
+ // 命令构造 golden(STEP-9):PaneWidth → display-message -p -t %7 -F '#{pane_width}'
40
+ // (delivery.py:34)。格式 check 不锁字段映射 —— 钉死 argv。
41
+ assert_eq!(
42
+ tmux_query_argv(&PaneId::new("%7"), PaneField::PaneWidth),
43
+ vec![
44
+ "tmux",
45
+ "display-message",
46
+ "-p",
47
+ "-t",
48
+ "%7",
49
+ "-F",
50
+ "#{pane_width}"
51
+ ]
52
+ );
53
+ // PaneMode 用裸格式参数(无 -F),pane_mode 直传(tmux_io.py:403)。
54
+ assert_eq!(
55
+ tmux_query_argv(&PaneId::new("%7"), PaneField::PaneMode),
56
+ vec!["tmux", "display-message", "-p", "-t", "%7", "#{pane_mode}"]
57
+ );
58
+ }
59
59
 
60
- #[test]
61
- fn query_pane_mode_is_not_applicable_on_wezterm() {
62
- // 设计 §3.1 query(pane_mode):非 tmux 后端无 pane-mode 概念 → recognizer 恒当 Input。
63
- // typed「不适用」收口为 Ok(None)(porter 不能返回 Ok(Some("anything")) 假绿)。RED via stub。
64
- let w = wezterm();
65
- let mode = w
66
- .query(&Target::Pane(PaneId::new("1")), PaneField::PaneMode)
67
- .expect("query pane_mode ok on wezterm (no error)");
68
- assert_eq!(
69
- mode, None,
70
- "wezterm has no pane-mode concept -> typed not-applicable is Ok(None)"
71
- );
72
- }
60
+ #[test]
61
+ fn query_pane_mode_is_not_applicable_on_wezterm() {
62
+ // 设计 §3.1 query(pane_mode):非 tmux 后端无 pane-mode 概念 → recognizer 恒当 Input。
63
+ // typed「不适用」收口为 Ok(None)(porter 不能返回 Ok(Some("anything")) 假绿)。RED via stub。
64
+ let w = wezterm();
65
+ let mode = w
66
+ .query(&Target::Pane(PaneId::new("1")), PaneField::PaneMode)
67
+ .expect("query pane_mode ok on wezterm (no error)");
68
+ assert_eq!(
69
+ mode, None,
70
+ "wezterm has no pane-mode concept -> typed not-applicable is Ok(None)"
71
+ );
72
+ }
73
73
 
74
- // ════════════════════════════════════════════════════════════════════════
75
- // GROUP H — liveness 三态(unknown ≠ dead ≠ live)
76
- // contracts-rust-native test_pane_liveness_three_valued_parity。
77
- // ════════════════════════════════════════════════════════════════════════
74
+ // ════════════════════════════════════════════════════════════════════════
75
+ // GROUP H — liveness 三态(unknown ≠ dead ≠ live)
76
+ // contracts-rust-native test_pane_liveness_three_valued_parity。
77
+ // ════════════════════════════════════════════════════════════════════════
78
78
 
79
- #[test]
80
- fn freshly_spawned_pane_is_live_not_unknown_via_transport() {
81
- // 已知存活 fixture:刚 spawn 的 pane 必须 == Live(不是 all-variant 析取的同义反复)。
82
- // 镜像 conpty_cross_process...==Unknown 的强度,但锁的是 Live 这一极。
83
- // RED via stub:spawn_first/liveness 都 unimplemented!()。
84
- let t = tmux();
85
- let env = BTreeMap::new();
86
- let spawned = t
87
- .spawn_first(
88
- &SessionName::new("team-sess"),
89
- &WindowName::new("win-1"),
90
- &["sh".into(), "-lc".into(), "sleep 60".into()],
91
- Path::new("/tmp/ws"),
92
- &env,
93
- )
94
- .expect("spawn_first ok");
95
- assert_eq!(
96
- t.liveness(&spawned.pane_id).expect("liveness ok"),
97
- PaneLiveness::Live,
98
- "a freshly-spawned (sleeping) pane must read Live, never Unknown/Dead"
99
- );
100
- }
79
+ #[test]
80
+ fn freshly_spawned_pane_is_live_not_unknown_via_transport() {
81
+ // 已知存活 fixture:刚 spawn 的 pane 必须 == Live(不是 all-variant 析取的同义反复)。
82
+ // 镜像 conpty_cross_process...==Unknown 的强度,但锁的是 Live 这一极。
83
+ // RED via stub:spawn_first/liveness 都 unimplemented!()。
84
+ let t = tmux();
85
+ let env = BTreeMap::new();
86
+ let spawned = t
87
+ .spawn_first(
88
+ &SessionName::new("team-sess"),
89
+ &WindowName::new("win-1"),
90
+ &["sh".into(), "-lc".into(), "sleep 60".into()],
91
+ Path::new("/tmp/ws"),
92
+ &env,
93
+ )
94
+ .expect("spawn_first ok");
95
+ assert_eq!(
96
+ t.liveness(&spawned.pane_id).expect("liveness ok"),
97
+ PaneLiveness::Live,
98
+ "a freshly-spawned (sleeping) pane must read Live, never Unknown/Dead"
99
+ );
100
+ }
101
101
 
102
- #[test]
103
- fn conpty_cross_process_pane_is_unknown_never_optimistic_live() {
104
- // contracts-rust-native:ConPTY 对非自有/跨进程 pane 判 Unknown,绝不乐观当 Live
105
- // (bug-085 穷尽三态;Unknown 显式 block ping,不 fallthrough idle)。RED via stub。
106
- let c = conpty();
107
- let res = c.liveness(&PaneId::new("foreign-leader")).expect("liveness ok");
108
- assert_eq!(
109
- res,
110
- PaneLiveness::Unknown,
111
- "ConPTY cross-process pane must be Unknown, never optimistic Live"
112
- );
113
- }
102
+ #[test]
103
+ fn conpty_cross_process_pane_is_unknown_never_optimistic_live() {
104
+ // contracts-rust-native:ConPTY 对非自有/跨进程 pane 判 Unknown,绝不乐观当 Live
105
+ // (bug-085 穷尽三态;Unknown 显式 block ping,不 fallthrough idle)。RED via stub。
106
+ let c = conpty();
107
+ let res = c
108
+ .liveness(&PaneId::new("foreign-leader"))
109
+ .expect("liveness ok");
110
+ assert_eq!(
111
+ res,
112
+ PaneLiveness::Unknown,
113
+ "ConPTY cross-process pane must be Unknown, never optimistic Live"
114
+ );
115
+ }
114
116
 
115
- // ════════════════════════════════════════════════════════════════════════
116
- // GROUP I — 能力性拒绝是 typed outcome,不是 Err(§10)
117
- // SetEnvOutcome / AttachOutcome 的 per-backend 语义(transport-backend-design §4c/§1.3)。
118
- // ════════════════════════════════════════════════════════════════════════
117
+ // ════════════════════════════════════════════════════════════════════════
118
+ // GROUP I — 能力性拒绝是 typed outcome,不是 Err(§10)
119
+ // SetEnvOutcome / AttachOutcome 的 per-backend 语义(transport-backend-design §4c/§1.3)。
120
+ // ════════════════════════════════════════════════════════════════════════
119
121
 
120
- #[test]
121
- fn set_env_outcome_variants_carry_per_backend_semantics() {
122
- // 三态语义不可混:tmux=Applied;wezterm/conpty worker=InternalizedAtSpawn;
123
- // 外部 leader pane=UnsupportedForExternalPane{reason}(typed,审计,非 Err)。
124
- assert_ne!(SetEnvOutcome::Applied, SetEnvOutcome::InternalizedAtSpawn);
125
- let unsupported = SetEnvOutcome::UnsupportedForExternalPane {
126
- reason: "external leader pane cannot be re-seeded".into(),
127
- };
128
- assert_ne!(unsupported, SetEnvOutcome::Applied);
129
- if let SetEnvOutcome::UnsupportedForExternalPane { reason } = unsupported {
130
- assert!(!reason.is_empty(), "refusal must carry an auditable reason");
131
- } else {
132
- panic!("expected UnsupportedForExternalPane");
133
- }
122
+ #[test]
123
+ fn set_env_outcome_variants_carry_per_backend_semantics() {
124
+ // 三态语义不可混:tmux=Applied;wezterm/conpty worker=InternalizedAtSpawn;
125
+ // 外部 leader pane=UnsupportedForExternalPane{reason}(typed,审计,非 Err)。
126
+ assert_ne!(SetEnvOutcome::Applied, SetEnvOutcome::InternalizedAtSpawn);
127
+ let unsupported = SetEnvOutcome::UnsupportedForExternalPane {
128
+ reason: "external leader pane cannot be re-seeded".into(),
129
+ };
130
+ assert_ne!(unsupported, SetEnvOutcome::Applied);
131
+ if let SetEnvOutcome::UnsupportedForExternalPane { reason } = unsupported {
132
+ assert!(!reason.is_empty(), "refusal must carry an auditable reason");
133
+ } else {
134
+ panic!("expected UnsupportedForExternalPane");
134
135
  }
136
+ }
135
137
 
136
- #[test]
137
- fn set_session_env_on_tmux_worker_is_applied_typed_not_err() {
138
- // tmux set-environment → Applied(§4c)。能力性结果走 Ok(outcome),不走 Err。RED via stub。
139
- let t = tmux();
140
- let outcome = t
141
- .set_session_env(&SessionName::new("team-sess"), "TEAM_AGENT_ID", "w1")
142
- .expect("set_session_env must be Ok(outcome), capability refusal is NOT Err");
143
- assert_eq!(outcome, SetEnvOutcome::Applied);
144
- }
138
+ #[test]
139
+ fn set_session_env_on_tmux_worker_is_applied_typed_not_err() {
140
+ // tmux set-environment → Applied(§4c)。能力性结果走 Ok(outcome),不走 Err。RED via stub。
141
+ let t = tmux();
142
+ let outcome = t
143
+ .set_session_env(&SessionName::new("team-sess"), "TEAM_AGENT_ID", "w1")
144
+ .expect("set_session_env must be Ok(outcome), capability refusal is NOT Err");
145
+ assert_eq!(outcome, SetEnvOutcome::Applied);
146
+ }
145
147
 
146
- #[test]
147
- fn set_session_env_on_wezterm_worker_is_internalized_at_spawn() {
148
- // wezterm/conpty worker env 已在 spawn 注入 → InternalizedAtSpawn,非 Err、非假绿。RED via stub。
149
- let w = wezterm();
150
- let outcome = w
151
- .set_session_env(&SessionName::new("team-sess"), "K", "V")
152
- .expect("must be Ok(outcome)");
153
- assert_eq!(outcome, SetEnvOutcome::InternalizedAtSpawn);
154
- }
148
+ #[test]
149
+ fn set_session_env_on_wezterm_worker_is_internalized_at_spawn() {
150
+ // wezterm/conpty worker env 已在 spawn 注入 → InternalizedAtSpawn,非 Err、非假绿。RED via stub。
151
+ let w = wezterm();
152
+ let outcome = w
153
+ .set_session_env(&SessionName::new("team-sess"), "K", "V")
154
+ .expect("must be Ok(outcome)");
155
+ assert_eq!(outcome, SetEnvOutcome::InternalizedAtSpawn);
156
+ }
155
157
 
156
- #[test]
157
- fn attach_outcome_variants_are_distinct_per_backend() {
158
- // tmux=Attached;wezterm=GuiAttachIsImplicit;conpty=Unsupported{reason}。
159
- assert_ne!(AttachOutcome::Attached, AttachOutcome::GuiAttachIsImplicit);
160
- let unsup = AttachOutcome::Unsupported {
161
- reason: "conpty has no attach concept".into(),
162
- };
163
- assert_ne!(unsup, AttachOutcome::Attached);
164
- if let AttachOutcome::Unsupported { reason } = unsup {
165
- assert!(!reason.is_empty());
166
- } else {
167
- panic!("expected Unsupported");
168
- }
158
+ #[test]
159
+ fn attach_outcome_variants_are_distinct_per_backend() {
160
+ // tmux=Attached;wezterm=GuiAttachIsImplicit;conpty=Unsupported{reason}。
161
+ assert_ne!(AttachOutcome::Attached, AttachOutcome::GuiAttachIsImplicit);
162
+ let unsup = AttachOutcome::Unsupported {
163
+ reason: "conpty has no attach concept".into(),
164
+ };
165
+ assert_ne!(unsup, AttachOutcome::Attached);
166
+ if let AttachOutcome::Unsupported { reason } = unsup {
167
+ assert!(!reason.is_empty());
168
+ } else {
169
+ panic!("expected Unsupported");
169
170
  }
171
+ }
170
172
 
171
- #[test]
172
- fn attach_session_conpty_is_unsupported_typed_not_err() {
173
- // contracts/design §3.1:conpty attach_session → Unsupported(typed,不假绿,非 Err)。RED via stub。
174
- let c = conpty();
175
- let outcome = c
176
- .attach_session(&SessionName::new("team-sess"))
177
- .expect("capability refusal must be Ok(Unsupported), not Err");
178
- // §10 不变量:能力性拒绝必须携带可审计 reason(不仅锁 variant,还锁 payload 非空)。
179
- match outcome {
180
- AttachOutcome::Unsupported { reason } => {
181
- assert!(
182
- !reason.is_empty(),
183
- "ConPTY attach refusal must carry an auditable reason"
184
- );
185
- }
186
- other => panic!("expected AttachOutcome::Unsupported, got {other:?}"),
173
+ #[test]
174
+ fn attach_session_conpty_is_unsupported_typed_not_err() {
175
+ // contracts/design §3.1:conpty attach_session → Unsupported(typed,不假绿,非 Err)。RED via stub。
176
+ let c = conpty();
177
+ let outcome = c
178
+ .attach_session(&SessionName::new("team-sess"))
179
+ .expect("capability refusal must be Ok(Unsupported), not Err");
180
+ // §10 不变量:能力性拒绝必须携带可审计 reason(不仅锁 variant,还锁 payload 非空)。
181
+ match outcome {
182
+ AttachOutcome::Unsupported { reason } => {
183
+ assert!(
184
+ !reason.is_empty(),
185
+ "ConPTY attach refusal must carry an auditable reason"
186
+ );
187
187
  }
188
+ other => panic!("expected AttachOutcome::Unsupported, got {other:?}"),
188
189
  }
190
+ }
189
191
 
190
- #[test]
191
- fn attach_session_wezterm_gui_attach_is_implicit() {
192
- // wezterm GUI 启动即 attach,无独立动作 → GuiAttachIsImplicit。RED via stub。
193
- let w = wezterm();
194
- let outcome = w
195
- .attach_session(&SessionName::new("team-sess"))
196
- .expect("must be Ok(outcome)");
197
- assert_eq!(outcome, AttachOutcome::GuiAttachIsImplicit);
198
- }
192
+ #[test]
193
+ fn attach_session_wezterm_gui_attach_is_implicit() {
194
+ // wezterm GUI 启动即 attach,无独立动作 → GuiAttachIsImplicit。RED via stub。
195
+ let w = wezterm();
196
+ let outcome = w
197
+ .attach_session(&SessionName::new("team-sess"))
198
+ .expect("must be Ok(outcome)");
199
+ assert_eq!(outcome, AttachOutcome::GuiAttachIsImplicit);
200
+ }
199
201
 
200
- // ════════════════════════════════════════════════════════════════════════
201
- // GROUP J — TransportError Display(thiserror;lib 边界)+ I/O 才走 Err
202
- // bug-084:os I/O/timeout 必须 Result 化,绝不裸穿 coordinator tick。
203
- // ════════════════════════════════════════════════════════════════════════
202
+ // ════════════════════════════════════════════════════════════════════════
203
+ // GROUP J — TransportError Display(thiserror;lib 边界)+ I/O 才走 Err
204
+ // bug-084:os I/O/timeout 必须 Result 化,绝不裸穿 coordinator tick。
205
+ // ════════════════════════════════════════════════════════════════════════
204
206
 
205
- #[test]
206
- fn transport_error_display_messages_byte_locked() {
207
- // 错误信息含定位字段(backend/stage/argv/target),便于 §5 系统集成调试。
208
- let e = TransportError::Inject {
209
- stage: InjectStage::PasteBuffer,
210
- source: std::io::Error::other("boom"),
211
- };
212
- assert_eq!(
213
- e.to_string(),
214
- "inject failed at stage PasteBuffer: boom"
215
- );
207
+ #[test]
208
+ fn transport_error_display_messages_byte_locked() {
209
+ // 错误信息含定位字段(backend/stage/argv/target),便于 §5 系统集成调试。
210
+ let e = TransportError::Inject {
211
+ stage: InjectStage::PasteBuffer,
212
+ source: std::io::Error::other("boom"),
213
+ };
214
+ assert_eq!(e.to_string(), "inject failed at stage PasteBuffer: boom");
216
215
 
217
- let sub = TransportError::Subprocess {
218
- argv: vec!["tmux".into(), "paste-buffer".into()],
219
- code: Some(1),
220
- stderr: "no such pane".into(),
221
- };
222
- assert_eq!(
223
- sub.to_string(),
224
- "subprocess [\"tmux\", \"paste-buffer\"] exited with Some(1): no such pane"
225
- );
216
+ let sub = TransportError::Subprocess {
217
+ argv: vec!["tmux".into(), "paste-buffer".into()],
218
+ code: Some(1),
219
+ stderr: "no such pane".into(),
220
+ };
221
+ assert_eq!(
222
+ sub.to_string(),
223
+ "subprocess [\"tmux\", \"paste-buffer\"] exited with Some(1): no such pane"
224
+ );
226
225
 
227
- let mux = TransportError::MuxUnavailable {
228
- backend: BackendKind::WezTerm,
229
- detail: "mux server not reachable".into(),
230
- };
231
- assert_eq!(
232
- mux.to_string(),
233
- "mux unavailable on WezTerm: mux server not reachable"
234
- );
226
+ let mux = TransportError::MuxUnavailable {
227
+ backend: BackendKind::WezTerm,
228
+ detail: "mux server not reachable".into(),
229
+ };
230
+ assert_eq!(
231
+ mux.to_string(),
232
+ "mux unavailable on WezTerm: mux server not reachable"
233
+ );
235
234
 
236
- let nf = TransportError::TargetNotFound {
237
- target: "%99".into(),
238
- };
239
- assert_eq!(nf.to_string(), "target not found: %99");
240
- }
235
+ let nf = TransportError::TargetNotFound {
236
+ target: "%99".into(),
237
+ };
238
+ assert_eq!(nf.to_string(), "target not found: %99");
239
+ }
241
240
 
242
- #[test]
243
- fn io_error_converts_into_transport_error_via_from() {
244
- // bug-084:I/O 错误 typed 化(#[from] std::io::Error)。
245
- let io = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
246
- let te: TransportError = io.into();
247
- assert!(matches!(te, TransportError::Io(_)));
248
- }
241
+ #[test]
242
+ fn io_error_converts_into_transport_error_via_from() {
243
+ // bug-084:I/O 错误 typed 化(#[from] std::io::Error)。
244
+ let io = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
245
+ let te: TransportError = io.into();
246
+ assert!(matches!(te, TransportError::Io(_)));
247
+ }
249
248
 
250
- // ════════════════════════════════════════════════════════════════════════
251
- // GROUP K — backend kind 诊断标识 + buffer 不进 trait(内化)
252
- // ════════════════════════════════════════════════════════════════════════
249
+ // ════════════════════════════════════════════════════════════════════════
250
+ // GROUP K — backend kind 诊断标识 + buffer 不进 trait(内化)
251
+ // ════════════════════════════════════════════════════════════════════════
253
252
 
254
- #[test]
255
- fn backend_kind_is_reported_per_backend() {
256
- assert_eq!(tmux().kind(), BackendKind::Tmux);
257
- assert_eq!(wezterm().kind(), BackendKind::WezTerm);
258
- assert_eq!(conpty().kind(), BackendKind::ConPty);
259
- }
253
+ #[test]
254
+ fn backend_kind_is_reported_per_backend() {
255
+ assert_eq!(tmux().kind(), BackendKind::Tmux);
256
+ assert_eq!(wezterm().kind(), BackendKind::WezTerm);
257
+ assert_eq!(conpty().kind(), BackendKind::ConPty);
258
+ }
260
259
 
261
- #[test]
262
- fn buffer_lifecycle_internalized_trait_exposes_only_inject() {
263
- // contracts-rust-native test_buffer_lifecycle_internalized_parity:
264
- // set/load/delete-buffer 是 tmux inject 的内部细节,trait 只暴露 inject;
265
- // 外部行为(注入成功、无陈旧 buffer 误注入)跨后端等价。
266
- // 这里以契约形式锁住:Transport trait 上**没有**任何 buffer 方法面 —— 唯一物理出口是 inject。
267
- // (编译期:若 porter 误把 buffer 提进 trait,本 stub 不再实现 trait → 编译失败。)
268
- let t = tmux();
269
- let report = t
270
- .inject(
271
- &Target::Pane(PaneId::new("%7")),
272
- &InjectPayload::Text("payload".into()),
273
- Key::Enter,
274
- true,
275
- )
276
- .expect("inject ok");
277
- // 非空文本注入抵达 submit,token 缺失时 verification=no_token(tmux_io.py:140)。
278
- assert_eq!(report.stage_reached, InjectStage::Submit);
279
- assert_eq!(report.inject_verification, InjectVerification::NoToken);
280
- // 「无陈旧 buffer 误注入」是行为锁:内部 argv 序列必须**以 delete-buffer 收尾**
281
- // (set-buffer → paste-buffer -p → delete-buffer;tmux_io.py:119-120)。
282
- let argv = tmux_inject_text_argv(&PaneId::new("%7"), "team-agent-buf", "payload", true);
283
- assert_eq!(
284
- argv.last(),
285
- Some(&vec![
286
- "tmux".to_string(),
287
- "delete-buffer".to_string(),
288
- "-b".to_string(),
289
- "team-agent-buf".to_string(),
290
- ]),
291
- "inject must delete its buffer (no stale buffer mis-injection)"
292
- );
293
- }
260
+ #[test]
261
+ fn buffer_lifecycle_internalized_trait_exposes_only_inject() {
262
+ // contracts-rust-native test_buffer_lifecycle_internalized_parity:
263
+ // set/load/delete-buffer 是 tmux inject 的内部细节,trait 只暴露 inject;
264
+ // 外部行为(注入成功、无陈旧 buffer 误注入)跨后端等价。
265
+ // 这里以契约形式锁住:Transport trait 上**没有**任何 buffer 方法面 —— 唯一物理出口是 inject。
266
+ // (编译期:若 porter 误把 buffer 提进 trait,本 stub 不再实现 trait → 编译失败。)
267
+ let t = tmux();
268
+ let report = t
269
+ .inject(
270
+ &Target::Pane(PaneId::new("%7")),
271
+ &InjectPayload::Text("payload".into()),
272
+ Key::Enter,
273
+ true,
274
+ )
275
+ .expect("inject ok");
276
+ // 非空文本注入抵达 submit,token 缺失时 verification=no_token(tmux_io.py:140)。
277
+ assert_eq!(report.stage_reached, InjectStage::Submit);
278
+ assert_eq!(report.inject_verification, InjectVerification::NoToken);
279
+ // 「无陈旧 buffer 误注入」是行为锁:内部 argv 序列必须**以 delete-buffer 收尾**
280
+ // (set-buffer → paste-buffer -p → delete-buffer;tmux_io.py:119-120)。
281
+ let argv = tmux_inject_text_argv(&PaneId::new("%7"), "team-agent-buf", "payload", true);
282
+ assert_eq!(
283
+ argv.last(),
284
+ Some(&vec![
285
+ "tmux".to_string(),
286
+ "delete-buffer".to_string(),
287
+ "-b".to_string(),
288
+ "team-agent-buf".to_string(),
289
+ ]),
290
+ "inject must delete its buffer (no stale buffer mis-injection)"
291
+ );
292
+ }
294
293
 
295
- // ════════════════════════════════════════════════════════════════════════
296
- // GROUP L — [真机] gated:WezTerm cli list / 正向身份登记(无法在此完整断言)
297
- // ════════════════════════════════════════════════════════════════════════
294
+ // ════════════════════════════════════════════════════════════════════════
295
+ // GROUP L — [真机] gated:WezTerm cli list / 正向身份登记(无法在此完整断言)
296
+ // ════════════════════════════════════════════════════════════════════════
298
297
 
299
- #[test]
300
- #[ignore = "real-machine-gated (fixture_kind=real): needs wezterm cli list --format json; \
298
+ #[test]
299
+ #[ignore = "real-machine-gated (fixture_kind=real): needs wezterm cli list --format json; \
301
300
  asserts pane_pid/current_command/leader_env forward-registration (GAP-2/GAP-3)"]
302
- fn wezterm_list_targets_forward_registration_real_machine() {
303
- // contracts-rust-native test_list_targets_enumerate_parity /
304
- // test_worker_identity_forward_registration_parity:
305
- // WezTerm 身份靠正向登记表投影(不反向读进程 env)。仅锁形状,实现期真机校验。
306
- let w = wezterm();
307
- let listed = w.list_targets().expect("list_targets ok");
308
- for info in &listed {
309
- // 正向登记:leader_env 是登记表投影,不是反向读 /proc。
310
- // [真机] list json 未必给 pane_pid(GAP-2)/ current_command(GAP-3) → Option。
311
- let _ = (&info.leader_env, &info.pane_pid, &info.current_command);
312
- }
301
+ fn wezterm_list_targets_forward_registration_real_machine() {
302
+ // contracts-rust-native test_list_targets_enumerate_parity /
303
+ // test_worker_identity_forward_registration_parity:
304
+ // WezTerm 身份靠正向登记表投影(不反向读进程 env)。仅锁形状,实现期真机校验。
305
+ let w = wezterm();
306
+ let listed = w.list_targets().expect("list_targets ok");
307
+ for info in &listed {
308
+ // 正向登记:leader_env 是登记表投影,不是反向读 /proc。
309
+ // [真机] list json 未必给 pane_pid(GAP-2)/ current_command(GAP-3) → Option。
310
+ let _ = (&info.leader_env, &info.pane_pid, &info.current_command);
313
311
  }
312
+ }
314
313
 
315
- #[test]
316
- #[ignore = "real-machine-gated (B1): kill_window/kill_session liveness transition needs a live mux"]
317
- fn kill_window_then_target_dead_real_machine() {
318
- // contracts-rust-native test_kill_window_stops_agent_parity:
319
- // kill_window 后 liveness=Dead 且 list_targets 不再列出。实现期真机校验。
320
- let t = tmux();
321
- t.kill_window(&Target::Pane(PaneId::new("%7")))
322
- .expect("kill_window ok");
323
- assert_eq!(
324
- t.liveness(&PaneId::new("%7")).expect("liveness ok"),
325
- PaneLiveness::Dead
326
- );
327
- }
314
+ #[test]
315
+ #[ignore = "real-machine-gated (B1): kill_window/kill_session liveness transition needs a live mux"]
316
+ fn kill_window_then_target_dead_real_machine() {
317
+ // contracts-rust-native test_kill_window_stops_agent_parity:
318
+ // kill_window 后 liveness=Dead 且 list_targets 不再列出。实现期真机校验。
319
+ let t = tmux();
320
+ t.kill_window(&Target::Pane(PaneId::new("%7")))
321
+ .expect("kill_window ok");
322
+ assert_eq!(
323
+ t.liveness(&PaneId::new("%7")).expect("liveness ok"),
324
+ PaneLiveness::Dead
325
+ );
326
+ }