@team-agent/installer 0.5.106 → 0.5.107

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 (29) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/tests/base.rs +112 -225
  4. package/crates/team-agent/src/compiler/tests.rs +46 -59
  5. package/crates/team-agent/src/leader/tests/byte_findings.rs +1 -14
  6. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +57 -173
  7. package/crates/team-agent/src/lifecycle/launch/pi_mcp_extension.js +226 -0
  8. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +0 -37
  9. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +0 -9
  10. package/crates/team-agent/src/lifecycle/tests/fixtures/pi_native_mcp_host.mjs +71 -0
  11. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -66
  12. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +0 -392
  13. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +0 -5
  14. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +2 -361
  15. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +1 -16
  16. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +20 -112
  17. package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1 -12
  18. package/crates/team-agent/src/lifecycle/tests/pi_native_mcp_capability_red.rs +546 -0
  19. package/crates/team-agent/src/lifecycle/tests.rs +1 -0
  20. package/crates/team-agent/src/lifecycle/worker_command_context.rs +11 -54
  21. package/crates/team-agent/src/messaging/tests/wave2.rs +2 -302
  22. package/crates/team-agent/src/model/name_similarity.rs +52 -67
  23. package/crates/team-agent/src/model/task_graph.rs +37 -124
  24. package/crates/team-agent/src/model/yaml/tests.rs +46 -165
  25. package/crates/team-agent/src/provider/tests/adapter.rs +0 -20
  26. package/crates/team-agent/src/provider/tests/classify.rs +106 -198
  27. package/crates/team-agent/src/tmux_backend/tests.rs +178 -501
  28. package/crates/team-agent/src/transport/tests/wire.rs +0 -55
  29. package/package.json +4 -4
