@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,586 @@
|
|
|
1
|
+
//! TMUX-BACKEND RED — every `Transport` method is `unimplemented!()` today, so these PANIC (RED)
|
|
2
|
+
//! until the porter wires the bodies + `RealCommandRunner`. The OS edge is mocked by
|
|
3
|
+
//! `MockCommandRunner` (records each argv; returns canned `CommandOutput`/io::Error you stage).
|
|
4
|
+
//! Each test asserts (1) the recorded argv == the golden-locked `transport::tmux_*_argv` builder
|
|
5
|
+
//! (or the golden command form for builder-less ops) and (2) the parsed typed return. Golden:
|
|
6
|
+
//! runtime.py (has-session/spawn/kill), leader/__init__.py:335 (set-environment), state.py:341
|
|
7
|
+
//! (_tmux_pane_liveness three-state, §bug-085 unknown != dead), transport.rs argv-builders.
|
|
8
|
+
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
9
|
+
|
|
10
|
+
use std::collections::{BTreeMap, VecDeque};
|
|
11
|
+
use std::path::Path;
|
|
12
|
+
use std::sync::{Arc, Mutex};
|
|
13
|
+
|
|
14
|
+
use super::{CommandOutput, CommandRunner, RealCommandRunner, TmuxBackend};
|
|
15
|
+
use crate::model::enums::PaneLiveness;
|
|
16
|
+
use crate::transport::{
|
|
17
|
+
normalize_capture, tmux_capture_argv, tmux_query_argv, tmux_send_keys_argv, tmux_spawn_argv,
|
|
18
|
+
AttachOutcome, CaptureRange, InjectPayload, InjectStage, InjectVerification, Key, PaneField,
|
|
19
|
+
PaneId, SessionName, SetEnvOutcome, SubmitVerification, Target, Transport, TransportError,
|
|
20
|
+
TurnVerification, WindowName,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type RecordedArgv = Arc<Mutex<Vec<Vec<String>>>>;
|
|
24
|
+
type RecordedStdin = Arc<Mutex<Vec<String>>>;
|
|
25
|
+
|
|
26
|
+
/// A staged runner response: a canned `CommandOutput`, or an io::Error (kind) for the error path.
|
|
27
|
+
#[derive(Clone)]
|
|
28
|
+
enum MockResp {
|
|
29
|
+
Out(CommandOutput),
|
|
30
|
+
Io(std::io::ErrorKind),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Records every argv it is asked to run; replays staged responses (then a default).
|
|
34
|
+
struct MockCommandRunner {
|
|
35
|
+
recorded: RecordedArgv,
|
|
36
|
+
stdin_recorded: RecordedStdin,
|
|
37
|
+
queue: Mutex<VecDeque<MockResp>>,
|
|
38
|
+
default: MockResp,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
impl CommandRunner for MockCommandRunner {
|
|
42
|
+
fn run(&self, argv: &[String]) -> Result<CommandOutput, std::io::Error> {
|
|
43
|
+
self.recorded.lock().unwrap().push(argv.to_vec());
|
|
44
|
+
let resp = self.queue.lock().unwrap().pop_front().unwrap_or_else(|| self.default.clone());
|
|
45
|
+
match resp {
|
|
46
|
+
MockResp::Out(o) => Ok(o),
|
|
47
|
+
MockResp::Io(kind) => Err(std::io::Error::new(kind, "mock runner io error")),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn run_with_stdin(
|
|
52
|
+
&self,
|
|
53
|
+
argv: &[String],
|
|
54
|
+
stdin: &str,
|
|
55
|
+
) -> Result<CommandOutput, std::io::Error> {
|
|
56
|
+
self.stdin_recorded.lock().unwrap().push(stdin.to_string());
|
|
57
|
+
self.run(argv)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn ok(stdout: &str) -> CommandOutput {
|
|
62
|
+
CommandOutput { success: true, code: Some(0), stdout: stdout.to_string(), stderr: String::new() }
|
|
63
|
+
}
|
|
64
|
+
fn fail(code: i32, stderr: &str) -> CommandOutput {
|
|
65
|
+
CommandOutput { success: false, code: Some(code), stdout: String::new(), stderr: stderr.to_string() }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Build a backend over a mock runner: `default` answers every un-queued call; `queued` is drained
|
|
69
|
+
/// first. Returns the backend + the shared recorded-argv handle (read AFTER the call).
|
|
70
|
+
fn backend_with(default: MockResp, queued: Vec<MockResp>) -> (TmuxBackend, RecordedArgv) {
|
|
71
|
+
let recorded = Arc::new(Mutex::new(Vec::new()));
|
|
72
|
+
let stdin_recorded = Arc::new(Mutex::new(Vec::new()));
|
|
73
|
+
let runner = MockCommandRunner {
|
|
74
|
+
recorded: Arc::clone(&recorded),
|
|
75
|
+
stdin_recorded,
|
|
76
|
+
queue: Mutex::new(queued.into_iter().collect()),
|
|
77
|
+
default,
|
|
78
|
+
};
|
|
79
|
+
(TmuxBackend::with_runner(Box::new(runner)), recorded)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fn backend_with_stdin(
|
|
83
|
+
default: MockResp,
|
|
84
|
+
queued: Vec<MockResp>,
|
|
85
|
+
) -> (TmuxBackend, RecordedArgv, RecordedStdin) {
|
|
86
|
+
let recorded = Arc::new(Mutex::new(Vec::new()));
|
|
87
|
+
let stdin_recorded = Arc::new(Mutex::new(Vec::new()));
|
|
88
|
+
let runner = MockCommandRunner {
|
|
89
|
+
recorded: Arc::clone(&recorded),
|
|
90
|
+
stdin_recorded: Arc::clone(&stdin_recorded),
|
|
91
|
+
queue: Mutex::new(queued.into_iter().collect()),
|
|
92
|
+
default,
|
|
93
|
+
};
|
|
94
|
+
(TmuxBackend::with_runner(Box::new(runner)), recorded, stdin_recorded)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fn svec(items: &[&str]) -> Vec<String> {
|
|
98
|
+
items.iter().map(|s| (*s).to_string()).collect()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── 1. has_session: exit 0 -> true, exit 1 -> false; argv = `tmux has-session -t <s>` ──────────
|
|
102
|
+
#[test]
|
|
103
|
+
fn has_session_argv_and_exit_code_maps_to_bool() {
|
|
104
|
+
let (be, rec) = backend_with(MockResp::Out(ok("")), vec![]);
|
|
105
|
+
assert!(be.has_session(&SessionName::new("sess")).expect("has_session"), "exit 0 -> true");
|
|
106
|
+
assert_eq!(rec.lock().unwrap()[0], svec(&["tmux", "has-session", "-t", "sess"]));
|
|
107
|
+
|
|
108
|
+
let (be, rec) = backend_with(MockResp::Out(fail(1, "can't find session: sess")), vec![]);
|
|
109
|
+
assert!(!be.has_session(&SessionName::new("sess")).expect("has_session"), "exit 1 -> false");
|
|
110
|
+
assert_eq!(rec.lock().unwrap()[0], svec(&["tmux", "has-session", "-t", "sess"]));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── 2. spawn_first / spawn_into frame via tmux_spawn_argv; canned output parses pane id ────────
|
|
114
|
+
#[test]
|
|
115
|
+
fn spawn_first_frames_via_new_session_builder_and_parses_pane_id() {
|
|
116
|
+
let (be, rec) = backend_with(MockResp::Out(ok("%3")), vec![]);
|
|
117
|
+
let s = SessionName::new("teamsess");
|
|
118
|
+
let w = WindowName::new("w1");
|
|
119
|
+
let env = BTreeMap::from([("TEAM_AGENT_ID".to_string(), "w1".to_string())]);
|
|
120
|
+
let result = be
|
|
121
|
+
.spawn_first(&s, &w, &svec(&["provider-bin", "--flag"]), Path::new("/work/dir"), &env)
|
|
122
|
+
.expect("spawn_first");
|
|
123
|
+
let argv = rec.lock().unwrap()[0].clone();
|
|
124
|
+
let cmd = argv.last().expect("the sh -lc command string").clone();
|
|
125
|
+
assert_eq!(
|
|
126
|
+
argv,
|
|
127
|
+
tmux_spawn_argv(&s, &w, &cmd, true),
|
|
128
|
+
"spawn_first must frame via tmux_spawn_argv (new-session -d -s <s> -n <w> sh -lc <cmd>)"
|
|
129
|
+
);
|
|
130
|
+
assert!(cmd.contains("provider-bin"), "the provider argv must be in the sh -lc command; got {cmd}");
|
|
131
|
+
assert_eq!(result.pane_id.as_str(), "%3", "SpawnResult.pane_id must parse from the tmux output");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#[test]
|
|
135
|
+
fn spawn_into_frames_via_new_window_builder() {
|
|
136
|
+
let (be, rec) = backend_with(MockResp::Out(ok("%4")), vec![]);
|
|
137
|
+
let s = SessionName::new("teamsess");
|
|
138
|
+
let w = WindowName::new("w2");
|
|
139
|
+
let result = be
|
|
140
|
+
.spawn_into(&s, &w, &svec(&["provider-bin"]), Path::new("/work/dir"), &BTreeMap::new())
|
|
141
|
+
.expect("spawn_into");
|
|
142
|
+
let argv = rec.lock().unwrap()[0].clone();
|
|
143
|
+
let cmd = argv.last().expect("the sh -lc command string").clone();
|
|
144
|
+
assert_eq!(
|
|
145
|
+
argv,
|
|
146
|
+
tmux_spawn_argv(&s, &w, &cmd, false),
|
|
147
|
+
"spawn_into must frame via tmux_spawn_argv first=false (new-window -t <s> -n <w> sh -lc <cmd>)"
|
|
148
|
+
);
|
|
149
|
+
assert_eq!(result.pane_id.as_str(), "%4");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── 3. set_session_env: argv = `tmux set-environment -t <s> <k> <v>`; success -> Applied ───────
|
|
153
|
+
#[test]
|
|
154
|
+
fn set_session_env_argv_and_applied_outcome() {
|
|
155
|
+
let (be, rec) = backend_with(MockResp::Out(ok("")), vec![]);
|
|
156
|
+
let outcome = be.set_session_env(&SessionName::new("sess"), "KEY", "VAL").expect("set env");
|
|
157
|
+
assert_eq!(rec.lock().unwrap()[0], svec(&["tmux", "set-environment", "-t", "sess", "KEY", "VAL"]));
|
|
158
|
+
assert_eq!(outcome, SetEnvOutcome::Applied, "tmux set-environment success -> SetEnvOutcome::Applied");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── 4. capture: argv = tmux_capture_argv; canned scrollback -> normalize_capture -> CapturedText ─
|
|
162
|
+
#[test]
|
|
163
|
+
fn capture_argv_and_normalizes_scrollback() {
|
|
164
|
+
let scroll = "line one \nbusy\u{a0}marker \n \n";
|
|
165
|
+
let (be, rec) = backend_with(MockResp::Out(ok(scroll)), vec![]);
|
|
166
|
+
let pane = PaneId::new("%7");
|
|
167
|
+
let captured = be
|
|
168
|
+
.capture(&Target::Pane(pane.clone()), CaptureRange::Tail(40))
|
|
169
|
+
.expect("capture");
|
|
170
|
+
assert_eq!(rec.lock().unwrap()[0], tmux_capture_argv(&pane, CaptureRange::Tail(40)));
|
|
171
|
+
assert_eq!(captured.text, normalize_capture(scroll), "capture output must be normalize_capture'd");
|
|
172
|
+
assert_eq!(captured.range, CaptureRange::Tail(40));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── 5a. send_keys: argv = tmux_send_keys_argv ──────────────────────────────────────────────────
|
|
176
|
+
#[test]
|
|
177
|
+
fn send_keys_argv_matches_builder() {
|
|
178
|
+
let (be, rec) = backend_with(MockResp::Out(ok("")), vec![]);
|
|
179
|
+
let pane = PaneId::new("%7");
|
|
180
|
+
be.send_keys(&Target::Pane(pane.clone()), &[Key::Enter]).expect("send_keys");
|
|
181
|
+
assert_eq!(rec.lock().unwrap()[0], tmux_send_keys_argv(&pane, &[Key::Enter]));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── 5b. inject (text): set/load-buffer(text) -> paste-buffer -p -> submit send-keys; report Submit ─
|
|
185
|
+
#[test]
|
|
186
|
+
fn inject_text_runs_buffer_paste_submit_sequence_and_reports_submit() {
|
|
187
|
+
let (be, rec) = backend_with(MockResp::Out(ok("hello")), vec![]);
|
|
188
|
+
let pane = PaneId::new("%7");
|
|
189
|
+
let report = be
|
|
190
|
+
.inject(&Target::Pane(pane.clone()), &InjectPayload::Text("hello".to_string()), Key::Enter, true)
|
|
191
|
+
.expect("inject");
|
|
192
|
+
let calls = rec.lock().unwrap().clone();
|
|
193
|
+
let is = |a: &[String], sub: &str| a.get(1).map(String::as_str) == Some(sub);
|
|
194
|
+
assert!(
|
|
195
|
+
calls.iter().any(|a| (is(a, "set-buffer") || is(a, "load-buffer")) && a.iter().any(|x| x.contains("hello"))),
|
|
196
|
+
"inject must stage the text into a tmux buffer (set-buffer/load-buffer); got {calls:?}"
|
|
197
|
+
);
|
|
198
|
+
assert!(
|
|
199
|
+
calls.iter().any(|a| is(a, "paste-buffer") && a.contains(&"-p".to_string()) && a.contains(&"%7".to_string())),
|
|
200
|
+
"inject must bracketed-paste (-p) the buffer to the pane; got {calls:?}"
|
|
201
|
+
);
|
|
202
|
+
assert!(
|
|
203
|
+
calls.iter().any(|a| is(a, "send-keys") && a.contains(&"Enter".to_string())),
|
|
204
|
+
"inject must send the submit key (Enter) last; got {calls:?}"
|
|
205
|
+
);
|
|
206
|
+
assert_eq!(report.stage_reached, InjectStage::Submit, "a fully-applied inject reaches the Submit stage");
|
|
207
|
+
assert_eq!(report.inject_verification, InjectVerification::NoToken);
|
|
208
|
+
assert_eq!(
|
|
209
|
+
report.submit_verification,
|
|
210
|
+
SubmitVerification::EnterSentWithoutPlaceholderCheck
|
|
211
|
+
);
|
|
212
|
+
assert_eq!(report.turn_verification, TurnVerification::NotYetObserved);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#[test]
|
|
216
|
+
fn inject_large_text_load_buffer_writes_stdin_and_token_report() {
|
|
217
|
+
let (be, rec, stdin_rec) = backend_with_stdin(MockResp::Out(ok("")), vec![]);
|
|
218
|
+
let text = format!("{}{}", "x".repeat(16 * 1024), " [team-agent-token:abc]");
|
|
219
|
+
let report = be
|
|
220
|
+
.inject(&Target::Pane(PaneId::new("%7")), &InjectPayload::Text(text.clone()), Key::Down, true)
|
|
221
|
+
.expect("inject large text");
|
|
222
|
+
|
|
223
|
+
assert_eq!(report.inject_verification, InjectVerification::CaptureContainsToken);
|
|
224
|
+
assert_eq!(
|
|
225
|
+
report.submit_verification,
|
|
226
|
+
SubmitVerification::KeySentAfterVisibleToken { key: Key::Down }
|
|
227
|
+
);
|
|
228
|
+
let calls = rec.lock().unwrap().clone();
|
|
229
|
+
assert_eq!(calls[0], svec(&["tmux", "load-buffer", "-b", "team-agent-send-abc", "-"]));
|
|
230
|
+
assert_eq!(stdin_rec.lock().unwrap()[0], text);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#[test]
|
|
234
|
+
fn send_keys_cancel_mode_queries_mode_and_dispatches_cancel_argv() {
|
|
235
|
+
let (be, rec) = backend_with(
|
|
236
|
+
MockResp::Out(ok("")),
|
|
237
|
+
vec![MockResp::Out(ok("tree-mode\n")), MockResp::Out(ok(""))],
|
|
238
|
+
);
|
|
239
|
+
be.send_keys(&Target::Pane(PaneId::new("%7")), &[Key::CancelMode])
|
|
240
|
+
.expect("cancel mode");
|
|
241
|
+
|
|
242
|
+
let calls = rec.lock().unwrap().clone();
|
|
243
|
+
assert_eq!(
|
|
244
|
+
calls[0],
|
|
245
|
+
svec(&["tmux", "display-message", "-p", "-t", "%7", "#{pane_mode}"])
|
|
246
|
+
);
|
|
247
|
+
assert_eq!(calls[1], svec(&["tmux", "send-keys", "-t", "%7", "q"]));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#[test]
|
|
251
|
+
fn cancel_mode_numeric_zero_is_input_ready_and_does_not_send_cancel() {
|
|
252
|
+
// Golden /tmp/transport_golden_probe.py:
|
|
253
|
+
// `_normalize_pane_mode("0") == ""`; `_prepare_tmux_pane_for_input` returns
|
|
254
|
+
// pane_input_ready and does NOT call `_pane_mode_cancel`.
|
|
255
|
+
// RED: pane_mode_from_raw("0") maps to Unknown, so Rust sends `-X cancel`.
|
|
256
|
+
let (be, rec) = backend_with(
|
|
257
|
+
MockResp::Out(ok("")),
|
|
258
|
+
vec![MockResp::Out(ok("0\n"))],
|
|
259
|
+
);
|
|
260
|
+
be.send_keys(&Target::Pane(PaneId::new("%7")), &[Key::CancelMode])
|
|
261
|
+
.expect("cancel mode input-ready no-op");
|
|
262
|
+
|
|
263
|
+
let calls = rec.lock().unwrap().clone();
|
|
264
|
+
assert_eq!(
|
|
265
|
+
calls,
|
|
266
|
+
vec![svec(&["tmux", "display-message", "-p", "-t", "%7", "#{pane_mode}"])],
|
|
267
|
+
"pane_mode='0' is Python input-ready; CancelMode must stop after the mode query, got {calls:?}"
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#[test]
|
|
272
|
+
fn inject_text_uses_message_id_scoped_buffer_from_token() {
|
|
273
|
+
// Golden delivery.py:109-114 passes buffer_name = `team-agent-send-{message_id}` into
|
|
274
|
+
// `_tmux_inject_text`; tmux_io.py then uses that exact name for set/load, paste, delete.
|
|
275
|
+
// This prevents interleaved sends from sharing a stale global tmux buffer.
|
|
276
|
+
// RED: Rust currently hard-codes `team-agent-buf`.
|
|
277
|
+
let (be, rec) = backend_with(MockResp::Out(ok("")), vec![]);
|
|
278
|
+
let text = "Team Agent message from leader:\n\nhello\n\n[team-agent-token:msg_abc123]".to_string();
|
|
279
|
+
be.inject(&Target::Pane(PaneId::new("%7")), &InjectPayload::Text(text), Key::Enter, true)
|
|
280
|
+
.expect("inject");
|
|
281
|
+
|
|
282
|
+
let calls = rec.lock().unwrap().clone();
|
|
283
|
+
let buffer_args: Vec<String> = calls
|
|
284
|
+
.iter()
|
|
285
|
+
.filter(|argv| matches!(argv.get(1).map(String::as_str), Some("set-buffer" | "load-buffer" | "paste-buffer" | "delete-buffer")))
|
|
286
|
+
.filter_map(|argv| argv.iter().position(|arg| arg == "-b").and_then(|i| argv.get(i + 1)).cloned())
|
|
287
|
+
.collect();
|
|
288
|
+
assert_eq!(
|
|
289
|
+
buffer_args,
|
|
290
|
+
vec![
|
|
291
|
+
"team-agent-send-msg_abc123".to_string(),
|
|
292
|
+
"team-agent-send-msg_abc123".to_string(),
|
|
293
|
+
"team-agent-send-msg_abc123".to_string(),
|
|
294
|
+
],
|
|
295
|
+
"every tmux buffer operation must use the message-id-scoped golden buffer name; calls={calls:?}"
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ── 6. liveness three-state (§bug-085): exit 0 -> Live; "can't find …" -> Dead; else -> Unknown ─
|
|
300
|
+
#[test]
|
|
301
|
+
fn liveness_is_three_state_unknown_is_not_dead() {
|
|
302
|
+
let (be, rec) = backend_with(MockResp::Out(ok("%7")), vec![]);
|
|
303
|
+
assert_eq!(be.liveness(&PaneId::new("%7")).expect("liveness"), PaneLiveness::Live);
|
|
304
|
+
let argv0 = rec.lock().unwrap()[0].clone();
|
|
305
|
+
assert!(
|
|
306
|
+
argv0.contains(&"display-message".to_string())
|
|
307
|
+
&& argv0.iter().any(|x| x.contains("#{pane_id}"))
|
|
308
|
+
&& argv0.contains(&"%7".to_string()),
|
|
309
|
+
"liveness must probe the pane via display-message #{{pane_id}}; got {argv0:?}"
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
let (be, _r) = backend_with(MockResp::Out(fail(1, "can't find pane %7")), vec![]);
|
|
313
|
+
assert_eq!(
|
|
314
|
+
be.liveness(&PaneId::new("%7")).expect("liveness"),
|
|
315
|
+
PaneLiveness::Dead,
|
|
316
|
+
"a 'can't find pane' failure -> Dead"
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
let (be, _r) = backend_with(MockResp::Out(fail(1, "error connecting to server: No such file or directory")), vec![]);
|
|
320
|
+
assert_eq!(
|
|
321
|
+
be.liveness(&PaneId::new("%7")).expect("liveness"),
|
|
322
|
+
PaneLiveness::Unknown,
|
|
323
|
+
"a NON-'can't find' failure is UNKNOWN, not DEAD (§bug-085 three-state)"
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── CP-1: per-team socket — for_workspace injects `-L ta-<hash>` at the run chokepoint; new() does NOT ─
|
|
328
|
+
#[test]
|
|
329
|
+
fn for_workspace_backend_injects_per_team_socket_but_default_backend_does_not() {
|
|
330
|
+
use super::socket_name_for_workspace;
|
|
331
|
+
let ws = Path::new("/tmp/ta-cp1-socket-test-ws");
|
|
332
|
+
let socket = socket_name_for_workspace(ws);
|
|
333
|
+
assert!(
|
|
334
|
+
socket.starts_with("ta-") && socket.len() == 15,
|
|
335
|
+
"socket name must be short + deterministic `ta-<12 hex>`; got {socket:?}"
|
|
336
|
+
);
|
|
337
|
+
// deterministic: the SAME workspace path always derives the SAME socket (CLI == daemon == ops).
|
|
338
|
+
assert_eq!(socket, socket_name_for_workspace(ws), "socket derivation must be deterministic");
|
|
339
|
+
|
|
340
|
+
// workspace-bound backend: every executed `tmux` argv gets `-L <socket>` after the leading token.
|
|
341
|
+
let recorded = Arc::new(Mutex::new(Vec::new()));
|
|
342
|
+
let runner = MockCommandRunner {
|
|
343
|
+
recorded: Arc::clone(&recorded),
|
|
344
|
+
stdin_recorded: Arc::new(Mutex::new(Vec::new())),
|
|
345
|
+
queue: Mutex::new(VecDeque::new()),
|
|
346
|
+
default: MockResp::Out(ok("")),
|
|
347
|
+
};
|
|
348
|
+
let be = TmuxBackend::with_runner_for_workspace(Box::new(runner), ws);
|
|
349
|
+
be.has_session(&SessionName::new("sess")).expect("has_session");
|
|
350
|
+
let argv = recorded.lock().unwrap()[0].clone();
|
|
351
|
+
assert_eq!(
|
|
352
|
+
argv,
|
|
353
|
+
svec(&["tmux", "-L", &socket, "has-session", "-t", "sess"]),
|
|
354
|
+
"for_workspace backend must inject `-L <socket>` right after `tmux`; got {argv:?}"
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
// default backend (new()/with_runner): NO `-L` — argv stays the golden-locked builder form.
|
|
358
|
+
let (be, rec) = backend_with(MockResp::Out(ok("")), vec![]);
|
|
359
|
+
be.has_session(&SessionName::new("sess")).expect("has_session");
|
|
360
|
+
assert_eq!(
|
|
361
|
+
rec.lock().unwrap()[0],
|
|
362
|
+
svec(&["tmux", "has-session", "-t", "sess"]),
|
|
363
|
+
"the default-socket backend must NOT inject `-L` (existing tests + non-team callers unaffected)"
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ── 7. kill_session / kill_window: golden argv; success -> Ok(()) ───────────────────────────────
|
|
368
|
+
#[test]
|
|
369
|
+
fn kill_session_and_kill_window_argv() {
|
|
370
|
+
let (be, rec) = backend_with(MockResp::Out(ok("")), vec![]);
|
|
371
|
+
be.kill_session(&SessionName::new("sess")).expect("kill_session");
|
|
372
|
+
assert_eq!(rec.lock().unwrap()[0], svec(&["tmux", "kill-session", "-t", "sess"]));
|
|
373
|
+
|
|
374
|
+
let (be, rec) = backend_with(MockResp::Out(ok("")), vec![]);
|
|
375
|
+
be.kill_window(&Target::Pane(PaneId::new("%7"))).expect("kill_window");
|
|
376
|
+
assert_eq!(rec.lock().unwrap()[0], svec(&["tmux", "kill-window", "-t", "%7"]));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ── 8. ERROR MAPPING: non-zero tmux exit -> TransportError::Subprocess; runner io::Error -> Err ──
|
|
380
|
+
#[test]
|
|
381
|
+
fn error_paths_map_to_transport_error_not_panic() {
|
|
382
|
+
// tmux cli non-zero exit (the Subprocess variant's documented purpose).
|
|
383
|
+
let (be, _r) = backend_with(MockResp::Out(fail(1, "no server running on /tmp/tmux-x/default")), vec![]);
|
|
384
|
+
let err = be.kill_session(&SessionName::new("sess")).expect_err("kill_session must error on non-zero exit");
|
|
385
|
+
assert!(
|
|
386
|
+
matches!(err, TransportError::Subprocess { code: Some(1), .. }),
|
|
387
|
+
"a non-zero tmux exit must map to TransportError::Subprocess{{code,stderr}}; got {err:?}"
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
// a runner io::Error (e.g. tmux not on PATH) must surface as a TransportError, never a panic.
|
|
391
|
+
let (be, _r) = backend_with(MockResp::Io(std::io::ErrorKind::NotFound), vec![]);
|
|
392
|
+
let err = be
|
|
393
|
+
.capture(&Target::Pane(PaneId::new("%7")), CaptureRange::Full)
|
|
394
|
+
.expect_err("capture must surface the runner io error");
|
|
395
|
+
assert!(
|
|
396
|
+
matches!(err, TransportError::Capture { .. } | TransportError::Io(_)),
|
|
397
|
+
"a runner io error must map to a TransportError (not panic); got {err:?}"
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── 9. RealCommandRunner GOLDEN 5s TIMEOUT (rt-host-b transient-session race) ────────────────────
|
|
402
|
+
// GOLDEN: terminal.py:12-13 `run_cmd(args, timeout=timeout, check=False)`; runtime.py:1010-1014
|
|
403
|
+
// `_tmux_session_exists` runs `tmux has-session -t <s>` with timeout=5. A has-session that outlives
|
|
404
|
+
// 5s raises `subprocess.TimeoutExpired`, which the coordinator daemon CATCHES
|
|
405
|
+
// (coordinator/__main__.py:60-90 `except Exception`) and treats as a TOLERATED transient
|
|
406
|
+
// (exponential backoff + retry next tick) — it is NEVER read as a definitive "session gone".
|
|
407
|
+
// The 5s subprocess timeout is golden's ONLY tolerance for a slow/hung probe.
|
|
408
|
+
//
|
|
409
|
+
// RUST GAP (THE BUG): `RealCommandRunner::run` (tmux_backend.rs:52) calls
|
|
410
|
+
// `std::process::Command::output()` with NO timeout, so a slow/hung tmux blocks indefinitely.
|
|
411
|
+
// On the (slow) mac mini this is the ~17% single-round-trip flake: a transient slow has-session
|
|
412
|
+
// tears down a healthy team. This is rt-host-b's deterministic 5/5 anchor — `run` on a HUNG
|
|
413
|
+
// command must abandon at the golden 5s and surface `Err(TimedOut)`, NOT block on the full
|
|
414
|
+
// subprocess.
|
|
415
|
+
//
|
|
416
|
+
// RED today: there is no timeout, so `run(["sleep","30"])` blocks ~30s and the `< 6s` bound fails.
|
|
417
|
+
// #[ignore] real-machine: this is the only test here that spawns a real subprocess.
|
|
418
|
+
// PORTER SEAM: add a 5s timeout inside `RealCommandRunner::run` (spawn child + wait-with-timeout
|
|
419
|
+
// via a thread/channel + kill the child on expiry), returning `Err(io::Error, kind TimedOut)` —
|
|
420
|
+
// NO new crate dependency. Keep the existing `CommandRunner::run(&[String]) -> Result<…, io::Error>`
|
|
421
|
+
// signature (the timeout is internal; do not add a parameter).
|
|
422
|
+
#[test]
|
|
423
|
+
#[ignore = "real-machine: spawns a real sleeping subprocess; asserts RealCommandRunner enforces \
|
|
424
|
+
the golden 5s timeout (terminal.py run_cmd timeout / runtime.py:1013 \
|
|
425
|
+
_tmux_session_exists timeout=5)"]
|
|
426
|
+
fn real_command_runner_enforces_golden_5s_timeout_on_hang() {
|
|
427
|
+
use std::time::{Duration, Instant};
|
|
428
|
+
let runner = RealCommandRunner;
|
|
429
|
+
let started = Instant::now();
|
|
430
|
+
let result = runner.run(&svec(&["sleep", "30"]));
|
|
431
|
+
let elapsed = started.elapsed();
|
|
432
|
+
assert!(
|
|
433
|
+
elapsed < Duration::from_secs(6),
|
|
434
|
+
"RealCommandRunner::run must abandon a hung command at the golden 5s timeout, not block on \
|
|
435
|
+
the full subprocess (terminal.py run_cmd timeout / runtime.py:1013 timeout=5); blocked {elapsed:?}"
|
|
436
|
+
);
|
|
437
|
+
let err = result.expect_err(
|
|
438
|
+
"a command outliving the 5s timeout must surface as Err (subprocess.TimeoutExpired analog) so \
|
|
439
|
+
the daemon backoff path tolerates it, instead of yielding a bogus has-session bool",
|
|
440
|
+
);
|
|
441
|
+
assert_eq!(
|
|
442
|
+
err.kind(),
|
|
443
|
+
std::io::ErrorKind::TimedOut,
|
|
444
|
+
"the timeout must be io::ErrorKind::TimedOut (golden: TimeoutExpired -> daemon except -> backoff/retry)"
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ── 10. query (TRANSPORT TRIO) — single-field display-message; nonzero -> None ──────────────────
|
|
449
|
+
// Golden _legacy_pane_discovery.py:35-39 _tmux_pane_info: `tmux display-message -p -t <target> -F
|
|
450
|
+
// <fmt>` (returncode != 0 -> None), single-field reads at state.py:346 (#{pane_id}) / delivery.py:34
|
|
451
|
+
// (#{pane_width}). The argv is exactly `transport::tmux_query_argv(pane, field)` (the golden-locked
|
|
452
|
+
// builder). RED today: `query` is unimplemented!() -> PANIC. Porter: pane_from_target(target) ->
|
|
453
|
+
// tmux_query_argv -> run; success => Some(stdout.trim()); nonzero => None (never Err).
|
|
454
|
+
#[test]
|
|
455
|
+
fn query_single_field_argv_and_nonzero_maps_to_none() {
|
|
456
|
+
// PaneId field: argv == the golden builder; present value parsed (trimmed) into Some.
|
|
457
|
+
let (be, rec) = backend_with(MockResp::Out(ok("%7\n")), vec![]);
|
|
458
|
+
let got = be.query(&Target::Pane(PaneId::new("%7")), PaneField::PaneId).expect("query ok");
|
|
459
|
+
assert_eq!(
|
|
460
|
+
rec.lock().unwrap()[0],
|
|
461
|
+
tmux_query_argv(&PaneId::new("%7"), PaneField::PaneId),
|
|
462
|
+
"query must build the golden single-field `display-message -p -t <t> -F #{{pane_id}}` argv"
|
|
463
|
+
);
|
|
464
|
+
assert_eq!(got, Some("%7".to_string()), "a present field value is parsed (stripped) into Some");
|
|
465
|
+
|
|
466
|
+
// PaneWidth uses -F too; lock argv + the parsed numeric-as-string field.
|
|
467
|
+
let (be, rec) = backend_with(MockResp::Out(ok("180\n")), vec![]);
|
|
468
|
+
let got = be.query(&Target::Pane(PaneId::new("%7")), PaneField::PaneWidth).expect("query ok");
|
|
469
|
+
assert_eq!(rec.lock().unwrap()[0], tmux_query_argv(&PaneId::new("%7"), PaneField::PaneWidth));
|
|
470
|
+
assert_eq!(got, Some("180".to_string()));
|
|
471
|
+
|
|
472
|
+
// nonzero exit (pane gone) -> None, NOT an Err (golden _tmux_pane_info: returncode != 0 -> None).
|
|
473
|
+
let (be, _r) = backend_with(MockResp::Out(fail(1, "can't find pane %7")), vec![]);
|
|
474
|
+
assert_eq!(
|
|
475
|
+
be.query(&Target::Pane(PaneId::new("%7")), PaneField::PaneId).expect("query ok on nonzero"),
|
|
476
|
+
None,
|
|
477
|
+
"a nonzero / pane-gone query must map to None (not Err)"
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ── 11. list_targets (TRANSPORT TRIO) — `list-panes -a -F TMUX_PANE_FORMAT` + per-line parse ────
|
|
482
|
+
// Golden _legacy_pane_discovery.py:29-33 _tmux_list_panes: `tmux list-panes -a -F <TMUX_PANE_FORMAT>`
|
|
483
|
+
// (returncode != 0 -> []), parse each tab line via _parse_tmux_pane_info. TMUX_PANE_FORMAT
|
|
484
|
+
// (runtime.py:456-460) is the byte-exact 11-field tab string locked below. RED today: list_targets is
|
|
485
|
+
// unimplemented!() -> PANIC. Porter: build the argv, split each stdout line on '\t', map the fields
|
|
486
|
+
// into PaneInfo (pane_active=="1" -> active). leader_env / pane_pid are the reverse-env real-machine
|
|
487
|
+
// bit (no field in TMUX_PANE_FORMAT) — out of this canned parse; the structured fields are locked here.
|
|
488
|
+
#[test]
|
|
489
|
+
fn list_targets_argv_and_parses_tmux_pane_format() {
|
|
490
|
+
const FMT: &str = "#{pane_id}\t#{session_name}\t#{window_index}\t#{window_name}\t#{pane_index}\t#{pane_tty}\t#{pane_current_command}\t#{pane_active}\t#{pane_current_path}\t#{session_attached}\t#{pane_in_mode}";
|
|
491
|
+
let stdout = "%7\tteam-x\t0\twin0\t0\t/dev/ttys003\tcodex\t1\t/Users/me/work\t1\t0\n\
|
|
492
|
+
%8\tteam-x\t1\twin1\t0\t/dev/ttys004\tnode\t0\t/Users/me/other\t0\t0\n";
|
|
493
|
+
let (be, rec) = backend_with(MockResp::Out(ok(stdout)), vec![]);
|
|
494
|
+
let panes = be.list_targets().expect("list_targets ok");
|
|
495
|
+
assert_eq!(
|
|
496
|
+
rec.lock().unwrap()[0],
|
|
497
|
+
svec(&["tmux", "list-panes", "-a", "-F", FMT]),
|
|
498
|
+
"list_targets must run `tmux list-panes -a -F <TMUX_PANE_FORMAT>` (golden _legacy_pane_discovery.py:29)"
|
|
499
|
+
);
|
|
500
|
+
assert_eq!(panes.len(), 2, "one PaneInfo per output line");
|
|
501
|
+
let p = &panes[0];
|
|
502
|
+
assert_eq!(p.pane_id.as_str(), "%7", "field[0] -> pane_id");
|
|
503
|
+
assert_eq!(p.session.as_str(), "team-x", "field[1] -> session_name");
|
|
504
|
+
assert_eq!(p.window_index, Some(0), "field[2] -> window_index (parsed u32)");
|
|
505
|
+
assert_eq!(p.window_name.as_ref().map(|w| w.as_str().to_string()), Some("win0".to_string()), "field[3] -> window_name");
|
|
506
|
+
assert_eq!(p.pane_index, Some(0), "field[4] -> pane_index (parsed u32)");
|
|
507
|
+
assert_eq!(p.tty.as_deref(), Some("/dev/ttys003"), "field[5] -> pane_tty");
|
|
508
|
+
assert_eq!(p.current_command.as_deref(), Some("codex"), "field[6] -> pane_current_command");
|
|
509
|
+
assert!(p.active, "field[7] pane_active='1' -> active=true");
|
|
510
|
+
assert_eq!(
|
|
511
|
+
p.current_path.as_ref().map(|x| x.to_string_lossy().to_string()),
|
|
512
|
+
Some("/Users/me/work".to_string()),
|
|
513
|
+
"field[8] -> pane_current_path"
|
|
514
|
+
);
|
|
515
|
+
assert!(!panes[1].active, "field[7] pane_active='0' -> active=false");
|
|
516
|
+
|
|
517
|
+
// nonzero exit -> empty vec (golden returncode != 0 -> []).
|
|
518
|
+
let (be, _r) = backend_with(MockResp::Out(fail(1, "no server running on /tmp/tmux-x/default")), vec![]);
|
|
519
|
+
assert!(
|
|
520
|
+
be.list_targets().expect("list_targets ok on nonzero").is_empty(),
|
|
521
|
+
"a nonzero list-panes must map to an EMPTY Vec (not Err)"
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ── 12. attach_session (TRANSPORT TRIO) — `tmux attach-session -t <s>` -> Attached ──────────────
|
|
526
|
+
// Golden tmux attach is `tmux attach-session -t <session>`; a successful attach -> AttachOutcome::
|
|
527
|
+
// Attached. RED today: attach_session is unimplemented!() -> PANIC. The in-process lock asserts the
|
|
528
|
+
// argv + outcome via the recording runner; the REAL attach is interactive (takes over the terminal)
|
|
529
|
+
// — that is the real-machine boundary, not unit-testable.
|
|
530
|
+
#[test]
|
|
531
|
+
fn attach_session_argv_and_attached_outcome() {
|
|
532
|
+
let (be, rec) = backend_with(MockResp::Out(ok("")), vec![]);
|
|
533
|
+
let outcome = be.attach_session(&SessionName::new("sess")).expect("attach ok");
|
|
534
|
+
assert_eq!(
|
|
535
|
+
rec.lock().unwrap()[0],
|
|
536
|
+
svec(&["tmux", "attach-session", "-t", "sess"]),
|
|
537
|
+
"attach_session must run `tmux attach-session -t <session>`"
|
|
538
|
+
);
|
|
539
|
+
assert_eq!(outcome, AttachOutcome::Attached, "a successful tmux attach -> AttachOutcome::Attached");
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ── 13. TARGET-SCAN WIRING (a): list_targets is the LIVE pane-discovery primitive ───────────────
|
|
543
|
+
// WAVE-2 Lane C. `list_targets` (the `tmux list-panes -a` scan, locked argv/parse in test #11) has
|
|
544
|
+
// ZERO production callers today — it is dead code. Golden wires pane discovery on top of it: status
|
|
545
|
+
// (_capture_missing_sessions / _tmux_session_exists, queries.py:46,52) and doctor (coordinator_health)
|
|
546
|
+
// consume the live scan. The in-process wiring obligation is exercised at the status level by
|
|
547
|
+
// cli::tests::status_tmux_session_present_uses_live_tmux_probe_not_is_some (RED). This #[ignore]
|
|
548
|
+
// real-machine seam locks that a LIVE `list_targets` actually enumerates the running panes, proving
|
|
549
|
+
// the primitive is usable by the status/doctor discovery the porter must wire.
|
|
550
|
+
#[test]
|
|
551
|
+
#[ignore = "real-machine: needs a live tmux server+session; asserts list_targets() (the dangling \
|
|
552
|
+
pane-discovery primitive, zero production callers) enumerates live panes so status/doctor \
|
|
553
|
+
discovery can consume it (golden _legacy_pane_discovery list-panes -a)"]
|
|
554
|
+
fn list_targets_is_live_pane_discovery_primitive_for_status_doctor() {
|
|
555
|
+
let be = TmuxBackend::with_runner(Box::new(RealCommandRunner));
|
|
556
|
+
let panes = be.list_targets().expect("live list_targets must not error");
|
|
557
|
+
assert!(
|
|
558
|
+
!panes.is_empty(),
|
|
559
|
+
"a live `tmux list-panes -a` must surface the running panes; status/doctor pane discovery \
|
|
560
|
+
is wired on top of this scan (currently dead code — zero production callers)"
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ── 14. TARGET-SCAN WIRING (b): R1 — caller_target.uuid is FIRST leader_session_uuid precedence ──
|
|
565
|
+
// WAVE-2 Lane C / wave2-laneB-rereview PROBE-D. When the caller-target scan lands, golden
|
|
566
|
+
// claim_lease_no_incident threads `_target_leader_session_uuid(caller_target)` as the FIRST
|
|
567
|
+
// leader_session_uuid precedence (leader/__init__.py:679-684): caller_target.uuid BEFORE
|
|
568
|
+
// owner.uuid / receiver.uuid / derived. A DIFFERENT live pane reclaiming a DEAD owner must persist
|
|
569
|
+
// the CALLER's uuid, not the dead owner's (PROBE-D: PY "NEWUUID" / RUST persists "OLD"). The
|
|
570
|
+
// caller-target uuid is read from the caller pane's INJECTED TEAM_AGENT_LEADER_SESSION_UUID via a
|
|
571
|
+
// per-pane env query (NOT a TMUX_PANE_FORMAT field), so the live scan is the dependency this seam
|
|
572
|
+
// marks. SCOPE NOTE: the decisive IN-PROCESS claim-path R1 RED belongs in leader/tests.rs, which is
|
|
573
|
+
// outside this task's (cli + tmux_backend) editor scope — flagged to the leader for the
|
|
574
|
+
// leader-contracts agent to graduate R1 to its own claim-path RED.
|
|
575
|
+
#[test]
|
|
576
|
+
#[ignore = "real-machine + SCOPE: R1 (PROBE-D) caller_target.uuid is FIRST leader_session_uuid \
|
|
577
|
+
precedence (leader/__init__.py:679-684); the in-process claim-path assertion lives in \
|
|
578
|
+
leader/tests.rs (out of cli+tmux_backend scope) — this seam marks the live caller-target \
|
|
579
|
+
env-scan dependency"]
|
|
580
|
+
fn r1_caller_target_uuid_is_first_leader_session_uuid_precedence_seam() {
|
|
581
|
+
// The caller-target scan (reading the caller pane's injected TEAM_AGENT_LEADER_SESSION_UUID)
|
|
582
|
+
// is the live precursor to R1's uuid precedence. The full uuid-persistence assertion is the
|
|
583
|
+
// leader claim path's obligation (see report). Here we only confirm the scan is reachable.
|
|
584
|
+
let be = TmuxBackend::with_runner(Box::new(RealCommandRunner));
|
|
585
|
+
let _panes = be.list_targets().expect("live list_targets (caller-target scan precursor)");
|
|
586
|
+
}
|