@team-agent/installer 0.2.11 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +744 -0
- package/Cargo.toml +34 -0
- package/crates/team-agent/Cargo.toml +33 -0
- package/crates/team-agent/src/cli/adapters.rs +1343 -0
- package/crates/team-agent/src/cli/diagnose.rs +554 -0
- package/crates/team-agent/src/cli/emit.rs +1204 -0
- package/crates/team-agent/src/cli/helpers.rs +88 -0
- package/crates/team-agent/src/cli/leader.rs +216 -0
- package/crates/team-agent/src/cli/mod.rs +1207 -0
- package/crates/team-agent/src/cli/profile.rs +306 -0
- package/crates/team-agent/src/cli/send.rs +215 -0
- package/crates/team-agent/src/cli/status.rs +179 -0
- package/crates/team-agent/src/cli/status_port.rs +502 -0
- package/crates/team-agent/src/cli/tests/base.rs +616 -0
- package/crates/team-agent/src/cli/tests/compile.rs +96 -0
- package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
- package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
- package/crates/team-agent/src/cli/tests/mod.rs +97 -0
- package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
- package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
- package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
- package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
- package/crates/team-agent/src/cli/types.rs +605 -0
- package/crates/team-agent/src/compiler/tests.rs +701 -0
- package/crates/team-agent/src/compiler.rs +489 -0
- package/crates/team-agent/src/coordinator/backoff.rs +153 -0
- package/crates/team-agent/src/coordinator/health.rs +557 -0
- package/crates/team-agent/src/coordinator/mod.rs +80 -0
- package/crates/team-agent/src/coordinator/orphan.rs +179 -0
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
- package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
- package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
- package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
- package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
- package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
- package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
- package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
- package/crates/team-agent/src/coordinator/tick.rs +2032 -0
- package/crates/team-agent/src/coordinator/types.rs +584 -0
- package/crates/team-agent/src/db/migration.rs +716 -0
- package/crates/team-agent/src/db/mod.rs +23 -0
- package/crates/team-agent/src/db/schema.rs +378 -0
- package/crates/team-agent/src/event_log.rs +375 -0
- package/crates/team-agent/src/fake_worker.rs +253 -0
- package/crates/team-agent/src/leader/helpers.rs +190 -0
- package/crates/team-agent/src/leader/inject.rs +33 -0
- package/crates/team-agent/src/leader/lease.rs +1084 -0
- package/crates/team-agent/src/leader/mod.rs +99 -0
- package/crates/team-agent/src/leader/owner_bind.rs +292 -0
- package/crates/team-agent/src/leader/rediscover/tests.rs +526 -0
- package/crates/team-agent/src/leader/rediscover.rs +1101 -0
- package/crates/team-agent/src/leader/start.rs +273 -0
- package/crates/team-agent/src/leader/takeover.rs +235 -0
- package/crates/team-agent/src/leader/tests/basics.rs +183 -0
- package/crates/team-agent/src/leader/tests/byte_findings.rs +237 -0
- package/crates/team-agent/src/leader/tests/identity.rs +206 -0
- package/crates/team-agent/src/leader/tests/idle.rs +272 -0
- package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
- package/crates/team-agent/src/leader/tests/lease_claim.rs +410 -0
- package/crates/team-agent/src/leader/tests/mod.rs +125 -0
- package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
- package/crates/team-agent/src/leader/types.rs +489 -0
- package/crates/team-agent/src/lib.rs +85 -0
- package/crates/team-agent/src/lifecycle/display.rs +228 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
- package/crates/team-agent/src/lifecycle/launch.rs +2109 -0
- package/crates/team-agent/src/lifecycle/mod.rs +62 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
- package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
- package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
- package/crates/team-agent/src/lifecycle/restart.rs +76 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
- package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +985 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
- package/crates/team-agent/src/lifecycle/tests.rs +27 -0
- package/crates/team-agent/src/lifecycle/types.rs +710 -0
- package/crates/team-agent/src/main.rs +41 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
- package/crates/team-agent/src/mcp_server/mod.rs +183 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
- package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
- package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
- package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +187 -0
- package/crates/team-agent/src/mcp_server/tests.rs +38 -0
- package/crates/team-agent/src/mcp_server/tools.rs +603 -0
- package/crates/team-agent/src/mcp_server/types.rs +421 -0
- package/crates/team-agent/src/mcp_server/wire.rs +468 -0
- package/crates/team-agent/src/message_store.rs +767 -0
- package/crates/team-agent/src/messaging/activity.rs +433 -0
- package/crates/team-agent/src/messaging/delivery.rs +743 -0
- package/crates/team-agent/src/messaging/helpers.rs +209 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +329 -0
- package/crates/team-agent/src/messaging/mod.rs +147 -0
- package/crates/team-agent/src/messaging/peers.rs +32 -0
- package/crates/team-agent/src/messaging/results.rs +553 -0
- package/crates/team-agent/src/messaging/scheduler.rs +344 -0
- package/crates/team-agent/src/messaging/selftest.rs +100 -0
- package/crates/team-agent/src/messaging/send.rs +578 -0
- package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
- package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
- package/crates/team-agent/src/messaging/trust.rs +192 -0
- package/crates/team-agent/src/messaging/types.rs +355 -0
- package/crates/team-agent/src/messaging/watchers.rs +591 -0
- package/crates/team-agent/src/model/enums.rs +311 -0
- package/crates/team-agent/src/model/errors.rs +17 -0
- package/crates/team-agent/src/model/ids.rs +155 -0
- package/crates/team-agent/src/model/mod.rs +22 -0
- package/crates/team-agent/src/model/paths.rs +228 -0
- package/crates/team-agent/src/model/permissions.rs +567 -0
- package/crates/team-agent/src/model/routing.rs +340 -0
- package/crates/team-agent/src/model/spec.rs +680 -0
- package/crates/team-agent/src/model/task_graph.rs +380 -0
- package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
- package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
- package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
- package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
- package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
- package/crates/team-agent/src/model/yaml/tests.rs +288 -0
- package/crates/team-agent/src/model/yaml.rs +800 -0
- package/crates/team-agent/src/packaging/install.rs +305 -0
- package/crates/team-agent/src/packaging/migrate.rs +30 -0
- package/crates/team-agent/src/packaging/mod.rs +82 -0
- package/crates/team-agent/src/packaging/repair.rs +24 -0
- package/crates/team-agent/src/packaging/tests.rs +829 -0
- package/crates/team-agent/src/packaging/types.rs +369 -0
- package/crates/team-agent/src/provider/adapter.rs +801 -0
- package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
- package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
- package/crates/team-agent/src/provider/classify.rs +456 -0
- package/crates/team-agent/src/provider/faults.rs +136 -0
- package/crates/team-agent/src/provider/helpers.rs +41 -0
- package/crates/team-agent/src/provider/mod.rs +53 -0
- package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
- package/crates/team-agent/src/provider/tests/classify.rs +240 -0
- package/crates/team-agent/src/provider/tests/faults.rs +120 -0
- package/crates/team-agent/src/provider/tests/idle.rs +208 -0
- package/crates/team-agent/src/provider/tests/wire.rs +213 -0
- package/crates/team-agent/src/provider/tests.rs +31 -0
- package/crates/team-agent/src/provider/types.rs +424 -0
- package/crates/team-agent/src/state/identity.rs +659 -0
- package/crates/team-agent/src/state/mod.rs +58 -0
- package/crates/team-agent/src/state/owner_gate.rs +423 -0
- package/crates/team-agent/src/state/persist.rs +712 -0
- package/crates/team-agent/src/state/projection.rs +657 -0
- package/crates/team-agent/src/state/selector.rs +105 -0
- package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +765 -0
- package/crates/team-agent/src/tmux_backend.rs +810 -0
- package/crates/team-agent/src/transport/test_support.rs +252 -0
- package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
- package/crates/team-agent/src/transport/tests/mod.rs +199 -0
- package/crates/team-agent/src/transport/tests/wire.rs +527 -0
- package/crates/team-agent/src/transport.rs +774 -0
- package/npm/install.mjs +118 -112
- package/package.json +15 -13
- package/crates/team-agent-core/Cargo.toml +0 -12
- package/crates/team-agent-core/src/lib.rs +0 -332
- package/crates/team-agent-core/src/main.rs +0 -152
- package/pyproject.toml +0 -18
- package/scripts/install.py +0 -88
- package/scripts/run_regression_tests.py +0 -83
- package/src/team_agent/__init__.py +0 -3
- package/src/team_agent/__main__.py +0 -5
- package/src/team_agent/_legacy_pane_discovery.py +0 -186
- package/src/team_agent/abnormal_track.py +0 -253
- package/src/team_agent/approvals/__init__.py +0 -65
- package/src/team_agent/approvals/constants.py +0 -6
- package/src/team_agent/approvals/parsing.py +0 -176
- package/src/team_agent/approvals/runtime_prompts.py +0 -171
- package/src/team_agent/approvals/status.py +0 -176
- package/src/team_agent/cli/__init__.py +0 -137
- package/src/team_agent/cli/commands.py +0 -481
- package/src/team_agent/cli/e2e.py +0 -202
- package/src/team_agent/cli/helpers.py +0 -226
- package/src/team_agent/cli/parser.py +0 -540
- package/src/team_agent/compiler.py +0 -334
- package/src/team_agent/coordinator/__init__.py +0 -53
- package/src/team_agent/coordinator/__main__.py +0 -119
- package/src/team_agent/coordinator/lifecycle.py +0 -411
- package/src/team_agent/coordinator/metadata.py +0 -61
- package/src/team_agent/coordinator/paths.py +0 -17
- package/src/team_agent/diagnose/__init__.py +0 -48
- package/src/team_agent/diagnose/checks.py +0 -101
- package/src/team_agent/diagnose/comms.py +0 -213
- package/src/team_agent/diagnose/health.py +0 -241
- package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
- package/src/team_agent/diagnose/preflight.py +0 -194
- package/src/team_agent/diagnose/quick_start.py +0 -324
- package/src/team_agent/display/__init__.py +0 -92
- package/src/team_agent/display/adaptive.py +0 -511
- package/src/team_agent/display/backend.py +0 -46
- package/src/team_agent/display/close.py +0 -154
- package/src/team_agent/display/ghostty.py +0 -77
- package/src/team_agent/display/rebuild.py +0 -102
- package/src/team_agent/display/tiling.py +0 -156
- package/src/team_agent/display/worker_window.py +0 -114
- package/src/team_agent/display/workspace.py +0 -382
- package/src/team_agent/errors.py +0 -10
- package/src/team_agent/events.py +0 -84
- package/src/team_agent/fake_worker.py +0 -80
- package/src/team_agent/idle_predicate.py +0 -218
- package/src/team_agent/idle_takeover.py +0 -59
- package/src/team_agent/idle_takeover_wiring.py +0 -114
- package/src/team_agent/launch/__init__.py +0 -41
- package/src/team_agent/launch/bootstrap.py +0 -85
- package/src/team_agent/launch/config.py +0 -106
- package/src/team_agent/launch/core.py +0 -301
- package/src/team_agent/launch/requirements.py +0 -57
- package/src/team_agent/leader/__init__.py +0 -926
- package/src/team_agent/leader_binding.py +0 -183
- package/src/team_agent/lifecycle/__init__.py +0 -5
- package/src/team_agent/lifecycle/agents.py +0 -278
- package/src/team_agent/lifecycle/operations.py +0 -411
- package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
- package/src/team_agent/lifecycle/start.py +0 -363
- package/src/team_agent/mcp_server/__init__.py +0 -42
- package/src/team_agent/mcp_server/__main__.py +0 -7
- package/src/team_agent/mcp_server/contracts.py +0 -148
- package/src/team_agent/mcp_server/normalize.py +0 -257
- package/src/team_agent/mcp_server/server.py +0 -150
- package/src/team_agent/mcp_server/tools.py +0 -352
- package/src/team_agent/message_store/__init__.py +0 -23
- package/src/team_agent/message_store/agent_health.py +0 -113
- package/src/team_agent/message_store/core.py +0 -497
- package/src/team_agent/message_store/leader_notification_log.py +0 -198
- package/src/team_agent/message_store/result_watchers.py +0 -251
- package/src/team_agent/message_store/schema.py +0 -308
- package/src/team_agent/message_store/schema_migration.py +0 -448
- package/src/team_agent/messaging/__init__.py +0 -1
- package/src/team_agent/messaging/activity_detector.py +0 -262
- package/src/team_agent/messaging/delivery.py +0 -504
- package/src/team_agent/messaging/deps.py +0 -247
- package/src/team_agent/messaging/idle_alerts.py +0 -423
- package/src/team_agent/messaging/internal_delivery.py +0 -46
- package/src/team_agent/messaging/leader.py +0 -497
- package/src/team_agent/messaging/leader_api_errors.py +0 -216
- package/src/team_agent/messaging/leader_panes.py +0 -673
- package/src/team_agent/messaging/owner_bypass.py +0 -29
- package/src/team_agent/messaging/result_delivery.py +0 -539
- package/src/team_agent/messaging/results.py +0 -447
- package/src/team_agent/messaging/scheduler.py +0 -450
- package/src/team_agent/messaging/send.py +0 -532
- package/src/team_agent/messaging/session_drift.py +0 -94
- package/src/team_agent/messaging/tmux_io.py +0 -506
- package/src/team_agent/messaging/tmux_prompt.py +0 -338
- package/src/team_agent/messaging/trust_auto_answer.py +0 -52
- package/src/team_agent/orchestrator/__init__.py +0 -376
- package/src/team_agent/orchestrator/plan.py +0 -122
- package/src/team_agent/orchestrator/state.py +0 -128
- package/src/team_agent/paths.py +0 -45
- package/src/team_agent/permissions.py +0 -123
- package/src/team_agent/profiles/__init__.py +0 -82
- package/src/team_agent/profiles/constants.py +0 -19
- package/src/team_agent/profiles/core.py +0 -407
- package/src/team_agent/profiles/helpers.py +0 -69
- package/src/team_agent/profiles/provider_env.py +0 -188
- package/src/team_agent/profiles/smoke.py +0 -201
- package/src/team_agent/provider_cli/__init__.py +0 -43
- package/src/team_agent/provider_cli/adapter.py +0 -172
- package/src/team_agent/provider_cli/base.py +0 -48
- package/src/team_agent/provider_cli/claude.py +0 -503
- package/src/team_agent/provider_cli/codex.py +0 -336
- package/src/team_agent/provider_cli/copilot.py +0 -8
- package/src/team_agent/provider_cli/fake.py +0 -39
- package/src/team_agent/provider_cli/gemini.py +0 -95
- package/src/team_agent/provider_cli/opencode.py +0 -8
- package/src/team_agent/provider_cli/prompt.py +0 -62
- package/src/team_agent/provider_cli/registry.py +0 -18
- package/src/team_agent/provider_cli/unsupported.py +0 -32
- package/src/team_agent/provider_state/README.md +0 -78
- package/src/team_agent/provider_state/__init__.py +0 -91
- package/src/team_agent/provider_state/claude.py +0 -86
- package/src/team_agent/provider_state/codex.py +0 -84
- package/src/team_agent/provider_state/common.py +0 -207
- package/src/team_agent/provider_state/registry.py +0 -118
- package/src/team_agent/providers.py +0 -163
- package/src/team_agent/quality_gates.py +0 -104
- package/src/team_agent/restart/__init__.py +0 -34
- package/src/team_agent/restart/orchestration.py +0 -554
- package/src/team_agent/restart/selection.py +0 -89
- package/src/team_agent/restart/snapshot.py +0 -70
- package/src/team_agent/routing.py +0 -84
- package/src/team_agent/runtime.py +0 -1243
- package/src/team_agent/rust_core.py +0 -327
- package/src/team_agent/sessions/__init__.py +0 -25
- package/src/team_agent/sessions/capture.py +0 -144
- package/src/team_agent/sessions/inventory.py +0 -44
- package/src/team_agent/sessions/resume.py +0 -135
- package/src/team_agent/simple_yaml.py +0 -236
- package/src/team_agent/spec.py +0 -370
- package/src/team_agent/state.py +0 -693
- package/src/team_agent/status/__init__.py +0 -63
- package/src/team_agent/status/approvals.py +0 -52
- package/src/team_agent/status/compact.py +0 -158
- package/src/team_agent/status/constants.py +0 -18
- package/src/team_agent/status/inbox.py +0 -58
- package/src/team_agent/status/peek.py +0 -117
- package/src/team_agent/status/queries.py +0 -199
- package/src/team_agent/task_graph.py +0 -80
- package/src/team_agent/terminal.py +0 -57
- package/src/team_agent/wake.py +0 -58
- package/src/team_agent/watch/__init__.py +0 -145
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
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
|
+
|
|
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
|
+
fn claim_lease_no_incident_vacant_acquire_advances_epoch_and_dual_writes() {
|
|
14
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
15
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
16
|
+
let ws = p2_temp_ws("claim_vacant");
|
|
17
|
+
let team_id = TeamKey::new("current");
|
|
18
|
+
let caller = PaneId::new("%5");
|
|
19
|
+
// empty (vacant) state with a session so dual-state writes both files.
|
|
20
|
+
let mut state = serde_json::json!({"session_name": "team-agent-x"});
|
|
21
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
22
|
+
let live = seeded_liveness(&["%5"]);
|
|
23
|
+
let r = claim_lease_no_incident(
|
|
24
|
+
&ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
|
|
25
|
+
)
|
|
26
|
+
.unwrap();
|
|
27
|
+
assert!(r.ok);
|
|
28
|
+
assert_eq!(r.status, LeaseStatus::Claimed);
|
|
29
|
+
assert_eq!(r.reason, Some(LeaseReason::VacantAcquired));
|
|
30
|
+
assert_eq!(r.owner_epoch, Some(OwnerEpoch(1)), "vacant acquire 0→1");
|
|
31
|
+
let owner = r.owner.as_ref().expect("claimed → owner");
|
|
32
|
+
assert_eq!(owner.pane_id, caller);
|
|
33
|
+
assert_eq!(owner.owner_epoch, OwnerEpoch(1));
|
|
34
|
+
assert_eq!(owner.claimed_via, ClaimedVia::ClaimLeader);
|
|
35
|
+
let receiver = r.receiver.as_ref().expect("claimed → receiver");
|
|
36
|
+
assert_eq!(receiver.pane_id, caller);
|
|
37
|
+
assert_eq!(receiver.discovery, Some(Discovery::ClaimLeader));
|
|
38
|
+
// dual-state: BOTH workspace state.json + team/<session> snapshot carry %5/epoch 1.
|
|
39
|
+
let ws_path = crate::state::persist::runtime_state_path(&ws);
|
|
40
|
+
let ws_state: serde_json::Value =
|
|
41
|
+
serde_json::from_str(&std::fs::read_to_string(&ws_path).unwrap()).unwrap();
|
|
42
|
+
assert_eq!(ws_state["team_owner"]["pane_id"], serde_json::json!("%5"));
|
|
43
|
+
assert_eq!(ws_state["team_owner"]["owner_epoch"], serde_json::json!(1));
|
|
44
|
+
let snap_path = crate::model::paths::runtime_dir(&ws)
|
|
45
|
+
.join("teams").join("team-agent-x").join("state.json");
|
|
46
|
+
let snap: serde_json::Value =
|
|
47
|
+
serde_json::from_str(&std::fs::read_to_string(&snap_path).unwrap()).unwrap();
|
|
48
|
+
assert_eq!(snap["team_owner"]["pane_id"], serde_json::json!("%5"), "dual-state snapshot owner");
|
|
49
|
+
assert_eq!(snap["team_owner"]["owner_epoch"], serde_json::json!(1));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// RED — DEAD-OWNER RECOVER: bound pane %1 absent from live set, caller %5
|
|
53
|
+
// eligible, confirm=false → still acquires (dead owner never blocks),
|
|
54
|
+
// reason=previous_owner_pane_dead, epoch 3→4. golden /tmp/probe_confirm.py.
|
|
55
|
+
#[test]
|
|
56
|
+
#[serial_test::serial(env)]
|
|
57
|
+
fn claim_lease_no_incident_dead_owner_recovers_without_confirm() {
|
|
58
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
59
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
60
|
+
let ws = p2_temp_ws("claim_dead");
|
|
61
|
+
let team_id = TeamKey::new("current");
|
|
62
|
+
let caller = PaneId::new("%5");
|
|
63
|
+
let mut state = serde_json::json!({
|
|
64
|
+
"session_name": "team-agent-x",
|
|
65
|
+
"team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"OLDUUID","owner_epoch":3,"claimed_at":"t","claimed_via":"claim-leader"},
|
|
66
|
+
"leader_receiver": {"pane_id":"%1","owner_epoch":3,"leader_session_uuid":"OLDUUID"},
|
|
67
|
+
});
|
|
68
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
69
|
+
// %1 (the recorded owner) is NOT live; only the caller %5 is live.
|
|
70
|
+
let live = seeded_liveness(&["%5"]);
|
|
71
|
+
let r = claim_lease_no_incident(
|
|
72
|
+
&ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
|
|
73
|
+
)
|
|
74
|
+
.unwrap();
|
|
75
|
+
assert!(r.ok);
|
|
76
|
+
assert_eq!(r.status, LeaseStatus::Claimed);
|
|
77
|
+
assert_eq!(r.reason, Some(LeaseReason::PreviousOwnerPaneDead));
|
|
78
|
+
assert_eq!(r.owner_epoch, Some(OwnerEpoch(4)), "dead-owner recover 3→4");
|
|
79
|
+
assert_eq!(r.owner.as_ref().unwrap().pane_id, caller);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// RED — LIVE-OWNER REFUSAL (no --confirm): bound pane %1 live, caller %5,
|
|
83
|
+
// confirm=false → refused force_confirm_required + action + bound_pane_id=%1
|
|
84
|
+
// + owner_epoch=2 (precheck epoch, NOT advanced). golden /tmp/probe_confirm.py.
|
|
85
|
+
#[test]
|
|
86
|
+
#[serial_test::serial(env)]
|
|
87
|
+
fn claim_lease_no_incident_live_owner_refuses_without_confirm() {
|
|
88
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
89
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
90
|
+
let ws = p2_temp_ws("claim_live");
|
|
91
|
+
let team_id = TeamKey::new("current");
|
|
92
|
+
let caller = PaneId::new("%5");
|
|
93
|
+
let mut state = serde_json::json!({
|
|
94
|
+
"session_name": "team-agent-x",
|
|
95
|
+
"team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"OWNERUUID","owner_epoch":2,"claimed_at":"t","claimed_via":"claim-leader"},
|
|
96
|
+
"leader_receiver": {"pane_id":"%1","owner_epoch":2,"leader_session_uuid":"OWNERUUID"},
|
|
97
|
+
});
|
|
98
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
99
|
+
// both %1 (live owner, matching uuid) and %5 (eligible caller) live.
|
|
100
|
+
let live = seeded_liveness(&["%1", "%5"]);
|
|
101
|
+
let r = claim_lease_no_incident(
|
|
102
|
+
&ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
|
|
103
|
+
)
|
|
104
|
+
.unwrap();
|
|
105
|
+
assert!(!r.ok);
|
|
106
|
+
assert_eq!(r.status, LeaseStatus::Refused);
|
|
107
|
+
assert_eq!(r.reason, Some(LeaseReason::ForceConfirmRequired));
|
|
108
|
+
assert_eq!(
|
|
109
|
+
r.action.as_deref(),
|
|
110
|
+
Some("rerun with --confirm to take over the live leader pane")
|
|
111
|
+
);
|
|
112
|
+
assert_eq!(r.bound_pane_id, Some(PaneId::new("%1")), "refusal carries bound pane");
|
|
113
|
+
assert_eq!(r.owner_epoch, Some(OwnerEpoch(2)), "refused → precheck epoch, NOT advanced");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// RED — ALREADY-BOUND: caller pane == bound pane → status=AlreadyBound,
|
|
117
|
+
// ok=true, owner_epoch unchanged (no advance, no rewrite). golden probe_claimed.py.
|
|
118
|
+
#[test]
|
|
119
|
+
#[serial_test::serial(env)]
|
|
120
|
+
fn claim_lease_no_incident_already_bound_when_caller_is_bound_pane() {
|
|
121
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
122
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
123
|
+
let ws = p2_temp_ws("claim_bound");
|
|
124
|
+
let team_id = TeamKey::new("current");
|
|
125
|
+
let caller = PaneId::new("%5");
|
|
126
|
+
let mut state = serde_json::json!({
|
|
127
|
+
"session_name": "team-agent-x",
|
|
128
|
+
"team_owner": {"pane_id":"%5","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":1,"claimed_at":"t","claimed_via":"claim-leader"},
|
|
129
|
+
"leader_receiver": {"pane_id":"%5","owner_epoch":1,"leader_session_uuid":"U"},
|
|
130
|
+
});
|
|
131
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
132
|
+
let live = seeded_liveness(&["%5"]);
|
|
133
|
+
let r = claim_lease_no_incident(
|
|
134
|
+
&ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
|
|
135
|
+
)
|
|
136
|
+
.unwrap();
|
|
137
|
+
assert!(r.ok);
|
|
138
|
+
assert_eq!(r.status, LeaseStatus::AlreadyBound);
|
|
139
|
+
assert_eq!(r.owner_epoch, Some(OwnerEpoch(1)), "already-bound → epoch unchanged");
|
|
140
|
+
assert!(r.reason.is_none(), "already-bound path carries no acquire reason");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#[test]
|
|
144
|
+
#[serial_test::serial(env)]
|
|
145
|
+
fn claim_leader_persists_full_tmux_endpoint_when_tmux_tmpdir_differs_from_coordinator() {
|
|
146
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
147
|
+
let leader_socket = "/tmp/ta-leader-root/tmux-501/dl2f";
|
|
148
|
+
let _e = EnvGuard::apply(&[
|
|
149
|
+
("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
|
|
150
|
+
("TMUX", Some("/tmp/ta-leader-root/tmux-501/dl2f,12345,0")),
|
|
151
|
+
("TMUX_TMPDIR", Some("/tmp/ta-coordinator-root")),
|
|
152
|
+
]);
|
|
153
|
+
let ws = p2_temp_ws("claim_full_endpoint");
|
|
154
|
+
let team_id = TeamKey::new("current");
|
|
155
|
+
let caller = PaneId::new("%5");
|
|
156
|
+
let mut state = serde_json::json!({"session_name": "team-agent-x"});
|
|
157
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
158
|
+
let live = seeded_liveness(&["%5"]);
|
|
159
|
+
|
|
160
|
+
let r = claim_lease_no_incident(
|
|
161
|
+
&ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
|
|
162
|
+
)
|
|
163
|
+
.unwrap();
|
|
164
|
+
|
|
165
|
+
assert!(r.ok);
|
|
166
|
+
assert_eq!(r.status, LeaseStatus::Claimed);
|
|
167
|
+
assert_eq!(
|
|
168
|
+
r.receiver.as_ref().and_then(|receiver| receiver.tmux_socket.as_deref()),
|
|
169
|
+
Some(leader_socket),
|
|
170
|
+
"claim-leader must persist the full $TMUX socket path, not only the -L short name"
|
|
171
|
+
);
|
|
172
|
+
let persisted: serde_json::Value = serde_json::from_str(
|
|
173
|
+
&std::fs::read_to_string(crate::state::persist::runtime_state_path(&ws)).unwrap(),
|
|
174
|
+
)
|
|
175
|
+
.unwrap();
|
|
176
|
+
assert_eq!(
|
|
177
|
+
persisted["leader_receiver"]["tmux_socket"],
|
|
178
|
+
serde_json::json!(leader_socket),
|
|
179
|
+
"state.json must carry enough endpoint information for later delivery to use tmux -S"
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#[test]
|
|
184
|
+
#[serial_test::serial(env)]
|
|
185
|
+
fn claim_leader_already_bound_requires_same_delivery_endpoint_not_only_same_pane_id() {
|
|
186
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
187
|
+
let current_socket = "/tmp/ta-current-leader-root/tmux-501/dl2f";
|
|
188
|
+
let stale_socket = "/tmp/ta-stale-leader-root/tmux-501/dl2f";
|
|
189
|
+
let _e = EnvGuard::apply(&[
|
|
190
|
+
("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
|
|
191
|
+
("TMUX", Some("/tmp/ta-current-leader-root/tmux-501/dl2f,12345,0")),
|
|
192
|
+
("TMUX_TMPDIR", Some("/tmp/ta-coordinator-root")),
|
|
193
|
+
]);
|
|
194
|
+
let ws = p2_temp_ws("claim_bound_endpoint");
|
|
195
|
+
let team_id = TeamKey::new("current");
|
|
196
|
+
let caller = PaneId::new("%5");
|
|
197
|
+
let mut state = serde_json::json!({
|
|
198
|
+
"session_name": "team-agent-x",
|
|
199
|
+
"team_owner": {
|
|
200
|
+
"pane_id":"%5",
|
|
201
|
+
"provider":"codex",
|
|
202
|
+
"machine_fingerprint":"fp",
|
|
203
|
+
"leader_session_uuid":"U",
|
|
204
|
+
"owner_epoch":1,
|
|
205
|
+
"claimed_at":"t",
|
|
206
|
+
"claimed_via":"claim-leader",
|
|
207
|
+
"tmux_socket": stale_socket
|
|
208
|
+
},
|
|
209
|
+
"leader_receiver": {
|
|
210
|
+
"pane_id":"%5",
|
|
211
|
+
"owner_epoch":1,
|
|
212
|
+
"leader_session_uuid":"U",
|
|
213
|
+
"tmux_socket": stale_socket
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
217
|
+
let live = seeded_liveness(&["%5"]);
|
|
218
|
+
|
|
219
|
+
let r = claim_lease_no_incident(
|
|
220
|
+
&ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
|
|
221
|
+
)
|
|
222
|
+
.unwrap();
|
|
223
|
+
|
|
224
|
+
assert_ne!(
|
|
225
|
+
r.status,
|
|
226
|
+
LeaseStatus::AlreadyBound,
|
|
227
|
+
"already_bound must verify the same delivery endpoint is reachable; matching bare pane \
|
|
228
|
+
ids are not sufficient because different tmux socket roots can both have %5"
|
|
229
|
+
);
|
|
230
|
+
assert_eq!(
|
|
231
|
+
r.receiver.as_ref().and_then(|receiver| receiver.tmux_socket.as_deref()),
|
|
232
|
+
Some(current_socket),
|
|
233
|
+
"claim should refresh the receiver to the caller's current full tmux endpoint"
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#[test]
|
|
238
|
+
#[serial_test::serial(env)]
|
|
239
|
+
fn claim_leader_already_bound_normalizes_short_tmux_endpoint_to_full_path() {
|
|
240
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
241
|
+
let current_socket = "/tmp/ta-current-leader-root/tmux-501/dl9aa40c88";
|
|
242
|
+
let short_socket = "dl9aa40c88";
|
|
243
|
+
let _e = EnvGuard::apply(&[
|
|
244
|
+
("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
|
|
245
|
+
("TMUX", Some("/tmp/ta-current-leader-root/tmux-501/dl9aa40c88,12345,0")),
|
|
246
|
+
("TMUX_TMPDIR", Some("/tmp/ta-coordinator-root")),
|
|
247
|
+
]);
|
|
248
|
+
let ws = p2_temp_ws("claim_bound_short_endpoint");
|
|
249
|
+
let team_id = TeamKey::new("current");
|
|
250
|
+
let caller = PaneId::new("%5");
|
|
251
|
+
let mut state = serde_json::json!({
|
|
252
|
+
"session_name": "team-agent-x",
|
|
253
|
+
"team_owner": {
|
|
254
|
+
"pane_id":"%5",
|
|
255
|
+
"provider":"codex",
|
|
256
|
+
"machine_fingerprint":"fp",
|
|
257
|
+
"leader_session_uuid":"U",
|
|
258
|
+
"owner_epoch":1,
|
|
259
|
+
"claimed_at":"t",
|
|
260
|
+
"claimed_via":"claim-leader",
|
|
261
|
+
"tmux_socket": short_socket
|
|
262
|
+
},
|
|
263
|
+
"leader_receiver": {
|
|
264
|
+
"pane_id":"%5",
|
|
265
|
+
"owner_epoch":1,
|
|
266
|
+
"leader_session_uuid":"U",
|
|
267
|
+
"tmux_socket": short_socket
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
271
|
+
let live = seeded_liveness(&["%5"]);
|
|
272
|
+
|
|
273
|
+
let r = claim_lease_no_incident(
|
|
274
|
+
&ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
|
|
275
|
+
)
|
|
276
|
+
.unwrap();
|
|
277
|
+
|
|
278
|
+
assert_ne!(
|
|
279
|
+
r.status,
|
|
280
|
+
LeaseStatus::AlreadyBound,
|
|
281
|
+
"claim already_bound must not treat a stored short socket name as equivalent to the \
|
|
282
|
+
caller's full $TMUX endpoint by basename; it must refresh/normalize the receiver"
|
|
283
|
+
);
|
|
284
|
+
assert_eq!(
|
|
285
|
+
r.receiver.as_ref().and_then(|receiver| receiver.tmux_socket.as_deref()),
|
|
286
|
+
Some(current_socket),
|
|
287
|
+
"claim should rewrite any legacy short leader_receiver.tmux_socket to the full physical endpoint"
|
|
288
|
+
);
|
|
289
|
+
let persisted: serde_json::Value = serde_json::from_str(
|
|
290
|
+
&std::fs::read_to_string(crate::state::persist::runtime_state_path(&ws)).unwrap(),
|
|
291
|
+
)
|
|
292
|
+
.unwrap();
|
|
293
|
+
assert_eq!(
|
|
294
|
+
persisted["leader_receiver"]["tmux_socket"],
|
|
295
|
+
serde_json::json!(current_socket),
|
|
296
|
+
"state must not preserve a short endpoint after explicit claim; state={persisted}"
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// RED — NOT-IN-TMUX-PANE: empty caller pane → refused not_in_tmux_pane with
|
|
301
|
+
// the EXACT golden action string (differs from the current claim_leader stub
|
|
302
|
+
// string). golden /tmp/probe_claim.py _lease_refused("not_in_tmux_pane",...).
|
|
303
|
+
#[test]
|
|
304
|
+
#[serial_test::serial(env)]
|
|
305
|
+
fn claim_lease_no_incident_empty_caller_refuses_not_in_tmux_pane() {
|
|
306
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
307
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
308
|
+
let ws = p2_temp_ws("claim_nopane");
|
|
309
|
+
let team_id = TeamKey::new("current");
|
|
310
|
+
let empty = PaneId::new("");
|
|
311
|
+
let mut state = serde_json::json!({});
|
|
312
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
313
|
+
let live = seeded_liveness(&[]);
|
|
314
|
+
let r = claim_lease_no_incident(
|
|
315
|
+
&ws, &mut state, None, &team_id, &empty, false, &event_log, &live,
|
|
316
|
+
)
|
|
317
|
+
.unwrap();
|
|
318
|
+
assert!(!r.ok);
|
|
319
|
+
assert_eq!(r.status, LeaseStatus::Refused);
|
|
320
|
+
assert_eq!(r.reason, Some(LeaseReason::NotInTmuxPane));
|
|
321
|
+
assert_eq!(
|
|
322
|
+
r.action.as_deref(),
|
|
323
|
+
Some("run team-agent claim-leader from the leader's tmux pane"),
|
|
324
|
+
"byte-parity: not_in_tmux_pane action string from _lease_refused (probe_claim.py)"
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// OLD (Python parity, pre-Bug-3): caller pane present but NOT in the live
|
|
329
|
+
// leader-shape set → refused with CallerNotLeaderShaped + eligibility action
|
|
330
|
+
// ("from a leader (claude/codex) tmux pane").
|
|
331
|
+
// NEW (Bug 3 / I-RN-3 / tests/explicit_claim_takeover_any_live_pane_red):
|
|
332
|
+
// the hard leader-shape gate is removed by design. claim_lease_no_incident
|
|
333
|
+
// now accepts any LIVE caller pane (Codex / Claude / any worker shape). The
|
|
334
|
+
// only remaining caller refusal on this no-incident path is "caller pane is
|
|
335
|
+
// not live at all"; with seeded_liveness(&[]) the caller %7 is therefore
|
|
336
|
+
// refused as CallerPaneNotLive (CallerNotLeaderShaped is no longer produced
|
|
337
|
+
// on this code path).
|
|
338
|
+
#[test]
|
|
339
|
+
#[serial_test::serial(env)]
|
|
340
|
+
fn claim_lease_no_incident_caller_not_live_refused_under_n_rn3_any_live_pane() {
|
|
341
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
342
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
343
|
+
let ws = p2_temp_ws("claim_notleader");
|
|
344
|
+
let team_id = TeamKey::new("current");
|
|
345
|
+
let caller = PaneId::new("%7");
|
|
346
|
+
let mut state = serde_json::json!({});
|
|
347
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
348
|
+
// Empty live-set → caller %7 is not a live pane at all.
|
|
349
|
+
let live = seeded_liveness(&[]);
|
|
350
|
+
let r = claim_lease_no_incident(
|
|
351
|
+
&ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
|
|
352
|
+
)
|
|
353
|
+
.unwrap();
|
|
354
|
+
assert!(!r.ok);
|
|
355
|
+
assert_eq!(r.status, LeaseStatus::Refused);
|
|
356
|
+
assert_eq!(r.reason, Some(LeaseReason::CallerPaneNotLive));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── 14b. leader_identity dict — workspace_abspath key (golden __init__.py:363) ──
|
|
360
|
+
//
|
|
361
|
+
// LOCK→RED: leader_identity IS implemented (owner_bind.rs:22) but its dict
|
|
362
|
+
// OMITS the golden `workspace_abspath` key (probe_lid.py shows it sits between
|
|
363
|
+
// machine_fingerprint and os_user). This pins the missing key so the porter
|
|
364
|
+
// must add it. Fails today (key absent → null) = RED against the omission.
|
|
365
|
+
#[test]
|
|
366
|
+
#[serial_test::serial(env)]
|
|
367
|
+
fn leader_identity_dict_includes_workspace_abspath_key() {
|
|
368
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
369
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
370
|
+
let ws = p2_temp_ws("lid_wsabs");
|
|
371
|
+
let v = leader_identity(&ws, None).unwrap();
|
|
372
|
+
let ctx = leader_identity_context(&ws, None, Some(&serde_json::json!({}))).unwrap();
|
|
373
|
+
// golden key present + equals the resolved workspace abspath from context.
|
|
374
|
+
assert!(
|
|
375
|
+
v.get("workspace_abspath").is_some(),
|
|
376
|
+
"golden leader_identity dict carries 'workspace_abspath' (probe_lid.py)"
|
|
377
|
+
);
|
|
378
|
+
assert_eq!(
|
|
379
|
+
v["workspace_abspath"].as_str().unwrap(),
|
|
380
|
+
ctx.workspace_abspath.to_string_lossy(),
|
|
381
|
+
"workspace_abspath must equal the context's resolved abspath"
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// LOCK — leader_identity key ORDER & full key set (golden probe_lid.py):
|
|
386
|
+
// [ok, uuid_prefix, machine_fingerprint, workspace_abspath, os_user, team_id,
|
|
387
|
+
// current_pane_id, last_seen_at, source]. Pins the 9-key surface CLI emits.
|
|
388
|
+
#[test]
|
|
389
|
+
#[serial_test::serial(env)]
|
|
390
|
+
fn leader_identity_dict_has_exact_nine_golden_keys() {
|
|
391
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
392
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
393
|
+
let ws = p2_temp_ws("lid_keys");
|
|
394
|
+
let v = leader_identity(&ws, None).unwrap();
|
|
395
|
+
let obj = v.as_object().expect("leader_identity → JSON object");
|
|
396
|
+
for key in [
|
|
397
|
+
"ok", "uuid_prefix", "machine_fingerprint", "workspace_abspath",
|
|
398
|
+
"os_user", "team_id", "current_pane_id", "last_seen_at", "source",
|
|
399
|
+
] {
|
|
400
|
+
assert!(obj.contains_key(key), "golden leader_identity dict must carry '{key}'");
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
405
|
+
// WAVE-2 Lane B — leader-lease DIVERGENCE round (adversarial review @ 2cd71ce).
|
|
406
|
+
// The lease gate is green but NOT byte-parity. Golden: leader/__init__.py
|
|
407
|
+
// (_claim_lease_no_incident:598, _receiver_from_claim_target:861, new_owner:686,
|
|
408
|
+
// _lease_epoch:400, _emit_lease_refusal:469, audit :712-729). These REDs lock the EXACT
|
|
409
|
+
// golden shape per divergence; driven via claim_lease_no_incident(+seeded_liveness), OS-safe.
|
|
410
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
2
|
+
use super::*;
|
|
3
|
+
use std::cell::Cell;
|
|
4
|
+
|
|
5
|
+
// ── helpers ────────────────────────────────────────────────────────────
|
|
6
|
+
/// derive 一个真 LeaderSessionUuid(骨架无公开裸构造器,仅 `derive`)。
|
|
7
|
+
fn uuid(fp: &str, ws: &str, user: &str, team: &str) -> LeaderSessionUuid {
|
|
8
|
+
LeaderSessionUuid::derive(fp, ws, user, team).unwrap()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/// 注入式 turn-state 分类器:MUST-NOT-13 命门 —— 统计 classify 调用次数,
|
|
12
|
+
/// 据此断言 idle 面经此 trait 分类、绝不直连任何 provider client。
|
|
13
|
+
struct CountingClassifier {
|
|
14
|
+
calls: Cell<usize>,
|
|
15
|
+
result: TurnState,
|
|
16
|
+
}
|
|
17
|
+
impl CountingClassifier {
|
|
18
|
+
fn new(result: TurnState) -> Self {
|
|
19
|
+
Self { calls: Cell::new(0), result }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
impl TurnStateClassifier for CountingClassifier {
|
|
23
|
+
fn classify(
|
|
24
|
+
&self,
|
|
25
|
+
_provider: Provider,
|
|
26
|
+
session_log_text: &str,
|
|
27
|
+
) -> Result<TurnClassification, LeaderError> {
|
|
28
|
+
self.calls.set(self.calls.get() + 1);
|
|
29
|
+
// 空 session-log 文本 → unknown(bug-085:None rollout_path 漏穿后读到空串)。
|
|
30
|
+
let state = if session_log_text.is_empty() { TurnState::Unknown } else { self.result };
|
|
31
|
+
Ok(TurnClassification {
|
|
32
|
+
state,
|
|
33
|
+
turn_id: None,
|
|
34
|
+
annotations: vec![],
|
|
35
|
+
reason: if state == TurnState::Unknown { Some("empty_session_log".into()) } else { None },
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
|
41
|
+
|
|
42
|
+
/// RAII env override: restores prior values on drop (incl. panic unwind), so a
|
|
43
|
+
/// RED assertion-panic cannot leak a dirty env into other tests.
|
|
44
|
+
struct EnvGuard {
|
|
45
|
+
saved: Vec<(String, Option<String>)>,
|
|
46
|
+
}
|
|
47
|
+
impl EnvGuard {
|
|
48
|
+
fn apply(vars: &[(&str, Option<&str>)]) -> Self {
|
|
49
|
+
let saved = vars.iter().map(|(k, _)| ((*k).to_string(), std::env::var(k).ok())).collect();
|
|
50
|
+
for (k, v) in vars {
|
|
51
|
+
match v {
|
|
52
|
+
Some(val) => std::env::set_var(k, val),
|
|
53
|
+
None => std::env::remove_var(k),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
Self { saved }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
impl Drop for EnvGuard {
|
|
60
|
+
fn drop(&mut self) {
|
|
61
|
+
for (k, v) in &self.saved {
|
|
62
|
+
match v {
|
|
63
|
+
Some(val) => std::env::set_var(k, val),
|
|
64
|
+
None => std::env::remove_var(k),
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn p2_temp_ws(tag: &str) -> PathBuf {
|
|
71
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_p2leader_{}_{}", tag, std::process::id()));
|
|
72
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
73
|
+
ws
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// P1 — state-supplied leader_session_uuid → source 'derived' (NOT 'env'); Python's
|
|
77
|
+
// source is binary override-vs-derived (leader/__init__.py:206).
|
|
78
|
+
struct SeededLiveness {
|
|
79
|
+
live_panes: std::collections::BTreeSet<String>,
|
|
80
|
+
}
|
|
81
|
+
impl crate::state::owner_gate::PaneLivenessProbe for SeededLiveness {
|
|
82
|
+
fn liveness(&self, pane_id: &str) -> crate::model::enums::PaneLiveness {
|
|
83
|
+
if self.live_panes.contains(pane_id) {
|
|
84
|
+
crate::model::enums::PaneLiveness::Live
|
|
85
|
+
} else {
|
|
86
|
+
crate::model::enums::PaneLiveness::Dead
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fn seeded_liveness(panes: &[&str]) -> SeededLiveness {
|
|
92
|
+
SeededLiveness { live_panes: panes.iter().map(|p| (*p).to_string()).collect() }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// RED — VACANT ACQUIRE: empty state + eligible caller → status=Claimed,
|
|
96
|
+
// reason=vacant_acquired, owner_epoch 0→1, owner+receiver bound to caller,
|
|
97
|
+
// claimed_via=claim-leader, BOTH state files written (dual-state).
|
|
98
|
+
// golden /tmp/probe_claimed.py: epoch 1, claimed_via "claim-leader",
|
|
99
|
+
// discovery "claim_leader", reason "vacant_acquired".
|
|
100
|
+
/// Event names from the workspace event log (events.jsonl), in write order.
|
|
101
|
+
fn event_names(ws: &std::path::Path) -> Vec<String> {
|
|
102
|
+
let path = crate::model::paths::logs_dir(ws).join("events.jsonl");
|
|
103
|
+
std::fs::read_to_string(&path)
|
|
104
|
+
.map(|t| {
|
|
105
|
+
t.lines()
|
|
106
|
+
.filter_map(|l| serde_json::from_str::<serde_json::Value>(l).ok())
|
|
107
|
+
.filter_map(|e| e.get("event").and_then(|v| v.as_str()).map(String::from))
|
|
108
|
+
.collect()
|
|
109
|
+
})
|
|
110
|
+
.unwrap_or_default()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// D1 [BLOCK] — claim-path team_owner is EXACTLY golden's 7 keys (NO os_user). Golden new_owner
|
|
114
|
+
// (__init__.py:686-694) = pane_id,provider,machine_fingerprint,leader_session_uuid,owner_epoch,
|
|
115
|
+
// claimed_at,claimed_via; only Family-A leader_binding writes os_user, NEVER the claim path. Rust
|
|
116
|
+
// make_owner sets os_user:Some(..) + TeamOwner has no skip_serializing_if -> 8 keys. RED.
|
|
117
|
+
|
|
118
|
+
mod basics;
|
|
119
|
+
mod wake_start_owner;
|
|
120
|
+
mod lease_api;
|
|
121
|
+
mod idle;
|
|
122
|
+
mod identity;
|
|
123
|
+
mod lease_claim;
|
|
124
|
+
mod byte_findings;
|
|
125
|
+
mod rediscover;
|