@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,8 +1,8 @@
|
|
|
1
|
-
use super::*;
|
|
2
1
|
use super::launch_spawn::{
|
|
3
2
|
quick_start_team_dir, seed_healthy_coordinator, DELEG_ROLE_ALPHA, DELEG_ROLE_BRAVO,
|
|
4
3
|
QS_VALID_ROLE,
|
|
5
4
|
};
|
|
5
|
+
use super::*;
|
|
6
6
|
use crate::transport::test_support::OfflineTransport;
|
|
7
7
|
|
|
8
8
|
/// A no-owner workspace (= self-contained team dir) with a compiled 2-agent spec (alpha, bravo) + state
|
|
@@ -10,7 +10,11 @@ use crate::transport::test_support::OfflineTransport;
|
|
|
10
10
|
pub(super) fn lanea_team_ws(status: &str) -> PathBuf {
|
|
11
11
|
let ws = temp_ws().join("laneateam");
|
|
12
12
|
std::fs::create_dir_all(ws.join("agents")).unwrap();
|
|
13
|
-
std::fs::write(
|
|
13
|
+
std::fs::write(
|
|
14
|
+
ws.join("TEAM.md"),
|
|
15
|
+
"---\nname: laneateam\nobjective: Lane A probe.\nprovider: codex\n---\n\nteam.\n",
|
|
16
|
+
)
|
|
17
|
+
.unwrap();
|
|
14
18
|
std::fs::write(ws.join("agents").join("alpha.md"), DELEG_ROLE_ALPHA).unwrap();
|
|
15
19
|
std::fs::write(ws.join("agents").join("bravo.md"), DELEG_ROLE_BRAVO).unwrap();
|
|
16
20
|
let spec = crate::compiler::compile_team(&ws).expect("compile lane-A team");
|
|
@@ -21,9 +25,18 @@ pub(super) fn lanea_team_ws(status: &str) -> PathBuf {
|
|
|
21
25
|
.replace("default_assignee: \"alpha\"", "default_assignee: \"bravo\"")
|
|
22
26
|
.replace("assign_to: \"alpha\"", "assign_to: \"bravo\"")
|
|
23
27
|
.replace("assignee: \"alpha\"", "assignee: \"bravo\"");
|
|
24
|
-
assert!(
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
assert!(
|
|
29
|
+
!yaml.contains("default_assignee: \"alpha\""),
|
|
30
|
+
"fixture unroute: default_assignee still alpha"
|
|
31
|
+
);
|
|
32
|
+
assert!(
|
|
33
|
+
!yaml.contains("assign_to: \"alpha\""),
|
|
34
|
+
"fixture unroute: a routing rule still assign_to alpha"
|
|
35
|
+
);
|
|
36
|
+
assert!(
|
|
37
|
+
!yaml.contains("assignee: \"alpha\""),
|
|
38
|
+
"fixture unroute: task still assignee alpha"
|
|
39
|
+
);
|
|
27
40
|
std::fs::write(ws.join("team.spec.yaml"), yaml).unwrap();
|
|
28
41
|
crate::state::persist::save_runtime_state(
|
|
29
42
|
&ws,
|
|
@@ -90,7 +103,10 @@ fn lanea_stop_agent_unknown_agent_is_unknown_worker_not_owner_refused() {
|
|
|
90
103
|
#[test]
|
|
91
104
|
fn lanea_fork_agent_unknown_source_is_unknown_worker_before_session_check() {
|
|
92
105
|
let ws = lanea_team_ws("stopped");
|
|
93
|
-
let text = format!(
|
|
106
|
+
let text = format!(
|
|
107
|
+
"{:?}",
|
|
108
|
+
fork_agent(&ws, &aid("ghost"), &aid("newfork"), None, false, None)
|
|
109
|
+
);
|
|
94
110
|
assert!(
|
|
95
111
|
text.contains("unknown worker"),
|
|
96
112
|
"fork_agent must reject an UNKNOWN source as 'unknown worker agent id: ghost' BEFORE the session-id \
|
|
@@ -105,7 +121,10 @@ fn lanea_fork_agent_unknown_source_is_unknown_worker_before_session_check() {
|
|
|
105
121
|
fn lanea_fork_agent_duplicate_target_is_already_exists_before_session_check() {
|
|
106
122
|
let ws = lanea_team_ws("stopped");
|
|
107
123
|
// target 'bravo' already exists in the spec -> duplicate; source 'alpha' exists (its session_id is irrelevant).
|
|
108
|
-
let text = format!(
|
|
124
|
+
let text = format!(
|
|
125
|
+
"{:?}",
|
|
126
|
+
fork_agent(&ws, &aid("alpha"), &aid("bravo"), None, false, None)
|
|
127
|
+
);
|
|
109
128
|
assert!(
|
|
110
129
|
text.contains("already exists"),
|
|
111
130
|
"fork_agent must reject a DUPLICATE target 'bravo' as 'agent id already exists' BEFORE the session-id \
|
|
@@ -125,8 +144,14 @@ fn lanea_stop_agent_kills_window_and_marks_stopped() {
|
|
|
125
144
|
let ws = lanea_team_ws("running");
|
|
126
145
|
let _ = stop_agent(&ws, &aid("alpha"), None); // real machine: tmux kill-window team-laneateam:alpha
|
|
127
146
|
let state = crate::state::persist::load_runtime_state(&ws).expect("load state");
|
|
128
|
-
let status = state
|
|
129
|
-
|
|
147
|
+
let status = state
|
|
148
|
+
.pointer("/agents/alpha/status")
|
|
149
|
+
.and_then(serde_json::Value::as_str);
|
|
150
|
+
assert_eq!(
|
|
151
|
+
status,
|
|
152
|
+
Some("stopped"),
|
|
153
|
+
"stop_agent must mark agents[alpha].status='stopped' after killing the window"
|
|
154
|
+
);
|
|
130
155
|
}
|
|
131
156
|
|
|
132
157
|
// ═════════════════════════════════════════════════════════════════════════
|
|
@@ -145,8 +170,10 @@ fn lanea_stop_agent_kills_window_and_marks_stopped() {
|
|
|
145
170
|
#[test]
|
|
146
171
|
fn add_agent_joins_w2_into_running_roster_and_existing_session() {
|
|
147
172
|
let team_dir = quick_start_team_dir(QS_VALID_ROLE); // <base>/teamdir (agents/implementer.md)
|
|
148
|
-
let workspace = team_dir
|
|
149
|
-
|
|
173
|
+
let workspace = team_dir
|
|
174
|
+
.parent()
|
|
175
|
+
.expect("team_workspace(team_dir) = parent"); // the RUNNING team's workspace
|
|
176
|
+
// a RUNNING team already in the session (roster = ['w1'], session_name set).
|
|
150
177
|
crate::state::persist::save_runtime_state(
|
|
151
178
|
workspace,
|
|
152
179
|
&json!({
|
|
@@ -156,7 +183,7 @@ fn add_agent_joins_w2_into_running_roster_and_existing_session() {
|
|
|
156
183
|
)
|
|
157
184
|
.unwrap();
|
|
158
185
|
seed_healthy_coordinator(workspace); // start_coordinator -> AlreadyRunning (no real daemon)
|
|
159
|
-
|
|
186
|
+
// the new agent's role file, OUTSIDE agents/ so it's not a duplicate of an existing agent.
|
|
160
187
|
let role_file = team_dir.join("w2-role.md");
|
|
161
188
|
std::fs::write(
|
|
162
189
|
&role_file,
|
|
@@ -165,7 +192,14 @@ fn add_agent_joins_w2_into_running_roster_and_existing_session() {
|
|
|
165
192
|
.unwrap();
|
|
166
193
|
let transport = OfflineTransport::new().with_session_present(true);
|
|
167
194
|
|
|
168
|
-
let _ = add_agent_with_transport(
|
|
195
|
+
let _ = add_agent_with_transport(
|
|
196
|
+
team_dir.as_path(),
|
|
197
|
+
&aid("w2"),
|
|
198
|
+
&role_file,
|
|
199
|
+
false,
|
|
200
|
+
None,
|
|
201
|
+
&transport,
|
|
202
|
+
);
|
|
169
203
|
|
|
170
204
|
// (1) [load-bearing] the RUNNING roster (team_workspace) must now contain w2.
|
|
171
205
|
let state = crate::state::persist::load_runtime_state(workspace).expect("load running state");
|
|
@@ -218,11 +252,11 @@ impl crate::tmux_backend::CommandRunner for SocketRecordingRunner {
|
|
|
218
252
|
fn socket_for_workspace(workspace: &std::path::Path) -> String {
|
|
219
253
|
use crate::transport::Transport as _;
|
|
220
254
|
let recorded = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
|
221
|
-
let runner = SocketRecordingRunner {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
255
|
+
let runner = SocketRecordingRunner {
|
|
256
|
+
recorded: std::sync::Arc::clone(&recorded),
|
|
257
|
+
};
|
|
258
|
+
let backend =
|
|
259
|
+
crate::tmux_backend::TmuxBackend::with_runner_for_workspace(Box::new(runner), workspace);
|
|
226
260
|
backend.has_session(&sess("team-implteam")).unwrap();
|
|
227
261
|
let socket = recorded.lock().unwrap()[0][2].clone();
|
|
228
262
|
socket
|
|
@@ -257,7 +291,10 @@ fn bug_a_team_dir_lifecycle_socket_matches_daemon_run_workspace_socket() {
|
|
|
257
291
|
fn bug_a_stop_agent_team_dir_input_kills_existing_window_real_machine() {
|
|
258
292
|
let team_dir = quick_start_team_dir(QS_VALID_ROLE);
|
|
259
293
|
let report = stop_agent(&team_dir, &aid("w1"), None).expect("real stop-agent");
|
|
260
|
-
assert!(
|
|
294
|
+
assert!(
|
|
295
|
+
report.stopped,
|
|
296
|
+
"existing worker window must be killed, not silently reported absent"
|
|
297
|
+
);
|
|
261
298
|
}
|
|
262
299
|
|
|
263
300
|
struct EnvVarGuard {
|
|
@@ -467,11 +504,16 @@ fn e27_stop_agent_uses_attached_explicit_state_socket() {
|
|
|
467
504
|
|
|
468
505
|
let report = stop_agent(&ws, &aid("alpha"), None).expect("stop-agent");
|
|
469
506
|
|
|
470
|
-
assert!(
|
|
507
|
+
assert!(
|
|
508
|
+
report.stopped,
|
|
509
|
+
"attached explicit-socket worker should be stopped"
|
|
510
|
+
);
|
|
471
511
|
assert_only_expected_socket_used(&shim.log, endpoint);
|
|
472
512
|
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
473
513
|
assert_eq!(
|
|
474
|
-
state
|
|
514
|
+
state
|
|
515
|
+
.pointer("/agents/alpha/status")
|
|
516
|
+
.and_then(serde_json::Value::as_str),
|
|
475
517
|
Some("stopped")
|
|
476
518
|
);
|
|
477
519
|
}
|
|
@@ -494,14 +536,19 @@ fn e27_reset_agent_uses_attached_explicit_state_socket_for_stop_and_spawn() {
|
|
|
494
536
|
);
|
|
495
537
|
assert_only_expected_socket_used(&shim.log, endpoint);
|
|
496
538
|
let raw = std::fs::read_to_string(&shim.log).unwrap();
|
|
497
|
-
assert!(
|
|
539
|
+
assert!(
|
|
540
|
+
raw.contains("kill-pane"),
|
|
541
|
+
"reset must stop the old pane first; argv log:\n{raw}"
|
|
542
|
+
);
|
|
498
543
|
assert!(
|
|
499
544
|
raw.contains("new-window") || raw.contains("new-session"),
|
|
500
545
|
"reset must respawn the worker through the same endpoint; argv log:\n{raw}"
|
|
501
546
|
);
|
|
502
547
|
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
503
548
|
assert_ne!(
|
|
504
|
-
state
|
|
549
|
+
state
|
|
550
|
+
.pointer("/agents/alpha/session_id")
|
|
551
|
+
.and_then(serde_json::Value::as_str),
|
|
505
552
|
Some("old-session"),
|
|
506
553
|
"discard-session must not preserve the old session id"
|
|
507
554
|
);
|
|
@@ -553,10 +600,16 @@ fn e27_stop_agent_expands_short_state_socket_name() {
|
|
|
553
600
|
// Read every events.jsonl the verb could write to (run-workspace resolves to either the team dir or its
|
|
554
601
|
// parent; read both so the lock is robust to that resolution).
|
|
555
602
|
fn lifecycle_events(ws: &std::path::Path) -> Vec<serde_json::Value> {
|
|
556
|
-
let mut out = crate::event_log::EventLog::new(ws)
|
|
603
|
+
let mut out = crate::event_log::EventLog::new(ws)
|
|
604
|
+
.tail(0)
|
|
605
|
+
.unwrap_or_default();
|
|
557
606
|
if let Ok(parent) = crate::model::paths::team_workspace(ws) {
|
|
558
607
|
if parent != ws {
|
|
559
|
-
out.extend(
|
|
608
|
+
out.extend(
|
|
609
|
+
crate::event_log::EventLog::new(&parent)
|
|
610
|
+
.tail(0)
|
|
611
|
+
.unwrap_or_default(),
|
|
612
|
+
);
|
|
560
613
|
}
|
|
561
614
|
}
|
|
562
615
|
out
|
|
@@ -565,16 +618,30 @@ fn lifecycle_events(ws: &std::path::Path) -> Vec<serde_json::Value> {
|
|
|
565
618
|
fn payload_keys(event: &serde_json::Value) -> std::collections::BTreeSet<String> {
|
|
566
619
|
event
|
|
567
620
|
.as_object()
|
|
568
|
-
.map(|o|
|
|
621
|
+
.map(|o| {
|
|
622
|
+
o.keys()
|
|
623
|
+
.filter(|k| *k != "ts" && *k != "event")
|
|
624
|
+
.cloned()
|
|
625
|
+
.collect()
|
|
626
|
+
})
|
|
569
627
|
.unwrap_or_default()
|
|
570
628
|
}
|
|
571
629
|
|
|
572
630
|
fn find_event<'a>(events: &'a [serde_json::Value], name: &str) -> Option<&'a serde_json::Value> {
|
|
573
|
-
events
|
|
631
|
+
events
|
|
632
|
+
.iter()
|
|
633
|
+
.find(|e| e.get("event").and_then(|v| v.as_str()) == Some(name))
|
|
574
634
|
}
|
|
575
635
|
|
|
576
636
|
fn names(events: &[serde_json::Value]) -> Vec<String> {
|
|
577
|
-
events
|
|
637
|
+
events
|
|
638
|
+
.iter()
|
|
639
|
+
.filter_map(|e| {
|
|
640
|
+
e.get("event")
|
|
641
|
+
.and_then(|v| v.as_str())
|
|
642
|
+
.map(ToString::to_string)
|
|
643
|
+
})
|
|
644
|
+
.collect()
|
|
578
645
|
}
|
|
579
646
|
|
|
580
647
|
// remove-agent — golden agents.py:66-147 writes lifecycle.remove_step_completed (per step) +
|
|
@@ -590,30 +657,62 @@ fn remove_agent_emits_golden_lifecycle_event_payloads() {
|
|
|
590
657
|
"remove_agent must write `remove_agent.complete` (golden agents.py:140); Rust restart/remove.rs emits NO \
|
|
591
658
|
events. events seen: {:?}", names(&events)
|
|
592
659
|
));
|
|
593
|
-
let expected: std::collections::BTreeSet<String> =
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
"
|
|
598
|
-
|
|
599
|
-
|
|
660
|
+
let expected: std::collections::BTreeSet<String> = [
|
|
661
|
+
"agent_id",
|
|
662
|
+
"from_spec",
|
|
663
|
+
"force",
|
|
664
|
+
"stopped",
|
|
665
|
+
"role_file_removed",
|
|
666
|
+
"cleared_locations",
|
|
667
|
+
]
|
|
668
|
+
.iter()
|
|
669
|
+
.map(ToString::to_string)
|
|
670
|
+
.collect();
|
|
671
|
+
assert_eq!(
|
|
672
|
+
payload_keys(complete),
|
|
673
|
+
expected,
|
|
674
|
+
"remove_agent.complete payload key set must match golden agents.py:140-147; got {:?}",
|
|
675
|
+
payload_keys(complete)
|
|
676
|
+
);
|
|
677
|
+
assert_eq!(
|
|
678
|
+
complete.get("agent_id").and_then(|v| v.as_str()),
|
|
679
|
+
Some("alpha")
|
|
680
|
+
);
|
|
681
|
+
assert_eq!(
|
|
682
|
+
complete.get("from_spec").and_then(|v| v.as_bool()),
|
|
683
|
+
Some(true)
|
|
684
|
+
);
|
|
600
685
|
assert_eq!(complete.get("force").and_then(|v| v.as_bool()), Some(true));
|
|
601
686
|
|
|
602
687
|
// lifecycle.remove_step_completed (golden agents.py:66-72) — fired per step; key set {agent_id, step,
|
|
603
688
|
// resource}; the workspace_state + agent_health steps fire for a stopped from_spec remove.
|
|
604
|
-
let step_events: Vec<&serde_json::Value> = events
|
|
605
|
-
.
|
|
606
|
-
|
|
689
|
+
let step_events: Vec<&serde_json::Value> = events
|
|
690
|
+
.iter()
|
|
691
|
+
.filter(|e| {
|
|
692
|
+
e.get("event").and_then(|v| v.as_str()) == Some("lifecycle.remove_step_completed")
|
|
693
|
+
})
|
|
694
|
+
.collect();
|
|
695
|
+
let steps: Vec<&str> = step_events
|
|
696
|
+
.iter()
|
|
697
|
+
.filter_map(|e| e.get("step").and_then(|v| v.as_str()))
|
|
698
|
+
.collect();
|
|
607
699
|
assert!(steps.contains(&"workspace_state") && steps.contains(&"agent_health"),
|
|
608
700
|
"remove_agent must write lifecycle.remove_step_completed for each step (golden agents.py:86,109); got steps {steps:?}");
|
|
609
|
-
let ws_step = step_events
|
|
701
|
+
let ws_step = step_events
|
|
702
|
+
.iter()
|
|
703
|
+
.find(|e| e.get("step").and_then(|v| v.as_str()) == Some("workspace_state"))
|
|
610
704
|
.expect("the workspace_state step event");
|
|
611
|
-
let expected_step: std::collections::BTreeSet<String> =
|
|
612
|
-
|
|
705
|
+
let expected_step: std::collections::BTreeSet<String> = ["agent_id", "step", "resource"]
|
|
706
|
+
.iter()
|
|
707
|
+
.map(ToString::to_string)
|
|
708
|
+
.collect();
|
|
613
709
|
assert_eq!(payload_keys(ws_step), expected_step,
|
|
614
710
|
"lifecycle.remove_step_completed (non-stop step) payload must be EXACTLY {{agent_id, step, resource}} (golden agents.py:68-70)");
|
|
615
|
-
assert_eq!(
|
|
616
|
-
"
|
|
711
|
+
assert_eq!(
|
|
712
|
+
ws_step.get("resource").and_then(|v| v.as_str()),
|
|
713
|
+
Some("state.json:agents"),
|
|
714
|
+
"workspace_state step resource == golden 'state.json:agents' (agents.py:82)"
|
|
715
|
+
);
|
|
617
716
|
}
|
|
618
717
|
|
|
619
718
|
// reset-agent — golden operations.py:123/132 writes discard.session_tombstone {agent_id,
|
|
@@ -627,7 +726,14 @@ fn reset_agent_emits_golden_lifecycle_event_payloads() {
|
|
|
627
726
|
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
628
727
|
|
|
629
728
|
let transport = OfflineTransport::new().with_session_present(true);
|
|
630
|
-
let _ = crate::lifecycle::reset_agent_with_transport(
|
|
729
|
+
let _ = crate::lifecycle::reset_agent_with_transport(
|
|
730
|
+
&ws,
|
|
731
|
+
&aid("alpha"),
|
|
732
|
+
true,
|
|
733
|
+
false,
|
|
734
|
+
None,
|
|
735
|
+
&transport,
|
|
736
|
+
);
|
|
631
737
|
let events = lifecycle_events(&ws);
|
|
632
738
|
|
|
633
739
|
// discard.session_tombstone (golden operations.py:123) — EXACT key set + the discarded session id.
|
|
@@ -635,23 +741,38 @@ fn reset_agent_emits_golden_lifecycle_event_payloads() {
|
|
|
635
741
|
"reset_agent must write `discard.session_tombstone` (golden operations.py:123); Rust restart/agent.rs emits \
|
|
636
742
|
NO events. events seen: {:?}", names(&events)
|
|
637
743
|
));
|
|
638
|
-
let expected_tomb: std::collections::BTreeSet<String> =
|
|
639
|
-
|
|
744
|
+
let expected_tomb: std::collections::BTreeSet<String> = ["agent_id", "discarded_session_id"]
|
|
745
|
+
.iter()
|
|
746
|
+
.map(ToString::to_string)
|
|
747
|
+
.collect();
|
|
640
748
|
assert_eq!(payload_keys(tombstone), expected_tomb,
|
|
641
749
|
"discard.session_tombstone payload key set must be EXACTLY {{agent_id, discarded_session_id}} (golden operations.py:123)");
|
|
642
|
-
assert_eq!(
|
|
643
|
-
|
|
644
|
-
|
|
750
|
+
assert_eq!(
|
|
751
|
+
tombstone.get("agent_id").and_then(|v| v.as_str()),
|
|
752
|
+
Some("alpha")
|
|
753
|
+
);
|
|
754
|
+
assert_eq!(
|
|
755
|
+
tombstone
|
|
756
|
+
.get("discarded_session_id")
|
|
757
|
+
.and_then(|v| v.as_str()),
|
|
758
|
+
Some("S-alpha"),
|
|
759
|
+
"discarded_session_id == the agent's stored session_id (golden operations.py:118)"
|
|
760
|
+
);
|
|
645
761
|
|
|
646
762
|
// reset_agent.complete (golden operations.py:132) — EXACT key set {agent_id, stopped, started}.
|
|
647
763
|
let complete = find_event(&events, "reset_agent.complete").unwrap_or_else(|| panic!(
|
|
648
764
|
"reset_agent must write `reset_agent.complete` (golden operations.py:132); events seen: {:?}", names(&events)
|
|
649
765
|
));
|
|
650
|
-
let expected_complete: std::collections::BTreeSet<String> =
|
|
651
|
-
|
|
766
|
+
let expected_complete: std::collections::BTreeSet<String> = ["agent_id", "stopped", "started"]
|
|
767
|
+
.iter()
|
|
768
|
+
.map(ToString::to_string)
|
|
769
|
+
.collect();
|
|
652
770
|
assert_eq!(payload_keys(complete), expected_complete,
|
|
653
771
|
"reset_agent.complete payload key set must be EXACTLY {{agent_id, stopped, started}} (golden operations.py:132)");
|
|
654
|
-
assert_eq!(
|
|
772
|
+
assert_eq!(
|
|
773
|
+
complete.get("agent_id").and_then(|v| v.as_str()),
|
|
774
|
+
Some("alpha")
|
|
775
|
+
);
|
|
655
776
|
}
|
|
656
777
|
|
|
657
778
|
// 0.4.10+ reset duplicate-window fix RED tests
|
|
@@ -666,7 +787,7 @@ fn reset_agent_emits_golden_lifecycle_event_payloads() {
|
|
|
666
787
|
/// kills by pane_id, returns stopped=true. Reset proceeds normally.
|
|
667
788
|
#[test]
|
|
668
789
|
fn reset_agent_stale_pane_with_same_role_residue_kills_by_pane_id() {
|
|
669
|
-
use crate::transport::{
|
|
790
|
+
use crate::transport::{PaneId as PaneIdT, PaneInfo, SessionName, WindowName};
|
|
670
791
|
let ws = lanea_team_ws("running");
|
|
671
792
|
let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
672
793
|
state["agents"]["alpha"]["pane_id"] = json!("%STALE");
|
|
@@ -713,8 +834,8 @@ fn reset_agent_stale_pane_with_same_role_residue_kills_by_pane_id() {
|
|
|
713
834
|
);
|
|
714
835
|
// The stop_complete event must report stopped=true (residual was killed).
|
|
715
836
|
let events = lifecycle_events(&ws);
|
|
716
|
-
let stop_complete =
|
|
717
|
-
.expect("stop_agent.complete must be emitted");
|
|
837
|
+
let stop_complete =
|
|
838
|
+
find_event(&events, "stop_agent.complete").expect("stop_agent.complete must be emitted");
|
|
718
839
|
assert_eq!(
|
|
719
840
|
stop_complete.get("stopped").and_then(|v| v.as_bool()),
|
|
720
841
|
Some(true),
|
|
@@ -756,8 +877,7 @@ fn reset_agent_stop_not_proven_grep_guard_hard_gate_wired() {
|
|
|
756
877
|
"gate must run only when stop.stopped=false (the dangerous case)"
|
|
757
878
|
);
|
|
758
879
|
assert!(
|
|
759
|
-
contents.contains("list_same_role_panes")
|
|
760
|
-
&& contents.contains("is_per_agent_window"),
|
|
880
|
+
contents.contains("list_same_role_panes") && contents.contains("is_per_agent_window"),
|
|
761
881
|
"stop_agent_at_paths must enumerate same-role panes via the new helpers"
|
|
762
882
|
);
|
|
763
883
|
}
|
|
@@ -805,12 +925,7 @@ fn reset_agent_already_absent_can_start() {
|
|
|
805
925
|
fn stop_agent_already_absent_still_returns_stopped_false() {
|
|
806
926
|
let ws = lanea_team_ws("stopped");
|
|
807
927
|
let transport = OfflineTransport::new().with_session_present(true);
|
|
808
|
-
let result = crate::lifecycle::stop_agent_with_transport(
|
|
809
|
-
&ws,
|
|
810
|
-
&aid("alpha"),
|
|
811
|
-
None,
|
|
812
|
-
&transport,
|
|
813
|
-
);
|
|
928
|
+
let result = crate::lifecycle::stop_agent_with_transport(&ws, &aid("alpha"), None, &transport);
|
|
814
929
|
let report = result.expect("stop_agent must return Ok for absent agent");
|
|
815
930
|
assert!(
|
|
816
931
|
!report.stopped,
|
|
@@ -832,11 +947,16 @@ fn stop_agent_emits_golden_complete_event_payload() {
|
|
|
832
947
|
"stop_agent must write `stop_agent.complete` (golden operations.py:98); Rust restart/agent.rs emits NO \
|
|
833
948
|
events for stop. events seen: {:?}", names(&events)
|
|
834
949
|
));
|
|
835
|
-
let expected: std::collections::BTreeSet<String> =
|
|
836
|
-
|
|
950
|
+
let expected: std::collections::BTreeSet<String> = ["agent_id", "target", "stopped"]
|
|
951
|
+
.iter()
|
|
952
|
+
.map(ToString::to_string)
|
|
953
|
+
.collect();
|
|
837
954
|
assert_eq!(payload_keys(complete), expected,
|
|
838
955
|
"stop_agent.complete payload key set must be EXACTLY {{agent_id, target, stopped}} (golden operations.py:98)");
|
|
839
|
-
assert_eq!(
|
|
956
|
+
assert_eq!(
|
|
957
|
+
complete.get("agent_id").and_then(|v| v.as_str()),
|
|
958
|
+
Some("alpha")
|
|
959
|
+
);
|
|
840
960
|
}
|
|
841
961
|
|
|
842
962
|
// start-agent — golden start.py:225 writes start_agent.agent_start on the spawn path (9 keys). Rust emits
|
|
@@ -845,7 +965,15 @@ fn stop_agent_emits_golden_complete_event_payload() {
|
|
|
845
965
|
fn start_agent_emits_golden_agent_start_event_payload() {
|
|
846
966
|
let ws = lanea_team_ws("stopped"); // not running -> fresh spawn path (not noop)
|
|
847
967
|
let transport = OfflineTransport::new();
|
|
848
|
-
let _ = crate::lifecycle::start_agent_with_transport(
|
|
968
|
+
let _ = crate::lifecycle::start_agent_with_transport(
|
|
969
|
+
&ws,
|
|
970
|
+
&aid("alpha"),
|
|
971
|
+
true,
|
|
972
|
+
false,
|
|
973
|
+
true,
|
|
974
|
+
None,
|
|
975
|
+
&transport,
|
|
976
|
+
);
|
|
849
977
|
let events = lifecycle_events(&ws);
|
|
850
978
|
|
|
851
979
|
let agent_start = find_event(&events, "start_agent.agent_start").unwrap_or_else(|| panic!(
|
|
@@ -853,14 +981,33 @@ fn start_agent_emits_golden_agent_start_event_payload() {
|
|
|
853
981
|
start_agent.noop. events seen: {:?}", names(&events)
|
|
854
982
|
));
|
|
855
983
|
let expected: std::collections::BTreeSet<String> = [
|
|
856
|
-
"agent_id",
|
|
857
|
-
"
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
"
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
"
|
|
984
|
+
"agent_id",
|
|
985
|
+
"provider",
|
|
986
|
+
"start_mode",
|
|
987
|
+
"session_id",
|
|
988
|
+
"session",
|
|
989
|
+
"window",
|
|
990
|
+
"tmux_start_mode",
|
|
991
|
+
"command",
|
|
992
|
+
"mcp_config",
|
|
993
|
+
]
|
|
994
|
+
.iter()
|
|
995
|
+
.map(ToString::to_string)
|
|
996
|
+
.collect();
|
|
997
|
+
assert_eq!(
|
|
998
|
+
payload_keys(agent_start),
|
|
999
|
+
expected,
|
|
1000
|
+
"start_agent.agent_start payload key set must match golden start.py:225-235 (9 keys)"
|
|
1001
|
+
);
|
|
1002
|
+
assert_eq!(
|
|
1003
|
+
agent_start.get("agent_id").and_then(|v| v.as_str()),
|
|
1004
|
+
Some("alpha")
|
|
1005
|
+
);
|
|
1006
|
+
assert_eq!(
|
|
1007
|
+
agent_start.get("window").and_then(|v| v.as_str()),
|
|
1008
|
+
Some("alpha"),
|
|
1009
|
+
"agent_start.window == agent_id (golden start.py:232)"
|
|
1010
|
+
);
|
|
864
1011
|
}
|
|
865
1012
|
|
|
866
1013
|
// restart — golden orchestration.py:507 writes ONE restart.resume_decision per non-paused worker
|
|
@@ -872,16 +1019,36 @@ fn restart_emits_golden_resume_decision_event_payload() {
|
|
|
872
1019
|
let _ = crate::lifecycle::restart_with_transport(&ws, true, None, &transport);
|
|
873
1020
|
let events = lifecycle_events(&ws);
|
|
874
1021
|
|
|
875
|
-
let decisions: Vec<&serde_json::Value> = events
|
|
876
|
-
.
|
|
877
|
-
|
|
878
|
-
|
|
1022
|
+
let decisions: Vec<&serde_json::Value> = events
|
|
1023
|
+
.iter()
|
|
1024
|
+
.filter(|e| e.get("event").and_then(|v| v.as_str()) == Some("restart.resume_decision"))
|
|
1025
|
+
.collect();
|
|
1026
|
+
assert!(
|
|
1027
|
+
!decisions.is_empty(),
|
|
1028
|
+
"restart must write one `restart.resume_decision` per non-paused worker \
|
|
1029
|
+
(golden orchestration.py:128/507); Rust emits NO restart events. events seen: {:?}",
|
|
1030
|
+
names(&events)
|
|
1031
|
+
);
|
|
879
1032
|
let expected: std::collections::BTreeSet<String> = [
|
|
880
|
-
"worker_id",
|
|
881
|
-
|
|
1033
|
+
"worker_id",
|
|
1034
|
+
"has_first_send_at",
|
|
1035
|
+
"has_session_id",
|
|
1036
|
+
"allow_fresh",
|
|
1037
|
+
"decision",
|
|
1038
|
+
"first_send_at",
|
|
1039
|
+
"session_id",
|
|
1040
|
+
]
|
|
1041
|
+
.iter()
|
|
1042
|
+
.map(ToString::to_string)
|
|
1043
|
+
.collect();
|
|
882
1044
|
assert_eq!(payload_keys(decisions[0]), expected,
|
|
883
1045
|
"restart.resume_decision payload key set must match golden orchestration.py:507-514 (7 keys, `worker_id`)");
|
|
884
|
-
let worker_ids: Vec<&str> = decisions
|
|
885
|
-
|
|
886
|
-
|
|
1046
|
+
let worker_ids: Vec<&str> = decisions
|
|
1047
|
+
.iter()
|
|
1048
|
+
.filter_map(|e| e.get("worker_id").and_then(|v| v.as_str()))
|
|
1049
|
+
.collect();
|
|
1050
|
+
assert!(
|
|
1051
|
+
worker_ids.contains(&"alpha") || worker_ids.contains(&"bravo"),
|
|
1052
|
+
"restart.resume_decision.worker_id must name a real worker (golden); got {worker_ids:?}"
|
|
1053
|
+
);
|
|
887
1054
|
}
|