@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +129 -54
- package/crates/team-agent/src/cli/diagnose.rs +1 -2
- package/crates/team-agent/src/cli/emit.rs +1 -7
- package/crates/team-agent/src/cli/helpers.rs +3 -1
- package/crates/team-agent/src/cli/leader.rs +2 -1
- package/crates/team-agent/src/cli/mod.rs +27 -7
- package/crates/team-agent/src/cli/named_address.rs +14 -5
- package/crates/team-agent/src/cli/profile.rs +19 -7
- package/crates/team-agent/src/cli/send.rs +9 -3
- package/crates/team-agent/src/cli/status.rs +8 -30
- package/crates/team-agent/src/cli/status_port.rs +1341 -1272
- package/crates/team-agent/src/cli/tests/base.rs +738 -660
- package/crates/team-agent/src/cli/tests/compile.rs +45 -18
- package/crates/team-agent/src/cli/tests/divergence.rs +462 -444
- package/crates/team-agent/src/cli/tests/lane_c.rs +365 -282
- package/crates/team-agent/src/cli/tests/leader_watch.rs +356 -329
- package/crates/team-agent/src/cli/tests/main_preserved.rs +672 -564
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +284 -224
- package/crates/team-agent/src/cli/tests/mod.rs +17 -7
- package/crates/team-agent/src/cli/tests/named_address.rs +8 -2
- package/crates/team-agent/src/cli/tests/peer_allow.rs +10 -2
- package/crates/team-agent/src/cli/tests/run_delegation.rs +314 -273
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +26 -15
- package/crates/team-agent/src/cli/tests/status_send.rs +707 -629
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +20 -4
- package/crates/team-agent/src/cli/tests/verb_profile.rs +46 -17
- package/crates/team-agent/src/cli/tests/verb_validate.rs +15 -3
- package/crates/team-agent/src/codex_app_server.rs +2 -5
- package/crates/team-agent/src/compiler/tests.rs +139 -33
- package/crates/team-agent/src/compiler.rs +55 -22
- package/crates/team-agent/src/conpty/backend.rs +23 -33
- package/crates/team-agent/src/coordinator/backoff.rs +2 -7
- package/crates/team-agent/src/coordinator/conpty_shim.rs +55 -67
- package/crates/team-agent/src/coordinator/health.rs +46 -31
- package/crates/team-agent/src/coordinator/mod.rs +3 -3
- package/crates/team-agent/src/coordinator/orphan.rs +22 -10
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +47 -52
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +55 -19
- package/crates/team-agent/src/coordinator/tests/basics.rs +179 -41
- package/crates/team-agent/src/coordinator/tests/daemon.rs +53 -13
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +78 -19
- package/crates/team-agent/src/coordinator/tests/main_preserved.rs +61 -11
- package/crates/team-agent/src/coordinator/tests/mod.rs +33 -39
- package/crates/team-agent/src/coordinator/tests/spine.rs +52 -12
- package/crates/team-agent/src/coordinator/tests/takeover.rs +73 -15
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +50 -15
- package/crates/team-agent/src/coordinator/tests/watch.rs +74 -20
- package/crates/team-agent/src/coordinator/tick.rs +19 -1
- package/crates/team-agent/src/db/message_store.rs +138 -30
- package/crates/team-agent/src/db/migration.rs +249 -61
- package/crates/team-agent/src/db/schema.rs +303 -82
- package/crates/team-agent/src/diagnose/comms.rs +9 -2
- package/crates/team-agent/src/diagnose/mod.rs +1 -3
- package/crates/team-agent/src/diagnose/orphans.rs +79 -61
- package/crates/team-agent/src/event_log.rs +70 -16
- package/crates/team-agent/src/layout/manager.rs +15 -4
- package/crates/team-agent/src/layout/mod.rs +4 -4
- package/crates/team-agent/src/layout/overlay.rs +10 -3
- package/crates/team-agent/src/layout/placement.rs +5 -1
- package/crates/team-agent/src/layout/recovery.rs +4 -2
- package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
- package/crates/team-agent/src/layout/sessions.rs +17 -9
- package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
- package/crates/team-agent/src/layout/worker_env.rs +52 -19
- package/crates/team-agent/src/leader/helpers.rs +7 -1
- package/crates/team-agent/src/leader/lease.rs +195 -82
- package/crates/team-agent/src/leader/owner_bind.rs +55 -22
- package/crates/team-agent/src/leader/rediscover/tests.rs +88 -24
- package/crates/team-agent/src/leader/rediscover.rs +74 -25
- package/crates/team-agent/src/leader/start.rs +75 -54
- package/crates/team-agent/src/leader/takeover.rs +46 -11
- package/crates/team-agent/src/leader/tests/basics.rs +320 -167
- package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
- package/crates/team-agent/src/leader/tests/identity.rs +428 -356
- package/crates/team-agent/src/leader/tests/idle.rs +285 -254
- package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
- package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
- package/crates/team-agent/src/leader/tests/mod.rs +115 -99
- package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
- package/crates/team-agent/src/lib.rs +4 -4
- package/crates/team-agent/src/lifecycle/display.rs +7 -3
- package/crates/team-agent/src/lifecycle/launch.rs +6 -1
- package/crates/team-agent/src/lifecycle/mod.rs +9 -1
- package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
- package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
- package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
- package/crates/team-agent/src/lifecycle/tests.rs +2 -2
- package/crates/team-agent/src/main.rs +4 -4
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
- package/crates/team-agent/src/mcp_server/mod.rs +11 -2
- package/crates/team-agent/src/mcp_server/types.rs +14 -3
- package/crates/team-agent/src/mcp_server/wire.rs +230 -68
- package/crates/team-agent/src/messaging/delivery.rs +20 -18
- package/crates/team-agent/src/messaging/helpers.rs +25 -4
- package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
- package/crates/team-agent/src/messaging/mod.rs +2 -3
- package/crates/team-agent/src/messaging/selftest.rs +46 -26
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
- package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
- package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
- package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
- package/crates/team-agent/src/messaging/trust.rs +25 -2
- package/crates/team-agent/src/messaging/watchers.rs +37 -9
- package/crates/team-agent/src/model/enums.rs +65 -16
- package/crates/team-agent/src/model/ids.rs +12 -3
- package/crates/team-agent/src/model/paths.rs +28 -7
- package/crates/team-agent/src/model/permissions.rs +176 -33
- package/crates/team-agent/src/model/routing.rs +66 -20
- package/crates/team-agent/src/model/spec.rs +365 -69
- package/crates/team-agent/src/model/task_graph.rs +36 -9
- package/crates/team-agent/src/model/yaml/tests.rs +24 -6
- package/crates/team-agent/src/model/yaml.rs +7 -6
- package/crates/team-agent/src/packaging/install.rs +23 -9
- package/crates/team-agent/src/packaging/migrate.rs +5 -7
- package/crates/team-agent/src/packaging/mod.rs +9 -1
- package/crates/team-agent/src/packaging/repair.rs +13 -6
- package/crates/team-agent/src/packaging/tests.rs +63 -16
- package/crates/team-agent/src/packaging/types.rs +22 -7
- package/crates/team-agent/src/platform/argv.rs +4 -1
- package/crates/team-agent/src/platform/file_lock.rs +22 -8
- package/crates/team-agent/src/platform/process.rs +21 -22
- package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
- package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
- package/crates/team-agent/src/provider/classify.rs +127 -42
- package/crates/team-agent/src/provider/faults.rs +17 -5
- package/crates/team-agent/src/provider/helpers.rs +6 -5
- package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
- package/crates/team-agent/src/state/repository.rs +27 -14
- package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
- package/crates/team-agent/src/tmux_backend.rs +80 -44
- package/crates/team-agent/src/topology.rs +27 -20
- package/crates/team-agent/src/transport/test_support.rs +20 -23
- package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
- package/crates/team-agent/src/transport/tests/mod.rs +178 -187
- package/crates/team-agent/src/transport/tests/wire.rs +561 -525
- package/crates/team-agent/src/transport.rs +12 -17
- package/crates/team-agent/src/transport_factory.rs +29 -14
- package/package.json +4 -4
|
@@ -1,508 +1,526 @@
|
|
|
1
1
|
use super::*;
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
// =========================================================================
|
|
4
|
+
// STEP-14 DIVERGENCE RED LANE — golden-pinned tests that FAIL against the
|
|
5
|
+
// current Rust port. Golden = team-agent-public @ v0.2.11 (439bef8),
|
|
6
|
+
// probed via PYTHONPATH=.../src python3 /tmp/probe_cli_all.py.
|
|
7
|
+
// Each test encodes the EXACT golden value; the porters green these next.
|
|
8
|
+
// =========================================================================
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
10
|
+
// ---- #1 / #15: classify_agent_bucket — raw "idle" with NO health => Unknown ----
|
|
11
|
+
// golden _agent_summary_counts has NO `raw == "idle"` arm (commands.py:320):
|
|
12
|
+
// idle is gated SOLELY on health.status=="idle". A raw "idle" with empty health
|
|
13
|
+
// falls through every branch to the final `else: unknown += 1`.
|
|
14
|
+
// golden probe: _agent_summary_counts({"a":{"status":"idle"}},{}) -> unknown=1.
|
|
15
|
+
// Rust cli.rs:601 adds `|| raw == "idle"` => Idle (WRONG).
|
|
16
|
+
#[test]
|
|
17
|
+
fn red_classify_raw_idle_no_health_is_unknown_not_idle() {
|
|
18
|
+
// golden: classify("idle","") lands in Unknown (the §11 bug-071/077/085 rule).
|
|
19
|
+
assert_eq!(
|
|
20
|
+
classify_agent_bucket("idle", ""),
|
|
21
|
+
SummaryBucket::Unknown,
|
|
22
|
+
"raw 'idle' with empty health MUST be Unknown (golden gates idle on health only)"
|
|
23
|
+
);
|
|
24
|
+
// and the uppercase variant (str.lower() in golden) also Unknown.
|
|
25
|
+
assert_eq!(classify_agent_bucket("IDLE", ""), SummaryBucket::Unknown);
|
|
26
|
+
// full-path golden: agent_summary_counts({"a":{"status":"idle"}},{}) -> unknown=1, idle=0.
|
|
27
|
+
let got = agent_summary_counts(&json!({"a": {"status": "idle"}}), &json!({}));
|
|
28
|
+
assert_eq!(
|
|
29
|
+
got,
|
|
30
|
+
SummaryCounts {
|
|
31
|
+
unknown: 1,
|
|
32
|
+
..Default::default()
|
|
33
|
+
},
|
|
34
|
+
"golden: raw idle agent with no health => unknown=1 (not idle=1)"
|
|
35
|
+
);
|
|
36
|
+
// health=="idle" is still Idle (the only legitimate idle trigger).
|
|
37
|
+
assert_eq!(classify_agent_bucket("", "idle"), SummaryBucket::Idle);
|
|
38
|
+
}
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
40
|
+
// ---- #2 / #21 / #24: format_latest_result faithfulness ----
|
|
41
|
+
// golden _latest_result_line (commands.py:333-337):
|
|
42
|
+
// summary = str(summary or "").replace("\n"," ")[:80]; printed as `{summary or '-'}`;
|
|
43
|
+
// agent_id printed as `{agent_id or '-'}`;
|
|
44
|
+
// created_at rendered through runtime._age_text ('-' for None/'' /invalid ISO; 'Nh ago' for valid).
|
|
45
|
+
// Rust format_latest_result passes summary verbatim, uses unwrap_or("-") (empty stays empty),
|
|
46
|
+
// and prints created_at raw (no age_text).
|
|
47
|
+
#[test]
|
|
48
|
+
fn red_format_latest_result_empty_summary_and_agent_map_to_dash() {
|
|
49
|
+
// golden: summary='' + created_at invalid -> 'latest result: a1 -> - @ -'
|
|
50
|
+
let line = format_status_summary(&json!({
|
|
51
|
+
"latest_results": [{"agent_id": "a1", "summary": "", "created_at": "bad-date"}]
|
|
52
|
+
}));
|
|
53
|
+
let latest = line.lines().nth(4).unwrap();
|
|
54
|
+
assert_eq!(
|
|
55
|
+
latest, "latest result: a1 -> - @ -",
|
|
56
|
+
"empty summary -> '-', invalid created_at -> '-' (age_text); golden commands.py:337"
|
|
57
|
+
);
|
|
58
|
+
// golden: empty agent_id -> '-'
|
|
59
|
+
let line2 = format_status_summary(&json!({
|
|
60
|
+
"latest_results": [{"agent_id": "", "summary": "hi", "created_at": Value::Null}]
|
|
61
|
+
}));
|
|
62
|
+
assert_eq!(
|
|
63
|
+
line2.lines().nth(4).unwrap(),
|
|
64
|
+
"latest result: - -> hi @ -",
|
|
65
|
+
"empty agent_id -> '-' (golden `agent_id or '-'`)"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
69
|
+
#[test]
|
|
70
|
+
fn red_format_latest_result_newline_flattened_and_truncated_80() {
|
|
71
|
+
// golden: summary 'line1\nline2' -> '\n'->' ' -> 'line1 line2'
|
|
72
|
+
let line = format_status_summary(&json!({
|
|
73
|
+
"latest_results": [{"agent_id": "a1", "summary": "line1\nline2", "created_at": Value::Null}]
|
|
74
|
+
}));
|
|
75
|
+
assert_eq!(
|
|
76
|
+
line.lines().nth(4).unwrap(),
|
|
77
|
+
"latest result: a1 -> line1 line2 @ -",
|
|
78
|
+
"newline in summary MUST flatten to a space (golden .replace('\\n',' '))"
|
|
79
|
+
);
|
|
80
|
+
// golden: 100-char summary truncated to exactly 80 chars.
|
|
81
|
+
let line2 = format_status_summary(&json!({
|
|
82
|
+
"latest_results": [{"agent_id": "a1", "summary": "Z".repeat(100), "created_at": Value::Null}]
|
|
83
|
+
}));
|
|
84
|
+
let latest = line2.lines().nth(4).unwrap();
|
|
85
|
+
let kept = latest
|
|
86
|
+
.strip_prefix("latest result: a1 -> ")
|
|
87
|
+
.unwrap()
|
|
88
|
+
.strip_suffix(" @ -")
|
|
89
|
+
.unwrap();
|
|
90
|
+
assert_eq!(
|
|
91
|
+
kept.chars().count(),
|
|
92
|
+
80,
|
|
93
|
+
"summary MUST cap at 80 chars (golden [:80])"
|
|
94
|
+
);
|
|
95
|
+
assert_eq!(kept, "Z".repeat(80));
|
|
96
|
+
}
|
|
90
97
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
98
|
+
#[test]
|
|
99
|
+
fn red_format_latest_result_created_at_is_age_text_not_raw_iso() {
|
|
100
|
+
// golden: a valid ISO created_at renders as relative age ('Nh ago'), NEVER the raw string.
|
|
101
|
+
// (We avoid asserting the exact age — time-dependent — and assert it is NOT verbatim.)
|
|
102
|
+
let line = format_status_summary(&json!({
|
|
103
|
+
"latest_results": [{"agent_id": "a1", "summary": "done", "created_at": "2020-01-01T00:00:00Z"}]
|
|
104
|
+
}));
|
|
105
|
+
let latest = line.lines().nth(4).unwrap();
|
|
106
|
+
let tail = latest.rsplit(" @ ").next().unwrap();
|
|
107
|
+
assert_ne!(
|
|
108
|
+
tail, "2020-01-01T00:00:00Z",
|
|
109
|
+
"created_at MUST be rendered as age_text (e.g. 'Nh ago'), not the raw ISO string"
|
|
110
|
+
);
|
|
111
|
+
assert!(
|
|
112
|
+
tail.ends_with(" ago"),
|
|
113
|
+
"valid ISO created_at renders as a relative age ('... ago'); got: {tail:?}"
|
|
114
|
+
);
|
|
115
|
+
}
|
|
109
116
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
117
|
+
// ---- #3: format_status_summary — falsy first latest_results element -> "none" ----
|
|
118
|
+
// golden (commands.py:268,333-335): latest = (latest_results or [{}])[0] if latest_results else None;
|
|
119
|
+
// _latest_result_line returns 'latest result: none' when the first element is falsy (None or {}).
|
|
120
|
+
// Rust .first() returns Some for [Null]/[{}] -> renders '- -> - @ -'.
|
|
121
|
+
#[test]
|
|
122
|
+
fn red_format_status_summary_falsy_first_latest_is_none() {
|
|
123
|
+
// golden: latest_results=[None] -> 'latest result: none'
|
|
124
|
+
let line_null = format_status_summary(&json!({"latest_results": [Value::Null]}));
|
|
125
|
+
assert_eq!(
|
|
126
|
+
line_null.lines().nth(4).unwrap(),
|
|
127
|
+
"latest result: none",
|
|
128
|
+
"a Null first element renders 'latest result: none' (golden falsy guard)"
|
|
129
|
+
);
|
|
130
|
+
// golden: latest_results=[{}] -> 'latest result: none'
|
|
131
|
+
let line_empty = format_status_summary(&json!({"latest_results": [{}]}));
|
|
132
|
+
assert_eq!(
|
|
133
|
+
line_empty.lines().nth(4).unwrap(),
|
|
134
|
+
"latest result: none",
|
|
135
|
+
"an empty-object first element renders 'latest result: none' (golden falsy guard)"
|
|
136
|
+
);
|
|
137
|
+
}
|
|
131
138
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
// ---- #4: format_status_summary — empty-string falsy fallbacks + current_command ----
|
|
140
|
+
// golden: '' is falsy via Python `or`:
|
|
141
|
+
// coordinator status '' -> 'stopped' (commands.py:284)
|
|
142
|
+
// pane_id '' -> '-' (line 285)
|
|
143
|
+
// cmd = pane_current_command or current_command or '-' (line 285) — note the current_command fallback.
|
|
144
|
+
// Rust serde unwrap_or keeps '' verbatim and has NO current_command read.
|
|
145
|
+
#[test]
|
|
146
|
+
fn red_format_status_summary_empty_string_coordinator_status_is_stopped() {
|
|
147
|
+
// golden: coordinator.status='' -> 'coordinator: stopped schema_ok=False tmux=False'
|
|
148
|
+
let line = format_status_summary(&json!({"coordinator": {"status": ""}}));
|
|
149
|
+
assert_eq!(
|
|
143
150
|
line.lines().next().unwrap(),
|
|
144
151
|
"coordinator: stopped schema_ok=false tmux=false",
|
|
145
152
|
"empty-string coordinator status MUST fall back to 'stopped' (golden `status or 'stopped'`)"
|
|
146
153
|
);
|
|
147
|
-
|
|
154
|
+
}
|
|
148
155
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
156
|
+
#[test]
|
|
157
|
+
fn red_format_status_summary_empty_pane_id_is_dash() {
|
|
158
|
+
// golden: pane_id='' -> 'receiver: - cmd=x topology=external'
|
|
159
|
+
let line = format_status_summary(&json!({
|
|
160
|
+
"leader_receiver": {"pane_id": "", "pane_current_command": "x"}
|
|
161
|
+
}));
|
|
162
|
+
assert_eq!(
|
|
163
|
+
line.lines().nth(1).unwrap(),
|
|
164
|
+
"receiver: - cmd=x topology=external",
|
|
165
|
+
"empty-string pane_id MUST fall back to '-' (golden `pane_id or '-'`)"
|
|
166
|
+
);
|
|
167
|
+
}
|
|
161
168
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
#[test]
|
|
170
|
+
fn red_format_status_summary_cmd_falls_back_to_current_command() {
|
|
171
|
+
// golden: missing pane_current_command + current_command='claude' -> 'receiver: %3 cmd=claude topology=external'
|
|
172
|
+
let line_missing = format_status_summary(&json!({
|
|
173
|
+
"leader_receiver": {"pane_id": "%3", "current_command": "claude"}
|
|
174
|
+
}));
|
|
175
|
+
assert_eq!(
|
|
169
176
|
line_missing.lines().nth(1).unwrap(),
|
|
170
177
|
"receiver: %3 cmd=claude topology=external",
|
|
171
178
|
"cmd MUST fall back to current_command when pane_current_command is absent (golden line 285)"
|
|
172
179
|
);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
// golden: empty pane_current_command + current_command='claude' -> 'receiver: %3 cmd=claude topology=external'
|
|
181
|
+
let line_empty = format_status_summary(&json!({
|
|
182
|
+
"leader_receiver": {"pane_id": "%3", "pane_current_command": "", "current_command": "claude"}
|
|
183
|
+
}));
|
|
184
|
+
assert_eq!(
|
|
185
|
+
line_empty.lines().nth(1).unwrap(),
|
|
186
|
+
"receiver: %3 cmd=claude topology=external",
|
|
187
|
+
"empty pane_current_command MUST fall through to current_command (golden falsy `or`)"
|
|
188
|
+
);
|
|
189
|
+
}
|
|
183
190
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
191
|
+
// ---- #5 (P2): format_status_summary — Python bool() truthiness coercion ----
|
|
192
|
+
// golden: schema_ok / tmux via bool(...) — int 1 -> True, string 'yes' -> True (commands.py:284).
|
|
193
|
+
// Rust as_bool() returns None for int/str -> false.
|
|
194
|
+
#[test]
|
|
195
|
+
fn red_format_status_summary_bool_coercion_truthy_nonbool() {
|
|
196
|
+
// golden: schema_ok=1 (int) -> schema_ok=True (printed lowercase 'true' per the upstream re-spell).
|
|
197
|
+
let line_int = format_status_summary(&json!({
|
|
198
|
+
"coordinator": {"status": "running", "schema_ok": 1}
|
|
199
|
+
}));
|
|
200
|
+
assert_eq!(
|
|
201
|
+
line_int.lines().next().unwrap(),
|
|
202
|
+
"coordinator: running schema_ok=true tmux=false",
|
|
203
|
+
"int 1 MUST coerce to truthy schema_ok (golden bool(1)==True)"
|
|
204
|
+
);
|
|
205
|
+
// golden: tmux_session_present='yes' (non-empty string) -> tmux=True.
|
|
206
|
+
let line_str = format_status_summary(&json!({"tmux_session_present": "yes"}));
|
|
207
|
+
assert_eq!(
|
|
208
|
+
line_str.lines().next().unwrap(),
|
|
209
|
+
"coordinator: stopped schema_ok=false tmux=true",
|
|
210
|
+
"non-empty string 'yes' MUST coerce to truthy tmux (golden bool('yes')==True)"
|
|
211
|
+
);
|
|
212
|
+
}
|
|
206
213
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
// ---- #6 / #18 / #26: parse_inbox_entries block grouping + title strip + [:80] cap ----
|
|
215
|
+
// golden _leader_inbox_entries groups blocks on `[` + 'fallback'; _leader_inbox_entry_title
|
|
216
|
+
// strips bracket/'Team Agent'/'Message id:'/'Task id:'/'From:'/'To:'/'Requires ack:'/'Artifacts:'
|
|
217
|
+
// lines, joins remaining content with single spaces, [:80] cap.
|
|
218
|
+
#[test]
|
|
219
|
+
fn red_inbox_realistic_two_message_grouping_and_metadata_strip() {
|
|
220
|
+
// golden full summary on a realistic 2-message fallback inbox (metadata + bodies):
|
|
221
|
+
let ws = tmp_workspace();
|
|
222
|
+
let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
|
|
223
|
+
let raw = "[m1 fallback]\nTeam Agent\nMessage id: m1\nFrom: worker-1\nTo: leader\n\
|
|
217
224
|
Requires ack: yes\nPlease review the PR and approve the deploy. It is blocking the release.\n\
|
|
218
225
|
[m2 fallback]\nTeam Agent\nFrom: worker-2\nBuild failed on CI, see logs.";
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
226
|
+
std::fs::write(&inbox, raw).unwrap();
|
|
227
|
+
let summary = consume_leader_inbox_summary(&ws, 500).expect("Some");
|
|
228
|
+
let expected = "Leader inbox: 2 new fallback entries\n\
|
|
222
229
|
- Please review the PR and approve the deploy. It is blocking the release.\n\
|
|
223
230
|
- Build failed on CI, see logs.\n\
|
|
224
231
|
Hint: team-agent inbox leader";
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
232
|
+
assert_eq!(
|
|
233
|
+
summary, expected,
|
|
234
|
+
"golden groups into 2 entries, strips Team Agent/Message id/From/To/Requires ack metadata"
|
|
235
|
+
);
|
|
236
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
237
|
+
}
|
|
231
238
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
239
|
+
#[test]
|
|
240
|
+
fn red_inbox_non_fallback_bracket_stays_in_entry() {
|
|
241
|
+
// golden: a `[...]` WITHOUT 'fallback' is content, not a header:
|
|
242
|
+
// '[only bracket no kw]\nbody line' -> ONE entry titled '[only bracket no kw] body line'.
|
|
243
|
+
let ws = tmp_workspace();
|
|
244
|
+
let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
|
|
245
|
+
std::fs::write(&inbox, "[only bracket no kw]\nbody line").unwrap();
|
|
246
|
+
let summary = consume_leader_inbox_summary(&ws, 500).expect("Some");
|
|
247
|
+
assert_eq!(
|
|
241
248
|
summary,
|
|
242
249
|
"Leader inbox: 1 new fallback entry\n- [only bracket no kw] body line\nHint: team-agent inbox leader",
|
|
243
250
|
"a non-'fallback' bracket line stays part of the entry body (golden grouping gate)"
|
|
244
251
|
);
|
|
245
|
-
|
|
246
|
-
|
|
252
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
253
|
+
}
|
|
247
254
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
255
|
+
#[test]
|
|
256
|
+
fn red_inbox_plain_lines_join_into_single_entry() {
|
|
257
|
+
// golden: no fallback header at all -> the whole text is ONE entry, lines joined w/ spaces.
|
|
258
|
+
// 'alpha\nbeta\ngamma' -> 1 entry 'alpha beta gamma'.
|
|
259
|
+
let ws = tmp_workspace();
|
|
260
|
+
let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
|
|
261
|
+
std::fs::write(&inbox, "alpha\nbeta\ngamma").unwrap();
|
|
262
|
+
let summary = consume_leader_inbox_summary(&ws, 500).expect("Some");
|
|
263
|
+
assert_eq!(
|
|
264
|
+
summary,
|
|
265
|
+
"Leader inbox: 1 new fallback entry\n- alpha beta gamma\nHint: team-agent inbox leader",
|
|
266
|
+
"with no fallback header golden collapses ALL lines into one space-joined entry"
|
|
267
|
+
);
|
|
268
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
269
|
+
}
|
|
263
270
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
271
|
+
#[test]
|
|
272
|
+
fn red_inbox_title_capped_at_80_chars() {
|
|
273
|
+
// golden: a 200-char body title is capped to exactly 80 chars ([:80] per entry).
|
|
274
|
+
let ws = tmp_workspace();
|
|
275
|
+
let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
|
|
276
|
+
let body = "X".repeat(200);
|
|
277
|
+
std::fs::write(&inbox, format!("[x fallback]\n{body}")).unwrap();
|
|
278
|
+
let summary = consume_leader_inbox_summary(&ws, 500).expect("Some");
|
|
279
|
+
assert_eq!(
|
|
280
|
+
summary,
|
|
281
|
+
format!(
|
|
282
|
+
"Leader inbox: 1 new fallback entry\n- {}\nHint: team-agent inbox leader",
|
|
283
|
+
"X".repeat(80)
|
|
284
|
+
),
|
|
285
|
+
"each entry title MUST cap at 80 chars (golden [:80]); Rust applies no per-entry cap"
|
|
286
|
+
);
|
|
287
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
288
|
+
}
|
|
282
289
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
290
|
+
// ---- #7 / #16: consume_leader_inbox_summary — mid-codepoint cursor must NOT panic ----
|
|
291
|
+
// golden seeks to a BYTE offset and decodes errors='replace': a cursor inside a multibyte
|
|
292
|
+
// char yields U+FFFD replacement chars and NEVER crashes (bug-084).
|
|
293
|
+
// Rust slices &text[offset..] which PANICS on a non-char-boundary. The panic IS the red.
|
|
294
|
+
#[test]
|
|
295
|
+
fn red_inbox_mid_codepoint_cursor_decodes_replacement_no_panic() {
|
|
296
|
+
// golden probe (/tmp/probe_cli_all.py): inbox '[a fallback]\n世界 message', cursor='14'
|
|
297
|
+
// (byte 14 is inside '世', whose bytes are 13..16) ->
|
|
298
|
+
// 'Leader inbox: 1 new fallback entry\n- ��界 message\nHint: team-agent inbox leader'
|
|
299
|
+
let ws = tmp_workspace();
|
|
300
|
+
let runtime = ws.join(".team").join("runtime");
|
|
301
|
+
std::fs::write(
|
|
302
|
+
runtime.join("leader-inbox.log"),
|
|
303
|
+
"[a fallback]\n世界 message",
|
|
304
|
+
)
|
|
305
|
+
.unwrap();
|
|
306
|
+
std::fs::write(runtime.join("leader-inbox.cursor"), "14").unwrap();
|
|
307
|
+
let summary = consume_leader_inbox_summary(&ws, 500)
|
|
308
|
+
.expect("mid-codepoint cursor MUST degrade gracefully, not crash");
|
|
309
|
+
assert_eq!(
|
|
299
310
|
summary,
|
|
300
311
|
"Leader inbox: 1 new fallback entry\n- \u{FFFD}\u{FFFD}界 message\nHint: team-agent inbox leader",
|
|
301
312
|
"mid-codepoint byte offset MUST yield U+FFFD replacement chars (golden errors='replace')"
|
|
302
313
|
);
|
|
303
|
-
|
|
304
|
-
|
|
314
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
315
|
+
}
|
|
305
316
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
317
|
+
// ---- #8 / #17: consume_leader_inbox_summary — offset>size resets to 0; garbage cursor ----
|
|
318
|
+
// golden helpers.py:38-40: offset<0 or offset>size -> offset=0 (re-read whole file);
|
|
319
|
+
// a ValueError cursor ('abc') -> offset=0 AND size=0 -> offset==size -> None WITHOUT advancing.
|
|
320
|
+
#[test]
|
|
321
|
+
fn red_inbox_beyond_size_cursor_resets_to_zero_and_resummarizes() {
|
|
322
|
+
// golden: file 'hello' inbox, cursor='99999' (> size) -> re-read from 0 -> summary; cursor advances.
|
|
323
|
+
let ws = tmp_workspace();
|
|
324
|
+
let runtime = ws.join(".team").join("runtime");
|
|
325
|
+
std::fs::write(runtime.join("leader-inbox.log"), "[a fallback]\nhello").unwrap();
|
|
326
|
+
std::fs::write(runtime.join("leader-inbox.cursor"), "99999").unwrap();
|
|
327
|
+
let summary = consume_leader_inbox_summary(&ws, 500)
|
|
328
|
+
.expect("over-size cursor MUST reset to 0 and re-summarize (golden offset>size => 0)");
|
|
329
|
+
assert_eq!(
|
|
330
|
+
summary, "Leader inbox: 1 new fallback entry\n- hello\nHint: team-agent inbox leader",
|
|
331
|
+
"cursor beyond file size MUST re-read the whole inbox (NOT clamp-to-len-then-None)"
|
|
332
|
+
);
|
|
333
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
334
|
+
}
|
|
325
335
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
+
#[test]
|
|
337
|
+
fn red_inbox_garbage_cursor_returns_none_without_advancing() {
|
|
338
|
+
// golden: cursor='abc' (ValueError) -> offset=0,size=0 -> offset==size -> None; cursor LEFT 'abc'.
|
|
339
|
+
let ws = tmp_workspace();
|
|
340
|
+
let runtime = ws.join(".team").join("runtime");
|
|
341
|
+
std::fs::write(runtime.join("leader-inbox.log"), "[a fallback]\nhello").unwrap();
|
|
342
|
+
let cursor_path = runtime.join("leader-inbox.cursor");
|
|
343
|
+
std::fs::write(&cursor_path, "abc").unwrap();
|
|
344
|
+
let result = consume_leader_inbox_summary(&ws, 500);
|
|
345
|
+
assert_eq!(
|
|
336
346
|
result, None,
|
|
337
347
|
"an unparseable cursor MUST treat size as 0 (offset==size==0) and return None (golden ValueError)"
|
|
338
348
|
);
|
|
339
|
-
|
|
340
|
-
|
|
349
|
+
let cursor_after = std::fs::read_to_string(&cursor_path).unwrap();
|
|
350
|
+
assert_eq!(
|
|
341
351
|
cursor_after, "abc",
|
|
342
352
|
"a garbage cursor MUST be left untouched (golden never advances it); Rust overwrites to len"
|
|
343
353
|
);
|
|
344
|
-
|
|
345
|
-
|
|
354
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
355
|
+
}
|
|
346
356
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
357
|
+
// ---- #9: render_inbox_summary — budget measured in CHARS (code points), not bytes ----
|
|
358
|
+
// golden uses Python str length (code points). Rust compares byte lengths.
|
|
359
|
+
#[test]
|
|
360
|
+
fn red_inbox_budget_is_char_count_not_bytes() {
|
|
361
|
+
// golden probe: 30 CJK chars, budget=100 -> char-len 97 <= 100 so golden KEEPS the full title.
|
|
362
|
+
let ws = tmp_workspace();
|
|
363
|
+
let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
|
|
364
|
+
std::fs::write(&inbox, format!("[x fallback]\n{}", "漢".repeat(30))).unwrap();
|
|
365
|
+
let summary = consume_leader_inbox_summary(&ws, 100).expect("Some");
|
|
366
|
+
assert_eq!(
|
|
367
|
+
summary,
|
|
368
|
+
format!(
|
|
369
|
+
"Leader inbox: 1 new fallback entry\n- {}\nHint: team-agent inbox leader",
|
|
370
|
+
"漢".repeat(30)
|
|
371
|
+
),
|
|
372
|
+
"budget MUST count code points (97 chars <= 100), not bytes; Rust byte-len drops it"
|
|
373
|
+
);
|
|
374
|
+
assert!(
|
|
375
|
+
!summary.contains("Truncated"),
|
|
376
|
+
"the CJK title fits the char budget and MUST NOT be truncated (golden char-len semantics)"
|
|
377
|
+
);
|
|
378
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
379
|
+
}
|
|
370
380
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
381
|
+
// ---- #10: render_inbox_summary — hard-trim when header+footer already exceed budget ----
|
|
382
|
+
// golden post-assembly: if len(summary) > budget, body='\n'.join(lines)[:keep].rstrip()
|
|
383
|
+
// with keep=max(0,budget-len(footer)-6), then `{body} ...\n{footer}` — even the HEADER is trimmed.
|
|
384
|
+
#[test]
|
|
385
|
+
fn red_inbox_small_budget_hard_trims_header() {
|
|
386
|
+
// golden probe: budget=80 on 10 entries -> 'Lea ...\nTruncated: ...'
|
|
387
|
+
let ws = tmp_workspace();
|
|
388
|
+
let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
|
|
389
|
+
let many = (0..10)
|
|
390
|
+
.map(|i| format!("[e{i} fallback]\nMessage number {i} with some text padding here"))
|
|
391
|
+
.collect::<Vec<_>>()
|
|
392
|
+
.join("\n");
|
|
393
|
+
std::fs::write(&inbox, &many).unwrap();
|
|
394
|
+
let summary = consume_leader_inbox_summary(&ws, 80).expect("Some");
|
|
395
|
+
assert_eq!(
|
|
396
|
+
summary, "Lea ...\nTruncated: more fallback entries available; run team-agent inbox leader",
|
|
397
|
+
"when header+footer alone exceed budget the body (incl header) MUST hard-trim (golden)"
|
|
398
|
+
);
|
|
399
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
400
|
+
}
|
|
392
401
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
402
|
+
// ---- #19 / #25: emit human-dict scalar formatting (None/True/False) + nested string quotes ----
|
|
403
|
+
// golden emit (helpers.py:16-21): scalar via Python str() -> None->'None', True->'True', False->'False';
|
|
404
|
+
// dict/list via json.dumps(ensure_ascii=False) -> string ELEMENTS keep their double quotes.
|
|
405
|
+
#[test]
|
|
406
|
+
fn red_emit_human_dict_scalar_none_true_false_and_quoted_strings() {
|
|
407
|
+
// golden probe: emit({"k":{"a":1,"b":[1,2]},"s":"hi","u":"世界","n":None,"f":False,"t":True}, False)
|
|
408
|
+
let out = emit(
|
|
409
|
+
&CmdOutput::Json(json!({
|
|
410
|
+
"k": {"a": 1, "b": [1, 2]},
|
|
411
|
+
"s": "hi",
|
|
412
|
+
"u": "世界",
|
|
413
|
+
"n": Value::Null,
|
|
414
|
+
"f": false,
|
|
415
|
+
"t": true,
|
|
416
|
+
})),
|
|
417
|
+
false,
|
|
418
|
+
)
|
|
419
|
+
.expect("dict human emit returns Some");
|
|
420
|
+
assert_eq!(
|
|
421
|
+
out, "k: {\"a\": 1, \"b\": [1, 2]}\ns: hi\nu: 世界\nn: None\nf: False\nt: True",
|
|
422
|
+
"top-level scalar Null/false/true MUST render Python-str 'None'/'False'/'True' (golden)"
|
|
423
|
+
);
|
|
424
|
+
}
|
|
417
425
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
426
|
+
#[test]
|
|
427
|
+
fn red_emit_human_dict_string_elements_keep_quotes_in_collections() {
|
|
428
|
+
// golden probe: emit({"items":["a","b"],"mixed":["x",1,True]}, False)
|
|
429
|
+
// -> 'items: ["a", "b"]\nmixed: ["x", 1, true]' (string elements KEEP double quotes;
|
|
430
|
+
// bool lowercased to json 'true' INSIDE the json.dumps collection).
|
|
431
|
+
let out = emit(
|
|
432
|
+
&CmdOutput::Json(json!({"items": ["a", "b"], "mixed": ["x", 1, true]})),
|
|
433
|
+
false,
|
|
434
|
+
)
|
|
435
|
+
.expect("dict human emit returns Some");
|
|
436
|
+
assert_eq!(
|
|
437
|
+
out, "items: [\"a\", \"b\"]\nmixed: [\"x\", 1, true]",
|
|
438
|
+
"string elements nested in a list MUST keep their double quotes (golden json.dumps)"
|
|
439
|
+
);
|
|
440
|
+
}
|
|
434
441
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
442
|
+
// ---- #20: send_target None routes to assignee, NEVER broadcast ----
|
|
443
|
+
// golden _send_target returns None for a no-target send; send_message(target=None) routes to
|
|
444
|
+
// the task assignee / leader receiver — '*' is the ONLY broadcast trigger.
|
|
445
|
+
// Rust maps None => MessageTarget::Broadcast (WRONG recipient set).
|
|
446
|
+
#[test]
|
|
447
|
+
fn red_send_target_none_is_not_broadcast() {
|
|
448
|
+
// golden: _send_target(targets=None, target=None) => None (single/assignee routing, NOT broadcast).
|
|
449
|
+
let got = send_target(None, None);
|
|
450
|
+
assert_ne!(
|
|
444
451
|
got,
|
|
445
452
|
MessageTarget::Broadcast,
|
|
446
453
|
"a no-target send MUST NOT broadcast to the whole team; golden routes to the assignee/leader. \
|
|
447
454
|
'*' is the only broadcast trigger."
|
|
448
455
|
);
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
456
|
+
// '*' remains the broadcast trigger (unchanged invariant).
|
|
457
|
+
assert_eq!(send_target(None, Some("*")), MessageTarget::Broadcast);
|
|
458
|
+
}
|
|
452
459
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
460
|
+
// ---- #23: cmd_doctor comms (human) returns COMMS_BOUNDARY_TEXT + sorted indented JSON ----
|
|
461
|
+
// golden: for --comms WITHOUT --json, cmd_doctor returns the STRING
|
|
462
|
+
// f"{COMMS_BOUNDARY_TEXT}\n{json.dumps(result, indent=2, ensure_ascii=False, sort_keys=True)}".
|
|
463
|
+
// Rust always does CmdResult::from_json -> CmdOutput::Json (wrong shape).
|
|
464
|
+
#[test]
|
|
465
|
+
fn red_cmd_doctor_comms_human_is_boundary_text_plus_sorted_json() {
|
|
466
|
+
const COMMS_BOUNDARY_TEXT: &str = "validates live pane binding consistency and zero-token comms contracts. Does NOT perform live runtime message round-trip. (zero token, zero pollution)";
|
|
467
|
+
let args = DoctorArgs {
|
|
468
|
+
spec: None,
|
|
469
|
+
workspace: PathBuf::from("."),
|
|
470
|
+
gate: None,
|
|
471
|
+
comms: true,
|
|
472
|
+
team: None,
|
|
473
|
+
fix: false,
|
|
474
|
+
fix_schema: false,
|
|
475
|
+
cleanup_orphans: false,
|
|
476
|
+
confirm: false,
|
|
477
|
+
json: false,
|
|
478
|
+
};
|
|
479
|
+
let result = cmd_doctor(&args).expect("comms doctor returns CmdResult");
|
|
480
|
+
let text = match result.output {
|
|
481
|
+
CmdOutput::Human(s) => s,
|
|
482
|
+
other => panic!(
|
|
483
|
+
"comms WITHOUT --json MUST be a Human boundary-text + JSON string, got {other:?}"
|
|
484
|
+
),
|
|
485
|
+
};
|
|
486
|
+
assert!(
|
|
480
487
|
text.starts_with(&format!("{COMMS_BOUNDARY_TEXT}\n")),
|
|
481
488
|
"comms human output MUST start with COMMS_BOUNDARY_TEXT then a newline (golden commands.py:231); got: {text:?}"
|
|
482
489
|
);
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
490
|
+
// the tail is the selftest result rendered as sort_keys+indent=2 JSON (parseable, sorted).
|
|
491
|
+
let json_tail = text
|
|
492
|
+
.strip_prefix(&format!("{COMMS_BOUNDARY_TEXT}\n"))
|
|
493
|
+
.unwrap();
|
|
494
|
+
let parsed: Value = serde_json::from_str(json_tail)
|
|
495
|
+
.expect("comms human tail MUST be indent=2 sort_keys JSON of the selftest result");
|
|
496
|
+
assert!(parsed.is_object(), "comms selftest JSON tail is an object");
|
|
497
|
+
}
|
|
489
498
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
499
|
+
// ---- #13 / #27 (P2): run() must NOT treat 'claude_code' as a passthrough trigger ----
|
|
500
|
+
// golden parser.py:86: only raw_argv[0] in {'codex','claude'} triggers leader passthrough.
|
|
501
|
+
// 'claude_code' is the internal provider name, NOT a CLI subcommand (argparse would reject it).
|
|
502
|
+
// Rust run() (cli.rs:1305) adds a 'claude_code' arm.
|
|
503
|
+
#[test]
|
|
504
|
+
fn red_run_claude_code_is_not_a_passthrough_trigger() {
|
|
505
|
+
// golden: `team-agent claude_code -h` is an invalid choice (NOT a clean passthrough exit).
|
|
506
|
+
// Rust currently routes it to cmd_leader_passthrough("claude_code",["-h"]) -> CmdResult::none()
|
|
507
|
+
// -> ExitCode::Ok. Golden would NOT treat it as a valid leader passthrough.
|
|
508
|
+
let exit = run(
|
|
509
|
+
&["claude_code".to_string(), "-h".to_string()],
|
|
510
|
+
Path::new("."),
|
|
511
|
+
);
|
|
512
|
+
assert_ne!(
|
|
501
513
|
exit,
|
|
502
514
|
ExitCode::Ok,
|
|
503
515
|
"'claude_code' MUST NOT be a leader passthrough trigger (golden gate is {{codex,claude}} only)"
|
|
504
516
|
);
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
517
|
+
// codex/claude REMAIN valid passthrough triggers (the -h fast path returns Ok).
|
|
518
|
+
assert_eq!(
|
|
519
|
+
run(&["codex".to_string(), "-h".to_string()], Path::new(".")),
|
|
520
|
+
ExitCode::Ok
|
|
521
|
+
);
|
|
522
|
+
assert_eq!(
|
|
523
|
+
run(&["claude".to_string(), "-h".to_string()], Path::new(".")),
|
|
524
|
+
ExitCode::Ok
|
|
525
|
+
);
|
|
526
|
+
}
|