@team-agent/installer 0.2.10 → 0.3.0
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 +744 -0
- package/Cargo.toml +34 -0
- package/crates/team-agent/Cargo.toml +33 -0
- package/crates/team-agent/src/cli/adapters.rs +1343 -0
- package/crates/team-agent/src/cli/diagnose.rs +554 -0
- package/crates/team-agent/src/cli/emit.rs +1077 -0
- package/crates/team-agent/src/cli/helpers.rs +88 -0
- package/crates/team-agent/src/cli/leader.rs +216 -0
- package/crates/team-agent/src/cli/mod.rs +1141 -0
- package/crates/team-agent/src/cli/profile.rs +306 -0
- package/crates/team-agent/src/cli/send.rs +215 -0
- package/crates/team-agent/src/cli/status.rs +179 -0
- package/crates/team-agent/src/cli/status_port.rs +502 -0
- package/crates/team-agent/src/cli/tests/base.rs +616 -0
- package/crates/team-agent/src/cli/tests/compile.rs +96 -0
- package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
- package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
- package/crates/team-agent/src/cli/tests/mod.rs +97 -0
- package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
- package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
- package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
- package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
- package/crates/team-agent/src/cli/types.rs +605 -0
- package/crates/team-agent/src/compiler/tests.rs +701 -0
- package/crates/team-agent/src/compiler.rs +489 -0
- package/crates/team-agent/src/coordinator/backoff.rs +153 -0
- package/crates/team-agent/src/coordinator/health.rs +436 -0
- package/crates/team-agent/src/coordinator/mod.rs +80 -0
- package/crates/team-agent/src/coordinator/orphan.rs +179 -0
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
- package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
- package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
- package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
- package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
- package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
- package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
- package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
- package/crates/team-agent/src/coordinator/tick.rs +2032 -0
- package/crates/team-agent/src/coordinator/types.rs +584 -0
- package/crates/team-agent/src/db/migration.rs +716 -0
- package/crates/team-agent/src/db/mod.rs +23 -0
- package/crates/team-agent/src/db/schema.rs +378 -0
- package/crates/team-agent/src/event_log.rs +375 -0
- package/crates/team-agent/src/fake_worker.rs +253 -0
- package/crates/team-agent/src/leader/helpers.rs +190 -0
- package/crates/team-agent/src/leader/inject.rs +33 -0
- package/crates/team-agent/src/leader/lease.rs +1063 -0
- package/crates/team-agent/src/leader/mod.rs +99 -0
- package/crates/team-agent/src/leader/owner_bind.rs +292 -0
- package/crates/team-agent/src/leader/rediscover/tests.rs +525 -0
- package/crates/team-agent/src/leader/rediscover.rs +1099 -0
- package/crates/team-agent/src/leader/start.rs +273 -0
- package/crates/team-agent/src/leader/takeover.rs +235 -0
- package/crates/team-agent/src/leader/tests/basics.rs +183 -0
- package/crates/team-agent/src/leader/tests/byte_findings.rs +234 -0
- package/crates/team-agent/src/leader/tests/identity.rs +206 -0
- package/crates/team-agent/src/leader/tests/idle.rs +271 -0
- package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
- package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -0
- package/crates/team-agent/src/leader/tests/mod.rs +125 -0
- package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
- package/crates/team-agent/src/leader/types.rs +487 -0
- package/crates/team-agent/src/lib.rs +85 -0
- package/crates/team-agent/src/lifecycle/display.rs +228 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
- package/crates/team-agent/src/lifecycle/launch.rs +1833 -0
- package/crates/team-agent/src/lifecycle/mod.rs +62 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
- package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
- package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
- package/crates/team-agent/src/lifecycle/restart.rs +76 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
- package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +933 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
- package/crates/team-agent/src/lifecycle/tests.rs +27 -0
- package/crates/team-agent/src/lifecycle/types.rs +685 -0
- package/crates/team-agent/src/main.rs +41 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
- package/crates/team-agent/src/mcp_server/mod.rs +183 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
- package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
- package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
- package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +159 -0
- package/crates/team-agent/src/mcp_server/tests.rs +38 -0
- package/crates/team-agent/src/mcp_server/tools.rs +603 -0
- package/crates/team-agent/src/mcp_server/types.rs +421 -0
- package/crates/team-agent/src/mcp_server/wire.rs +388 -0
- package/crates/team-agent/src/message_store.rs +767 -0
- package/crates/team-agent/src/messaging/activity.rs +433 -0
- package/crates/team-agent/src/messaging/delivery.rs +542 -0
- package/crates/team-agent/src/messaging/helpers.rs +209 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +340 -0
- package/crates/team-agent/src/messaging/mod.rs +147 -0
- package/crates/team-agent/src/messaging/peers.rs +32 -0
- package/crates/team-agent/src/messaging/results.rs +537 -0
- package/crates/team-agent/src/messaging/scheduler.rs +344 -0
- package/crates/team-agent/src/messaging/selftest.rs +100 -0
- package/crates/team-agent/src/messaging/send.rs +582 -0
- package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
- package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
- package/crates/team-agent/src/messaging/trust.rs +192 -0
- package/crates/team-agent/src/messaging/types.rs +355 -0
- package/crates/team-agent/src/messaging/watchers.rs +591 -0
- package/crates/team-agent/src/model/enums.rs +311 -0
- package/crates/team-agent/src/model/errors.rs +17 -0
- package/crates/team-agent/src/model/ids.rs +155 -0
- package/crates/team-agent/src/model/mod.rs +22 -0
- package/crates/team-agent/src/model/paths.rs +228 -0
- package/crates/team-agent/src/model/permissions.rs +567 -0
- package/crates/team-agent/src/model/routing.rs +340 -0
- package/crates/team-agent/src/model/spec.rs +680 -0
- package/crates/team-agent/src/model/task_graph.rs +380 -0
- package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
- package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
- package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
- package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
- package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
- package/crates/team-agent/src/model/yaml/tests.rs +288 -0
- package/crates/team-agent/src/model/yaml.rs +800 -0
- package/crates/team-agent/src/packaging/install.rs +305 -0
- package/crates/team-agent/src/packaging/migrate.rs +30 -0
- package/crates/team-agent/src/packaging/mod.rs +82 -0
- package/crates/team-agent/src/packaging/repair.rs +24 -0
- package/crates/team-agent/src/packaging/tests.rs +829 -0
- package/crates/team-agent/src/packaging/types.rs +369 -0
- package/crates/team-agent/src/provider/adapter.rs +801 -0
- package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
- package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
- package/crates/team-agent/src/provider/classify.rs +456 -0
- package/crates/team-agent/src/provider/faults.rs +136 -0
- package/crates/team-agent/src/provider/helpers.rs +41 -0
- package/crates/team-agent/src/provider/mod.rs +53 -0
- package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
- package/crates/team-agent/src/provider/tests/classify.rs +240 -0
- package/crates/team-agent/src/provider/tests/faults.rs +120 -0
- package/crates/team-agent/src/provider/tests/idle.rs +208 -0
- package/crates/team-agent/src/provider/tests/wire.rs +213 -0
- package/crates/team-agent/src/provider/tests.rs +31 -0
- package/crates/team-agent/src/provider/types.rs +424 -0
- package/crates/team-agent/src/state/identity.rs +656 -0
- package/crates/team-agent/src/state/mod.rs +58 -0
- package/crates/team-agent/src/state/owner_gate.rs +423 -0
- package/crates/team-agent/src/state/persist.rs +712 -0
- package/crates/team-agent/src/state/projection.rs +657 -0
- package/crates/team-agent/src/state/selector.rs +105 -0
- package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +586 -0
- package/crates/team-agent/src/tmux_backend.rs +758 -0
- package/crates/team-agent/src/transport/test_support.rs +252 -0
- package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
- package/crates/team-agent/src/transport/tests/mod.rs +199 -0
- package/crates/team-agent/src/transport/tests/wire.rs +527 -0
- package/crates/team-agent/src/transport.rs +774 -0
- package/npm/install.mjs +90 -106
- package/package.json +15 -13
- package/crates/team-agent-core/Cargo.toml +0 -12
- package/crates/team-agent-core/src/lib.rs +0 -332
- package/crates/team-agent-core/src/main.rs +0 -152
- package/pyproject.toml +0 -18
- package/scripts/install.py +0 -88
- package/scripts/run_regression_tests.py +0 -83
- package/src/team_agent/__init__.py +0 -3
- package/src/team_agent/__main__.py +0 -5
- package/src/team_agent/_legacy_pane_discovery.py +0 -186
- package/src/team_agent/abnormal_track.py +0 -253
- package/src/team_agent/approvals/__init__.py +0 -65
- package/src/team_agent/approvals/constants.py +0 -6
- package/src/team_agent/approvals/parsing.py +0 -176
- package/src/team_agent/approvals/runtime_prompts.py +0 -171
- package/src/team_agent/approvals/status.py +0 -176
- package/src/team_agent/cli/__init__.py +0 -137
- package/src/team_agent/cli/commands.py +0 -481
- package/src/team_agent/cli/e2e.py +0 -202
- package/src/team_agent/cli/helpers.py +0 -226
- package/src/team_agent/cli/parser.py +0 -540
- package/src/team_agent/compiler.py +0 -334
- package/src/team_agent/coordinator/__init__.py +0 -53
- package/src/team_agent/coordinator/__main__.py +0 -83
- package/src/team_agent/coordinator/lifecycle.py +0 -363
- package/src/team_agent/coordinator/metadata.py +0 -61
- package/src/team_agent/coordinator/paths.py +0 -17
- package/src/team_agent/diagnose/__init__.py +0 -48
- package/src/team_agent/diagnose/checks.py +0 -101
- package/src/team_agent/diagnose/comms.py +0 -213
- package/src/team_agent/diagnose/health.py +0 -241
- package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
- package/src/team_agent/diagnose/preflight.py +0 -194
- package/src/team_agent/diagnose/quick_start.py +0 -324
- package/src/team_agent/display/__init__.py +0 -92
- package/src/team_agent/display/adaptive.py +0 -511
- package/src/team_agent/display/backend.py +0 -46
- package/src/team_agent/display/close.py +0 -154
- package/src/team_agent/display/ghostty.py +0 -77
- package/src/team_agent/display/rebuild.py +0 -102
- package/src/team_agent/display/tiling.py +0 -156
- package/src/team_agent/display/worker_window.py +0 -114
- package/src/team_agent/display/workspace.py +0 -382
- package/src/team_agent/errors.py +0 -10
- package/src/team_agent/events.py +0 -84
- package/src/team_agent/fake_worker.py +0 -80
- package/src/team_agent/idle_predicate.py +0 -200
- package/src/team_agent/idle_takeover.py +0 -59
- package/src/team_agent/idle_takeover_wiring.py +0 -111
- package/src/team_agent/launch/__init__.py +0 -41
- package/src/team_agent/launch/bootstrap.py +0 -85
- package/src/team_agent/launch/config.py +0 -106
- package/src/team_agent/launch/core.py +0 -301
- package/src/team_agent/launch/requirements.py +0 -57
- package/src/team_agent/leader/__init__.py +0 -926
- package/src/team_agent/leader_binding.py +0 -183
- package/src/team_agent/lifecycle/__init__.py +0 -5
- package/src/team_agent/lifecycle/agents.py +0 -278
- package/src/team_agent/lifecycle/operations.py +0 -411
- package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
- package/src/team_agent/lifecycle/start.py +0 -363
- package/src/team_agent/mcp_server/__init__.py +0 -42
- package/src/team_agent/mcp_server/__main__.py +0 -7
- package/src/team_agent/mcp_server/contracts.py +0 -148
- package/src/team_agent/mcp_server/normalize.py +0 -257
- package/src/team_agent/mcp_server/server.py +0 -150
- package/src/team_agent/mcp_server/tools.py +0 -352
- package/src/team_agent/message_store/__init__.py +0 -23
- package/src/team_agent/message_store/agent_health.py +0 -113
- package/src/team_agent/message_store/core.py +0 -497
- package/src/team_agent/message_store/leader_notification_log.py +0 -198
- package/src/team_agent/message_store/result_watchers.py +0 -251
- package/src/team_agent/message_store/schema.py +0 -308
- package/src/team_agent/message_store/schema_migration.py +0 -448
- package/src/team_agent/messaging/__init__.py +0 -1
- package/src/team_agent/messaging/activity_detector.py +0 -254
- package/src/team_agent/messaging/delivery.py +0 -473
- package/src/team_agent/messaging/deps.py +0 -247
- package/src/team_agent/messaging/idle_alerts.py +0 -423
- package/src/team_agent/messaging/internal_delivery.py +0 -46
- package/src/team_agent/messaging/leader.py +0 -497
- package/src/team_agent/messaging/leader_api_errors.py +0 -216
- package/src/team_agent/messaging/leader_panes.py +0 -673
- package/src/team_agent/messaging/owner_bypass.py +0 -29
- package/src/team_agent/messaging/result_delivery.py +0 -539
- package/src/team_agent/messaging/results.py +0 -447
- package/src/team_agent/messaging/scheduler.py +0 -450
- package/src/team_agent/messaging/send.py +0 -532
- package/src/team_agent/messaging/session_drift.py +0 -94
- package/src/team_agent/messaging/tmux_io.py +0 -506
- package/src/team_agent/messaging/tmux_prompt.py +0 -338
- package/src/team_agent/messaging/trust_auto_answer.py +0 -52
- package/src/team_agent/orchestrator/__init__.py +0 -376
- package/src/team_agent/orchestrator/plan.py +0 -122
- package/src/team_agent/orchestrator/state.py +0 -128
- package/src/team_agent/paths.py +0 -45
- package/src/team_agent/permissions.py +0 -123
- package/src/team_agent/profiles/__init__.py +0 -82
- package/src/team_agent/profiles/constants.py +0 -19
- package/src/team_agent/profiles/core.py +0 -407
- package/src/team_agent/profiles/helpers.py +0 -69
- package/src/team_agent/profiles/provider_env.py +0 -188
- package/src/team_agent/profiles/smoke.py +0 -201
- package/src/team_agent/provider_cli/__init__.py +0 -43
- package/src/team_agent/provider_cli/adapter.py +0 -172
- package/src/team_agent/provider_cli/base.py +0 -48
- package/src/team_agent/provider_cli/claude.py +0 -457
- package/src/team_agent/provider_cli/codex.py +0 -336
- package/src/team_agent/provider_cli/copilot.py +0 -8
- package/src/team_agent/provider_cli/fake.py +0 -39
- package/src/team_agent/provider_cli/gemini.py +0 -95
- package/src/team_agent/provider_cli/opencode.py +0 -8
- package/src/team_agent/provider_cli/prompt.py +0 -62
- package/src/team_agent/provider_cli/registry.py +0 -18
- package/src/team_agent/provider_cli/unsupported.py +0 -32
- package/src/team_agent/provider_state/README.md +0 -78
- package/src/team_agent/provider_state/__init__.py +0 -86
- package/src/team_agent/provider_state/claude.py +0 -86
- package/src/team_agent/provider_state/codex.py +0 -84
- package/src/team_agent/provider_state/common.py +0 -207
- package/src/team_agent/provider_state/registry.py +0 -118
- package/src/team_agent/providers.py +0 -163
- package/src/team_agent/quality_gates.py +0 -104
- package/src/team_agent/restart/__init__.py +0 -34
- package/src/team_agent/restart/orchestration.py +0 -554
- package/src/team_agent/restart/selection.py +0 -89
- package/src/team_agent/restart/snapshot.py +0 -70
- package/src/team_agent/routing.py +0 -84
- package/src/team_agent/runtime.py +0 -1239
- package/src/team_agent/rust_core.py +0 -327
- package/src/team_agent/sessions/__init__.py +0 -25
- package/src/team_agent/sessions/capture.py +0 -143
- package/src/team_agent/sessions/inventory.py +0 -44
- package/src/team_agent/sessions/resume.py +0 -135
- package/src/team_agent/simple_yaml.py +0 -236
- package/src/team_agent/spec.py +0 -370
- package/src/team_agent/state.py +0 -602
- package/src/team_agent/status/__init__.py +0 -63
- package/src/team_agent/status/approvals.py +0 -52
- package/src/team_agent/status/compact.py +0 -158
- package/src/team_agent/status/constants.py +0 -18
- package/src/team_agent/status/inbox.py +0 -58
- package/src/team_agent/status/peek.py +0 -117
- package/src/team_agent/status/queries.py +0 -199
- package/src/team_agent/task_graph.py +0 -80
- package/src/team_agent/terminal.py +0 -57
- package/src/team_agent/wake.py +0 -58
- package/src/team_agent/watch/__init__.py +0 -145
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
use super::launch_spawn::{
|
|
3
|
+
quick_start_team_dir, seed_healthy_coordinator, DELEG_ROLE_ALPHA, DELEG_ROLE_BRAVO,
|
|
4
|
+
QS_VALID_ROLE,
|
|
5
|
+
};
|
|
6
|
+
use crate::transport::test_support::OfflineTransport;
|
|
7
|
+
|
|
8
|
+
/// A no-owner workspace (= self-contained team dir) with a compiled 2-agent spec (alpha, bravo) + state
|
|
9
|
+
/// listing both at `status`. ensure_owner_allowed passes (no team_owner); load_spec finds alpha/bravo.
|
|
10
|
+
pub(super) fn lanea_team_ws(status: &str) -> PathBuf {
|
|
11
|
+
let ws = temp_ws().join("laneateam");
|
|
12
|
+
std::fs::create_dir_all(ws.join("agents")).unwrap();
|
|
13
|
+
std::fs::write(ws.join("TEAM.md"), "---\nname: laneateam\nobjective: Lane A probe.\nprovider: codex\n---\n\nteam.\n").unwrap();
|
|
14
|
+
std::fs::write(ws.join("agents").join("alpha.md"), DELEG_ROLE_ALPHA).unwrap();
|
|
15
|
+
std::fs::write(ws.join("agents").join("bravo.md"), DELEG_ROLE_BRAVO).unwrap();
|
|
16
|
+
let spec = crate::compiler::compile_team(&ws).expect("compile lane-A team");
|
|
17
|
+
// Re-point routing/tasks at the STAYING agent `bravo` so removing `alpha` validates cleanly (golden
|
|
18
|
+
// remove_agent removes from agents+startup_order then validate_spec raises on dangling refs — a routed
|
|
19
|
+
// agent is not removable in golden; alpha here stands in for an unrouted/dynamic worker). See lanea_ws_agents.
|
|
20
|
+
let yaml = crate::model::yaml::dumps(&spec)
|
|
21
|
+
.replace("default_assignee: \"alpha\"", "default_assignee: \"bravo\"")
|
|
22
|
+
.replace("assign_to: \"alpha\"", "assign_to: \"bravo\"")
|
|
23
|
+
.replace("assignee: \"alpha\"", "assignee: \"bravo\"");
|
|
24
|
+
assert!(!yaml.contains("default_assignee: \"alpha\""), "fixture unroute: default_assignee still alpha");
|
|
25
|
+
assert!(!yaml.contains("assign_to: \"alpha\""), "fixture unroute: a routing rule still assign_to alpha");
|
|
26
|
+
assert!(!yaml.contains("assignee: \"alpha\""), "fixture unroute: task still assignee alpha");
|
|
27
|
+
std::fs::write(ws.join("team.spec.yaml"), yaml).unwrap();
|
|
28
|
+
crate::state::persist::save_runtime_state(
|
|
29
|
+
&ws,
|
|
30
|
+
&json!({
|
|
31
|
+
"session_name": "team-laneateam",
|
|
32
|
+
"agents": {
|
|
33
|
+
"alpha": {"status": status, "provider": "codex", "window": "alpha"},
|
|
34
|
+
"bravo": {"status": status, "provider": "codex", "window": "bravo"}
|
|
35
|
+
}
|
|
36
|
+
}),
|
|
37
|
+
)
|
|
38
|
+
.unwrap();
|
|
39
|
+
ws
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// remove_agent [P0] — from_spec + force on a NON-running agent atomically removes it from state.agents
|
|
43
|
+
// (golden agents.py: pop agents[agent_id] + save). Pure fs/state (non-running -> no stop/tmux). Today the
|
|
44
|
+
// stub returns OwnerRefused and removes nothing -> RED.
|
|
45
|
+
#[test]
|
|
46
|
+
fn lanea_remove_agent_from_spec_force_removes_from_state() {
|
|
47
|
+
let ws = lanea_team_ws("stopped"); // non-running -> the running+force stop branch is skipped (no tmux)
|
|
48
|
+
let _ = remove_agent(&ws, &aid("alpha"), true, true, None); // from_spec=true, force=true
|
|
49
|
+
let state = crate::state::persist::load_runtime_state(&ws).expect("load state");
|
|
50
|
+
let agents = state.get("agents").and_then(serde_json::Value::as_object);
|
|
51
|
+
assert!(
|
|
52
|
+
agents.is_some_and(|a| !a.contains_key("alpha")),
|
|
53
|
+
"remove_agent(from_spec, force) must atomically remove 'alpha' from state.agents (golden agents.py); \
|
|
54
|
+
the stub returns OwnerRefused and removes nothing; state.agents={agents:?}"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// remove_agent [P1] — a RUNNING agent removed without --force is RefusedForceRequired (golden
|
|
59
|
+
// agents.py:56; _is_running is status-based for status in {running,busy} -> no tmux). Today the stub
|
|
60
|
+
// returns OwnerRefused (a different, wrong refusal) -> RED.
|
|
61
|
+
#[test]
|
|
62
|
+
fn lanea_remove_agent_running_without_force_is_refused_force_required() {
|
|
63
|
+
let ws = lanea_team_ws("running"); // alpha is running (status-based _is_running -> true, no tmux)
|
|
64
|
+
match remove_agent(&ws, &aid("alpha"), true, false, None) {
|
|
65
|
+
Ok(RemoveAgentOutcome::RefusedForceRequired { agent_id }) => assert_eq!(agent_id, aid("alpha")),
|
|
66
|
+
other => panic!(
|
|
67
|
+
"a RUNNING agent removed without --force must be RefusedForceRequired (golden agents.py:56); \
|
|
68
|
+
the stub returns OwnerRefused; got {other:?}"
|
|
69
|
+
),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// stop_agent [P0] — an UNKNOWN agent id (past the owner gate) raises "unknown worker agent id: <id>"
|
|
74
|
+
// (golden operations.py:73), NOT OwnerRefused. The owner gate passes on a no-owner ws, then _find_agent
|
|
75
|
+
// fails. The check precedes any tmux. Today the stub returns OwnerRefused (never loads the spec) -> RED.
|
|
76
|
+
#[test]
|
|
77
|
+
fn lanea_stop_agent_unknown_agent_is_unknown_worker_not_owner_refused() {
|
|
78
|
+
let ws = lanea_team_ws("stopped");
|
|
79
|
+
let text = format!("{:?}", stop_agent(&ws, &aid("ghost"), None));
|
|
80
|
+
assert!(
|
|
81
|
+
text.contains("unknown worker"),
|
|
82
|
+
"stop_agent past the owner gate must raise 'unknown worker agent id: ghost' for an unknown agent \
|
|
83
|
+
(golden operations.py:73), not OwnerRefused; got {text}"
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// fork_agent [P0] — precedence: an UNKNOWN source is rejected as "unknown worker agent id" BEFORE the
|
|
88
|
+
// source-session-id check (golden operations.py:284-25). Today the stub always returns "source session_id
|
|
89
|
+
// is missing" (never loads the spec) -> RED.
|
|
90
|
+
#[test]
|
|
91
|
+
fn lanea_fork_agent_unknown_source_is_unknown_worker_before_session_check() {
|
|
92
|
+
let ws = lanea_team_ws("stopped");
|
|
93
|
+
let text = format!("{:?}", fork_agent(&ws, &aid("ghost"), &aid("newfork"), false, None));
|
|
94
|
+
assert!(
|
|
95
|
+
text.contains("unknown worker"),
|
|
96
|
+
"fork_agent must reject an UNKNOWN source as 'unknown worker agent id: ghost' BEFORE the session-id \
|
|
97
|
+
check (golden precedence); the stub returns 'source session_id is missing'; got {text}"
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// fork_agent [P0] — precedence: a DUPLICATE fork-target id is rejected as "agent id already exists" BEFORE
|
|
102
|
+
// the source-session-id check (golden operations.py:284-19, the FIRST guard). Today the stub returns
|
|
103
|
+
// "source session_id is missing" -> RED.
|
|
104
|
+
#[test]
|
|
105
|
+
fn lanea_fork_agent_duplicate_target_is_already_exists_before_session_check() {
|
|
106
|
+
let ws = lanea_team_ws("stopped");
|
|
107
|
+
// target 'bravo' already exists in the spec -> duplicate; source 'alpha' exists (its session_id is irrelevant).
|
|
108
|
+
let text = format!("{:?}", fork_agent(&ws, &aid("alpha"), &aid("bravo"), false, None));
|
|
109
|
+
assert!(
|
|
110
|
+
text.contains("already exists"),
|
|
111
|
+
"fork_agent must reject a DUPLICATE target 'bravo' as 'agent id already exists' BEFORE the session-id \
|
|
112
|
+
check (golden precedence, the first guard); the stub returns 'source session_id is missing'; got {text}"
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// SEAM-GATED real-machine boundary (note to porter): stop_agent / reset_agent / fork_agent must drive a
|
|
117
|
+
// transport (kill_window / re-spawn / native-fork spawn). The porter adds stop_agent_with_transport /
|
|
118
|
+
// reset_agent_with_transport / fork_agent_with_transport(.., &dyn Transport) (mirror restart_with_transport)
|
|
119
|
+
// so kill_window / spawn are assertable in-process via a recording transport. Until then the PUBLIC fns
|
|
120
|
+
// hit real tmux -> #[ignore]. This documents the stop_agent kill_window + mark-stopped observable.
|
|
121
|
+
#[test]
|
|
122
|
+
#[ignore = "real-machine: stop_agent kills the real tmux window. PORTER SEAM: stop_agent_with_transport(.., \
|
|
123
|
+
&dyn Transport) so kill_window(session:window) + agents[a].status='stopped' is assertable in-process."]
|
|
124
|
+
fn lanea_stop_agent_kills_window_and_marks_stopped() {
|
|
125
|
+
let ws = lanea_team_ws("running");
|
|
126
|
+
let _ = stop_agent(&ws, &aid("alpha"), None); // real machine: tmux kill-window team-laneateam:alpha
|
|
127
|
+
let state = crate::state::persist::load_runtime_state(&ws).expect("load state");
|
|
128
|
+
let status = state.pointer("/agents/alpha/status").and_then(serde_json::Value::as_str);
|
|
129
|
+
assert_eq!(status, Some("stopped"), "stop_agent must mark agents[alpha].status='stopped' after killing the window");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
133
|
+
// rt-host-a P1 — `add-agent w2` recompiles + spawns w2 but does NOT JOIN it to the LIVE team: (a) the
|
|
134
|
+
// running roster (state.agents at team_workspace) stays ['w1'] — add_agent upserts w2 into the team-DIR
|
|
135
|
+
// workspace, not the running workspace; (b) w2 is spawned detached / not spawn_into the existing team
|
|
136
|
+
// session. Coordinator can't deliver to w2 -> `send w2` never round-trips. Golden lifecycle/operations.py:
|
|
137
|
+
// add_agent -> start_agent(allow_fresh) spawns INTO the team session + the agent is in runtime state.
|
|
138
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
139
|
+
|
|
140
|
+
// RED — add_agent_with_transport over a seeded RUNNING team must JOIN w2: (1) the RUNNING roster
|
|
141
|
+
// (load_runtime_state(team_workspace)) gains "w2", AND (2) the transport records a spawn_INTO (not
|
|
142
|
+
// spawn_first) so w2 joins the existing team session. Today the roster stays ['w1'] (w2 written to the
|
|
143
|
+
// wrong workspace) and start_agent_with_transport is a stub (zero spawns) -> RED. OS-safe: recording
|
|
144
|
+
// transport (no real tmux) + seeded-healthy-coordinator (start_coordinator AlreadyRunning).
|
|
145
|
+
#[test]
|
|
146
|
+
fn add_agent_joins_w2_into_running_roster_and_existing_session() {
|
|
147
|
+
let team_dir = quick_start_team_dir(QS_VALID_ROLE); // <base>/teamdir (agents/implementer.md)
|
|
148
|
+
let workspace = team_dir.parent().expect("team_workspace(team_dir) = parent"); // the RUNNING team's workspace
|
|
149
|
+
// a RUNNING team already in the session (roster = ['w1'], session_name set).
|
|
150
|
+
crate::state::persist::save_runtime_state(
|
|
151
|
+
workspace,
|
|
152
|
+
&json!({
|
|
153
|
+
"session_name": "team-implteam",
|
|
154
|
+
"agents": {"w1": {"status": "running", "provider": "codex", "window": "w1"}}
|
|
155
|
+
}),
|
|
156
|
+
)
|
|
157
|
+
.unwrap();
|
|
158
|
+
seed_healthy_coordinator(workspace); // start_coordinator -> AlreadyRunning (no real daemon)
|
|
159
|
+
// the new agent's role file, OUTSIDE agents/ so it's not a duplicate of an existing agent.
|
|
160
|
+
let role_file = team_dir.join("w2-role.md");
|
|
161
|
+
std::fs::write(
|
|
162
|
+
&role_file,
|
|
163
|
+
"---\nname: w2\nrole: Worker Two\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nWorker two.\n",
|
|
164
|
+
)
|
|
165
|
+
.unwrap();
|
|
166
|
+
let transport = OfflineTransport::new().with_session_present(true);
|
|
167
|
+
|
|
168
|
+
let _ = add_agent_with_transport(team_dir.as_path(), &aid("w2"), &role_file, false, None, &transport);
|
|
169
|
+
|
|
170
|
+
// (1) [load-bearing] the RUNNING roster (team_workspace) must now contain w2.
|
|
171
|
+
let state = crate::state::persist::load_runtime_state(workspace).expect("load running state");
|
|
172
|
+
assert!(
|
|
173
|
+
state.pointer("/agents/w2").is_some(),
|
|
174
|
+
"add-agent must JOIN w2 to the RUNNING roster (state.agents at team_workspace); today w2 is upserted \
|
|
175
|
+
into the team-DIR workspace, not the running workspace, so the roster stays ['w1'] and the \
|
|
176
|
+
coordinator can't deliver to w2; running agents={:?}",
|
|
177
|
+
state.get("agents")
|
|
178
|
+
);
|
|
179
|
+
// (2) w2 must spawn_INTO the existing team session (not a detached/new session via spawn_first).
|
|
180
|
+
let recorded = transport.spawn_records();
|
|
181
|
+
assert!(
|
|
182
|
+
recorded.iter().any(|(kind, _)| kind == "spawn_into"),
|
|
183
|
+
"add-agent must spawn w2 INTO the existing team session (spawn_into); today start_agent_with_transport \
|
|
184
|
+
is a stub -> ZERO spawns recorded; got {recorded:?}"
|
|
185
|
+
);
|
|
186
|
+
assert!(
|
|
187
|
+
!recorded.iter().any(|(kind, _)| kind == "spawn_first"),
|
|
188
|
+
"w2 must NOT create a NEW session (spawn_first) — that's the detached-spawn bug; got {recorded:?}"
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// REAL-MACHINE e2e boundary (rt-host-a verifies): after `add-agent w2`, the coordinator delivers
|
|
193
|
+
// `send w2` and w2 reports a result (full round-trip). #[ignore] — needs a live tmux session + worker.
|
|
194
|
+
#[test]
|
|
195
|
+
#[ignore = "real-machine: add-agent then send w2 round-trips"]
|
|
196
|
+
fn add_agent_then_send_w2_round_trips() {
|
|
197
|
+
// The framework asserts: add-agent w2 -> w2 in the live session + roster -> `send w2 <msg>` ->
|
|
198
|
+
// coordinator delivers -> w2 emits a result_envelope (results row). Not unit-testable in-process.
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#[derive(Clone)]
|
|
202
|
+
struct SocketRecordingRunner {
|
|
203
|
+
recorded: std::sync::Arc<std::sync::Mutex<Vec<Vec<String>>>>,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
impl crate::tmux_backend::CommandRunner for SocketRecordingRunner {
|
|
207
|
+
fn run(&self, argv: &[String]) -> Result<crate::tmux_backend::CommandOutput, std::io::Error> {
|
|
208
|
+
self.recorded.lock().unwrap().push(argv.to_vec());
|
|
209
|
+
Ok(crate::tmux_backend::CommandOutput {
|
|
210
|
+
success: true,
|
|
211
|
+
code: Some(0),
|
|
212
|
+
stdout: String::new(),
|
|
213
|
+
stderr: String::new(),
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
fn socket_for_workspace(workspace: &std::path::Path) -> String {
|
|
219
|
+
use crate::transport::Transport as _;
|
|
220
|
+
let recorded = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
|
221
|
+
let runner = SocketRecordingRunner { recorded: std::sync::Arc::clone(&recorded) };
|
|
222
|
+
let backend = crate::tmux_backend::TmuxBackend::with_runner_for_workspace(
|
|
223
|
+
Box::new(runner),
|
|
224
|
+
workspace,
|
|
225
|
+
);
|
|
226
|
+
backend.has_session(&sess("team-implteam")).unwrap();
|
|
227
|
+
let socket = recorded.lock().unwrap()[0][2].clone();
|
|
228
|
+
socket
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// BUG A / CP-1: public lifecycle handlers such as stop_agent receive either the run workspace or
|
|
232
|
+
// the team dir, but the daemon is bound to the run workspace socket. A team-dir raw for_workspace
|
|
233
|
+
// derives a different -L socket and makes stop_agent silently see no windows.
|
|
234
|
+
#[test]
|
|
235
|
+
fn bug_a_team_dir_lifecycle_socket_matches_daemon_run_workspace_socket() {
|
|
236
|
+
let run_ws = temp_ws();
|
|
237
|
+
std::fs::create_dir_all(crate::model::paths::runtime_dir(&run_ws)).unwrap();
|
|
238
|
+
let team_dir = run_ws.join("agents");
|
|
239
|
+
std::fs::create_dir_all(&team_dir).unwrap();
|
|
240
|
+
|
|
241
|
+
let daemon_socket = socket_for_workspace(&run_ws);
|
|
242
|
+
let lifecycle_ws = crate::lifecycle::restart::lifecycle_run_workspace(&team_dir).unwrap();
|
|
243
|
+
let stop_socket = socket_for_workspace(&lifecycle_ws);
|
|
244
|
+
assert_eq!(
|
|
245
|
+
stop_socket, daemon_socket,
|
|
246
|
+
"team-dir lifecycle ops must derive the same -L socket as the daemon run workspace"
|
|
247
|
+
);
|
|
248
|
+
assert_ne!(
|
|
249
|
+
socket_for_workspace(&team_dir),
|
|
250
|
+
daemon_socket,
|
|
251
|
+
"regression guard: raw team_dir for_workspace is the buggy socket"
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
#[test]
|
|
256
|
+
#[ignore = "real-machine: stop_agent with a team-dir input kills an existing worker window on the daemon -L socket"]
|
|
257
|
+
fn bug_a_stop_agent_team_dir_input_kills_existing_window_real_machine() {
|
|
258
|
+
let team_dir = quick_start_team_dir(QS_VALID_ROLE);
|
|
259
|
+
let report = stop_agent(&team_dir, &aid("w1"), None).expect("real stop-agent");
|
|
260
|
+
assert!(report.stopped, "existing worker window must be killed, not silently reported absent");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
264
|
+
// WAVE-2 · LANE A v2 — DEEPENED byte-parity REDs (stop / reset / remove / fork).
|
|
265
|
+
//
|
|
266
|
+
// The shallow 5 lanea_ tests pass, but the port is NOT byte-parity. /tmp/lanea_blockers.json caught
|
|
267
|
+
// 15 CONFIRMED blockers + 2 warns. These lock the GOLDEN observable for each.
|
|
268
|
+
// Golden (truth source): lifecycle/operations.py (stop:62 / reset:102 / fork:284),
|
|
269
|
+
// lifecycle/agents.py (remove:22 + _RemoveRollback + _is_running + _find_worker),
|
|
270
|
+
// runtime.py:1023 _tmux_window_exists, display/close.py (close_ghostty_workspace_slot:51).
|
|
271
|
+
//
|
|
272
|
+
// Transport-driven via LaneTransport (records kill_window + spawns; list_windows/list_targets answer
|
|
273
|
+
// from a configurable window set = golden's _tmux_window_exists primitive). OS-safe (no real tmux;
|
|
274
|
+
// seeded-healthy coordinator where start_coordinator is reached). Rollback-internal bits that need a
|
|
275
|
+
// production failure-injection seam (agent_health re-upsert; fork post-spawn arms) are #[ignore]
|
|
276
|
+
|
|
277
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
278
|
+
// Wave3 EVENT PAYLOAD BYTE-LOCK — lifecycle verbs must write golden's events.jsonl payloads.
|
|
279
|
+
// events.jsonl = json.dumps({ts, event, **fields}, sort_keys=True) (event_log.rs:4 / golden events.py:35)
|
|
280
|
+
// → byte form has ALPHABETICALLY-sorted keys (NOT insertion order; that is state.json's rule). So the
|
|
281
|
+
// byte-lock = event name + field KEY SET + field VALUES (ts is a live timestamp → tolerated; order is
|
|
282
|
+
// sort_keys-deterministic given keys+values). Rust restart/remove.rs + restart/agent.rs write ZERO
|
|
283
|
+
// lifecycle events (crate-wide grep: the names exist in types.rs but are never emitted) -> RED.
|
|
284
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
285
|
+
|
|
286
|
+
// Read every events.jsonl the verb could write to (run-workspace resolves to either the team dir or its
|
|
287
|
+
// parent; read both so the lock is robust to that resolution).
|
|
288
|
+
fn lifecycle_events(ws: &std::path::Path) -> Vec<serde_json::Value> {
|
|
289
|
+
let mut out = crate::event_log::EventLog::new(ws).tail(0).unwrap_or_default();
|
|
290
|
+
if let Ok(parent) = crate::model::paths::team_workspace(ws) {
|
|
291
|
+
if parent != ws {
|
|
292
|
+
out.extend(crate::event_log::EventLog::new(&parent).tail(0).unwrap_or_default());
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
out
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
fn payload_keys(event: &serde_json::Value) -> std::collections::BTreeSet<String> {
|
|
299
|
+
event
|
|
300
|
+
.as_object()
|
|
301
|
+
.map(|o| o.keys().filter(|k| *k != "ts" && *k != "event").cloned().collect())
|
|
302
|
+
.unwrap_or_default()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
fn find_event<'a>(events: &'a [serde_json::Value], name: &str) -> Option<&'a serde_json::Value> {
|
|
306
|
+
events.iter().find(|e| e.get("event").and_then(|v| v.as_str()) == Some(name))
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
fn names(events: &[serde_json::Value]) -> Vec<String> {
|
|
310
|
+
events.iter().filter_map(|e| e.get("event").and_then(|v| v.as_str()).map(ToString::to_string)).collect()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// remove-agent — golden agents.py:66-147 writes lifecycle.remove_step_completed (per step) +
|
|
314
|
+
// remove_agent.complete. (stopped agent + from_spec + force: no stop step, pure fs/state, no spawn.)
|
|
315
|
+
#[test]
|
|
316
|
+
fn remove_agent_emits_golden_lifecycle_event_payloads() {
|
|
317
|
+
let ws = lanea_team_ws("stopped");
|
|
318
|
+
let _ = remove_agent(&ws, &aid("alpha"), true, true, None); // from_spec=true, force=true
|
|
319
|
+
let events = lifecycle_events(&ws);
|
|
320
|
+
|
|
321
|
+
// remove_agent.complete (golden agents.py:140-147) — EXACT field key set + load-bearing values.
|
|
322
|
+
let complete = find_event(&events, "remove_agent.complete").unwrap_or_else(|| panic!(
|
|
323
|
+
"remove_agent must write `remove_agent.complete` (golden agents.py:140); Rust restart/remove.rs emits NO \
|
|
324
|
+
events. events seen: {:?}", names(&events)
|
|
325
|
+
));
|
|
326
|
+
let expected: std::collections::BTreeSet<String> =
|
|
327
|
+
["agent_id", "from_spec", "force", "stopped", "role_file_removed", "cleared_locations"]
|
|
328
|
+
.iter().map(ToString::to_string).collect();
|
|
329
|
+
assert_eq!(payload_keys(complete), expected,
|
|
330
|
+
"remove_agent.complete payload key set must match golden agents.py:140-147; got {:?}", payload_keys(complete));
|
|
331
|
+
assert_eq!(complete.get("agent_id").and_then(|v| v.as_str()), Some("alpha"));
|
|
332
|
+
assert_eq!(complete.get("from_spec").and_then(|v| v.as_bool()), Some(true));
|
|
333
|
+
assert_eq!(complete.get("force").and_then(|v| v.as_bool()), Some(true));
|
|
334
|
+
|
|
335
|
+
// lifecycle.remove_step_completed (golden agents.py:66-72) — fired per step; key set {agent_id, step,
|
|
336
|
+
// resource}; the workspace_state + agent_health steps fire for a stopped from_spec remove.
|
|
337
|
+
let step_events: Vec<&serde_json::Value> = events.iter()
|
|
338
|
+
.filter(|e| e.get("event").and_then(|v| v.as_str()) == Some("lifecycle.remove_step_completed")).collect();
|
|
339
|
+
let steps: Vec<&str> = step_events.iter().filter_map(|e| e.get("step").and_then(|v| v.as_str())).collect();
|
|
340
|
+
assert!(steps.contains(&"workspace_state") && steps.contains(&"agent_health"),
|
|
341
|
+
"remove_agent must write lifecycle.remove_step_completed for each step (golden agents.py:86,109); got steps {steps:?}");
|
|
342
|
+
let ws_step = step_events.iter().find(|e| e.get("step").and_then(|v| v.as_str()) == Some("workspace_state"))
|
|
343
|
+
.expect("the workspace_state step event");
|
|
344
|
+
let expected_step: std::collections::BTreeSet<String> =
|
|
345
|
+
["agent_id", "step", "resource"].iter().map(ToString::to_string).collect();
|
|
346
|
+
assert_eq!(payload_keys(ws_step), expected_step,
|
|
347
|
+
"lifecycle.remove_step_completed (non-stop step) payload must be EXACTLY {{agent_id, step, resource}} (golden agents.py:68-70)");
|
|
348
|
+
assert_eq!(ws_step.get("resource").and_then(|v| v.as_str()), Some("state.json:agents"),
|
|
349
|
+
"workspace_state step resource == golden 'state.json:agents' (agents.py:82)");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// reset-agent — golden operations.py:123/132 writes discard.session_tombstone {agent_id,
|
|
353
|
+
// discarded_session_id} + reset_agent.complete {agent_id, stopped, started}. OfflineTransport (no spawn).
|
|
354
|
+
#[test]
|
|
355
|
+
fn reset_agent_emits_golden_lifecycle_event_payloads() {
|
|
356
|
+
let ws = lanea_team_ws("running");
|
|
357
|
+
// give alpha a stored session so discard.session_tombstone.discarded_session_id is meaningful (golden operations.py:118).
|
|
358
|
+
let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
359
|
+
state["agents"]["alpha"]["session_id"] = json!("S-alpha");
|
|
360
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
361
|
+
|
|
362
|
+
let transport = OfflineTransport::new().with_session_present(true);
|
|
363
|
+
let _ = crate::lifecycle::reset_agent_with_transport(&ws, &aid("alpha"), true, false, None, &transport);
|
|
364
|
+
let events = lifecycle_events(&ws);
|
|
365
|
+
|
|
366
|
+
// discard.session_tombstone (golden operations.py:123) — EXACT key set + the discarded session id.
|
|
367
|
+
let tombstone = find_event(&events, "discard.session_tombstone").unwrap_or_else(|| panic!(
|
|
368
|
+
"reset_agent must write `discard.session_tombstone` (golden operations.py:123); Rust restart/agent.rs emits \
|
|
369
|
+
NO events. events seen: {:?}", names(&events)
|
|
370
|
+
));
|
|
371
|
+
let expected_tomb: std::collections::BTreeSet<String> =
|
|
372
|
+
["agent_id", "discarded_session_id"].iter().map(ToString::to_string).collect();
|
|
373
|
+
assert_eq!(payload_keys(tombstone), expected_tomb,
|
|
374
|
+
"discard.session_tombstone payload key set must be EXACTLY {{agent_id, discarded_session_id}} (golden operations.py:123)");
|
|
375
|
+
assert_eq!(tombstone.get("agent_id").and_then(|v| v.as_str()), Some("alpha"));
|
|
376
|
+
assert_eq!(tombstone.get("discarded_session_id").and_then(|v| v.as_str()), Some("S-alpha"),
|
|
377
|
+
"discarded_session_id == the agent's stored session_id (golden operations.py:118)");
|
|
378
|
+
|
|
379
|
+
// reset_agent.complete (golden operations.py:132) — EXACT key set {agent_id, stopped, started}.
|
|
380
|
+
let complete = find_event(&events, "reset_agent.complete").unwrap_or_else(|| panic!(
|
|
381
|
+
"reset_agent must write `reset_agent.complete` (golden operations.py:132); events seen: {:?}", names(&events)
|
|
382
|
+
));
|
|
383
|
+
let expected_complete: std::collections::BTreeSet<String> =
|
|
384
|
+
["agent_id", "stopped", "started"].iter().map(ToString::to_string).collect();
|
|
385
|
+
assert_eq!(payload_keys(complete), expected_complete,
|
|
386
|
+
"reset_agent.complete payload key set must be EXACTLY {{agent_id, stopped, started}} (golden operations.py:132)");
|
|
387
|
+
assert_eq!(complete.get("agent_id").and_then(|v| v.as_str()), Some("alpha"));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// stop-agent — golden operations.py:98 writes stop_agent.complete {agent_id, target, stopped}
|
|
391
|
+
// (+ stop_agent.window_stop_failed {agent_id, target, stderr} on kill failure). Rust emits none.
|
|
392
|
+
#[test]
|
|
393
|
+
fn stop_agent_emits_golden_complete_event_payload() {
|
|
394
|
+
let ws = lanea_team_ws("running"); // alpha running (window "alpha")
|
|
395
|
+
let transport = OfflineTransport::new().with_session_present(true);
|
|
396
|
+
let _ = crate::lifecycle::stop_agent_with_transport(&ws, &aid("alpha"), None, &transport);
|
|
397
|
+
let events = lifecycle_events(&ws);
|
|
398
|
+
|
|
399
|
+
let complete = find_event(&events, "stop_agent.complete").unwrap_or_else(|| panic!(
|
|
400
|
+
"stop_agent must write `stop_agent.complete` (golden operations.py:98); Rust restart/agent.rs emits NO \
|
|
401
|
+
events for stop. events seen: {:?}", names(&events)
|
|
402
|
+
));
|
|
403
|
+
let expected: std::collections::BTreeSet<String> =
|
|
404
|
+
["agent_id", "target", "stopped"].iter().map(ToString::to_string).collect();
|
|
405
|
+
assert_eq!(payload_keys(complete), expected,
|
|
406
|
+
"stop_agent.complete payload key set must be EXACTLY {{agent_id, target, stopped}} (golden operations.py:98)");
|
|
407
|
+
assert_eq!(complete.get("agent_id").and_then(|v| v.as_str()), Some("alpha"));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// start-agent — golden start.py:225 writes start_agent.agent_start on the spawn path (9 keys). Rust emits
|
|
411
|
+
// ONLY start_agent.noop (common.rs:315, for the already-running path); the fresh-spawn path emits nothing.
|
|
412
|
+
#[test]
|
|
413
|
+
fn start_agent_emits_golden_agent_start_event_payload() {
|
|
414
|
+
let ws = lanea_team_ws("stopped"); // not running -> fresh spawn path (not noop)
|
|
415
|
+
let transport = OfflineTransport::new();
|
|
416
|
+
let _ = crate::lifecycle::start_agent_with_transport(&ws, &aid("alpha"), true, false, true, None, &transport);
|
|
417
|
+
let events = lifecycle_events(&ws);
|
|
418
|
+
|
|
419
|
+
let agent_start = find_event(&events, "start_agent.agent_start").unwrap_or_else(|| panic!(
|
|
420
|
+
"start_agent (fresh-spawn) must write `start_agent.agent_start` (golden start.py:225); Rust emits only \
|
|
421
|
+
start_agent.noop. events seen: {:?}", names(&events)
|
|
422
|
+
));
|
|
423
|
+
let expected: std::collections::BTreeSet<String> = [
|
|
424
|
+
"agent_id", "provider", "start_mode", "session_id", "session", "window",
|
|
425
|
+
"tmux_start_mode", "command", "mcp_config",
|
|
426
|
+
].iter().map(ToString::to_string).collect();
|
|
427
|
+
assert_eq!(payload_keys(agent_start), expected,
|
|
428
|
+
"start_agent.agent_start payload key set must match golden start.py:225-235 (9 keys)");
|
|
429
|
+
assert_eq!(agent_start.get("agent_id").and_then(|v| v.as_str()), Some("alpha"));
|
|
430
|
+
assert_eq!(agent_start.get("window").and_then(|v| v.as_str()), Some("alpha"),
|
|
431
|
+
"agent_start.window == agent_id (golden start.py:232)");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// restart — golden orchestration.py:507 writes ONE restart.resume_decision per non-paused worker
|
|
435
|
+
// (7 keys, NOTE `worker_id` not `agent_id`). Rust emits no restart events.
|
|
436
|
+
#[test]
|
|
437
|
+
fn restart_emits_golden_resume_decision_event_payload() {
|
|
438
|
+
let ws = lanea_team_ws("running"); // alpha + bravo present -> a resume_decision each
|
|
439
|
+
let transport = OfflineTransport::new().with_session_present(true);
|
|
440
|
+
let _ = crate::lifecycle::restart_with_transport(&ws, true, None, &transport);
|
|
441
|
+
let events = lifecycle_events(&ws);
|
|
442
|
+
|
|
443
|
+
let decisions: Vec<&serde_json::Value> = events.iter()
|
|
444
|
+
.filter(|e| e.get("event").and_then(|v| v.as_str()) == Some("restart.resume_decision")).collect();
|
|
445
|
+
assert!(!decisions.is_empty(), "restart must write one `restart.resume_decision` per non-paused worker \
|
|
446
|
+
(golden orchestration.py:128/507); Rust emits NO restart events. events seen: {:?}", names(&events));
|
|
447
|
+
let expected: std::collections::BTreeSet<String> = [
|
|
448
|
+
"worker_id", "has_first_send_at", "has_session_id", "allow_fresh", "decision", "first_send_at", "session_id",
|
|
449
|
+
].iter().map(ToString::to_string).collect();
|
|
450
|
+
assert_eq!(payload_keys(decisions[0]), expected,
|
|
451
|
+
"restart.resume_decision payload key set must match golden orchestration.py:507-514 (7 keys, `worker_id`)");
|
|
452
|
+
let worker_ids: Vec<&str> = decisions.iter().filter_map(|e| e.get("worker_id").and_then(|v| v.as_str())).collect();
|
|
453
|
+
assert!(worker_ids.contains(&"alpha") || worker_ids.contains(&"bravo"),
|
|
454
|
+
"restart.resume_decision.worker_id must name a real worker (golden); got {worker_ids:?}");
|
|
455
|
+
}
|