@team-agent/installer 0.5.42 → 0.5.44
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 +126 -16
- 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 +51 -56
- 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/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 +87 -19
- package/crates/team-agent/src/leader/helpers.rs +7 -1
- package/crates/team-agent/src/leader/lease.rs +199 -89
- package/crates/team-agent/src/leader/owner_bind.rs +55 -22
- package/crates/team-agent/src/leader/provider_attribution.rs +25 -6
- 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/registry.rs +1 -1
- 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 +55 -15
- 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/common.rs +6 -2
- 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/phase_golden.rs +3 -0
- 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 +54 -24
- 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/persist.rs +28 -0
- package/crates/team-agent/src/state/repository.rs +8 -3
- 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 +40 -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,670 +1,710 @@
|
|
|
1
1
|
use super::*;
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
#[test]
|
|
9
|
-
fn status_compact_flag_default_is_compact() {
|
|
10
|
-
// golden: cmd_status without --detail -> runtime.status(compact=not(False)) == compact=True.
|
|
11
|
-
assert!(status_compact_flag(false), "detail=false MUST map to compact=true (commands.py:99)");
|
|
12
|
-
}
|
|
3
|
+
// =========================================================================
|
|
4
|
+
// status_compact_flag (commands.py:99): compact = !detail — CLI 独占不变量
|
|
5
|
+
// gate: 'detail=false => compact=true mapping, the one byte-level invariant CLI owns'.
|
|
6
|
+
// =========================================================================
|
|
13
7
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
#[test]
|
|
9
|
+
fn status_compact_flag_default_is_compact() {
|
|
10
|
+
// golden: cmd_status without --detail -> runtime.status(compact=not(False)) == compact=True.
|
|
11
|
+
assert!(
|
|
12
|
+
status_compact_flag(false),
|
|
13
|
+
"detail=false MUST map to compact=true (commands.py:99)"
|
|
14
|
+
);
|
|
15
|
+
}
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
17
|
+
#[test]
|
|
18
|
+
fn status_compact_flag_detail_is_full() {
|
|
19
|
+
// golden: cmd_status --detail -> runtime.status(compact=not(True)) == compact=False.
|
|
20
|
+
assert!(
|
|
21
|
+
!status_compact_flag(true),
|
|
22
|
+
"detail=true MUST map to compact=false (full projection)"
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// =========================================================================
|
|
27
|
+
// status_port::status — REAL caller against SEEDED fixture (gate: 'zero callers').
|
|
28
|
+
// Asserts the --json projection shape that the compact-vs-detail wiring selects.
|
|
29
|
+
// RED: status_port::status is unimplemented!() so the call panics until ported.
|
|
30
|
+
// =========================================================================
|
|
31
|
+
|
|
32
|
+
// =========================================================================
|
|
33
|
+
// RM-039-STAT-001 regression guard (real-machine evidence 2026-06-22).
|
|
34
|
+
//
|
|
35
|
+
// Architect verdict (bugs-stat001-sess001-architecture-analysis.md §root-cause):
|
|
36
|
+
// the coordinator-tick activity classifier writes
|
|
37
|
+
// `activity {status, confidence, rationale}` to the top-level
|
|
38
|
+
// `agents.<id>` slot of state.json (T1 invariant 60/61); the compact
|
|
39
|
+
// `status --json` projection MUST preserve it, last_output_at, and
|
|
40
|
+
// the enrich_agents-injected `interacted` marker. Lifecycle `status`
|
|
41
|
+
// and turn `activity.status` are separate fields per T1; the
|
|
42
|
+
// projection MUST NOT collapse them.
|
|
43
|
+
// =========================================================================
|
|
44
|
+
#[test]
|
|
45
|
+
fn rm039_stat001_compact_status_preserves_activity_and_last_output() {
|
|
46
|
+
let ws = seed_status_workspace();
|
|
47
|
+
let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
48
|
+
if let Some(agents) = state
|
|
49
|
+
.pointer_mut("/agents")
|
|
50
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
51
|
+
{
|
|
52
|
+
if let Some(agent) = agents
|
|
53
|
+
.get_mut("a1")
|
|
44
54
|
.and_then(serde_json::Value::as_object_mut)
|
|
45
55
|
{
|
|
46
|
-
|
|
47
|
-
.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
json!("2026-06-22T02:52:30+00:00"),
|
|
61
|
-
);
|
|
62
|
-
// first_send_at is already set by seed_status_workspace so
|
|
63
|
-
// enrich_agents will inject `interacted` with the same ISO value.
|
|
64
|
-
}
|
|
56
|
+
agent.insert(
|
|
57
|
+
"activity".to_string(),
|
|
58
|
+
json!({
|
|
59
|
+
"status": "working",
|
|
60
|
+
"confidence": 0.95,
|
|
61
|
+
"rationale": "provider_jsonl:open_turn",
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
agent.insert(
|
|
65
|
+
"last_output_at".to_string(),
|
|
66
|
+
json!("2026-06-22T02:52:30+00:00"),
|
|
67
|
+
);
|
|
68
|
+
// first_send_at is already set by seed_status_workspace so
|
|
69
|
+
// enrich_agents will inject `interacted` with the same ISO value.
|
|
65
70
|
}
|
|
66
|
-
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
67
|
-
|
|
68
|
-
let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false)
|
|
69
|
-
.expect("compact status should project a value");
|
|
70
|
-
let agent = v
|
|
71
|
-
.pointer("/agents/a1")
|
|
72
|
-
.and_then(serde_json::Value::as_object)
|
|
73
|
-
.expect("seeded agent a1 must appear in compact projection");
|
|
74
|
-
|
|
75
|
-
// T1 split: lifecycle `status` stays unchanged.
|
|
76
|
-
assert_eq!(
|
|
77
|
-
agent.get("status").and_then(serde_json::Value::as_str),
|
|
78
|
-
Some("running"),
|
|
79
|
-
"RM-039-STAT-001: compact projection must NOT collapse `status` into `activity.status`"
|
|
80
|
-
);
|
|
81
|
-
// Turn activity preserved (the field that was historically dropped).
|
|
82
|
-
let activity = agent
|
|
83
|
-
.get("activity")
|
|
84
|
-
.expect("RM-039-STAT-001: compact projection must preserve `activity`");
|
|
85
|
-
assert_eq!(
|
|
86
|
-
activity.pointer("/status").and_then(serde_json::Value::as_str),
|
|
87
|
-
Some("working"),
|
|
88
|
-
"compact activity.status must survive the projection"
|
|
89
|
-
);
|
|
90
|
-
assert_eq!(
|
|
91
|
-
activity.pointer("/rationale").and_then(serde_json::Value::as_str),
|
|
92
|
-
Some("provider_jsonl:open_turn"),
|
|
93
|
-
"compact activity.rationale must survive the projection"
|
|
94
|
-
);
|
|
95
|
-
// last_output_at is the timestamp the classifier advances when
|
|
96
|
-
// scrollback digest changes; operators read it alongside activity.
|
|
97
|
-
assert_eq!(
|
|
98
|
-
agent.get("last_output_at").and_then(serde_json::Value::as_str),
|
|
99
|
-
Some("2026-06-22T02:52:30+00:00"),
|
|
100
|
-
"compact projection must preserve `last_output_at` for the \"is something moving\" view"
|
|
101
|
-
);
|
|
102
|
-
// 0.4.x compact slim: `interacted` moves to --detail; the 4-field
|
|
103
|
-
// compact agent row keeps only status/provider/activity/last_output_at.
|
|
104
|
-
assert!(
|
|
105
|
-
agent.get("interacted").is_none(),
|
|
106
|
-
"0.4.x: compact projection drops `interacted` (moved to --detail)"
|
|
107
|
-
);
|
|
108
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
109
71
|
}
|
|
72
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
73
|
+
|
|
74
|
+
let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false)
|
|
75
|
+
.expect("compact status should project a value");
|
|
76
|
+
let agent = v
|
|
77
|
+
.pointer("/agents/a1")
|
|
78
|
+
.and_then(serde_json::Value::as_object)
|
|
79
|
+
.expect("seeded agent a1 must appear in compact projection");
|
|
80
|
+
|
|
81
|
+
// T1 split: lifecycle `status` stays unchanged.
|
|
82
|
+
assert_eq!(
|
|
83
|
+
agent.get("status").and_then(serde_json::Value::as_str),
|
|
84
|
+
Some("running"),
|
|
85
|
+
"RM-039-STAT-001: compact projection must NOT collapse `status` into `activity.status`"
|
|
86
|
+
);
|
|
87
|
+
// Turn activity preserved (the field that was historically dropped).
|
|
88
|
+
let activity = agent
|
|
89
|
+
.get("activity")
|
|
90
|
+
.expect("RM-039-STAT-001: compact projection must preserve `activity`");
|
|
91
|
+
assert_eq!(
|
|
92
|
+
activity
|
|
93
|
+
.pointer("/status")
|
|
94
|
+
.and_then(serde_json::Value::as_str),
|
|
95
|
+
Some("working"),
|
|
96
|
+
"compact activity.status must survive the projection"
|
|
97
|
+
);
|
|
98
|
+
assert_eq!(
|
|
99
|
+
activity
|
|
100
|
+
.pointer("/rationale")
|
|
101
|
+
.and_then(serde_json::Value::as_str),
|
|
102
|
+
Some("provider_jsonl:open_turn"),
|
|
103
|
+
"compact activity.rationale must survive the projection"
|
|
104
|
+
);
|
|
105
|
+
// last_output_at is the timestamp the classifier advances when
|
|
106
|
+
// scrollback digest changes; operators read it alongside activity.
|
|
107
|
+
assert_eq!(
|
|
108
|
+
agent
|
|
109
|
+
.get("last_output_at")
|
|
110
|
+
.and_then(serde_json::Value::as_str),
|
|
111
|
+
Some("2026-06-22T02:52:30+00:00"),
|
|
112
|
+
"compact projection must preserve `last_output_at` for the \"is something moving\" view"
|
|
113
|
+
);
|
|
114
|
+
// 0.4.x compact slim: `interacted` moves to --detail; the 4-field
|
|
115
|
+
// compact agent row keeps only status/provider/activity/last_output_at.
|
|
116
|
+
assert!(
|
|
117
|
+
agent.get("interacted").is_none(),
|
|
118
|
+
"0.4.x: compact projection drops `interacted` (moved to --detail)"
|
|
119
|
+
);
|
|
120
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
121
|
+
}
|
|
110
122
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
123
|
+
// =========================================================================
|
|
124
|
+
// RM-039-STAT-001 second-round regression (architect verdict 2026-06-22).
|
|
125
|
+
//
|
|
126
|
+
// Real failure shape: tick wrote `activity` to root .agents.coder and
|
|
127
|
+
// .teams.current.agents.coder because `team_state_key` cascaded to the
|
|
128
|
+
// `team_dir = "./.team/current"` basename. But status's selector reads
|
|
129
|
+
// the `active_team_key = "rm039-status-working-891"` projection, which
|
|
130
|
+
// was stale. The compact whitelist alone cannot fix this — by the time
|
|
131
|
+
// compact_agent_state runs, the selected team slot already lacks activity.
|
|
132
|
+
//
|
|
133
|
+
// Fix expected at the projection/state-key layer: when load_runtime_state
|
|
134
|
+
// sees `active_team_key` naming an existing teams entry and no root
|
|
135
|
+
// `team_key`, it must promote `team_key = active_team_key` so subsequent
|
|
136
|
+
// tick writes and status reads agree on which teams entry to use. The
|
|
137
|
+
// assertion here drives the real CLI path through `status_port::status`.
|
|
138
|
+
// =========================================================================
|
|
139
|
+
#[test]
|
|
140
|
+
fn rm039_stat001_status_resolves_active_team_when_root_team_key_missing() {
|
|
141
|
+
let ws = seed_status_workspace();
|
|
142
|
+
// Seed the exact dirty shape from the evidence: active_team_key
|
|
143
|
+
// disagrees with team_dir basename; root team_key absent; the
|
|
144
|
+
// teams.<active> entry is stale (no activity); root + teams.current
|
|
145
|
+
// carry activity that the tick had already written there.
|
|
146
|
+
let active = "rm039-status-working-891";
|
|
147
|
+
let activity = json!({
|
|
148
|
+
"status": "working",
|
|
149
|
+
"confidence": 0.95,
|
|
150
|
+
"rationale": "provider_jsonl:open_turn",
|
|
151
|
+
});
|
|
152
|
+
let state = json!({
|
|
153
|
+
"session_name": "team-rm039-status-working",
|
|
154
|
+
"team_dir": "./.team/current",
|
|
155
|
+
"active_team_key": active,
|
|
156
|
+
// intentionally no top-level "team_key" — that is the bug shape.
|
|
157
|
+
"leader": {"id": "leader"},
|
|
158
|
+
"leader_receiver": {"pane_id": "%3", "status": "running"},
|
|
159
|
+
"agents": {
|
|
160
|
+
"coder": {
|
|
161
|
+
"status": "running",
|
|
162
|
+
"first_send_at": "2026-01-01T00:00:00Z",
|
|
163
|
+
"activity": activity.clone(),
|
|
164
|
+
"last_output_at": "2026-06-22T02:52:30+00:00",
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
"teams": {
|
|
168
|
+
"current": {
|
|
169
|
+
"active_team_key": active,
|
|
170
|
+
"session_name": "team-rm039-status-working",
|
|
171
|
+
"agents": {
|
|
172
|
+
"coder": {
|
|
173
|
+
"status": "running",
|
|
174
|
+
"first_send_at": "2026-01-01T00:00:00Z",
|
|
175
|
+
"activity": activity.clone(),
|
|
176
|
+
}
|
|
153
177
|
}
|
|
154
178
|
},
|
|
155
|
-
|
|
156
|
-
"
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
"
|
|
160
|
-
"
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
active: {
|
|
168
|
-
"active_team_key": active,
|
|
169
|
-
"session_name": "team-rm039-status-working",
|
|
170
|
-
"agents": {
|
|
171
|
-
"coder": {
|
|
172
|
-
"status": "running",
|
|
173
|
-
"first_send_at": "2026-01-01T00:00:00Z",
|
|
174
|
-
// NO `activity` here — this is the stale entry
|
|
175
|
-
// that the selector landed on pre-fix.
|
|
176
|
-
}
|
|
179
|
+
active: {
|
|
180
|
+
"active_team_key": active,
|
|
181
|
+
"session_name": "team-rm039-status-working",
|
|
182
|
+
"agents": {
|
|
183
|
+
"coder": {
|
|
184
|
+
"status": "running",
|
|
185
|
+
"first_send_at": "2026-01-01T00:00:00Z",
|
|
186
|
+
// NO `activity` here — this is the stale entry
|
|
187
|
+
// that the selector landed on pre-fix.
|
|
177
188
|
}
|
|
178
189
|
}
|
|
179
190
|
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
even when active_team_key disagrees with team_dir basename"
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
"
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
std::fs::write(
|
|
194
|
+
ws.join(".team").join("runtime").join("state.json"),
|
|
195
|
+
serde_json::to_vec_pretty(&state).unwrap(),
|
|
196
|
+
)
|
|
197
|
+
.unwrap();
|
|
198
|
+
|
|
199
|
+
let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false)
|
|
200
|
+
.expect("compact status should project a value");
|
|
201
|
+
let agent = v
|
|
202
|
+
.pointer("/agents/coder")
|
|
203
|
+
.and_then(serde_json::Value::as_object)
|
|
204
|
+
.expect("coder agent must appear in compact projection");
|
|
205
|
+
let got_activity = agent.get("activity").expect(
|
|
206
|
+
"RM-039-STAT-001 second-round: activity must reach the compact projection \
|
|
207
|
+
even when active_team_key disagrees with team_dir basename",
|
|
208
|
+
);
|
|
209
|
+
assert_eq!(
|
|
210
|
+
got_activity
|
|
211
|
+
.pointer("/status")
|
|
212
|
+
.and_then(serde_json::Value::as_str),
|
|
213
|
+
Some("working"),
|
|
214
|
+
"RM-039-STAT-001 second-round: compact status.agents.coder.activity.status must \
|
|
201
215
|
be `working` when state.active_team_key names an existing teams entry, \
|
|
202
216
|
regardless of team_dir basename"
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
217
|
+
);
|
|
218
|
+
// Lifecycle status unchanged — T1 split invariant.
|
|
219
|
+
assert_eq!(
|
|
220
|
+
agent.get("status").and_then(serde_json::Value::as_str),
|
|
221
|
+
Some("running"),
|
|
222
|
+
"lifecycle status must NOT collapse into activity.status"
|
|
223
|
+
);
|
|
224
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
225
|
+
}
|
|
212
226
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
assert!(
|
|
259
|
-
!obj.contains_key(forbidden),
|
|
260
|
-
"0.4.x: compact must NOT contain diagnostic key `{forbidden}` (--detail only)"
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
// seeded agent surfaces through the projection.
|
|
227
|
+
#[test]
|
|
228
|
+
fn status_port_status_compact_json_shape_against_seeded_fixture() {
|
|
229
|
+
// cmd_status json branch (detail=false) delegates status_port::status(compact=true).
|
|
230
|
+
// 0.4.x compact slim: exactly 7 top-level fields; diagnostics moved
|
|
231
|
+
// to --detail. Plan: .team/artifacts/status-compact-plan.md.
|
|
232
|
+
let ws = seed_status_workspace();
|
|
233
|
+
let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false)
|
|
234
|
+
.expect("seeded fixture status should project a value");
|
|
235
|
+
let obj = v.as_object().expect("--json status is a dict");
|
|
236
|
+
// Exactly these 7 keys, no more.
|
|
237
|
+
let expected: std::collections::BTreeSet<&str> = [
|
|
238
|
+
"ok",
|
|
239
|
+
"team",
|
|
240
|
+
"session_name",
|
|
241
|
+
"leader_attach_command",
|
|
242
|
+
"ready",
|
|
243
|
+
"not_ready",
|
|
244
|
+
"agents",
|
|
245
|
+
]
|
|
246
|
+
.iter()
|
|
247
|
+
.copied()
|
|
248
|
+
.collect();
|
|
249
|
+
let actual: std::collections::BTreeSet<&str> = obj.keys().map(String::as_str).collect();
|
|
250
|
+
assert_eq!(
|
|
251
|
+
actual, expected,
|
|
252
|
+
"0.4.x compact must expose exactly 7 keys; got {actual:?}"
|
|
253
|
+
);
|
|
254
|
+
// Diagnostic keys must NOT leak into the default compact payload.
|
|
255
|
+
for forbidden in [
|
|
256
|
+
"leader_topology",
|
|
257
|
+
"is_external_leader",
|
|
258
|
+
"leader_client",
|
|
259
|
+
"tmux_session_present",
|
|
260
|
+
"leader_receiver",
|
|
261
|
+
"agent_health",
|
|
262
|
+
"tasks",
|
|
263
|
+
"messages",
|
|
264
|
+
"queued_messages",
|
|
265
|
+
"results",
|
|
266
|
+
"latest_results",
|
|
267
|
+
"coordinator",
|
|
268
|
+
"readiness",
|
|
269
|
+
"reminder",
|
|
270
|
+
"last_events",
|
|
271
|
+
] {
|
|
264
272
|
assert!(
|
|
265
|
-
obj
|
|
266
|
-
"
|
|
273
|
+
!obj.contains_key(forbidden),
|
|
274
|
+
"0.4.x: compact must NOT contain diagnostic key `{forbidden}` (--detail only)"
|
|
267
275
|
);
|
|
268
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
269
276
|
}
|
|
277
|
+
// seeded agent surfaces through the projection.
|
|
278
|
+
assert!(
|
|
279
|
+
obj["agents"].as_object().unwrap().contains_key("a1"),
|
|
280
|
+
"seeded agent a1 must appear in compact agents projection"
|
|
281
|
+
);
|
|
282
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
283
|
+
}
|
|
270
284
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
285
|
+
#[test]
|
|
286
|
+
fn cmd_status_human_appends_harness_reminder() {
|
|
287
|
+
let ws = seed_status_workspace();
|
|
288
|
+
let args = StatusArgs {
|
|
289
|
+
agent: None,
|
|
290
|
+
workspace: ws.clone(),
|
|
291
|
+
detail: false,
|
|
292
|
+
summary: false,
|
|
293
|
+
json: false,
|
|
294
|
+
team: None,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
let r = cmd_status_for_team(&args, None).expect("status");
|
|
298
|
+
let text = match r.output {
|
|
299
|
+
CmdOutput::Human(text) => text,
|
|
300
|
+
other => panic!("expected human status output, got {other:?}"),
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
assert!(text.ends_with(crate::cli::STATUS_REMINDER), "{text}");
|
|
304
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#[test]
|
|
308
|
+
fn status_port_status_reports_managed_leader_topology_and_attach_command() {
|
|
309
|
+
let ws = seed_status_workspace();
|
|
310
|
+
let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
311
|
+
if let Some(obj) = state.as_object_mut() {
|
|
312
|
+
obj.insert("is_external_leader".to_string(), json!(false));
|
|
313
|
+
obj.insert("session_name".to_string(), json!("team-current"));
|
|
291
314
|
}
|
|
315
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
316
|
+
|
|
317
|
+
// 0.4.x: leader_topology / is_external_leader moved to --detail.
|
|
318
|
+
// leader_attach_command stays in the slim compact payload.
|
|
319
|
+
let slim = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false).expect("status");
|
|
320
|
+
let attach = slim["leader_attach_command"]
|
|
321
|
+
.as_str()
|
|
322
|
+
.expect("compact still includes leader_attach_command");
|
|
323
|
+
assert!(attach.contains("attach -t team-current:leader"), "{attach}");
|
|
324
|
+
|
|
325
|
+
let detail =
|
|
326
|
+
status_port::status(&ws, /*compact=*/ false, /*detail=*/ true).expect("status detail");
|
|
327
|
+
assert_eq!(detail["leader_topology"], json!("managed"));
|
|
328
|
+
assert_eq!(detail["is_external_leader"], json!(false));
|
|
329
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
330
|
+
}
|
|
292
331
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
// leader_attach_command stays in the slim compact payload.
|
|
305
|
-
let slim = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false).expect("status");
|
|
306
|
-
let attach = slim["leader_attach_command"]
|
|
307
|
-
.as_str()
|
|
308
|
-
.expect("compact still includes leader_attach_command");
|
|
309
|
-
assert!(attach.contains("attach -t team-current:leader"), "{attach}");
|
|
310
|
-
|
|
311
|
-
let detail = status_port::status(&ws, /*compact=*/ false, /*detail=*/ true).expect("status detail");
|
|
312
|
-
assert_eq!(detail["leader_topology"], json!("managed"));
|
|
313
|
-
assert_eq!(detail["is_external_leader"], json!(false));
|
|
314
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
332
|
+
#[test]
|
|
333
|
+
fn status_port_managed_attach_command_uses_receiver_window_name() {
|
|
334
|
+
let ws = seed_status_workspace();
|
|
335
|
+
let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
336
|
+
if let Some(obj) = state.as_object_mut() {
|
|
337
|
+
obj.insert("is_external_leader".to_string(), json!(false));
|
|
338
|
+
obj.insert("session_name".to_string(), json!("team-current"));
|
|
339
|
+
obj.insert(
|
|
340
|
+
"leader_receiver".to_string(),
|
|
341
|
+
json!({"pane_id": "%3", "window_name": "claude_code", "status": "attached"}),
|
|
342
|
+
);
|
|
315
343
|
}
|
|
344
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
316
345
|
|
|
317
|
-
|
|
318
|
-
fn status_port_managed_attach_command_uses_receiver_window_name() {
|
|
319
|
-
let ws = seed_status_workspace();
|
|
320
|
-
let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
321
|
-
if let Some(obj) = state.as_object_mut() {
|
|
322
|
-
obj.insert("is_external_leader".to_string(), json!(false));
|
|
323
|
-
obj.insert("session_name".to_string(), json!("team-current"));
|
|
324
|
-
obj.insert(
|
|
325
|
-
"leader_receiver".to_string(),
|
|
326
|
-
json!({"pane_id": "%3", "window_name": "claude_code", "status": "attached"}),
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
346
|
+
let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false).expect("status");
|
|
330
347
|
|
|
331
|
-
|
|
348
|
+
let attach = v["leader_attach_command"]
|
|
349
|
+
.as_str()
|
|
350
|
+
.expect("managed status includes attach command");
|
|
351
|
+
assert!(
|
|
352
|
+
attach.contains("attach -t team-current:claude_code"),
|
|
353
|
+
"{attach}"
|
|
354
|
+
);
|
|
355
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
356
|
+
}
|
|
332
357
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
358
|
+
#[test]
|
|
359
|
+
fn leader_attach_command_for_plan_uses_plan_leader_window() {
|
|
360
|
+
let ws = tmp_workspace();
|
|
361
|
+
let plan = crate::leader::LeaderStartPlan {
|
|
362
|
+
mode: crate::leader::LeaderStartMode::ManagedTmuxClient,
|
|
363
|
+
provider: crate::provider::Provider::ClaudeCode,
|
|
364
|
+
workspace: ws.clone(),
|
|
365
|
+
socket: crate::leader::LeaderLaunchSocket::Workspace,
|
|
366
|
+
session_name: Some(crate::transport::SessionName::new(
|
|
367
|
+
"team-agent-leader-claude_code-demo".to_string(),
|
|
368
|
+
)),
|
|
369
|
+
argv: Vec::new(),
|
|
370
|
+
provider_argv: Vec::new(),
|
|
371
|
+
leader_window: Some(crate::transport::WindowName::new("claude_code")),
|
|
372
|
+
is_external_leader: false,
|
|
373
|
+
leader_env: std::collections::BTreeMap::new(),
|
|
374
|
+
identity: None,
|
|
375
|
+
detached: false,
|
|
376
|
+
};
|
|
339
377
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
let ws = tmp_workspace();
|
|
343
|
-
let plan = crate::leader::LeaderStartPlan {
|
|
344
|
-
mode: crate::leader::LeaderStartMode::ManagedTmuxClient,
|
|
345
|
-
provider: crate::provider::Provider::ClaudeCode,
|
|
346
|
-
workspace: ws.clone(),
|
|
347
|
-
socket: crate::leader::LeaderLaunchSocket::Workspace,
|
|
348
|
-
session_name: Some(crate::transport::SessionName::new(
|
|
349
|
-
"team-agent-leader-claude_code-demo".to_string(),
|
|
350
|
-
)),
|
|
351
|
-
argv: Vec::new(),
|
|
352
|
-
provider_argv: Vec::new(),
|
|
353
|
-
leader_window: Some(crate::transport::WindowName::new("claude_code")),
|
|
354
|
-
is_external_leader: false,
|
|
355
|
-
leader_env: std::collections::BTreeMap::new(),
|
|
356
|
-
identity: None,
|
|
357
|
-
detached: false,
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
let attach = lifecycle_port::leader_attach_command_for_plan(&ws, &plan)
|
|
361
|
-
.expect("managed plan has attach command");
|
|
378
|
+
let attach = lifecycle_port::leader_attach_command_for_plan(&ws, &plan)
|
|
379
|
+
.expect("managed plan has attach command");
|
|
362
380
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
381
|
+
assert!(
|
|
382
|
+
attach.contains("attach -t team-agent-leader-claude_code-demo:claude_code"),
|
|
383
|
+
"{attach}"
|
|
384
|
+
);
|
|
385
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
386
|
+
}
|
|
369
387
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
379
|
-
|
|
380
|
-
// 0.4.x: topology/external markers moved to --detail; compact keeps attach.
|
|
381
|
-
let slim = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false).expect("status");
|
|
382
|
-
let attach = slim["leader_attach_command"]
|
|
383
|
-
.as_str()
|
|
384
|
-
.expect("missing marker defaults to managed attach command");
|
|
385
|
-
assert!(attach.contains("attach -t team-current:leader"), "{attach}");
|
|
386
|
-
|
|
387
|
-
let detail = status_port::status(&ws, /*compact=*/ false, /*detail=*/ true).expect("status detail");
|
|
388
|
-
assert_eq!(detail["leader_topology"], json!("managed"));
|
|
389
|
-
assert_eq!(detail["is_external_leader"], json!(false));
|
|
390
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
388
|
+
#[test]
|
|
389
|
+
fn status_port_missing_topology_marker_defaults_to_managed() {
|
|
390
|
+
let ws = seed_status_workspace();
|
|
391
|
+
let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
392
|
+
if let Some(obj) = state.as_object_mut() {
|
|
393
|
+
obj.remove("is_external_leader");
|
|
394
|
+
obj.insert("session_name".to_string(), json!("team-current"));
|
|
391
395
|
}
|
|
396
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
397
|
+
|
|
398
|
+
// 0.4.x: topology/external markers moved to --detail; compact keeps attach.
|
|
399
|
+
let slim = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false).expect("status");
|
|
400
|
+
let attach = slim["leader_attach_command"]
|
|
401
|
+
.as_str()
|
|
402
|
+
.expect("missing marker defaults to managed attach command");
|
|
403
|
+
assert!(attach.contains("attach -t team-current:leader"), "{attach}");
|
|
404
|
+
|
|
405
|
+
let detail =
|
|
406
|
+
status_port::status(&ws, /*compact=*/ false, /*detail=*/ true).expect("status detail");
|
|
407
|
+
assert_eq!(detail["leader_topology"], json!("managed"));
|
|
408
|
+
assert_eq!(detail["is_external_leader"], json!(false));
|
|
409
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
410
|
+
}
|
|
392
411
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
412
|
+
#[test]
|
|
413
|
+
fn status_port_status_detail_full_keeps_uncompacted_events() {
|
|
414
|
+
// 0.4.x: --detail (compact=false) preserves ALL diagnostic fields the
|
|
415
|
+
// compact slim payload drops. Pin the must-keep set so future
|
|
416
|
+
// refactors can't accidentally strip detail diagnostics.
|
|
417
|
+
let ws = seed_status_workspace();
|
|
418
|
+
let full = status_port::status(&ws, /*compact=*/ false, /*detail=*/ true)
|
|
419
|
+
.expect("seeded fixture full status should project a value");
|
|
420
|
+
let compact = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false)
|
|
421
|
+
.expect("seeded fixture compact status should project a value");
|
|
422
|
+
assert_ne!(
|
|
423
|
+
full, compact,
|
|
424
|
+
"detail (full) and default (compact) projections must differ"
|
|
425
|
+
);
|
|
426
|
+
let full_obj = full.as_object().expect("full status is a dict");
|
|
427
|
+
for key in [
|
|
428
|
+
"coordinator",
|
|
429
|
+
"readiness",
|
|
430
|
+
"leader_receiver",
|
|
431
|
+
"agent_health",
|
|
432
|
+
"tasks",
|
|
433
|
+
"messages",
|
|
434
|
+
"queued_messages",
|
|
435
|
+
"results",
|
|
436
|
+
"latest_results",
|
|
437
|
+
"last_events",
|
|
438
|
+
] {
|
|
439
|
+
assert!(
|
|
440
|
+
full_obj.contains_key(key),
|
|
441
|
+
"0.4.x: --detail must preserve `{key}` (compact slimming escape hatch)"
|
|
442
|
+
);
|
|
424
443
|
}
|
|
444
|
+
assert_eq!(full_obj["agents"].as_object().unwrap().len(), 1);
|
|
445
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
446
|
+
}
|
|
425
447
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
448
|
+
// =========================================================================
|
|
449
|
+
// send_options_from_args (commands.py:170-177): SendArgs->SendOptions 旗标取反
|
|
450
|
+
// gate: 'no_ack:true => requires_ack:false and no_wait:true => wait_visible:false';
|
|
451
|
+
// watch_result flag maps into SendOptions.
|
|
452
|
+
// RED: send_options_from_args is unimplemented!() until ported.
|
|
453
|
+
// =========================================================================
|
|
454
|
+
|
|
455
|
+
fn send_args_fixture() -> SendArgs {
|
|
456
|
+
SendArgs {
|
|
457
|
+
target: Some("alice".into()),
|
|
458
|
+
message: vec!["hello".into(), "world".into(), "foo".into()],
|
|
459
|
+
targets: None,
|
|
460
|
+
workspace: PathBuf::from("."),
|
|
461
|
+
team: Some("teamA".into()),
|
|
462
|
+
task: Some("t-1".into()),
|
|
463
|
+
sender: "leader".into(),
|
|
464
|
+
no_ack: true,
|
|
465
|
+
no_wait: true,
|
|
466
|
+
watch_result: true,
|
|
467
|
+
timeout: 12.5,
|
|
468
|
+
confirm_human: false,
|
|
469
|
+
json: false,
|
|
470
|
+
message_id: None,
|
|
471
|
+
pane: None,
|
|
472
|
+
to_name: None,
|
|
451
473
|
to_leader: None,
|
|
452
|
-
}
|
|
453
474
|
}
|
|
475
|
+
}
|
|
454
476
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
477
|
+
fn queued_send_args_fixture(json: bool) -> SendArgs {
|
|
478
|
+
let ws = deleg_uniq_dir("send-human");
|
|
479
|
+
let _ = crate::message_store::MessageStore::open(&ws).unwrap();
|
|
480
|
+
crate::state::persist::save_runtime_state(
|
|
481
|
+
&ws,
|
|
482
|
+
&json!({
|
|
483
|
+
"active_team_key": "current",
|
|
484
|
+
"teams": {"current": {"agents": {"w1": {"provider": "codex"}}}}
|
|
485
|
+
}),
|
|
486
|
+
)
|
|
487
|
+
.unwrap();
|
|
488
|
+
SendArgs {
|
|
489
|
+
workspace: ws,
|
|
490
|
+
target: Some("w1".into()),
|
|
491
|
+
team: None,
|
|
492
|
+
task: None,
|
|
493
|
+
watch_result: false,
|
|
494
|
+
json,
|
|
495
|
+
..send_args_fixture()
|
|
475
496
|
}
|
|
497
|
+
}
|
|
476
498
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
499
|
+
#[test]
|
|
500
|
+
fn send_options_negates_no_ack_and_no_wait_and_carries_watch() {
|
|
501
|
+
// golden (commands.py:172,174,176): requires_ack=not no_ack; wait_visible=not no_wait;
|
|
502
|
+
// watch_result passthrough. With no_ack=true,no_wait=true,watch_result=true:
|
|
503
|
+
// requires_ack=false, wait_visible=false, watch_result=true.
|
|
504
|
+
let opts = send_options_from_args(&send_args_fixture());
|
|
505
|
+
assert!(
|
|
506
|
+
!opts.requires_ack,
|
|
507
|
+
"no_ack:true MUST map to requires_ack:false (off-by-inversion guard)"
|
|
508
|
+
);
|
|
509
|
+
assert!(
|
|
510
|
+
!opts.wait_visible,
|
|
511
|
+
"no_wait:true MUST map to wait_visible:false"
|
|
512
|
+
);
|
|
513
|
+
assert!(
|
|
514
|
+
opts.watch_result,
|
|
515
|
+
"watch_result flag MUST pass through into SendOptions"
|
|
516
|
+
);
|
|
517
|
+
assert!(!opts.confirm_human);
|
|
518
|
+
assert_eq!(opts.sender, "leader");
|
|
519
|
+
assert_eq!(opts.timeout, 12.5);
|
|
520
|
+
}
|
|
490
521
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
522
|
+
#[test]
|
|
523
|
+
fn send_options_default_flags_are_acked_and_waited() {
|
|
524
|
+
// golden: no_ack=false,no_wait=false,watch_result=false ->
|
|
525
|
+
// requires_ack=true, wait_visible=true, watch_result=false (Python defaults inverted back).
|
|
526
|
+
let args = SendArgs {
|
|
527
|
+
no_ack: false,
|
|
528
|
+
no_wait: false,
|
|
529
|
+
watch_result: false,
|
|
530
|
+
..send_args_fixture()
|
|
531
|
+
};
|
|
532
|
+
let opts = send_options_from_args(&args);
|
|
533
|
+
assert!(
|
|
534
|
+
opts.requires_ack,
|
|
535
|
+
"no_ack:false MUST map to requires_ack:true"
|
|
536
|
+
);
|
|
537
|
+
assert!(
|
|
538
|
+
opts.wait_visible,
|
|
539
|
+
"no_wait:false MUST map to wait_visible:true"
|
|
540
|
+
);
|
|
541
|
+
assert!(!opts.watch_result);
|
|
542
|
+
}
|
|
506
543
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
}
|
|
544
|
+
// =========================================================================
|
|
545
|
+
// cmd_send — REAL caller (gate: 'cmd_send has NO test beyond send_target').
|
|
546
|
+
// Asserts (1) message Vec joined by single space surfaces to send_message,
|
|
547
|
+
// (2) the registered-watcher notice ({status:'registered',...} -> result['watch'],
|
|
548
|
+
// send.py:326-337) survives into CmdResult Json output,
|
|
549
|
+
// (3) DeliveryOutcome->exit-code derivation (ok=true -> ExitCode::Ok).
|
|
550
|
+
// RED: cmd_send is unimplemented!() so it panics until ported.
|
|
551
|
+
// =========================================================================
|
|
552
|
+
|
|
553
|
+
#[test]
|
|
554
|
+
fn cmd_send_joins_message_with_single_space() {
|
|
555
|
+
// golden (commands.py:169): " ".join(["hello","world","foo"]) == "hello world foo".
|
|
556
|
+
// Drive cmd_send; the joined content must reach send_message (RED until ported).
|
|
557
|
+
let args = SendArgs {
|
|
558
|
+
json: true,
|
|
559
|
+
..send_args_fixture()
|
|
560
|
+
};
|
|
561
|
+
let r = cmd_send(&args).expect("cmd_send returns CmdResult");
|
|
562
|
+
// The delegate's DeliveryOutcome -> Json must carry an `ok` key feeding exit-code.
|
|
563
|
+
match r.output {
|
|
564
|
+
CmdOutput::Json(ref v) => {
|
|
565
|
+
assert!(v.get("ok").is_some(), "send result Json must carry `ok`");
|
|
566
|
+
if v.get("ok").and_then(|ok| ok.as_bool()) == Some(true) {
|
|
567
|
+
assert_eq!(
|
|
568
|
+
v.get("reminder").and_then(|reminder| reminder.as_str()),
|
|
569
|
+
Some(crate::cli::SEND_REMINDER)
|
|
570
|
+
);
|
|
535
571
|
}
|
|
536
|
-
other => panic!("cmd_send must emit Json DeliveryOutcome, got {other:?}"),
|
|
537
572
|
}
|
|
573
|
+
other => panic!("cmd_send must emit Json DeliveryOutcome, got {other:?}"),
|
|
538
574
|
}
|
|
575
|
+
}
|
|
539
576
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
577
|
+
#[test]
|
|
578
|
+
fn cmd_send_default_human_output_is_one_line_without_false_delivered() {
|
|
579
|
+
let r = cmd_send(&queued_send_args_fixture(false)).expect("cmd_send returns CmdResult");
|
|
580
|
+
assert!(!r.as_json);
|
|
581
|
+
let text = emit(&r.output, r.as_json).expect("send should render human text");
|
|
582
|
+
let lines: Vec<_> = text.lines().collect();
|
|
583
|
+
assert_eq!(
|
|
584
|
+
lines.len(),
|
|
585
|
+
1,
|
|
586
|
+
"default send output must be one line: {text}"
|
|
587
|
+
);
|
|
588
|
+
assert!(
|
|
589
|
+
lines[0].contains("ok:")
|
|
590
|
+
&& lines[0].contains("status:")
|
|
591
|
+
&& lines[0].contains("message_id:")
|
|
592
|
+
&& lines[0].contains("target:"),
|
|
593
|
+
"default send output must keep only the core fields; got {text}"
|
|
594
|
+
);
|
|
595
|
+
assert!(
|
|
596
|
+
!text.contains("delivered"),
|
|
597
|
+
"queued send output must not claim or mention delivered; got {text}"
|
|
598
|
+
);
|
|
599
|
+
for hidden in [
|
|
600
|
+
"agent_id:",
|
|
601
|
+
"sender:",
|
|
602
|
+
"message_status:",
|
|
603
|
+
"verification:",
|
|
604
|
+
"stage:",
|
|
605
|
+
"reason:",
|
|
606
|
+
"channel:",
|
|
607
|
+
"reminder:",
|
|
608
|
+
] {
|
|
554
609
|
assert!(
|
|
555
|
-
!text.contains(
|
|
556
|
-
"
|
|
610
|
+
!text.contains(hidden),
|
|
611
|
+
"default send output should hide {hidden} unless needed; got {text}"
|
|
557
612
|
);
|
|
558
|
-
for hidden in [
|
|
559
|
-
"agent_id:",
|
|
560
|
-
"sender:",
|
|
561
|
-
"message_status:",
|
|
562
|
-
"verification:",
|
|
563
|
-
"stage:",
|
|
564
|
-
"reason:",
|
|
565
|
-
"channel:",
|
|
566
|
-
"reminder:",
|
|
567
|
-
] {
|
|
568
|
-
assert!(
|
|
569
|
-
!text.contains(hidden),
|
|
570
|
-
"default send output should hide {hidden} unless needed; got {text}"
|
|
571
|
-
);
|
|
572
|
-
}
|
|
573
613
|
}
|
|
614
|
+
}
|
|
574
615
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
}
|
|
603
|
-
assert_eq!(v.get("verification"), Some(&serde_json::Value::Null));
|
|
604
|
-
assert_eq!(v.get("stage"), Some(&serde_json::Value::Null));
|
|
605
|
-
assert_eq!(v.get("reason"), Some(&serde_json::Value::Null));
|
|
606
|
-
assert_eq!(v.get("channel"), Some(&serde_json::Value::Null));
|
|
607
|
-
assert_eq!(v.get("delivered").and_then(|d| d.as_bool()), Some(false));
|
|
608
|
-
assert!(
|
|
609
|
-
!v.get("reminder")
|
|
610
|
-
.and_then(|reminder| reminder.as_str())
|
|
611
|
-
.unwrap_or_default()
|
|
612
|
-
.contains("Message delivered."),
|
|
613
|
-
"queued JSON reminder must not contradict delivered:false: {v}"
|
|
614
|
-
);
|
|
616
|
+
#[test]
|
|
617
|
+
fn cmd_send_json_shape_keeps_056_fields() {
|
|
618
|
+
let args = queued_send_args_fixture(true);
|
|
619
|
+
let r = cmd_send(&args).expect("cmd_send returns CmdResult");
|
|
620
|
+
let v = match r.output {
|
|
621
|
+
CmdOutput::Json(v) => v,
|
|
622
|
+
other => panic!("--json send must emit Json, got {other:?}"),
|
|
623
|
+
};
|
|
624
|
+
let obj = v.as_object().expect("--json send output must be object");
|
|
625
|
+
for key in [
|
|
626
|
+
"ok",
|
|
627
|
+
"status",
|
|
628
|
+
"delivery_status",
|
|
629
|
+
"delivered",
|
|
630
|
+
"target",
|
|
631
|
+
"agent_id",
|
|
632
|
+
"content_length_bytes",
|
|
633
|
+
"sender",
|
|
634
|
+
"message_id",
|
|
635
|
+
"message_status",
|
|
636
|
+
"verification",
|
|
637
|
+
"stage",
|
|
638
|
+
"reason",
|
|
639
|
+
"channel",
|
|
640
|
+
"reminder",
|
|
641
|
+
] {
|
|
642
|
+
assert!(obj.contains_key(key), "--json send shape lost {key}: {v}");
|
|
615
643
|
}
|
|
644
|
+
assert_eq!(v.get("verification"), Some(&serde_json::Value::Null));
|
|
645
|
+
assert_eq!(v.get("stage"), Some(&serde_json::Value::Null));
|
|
646
|
+
assert_eq!(v.get("reason"), Some(&serde_json::Value::Null));
|
|
647
|
+
assert_eq!(v.get("channel"), Some(&serde_json::Value::Null));
|
|
648
|
+
assert_eq!(v.get("delivered").and_then(|d| d.as_bool()), Some(false));
|
|
649
|
+
assert!(
|
|
650
|
+
!v.get("reminder")
|
|
651
|
+
.and_then(|reminder| reminder.as_str())
|
|
652
|
+
.unwrap_or_default()
|
|
653
|
+
.contains("Message delivered."),
|
|
654
|
+
"queued JSON reminder must not contradict delivered:false: {v}"
|
|
655
|
+
);
|
|
656
|
+
}
|
|
616
657
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
658
|
+
#[test]
|
|
659
|
+
fn cmd_send_watch_result_does_not_register_before_delivery() {
|
|
660
|
+
// 0.5.x send contract: --watch-result may only advertise a watcher after
|
|
661
|
+
// initial worker delivery is physically proven.
|
|
662
|
+
let args = SendArgs {
|
|
663
|
+
json: true,
|
|
664
|
+
..send_args_fixture()
|
|
665
|
+
};
|
|
666
|
+
let r = cmd_send(&args).expect("cmd_send returns CmdResult");
|
|
667
|
+
let v = match r.output {
|
|
668
|
+
CmdOutput::Json(v) => v,
|
|
669
|
+
other => panic!("expected Json, got {other:?}"),
|
|
670
|
+
};
|
|
671
|
+
assert!(
|
|
672
|
+
v.get("delivery_status").and_then(|s| s.as_str()).is_some(),
|
|
673
|
+
"send output must expose delivery_status; got {v}"
|
|
674
|
+
);
|
|
675
|
+
assert_eq!(
|
|
676
|
+
v.get("delivered").and_then(|s| s.as_bool()),
|
|
677
|
+
Some(false),
|
|
678
|
+
"undelivered send outcome must not look delivered"
|
|
679
|
+
);
|
|
680
|
+
assert!(
|
|
681
|
+
!v.as_object().unwrap().contains_key("watch"),
|
|
682
|
+
"watch_result:true must not attach result['watch'] before delivery; got {v}"
|
|
683
|
+
);
|
|
684
|
+
}
|
|
644
685
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
}
|
|
686
|
+
#[test]
|
|
687
|
+
fn cmd_send_failed_outcome_yields_error_exit() {
|
|
688
|
+
// DeliveryOutcome ok=false (e.g. refused) -> from_json -> ExitCode::Error (parser.py:507).
|
|
689
|
+
// A failed send to a target must propagate non-zero exit reporting through CmdResult.
|
|
690
|
+
let args = SendArgs {
|
|
691
|
+
target: Some("nonexistent".into()),
|
|
692
|
+
no_ack: false,
|
|
693
|
+
no_wait: false,
|
|
694
|
+
watch_result: false,
|
|
695
|
+
..send_args_fixture()
|
|
696
|
+
};
|
|
697
|
+
let r = cmd_send(&args).expect("cmd_send returns CmdResult even on delivery failure");
|
|
698
|
+
if let CmdOutput::Json(ref v) = r.output {
|
|
699
|
+
if v.get("ok").and_then(|b| b.as_bool()) == Some(false) {
|
|
700
|
+
assert_eq!(
|
|
701
|
+
r.exit,
|
|
702
|
+
ExitCode::Error,
|
|
703
|
+
"ok:false DeliveryOutcome MUST derive ExitCode::Error (non-zero exit)"
|
|
704
|
+
);
|
|
665
705
|
}
|
|
666
706
|
}
|
|
667
|
-
|
|
707
|
+
}
|
|
668
708
|
|
|
669
709
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
670
710
|
// coordinator.ok — non-compact status carries the FULL coordinator_health (incl. `ok`); compact
|
|
@@ -676,14 +716,27 @@ use super::*;
|
|
|
676
716
|
fn status_noncompact_coordinator_includes_ok() {
|
|
677
717
|
let ws = seed_status_workspace();
|
|
678
718
|
let v = status_port::status(&ws, /*compact=*/ false, /*detail=*/ true).expect("status");
|
|
679
|
-
let coord = v
|
|
719
|
+
let coord = v
|
|
720
|
+
.get("coordinator")
|
|
721
|
+
.and_then(|c| c.as_object())
|
|
722
|
+
.expect("coordinator object");
|
|
680
723
|
assert!(
|
|
681
724
|
coord.contains_key("ok"),
|
|
682
725
|
"non-compact coordinator MUST carry `ok` (golden queries.py:77 full coordinator_health); got keys {:?}",
|
|
683
726
|
coord.keys().collect::<Vec<_>>()
|
|
684
727
|
);
|
|
685
|
-
for key in [
|
|
686
|
-
|
|
728
|
+
for key in [
|
|
729
|
+
"ok",
|
|
730
|
+
"status",
|
|
731
|
+
"pid",
|
|
732
|
+
"metadata",
|
|
733
|
+
"metadata_ok",
|
|
734
|
+
"schema_ok",
|
|
735
|
+
] {
|
|
736
|
+
assert!(
|
|
737
|
+
coord.contains_key(key),
|
|
738
|
+
"non-compact coordinator missing `{key}`"
|
|
739
|
+
);
|
|
687
740
|
}
|
|
688
741
|
assert_eq!(
|
|
689
742
|
coord.get("ok").and_then(|v| v.as_bool()),
|
|
@@ -722,7 +775,10 @@ fn cmd_send_unknown_task_surfaces_golden_error_envelope_not_silent() {
|
|
|
722
775
|
let ws = std::env::temp_dir().join(format!(
|
|
723
776
|
"ta-cli-sendunk-{}-{}",
|
|
724
777
|
std::process::id(),
|
|
725
|
-
std::time::SystemTime::now()
|
|
778
|
+
std::time::SystemTime::now()
|
|
779
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
780
|
+
.unwrap()
|
|
781
|
+
.as_nanos()
|
|
726
782
|
));
|
|
727
783
|
std::fs::create_dir_all(ws.join(".team").join("runtime")).unwrap();
|
|
728
784
|
std::fs::write(
|
|
@@ -735,8 +791,10 @@ fn cmd_send_unknown_task_surfaces_golden_error_envelope_not_silent() {
|
|
|
735
791
|
"agents": { "w1": { "status": "running" } },
|
|
736
792
|
"tasks": []
|
|
737
793
|
}}
|
|
738
|
-
}))
|
|
739
|
-
|
|
794
|
+
}))
|
|
795
|
+
.unwrap(),
|
|
796
|
+
)
|
|
797
|
+
.unwrap();
|
|
740
798
|
let _ = crate::message_store::MessageStore::open(&ws);
|
|
741
799
|
let args = SendArgs {
|
|
742
800
|
target: Some("w1".into()),
|
|
@@ -759,7 +817,10 @@ fn cmd_send_unknown_task_surfaces_golden_error_envelope_not_silent() {
|
|
|
759
817
|
payload.error, "unknown task id: t-unknown",
|
|
760
818
|
"CLI error field == golden bare message (golden runtime.py:1032 str(exc)); NO 'validation:' prefix"
|
|
761
819
|
);
|
|
762
|
-
assert_eq!(
|
|
820
|
+
assert_eq!(
|
|
821
|
+
payload.action,
|
|
822
|
+
"run `team-agent doctor` or inspect the log path shown here"
|
|
823
|
+
);
|
|
763
824
|
let _ = std::fs::remove_dir_all(&ws);
|
|
764
825
|
}
|
|
765
826
|
|
|
@@ -775,7 +836,10 @@ fn run_send_unknown_task_renders_error_not_silent_swallow() {
|
|
|
775
836
|
let ws = std::env::temp_dir().join(format!(
|
|
776
837
|
"ta-run-sendunk-{}-{}",
|
|
777
838
|
std::process::id(),
|
|
778
|
-
std::time::SystemTime::now()
|
|
839
|
+
std::time::SystemTime::now()
|
|
840
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
841
|
+
.unwrap()
|
|
842
|
+
.as_nanos()
|
|
779
843
|
));
|
|
780
844
|
std::fs::create_dir_all(ws.join(".team").join("runtime")).unwrap();
|
|
781
845
|
std::fs::write(
|
|
@@ -788,13 +852,21 @@ fn run_send_unknown_task_renders_error_not_silent_swallow() {
|
|
|
788
852
|
"agents": { "w1": { "status": "running" } },
|
|
789
853
|
"tasks": []
|
|
790
854
|
}}
|
|
791
|
-
}))
|
|
792
|
-
|
|
855
|
+
}))
|
|
856
|
+
.unwrap(),
|
|
857
|
+
)
|
|
858
|
+
.unwrap();
|
|
793
859
|
let _ = crate::message_store::MessageStore::open(&ws);
|
|
794
860
|
let argv: Vec<String> = ["send", "w1", "--task", "t-unknown", "go", "--json"]
|
|
795
|
-
.iter()
|
|
861
|
+
.iter()
|
|
862
|
+
.map(ToString::to_string)
|
|
863
|
+
.collect();
|
|
796
864
|
let code = run(&argv, &ws);
|
|
797
|
-
assert_eq!(
|
|
865
|
+
assert_eq!(
|
|
866
|
+
code,
|
|
867
|
+
ExitCode::Error,
|
|
868
|
+
"run(send --task <unknown>) must exit Error, not Ok"
|
|
869
|
+
);
|
|
798
870
|
// run() must have RENDERED (emit_cli_error wrote the cli-error log); a swallow leaves none.
|
|
799
871
|
let logs_dir = ws.join(".team").join("logs");
|
|
800
872
|
let mut found = String::new();
|
|
@@ -867,11 +939,17 @@ fn run_leader_passthrough_flag_after_dashdash_renders_error_not_silent_swallow()
|
|
|
867
939
|
#[test]
|
|
868
940
|
fn r8_project_requeued_exhausted_watchers_golden_string_list() {
|
|
869
941
|
// golden attach event shape (what D4 emits): {watcher_ids:[str], count, trigger}.
|
|
870
|
-
let golden_event =
|
|
942
|
+
let golden_event =
|
|
943
|
+
serde_json::json!({"watcher_ids": ["w1", "w2"], "count": 2, "trigger": "attach_leader"});
|
|
871
944
|
let projected = crate::cli::leader_port::project_requeued_exhausted_watchers(&golden_event);
|
|
872
|
-
let list = projected
|
|
945
|
+
let list = projected
|
|
946
|
+
.as_array()
|
|
947
|
+
.expect("requeued_exhausted_watchers must be a JSON array");
|
|
873
948
|
let ids: Vec<&str> = list.iter().filter_map(|v| v.as_str()).collect();
|
|
874
|
-
assert_eq!(
|
|
949
|
+
assert_eq!(
|
|
950
|
+
ids,
|
|
951
|
+
vec!["w1", "w2"],
|
|
875
952
|
"D6: CLI requeued_exhausted_watchers must project the golden watcher_ids STRING list \
|
|
876
|
-
(leader/__init__.py:56), not the `requeued` Vec<WatcherNotice> objects; got {projected:?}"
|
|
953
|
+
(leader/__init__.py:56), not the `requeued` Vec<WatcherNotice> objects; got {projected:?}"
|
|
954
|
+
);
|
|
877
955
|
}
|