package/Cargo.lock CHANGED
@@ -581,7 +581,7 @@ dependencies = [
581
581
 
582
582
  [[package]]
583
583
  name = "team-agent"
584
- version = "0.5.106"
584
+ version = "0.5.107"
585
585
  dependencies = [
586
586
  "anyhow",
587
587
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.106"
12
+ version = "0.5.107"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -13,33 +13,19 @@ use serde_json::json;
13
13
  // =========================================================================
14
14
 
15
15
  #[test]
16
- fn provider_args_strips_leading_dashdash() {
17
- // golden: _provider_args(["--","-x"]) == ["-x"]
18
- assert_eq!(
19
- provider_args(&["--".into(), "-x".into()]),
20
- vec!["-x".to_string()]
21
- );
22
- }
23
-
24
- #[test]
25
- fn provider_args_keeps_when_no_leading_dashdash() {
26
- // golden: _provider_args(["-x","-y"]) == ["-x","-y"]
27
- assert_eq!(
28
- provider_args(&["-x".into(), "-y".into()]),
29
- vec!["-x".to_string(), "-y".to_string()]
30
- );
31
- }
32
-
33
- #[test]
34
- fn provider_args_empty_is_empty() {
35
- // golden: _provider_args([]) == []
36
- assert_eq!(provider_args(&[]), Vec::<String>::new());
37
- }
38
-
39
- #[test]
40
- fn provider_args_lone_dashdash_yields_empty() {
41
- // golden: _provider_args(["--"]) == [] (values[1:] of single-elem list)
42
- assert_eq!(provider_args(&["--".into()]), Vec::<String>::new());
16
+ fn provider_args_cases() {
17
+ // (old case-id, input, Python golden); values[1:] of a lone "--" is empty.
18
+ const CASES: &[(&str, &[&str], &[&str])] = &[
19
+ ("provider_args_strips_leading_dashdash", &["--", "-x"], &["-x"]),
20
+ ("provider_args_keeps_when_no_leading_dashdash", &["-x", "-y"], &["-x", "-y"]),
21
+ ("provider_args_empty_is_empty", &[], &[]),
22
+ ("provider_args_lone_dashdash_yields_empty", &["--"], &[]),
23
+ ];
24
+ for (id, input, expected) in CASES {
25
+ let argv: Vec<String> = input.iter().map(|arg| (*arg).to_string()).collect();
26
+ let expected: Vec<String> = expected.iter().map(|arg| (*arg).to_string()).collect();
27
+ assert_eq!(provider_args(&argv), expected, "{id}");
28
+ }
43
29
  }
44
30
 
45
31
  // =========================================================================
@@ -47,118 +33,72 @@ fn provider_args_lone_dashdash_yields_empty() {
47
33
  // =========================================================================
48
34
 
49
35
  #[test]
50
- fn leader_launcher_args_empty_all_default() {
51
- // golden: {'provider_args': [], 'attach_existing': False, 'confirm_attach': False, 'attach_session': None}
52
- let got = leader_launcher_args(&[]).expect("empty should parse");
53
- assert_eq!(got, LeaderLauncherArgs::default());
54
- assert!(got.provider_args.is_empty());
55
- assert!(!got.attach_existing);
56
- assert!(!got.confirm_attach);
57
- assert_eq!(got.attach_session, None);
58
- assert!(!got.external_leader);
59
- }
60
-
61
- #[test]
62
- fn leader_launcher_args_attach_and_confirm() {
63
- // golden: ["--attach","--confirm"] -> attach_existing=True, confirm_attach=True
64
- let got = leader_launcher_args(&["--attach".into(), "--confirm".into()]).unwrap();
65
- assert!(got.attach_existing);
66
- assert!(got.confirm_attach);
67
- assert!(got.provider_args.is_empty());
68
- assert_eq!(got.attach_session, None);
69
- }
70
-
71
- #[test]
72
- fn leader_launcher_args_attach_existing_alias() {
73
- // golden: ["--attach-existing"] -> attach_existing=True (alias of --attach)
74
- let got = leader_launcher_args(&["--attach-existing".into()]).unwrap();
75
- assert!(got.attach_existing);
76
- assert!(!got.confirm_attach);
77
- }
78
-
79
- #[test]
80
- fn leader_launcher_args_external_leader_opt_out() {
81
- let got = leader_launcher_args(&[
82
- "--external-leader".into(),
83
- "--".into(),
84
- "--model".into(),
85
- "opus".into(),
86
- ])
87
- .unwrap();
88
- assert!(got.external_leader);
89
- assert!(!got.attach_existing);
90
- assert_eq!(
91
- got.provider_args,
92
- vec!["--model".to_string(), "opus".to_string()]
93
- );
94
- }
95
-
96
- #[test]
97
- fn leader_launcher_args_external_leader_after_dashdash_errors() {
98
- let err = leader_launcher_args(&["--".into(), "--external-leader".into()])
99
- .expect_err("Team Agent flags after -- must not be silently passed to provider");
100
- assert!(
101
- err.to_string()
102
- .contains("Team Agent launcher flag --external-leader must appear before --"),
103
- "unexpected error: {err}"
104
- );
105
- }
106
-
107
- #[test]
108
- fn leader_launcher_args_attach_session_spaced() {
109
- // golden: ["--attach-session","mysess"] -> attach_session="mysess"
110
- let got = leader_launcher_args(&["--attach-session".into(), "mysess".into()]).unwrap();
111
- assert_eq!(got.attach_session, Some("mysess".to_string()));
112
- assert!(!got.attach_existing);
113
- }
114
-
115
- #[test]
116
- fn leader_launcher_args_attach_session_equals() {
117
- // golden: ["--attach-session=mysess"] -> attach_session="mysess"
118
- let got = leader_launcher_args(&["--attach-session=mysess".into()]).unwrap();
119
- assert_eq!(got.attach_session, Some("mysess".to_string()));
120
- }
121
-
122
- #[test]
123
- fn leader_launcher_args_dashdash_passthrough_strips_separator() {
124
- // ["--attach","--","-x","--provider-confirm"] ->
125
- // provider_args=["-x","--provider-confirm"], attach_existing=True, confirm_attach=False
126
- // Known Team Agent launcher flags after `--` are rejected by a separate guard.
127
- let got = leader_launcher_args(&[
128
- "--attach".into(),
129
- "--".into(),
130
- "-x".into(),
131
- "--provider-confirm".into(),
132
- ])
133
- .unwrap();
134
- assert!(got.attach_existing);
135
- assert!(!got.confirm_attach);
136
- assert_eq!(
137
- got.provider_args,
138
- vec!["-x".to_string(), "--provider-confirm".to_string()]
139
- );
140
- }
141
-
142
- #[test]
143
- fn leader_launcher_args_unknown_tokens_collect_as_provider_args() {
144
- // golden: ["foo","--attach","bar"] -> provider_args=["foo","bar"], attach_existing=True
145
- let got = leader_launcher_args(&["foo".into(), "--attach".into(), "bar".into()]).unwrap();
146
- assert_eq!(
147
- got.provider_args,
148
- vec!["foo".to_string(), "bar".to_string()]
149
- );
150
- assert!(got.attach_existing);
151
- }
152
-
153
- #[test]
154
- fn leader_launcher_args_attach_session_missing_value_errors() {
155
- // golden: ["--attach-session"] raises RuntimeError("--attach-session requires a tmux session name")
156
- let err = leader_launcher_args(&["--attach-session".into()]).unwrap_err();
157
- let msg = err.to_string();
158
- assert!(
159
- msg.contains("--attach-session requires a tmux session name"),
160
- "expected exact missing-value message, got: {msg}"
161
- );
36
+ fn leader_launcher_args_success_cases() {
37
+ // (old case-id, argv, complete expected structure), including default false flags.
38
+ let cases: &[(&str, &[&str], LeaderLauncherArgs)] = &[
39
+ ("leader_launcher_args_empty_all_default", &[], LeaderLauncherArgs::default()),
40
+ ("leader_launcher_args_attach_and_confirm", &["--attach", "--confirm"], LeaderLauncherArgs {
41
+ attach_existing: true,
42
+ confirm_attach: true,
43
+ ..Default::default()
44
+ }),
45
+ // --attach-existing is an alias of --attach.
46
+ ("leader_launcher_args_attach_existing_alias", &["--attach-existing"], LeaderLauncherArgs {
47
+ attach_existing: true,
48
+ ..Default::default()
49
+ }),
50
+ ("leader_launcher_args_external_leader_opt_out", &["--external-leader", "--", "--model", "opus"], LeaderLauncherArgs {
51
+ provider_args: vec!["--model".to_string(), "opus".to_string()],
52
+ external_leader: true,
53
+ ..Default::default()
54
+ }),
55
+ ("leader_launcher_args_attach_session_spaced", &["--attach-session", "mysess"], LeaderLauncherArgs {
56
+ attach_session: Some("mysess".to_string()),
57
+ ..Default::default()
58
+ }),
59
+ ("leader_launcher_args_attach_session_equals", &["--attach-session=mysess"], LeaderLauncherArgs {
60
+ attach_session: Some("mysess".to_string()),
61
+ ..Default::default()
62
+ }),
63
+ // Known Team Agent launcher flags after `--` are rejected by the error table.
64
+ ("leader_launcher_args_dashdash_passthrough_strips_separator", &["--attach", "--", "-x", "--provider-confirm"], LeaderLauncherArgs {
65
+ provider_args: vec!["-x".to_string(), "--provider-confirm".to_string()],
66
+ attach_existing: true,
67
+ ..Default::default()
68
+ }),
69
+ ("leader_launcher_args_unknown_tokens_collect_as_provider_args", &["foo", "--attach", "bar"], LeaderLauncherArgs {
70
+ provider_args: vec!["foo".to_string(), "bar".to_string()],
71
+ attach_existing: true,
72
+ ..Default::default()
73
+ }),
74
+ ];
75
+ for (id, input, expected) in cases {
76
+ let argv: Vec<String> = input.iter().map(|arg| (*arg).to_string()).collect();
77
+ let got = leader_launcher_args(&argv).unwrap_or_else(|err| panic!("{id}: {err}"));
78
+ assert_eq!(got, *expected, "{id}");
79
+ }
80
+ }
81
+
82
+ #[test]
83
+ fn leader_launcher_args_error_cases() {
84
+ // Preserve the original error-substring contract, rather than requiring exact equality.
85
+ const CASES: &[(&str, &[&str], &str)] = &[
86
+ (
87
+ "leader_launcher_args_external_leader_after_dashdash_errors",
88
+ &["--", "--external-leader"],
89
+ "Team Agent launcher flag --external-leader must appear before --",
90
+ ),
91
+ (
92
+ "leader_launcher_args_attach_session_missing_value_errors",
93
+ &["--attach-session"],
94
+ "--attach-session requires a tmux session name",
95
+ ),
96
+ ];
97
+ for (id, input, expected) in CASES {
98
+ let argv: Vec<String> = input.iter().map(|arg| (*arg).to_string()).collect();
99
+ let err = leader_launcher_args(&argv).expect_err(id);
100
+ assert!(err.to_string().contains(*expected), "{id}: unexpected error: {err}");
101
+ }
162
102
  }
163
103
 
164
104
  // =========================================================================
@@ -166,37 +106,22 @@ fn leader_launcher_args_attach_session_missing_value_errors() {
166
106
  // =========================================================================
167
107
 
168
108
  #[test]
169
- fn send_target_fanout_strips_and_filters_empty() {
170
- // golden: _send_target(targets="a, b ,,c") == ["a","b","c"]
171
- let got = send_target(Some("a, b ,,c"), None);
172
- assert_eq!(
173
- got,
174
- MessageTarget::Fanout(vec!["a".to_string(), "b".to_string(), "c".to_string()])
175
- );
176
- }
177
-
178
- #[test]
179
- fn send_target_single_target() {
180
- // golden: _send_target(target="agent_x") == "agent_x"
181
- assert_eq!(
182
- send_target(None, Some("agent_x")),
183
- MessageTarget::Single("agent_x".to_string())
184
- );
185
- }
186
-
187
- #[test]
188
- fn send_target_broadcast_star() {
189
- // skeleton contract: bare "*" target -> Broadcast (send.py interprets "*" as全队广播)
190
- assert_eq!(send_target(None, Some("*")), MessageTarget::Broadcast);
191
- }
192
-
193
- #[test]
194
- fn send_target_empty_targets_falls_through_to_target() {
195
- // golden: targets="" is falsy in Python -> returns args.target ("fallback")
196
- assert_eq!(
197
- send_target(Some(""), Some("fallback")),
198
- MessageTarget::Single("fallback".to_string())
199
- );
109
+ fn send_target_cases() {
110
+ // (old case-id, targets, target, complete expected enum).
111
+ let cases: &[(&str, Option<&str>, Option<&str>, MessageTarget)] = &[
112
+ (
113
+ "send_target_fanout_strips_and_filters_empty", Some("a, b ,,c"), None,
114
+ MessageTarget::Fanout(vec!["a".to_string(), "b".to_string(), "c".to_string()]),
115
+ ),
116
+ ("send_target_single_target", None, Some("agent_x"), MessageTarget::Single("agent_x".to_string())),
117
+ // A bare "*" means broadcast to the whole team.
118
+ ("send_target_broadcast_star", None, Some("*"), MessageTarget::Broadcast),
119
+ // targets="" is falsy in Python, so it falls through to args.target.
120
+ ("send_target_empty_targets_falls_through_to_target", Some(""), Some("fallback"), MessageTarget::Single("fallback".to_string())),
121
+ ];
122
+ for (id, targets, target, expected) in cases {
123
+ assert_eq!(send_target(*targets, *target), *expected, "{id}");
124
+ }
200
125
  }
201
126
 
202
127
  // =========================================================================
@@ -205,61 +130,23 @@ fn send_target_empty_targets_falls_through_to_target() {
205
130
  // =========================================================================
206
131
 
207
132
  #[test]
208
- fn classify_failed_takes_priority() {
209
- // raw in {failed,error} OR hstatus in {failed,error} -> Failed
210
- assert_eq!(classify_agent_bucket("failed", ""), SummaryBucket::Failed);
211
- assert_eq!(classify_agent_bucket("error", ""), SummaryBucket::Failed);
212
- assert_eq!(
213
- classify_agent_bucket("running", "error"),
214
- SummaryBucket::Failed
215
- );
216
- }
217
-
218
- #[test]
219
- fn classify_stopped() {
220
- // raw in {stopped,done} OR hstatus==done -> Stopped
221
- assert_eq!(classify_agent_bucket("stopped", ""), SummaryBucket::Stopped);
222
- assert_eq!(classify_agent_bucket("done", ""), SummaryBucket::Stopped);
223
- assert_eq!(
224
- classify_agent_bucket("running", "done"),
225
- SummaryBucket::Stopped
226
- );
227
- }
228
-
229
- #[test]
230
- fn classify_busy() {
231
- // raw==busy OR hstatus in {running,working} -> Busy
232
- assert_eq!(classify_agent_bucket("busy", ""), SummaryBucket::Busy);
233
- assert_eq!(classify_agent_bucket("", "running"), SummaryBucket::Busy);
234
- assert_eq!(classify_agent_bucket("", "working"), SummaryBucket::Busy);
235
- }
236
-
237
- #[test]
238
- fn classify_hstatus_idle_beats_raw_running() {
239
- // golden: raw=running, h=idle -> idle (hstatus==idle branch precedes raw==running branch)
240
- assert_eq!(
241
- classify_agent_bucket("running", "idle"),
242
- SummaryBucket::Idle
243
- );
244
- }
245
-
246
- #[test]
247
- fn classify_pure_running() {
248
- // raw==running, no overriding hstatus -> Running
249
- assert_eq!(classify_agent_bucket("running", ""), SummaryBucket::Running);
250
- }
251
-
252
- #[test]
253
- fn classify_blocked_and_unmatched_are_unknown_never_idle() {
254
- // bug-071/077/085: blocked/stuck/missing AND any unmatched value -> Unknown, NOT idle.
255
- assert_eq!(classify_agent_bucket("blocked", ""), SummaryBucket::Unknown);
256
- assert_eq!(classify_agent_bucket("stuck", ""), SummaryBucket::Unknown);
257
- assert_eq!(classify_agent_bucket("", "missing"), SummaryBucket::Unknown);
258
- assert_eq!(
259
- classify_agent_bucket("weird_value", ""),
260
- SummaryBucket::Unknown
261
- );
262
- assert_eq!(classify_agent_bucket("", ""), SummaryBucket::Unknown);
133
+ fn classify_agent_bucket_cases() {
134
+ // (old case-id, all original (raw, health) inputs, expected bucket).
135
+ const CASES: &[(&str, &[(&str, &str)], SummaryBucket)] = &[
136
+ ("classify_failed_takes_priority", &[("failed", ""), ("error", ""), ("running", "error")], SummaryBucket::Failed),
137
+ ("classify_stopped", &[("stopped", ""), ("done", ""), ("running", "done")], SummaryBucket::Stopped),
138
+ ("classify_busy", &[("busy", ""), ("", "running"), ("", "working")], SummaryBucket::Busy),
139
+ // hstatus==idle precedes the raw==running branch.
140
+ ("classify_hstatus_idle_beats_raw_running", &[("running", "idle")], SummaryBucket::Idle),
141
+ ("classify_pure_running", &[("running", "")], SummaryBucket::Running),
142
+ // bug-071/077/085: unmatched and blocked/stuck/missing are Unknown, NEVER idle.
143
+ ("classify_blocked_and_unmatched_are_unknown_never_idle", &[("blocked", ""), ("stuck", ""), ("", "missing"), ("weird_value", ""), ("", "")], SummaryBucket::Unknown),
144
+ ];
145
+ for (id, inputs, expected) in CASES {
146
+ for &(raw, health) in inputs.iter() {
147
+ assert_eq!(classify_agent_bucket(raw, health), *expected, "{id}: raw={raw:?}, health={health:?}");
148
+ }
149
+ }
263
150
  }
264
151
 
265
152
  #[test]
@@ -119,65 +119,52 @@ Implement bounded tasks and report result_envelope_v1.
119
119
  // ───────────────────────────── read_front_matter ─────────────────────────────
120
120
 
121
121
  #[test]
122
- fn front_matter_no_marker_returns_empty_meta_and_full_text() {
123
- // No leading "---\n" → ({}, text) verbatim (compiler.py:175-176).
124
- let p = write_tmp("no_marker.md", "hello\nworld\n");
125
- let (meta, body) = read_front_matter(&p).unwrap();
126
- assert_eq!(meta, Value::Map(vec![]));
127
- assert_eq!(body, "hello\nworld\n");
128
- }
129
-
130
- #[test]
131
- fn front_matter_basic_splits_meta_and_lstrips_body() {
132
- let p = write_tmp("basic.md", "---\nname: x\nrole: R\n---\n\nbody line\n");
133
- let (meta, body) = read_front_matter(&p).unwrap();
134
- assert_eq!(
135
- meta,
136
- Value::Map(vec![
137
- ("name".to_string(), Value::Str("x".to_string())),
138
- ("role".to_string(), Value::Str("R".to_string())),
139
- ])
140
- );
141
- assert_eq!(body, "body line\n");
142
- }
143
-
144
- #[test]
145
- fn front_matter_empty_block_is_empty_map() {
146
- // "---\n\n---\nbody\n": closing marker at the blank line → raw "" → {} ; body "body\n".
147
- let p = write_tmp("empty.md", "---\n\n---\nbody\n");
148
- let (meta, body) = read_front_matter(&p).unwrap();
149
- assert_eq!(meta, Value::Map(vec![]));
150
- assert_eq!(body, "body\n");
151
- }
152
-
153
- #[test]
154
- fn front_matter_body_lstrip_strips_only_newlines() {
155
- // body.lstrip("\n") removes leading NEWLINES but keeps the 2-space indent.
156
- let p = write_tmp("lstrip.md", "---\nname: x\n---\n\n\n indented body\n");
157
- let (meta, body) = read_front_matter(&p).unwrap();
158
- assert_eq!(
159
- meta,
160
- Value::Map(vec![("name".to_string(), Value::Str("x".to_string()))])
161
- );
162
- assert_eq!(body, " indented body\n");
163
- }
164
-
165
- #[test]
166
- fn front_matter_unterminated_falls_back_to_plain_text() {
167
- let text = "---\nname: x\n";
168
- let p = write_tmp("unterminated.md", text);
169
- let (meta, body) = read_front_matter(&p).expect("unterminated front matter is plain text");
170
- assert_eq!(meta, Value::Map(Vec::new()));
171
- assert_eq!(body, text);
172
- }
173
-
174
- #[test]
175
- fn front_matter_non_object_falls_back_to_plain_text() {
176
- let text = "---\n- a\n- b\n---\nbody\n";
177
- let p = write_tmp("list.md", text);
178
- let (meta, body) = read_front_matter(&p).expect("non-object front matter is plain text");
179
- assert_eq!(meta, Value::Map(Vec::new()));
180
- assert_eq!(body, text);
122
+ fn front_matter_cases() {
123
+ struct Case {
124
+ id: &'static str,
125
+ file: &'static str,
126
+ text: &'static str,
127
+ meta: &'static [(&'static str, &'static str)],
128
+ body: &'static str,
129
+ }
130
+ const CASES: &[Case] = &[
131
+ // No leading "---\n" → ({}, text) verbatim (compiler.py:175-176).
132
+ Case {
133
+ id: "front_matter_no_marker_returns_empty_meta_and_full_text", file: "no_marker.md",
134
+ text: "hello\nworld\n", meta: &[], body: "hello\nworld\n",
135
+ },
136
+ Case {
137
+ id: "front_matter_basic_splits_meta_and_lstrips_body", file: "basic.md",
138
+ text: "---\nname: x\nrole: R\n---\n\nbody line\n", meta: &[("name", "x"), ("role", "R")], body: "body line\n",
139
+ },
140
+ // Closing marker at the blank line → raw "" → {}; body "body\n".
141
+ Case {
142
+ id: "front_matter_empty_block_is_empty_map", file: "empty.md",
143
+ text: "---\n\n---\nbody\n", meta: &[], body: "body\n",
144
+ },
145
+ // body.lstrip("\n") removes leading NEWLINES but keeps the 2-space indent.
146
+ Case {
147
+ id: "front_matter_body_lstrip_strips_only_newlines", file: "lstrip.md",
148
+ text: "---\nname: x\n---\n\n\n indented body\n", meta: &[("name", "x")], body: " indented body\n",
149
+ },
150
+ Case {
151
+ id: "front_matter_unterminated_falls_back_to_plain_text", file: "unterminated.md",
152
+ text: "---\nname: x\n", meta: &[], body: "---\nname: x\n",
153
+ },
154
+ Case {
155
+ id: "front_matter_non_object_falls_back_to_plain_text", file: "list.md",
156
+ text: "---\n- a\n- b\n---\nbody\n", meta: &[], body: "---\n- a\n- b\n---\nbody\n",
157
+ },
158
+ ];
159
+ for case in CASES {
160
+ let p = write_tmp(case.file, case.text);
161
+ let (meta, body) = read_front_matter(&p).unwrap_or_else(|err| panic!("{}: {err}", case.id));
162
+ let expected_meta = Value::Map(case.meta.iter().map(|(key, value)| {
163
+ ((*key).to_string(), Value::Str((*value).to_string()))
164
+ }).collect());
165
+ assert_eq!(meta, expected_meta, "{}: metadata", case.id);
166
+ assert_eq!(body, case.body, "{}: body", case.id);
167
+ }
181
168
  }
182
169
 
183
170
  // ───────────────────────────── compile_team: full dict parity ─────────────────────────────
@@ -54,8 +54,7 @@ fn d1_claim_team_owner_includes_os_user() {
54
54
  // fingerprint/requested_provider/warning. Golden _receiver_from_claim_target (__init__.py:861-877).
55
55
  // Rust LeaderReceiver serializes all 17 (no skip_serializing_if) -> 3 always-null extras leak. RED.
56
56
  // (The POPULATED tmux values session_name/window_*/pane_* come from the caller-target scan — a
57
- // deferred real-tmux seam, see d2_receiver_populated_from_caller_target_seam; the KEY-SET + ORDER
58
- // locked here are unchanged by that scan.)
57
+ // deferred real-tmux seam; the KEY-SET + ORDER locked here are unchanged by that scan.)
59
58
  #[test]
60
59
  #[serial_test::serial(env)]
61
60
  fn d2_claim_leader_receiver_is_fifteen_golden_keys_in_order_no_extras() {
@@ -98,18 +97,6 @@ fn d2_claim_leader_receiver_is_fifteen_golden_keys_in_order_no_extras() {
98
97
  ], "golden _receiver_from_claim_target 15-key set + ORDER (__init__.py:861-877 + BUG-4 socket-qualified receiver)");
99
98
  }
100
99
 
101
- // D2 seam — the caller-target SCAN that fills session_name/window_index/window_name/pane_index/
102
- // pane_tty/pane_current_command from core_list_targets (golden _receiver_from_claim_target reads
103
- // target[...]). Rust make_receiver leaves them None (no scan). Real-tmux: needs core_list_targets.
104
- #[test]
105
- #[ignore = "real-tmux seam: leader_receiver session_name/window_*/pane_* are populated from the \
106
- caller target via core_list_targets (golden _receiver_from_claim_target); Rust has no \
107
- scan (values null). Porter wires the target scan; this asserts the populated values."]
108
- fn d2_receiver_populated_from_caller_target_seam() {
109
- // Golden contract: new_receiver.session_name == caller_target.session_name, etc. Needs a live
110
- // tmux target list (core_list_targets) — out of in-process scope.
111
- }
112
-
113
100
  // D3 [BLOCK] — bound_pane is RECEIVER-first (golden :624 receiver.pane_id OR owner.pane_id). PROBE-B
114
101
  // (receiver=%9, owner=%1, caller=%9): golden ok=True already_bound. Rust derives OWNER-first
115
102
  // (owner %1 != caller %9) -> owner live + !confirm -> refused force_confirm_required. RED.