@team-agent/installer 0.5.61 → 0.5.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +5 -2
- package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
- package/crates/team-agent/src/cli/diagnose.rs +1 -0
- package/crates/team-agent/src/cli/emit.rs +41 -7
- package/crates/team-agent/src/cli/helpers.rs +1 -0
- package/crates/team-agent/src/cli/leader.rs +6 -1
- package/crates/team-agent/src/cli/leaders.rs +2 -0
- package/crates/team-agent/src/cli/mod.rs +33 -3
- package/crates/team-agent/src/cli/named_address.rs +8 -1
- package/crates/team-agent/src/cli/profile.rs +2 -1
- package/crates/team-agent/src/cli/send/coordinator.rs +1 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +3 -0
- package/crates/team-agent/src/cli/send/persist.rs +1 -0
- package/crates/team-agent/src/cli/send/presentation.rs +1 -0
- package/crates/team-agent/src/cli/send/resolve.rs +206 -1
- package/crates/team-agent/src/cli/send.rs +2 -1
- package/crates/team-agent/src/cli/spec.rs +1 -0
- package/crates/team-agent/src/cli/status.rs +2 -1
- package/crates/team-agent/src/cli/status_port/compact.rs +1 -0
- package/crates/team-agent/src/cli/status_port/format.rs +1 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +1 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +38 -0
- package/crates/team-agent/src/cli/types.rs +3 -2
- package/crates/team-agent/src/communication_mode/mod.rs +1 -0
- package/crates/team-agent/src/compiler.rs +1 -0
- package/crates/team-agent/src/conpty/backend.rs +1 -0
- package/crates/team-agent/src/conpty/mod.rs +1 -0
- package/crates/team-agent/src/coordinator/backoff.rs +5 -8
- package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
- package/crates/team-agent/src/coordinator/health.rs +1 -0
- package/crates/team-agent/src/coordinator/mod.rs +2 -2
- package/crates/team-agent/src/coordinator/orphan.rs +4 -116
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +15 -866
- package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
- package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
- package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +139 -375
- package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
- package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -49
- package/crates/team-agent/src/coordinator/tick.rs +23 -40
- package/crates/team-agent/src/coordinator/types.rs +3 -176
- package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
- package/crates/team-agent/src/db/message_store.rs +2 -1
- package/crates/team-agent/src/db/migration.rs +5 -4
- package/crates/team-agent/src/db/mod.rs +1 -0
- package/crates/team-agent/src/db/schema.rs +1 -0
- package/crates/team-agent/src/diagnose/comms.rs +2 -1
- package/crates/team-agent/src/diagnose/mod.rs +1 -0
- package/crates/team-agent/src/diagnose/orphans.rs +1 -0
- package/crates/team-agent/src/layout/manager.rs +3 -7
- package/crates/team-agent/src/layout/mod.rs +2 -2
- package/crates/team-agent/src/layout/overlay.rs +6 -1
- package/crates/team-agent/src/layout/placement.rs +1 -0
- package/crates/team-agent/src/layout/recovery.rs +3 -0
- package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
- package/crates/team-agent/src/layout/sessions.rs +8 -1
- package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
- package/crates/team-agent/src/layout/worker_env.rs +3 -0
- package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
- package/crates/team-agent/src/leader/helpers.rs +1 -0
- package/crates/team-agent/src/leader/incident.rs +1 -0
- package/crates/team-agent/src/leader/lease.rs +27 -22
- package/crates/team-agent/src/leader/mod.rs +2 -3
- package/crates/team-agent/src/leader/owner_bind.rs +13 -3
- package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
- package/crates/team-agent/src/leader/rediscover.rs +4 -17
- package/crates/team-agent/src/leader/registry.rs +3 -15
- package/crates/team-agent/src/leader/start.rs +373 -125
- package/crates/team-agent/src/leader/takeover.rs +10 -232
- package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
- package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
- package/crates/team-agent/src/leader/tests/identity.rs +7 -2
- package/crates/team-agent/src/leader/tests/idle.rs +0 -193
- package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
- package/crates/team-agent/src/leader/tests/mod.rs +4 -41
- package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
- package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
- package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
- package/crates/team-agent/src/leader/types.rs +1 -28
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/display.rs +7 -1
- package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
- package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -27
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch.rs +11 -12
- package/crates/team-agent/src/lifecycle/mod.rs +2 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +3 -66
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
- package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
- package/crates/team-agent/src/lifecycle/restart.rs +13 -15
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
- package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
- package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
- package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
- package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
- package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
- package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
- package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
- package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
- package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
- package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
- package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
- package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
- package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
- package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
- package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
- package/crates/team-agent/src/lifecycle/tests.rs +59 -0
- package/crates/team-agent/src/lifecycle/types.rs +0 -9
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
- package/crates/team-agent/src/mcp_server/mod.rs +1 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +4 -3
- package/crates/team-agent/src/mcp_server/tools.rs +1 -0
- package/crates/team-agent/src/mcp_server/types.rs +1 -18
- package/crates/team-agent/src/mcp_server/wire.rs +5 -5
- package/crates/team-agent/src/messaging/activity.rs +1 -0
- package/crates/team-agent/src/messaging/address.rs +1 -0
- package/crates/team-agent/src/messaging/delivery.rs +4 -3
- package/crates/team-agent/src/messaging/helpers.rs +2 -1
- package/crates/team-agent/src/messaging/leader_channel.rs +1 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +2 -1
- package/crates/team-agent/src/messaging/mod.rs +10 -14
- package/crates/team-agent/src/messaging/peers.rs +2 -0
- package/crates/team-agent/src/messaging/persist.rs +3 -1
- package/crates/team-agent/src/messaging/presentation.rs +2 -1
- package/crates/team-agent/src/messaging/results.rs +1 -0
- package/crates/team-agent/src/messaging/scheduler.rs +1 -0
- package/crates/team-agent/src/messaging/selftest.rs +1 -1
- package/crates/team-agent/src/messaging/send.rs +41 -8
- package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
- package/crates/team-agent/src/messaging/types.rs +2 -2
- package/crates/team-agent/src/messaging/watchers.rs +19 -13
- package/crates/team-agent/src/model/enums.rs +1 -0
- package/crates/team-agent/src/model/errors.rs +1 -0
- package/crates/team-agent/src/model/ids.rs +1 -0
- package/crates/team-agent/src/model/mod.rs +1 -0
- package/crates/team-agent/src/model/name_similarity.rs +1 -0
- package/crates/team-agent/src/model/pane_authority_refusal.rs +1 -0
- package/crates/team-agent/src/model/paths.rs +1 -0
- package/crates/team-agent/src/model/permissions.rs +3 -2
- package/crates/team-agent/src/model/routing.rs +1 -0
- package/crates/team-agent/src/model/spec.rs +1 -0
- package/crates/team-agent/src/model/task_graph.rs +1 -0
- package/crates/team-agent/src/model/yaml/tests.rs +1 -0
- package/crates/team-agent/src/model/yaml.rs +1 -0
- package/crates/team-agent/src/packaging/install.rs +9 -147
- package/crates/team-agent/src/packaging/migrate.rs +2 -0
- package/crates/team-agent/src/packaging/mod.rs +1 -0
- package/crates/team-agent/src/packaging/repair.rs +2 -0
- package/crates/team-agent/src/packaging/tests.rs +29 -218
- package/crates/team-agent/src/packaging/types.rs +8 -15
- package/crates/team-agent/src/platform/argv.rs +4 -0
- package/crates/team-agent/src/platform/errors.rs +10 -1
- package/crates/team-agent/src/platform/file_lock.rs +4 -157
- package/crates/team-agent/src/platform/mod.rs +0 -61
- package/crates/team-agent/src/platform/process.rs +1 -0
- package/crates/team-agent/src/provider/adapter.rs +1 -0
- package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
- package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
- package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
- package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
- package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
- package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
- package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
- package/crates/team-agent/src/provider/classify.rs +1 -0
- package/crates/team-agent/src/provider/faults.rs +1 -26
- package/crates/team-agent/src/provider/helpers.rs +1 -0
- package/crates/team-agent/src/provider/mod.rs +1 -1
- package/crates/team-agent/src/provider/session/capture.rs +1 -4
- package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
- package/crates/team-agent/src/provider/session/mod.rs +2 -4
- package/crates/team-agent/src/provider/session/resume.rs +2 -209
- package/crates/team-agent/src/provider/session_scan/claude.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
- package/crates/team-agent/src/provider/session_scan.rs +1 -42
- package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
- package/crates/team-agent/src/provider/types.rs +1 -0
- package/crates/team-agent/src/provider/wire.rs +1 -0
- package/crates/team-agent/src/state/identity.rs +16 -5
- package/crates/team-agent/src/state/identity_keys.rs +1 -0
- package/crates/team-agent/src/state/mod.rs +2 -0
- package/crates/team-agent/src/state/owner_gate.rs +5 -0
- package/crates/team-agent/src/state/ownership.rs +10 -4
- package/crates/team-agent/src/state/paths.rs +12 -0
- package/crates/team-agent/src/state/persist.rs +9 -3
- package/crates/team-agent/src/state/projection.rs +15 -2
- package/crates/team-agent/src/state/repository/intent.rs +1 -0
- package/crates/team-agent/src/state/repository/tests.rs +1 -0
- package/crates/team-agent/src/state/repository.rs +7 -13
- package/crates/team-agent/src/state/selector.rs +1 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
- package/crates/team-agent/src/tmux_backend.rs +63 -0
- package/crates/team-agent/src/transport.rs +15 -0
- package/package.json +4 -4
- package/crates/team-agent/src/leader/inject.rs +0 -33
- package/crates/team-agent/src/provider/command.rs +0 -80
|
@@ -0,0 +1,1103 @@
|
|
|
1
|
+
//! Team-in-team sibling quick-start contracts.
|
|
2
|
+
//!
|
|
3
|
+
//! #241: two team directories under the same parent workspace must be independent teams. An
|
|
4
|
+
//! existing runtime for `teamA` must not make `quick-start teamB` return `ExistingRuntime` before
|
|
5
|
+
//! reaching the existing CR-040/042 per-team state/session merge path.
|
|
6
|
+
|
|
7
|
+
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
8
|
+
|
|
9
|
+
#[path = "../../../tests/support/hermetic.rs"]
|
|
10
|
+
mod hermetic_guard;
|
|
11
|
+
#[allow(dead_code)]
|
|
12
|
+
fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
|
|
13
|
+
|
|
14
|
+
use std::collections::{BTreeMap, HashSet};
|
|
15
|
+
use std::path::{Path, PathBuf};
|
|
16
|
+
use std::process::Command;
|
|
17
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
18
|
+
use std::sync::Mutex;
|
|
19
|
+
|
|
20
|
+
use rusqlite::params;
|
|
21
|
+
use serde_json::Value;
|
|
22
|
+
use serial_test::{file_serial, serial};
|
|
23
|
+
use team_agent::event_log::EventLog;
|
|
24
|
+
use team_agent::lifecycle::{quick_start_with_transport, QuickStartReport};
|
|
25
|
+
use team_agent::message_store::MessageStore;
|
|
26
|
+
use team_agent::messaging::deliver_pending_messages;
|
|
27
|
+
use team_agent::state::persist::load_runtime_state;
|
|
28
|
+
use team_agent::tmux_backend::TmuxBackend;
|
|
29
|
+
use team_agent::transport::{
|
|
30
|
+
AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
|
|
31
|
+
InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, SessionName, SetEnvOutcome,
|
|
32
|
+
SpawnResult, SubmitVerification, Target, Transport, TransportError, TurnVerification,
|
|
33
|
+
WindowName,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
#[test]
|
|
37
|
+
#[ignore = "real-machine: quick-start/session lifecycle gate"]
|
|
38
|
+
#[serial(env)]
|
|
39
|
+
fn quick_start_sibling_teamdir_in_same_workspace_starts_new_team_not_existing_runtime() {
|
|
40
|
+
let _env = EnvGuard::unset([
|
|
41
|
+
"TMUX",
|
|
42
|
+
"TMUX_PANE",
|
|
43
|
+
"TEAM_AGENT_ID",
|
|
44
|
+
"TEAM_AGENT_TEAM_ID",
|
|
45
|
+
"TEAM_AGENT_LEADER_PANE_ID",
|
|
46
|
+
"TEAM_AGENT_LEADER_SESSION_UUID",
|
|
47
|
+
"TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
|
|
48
|
+
"TEAM_AGENT_LEADER_PROVIDER",
|
|
49
|
+
]);
|
|
50
|
+
let root = tmp_dir("sibling-teamdirs");
|
|
51
|
+
seed_healthy_coordinator(&root);
|
|
52
|
+
let team_a = team_dir(&root, "teamA", "worker_a");
|
|
53
|
+
let team_b = team_dir(&root, "teamB", "worker_b");
|
|
54
|
+
let transport = SessionRecordingTransport::default();
|
|
55
|
+
|
|
56
|
+
let first = quick_start_with_transport(&team_a, Some("teamA"), true, Some("teamA"), &transport)
|
|
57
|
+
.expect("fixture: first teamA quick-start should succeed");
|
|
58
|
+
assert_ready_team("teamA first quick-start", &first, "team-teamA");
|
|
59
|
+
|
|
60
|
+
let second =
|
|
61
|
+
quick_start_with_transport(&team_b, Some("teamB"), true, Some("teamB"), &transport)
|
|
62
|
+
.expect("teamB quick-start must not be refused by teamA's runtime");
|
|
63
|
+
|
|
64
|
+
assert_ready_team("sibling teamB quick-start", &second, "team-teamB");
|
|
65
|
+
let state =
|
|
66
|
+
load_runtime_state(&root).expect("state.json should exist in shared parent workspace");
|
|
67
|
+
assert_team_present(&state, "teamA", "team-teamA");
|
|
68
|
+
assert_team_present(&state, "teamB", "team-teamB");
|
|
69
|
+
assert_eq!(
|
|
70
|
+
state.get("active_team_key").and_then(Value::as_str),
|
|
71
|
+
Some("teamB"),
|
|
72
|
+
"the newly launched sibling team should become the active top-level projection; state={state}"
|
|
73
|
+
);
|
|
74
|
+
let sessions = transport.spawned_sessions();
|
|
75
|
+
assert!(
|
|
76
|
+
sessions.iter().any(|session| session == "team-teamA")
|
|
77
|
+
&& sessions.iter().any(|session| session == "team-teamB"),
|
|
78
|
+
"sibling teamdirs must spawn independent tmux sessions derived from requested team identity; sessions={sessions:?}"
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[test]
|
|
83
|
+
#[ignore = "real-machine: quick-start/session lifecycle gate"]
|
|
84
|
+
#[serial(env)]
|
|
85
|
+
fn quick_start_sibling_teamdir_without_team_arg_infers_compiled_spec_name() {
|
|
86
|
+
let _env = EnvGuard::unset([
|
|
87
|
+
"TMUX",
|
|
88
|
+
"TMUX_PANE",
|
|
89
|
+
"TEAM_AGENT_ID",
|
|
90
|
+
"TEAM_AGENT_TEAM_ID",
|
|
91
|
+
"TEAM_AGENT_LEADER_PANE_ID",
|
|
92
|
+
"TEAM_AGENT_LEADER_SESSION_UUID",
|
|
93
|
+
"TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
|
|
94
|
+
"TEAM_AGENT_LEADER_PROVIDER",
|
|
95
|
+
]);
|
|
96
|
+
let root = tmp_dir("sibling-no-team-infers-spec-name");
|
|
97
|
+
seed_healthy_coordinator(&root);
|
|
98
|
+
let team_a = team_dir(&root, "teamA", "worker_a");
|
|
99
|
+
let team_b = team_dir(&root, "teamB", "worker_b");
|
|
100
|
+
let transport = SessionRecordingTransport::default();
|
|
101
|
+
|
|
102
|
+
let first = quick_start_with_transport(&team_a, Some("teamA"), true, Some("teamA"), &transport)
|
|
103
|
+
.expect("fixture: first teamA quick-start should succeed");
|
|
104
|
+
assert_ready_team("teamA first quick-start", &first, "team-teamA");
|
|
105
|
+
|
|
106
|
+
let second = quick_start_with_transport(&team_b, None, true, None, &transport)
|
|
107
|
+
.expect("quick-start teamB without --team should still use TEAM.md name=teamB");
|
|
108
|
+
|
|
109
|
+
assert_ready_team(
|
|
110
|
+
"sibling teamB quick-start without --team",
|
|
111
|
+
&second,
|
|
112
|
+
"team-teamB",
|
|
113
|
+
);
|
|
114
|
+
let state =
|
|
115
|
+
load_runtime_state(&root).expect("state.json should exist in shared parent workspace");
|
|
116
|
+
assert_team_present(&state, "teamA", "team-teamA");
|
|
117
|
+
assert_team_present(&state, "teamB", "team-teamB");
|
|
118
|
+
assert_eq!(
|
|
119
|
+
state.get("active_team_key").and_then(Value::as_str),
|
|
120
|
+
Some("teamB"),
|
|
121
|
+
"quick-start <teamBdir> without --team must infer compiled spec name=teamB and activate that sibling team; state={state}"
|
|
122
|
+
);
|
|
123
|
+
let sessions = transport.spawned_sessions();
|
|
124
|
+
assert!(
|
|
125
|
+
sessions.iter().any(|session| session == "team-teamA")
|
|
126
|
+
&& sessions.iter().any(|session| session == "team-teamB"),
|
|
127
|
+
"missing --team must not collapse sibling teamB into teamA's existing runtime; sessions={sessions:?}"
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#[test]
|
|
132
|
+
#[ignore = "real-machine: quick-start/session lifecycle gate"]
|
|
133
|
+
#[serial(env)]
|
|
134
|
+
fn quick_start_sibling_teamdir_without_team_arg_same_spec_name_still_returns_existing_runtime() {
|
|
135
|
+
let _env = EnvGuard::unset([
|
|
136
|
+
"TMUX",
|
|
137
|
+
"TMUX_PANE",
|
|
138
|
+
"TEAM_AGENT_ID",
|
|
139
|
+
"TEAM_AGENT_TEAM_ID",
|
|
140
|
+
"TEAM_AGENT_LEADER_PANE_ID",
|
|
141
|
+
"TEAM_AGENT_LEADER_SESSION_UUID",
|
|
142
|
+
"TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
|
|
143
|
+
"TEAM_AGENT_LEADER_PROVIDER",
|
|
144
|
+
]);
|
|
145
|
+
let root = tmp_dir("sibling-no-team-same-spec-name");
|
|
146
|
+
seed_healthy_coordinator(&root);
|
|
147
|
+
let team_a = team_dir(&root, "teamA", "worker_a");
|
|
148
|
+
let team_b_same_name = team_dir_with_name(&root, "teamB", "teamA", "worker_b");
|
|
149
|
+
let transport = SessionRecordingTransport::default();
|
|
150
|
+
|
|
151
|
+
let first = quick_start_with_transport(&team_a, Some("teamA"), true, Some("teamA"), &transport)
|
|
152
|
+
.expect("fixture: first teamA quick-start should succeed");
|
|
153
|
+
assert_ready_team("teamA first quick-start", &first, "team-teamA");
|
|
154
|
+
|
|
155
|
+
let duplicate = quick_start_with_transport(&team_b_same_name, None, true, None, &transport)
|
|
156
|
+
.expect("same spec.name should return a typed ExistingRuntime report");
|
|
157
|
+
|
|
158
|
+
match duplicate {
|
|
159
|
+
QuickStartReport::ExistingRuntime {
|
|
160
|
+
session_name,
|
|
161
|
+
state_path,
|
|
162
|
+
..
|
|
163
|
+
} => {
|
|
164
|
+
assert_eq!(session_name.as_ref().map(|s| s.as_str()), Some("team-teamA"));
|
|
165
|
+
assert!(
|
|
166
|
+
state_path.as_ref().is_some_and(|path| path.starts_with(&root)),
|
|
167
|
+
"same-name ExistingRuntime should point at the shared workspace state path; state_path={state_path:?}"
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
other => panic!(
|
|
171
|
+
"when sibling teamdir compiles to existing spec.name=teamA, quick-start without --team should remain ExistingRuntime; got {other:?}"
|
|
172
|
+
),
|
|
173
|
+
}
|
|
174
|
+
assert_eq!(
|
|
175
|
+
transport.spawned_sessions(),
|
|
176
|
+
vec!["team-teamA".to_string()],
|
|
177
|
+
"same compiled spec.name must not spawn a second sibling session without --fresh"
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#[test]
|
|
182
|
+
#[ignore = "real-machine: quick-start/session lifecycle gate"]
|
|
183
|
+
#[serial(env)]
|
|
184
|
+
fn quick_start_same_existing_team_still_returns_existing_runtime() {
|
|
185
|
+
let _env = EnvGuard::unset([
|
|
186
|
+
"TMUX",
|
|
187
|
+
"TMUX_PANE",
|
|
188
|
+
"TEAM_AGENT_ID",
|
|
189
|
+
"TEAM_AGENT_TEAM_ID",
|
|
190
|
+
"TEAM_AGENT_LEADER_PANE_ID",
|
|
191
|
+
"TEAM_AGENT_LEADER_SESSION_UUID",
|
|
192
|
+
"TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
|
|
193
|
+
"TEAM_AGENT_LEADER_PROVIDER",
|
|
194
|
+
]);
|
|
195
|
+
let root = tmp_dir("same-team-existing");
|
|
196
|
+
seed_healthy_coordinator(&root);
|
|
197
|
+
let team_a = team_dir(&root, "teamA", "worker_a");
|
|
198
|
+
let transport = SessionRecordingTransport::default();
|
|
199
|
+
|
|
200
|
+
let first = quick_start_with_transport(&team_a, Some("teamA"), true, Some("teamA"), &transport)
|
|
201
|
+
.expect("fixture: first teamA quick-start should succeed");
|
|
202
|
+
assert_ready_team("teamA first quick-start", &first, "team-teamA");
|
|
203
|
+
|
|
204
|
+
let duplicate =
|
|
205
|
+
quick_start_with_transport(&team_a, Some("teamA"), true, Some("teamA"), &transport)
|
|
206
|
+
.expect("same-team duplicate should return a typed report, not an error");
|
|
207
|
+
|
|
208
|
+
match duplicate {
|
|
209
|
+
QuickStartReport::ExistingRuntime {
|
|
210
|
+
team,
|
|
211
|
+
session_name,
|
|
212
|
+
state_path,
|
|
213
|
+
..
|
|
214
|
+
} => {
|
|
215
|
+
assert_eq!(team.as_deref(), Some("teamA"));
|
|
216
|
+
assert_eq!(session_name.as_ref().map(|s| s.as_str()), Some("team-teamA"));
|
|
217
|
+
assert!(
|
|
218
|
+
state_path.as_ref().is_some_and(|path| path.starts_with(&root)),
|
|
219
|
+
"ExistingRuntime should point at the shared workspace state path; state_path={state_path:?}"
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
other => panic!(
|
|
223
|
+
"same requested team with an existing runtime should remain ExistingRuntime; got {other:?}"
|
|
224
|
+
),
|
|
225
|
+
}
|
|
226
|
+
assert_eq!(
|
|
227
|
+
transport.spawned_sessions(),
|
|
228
|
+
vec!["team-teamA".to_string()],
|
|
229
|
+
"same-team duplicate must not spawn another session without --fresh"
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#[test]
|
|
234
|
+
#[ignore = "real-machine: quick-start/session lifecycle gate"]
|
|
235
|
+
#[serial(env)]
|
|
236
|
+
fn quick_start_sibling_after_shutdown_is_allowed_not_nested() {
|
|
237
|
+
let _env = EnvGuard::unset([
|
|
238
|
+
"TMUX",
|
|
239
|
+
"TMUX_PANE",
|
|
240
|
+
"TEAM_AGENT_ID",
|
|
241
|
+
"TEAM_AGENT_TEAM_ID",
|
|
242
|
+
"TEAM_AGENT_LEADER_PANE_ID",
|
|
243
|
+
"TEAM_AGENT_LEADER_SESSION_UUID",
|
|
244
|
+
"TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
|
|
245
|
+
"TEAM_AGENT_LEADER_PROVIDER",
|
|
246
|
+
]);
|
|
247
|
+
let root = tmp_dir("sibling-after-shutdown");
|
|
248
|
+
seed_healthy_coordinator(&root);
|
|
249
|
+
let team_a = team_dir(&root, "teamA", "worker_a");
|
|
250
|
+
let team_b = team_dir(&root, "child-team", "worker_b");
|
|
251
|
+
let transport = SessionRecordingTransport::default();
|
|
252
|
+
|
|
253
|
+
let first = quick_start_with_transport(&team_a, Some("teamA"), true, Some("teamA"), &transport)
|
|
254
|
+
.expect("fixture: first teamA quick-start should succeed");
|
|
255
|
+
assert_ready_team("teamA first quick-start", &first, "team-teamA");
|
|
256
|
+
|
|
257
|
+
let shutdown = team_agent::cli::lifecycle_port::shutdown_with_transport(
|
|
258
|
+
&root,
|
|
259
|
+
true,
|
|
260
|
+
Some("teamA"),
|
|
261
|
+
&transport,
|
|
262
|
+
)
|
|
263
|
+
.expect("fixture: shutdown --team teamA should mark the old team stopped");
|
|
264
|
+
assert_eq!(
|
|
265
|
+
shutdown.get("ok").and_then(Value::as_bool),
|
|
266
|
+
Some(true),
|
|
267
|
+
"fixture: shutdown must complete before sibling quick-start; shutdown={shutdown}"
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
let second = quick_start_with_transport(
|
|
271
|
+
&team_b,
|
|
272
|
+
None,
|
|
273
|
+
false,
|
|
274
|
+
None,
|
|
275
|
+
&transport,
|
|
276
|
+
)
|
|
277
|
+
.expect(
|
|
278
|
+
"after a same-workspace team has been shut down, bare quick-start <child-teamdir> must be treated as a sibling team, not an ambiguous nested quick-start",
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
assert_ready_team(
|
|
282
|
+
"bare sibling child-like quick-start after shutdown",
|
|
283
|
+
&second,
|
|
284
|
+
"team-child-team",
|
|
285
|
+
);
|
|
286
|
+
let state = load_runtime_state(&root).expect("state after sibling quick-start");
|
|
287
|
+
assert_team_present(&state, "teamA", "team-teamA");
|
|
288
|
+
assert_team_present(&state, "child-team", "team-child-team");
|
|
289
|
+
assert_eq!(
|
|
290
|
+
state.get("active_team_key").and_then(Value::as_str),
|
|
291
|
+
Some("child-team"),
|
|
292
|
+
"bare quick-start after shutdown should activate sibling child-like team; state={state}"
|
|
293
|
+
);
|
|
294
|
+
assert_eq!(
|
|
295
|
+
state["teams"]["child-team"].get("parent_team_key").and_then(Value::as_str),
|
|
296
|
+
None,
|
|
297
|
+
"sibling child-like team after shutdown must not be persisted as a nested child; state={state}"
|
|
298
|
+
);
|
|
299
|
+
assert!(
|
|
300
|
+
transport
|
|
301
|
+
.spawned_sessions()
|
|
302
|
+
.iter()
|
|
303
|
+
.any(|session| session == "team-child-team"),
|
|
304
|
+
"bare sibling quick-start after shutdown must spawn its own team-child-team session; sessions={:?}",
|
|
305
|
+
transport.spawned_sessions()
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#[test]
|
|
310
|
+
#[ignore = "real-machine: needs real tmux/coordinator/binary"]
|
|
311
|
+
#[serial(env)]
|
|
312
|
+
#[file_serial(tmux)]
|
|
313
|
+
fn dirty_sibling_quick_start_from_same_leader_pane_binds_receiver_and_delivers_to_leader() {
|
|
314
|
+
let case = RealSiblingCase::new("dirty-sibling-real");
|
|
315
|
+
let leader = case.spawn_leader_pane();
|
|
316
|
+
let leader_env = [
|
|
317
|
+
("TEAM_AGENT_LEADER_PANE_ID", leader.as_str()),
|
|
318
|
+
("TMUX_PANE", leader.as_str()),
|
|
319
|
+
("TEAM_AGENT_LEADER_PROVIDER", "codex"),
|
|
320
|
+
];
|
|
321
|
+
let current = fake_team_dir(&case.root, "current", "current_worker");
|
|
322
|
+
let sibling = fake_team_dir(&case.root, "sibling", "sibling_worker");
|
|
323
|
+
|
|
324
|
+
let current_out = case.run_team_agent_json(
|
|
325
|
+
&[
|
|
326
|
+
"quick-start",
|
|
327
|
+
current.to_str().unwrap(),
|
|
328
|
+
"--workspace",
|
|
329
|
+
case.root.to_str().unwrap(),
|
|
330
|
+
"--team-id",
|
|
331
|
+
"current",
|
|
332
|
+
"--yes",
|
|
333
|
+
"--json",
|
|
334
|
+
],
|
|
335
|
+
&leader_env,
|
|
336
|
+
);
|
|
337
|
+
assert_eq!(
|
|
338
|
+
current_out.get("ok").and_then(Value::as_bool),
|
|
339
|
+
Some(true),
|
|
340
|
+
"fixture sanity: current quick-start from leader pane must succeed; out={current_out}"
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
let before_sibling = load_runtime_state(&case.root).expect("state before sibling quick-start");
|
|
344
|
+
let current_receiver_before = before_sibling
|
|
345
|
+
.pointer("/teams/current/leader_receiver")
|
|
346
|
+
.cloned()
|
|
347
|
+
.expect("current receiver exists before sibling quick-start");
|
|
348
|
+
|
|
349
|
+
let sibling_out = case.run_team_agent_json(
|
|
350
|
+
&[
|
|
351
|
+
"quick-start",
|
|
352
|
+
sibling.to_str().unwrap(),
|
|
353
|
+
"--workspace",
|
|
354
|
+
case.root.to_str().unwrap(),
|
|
355
|
+
"--team-id",
|
|
356
|
+
"sibling",
|
|
357
|
+
"--yes",
|
|
358
|
+
"--json",
|
|
359
|
+
],
|
|
360
|
+
&leader_env,
|
|
361
|
+
);
|
|
362
|
+
let state = load_runtime_state(&case.root).expect("state after sibling quick-start");
|
|
363
|
+
let receiver = state
|
|
364
|
+
.pointer("/teams/sibling/leader_receiver")
|
|
365
|
+
.unwrap_or_else(|| panic!("state.teams.sibling.leader_receiver must exist; state={state}"));
|
|
366
|
+
let mut failures = Vec::new();
|
|
367
|
+
if receiver.get("status").and_then(Value::as_str) != Some("attached") {
|
|
368
|
+
failures.push(format!(
|
|
369
|
+
"teams.sibling.leader_receiver.status must be attached when quick-start is run from a positive leader pane; receiver={receiver}"
|
|
370
|
+
));
|
|
371
|
+
}
|
|
372
|
+
if receiver.get("pane_id").and_then(Value::as_str) != Some(leader.as_str()) {
|
|
373
|
+
failures.push(format!(
|
|
374
|
+
"the same physical pane may be leader receiver for current and sibling team scopes; expected sibling pane_id={}, receiver={receiver}",
|
|
375
|
+
leader.as_str()
|
|
376
|
+
));
|
|
377
|
+
}
|
|
378
|
+
let current_receiver_after = state
|
|
379
|
+
.pointer("/teams/current/leader_receiver")
|
|
380
|
+
.cloned()
|
|
381
|
+
.expect("current receiver exists after sibling quick-start");
|
|
382
|
+
if current_receiver_after != current_receiver_before {
|
|
383
|
+
failures.push(format!(
|
|
384
|
+
"sibling quick-start must not rewrite current team's leader_receiver; before={current_receiver_before} after={current_receiver_after}"
|
|
385
|
+
));
|
|
386
|
+
}
|
|
387
|
+
if receiver.get("status").and_then(Value::as_str) == Some("unbound")
|
|
388
|
+
&& sibling_out.get("ok").and_then(Value::as_bool) == Some(true)
|
|
389
|
+
{
|
|
390
|
+
failures.push(format!(
|
|
391
|
+
"quick-start --json must not return ok=true after saving the launched team with leader_receiver.status=unbound; it must return degraded/ok=false with a claim-leader next action. out={sibling_out}"
|
|
392
|
+
));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
let token = format!("DIRTY_SIBLING_TO_LEADER_{}", std::process::id());
|
|
396
|
+
let send_raw = case.run_team_agent_status(
|
|
397
|
+
&[
|
|
398
|
+
"send",
|
|
399
|
+
"leader",
|
|
400
|
+
&token,
|
|
401
|
+
"--workspace",
|
|
402
|
+
case.root.to_str().unwrap(),
|
|
403
|
+
"--team",
|
|
404
|
+
"sibling",
|
|
405
|
+
"--json",
|
|
406
|
+
],
|
|
407
|
+
&[("TEAM_AGENT_ID", "sibling_worker")],
|
|
408
|
+
);
|
|
409
|
+
let send_out: Value = serde_json::from_slice(&send_raw.stdout).unwrap_or_else(|err| {
|
|
410
|
+
panic!(
|
|
411
|
+
"send stdout must be JSON even on delivery refusal: {err}; status={:?} stdout={} stderr={}",
|
|
412
|
+
send_raw.status.code(),
|
|
413
|
+
String::from_utf8_lossy(&send_raw.stdout),
|
|
414
|
+
String::from_utf8_lossy(&send_raw.stderr)
|
|
415
|
+
)
|
|
416
|
+
});
|
|
417
|
+
let _ = case.run_team_agent_status(
|
|
418
|
+
&[
|
|
419
|
+
"coordinator",
|
|
420
|
+
"--workspace",
|
|
421
|
+
case.root.to_str().unwrap(),
|
|
422
|
+
"--once",
|
|
423
|
+
],
|
|
424
|
+
&[],
|
|
425
|
+
);
|
|
426
|
+
let row = message_row_for_token(&case.root, &token);
|
|
427
|
+
match row.as_ref() {
|
|
428
|
+
Some((status, delivered_at)) if status == "delivered" && delivered_at.is_some() => {}
|
|
429
|
+
other => failures.push(format!(
|
|
430
|
+
"sibling_worker -> leader must produce a delivered DB row with delivered_at after quick-start binds the leader receiver; send_out={send_out} row={other:?}"
|
|
431
|
+
)),
|
|
432
|
+
}
|
|
433
|
+
let leader_text = case.capture_pane(&leader);
|
|
434
|
+
if !leader_text.contains(&token) {
|
|
435
|
+
failures.push(format!(
|
|
436
|
+
"leader pane {} must visibly receive sibling worker token {token}; capture={leader_text:?}",
|
|
437
|
+
leader.as_str()
|
|
438
|
+
));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
assert!(
|
|
442
|
+
failures.is_empty(),
|
|
443
|
+
"dirty sibling quick-start from the same leader pane must bind per-team receiver and make worker->leader delivery real, without delivery-time auto-claim:\n{}\nstate={state}\nsibling_out={sibling_out}",
|
|
444
|
+
failures.join("\n")
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
#[test]
|
|
449
|
+
#[ignore = "real-machine: quick-start/session lifecycle gate"]
|
|
450
|
+
#[serial(env)]
|
|
451
|
+
fn quick_start_returns_degraded_when_no_positive_caller_pane() {
|
|
452
|
+
let _env = EnvGuard::unset([
|
|
453
|
+
"TMUX",
|
|
454
|
+
"TMUX_PANE",
|
|
455
|
+
"TEAM_AGENT_ID",
|
|
456
|
+
"TEAM_AGENT_TEAM_ID",
|
|
457
|
+
"TEAM_AGENT_LEADER_PANE_ID",
|
|
458
|
+
"TEAM_AGENT_LEADER_SESSION_UUID",
|
|
459
|
+
"TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
|
|
460
|
+
"TEAM_AGENT_LEADER_PROVIDER",
|
|
461
|
+
]);
|
|
462
|
+
let root = tmp_dir("no-positive-caller-degraded");
|
|
463
|
+
let team = fake_team_dir(&root, "unbound", "worker_a");
|
|
464
|
+
let out = Command::new(crate::lifecycle::tests::test_binary_path())
|
|
465
|
+
.args([
|
|
466
|
+
"quick-start",
|
|
467
|
+
team.to_str().unwrap(),
|
|
468
|
+
"--workspace",
|
|
469
|
+
root.to_str().unwrap(),
|
|
470
|
+
"--team-id",
|
|
471
|
+
"unbound",
|
|
472
|
+
"--yes",
|
|
473
|
+
"--json",
|
|
474
|
+
])
|
|
475
|
+
.current_dir(&root)
|
|
476
|
+
.output()
|
|
477
|
+
.expect("run team-agent quick-start");
|
|
478
|
+
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
479
|
+
let value: Value = serde_json::from_slice(&out.stdout).unwrap_or_else(|err| {
|
|
480
|
+
panic!(
|
|
481
|
+
"quick-start stdout must be JSON: {err}; stdout={stdout} stderr={}",
|
|
482
|
+
String::from_utf8_lossy(&out.stderr)
|
|
483
|
+
)
|
|
484
|
+
});
|
|
485
|
+
let state = load_runtime_state(&root).expect("state after unbound quick-start");
|
|
486
|
+
let receiver = state
|
|
487
|
+
.pointer("/teams/unbound/leader_receiver")
|
|
488
|
+
.or_else(|| state.pointer("/leader_receiver"))
|
|
489
|
+
.unwrap_or_else(|| panic!("leader_receiver must exist after quick-start; state={state}"));
|
|
490
|
+
|
|
491
|
+
assert_eq!(
|
|
492
|
+
receiver.get("status").and_then(Value::as_str),
|
|
493
|
+
Some("unbound"),
|
|
494
|
+
"fixture sanity: no positive caller pane should persist an unbound receiver; receiver={receiver} state={state}"
|
|
495
|
+
);
|
|
496
|
+
assert_ne!(
|
|
497
|
+
value.get("ok").and_then(Value::as_bool),
|
|
498
|
+
Some(true),
|
|
499
|
+
"C1-C3: quick-start --json must not return ok=true when launched leader_receiver is unbound; value={value} state={state}"
|
|
500
|
+
);
|
|
501
|
+
assert!(
|
|
502
|
+
value
|
|
503
|
+
.get("state")
|
|
504
|
+
.and_then(Value::as_str)
|
|
505
|
+
.is_some_and(|state| state == "leader_receiver_unbound")
|
|
506
|
+
|| value
|
|
507
|
+
.get("worker_readiness")
|
|
508
|
+
.and_then(|v| v.get("state"))
|
|
509
|
+
.and_then(Value::as_str)
|
|
510
|
+
.is_some_and(|state| state == "leader_receiver_unbound"),
|
|
511
|
+
"C2: unbound quick-start must return structured degraded state=leader_receiver_unbound; value={value}"
|
|
512
|
+
);
|
|
513
|
+
assert!(
|
|
514
|
+
value.to_string().contains("claim-leader"),
|
|
515
|
+
"C2: unbound quick-start must include a claim-leader recovery next action; value={value}"
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
#[test]
|
|
520
|
+
#[ignore = "real-machine: quick-start/session lifecycle gate"]
|
|
521
|
+
#[serial(env)]
|
|
522
|
+
fn delivery_does_not_autobind_unbound_receiver() {
|
|
523
|
+
let root = tmp_dir("delivery-no-autobind");
|
|
524
|
+
seed_unbound_delivery_state(&root);
|
|
525
|
+
let before = load_runtime_state(&root).expect("state before delivery");
|
|
526
|
+
let store = MessageStore::open(&root).unwrap();
|
|
527
|
+
let message_id = store
|
|
528
|
+
.create_message(
|
|
529
|
+
Some("task-1"),
|
|
530
|
+
"sibling_worker",
|
|
531
|
+
"leader",
|
|
532
|
+
"delivery must not claim while sending to leader",
|
|
533
|
+
None,
|
|
534
|
+
false,
|
|
535
|
+
Some("sibling"),
|
|
536
|
+
)
|
|
537
|
+
.unwrap();
|
|
538
|
+
let event_log = EventLog::new(&root);
|
|
539
|
+
let transport = SessionRecordingTransport::default();
|
|
540
|
+
|
|
541
|
+
let delivered = deliver_pending_messages(&root, &before, &transport, &event_log)
|
|
542
|
+
.expect("delivery should return a rebind-required outcome, not panic");
|
|
543
|
+
|
|
544
|
+
let after = load_runtime_state(&root).expect("state after delivery");
|
|
545
|
+
assert!(
|
|
546
|
+
delivered.is_empty(),
|
|
547
|
+
"unbound leader receiver must not be delivered by auto-binding during delivery; delivered={delivered:?}"
|
|
548
|
+
);
|
|
549
|
+
assert_eq!(
|
|
550
|
+
before.pointer("/teams/sibling/leader_receiver"),
|
|
551
|
+
after.pointer("/teams/sibling/leader_receiver"),
|
|
552
|
+
"C8/C14: delivery must not mutate leader_receiver/team_owner/owner_epoch while handling rebind_required; message_id={message_id} before={before} after={after}"
|
|
553
|
+
);
|
|
554
|
+
let events = std::fs::read_to_string(root.join(".team/logs/events.jsonl")).unwrap_or_default();
|
|
555
|
+
assert!(
|
|
556
|
+
events.contains("leader_not_attached") || events.contains("rebind_required"),
|
|
557
|
+
"unbound delivery must emit leader_not_attached/rebind_required; events={events}"
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
#[test]
|
|
562
|
+
fn delivery_grep_guard_no_owner_mutation() {
|
|
563
|
+
let delivery = std::fs::read_to_string(concat!(
|
|
564
|
+
env!("CARGO_MANIFEST_DIR"),
|
|
565
|
+
"/src/messaging/delivery.rs"
|
|
566
|
+
))
|
|
567
|
+
.unwrap();
|
|
568
|
+
let leader_receiver = std::fs::read_to_string(concat!(
|
|
569
|
+
env!("CARGO_MANIFEST_DIR"),
|
|
570
|
+
"/src/messaging/leader_receiver.rs"
|
|
571
|
+
))
|
|
572
|
+
.unwrap_or_default();
|
|
573
|
+
let send_to_leader_body = leader_receiver
|
|
574
|
+
.split("pub fn send_to_leader_receiver")
|
|
575
|
+
.nth(1)
|
|
576
|
+
.and_then(|tail| tail.split("pub fn mirror_peer_message_to_leader").next())
|
|
577
|
+
.unwrap_or("");
|
|
578
|
+
let send = std::fs::read_to_string(concat!(
|
|
579
|
+
env!("CARGO_MANIFEST_DIR"),
|
|
580
|
+
"/src/messaging/send.rs"
|
|
581
|
+
))
|
|
582
|
+
.unwrap();
|
|
583
|
+
let scheduler = std::fs::read_to_string(concat!(
|
|
584
|
+
env!("CARGO_MANIFEST_DIR"),
|
|
585
|
+
"/src/messaging/scheduler.rs"
|
|
586
|
+
))
|
|
587
|
+
.unwrap_or_default();
|
|
588
|
+
let tick = std::fs::read_to_string(concat!(
|
|
589
|
+
env!("CARGO_MANIFEST_DIR"),
|
|
590
|
+
"/src/coordinator/tick.rs"
|
|
591
|
+
))
|
|
592
|
+
.unwrap();
|
|
593
|
+
let global_delivery_guard =
|
|
594
|
+
format!("{delivery}\n{send_to_leader_body}\n{send}\n{scheduler}\n{tick}");
|
|
595
|
+
for forbidden in [
|
|
596
|
+
"seed_launched_owner",
|
|
597
|
+
"seed_unbound_launched_owner",
|
|
598
|
+
"claim_lease",
|
|
599
|
+
"claim_leader_receiver(",
|
|
600
|
+
] {
|
|
601
|
+
assert!(
|
|
602
|
+
!global_delivery_guard.contains(forbidden),
|
|
603
|
+
"C7/C14: delivery/send/scheduler/coordinator delivery paths must stay reclaim-neutral and must not write owner/receiver fields; forbidden={forbidden}"
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
#[test]
|
|
609
|
+
#[ignore = "real-machine: quick-start/session lifecycle gate"]
|
|
610
|
+
#[serial(env)]
|
|
611
|
+
fn worker_pane_seeded_as_leader_still_dropped() {
|
|
612
|
+
let _env = EnvGuard::set([
|
|
613
|
+
("TMUX_PANE", "%1-first"),
|
|
614
|
+
("TEAM_AGENT_LEADER_PANE_ID", ""),
|
|
615
|
+
("TEAM_AGENT_ID", ""),
|
|
616
|
+
("TEAM_AGENT_TEAM_ID", ""),
|
|
617
|
+
("TEAM_AGENT_LEADER_SESSION_UUID", ""),
|
|
618
|
+
("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", ""),
|
|
619
|
+
("TEAM_AGENT_LEADER_PROVIDER", ""),
|
|
620
|
+
("TEAM_AGENT_OWNER_TEAM_ID", ""),
|
|
621
|
+
]);
|
|
622
|
+
let root = tmp_dir("worker-pane-seeded-dropped");
|
|
623
|
+
let team = fake_team_dir(&root, "workerseed", "worker_a");
|
|
624
|
+
let transport = SessionRecordingTransport::default();
|
|
625
|
+
|
|
626
|
+
team_agent::lifecycle::quick_start_with_transport_in_workspace(
|
|
627
|
+
&root,
|
|
628
|
+
&team,
|
|
629
|
+
Some("workerseed"),
|
|
630
|
+
true,
|
|
631
|
+
Some("workerseed"),
|
|
632
|
+
&transport,
|
|
633
|
+
)
|
|
634
|
+
.expect("quick-start should complete with recording transport");
|
|
635
|
+
|
|
636
|
+
let state = load_runtime_state(&root).expect("state after worker-pane quick-start");
|
|
637
|
+
let receiver = state
|
|
638
|
+
.pointer("/teams/workerseed/leader_receiver")
|
|
639
|
+
.or_else(|| state.pointer("/leader_receiver"))
|
|
640
|
+
.unwrap_or_else(|| panic!("leader_receiver must exist; state={state}"));
|
|
641
|
+
assert_eq!(
|
|
642
|
+
receiver.get("status").and_then(Value::as_str),
|
|
643
|
+
Some("unbound"),
|
|
644
|
+
"C6: bare worker pane seeding must still be dropped; only a positive caller leader pane can bind. receiver={receiver} state={state}"
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
fn assert_ready_team(label: &str, report: &QuickStartReport, expected_session: &str) {
|
|
649
|
+
match report {
|
|
650
|
+
QuickStartReport::Ready { session_name, .. } => assert_eq!(
|
|
651
|
+
session_name.as_str(),
|
|
652
|
+
expected_session,
|
|
653
|
+
"{label}: session_name should derive from requested team identity"
|
|
654
|
+
),
|
|
655
|
+
other => panic!("{label}: expected Ready, got {other:?}"),
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
fn assert_team_present(state: &Value, team_key: &str, expected_session: &str) {
|
|
660
|
+
let Some(team) = state.get("teams").and_then(|teams| teams.get(team_key)) else {
|
|
661
|
+
panic!("state.teams.{team_key} must exist after sibling quick-start; state={state}");
|
|
662
|
+
};
|
|
663
|
+
assert_eq!(
|
|
664
|
+
team.get("session_name").and_then(Value::as_str),
|
|
665
|
+
Some(expected_session),
|
|
666
|
+
"state.teams.{team_key}.session_name must be isolated per requested team; team={team}"
|
|
667
|
+
);
|
|
668
|
+
assert!(
|
|
669
|
+
team.get("agents")
|
|
670
|
+
.and_then(Value::as_object)
|
|
671
|
+
.is_some_and(|agents| !agents.is_empty()),
|
|
672
|
+
"state.teams.{team_key}.agents must be retained; team={team}"
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
struct RealSiblingCase {
|
|
677
|
+
root: PathBuf,
|
|
678
|
+
backend: TmuxBackend,
|
|
679
|
+
leader_session: SessionName,
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
impl RealSiblingCase {
|
|
683
|
+
fn new(tag: &str) -> Self {
|
|
684
|
+
let root = tmp_dir(tag);
|
|
685
|
+
let backend = TmuxBackend::for_workspace(&root);
|
|
686
|
+
let leader_session = SessionName::new(format!(
|
|
687
|
+
"team-red2-leader-{}-{}",
|
|
688
|
+
std::process::id(),
|
|
689
|
+
root.file_name()
|
|
690
|
+
.and_then(|name| name.to_str())
|
|
691
|
+
.unwrap_or("case")
|
|
692
|
+
));
|
|
693
|
+
Self {
|
|
694
|
+
root,
|
|
695
|
+
backend,
|
|
696
|
+
leader_session,
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
fn spawn_leader_pane(&self) -> PaneId {
|
|
701
|
+
let result = self
|
|
702
|
+
.backend
|
|
703
|
+
.spawn_first(
|
|
704
|
+
&self.leader_session,
|
|
705
|
+
&WindowName::new("leader"),
|
|
706
|
+
&[
|
|
707
|
+
"sh".to_string(),
|
|
708
|
+
"-lc".to_string(),
|
|
709
|
+
"stty -echo 2>/dev/null; exec cat".to_string(),
|
|
710
|
+
],
|
|
711
|
+
&self.root,
|
|
712
|
+
&BTreeMap::new(),
|
|
713
|
+
)
|
|
714
|
+
.expect("spawn real tmux leader pane");
|
|
715
|
+
result.pane_id
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
fn run_team_agent_json(&self, args: &[&str], extra_env: &[(&str, &str)]) -> Value {
|
|
719
|
+
let output = self.run_team_agent_status(args, extra_env);
|
|
720
|
+
assert!(
|
|
721
|
+
output.status.success(),
|
|
722
|
+
"team-agent {:?} should exit 0; status={:?} stdout={} stderr={}",
|
|
723
|
+
args,
|
|
724
|
+
output.status.code(),
|
|
725
|
+
String::from_utf8_lossy(&output.stdout),
|
|
726
|
+
String::from_utf8_lossy(&output.stderr)
|
|
727
|
+
);
|
|
728
|
+
serde_json::from_slice::<Value>(&output.stdout).unwrap_or_else(|err| {
|
|
729
|
+
panic!(
|
|
730
|
+
"team-agent {:?} stdout must be JSON: {err}; stdout={} stderr={}",
|
|
731
|
+
args,
|
|
732
|
+
String::from_utf8_lossy(&output.stdout),
|
|
733
|
+
String::from_utf8_lossy(&output.stderr)
|
|
734
|
+
)
|
|
735
|
+
})
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
fn run_team_agent_status(
|
|
739
|
+
&self,
|
|
740
|
+
args: &[&str],
|
|
741
|
+
extra_env: &[(&str, &str)],
|
|
742
|
+
) -> std::process::Output {
|
|
743
|
+
let mut command = Command::new(crate::lifecycle::tests::test_binary_path());
|
|
744
|
+
command.args(args);
|
|
745
|
+
command.current_dir(&self.root);
|
|
746
|
+
for key in [
|
|
747
|
+
"TEAM_AGENT_ID",
|
|
748
|
+
"TEAM_AGENT_TEAM_ID",
|
|
749
|
+
"TEAM_AGENT_OWNER_TEAM_ID",
|
|
750
|
+
"TEAM_AGENT_LEADER_PANE_ID",
|
|
751
|
+
"TEAM_AGENT_LEADER_SESSION_UUID",
|
|
752
|
+
"TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
|
|
753
|
+
"TEAM_AGENT_LEADER_PROVIDER",
|
|
754
|
+
"TMUX",
|
|
755
|
+
"TMUX_PANE",
|
|
756
|
+
] {
|
|
757
|
+
command.env_remove(key);
|
|
758
|
+
}
|
|
759
|
+
for (key, value) in extra_env {
|
|
760
|
+
command.env(key, value);
|
|
761
|
+
}
|
|
762
|
+
command.output().expect("run team-agent binary")
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
fn capture_pane(&self, pane: &PaneId) -> String {
|
|
766
|
+
self.backend
|
|
767
|
+
.capture(&Target::Pane(pane.clone()), CaptureRange::Full)
|
|
768
|
+
.expect("capture leader pane")
|
|
769
|
+
.text
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
impl Drop for RealSiblingCase {
|
|
774
|
+
fn drop(&mut self) {
|
|
775
|
+
let _ = self.backend.kill_server();
|
|
776
|
+
let _ = std::fs::remove_dir_all(&self.root);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
fn message_row_for_token(workspace: &Path, token: &str) -> Option<(String, Option<String>)> {
|
|
781
|
+
let db = workspace.join(".team/runtime/team.db");
|
|
782
|
+
let conn = team_agent::db::schema::open_db(&db).ok()?;
|
|
783
|
+
conn.query_row(
|
|
784
|
+
"select status, delivered_at
|
|
785
|
+
from messages
|
|
786
|
+
where owner_team_id='sibling'
|
|
787
|
+
and sender='sibling_worker'
|
|
788
|
+
and recipient='leader'
|
|
789
|
+
and content like ?1
|
|
790
|
+
order by created_at desc
|
|
791
|
+
limit 1",
|
|
792
|
+
params![format!("%{token}%")],
|
|
793
|
+
|row| Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?)),
|
|
794
|
+
)
|
|
795
|
+
.ok()
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
fn fake_team_dir(root: &Path, name: &str, agent_id: &str) -> PathBuf {
|
|
799
|
+
let team = root.join(name);
|
|
800
|
+
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
801
|
+
std::fs::write(
|
|
802
|
+
team.join("TEAM.md"),
|
|
803
|
+
format!(
|
|
804
|
+
"---\nname: {name}\nobjective: Real sibling quick-start receiver binding contract.\nprovider: fake\n---\n\n{name} team.\n"
|
|
805
|
+
),
|
|
806
|
+
)
|
|
807
|
+
.unwrap();
|
|
808
|
+
std::fs::write(
|
|
809
|
+
team.join("agents").join(format!("{agent_id}.md")),
|
|
810
|
+
format!(
|
|
811
|
+
"---\nname: {agent_id}\nrole: Worker\nprovider: fake\ntools:\n - mcp_team\n---\n\nWorker.\n"
|
|
812
|
+
),
|
|
813
|
+
)
|
|
814
|
+
.unwrap();
|
|
815
|
+
team
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
fn seed_unbound_delivery_state(root: &Path) {
|
|
819
|
+
team_agent::state::persist::save_runtime_state(
|
|
820
|
+
root,
|
|
821
|
+
&serde_json::json!({
|
|
822
|
+
"active_team_key": "sibling",
|
|
823
|
+
"session_name": "team-sibling",
|
|
824
|
+
"agents": {
|
|
825
|
+
"sibling_worker": {
|
|
826
|
+
"status": "running",
|
|
827
|
+
"provider": "fake",
|
|
828
|
+
"window": "sibling_worker",
|
|
829
|
+
"owner_team_id": "sibling"
|
|
830
|
+
}
|
|
831
|
+
},
|
|
832
|
+
"teams": {
|
|
833
|
+
"sibling": {
|
|
834
|
+
"session_name": "team-sibling",
|
|
835
|
+
"leader_receiver": {
|
|
836
|
+
"mode": "direct_tmux",
|
|
837
|
+
"status": "unbound",
|
|
838
|
+
"provider": "codex",
|
|
839
|
+
"owner_epoch": 1
|
|
840
|
+
},
|
|
841
|
+
"team_owner": {
|
|
842
|
+
"provider": "codex",
|
|
843
|
+
"owner_epoch": 1
|
|
844
|
+
},
|
|
845
|
+
"agents": {
|
|
846
|
+
"sibling_worker": {
|
|
847
|
+
"status": "running",
|
|
848
|
+
"provider": "fake",
|
|
849
|
+
"window": "sibling_worker",
|
|
850
|
+
"owner_team_id": "sibling"
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}),
|
|
856
|
+
)
|
|
857
|
+
.unwrap();
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
fn team_dir(root: &Path, name: &str, agent_id: &str) -> PathBuf {
|
|
861
|
+
team_dir_with_name(root, name, name, agent_id)
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
fn team_dir_with_name(root: &Path, dir_name: &str, spec_name: &str, agent_id: &str) -> PathBuf {
|
|
865
|
+
let team = root.join(dir_name);
|
|
866
|
+
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
867
|
+
std::fs::write(
|
|
868
|
+
team.join("TEAM.md"),
|
|
869
|
+
format!(
|
|
870
|
+
"---\nname: {spec_name}\nobjective: Sibling team quick-start contract.\nprovider: codex\n---\n\n{spec_name} team.\n"
|
|
871
|
+
),
|
|
872
|
+
)
|
|
873
|
+
.unwrap();
|
|
874
|
+
std::fs::write(
|
|
875
|
+
team.join("agents").join(format!("{agent_id}.md")),
|
|
876
|
+
role_doc(agent_id),
|
|
877
|
+
)
|
|
878
|
+
.unwrap();
|
|
879
|
+
team
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
fn role_doc(name: &str) -> String {
|
|
883
|
+
format!(
|
|
884
|
+
"---\nname: {name}\nrole: Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nWorker.\n"
|
|
885
|
+
)
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
fn seed_healthy_coordinator(workspace: &Path) {
|
|
889
|
+
let workspace = team_agent::coordinator::WorkspacePath::new(workspace.to_path_buf());
|
|
890
|
+
std::fs::create_dir_all(team_agent::model::paths::runtime_dir(workspace.as_path())).unwrap();
|
|
891
|
+
let _ = team_agent::message_store::MessageStore::open(workspace.as_path()).unwrap();
|
|
892
|
+
let pid = team_agent::coordinator::Pid::new(std::process::id());
|
|
893
|
+
team_agent::coordinator::write_coordinator_metadata(
|
|
894
|
+
&workspace,
|
|
895
|
+
pid,
|
|
896
|
+
team_agent::coordinator::MetadataSource::Boot,
|
|
897
|
+
)
|
|
898
|
+
.unwrap();
|
|
899
|
+
std::fs::write(
|
|
900
|
+
team_agent::coordinator::coordinator_pid_path(&workspace),
|
|
901
|
+
pid.to_string(),
|
|
902
|
+
)
|
|
903
|
+
.unwrap();
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
fn tmp_dir(tag: &str) -> PathBuf {
|
|
907
|
+
static N: AtomicU64 = AtomicU64::new(0);
|
|
908
|
+
let dir = std::env::temp_dir().join(format!(
|
|
909
|
+
"ta-rs-team-in-team-sibling-{tag}-{}-{}",
|
|
910
|
+
std::process::id(),
|
|
911
|
+
N.fetch_add(1, Ordering::Relaxed)
|
|
912
|
+
));
|
|
913
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
914
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
915
|
+
std::fs::canonicalize(dir).unwrap()
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
struct EnvGuard {
|
|
919
|
+
previous: Vec<(&'static str, Option<String>)>,
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
impl EnvGuard {
|
|
923
|
+
fn unset(keys: [&'static str; 8]) -> Self {
|
|
924
|
+
let previous = keys
|
|
925
|
+
.iter()
|
|
926
|
+
.map(|key| (*key, std::env::var(key).ok()))
|
|
927
|
+
.collect::<Vec<_>>();
|
|
928
|
+
for key in keys {
|
|
929
|
+
unsafe {
|
|
930
|
+
std::env::remove_var(key);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
Self { previous }
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
fn set(values: [(&'static str, &'static str); 8]) -> Self {
|
|
937
|
+
let previous = values
|
|
938
|
+
.iter()
|
|
939
|
+
.map(|(key, _)| (*key, std::env::var(key).ok()))
|
|
940
|
+
.collect::<Vec<_>>();
|
|
941
|
+
for (key, value) in values {
|
|
942
|
+
unsafe {
|
|
943
|
+
if value.is_empty() {
|
|
944
|
+
std::env::remove_var(key);
|
|
945
|
+
} else {
|
|
946
|
+
std::env::set_var(key, value);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
Self { previous }
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
impl Drop for EnvGuard {
|
|
955
|
+
fn drop(&mut self) {
|
|
956
|
+
for (key, value) in self.previous.drain(..).rev() {
|
|
957
|
+
unsafe {
|
|
958
|
+
if let Some(value) = value {
|
|
959
|
+
std::env::set_var(key, value);
|
|
960
|
+
} else {
|
|
961
|
+
std::env::remove_var(key);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
#[derive(Debug, Default)]
|
|
969
|
+
struct SessionRecordingTransport {
|
|
970
|
+
sessions: Mutex<HashSet<String>>,
|
|
971
|
+
spawned: Mutex<Vec<String>>,
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
impl SessionRecordingTransport {
|
|
975
|
+
fn spawned_sessions(&self) -> Vec<String> {
|
|
976
|
+
self.spawned.lock().unwrap().clone()
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
fn spawn_result(
|
|
980
|
+
&self,
|
|
981
|
+
session: &SessionName,
|
|
982
|
+
window: &WindowName,
|
|
983
|
+
kind: &'static str,
|
|
984
|
+
) -> SpawnResult {
|
|
985
|
+
self.sessions
|
|
986
|
+
.lock()
|
|
987
|
+
.unwrap()
|
|
988
|
+
.insert(session.as_str().to_string());
|
|
989
|
+
let mut spawned = self.spawned.lock().unwrap();
|
|
990
|
+
spawned.push(session.as_str().to_string());
|
|
991
|
+
SpawnResult {
|
|
992
|
+
pane_id: PaneId::new(format!("%{}-{kind}", spawned.len())),
|
|
993
|
+
session: session.clone(),
|
|
994
|
+
window: window.clone(),
|
|
995
|
+
child_pid: None,
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
impl Transport for SessionRecordingTransport {
|
|
1001
|
+
fn kind(&self) -> BackendKind {
|
|
1002
|
+
BackendKind::Tmux
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
fn spawn_first(
|
|
1006
|
+
&self,
|
|
1007
|
+
session: &SessionName,
|
|
1008
|
+
window: &WindowName,
|
|
1009
|
+
_argv: &[String],
|
|
1010
|
+
_cwd: &Path,
|
|
1011
|
+
_env: &BTreeMap<String, String>,
|
|
1012
|
+
) -> Result<SpawnResult, TransportError> {
|
|
1013
|
+
Ok(self.spawn_result(session, window, "first"))
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
fn spawn_into(
|
|
1017
|
+
&self,
|
|
1018
|
+
session: &SessionName,
|
|
1019
|
+
window: &WindowName,
|
|
1020
|
+
_argv: &[String],
|
|
1021
|
+
_cwd: &Path,
|
|
1022
|
+
_env: &BTreeMap<String, String>,
|
|
1023
|
+
) -> Result<SpawnResult, TransportError> {
|
|
1024
|
+
Ok(self.spawn_result(session, window, "into"))
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
fn inject(
|
|
1028
|
+
&self,
|
|
1029
|
+
_target: &Target,
|
|
1030
|
+
_payload: &InjectPayload,
|
|
1031
|
+
_submit: Key,
|
|
1032
|
+
_bracketed: bool,
|
|
1033
|
+
) -> Result<InjectReport, TransportError> {
|
|
1034
|
+
Ok(InjectReport {
|
|
1035
|
+
stage_reached: InjectStage::Submit,
|
|
1036
|
+
inject_verification: InjectVerification::CaptureContainsToken,
|
|
1037
|
+
submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
|
|
1038
|
+
turn_verification: TurnVerification::NotYetObserved,
|
|
1039
|
+
attempts: 1,
|
|
1040
|
+
submit_diagnostics: None,
|
|
1041
|
+
})
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
fn send_keys(&self, _target: &Target, _keys: &[Key]) -> Result<(), TransportError> {
|
|
1045
|
+
Ok(())
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
fn capture(
|
|
1049
|
+
&self,
|
|
1050
|
+
_target: &Target,
|
|
1051
|
+
range: CaptureRange,
|
|
1052
|
+
) -> Result<CapturedText, TransportError> {
|
|
1053
|
+
Ok(CapturedText {
|
|
1054
|
+
text: String::new(),
|
|
1055
|
+
range,
|
|
1056
|
+
})
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
fn query(&self, _target: &Target, _field: PaneField) -> Result<Option<String>, TransportError> {
|
|
1060
|
+
Ok(None)
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
fn liveness(
|
|
1064
|
+
&self,
|
|
1065
|
+
_pane: &PaneId,
|
|
1066
|
+
) -> Result<team_agent::transport::PaneLiveness, TransportError> {
|
|
1067
|
+
Ok(team_agent::transport::PaneLiveness::Live)
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
1071
|
+
Ok(Vec::new())
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
fn has_session(&self, session: &SessionName) -> Result<bool, TransportError> {
|
|
1075
|
+
Ok(self.sessions.lock().unwrap().contains(session.as_str()))
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
|
|
1079
|
+
Ok(Vec::new())
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
fn set_session_env(
|
|
1083
|
+
&self,
|
|
1084
|
+
_session: &SessionName,
|
|
1085
|
+
_key: &str,
|
|
1086
|
+
_value: &str,
|
|
1087
|
+
) -> Result<SetEnvOutcome, TransportError> {
|
|
1088
|
+
Ok(SetEnvOutcome::Applied)
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
|
|
1092
|
+
self.sessions.lock().unwrap().remove(_session.as_str());
|
|
1093
|
+
Ok(())
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
|
|
1097
|
+
Ok(())
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
|
|
1101
|
+
Ok(AttachOutcome::Attached)
|
|
1102
|
+
}
|
|
1103
|
+
}
|