@team-agent/installer 0.5.42 → 0.5.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +129 -54
- package/crates/team-agent/src/cli/diagnose.rs +1 -2
- package/crates/team-agent/src/cli/emit.rs +1 -7
- package/crates/team-agent/src/cli/helpers.rs +3 -1
- package/crates/team-agent/src/cli/leader.rs +2 -1
- package/crates/team-agent/src/cli/mod.rs +126 -16
- package/crates/team-agent/src/cli/named_address.rs +14 -5
- package/crates/team-agent/src/cli/profile.rs +19 -7
- package/crates/team-agent/src/cli/send.rs +9 -3
- package/crates/team-agent/src/cli/status.rs +8 -30
- package/crates/team-agent/src/cli/status_port.rs +1341 -1272
- package/crates/team-agent/src/cli/tests/base.rs +738 -660
- package/crates/team-agent/src/cli/tests/compile.rs +45 -18
- package/crates/team-agent/src/cli/tests/divergence.rs +462 -444
- package/crates/team-agent/src/cli/tests/lane_c.rs +365 -282
- package/crates/team-agent/src/cli/tests/leader_watch.rs +356 -329
- package/crates/team-agent/src/cli/tests/main_preserved.rs +672 -564
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +284 -224
- package/crates/team-agent/src/cli/tests/mod.rs +17 -7
- package/crates/team-agent/src/cli/tests/named_address.rs +8 -2
- package/crates/team-agent/src/cli/tests/peer_allow.rs +10 -2
- package/crates/team-agent/src/cli/tests/run_delegation.rs +314 -273
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +26 -15
- package/crates/team-agent/src/cli/tests/status_send.rs +707 -629
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +20 -4
- package/crates/team-agent/src/cli/tests/verb_profile.rs +46 -17
- package/crates/team-agent/src/cli/tests/verb_validate.rs +15 -3
- package/crates/team-agent/src/codex_app_server.rs +2 -5
- package/crates/team-agent/src/compiler/tests.rs +139 -33
- package/crates/team-agent/src/compiler.rs +55 -22
- package/crates/team-agent/src/conpty/backend.rs +23 -33
- package/crates/team-agent/src/coordinator/backoff.rs +2 -7
- package/crates/team-agent/src/coordinator/conpty_shim.rs +55 -67
- package/crates/team-agent/src/coordinator/health.rs +46 -31
- package/crates/team-agent/src/coordinator/mod.rs +3 -3
- package/crates/team-agent/src/coordinator/orphan.rs +22 -10
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +51 -56
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +55 -19
- package/crates/team-agent/src/coordinator/tests/basics.rs +179 -41
- package/crates/team-agent/src/coordinator/tests/daemon.rs +53 -13
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +78 -19
- package/crates/team-agent/src/coordinator/tests/main_preserved.rs +61 -11
- package/crates/team-agent/src/coordinator/tests/mod.rs +33 -39
- package/crates/team-agent/src/coordinator/tests/spine.rs +52 -12
- package/crates/team-agent/src/coordinator/tests/takeover.rs +73 -15
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +50 -15
- package/crates/team-agent/src/coordinator/tests/watch.rs +74 -20
- package/crates/team-agent/src/db/message_store.rs +138 -30
- package/crates/team-agent/src/db/migration.rs +249 -61
- package/crates/team-agent/src/db/schema.rs +303 -82
- package/crates/team-agent/src/diagnose/comms.rs +9 -2
- package/crates/team-agent/src/diagnose/mod.rs +1 -3
- package/crates/team-agent/src/diagnose/orphans.rs +79 -61
- package/crates/team-agent/src/event_log.rs +70 -16
- package/crates/team-agent/src/layout/manager.rs +15 -4
- package/crates/team-agent/src/layout/mod.rs +4 -4
- package/crates/team-agent/src/layout/overlay.rs +10 -3
- package/crates/team-agent/src/layout/placement.rs +5 -1
- package/crates/team-agent/src/layout/recovery.rs +4 -2
- package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
- package/crates/team-agent/src/layout/sessions.rs +17 -9
- package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
- package/crates/team-agent/src/layout/worker_env.rs +87 -19
- package/crates/team-agent/src/leader/helpers.rs +7 -1
- package/crates/team-agent/src/leader/lease.rs +199 -89
- package/crates/team-agent/src/leader/owner_bind.rs +55 -22
- package/crates/team-agent/src/leader/provider_attribution.rs +25 -6
- package/crates/team-agent/src/leader/rediscover/tests.rs +88 -24
- package/crates/team-agent/src/leader/rediscover.rs +74 -25
- package/crates/team-agent/src/leader/registry.rs +1 -1
- package/crates/team-agent/src/leader/start.rs +75 -54
- package/crates/team-agent/src/leader/takeover.rs +46 -11
- package/crates/team-agent/src/leader/tests/basics.rs +320 -167
- package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
- package/crates/team-agent/src/leader/tests/identity.rs +428 -356
- package/crates/team-agent/src/leader/tests/idle.rs +285 -254
- package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
- package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
- package/crates/team-agent/src/leader/tests/mod.rs +115 -99
- package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
- package/crates/team-agent/src/lib.rs +4 -4
- package/crates/team-agent/src/lifecycle/display.rs +7 -3
- package/crates/team-agent/src/lifecycle/launch.rs +55 -15
- package/crates/team-agent/src/lifecycle/mod.rs +9 -1
- package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
- package/crates/team-agent/src/lifecycle/restart/common.rs +6 -2
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
- package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
- package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests.rs +2 -2
- package/crates/team-agent/src/main.rs +4 -4
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
- package/crates/team-agent/src/mcp_server/mod.rs +11 -2
- package/crates/team-agent/src/mcp_server/types.rs +14 -3
- package/crates/team-agent/src/mcp_server/wire.rs +230 -68
- package/crates/team-agent/src/messaging/delivery.rs +20 -18
- package/crates/team-agent/src/messaging/helpers.rs +25 -4
- package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
- package/crates/team-agent/src/messaging/mod.rs +2 -3
- package/crates/team-agent/src/messaging/selftest.rs +46 -26
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
- package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
- package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
- package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
- package/crates/team-agent/src/messaging/trust.rs +25 -2
- package/crates/team-agent/src/messaging/watchers.rs +37 -9
- package/crates/team-agent/src/model/enums.rs +65 -16
- package/crates/team-agent/src/model/ids.rs +12 -3
- package/crates/team-agent/src/model/paths.rs +28 -7
- package/crates/team-agent/src/model/permissions.rs +176 -33
- package/crates/team-agent/src/model/routing.rs +66 -20
- package/crates/team-agent/src/model/spec.rs +365 -69
- package/crates/team-agent/src/model/task_graph.rs +36 -9
- package/crates/team-agent/src/model/yaml/tests.rs +24 -6
- package/crates/team-agent/src/model/yaml.rs +7 -6
- package/crates/team-agent/src/packaging/install.rs +23 -9
- package/crates/team-agent/src/packaging/migrate.rs +5 -7
- package/crates/team-agent/src/packaging/mod.rs +9 -1
- package/crates/team-agent/src/packaging/repair.rs +13 -6
- package/crates/team-agent/src/packaging/tests.rs +63 -16
- package/crates/team-agent/src/packaging/types.rs +22 -7
- package/crates/team-agent/src/platform/argv.rs +4 -1
- package/crates/team-agent/src/platform/file_lock.rs +22 -8
- package/crates/team-agent/src/platform/process.rs +54 -24
- package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
- package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
- package/crates/team-agent/src/provider/classify.rs +127 -42
- package/crates/team-agent/src/provider/faults.rs +17 -5
- package/crates/team-agent/src/provider/helpers.rs +6 -5
- package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
- package/crates/team-agent/src/state/persist.rs +28 -0
- package/crates/team-agent/src/state/repository.rs +8 -3
- package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
- package/crates/team-agent/src/tmux_backend.rs +80 -44
- package/crates/team-agent/src/topology.rs +40 -20
- package/crates/team-agent/src/transport/test_support.rs +20 -23
- package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
- package/crates/team-agent/src/transport/tests/mod.rs +178 -187
- package/crates/team-agent/src/transport/tests/wire.rs +561 -525
- package/crates/team-agent/src/transport.rs +12 -17
- package/crates/team-agent/src/transport_factory.rs +29 -14
- package/package.json +4 -4
|
@@ -1,634 +1,684 @@
|
|
|
1
1
|
use super::*;
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
// = RED. The porter must reproduce the EXACT LeaseResult shape per outcome.
|
|
4
|
+
// We seed state.json directly and inject a PaneLivenessProbe so no real tmux
|
|
5
|
+
// is needed for the no-pane / live-owner / dead-owner gate decisions.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
)
|
|
60
|
-
|
|
7
|
+
/// Injectable liveness probe (`state::owner_gate::PaneLivenessProbe`): a pane
|
|
8
|
+
/// in the seeded set is `Live`, otherwise `Dead`. The porter's real probe
|
|
9
|
+
/// (step 9 tmux) replaces it; here it drives the no-incident lease gate
|
|
10
|
+
/// (bound-pane liveness + caller eligibility) deterministically with no tmux.
|
|
11
|
+
#[test]
|
|
12
|
+
#[serial_test::serial(env)]
|
|
13
|
+
// Foundation-0 F0-2 conversion: `claim_lease_no_incident` used to
|
|
14
|
+
// dual-write the legacy per-session snapshot; that is retired
|
|
15
|
+
// (`.team/artifacts/foundation-0-slice-design.md` §§4-5). Canonical
|
|
16
|
+
// teams.<team_key> now carries the sole authoritative owner tuple.
|
|
17
|
+
fn claim_lease_no_incident_vacant_acquire_advances_epoch_and_writes_canonical_only() {
|
|
18
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
19
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
20
|
+
let ws = p2_temp_ws("claim_vacant");
|
|
21
|
+
let team_id = TeamKey::new("current");
|
|
22
|
+
let caller = PaneId::new("%5");
|
|
23
|
+
let mut state = serde_json::json!({"session_name": "team-agent-x"});
|
|
24
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
25
|
+
let live = seeded_liveness(&["%5"]);
|
|
26
|
+
let r = claim_lease_no_incident(
|
|
27
|
+
&ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
|
|
28
|
+
)
|
|
29
|
+
.unwrap();
|
|
30
|
+
assert!(r.ok);
|
|
31
|
+
assert_eq!(r.status, LeaseStatus::Claimed);
|
|
32
|
+
assert_eq!(r.reason, Some(LeaseReason::VacantAcquired));
|
|
33
|
+
assert_eq!(r.owner_epoch, Some(OwnerEpoch(1)), "vacant acquire 0→1");
|
|
34
|
+
let owner = r.owner.as_ref().expect("claimed → owner");
|
|
35
|
+
assert_eq!(owner.pane_id, caller);
|
|
36
|
+
assert_eq!(owner.owner_epoch, OwnerEpoch(1));
|
|
37
|
+
assert_eq!(owner.claimed_via, ClaimedVia::ClaimLeader);
|
|
38
|
+
let receiver = r.receiver.as_ref().expect("claimed → receiver");
|
|
39
|
+
assert_eq!(receiver.pane_id, caller);
|
|
40
|
+
assert_eq!(receiver.discovery, Some(Discovery::ClaimLeader));
|
|
41
|
+
let ws_path = crate::state::persist::runtime_state_path(&ws);
|
|
42
|
+
let ws_state: serde_json::Value =
|
|
43
|
+
serde_json::from_str(&std::fs::read_to_string(&ws_path).unwrap()).unwrap();
|
|
44
|
+
let canonical_key = crate::state::projection::team_state_key(&ws_state);
|
|
45
|
+
let ws_owner = crate::state::ownership::read_owner_value(&ws_state, &canonical_key)
|
|
46
|
+
.expect("Stage 3d: vacant acquire writes canonical owner");
|
|
47
|
+
assert_eq!(ws_owner["pane_id"], serde_json::json!("%5"));
|
|
48
|
+
assert_eq!(ws_owner["owner_epoch"], serde_json::json!(1));
|
|
49
|
+
let _ = &team_id;
|
|
50
|
+
// F0-2: canonical claim path must NOT side-effect the legacy
|
|
51
|
+
// per-session snapshot any more.
|
|
52
|
+
let snap_path = crate::model::paths::runtime_dir(&ws)
|
|
53
|
+
.join("teams")
|
|
54
|
+
.join("team-agent-x")
|
|
55
|
+
.join("state.json");
|
|
56
|
+
assert!(
|
|
57
|
+
!snap_path.exists(),
|
|
58
|
+
"F0-2: vacant claim must not write a legacy per-session snapshot; found {}",
|
|
59
|
+
snap_path.display(),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
63
|
+
// RED — DEAD-OWNER RECOVER: bound pane %1 absent from live set, caller %5
|
|
64
|
+
// eligible, confirm=false → still acquires (dead owner never blocks),
|
|
65
|
+
// reason=previous_owner_pane_dead, epoch 3→4. golden /tmp/probe_confirm.py.
|
|
66
|
+
#[test]
|
|
67
|
+
#[serial_test::serial(env)]
|
|
68
|
+
fn claim_lease_no_incident_dead_owner_recovers_without_confirm() {
|
|
69
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
70
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
71
|
+
let ws = p2_temp_ws("claim_dead");
|
|
72
|
+
let team_id = TeamKey::new("current");
|
|
73
|
+
let caller = PaneId::new("%5");
|
|
74
|
+
let mut state = serde_json::json!({
|
|
75
|
+
"session_name": "team-agent-x",
|
|
76
|
+
"team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"OLDUUID","owner_epoch":3,"claimed_at":"t","claimed_via":"claim-leader"},
|
|
77
|
+
"leader_receiver": {"pane_id":"%1","owner_epoch":3,"leader_session_uuid":"OLDUUID"},
|
|
78
|
+
});
|
|
79
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
80
|
+
// %1 (the recorded owner) is NOT live; only the caller %5 is live.
|
|
81
|
+
let live = seeded_liveness(&["%5"]);
|
|
82
|
+
let r = claim_lease_no_incident(
|
|
83
|
+
&ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
|
|
84
|
+
)
|
|
85
|
+
.unwrap();
|
|
86
|
+
assert!(r.ok);
|
|
87
|
+
assert_eq!(r.status, LeaseStatus::Claimed);
|
|
88
|
+
assert_eq!(r.reason, Some(LeaseReason::PreviousOwnerPaneDead));
|
|
89
|
+
assert_eq!(r.owner_epoch, Some(OwnerEpoch(4)), "dead-owner recover 3→4");
|
|
90
|
+
assert_eq!(r.owner.as_ref().unwrap().pane_id, caller);
|
|
91
|
+
}
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
93
|
+
// RED — LIVE-OWNER REFUSAL (no --confirm): bound pane %1 live, caller %5,
|
|
94
|
+
// confirm=false → refused force_confirm_required + action + bound_pane_id=%1
|
|
95
|
+
// + owner_epoch=2 (precheck epoch, NOT advanced). golden /tmp/probe_confirm.py.
|
|
96
|
+
#[test]
|
|
97
|
+
#[serial_test::serial(env)]
|
|
98
|
+
fn claim_lease_no_incident_live_owner_refuses_without_confirm() {
|
|
99
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
100
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
101
|
+
let ws = p2_temp_ws("claim_live");
|
|
102
|
+
let team_id = TeamKey::new("current");
|
|
103
|
+
let caller = PaneId::new("%5");
|
|
104
|
+
let mut state = serde_json::json!({
|
|
105
|
+
"session_name": "team-agent-x",
|
|
106
|
+
"team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"OWNERUUID","owner_epoch":2,"claimed_at":"t","claimed_via":"claim-leader"},
|
|
107
|
+
"leader_receiver": {"pane_id":"%1","owner_epoch":2,"leader_session_uuid":"OWNERUUID"},
|
|
108
|
+
});
|
|
109
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
110
|
+
// both %1 (live owner, matching uuid) and %5 (eligible caller) live.
|
|
111
|
+
let live = seeded_liveness(&["%1", "%5"]);
|
|
112
|
+
let r = claim_lease_no_incident(
|
|
113
|
+
&ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
|
|
114
|
+
)
|
|
115
|
+
.unwrap();
|
|
116
|
+
assert!(!r.ok);
|
|
117
|
+
assert_eq!(r.status, LeaseStatus::Refused);
|
|
118
|
+
assert_eq!(r.reason, Some(LeaseReason::ForceConfirmRequired));
|
|
119
|
+
assert_eq!(
|
|
120
|
+
r.action.as_deref(),
|
|
121
|
+
Some("rerun with --confirm to take over the live leader pane")
|
|
122
|
+
);
|
|
123
|
+
assert_eq!(
|
|
124
|
+
r.bound_pane_id,
|
|
125
|
+
Some(PaneId::new("%1")),
|
|
126
|
+
"refusal carries bound pane"
|
|
127
|
+
);
|
|
128
|
+
assert_eq!(
|
|
129
|
+
r.owner_epoch,
|
|
130
|
+
Some(OwnerEpoch(2)),
|
|
131
|
+
"refused → precheck epoch, NOT advanced"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
125
134
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
135
|
+
// RED — ALREADY-BOUND: caller pane == bound pane → status=AlreadyBound,
|
|
136
|
+
// ok=true, owner_epoch unchanged (no advance, no rewrite). golden probe_claimed.py.
|
|
137
|
+
#[test]
|
|
138
|
+
#[serial_test::serial(env)]
|
|
139
|
+
fn claim_lease_no_incident_already_bound_when_caller_is_bound_pane() {
|
|
140
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
141
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
142
|
+
let ws = p2_temp_ws("claim_bound");
|
|
143
|
+
let team_id = TeamKey::new("current");
|
|
144
|
+
let caller = PaneId::new("%5");
|
|
145
|
+
let mut state = serde_json::json!({
|
|
146
|
+
"session_name": "team-agent-x",
|
|
147
|
+
"team_owner": {"pane_id":"%5","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":1,"claimed_at":"t","claimed_via":"claim-leader"},
|
|
148
|
+
"leader_receiver": {"pane_id":"%5","owner_epoch":1,"leader_session_uuid":"U"},
|
|
149
|
+
});
|
|
150
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
151
|
+
let live = seeded_liveness(&["%5"]);
|
|
152
|
+
let r = claim_lease_no_incident(
|
|
153
|
+
&ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
|
|
154
|
+
)
|
|
155
|
+
.unwrap();
|
|
156
|
+
assert!(r.ok);
|
|
157
|
+
assert_eq!(r.status, LeaseStatus::AlreadyBound);
|
|
158
|
+
assert_eq!(
|
|
159
|
+
r.owner_epoch,
|
|
160
|
+
Some(OwnerEpoch(1)),
|
|
161
|
+
"already-bound → epoch unchanged"
|
|
162
|
+
);
|
|
163
|
+
assert!(
|
|
164
|
+
r.reason.is_none(),
|
|
165
|
+
"already-bound path carries no acquire reason"
|
|
166
|
+
);
|
|
167
|
+
}
|
|
152
168
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
#[test]
|
|
170
|
+
#[serial_test::serial(env)]
|
|
171
|
+
fn claim_leader_persists_full_tmux_endpoint_when_tmux_tmpdir_differs_from_coordinator() {
|
|
172
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
173
|
+
let leader_socket = "/tmp/ta-leader-root/tmux-501/dl2f";
|
|
174
|
+
let _e = EnvGuard::apply(&[
|
|
175
|
+
("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
|
|
176
|
+
("TMUX", Some("/tmp/ta-leader-root/tmux-501/dl2f,12345,0")),
|
|
177
|
+
("TMUX_TMPDIR", Some("/tmp/ta-coordinator-root")),
|
|
178
|
+
]);
|
|
179
|
+
let ws = p2_temp_ws("claim_full_endpoint");
|
|
180
|
+
let team_id = TeamKey::new("current");
|
|
181
|
+
let caller = PaneId::new("%5");
|
|
182
|
+
let mut state = serde_json::json!({"session_name": "team-agent-x"});
|
|
183
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
184
|
+
let live = seeded_liveness(&["%5"]);
|
|
169
185
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
186
|
+
let r = claim_lease_no_incident(
|
|
187
|
+
&ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
|
|
188
|
+
)
|
|
189
|
+
.unwrap();
|
|
174
190
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
.unwrap()
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
)
|
|
194
|
-
|
|
191
|
+
assert!(r.ok);
|
|
192
|
+
assert_eq!(r.status, LeaseStatus::Claimed);
|
|
193
|
+
assert_eq!(
|
|
194
|
+
r.receiver
|
|
195
|
+
.as_ref()
|
|
196
|
+
.and_then(|receiver| receiver.tmux_socket.as_deref()),
|
|
197
|
+
Some(leader_socket),
|
|
198
|
+
"claim-leader must persist the full $TMUX socket path, not only the -L short name"
|
|
199
|
+
);
|
|
200
|
+
let persisted: serde_json::Value = serde_json::from_str(
|
|
201
|
+
&std::fs::read_to_string(crate::state::persist::runtime_state_path(&ws)).unwrap(),
|
|
202
|
+
)
|
|
203
|
+
.unwrap();
|
|
204
|
+
// Stage 3d: canonical receiver at teams.<team_key>.leader_receiver.
|
|
205
|
+
let team_key = crate::state::projection::team_state_key(&persisted);
|
|
206
|
+
let receiver = &persisted["teams"][&team_key]["leader_receiver"];
|
|
207
|
+
assert_eq!(
|
|
208
|
+
receiver["tmux_socket"],
|
|
209
|
+
serde_json::json!(leader_socket),
|
|
210
|
+
"state.json must carry enough endpoint information for later delivery to use tmux -S"
|
|
211
|
+
);
|
|
212
|
+
}
|
|
195
213
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
"
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
214
|
+
#[test]
|
|
215
|
+
#[serial_test::serial(env)]
|
|
216
|
+
fn claim_leader_already_bound_requires_same_delivery_endpoint_not_only_same_pane_id() {
|
|
217
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
218
|
+
let current_socket = "/tmp/ta-current-leader-root/tmux-501/dl2f";
|
|
219
|
+
let stale_socket = "/tmp/ta-stale-leader-root/tmux-501/dl2f";
|
|
220
|
+
let _e = EnvGuard::apply(&[
|
|
221
|
+
("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
|
|
222
|
+
(
|
|
223
|
+
"TMUX",
|
|
224
|
+
Some("/tmp/ta-current-leader-root/tmux-501/dl2f,12345,0"),
|
|
225
|
+
),
|
|
226
|
+
("TMUX_TMPDIR", Some("/tmp/ta-coordinator-root")),
|
|
227
|
+
]);
|
|
228
|
+
let ws = p2_temp_ws("claim_bound_endpoint");
|
|
229
|
+
let team_id = TeamKey::new("current");
|
|
230
|
+
let caller = PaneId::new("%5");
|
|
231
|
+
let mut state = serde_json::json!({
|
|
232
|
+
"session_name": "team-agent-x",
|
|
233
|
+
"team_owner": {
|
|
234
|
+
"pane_id":"%5",
|
|
235
|
+
"provider":"codex",
|
|
236
|
+
"machine_fingerprint":"fp",
|
|
237
|
+
"leader_session_uuid":"U",
|
|
238
|
+
"owner_epoch":1,
|
|
239
|
+
"claimed_at":"t",
|
|
240
|
+
"claimed_via":"claim-leader",
|
|
241
|
+
"tmux_socket": stale_socket
|
|
242
|
+
},
|
|
243
|
+
"leader_receiver": {
|
|
244
|
+
"pane_id":"%5",
|
|
245
|
+
"owner_epoch":1,
|
|
246
|
+
"leader_session_uuid":"U",
|
|
247
|
+
"tmux_socket": stale_socket
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
251
|
+
let live = seeded_liveness(&["%5"]);
|
|
231
252
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
253
|
+
let r = claim_lease_no_incident(
|
|
254
|
+
&ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
|
|
255
|
+
)
|
|
256
|
+
.unwrap();
|
|
236
257
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
258
|
+
assert_ne!(
|
|
259
|
+
r.status,
|
|
260
|
+
LeaseStatus::AlreadyBound,
|
|
261
|
+
"already_bound must verify the same delivery endpoint is reachable; matching bare pane \
|
|
241
262
|
ids are not sufficient because different tmux socket roots can both have %5"
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
)
|
|
248
|
-
|
|
263
|
+
);
|
|
264
|
+
assert_eq!(
|
|
265
|
+
r.receiver
|
|
266
|
+
.as_ref()
|
|
267
|
+
.and_then(|receiver| receiver.tmux_socket.as_deref()),
|
|
268
|
+
Some(current_socket),
|
|
269
|
+
"claim should refresh the receiver to the caller's current full tmux endpoint"
|
|
270
|
+
);
|
|
271
|
+
}
|
|
249
272
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
"
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
273
|
+
#[test]
|
|
274
|
+
#[serial_test::serial(env)]
|
|
275
|
+
fn claim_leader_already_bound_normalizes_short_tmux_endpoint_to_full_path() {
|
|
276
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
277
|
+
let current_socket = "/tmp/ta-current-leader-root/tmux-501/dl9aa40c88";
|
|
278
|
+
let short_socket = "dl9aa40c88";
|
|
279
|
+
let _e = EnvGuard::apply(&[
|
|
280
|
+
("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
|
|
281
|
+
(
|
|
282
|
+
"TMUX",
|
|
283
|
+
Some("/tmp/ta-current-leader-root/tmux-501/dl9aa40c88,12345,0"),
|
|
284
|
+
),
|
|
285
|
+
("TMUX_TMPDIR", Some("/tmp/ta-coordinator-root")),
|
|
286
|
+
]);
|
|
287
|
+
let ws = p2_temp_ws("claim_bound_short_endpoint");
|
|
288
|
+
let team_id = TeamKey::new("current");
|
|
289
|
+
let caller = PaneId::new("%5");
|
|
290
|
+
let mut state = serde_json::json!({
|
|
291
|
+
"session_name": "team-agent-x",
|
|
292
|
+
"team_owner": {
|
|
293
|
+
"pane_id":"%5",
|
|
294
|
+
"provider":"codex",
|
|
295
|
+
"machine_fingerprint":"fp",
|
|
296
|
+
"leader_session_uuid":"U",
|
|
297
|
+
"owner_epoch":1,
|
|
298
|
+
"claimed_at":"t",
|
|
299
|
+
"claimed_via":"claim-leader",
|
|
300
|
+
"tmux_socket": short_socket
|
|
301
|
+
},
|
|
302
|
+
"leader_receiver": {
|
|
303
|
+
"pane_id":"%5",
|
|
304
|
+
"owner_epoch":1,
|
|
305
|
+
"leader_session_uuid":"U",
|
|
306
|
+
"tmux_socket": short_socket
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
310
|
+
let live = seeded_liveness(&["%5"]);
|
|
285
311
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
312
|
+
let r = claim_lease_no_incident(
|
|
313
|
+
&ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
|
|
314
|
+
)
|
|
315
|
+
.unwrap();
|
|
290
316
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
317
|
+
assert_ne!(
|
|
318
|
+
r.status,
|
|
319
|
+
LeaseStatus::AlreadyBound,
|
|
320
|
+
"claim already_bound must not treat a stored short socket name as equivalent to the \
|
|
295
321
|
caller's full $TMUX endpoint by basename; it must refresh/normalize the receiver"
|
|
296
|
-
|
|
297
|
-
|
|
322
|
+
);
|
|
323
|
+
assert_eq!(
|
|
298
324
|
r.receiver.as_ref().and_then(|receiver| receiver.tmux_socket.as_deref()),
|
|
299
325
|
Some(current_socket),
|
|
300
326
|
"claim should rewrite any legacy short leader_receiver.tmux_socket to the full physical endpoint"
|
|
301
327
|
);
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
328
|
+
let persisted: serde_json::Value = serde_json::from_str(
|
|
329
|
+
&std::fs::read_to_string(crate::state::persist::runtime_state_path(&ws)).unwrap(),
|
|
330
|
+
)
|
|
331
|
+
.unwrap();
|
|
332
|
+
// Stage 3d: canonical receiver at teams.<team_key>.leader_receiver.
|
|
333
|
+
let team_key = crate::state::projection::team_state_key(&persisted);
|
|
334
|
+
let receiver = &persisted["teams"][&team_key]["leader_receiver"];
|
|
335
|
+
assert_eq!(
|
|
336
|
+
receiver["tmux_socket"],
|
|
337
|
+
serde_json::json!(current_socket),
|
|
338
|
+
"state must not preserve a short endpoint after explicit claim; state={persisted}"
|
|
339
|
+
);
|
|
340
|
+
}
|
|
315
341
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
342
|
+
// RED — NOT-IN-TMUX-PANE: empty caller pane → refused not_in_tmux_pane with
|
|
343
|
+
// the EXACT golden action string (differs from the current claim_leader stub
|
|
344
|
+
// string). golden /tmp/probe_claim.py _lease_refused("not_in_tmux_pane",...).
|
|
345
|
+
#[test]
|
|
346
|
+
#[serial_test::serial(env)]
|
|
347
|
+
fn claim_lease_no_incident_empty_caller_refuses_not_in_tmux_pane() {
|
|
348
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
349
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
350
|
+
let ws = p2_temp_ws("claim_nopane");
|
|
351
|
+
let team_id = TeamKey::new("current");
|
|
352
|
+
let empty = PaneId::new("");
|
|
353
|
+
let mut state = serde_json::json!({});
|
|
354
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
355
|
+
let live = seeded_liveness(&[]);
|
|
356
|
+
let r = claim_lease_no_incident(
|
|
357
|
+
&ws, &mut state, None, &team_id, &empty, false, &event_log, &live,
|
|
358
|
+
)
|
|
359
|
+
.unwrap();
|
|
360
|
+
assert!(!r.ok);
|
|
361
|
+
assert_eq!(r.status, LeaseStatus::Refused);
|
|
362
|
+
assert_eq!(r.reason, Some(LeaseReason::NotInTmuxPane));
|
|
363
|
+
assert_eq!(
|
|
364
|
+
r.action.as_deref(),
|
|
365
|
+
Some("run team-agent claim-leader from the leader's tmux pane"),
|
|
366
|
+
"byte-parity: not_in_tmux_pane action string from _lease_refused (probe_claim.py)"
|
|
367
|
+
);
|
|
368
|
+
}
|
|
343
369
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
370
|
+
// OLD (Python parity, pre-Bug-3): caller pane present but NOT in the live
|
|
371
|
+
// leader-shape set → refused with CallerNotLeaderShaped + eligibility action
|
|
372
|
+
// ("from a leader (claude/codex) tmux pane").
|
|
373
|
+
// NEW (Bug 3 / I-RN-3 / tests/explicit_claim_takeover_any_live_pane_red):
|
|
374
|
+
// the hard leader-shape gate is removed by design. claim_lease_no_incident
|
|
375
|
+
// now accepts any LIVE caller pane (Codex / Claude / any worker shape). The
|
|
376
|
+
// only remaining caller refusal on this no-incident path is "caller pane is
|
|
377
|
+
// not live at all"; with seeded_liveness(&[]) the caller %7 is therefore
|
|
378
|
+
// refused as CallerPaneNotLive (CallerNotLeaderShaped is no longer produced
|
|
379
|
+
// on this code path).
|
|
380
|
+
#[test]
|
|
381
|
+
#[serial_test::serial(env)]
|
|
382
|
+
fn claim_lease_no_incident_caller_not_live_refused_under_n_rn3_any_live_pane() {
|
|
383
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
384
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
385
|
+
let ws = p2_temp_ws("claim_notleader");
|
|
386
|
+
let team_id = TeamKey::new("current");
|
|
387
|
+
let caller = PaneId::new("%7");
|
|
388
|
+
let mut state = serde_json::json!({});
|
|
389
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
390
|
+
// Empty live-set → caller %7 is not a live pane at all.
|
|
391
|
+
let live = seeded_liveness(&[]);
|
|
392
|
+
let r = claim_lease_no_incident(
|
|
393
|
+
&ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
|
|
394
|
+
)
|
|
395
|
+
.unwrap();
|
|
396
|
+
assert!(!r.ok);
|
|
397
|
+
assert_eq!(r.status, LeaseStatus::Refused);
|
|
398
|
+
assert_eq!(r.reason, Some(LeaseReason::CallerPaneNotLive));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── 14b. leader_identity dict — workspace_abspath key (golden __init__.py:363) ──
|
|
402
|
+
//
|
|
403
|
+
// LOCK→RED: leader_identity IS implemented (owner_bind.rs:22) but its dict
|
|
404
|
+
// OMITS the golden `workspace_abspath` key (probe_lid.py shows it sits between
|
|
405
|
+
// machine_fingerprint and os_user). This pins the missing key so the porter
|
|
406
|
+
// must add it. Fails today (key absent → null) = RED against the omission.
|
|
407
|
+
#[test]
|
|
408
|
+
#[serial_test::serial(env)]
|
|
409
|
+
fn leader_identity_dict_includes_workspace_abspath_key() {
|
|
410
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
411
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
412
|
+
let ws = p2_temp_ws("lid_wsabs");
|
|
413
|
+
let v = leader_identity(&ws, None).unwrap();
|
|
414
|
+
let ctx = leader_identity_context(&ws, None, Some(&serde_json::json!({}))).unwrap();
|
|
415
|
+
// golden key present + equals the resolved workspace abspath from context.
|
|
416
|
+
assert!(
|
|
417
|
+
v.get("workspace_abspath").is_some(),
|
|
418
|
+
"golden leader_identity dict carries 'workspace_abspath' (probe_lid.py)"
|
|
419
|
+
);
|
|
420
|
+
assert_eq!(
|
|
421
|
+
v["workspace_abspath"].as_str().unwrap(),
|
|
422
|
+
ctx.workspace_abspath.to_string_lossy(),
|
|
423
|
+
"workspace_abspath must equal the context's resolved abspath"
|
|
424
|
+
);
|
|
425
|
+
}
|
|
374
426
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
427
|
+
// LOCK — leader_identity key ORDER & full key set (golden probe_lid.py):
|
|
428
|
+
// [ok, uuid_prefix, machine_fingerprint, workspace_abspath, os_user, team_id,
|
|
429
|
+
// current_pane_id, last_seen_at, source]. Pins the 9-key surface CLI emits.
|
|
430
|
+
#[test]
|
|
431
|
+
#[serial_test::serial(env)]
|
|
432
|
+
fn leader_identity_dict_has_exact_nine_golden_keys() {
|
|
433
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
434
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
435
|
+
let ws = p2_temp_ws("lid_keys");
|
|
436
|
+
let v = leader_identity(&ws, None).unwrap();
|
|
437
|
+
let obj = v.as_object().expect("leader_identity → JSON object");
|
|
438
|
+
for key in [
|
|
439
|
+
"ok",
|
|
440
|
+
"uuid_prefix",
|
|
441
|
+
"machine_fingerprint",
|
|
442
|
+
"workspace_abspath",
|
|
443
|
+
"os_user",
|
|
444
|
+
"team_id",
|
|
445
|
+
"current_pane_id",
|
|
446
|
+
"last_seen_at",
|
|
447
|
+
"source",
|
|
448
|
+
] {
|
|
390
449
|
assert!(
|
|
391
|
-
|
|
392
|
-
"golden leader_identity dict
|
|
450
|
+
obj.contains_key(key),
|
|
451
|
+
"golden leader_identity dict must carry '{key}'"
|
|
393
452
|
);
|
|
394
|
-
assert_eq!(
|
|
395
|
-
v["workspace_abspath"].as_str().unwrap(),
|
|
396
|
-
ctx.workspace_abspath.to_string_lossy(),
|
|
397
|
-
"workspace_abspath must equal the context's resolved abspath"
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// LOCK — leader_identity key ORDER & full key set (golden probe_lid.py):
|
|
402
|
-
// [ok, uuid_prefix, machine_fingerprint, workspace_abspath, os_user, team_id,
|
|
403
|
-
// current_pane_id, last_seen_at, source]. Pins the 9-key surface CLI emits.
|
|
404
|
-
#[test]
|
|
405
|
-
#[serial_test::serial(env)]
|
|
406
|
-
fn leader_identity_dict_has_exact_nine_golden_keys() {
|
|
407
|
-
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
408
|
-
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
409
|
-
let ws = p2_temp_ws("lid_keys");
|
|
410
|
-
let v = leader_identity(&ws, None).unwrap();
|
|
411
|
-
let obj = v.as_object().expect("leader_identity → JSON object");
|
|
412
|
-
for key in [
|
|
413
|
-
"ok", "uuid_prefix", "machine_fingerprint", "workspace_abspath",
|
|
414
|
-
"os_user", "team_id", "current_pane_id", "last_seen_at", "source",
|
|
415
|
-
] {
|
|
416
|
-
assert!(obj.contains_key(key), "golden leader_identity dict must carry '{key}'");
|
|
417
|
-
}
|
|
418
453
|
}
|
|
454
|
+
}
|
|
419
455
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
456
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
457
|
+
// WAVE-2 Lane B — leader-lease DIVERGENCE round (adversarial review @ 2cd71ce).
|
|
458
|
+
// The lease gate is green but NOT byte-parity. Golden: leader/__init__.py
|
|
459
|
+
// (_claim_lease_no_incident:598, _receiver_from_claim_target:861, new_owner:686,
|
|
460
|
+
// _lease_epoch:400, _emit_lease_refusal:469, audit :712-729). These REDs lock the EXACT
|
|
461
|
+
// golden shape per divergence; driven via claim_lease_no_incident(+seeded_liveness), OS-safe.
|
|
462
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
427
463
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
464
|
+
/// **S3-OWNER-001 regression** (architect Stage 3 owner persist fix,
|
|
465
|
+
/// .team/artifacts/stage3-owner-persist-fix.md). Pre-fix on commit
|
|
466
|
+
/// `5d980c7`:
|
|
467
|
+
/// 1. `claim-leader --team alpha` returns `ok=true/status=claimed`.
|
|
468
|
+
/// 2. In-memory state has `state.teams.alpha.team_owner` set by
|
|
469
|
+
/// `write_binding_to_state` → `state::ownership::write_owner`.
|
|
470
|
+
/// 3. `save_claim_team_scoped_state` calls `compact_team_state(state)` to
|
|
471
|
+
/// build the per-team snapshot. `compact_team_state` strips the
|
|
472
|
+
/// `teams` key entirely — the just-written nested ownership record
|
|
473
|
+
/// is DROPPED on the way to disk.
|
|
474
|
+
/// 4. After Stage 3d removed the top-level dual-write, no copy survives.
|
|
475
|
+
/// 5. Reloading the workspace state.json shows `persisted_owner_locations=[]`.
|
|
476
|
+
///
|
|
477
|
+
/// Fix: `save_claim_team_scoped_state` now uses
|
|
478
|
+
/// `compact_team_state_preserving_ownership(state, target_key)` which
|
|
479
|
+
/// re-injects `team_owner / leader_receiver / owner_epoch` from
|
|
480
|
+
/// `state.teams[target_key]` into the compacted entry. Top-level fields
|
|
481
|
+
/// remain absent (Stage 3d canonical shape preserved).
|
|
482
|
+
#[test]
|
|
483
|
+
#[serial_test::serial(env)]
|
|
484
|
+
fn claim_team_scoped_persists_canonical_owner_under_teams_target_key() {
|
|
485
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
486
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
487
|
+
let ws = p2_temp_ws("claim_team_persist");
|
|
488
|
+
let target = "alpha";
|
|
489
|
+
let team_id = TeamKey::new(target);
|
|
490
|
+
let caller = PaneId::new("%5");
|
|
491
|
+
// Seed runtime state with a teams entry but no owner fields anywhere
|
|
492
|
+
// (the S3-OWNER-001 starting condition). team_dir is set so
|
|
493
|
+
// team_state_key resolves to the target team_key, matching the
|
|
494
|
+
// claim path's projected-state shape.
|
|
495
|
+
let mut state = serde_json::json!({
|
|
496
|
+
"session_name": "team-agent-x",
|
|
497
|
+
"team_dir": format!("/tmp/{}", target),
|
|
498
|
+
"teams": {target: {}},
|
|
499
|
+
});
|
|
500
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
501
|
+
let live = seeded_liveness(&["%5"]);
|
|
502
|
+
let r = claim_lease_no_incident(
|
|
503
|
+
&ws,
|
|
504
|
+
&mut state,
|
|
505
|
+
Some(target),
|
|
506
|
+
&team_id,
|
|
507
|
+
&caller,
|
|
508
|
+
false,
|
|
509
|
+
&event_log,
|
|
510
|
+
&live,
|
|
511
|
+
)
|
|
512
|
+
.unwrap();
|
|
513
|
+
assert!(r.ok, "S3-OWNER-001: claim must succeed; got {r:?}");
|
|
514
|
+
assert_eq!(r.status, LeaseStatus::Claimed);
|
|
472
515
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
)
|
|
478
|
-
.unwrap();
|
|
516
|
+
// Reload state from disk — this is where the pre-fix loss surfaced.
|
|
517
|
+
let persisted_path = crate::state::persist::runtime_state_path(&ws);
|
|
518
|
+
let persisted: serde_json::Value =
|
|
519
|
+
serde_json::from_str(&std::fs::read_to_string(&persisted_path).unwrap()).unwrap();
|
|
479
520
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
521
|
+
// Canonical: teams.<target_key>.{team_owner, leader_receiver, owner_epoch}.
|
|
522
|
+
let entry = &persisted["teams"][target];
|
|
523
|
+
assert_eq!(
|
|
524
|
+
entry["team_owner"]["pane_id"],
|
|
525
|
+
serde_json::json!("%5"),
|
|
526
|
+
"S3-OWNER-001 fix: teams.{target}.team_owner.pane_id persisted; \
|
|
486
527
|
persisted={persisted}"
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
528
|
+
);
|
|
529
|
+
assert_eq!(
|
|
530
|
+
entry["leader_receiver"]["pane_id"],
|
|
531
|
+
serde_json::json!("%5"),
|
|
532
|
+
"S3-OWNER-001 fix: teams.{target}.leader_receiver.pane_id persisted; \
|
|
492
533
|
persisted={persisted}"
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
534
|
+
);
|
|
535
|
+
assert_eq!(
|
|
536
|
+
entry["owner_epoch"],
|
|
537
|
+
serde_json::json!(1),
|
|
538
|
+
"S3-OWNER-001 fix: teams.{target}.owner_epoch=1 persisted; \
|
|
497
539
|
persisted={persisted}"
|
|
498
|
-
|
|
540
|
+
);
|
|
499
541
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
542
|
+
// Stage 3d invariant: top-level owner fields stay ABSENT (no
|
|
543
|
+
// recurrence of the legacy dual-write).
|
|
544
|
+
assert!(
|
|
545
|
+
persisted.get("team_owner").is_none(),
|
|
546
|
+
"Stage 3d: top-level team_owner must remain absent; persisted={persisted}"
|
|
547
|
+
);
|
|
548
|
+
assert!(
|
|
549
|
+
persisted.get("leader_receiver").is_none(),
|
|
550
|
+
"Stage 3d: top-level leader_receiver must remain absent; persisted={persisted}"
|
|
551
|
+
);
|
|
552
|
+
assert!(
|
|
553
|
+
persisted.get("owner_epoch").is_none(),
|
|
554
|
+
"Stage 3d: top-level owner_epoch must remain absent; persisted={persisted}"
|
|
555
|
+
);
|
|
556
|
+
}
|
|
515
557
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
"owner_epoch": 1,
|
|
559
|
-
"leader_session_uuid": "OLDUUID",
|
|
560
|
-
},
|
|
558
|
+
/// **S3-OWNER-002 regression** (architect Stage 3 top-level cleanup,
|
|
559
|
+
/// .team/artifacts/stage3-toplevel-cleanup-fix.md). Even with the
|
|
560
|
+
/// nested-ownership-preserving compact (aae818f), a SECOND claim on a
|
|
561
|
+
/// state whose root still carries legacy top-level `team_owner` /
|
|
562
|
+
/// `leader_receiver` / `owner_epoch` would leave the stale root copy
|
|
563
|
+
/// on disk while the canonical `teams.<key>` advanced. Evidence:
|
|
564
|
+
/// `state-after-claim.json` showed `team_owner=%2/epoch=1` at root and
|
|
565
|
+
/// `teams.<key>.team_owner=%3/epoch=2` — exact dual-source recurrence.
|
|
566
|
+
///
|
|
567
|
+
/// Fix: `save_claim_team_scoped_state` strips legacy root ownership
|
|
568
|
+
/// fields before save via `state::ownership::strip_top_level_ownership`,
|
|
569
|
+
/// and the legacy "copy top-level owner from incoming state into
|
|
570
|
+
/// merged" loop is removed. After every successful named-team owner
|
|
571
|
+
/// mutation, persisted root is canonical-only.
|
|
572
|
+
#[test]
|
|
573
|
+
#[serial_test::serial(env)]
|
|
574
|
+
fn second_claim_strips_legacy_top_level_ownership_from_persisted_root() {
|
|
575
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
576
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
577
|
+
let ws = p2_temp_ws("claim_team_second_strip");
|
|
578
|
+
let target = "alpha";
|
|
579
|
+
let team_id = TeamKey::new(target);
|
|
580
|
+
let caller = PaneId::new("%3");
|
|
581
|
+
// Seed a state whose root carries stale legacy top-level owner
|
|
582
|
+
// (%2/epoch=1) AND a teams.<target_key> entry that already has
|
|
583
|
+
// canonical ownership at the previous epoch. This is the
|
|
584
|
+
// post-S3-OWNER-001-fix shape on disk after one prior claim.
|
|
585
|
+
let mut state = serde_json::json!({
|
|
586
|
+
"session_name": "team-agent-x",
|
|
587
|
+
"team_dir": format!("/tmp/{}", target),
|
|
588
|
+
"active_team_key": target,
|
|
589
|
+
"team_owner": {
|
|
590
|
+
"pane_id": "%2",
|
|
591
|
+
"provider": "codex",
|
|
592
|
+
"machine_fingerprint": "fp",
|
|
593
|
+
"leader_session_uuid": "OLDUUID",
|
|
594
|
+
"owner_epoch": 1,
|
|
595
|
+
"claimed_at": "t",
|
|
596
|
+
"claimed_via": "claim-leader",
|
|
597
|
+
},
|
|
598
|
+
"leader_receiver": {
|
|
599
|
+
"pane_id": "%2",
|
|
561
600
|
"owner_epoch": 1,
|
|
562
|
-
"
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
},
|
|
573
|
-
"leader_receiver": {
|
|
574
|
-
"pane_id": "%2",
|
|
575
|
-
"owner_epoch": 1,
|
|
576
|
-
"leader_session_uuid": "OLDUUID",
|
|
577
|
-
},
|
|
601
|
+
"leader_session_uuid": "OLDUUID",
|
|
602
|
+
},
|
|
603
|
+
"owner_epoch": 1,
|
|
604
|
+
"teams": {
|
|
605
|
+
target: {
|
|
606
|
+
"team_owner": {
|
|
607
|
+
"pane_id": "%2",
|
|
608
|
+
"provider": "codex",
|
|
609
|
+
"machine_fingerprint": "fp",
|
|
610
|
+
"leader_session_uuid": "OLDUUID",
|
|
578
611
|
"owner_epoch": 1,
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
612
|
+
"claimed_at": "t",
|
|
613
|
+
"claimed_via": "claim-leader",
|
|
614
|
+
},
|
|
615
|
+
"leader_receiver": {
|
|
616
|
+
"pane_id": "%2",
|
|
617
|
+
"owner_epoch": 1,
|
|
618
|
+
"leader_session_uuid": "OLDUUID",
|
|
619
|
+
},
|
|
620
|
+
"owner_epoch": 1,
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
// Persist the seeded shape so `save_claim_team_scoped_state` loads
|
|
625
|
+
// it back as `existing` on save.
|
|
626
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
627
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
628
|
+
// Both panes live so the dead-owner reclaim branch doesn't apply;
|
|
629
|
+
// explicit takeover via confirm=true forces the claim past the
|
|
630
|
+
// live-owner gate.
|
|
631
|
+
let live = seeded_liveness(&["%2", "%3"]);
|
|
632
|
+
let r = claim_lease_no_incident(
|
|
633
|
+
&ws,
|
|
634
|
+
&mut state,
|
|
635
|
+
Some(target),
|
|
636
|
+
&team_id,
|
|
637
|
+
&caller,
|
|
638
|
+
true,
|
|
639
|
+
&event_log,
|
|
640
|
+
&live,
|
|
641
|
+
)
|
|
642
|
+
.unwrap();
|
|
643
|
+
assert!(
|
|
644
|
+
r.ok,
|
|
645
|
+
"S3-OWNER-002: explicit takeover must succeed; got {r:?}"
|
|
646
|
+
);
|
|
647
|
+
assert_eq!(r.status, LeaseStatus::Claimed);
|
|
596
648
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
)
|
|
601
|
-
.unwrap();
|
|
649
|
+
let persisted_path = crate::state::persist::runtime_state_path(&ws);
|
|
650
|
+
let persisted: serde_json::Value =
|
|
651
|
+
serde_json::from_str(&std::fs::read_to_string(&persisted_path).unwrap()).unwrap();
|
|
602
652
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
653
|
+
// Canonical advanced.
|
|
654
|
+
let entry = &persisted["teams"][target];
|
|
655
|
+
assert_eq!(
|
|
656
|
+
entry["team_owner"]["pane_id"],
|
|
657
|
+
serde_json::json!("%3"),
|
|
658
|
+
"S3-OWNER-002 fix: canonical owner advanced to new pane; persisted={persisted}"
|
|
659
|
+
);
|
|
660
|
+
assert_eq!(
|
|
661
|
+
entry["owner_epoch"],
|
|
662
|
+
serde_json::json!(2),
|
|
663
|
+
"S3-OWNER-002 fix: canonical owner_epoch advanced to 2; persisted={persisted}"
|
|
664
|
+
);
|
|
665
|
+
assert_eq!(
|
|
666
|
+
entry["leader_receiver"]["pane_id"],
|
|
667
|
+
serde_json::json!("%3"),
|
|
668
|
+
"S3-OWNER-002 fix: canonical receiver advanced; persisted={persisted}"
|
|
669
|
+
);
|
|
620
670
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
671
|
+
// Top-level stale legacy owner stripped.
|
|
672
|
+
assert!(
|
|
673
|
+
persisted.get("team_owner").is_none(),
|
|
674
|
+
"S3-OWNER-002 fix: stale top-level team_owner must be stripped; persisted={persisted}"
|
|
675
|
+
);
|
|
676
|
+
assert!(
|
|
677
|
+
persisted.get("leader_receiver").is_none(),
|
|
678
|
+
"S3-OWNER-002 fix: stale top-level leader_receiver must be stripped; persisted={persisted}"
|
|
679
|
+
);
|
|
680
|
+
assert!(
|
|
681
|
+
persisted.get("owner_epoch").is_none(),
|
|
682
|
+
"S3-OWNER-002 fix: stale top-level owner_epoch must be stripped; persisted={persisted}"
|
|
683
|
+
);
|
|
684
|
+
}
|