@team-agent/installer 0.2.11 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +744 -0
- package/Cargo.toml +34 -0
- package/crates/team-agent/Cargo.toml +33 -0
- package/crates/team-agent/src/cli/adapters.rs +1343 -0
- package/crates/team-agent/src/cli/diagnose.rs +554 -0
- package/crates/team-agent/src/cli/emit.rs +1077 -0
- package/crates/team-agent/src/cli/helpers.rs +88 -0
- package/crates/team-agent/src/cli/leader.rs +216 -0
- package/crates/team-agent/src/cli/mod.rs +1141 -0
- package/crates/team-agent/src/cli/profile.rs +306 -0
- package/crates/team-agent/src/cli/send.rs +215 -0
- package/crates/team-agent/src/cli/status.rs +179 -0
- package/crates/team-agent/src/cli/status_port.rs +502 -0
- package/crates/team-agent/src/cli/tests/base.rs +616 -0
- package/crates/team-agent/src/cli/tests/compile.rs +96 -0
- package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
- package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
- package/crates/team-agent/src/cli/tests/mod.rs +97 -0
- package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
- package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
- package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
- package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
- package/crates/team-agent/src/cli/types.rs +605 -0
- package/crates/team-agent/src/compiler/tests.rs +701 -0
- package/crates/team-agent/src/compiler.rs +489 -0
- package/crates/team-agent/src/coordinator/backoff.rs +153 -0
- package/crates/team-agent/src/coordinator/health.rs +436 -0
- package/crates/team-agent/src/coordinator/mod.rs +80 -0
- package/crates/team-agent/src/coordinator/orphan.rs +179 -0
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
- package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
- package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
- package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
- package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
- package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
- package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
- package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
- package/crates/team-agent/src/coordinator/tick.rs +2032 -0
- package/crates/team-agent/src/coordinator/types.rs +584 -0
- package/crates/team-agent/src/db/migration.rs +716 -0
- package/crates/team-agent/src/db/mod.rs +23 -0
- package/crates/team-agent/src/db/schema.rs +378 -0
- package/crates/team-agent/src/event_log.rs +375 -0
- package/crates/team-agent/src/fake_worker.rs +253 -0
- package/crates/team-agent/src/leader/helpers.rs +190 -0
- package/crates/team-agent/src/leader/inject.rs +33 -0
- package/crates/team-agent/src/leader/lease.rs +1063 -0
- package/crates/team-agent/src/leader/mod.rs +99 -0
- package/crates/team-agent/src/leader/owner_bind.rs +292 -0
- package/crates/team-agent/src/leader/rediscover/tests.rs +525 -0
- package/crates/team-agent/src/leader/rediscover.rs +1099 -0
- package/crates/team-agent/src/leader/start.rs +273 -0
- package/crates/team-agent/src/leader/takeover.rs +235 -0
- package/crates/team-agent/src/leader/tests/basics.rs +183 -0
- package/crates/team-agent/src/leader/tests/byte_findings.rs +234 -0
- package/crates/team-agent/src/leader/tests/identity.rs +206 -0
- package/crates/team-agent/src/leader/tests/idle.rs +271 -0
- package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
- package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -0
- package/crates/team-agent/src/leader/tests/mod.rs +125 -0
- package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
- package/crates/team-agent/src/leader/types.rs +487 -0
- package/crates/team-agent/src/lib.rs +85 -0
- package/crates/team-agent/src/lifecycle/display.rs +228 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
- package/crates/team-agent/src/lifecycle/launch.rs +1833 -0
- package/crates/team-agent/src/lifecycle/mod.rs +62 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
- package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
- package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
- package/crates/team-agent/src/lifecycle/restart.rs +76 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
- package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +933 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
- package/crates/team-agent/src/lifecycle/tests.rs +27 -0
- package/crates/team-agent/src/lifecycle/types.rs +685 -0
- package/crates/team-agent/src/main.rs +41 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
- package/crates/team-agent/src/mcp_server/mod.rs +183 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
- package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
- package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
- package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +159 -0
- package/crates/team-agent/src/mcp_server/tests.rs +38 -0
- package/crates/team-agent/src/mcp_server/tools.rs +603 -0
- package/crates/team-agent/src/mcp_server/types.rs +421 -0
- package/crates/team-agent/src/mcp_server/wire.rs +388 -0
- package/crates/team-agent/src/message_store.rs +767 -0
- package/crates/team-agent/src/messaging/activity.rs +433 -0
- package/crates/team-agent/src/messaging/delivery.rs +542 -0
- package/crates/team-agent/src/messaging/helpers.rs +209 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +340 -0
- package/crates/team-agent/src/messaging/mod.rs +147 -0
- package/crates/team-agent/src/messaging/peers.rs +32 -0
- package/crates/team-agent/src/messaging/results.rs +537 -0
- package/crates/team-agent/src/messaging/scheduler.rs +344 -0
- package/crates/team-agent/src/messaging/selftest.rs +100 -0
- package/crates/team-agent/src/messaging/send.rs +582 -0
- package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
- package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
- package/crates/team-agent/src/messaging/trust.rs +192 -0
- package/crates/team-agent/src/messaging/types.rs +355 -0
- package/crates/team-agent/src/messaging/watchers.rs +591 -0
- package/crates/team-agent/src/model/enums.rs +311 -0
- package/crates/team-agent/src/model/errors.rs +17 -0
- package/crates/team-agent/src/model/ids.rs +155 -0
- package/crates/team-agent/src/model/mod.rs +22 -0
- package/crates/team-agent/src/model/paths.rs +228 -0
- package/crates/team-agent/src/model/permissions.rs +567 -0
- package/crates/team-agent/src/model/routing.rs +340 -0
- package/crates/team-agent/src/model/spec.rs +680 -0
- package/crates/team-agent/src/model/task_graph.rs +380 -0
- package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
- package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
- package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
- package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
- package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
- package/crates/team-agent/src/model/yaml/tests.rs +288 -0
- package/crates/team-agent/src/model/yaml.rs +800 -0
- package/crates/team-agent/src/packaging/install.rs +305 -0
- package/crates/team-agent/src/packaging/migrate.rs +30 -0
- package/crates/team-agent/src/packaging/mod.rs +82 -0
- package/crates/team-agent/src/packaging/repair.rs +24 -0
- package/crates/team-agent/src/packaging/tests.rs +829 -0
- package/crates/team-agent/src/packaging/types.rs +369 -0
- package/crates/team-agent/src/provider/adapter.rs +801 -0
- package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
- package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
- package/crates/team-agent/src/provider/classify.rs +456 -0
- package/crates/team-agent/src/provider/faults.rs +136 -0
- package/crates/team-agent/src/provider/helpers.rs +41 -0
- package/crates/team-agent/src/provider/mod.rs +53 -0
- package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
- package/crates/team-agent/src/provider/tests/classify.rs +240 -0
- package/crates/team-agent/src/provider/tests/faults.rs +120 -0
- package/crates/team-agent/src/provider/tests/idle.rs +208 -0
- package/crates/team-agent/src/provider/tests/wire.rs +213 -0
- package/crates/team-agent/src/provider/tests.rs +31 -0
- package/crates/team-agent/src/provider/types.rs +424 -0
- package/crates/team-agent/src/state/identity.rs +656 -0
- package/crates/team-agent/src/state/mod.rs +58 -0
- package/crates/team-agent/src/state/owner_gate.rs +423 -0
- package/crates/team-agent/src/state/persist.rs +712 -0
- package/crates/team-agent/src/state/projection.rs +657 -0
- package/crates/team-agent/src/state/selector.rs +105 -0
- package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +586 -0
- package/crates/team-agent/src/tmux_backend.rs +758 -0
- package/crates/team-agent/src/transport/test_support.rs +252 -0
- package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
- package/crates/team-agent/src/transport/tests/mod.rs +199 -0
- package/crates/team-agent/src/transport/tests/wire.rs +527 -0
- package/crates/team-agent/src/transport.rs +774 -0
- package/npm/install.mjs +90 -106
- package/package.json +15 -13
- package/crates/team-agent-core/Cargo.toml +0 -12
- package/crates/team-agent-core/src/lib.rs +0 -332
- package/crates/team-agent-core/src/main.rs +0 -152
- package/pyproject.toml +0 -18
- package/scripts/install.py +0 -88
- package/scripts/run_regression_tests.py +0 -83
- package/src/team_agent/__init__.py +0 -3
- package/src/team_agent/__main__.py +0 -5
- package/src/team_agent/_legacy_pane_discovery.py +0 -186
- package/src/team_agent/abnormal_track.py +0 -253
- package/src/team_agent/approvals/__init__.py +0 -65
- package/src/team_agent/approvals/constants.py +0 -6
- package/src/team_agent/approvals/parsing.py +0 -176
- package/src/team_agent/approvals/runtime_prompts.py +0 -171
- package/src/team_agent/approvals/status.py +0 -176
- package/src/team_agent/cli/__init__.py +0 -137
- package/src/team_agent/cli/commands.py +0 -481
- package/src/team_agent/cli/e2e.py +0 -202
- package/src/team_agent/cli/helpers.py +0 -226
- package/src/team_agent/cli/parser.py +0 -540
- package/src/team_agent/compiler.py +0 -334
- package/src/team_agent/coordinator/__init__.py +0 -53
- package/src/team_agent/coordinator/__main__.py +0 -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,183 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
// =====================================================================
|
|
4
|
+
// 1. 锁名 / 闭枚举 / serde 字节对齐(纯数据,不依赖 unimplemented body)
|
|
5
|
+
// =====================================================================
|
|
6
|
+
|
|
7
|
+
// LEADER_OWNERSHIP_LOCK = "send"(__init__.py:393):attach/claim/takeover/autobind 共享同一临界区。
|
|
8
|
+
#[test]
|
|
9
|
+
fn ownership_lock_is_send() {
|
|
10
|
+
assert_eq!(LEADER_OWNERSHIP_LOCK, "send");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// LeaseReason 闭枚举的 serde 字节必须与 _LEASE_REASON_ENUM ∪ {caller_pane_missing} 一致。
|
|
14
|
+
// golden(probe_leader.py):enum 8 值(snake)+ binding 的 caller_pane_missing。
|
|
15
|
+
#[test]
|
|
16
|
+
fn lease_reason_serializes_to_exact_python_strings() {
|
|
17
|
+
let cases = [
|
|
18
|
+
(LeaseReason::VacantAcquired, "\"vacant_acquired\""),
|
|
19
|
+
(LeaseReason::PreviousOwnerPaneDead, "\"previous_owner_pane_dead\""),
|
|
20
|
+
(LeaseReason::PreviousOwnerAliveRefused, "\"previous_owner_alive_refused\""),
|
|
21
|
+
(LeaseReason::OwnerEpochAdvanced, "\"owner_epoch_advanced\""),
|
|
22
|
+
(LeaseReason::ForceConfirmRequired, "\"force_confirm_required\""),
|
|
23
|
+
(LeaseReason::CallerNotLeaderShaped, "\"caller_not_leader_shaped\""),
|
|
24
|
+
(LeaseReason::CallerCwdMismatch, "\"caller_cwd_mismatch\""),
|
|
25
|
+
(LeaseReason::NotInTmuxPane, "\"not_in_tmux_pane\""),
|
|
26
|
+
(LeaseReason::CallerPaneMissing, "\"caller_pane_missing\""),
|
|
27
|
+
];
|
|
28
|
+
for (r, want) in cases {
|
|
29
|
+
assert_eq!(serde_json::to_string(&r).unwrap(), want, "{r:?}");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// _LEASE_REBIND_REQUIRED_REASONS = {not_in_tmux_pane, caller_not_leader_shaped, caller_cwd_mismatch}
|
|
34
|
+
// (__init__.py:384-386)→ 决定 refusal 事件名 rebind_required vs claim_refused。
|
|
35
|
+
// is_rebind_required 是 unimplemented!() → 调用即 panic = RED。
|
|
36
|
+
#[test]
|
|
37
|
+
fn lease_reason_rebind_required_membership_matches_python() {
|
|
38
|
+
// 三个 rebind-required(golden probe_leader.py rebind_required 集)。
|
|
39
|
+
assert!(LeaseReason::NotInTmuxPane.is_rebind_required());
|
|
40
|
+
assert!(LeaseReason::CallerNotLeaderShaped.is_rebind_required());
|
|
41
|
+
assert!(LeaseReason::CallerCwdMismatch.is_rebind_required());
|
|
42
|
+
// 其余一律 claim_refused(非 rebind-required)。
|
|
43
|
+
assert!(!LeaseReason::VacantAcquired.is_rebind_required());
|
|
44
|
+
assert!(!LeaseReason::PreviousOwnerPaneDead.is_rebind_required());
|
|
45
|
+
assert!(!LeaseReason::PreviousOwnerAliveRefused.is_rebind_required());
|
|
46
|
+
assert!(!LeaseReason::OwnerEpochAdvanced.is_rebind_required());
|
|
47
|
+
assert!(!LeaseReason::ForceConfirmRequired.is_rebind_required());
|
|
48
|
+
assert!(!LeaseReason::CallerPaneMissing.is_rebind_required());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// LeaseStatus serde(probe_leader3.py):already_bound / claimed / refused / dry_run。
|
|
52
|
+
#[test]
|
|
53
|
+
fn lease_status_serializes_to_exact_python_strings() {
|
|
54
|
+
assert_eq!(serde_json::to_string(&LeaseStatus::AlreadyBound).unwrap(), "\"already_bound\"");
|
|
55
|
+
assert_eq!(serde_json::to_string(&LeaseStatus::Claimed).unwrap(), "\"claimed\"");
|
|
56
|
+
assert_eq!(serde_json::to_string(&LeaseStatus::Refused).unwrap(), "\"refused\"");
|
|
57
|
+
assert_eq!(serde_json::to_string(&LeaseStatus::DryRun).unwrap(), "\"dry_run\"");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Discovery serde(probe_leader3.py):attach_readopt/claim_leader/env_pane/explicit_pane/current_pane。
|
|
61
|
+
#[test]
|
|
62
|
+
fn discovery_serializes_to_exact_python_strings() {
|
|
63
|
+
assert_eq!(serde_json::to_string(&Discovery::AttachReadopt).unwrap(), "\"attach_readopt\"");
|
|
64
|
+
assert_eq!(serde_json::to_string(&Discovery::ClaimLeader).unwrap(), "\"claim_leader\"");
|
|
65
|
+
assert_eq!(serde_json::to_string(&Discovery::EnvPane).unwrap(), "\"env_pane\"");
|
|
66
|
+
assert_eq!(serde_json::to_string(&Discovery::ExplicitPane).unwrap(), "\"explicit_pane\"");
|
|
67
|
+
assert_eq!(serde_json::to_string(&Discovery::CurrentPane).unwrap(), "\"current_pane\"");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ClaimedVia 是 kebab-case(__init__.py:545/693/876 等写 "attach-leader"/"claim-leader")。
|
|
71
|
+
#[test]
|
|
72
|
+
fn claimed_via_serializes_kebab_case() {
|
|
73
|
+
assert_eq!(serde_json::to_string(&ClaimedVia::ClaimLeader).unwrap(), "\"claim-leader\"");
|
|
74
|
+
assert_eq!(serde_json::to_string(&ClaimedVia::AttachLeader).unwrap(), "\"attach-leader\"");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// LeaseSource serde:launch/quick_start/restart/manual(__init__.py source 取值)。
|
|
78
|
+
#[test]
|
|
79
|
+
fn lease_source_serializes_snake_case() {
|
|
80
|
+
assert_eq!(serde_json::to_string(&LeaseSource::Launch).unwrap(), "\"launch\"");
|
|
81
|
+
assert_eq!(serde_json::to_string(&LeaseSource::QuickStart).unwrap(), "\"quick_start\"");
|
|
82
|
+
assert_eq!(serde_json::to_string(&LeaseSource::Restart).unwrap(), "\"restart\"");
|
|
83
|
+
assert_eq!(serde_json::to_string(&LeaseSource::Manual).unwrap(), "\"manual\"");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ReceiverMode/Status:direct_tmux / attached(__init__.py:283-284)。
|
|
87
|
+
#[test]
|
|
88
|
+
fn receiver_mode_and_status_serialize_to_python_strings() {
|
|
89
|
+
assert_eq!(serde_json::to_string(&ReceiverMode::DirectTmux).unwrap(), "\"direct_tmux\"");
|
|
90
|
+
assert_eq!(serde_json::to_string(&ReceiverStatus::Attached).unwrap(), "\"attached\"");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// LeaderStartMode:exec_provider/new_tmux_session/attach_existing(leader_start_plan)。
|
|
94
|
+
#[test]
|
|
95
|
+
fn leader_start_mode_serializes_to_python_strings() {
|
|
96
|
+
assert_eq!(serde_json::to_string(&LeaderStartMode::ExecProvider).unwrap(), "\"exec_provider\"");
|
|
97
|
+
assert_eq!(serde_json::to_string(&LeaderStartMode::NewTmuxSession).unwrap(), "\"new_tmux_session\"");
|
|
98
|
+
assert_eq!(serde_json::to_string(&LeaderStartMode::AttachExisting).unwrap(), "\"attach_existing\"");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// NodeRole:worker/leader(build_idle_nodes role 字段)。
|
|
102
|
+
#[test]
|
|
103
|
+
fn node_role_serializes_to_python_strings() {
|
|
104
|
+
assert_eq!(serde_json::to_string(&NodeRole::Worker).unwrap(), "\"worker\"");
|
|
105
|
+
assert_eq!(serde_json::to_string(&NodeRole::Leader).unwrap(), "\"leader\"");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// RereadReason:wake.py 五值(probe_leader.py)。
|
|
109
|
+
#[test]
|
|
110
|
+
fn reread_reason_serializes_to_python_strings() {
|
|
111
|
+
assert_eq!(serde_json::to_string(&RereadReason::NoFile).unwrap(), "\"no_file\"");
|
|
112
|
+
assert_eq!(serde_json::to_string(&RereadReason::NeverClassified).unwrap(), "\"never_classified\"");
|
|
113
|
+
assert_eq!(serde_json::to_string(&RereadReason::FileChanged).unwrap(), "\"file_changed\"");
|
|
114
|
+
assert_eq!(
|
|
115
|
+
serde_json::to_string(&RereadReason::QuiescentAlreadyClassified).unwrap(),
|
|
116
|
+
"\"quiescent_already_classified\""
|
|
117
|
+
);
|
|
118
|
+
assert_eq!(serde_json::to_string(&RereadReason::Unchanged).unwrap(), "\"unchanged\"");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// =====================================================================
|
|
122
|
+
// 2. LeaderSessionUuidSource — 漂移 NOTE(card §168-179)
|
|
123
|
+
// =====================================================================
|
|
124
|
+
|
|
125
|
+
// leader plan 侧 _leader_identity_context(__init__.py:206)只产 "override"/"derived";
|
|
126
|
+
// identity lane(state.py:332)用 "explicit-override"/"env"/"derived"。
|
|
127
|
+
// 此 enum 的 serde 字节当对齐 LEADER 侧:Override→"override"。Env→"env" 也在闭集。
|
|
128
|
+
#[test]
|
|
129
|
+
fn leader_session_uuid_source_serializes_leader_plan_strings() {
|
|
130
|
+
assert_eq!(serde_json::to_string(&LeaderSessionUuidSource::Derived).unwrap(), "\"derived\"");
|
|
131
|
+
// NOTE(drift):leader plan 写裸 "override"(非 identity lane 的 "explicit-override")。
|
|
132
|
+
assert_eq!(serde_json::to_string(&LeaderSessionUuidSource::Override).unwrap(), "\"override\"");
|
|
133
|
+
assert_eq!(serde_json::to_string(&LeaderSessionUuidSource::Env).unwrap(), "\"env\"");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 漂移可观测:identity lane 的 caller dict 用 "explicit-override",二者不可互换。
|
|
137
|
+
// 这是 leader 集成时必须 reconcile 的 NOTE —— 锁死 leader-plan 侧字节,防被误统一成
|
|
138
|
+
// identity 串。serde override≠"explicit-override"。
|
|
139
|
+
#[test]
|
|
140
|
+
fn leader_plan_override_is_not_identity_explicit_override_string() {
|
|
141
|
+
let leader_plan = serde_json::to_string(&LeaderSessionUuidSource::Override).unwrap();
|
|
142
|
+
assert_eq!(leader_plan, "\"override\"");
|
|
143
|
+
assert_ne!(leader_plan, "\"explicit-override\"", "drift NOTE: leader plan 用 'override',不可写成 identity 的 'explicit-override'");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// =====================================================================
|
|
147
|
+
// 3. LeaderEvent::name() — §40 字节级一致(unimplemented → RED)
|
|
148
|
+
// =====================================================================
|
|
149
|
+
|
|
150
|
+
// 全部 leader 审计事件名(probe_leader2.py FOUND 验证;ping 来自 idle_predicate 跨 lane,亦 golden)。
|
|
151
|
+
// 注:ReceiverClaimLeaderNotification 在 v0.2.11 任何 source 都查无 "leader_receiver.claim_leader_notification"
|
|
152
|
+
// 字面 → 见 deferred,此处不钉该串(只钉可验证的 23 个)。
|
|
153
|
+
#[test]
|
|
154
|
+
fn leader_event_names_match_python_byte_for_byte() {
|
|
155
|
+
let cases = [
|
|
156
|
+
(LeaderEvent::ReceiverAttached, "leader_receiver.attached"),
|
|
157
|
+
(LeaderEvent::ReceiverRebindApplied, "leader_receiver.rebind_applied"),
|
|
158
|
+
(LeaderEvent::ReceiverClaimApplied, "leader_receiver.claim_applied"),
|
|
159
|
+
(LeaderEvent::ReceiverClaimRefused, "leader_receiver.claim_refused"),
|
|
160
|
+
(LeaderEvent::ReceiverRebindRequired, "leader_receiver.rebind_required"),
|
|
161
|
+
(LeaderEvent::ReceiverAttachFailed, "leader_receiver.attach_failed"),
|
|
162
|
+
(LeaderEvent::ReceiverStateDivergenceRepaired, "leader_receiver.state_divergence_repaired"),
|
|
163
|
+
(LeaderEvent::ReceiverFirstTimeEnvSeeded, "leader_receiver.first_time_env_seeded"),
|
|
164
|
+
(LeaderEvent::ReceiverAutobindSkipped, "leader_receiver.autobind_skipped"),
|
|
165
|
+
(LeaderEvent::ReceiverRequeuedExhaustedWatchers, "leader_receiver.requeued_exhausted_watchers"),
|
|
166
|
+
(LeaderEvent::ReceiverAmbiguousCandidates, "leader_receiver.ambiguous_candidates"),
|
|
167
|
+
(LeaderEvent::ReceiverClaimRequeue, "leader_receiver.claim_requeue"),
|
|
168
|
+
(LeaderEvent::OwnerAdoptedOnRestart, "owner.adopted_on_restart"),
|
|
169
|
+
(LeaderEvent::OwnerBoundFromCallerPane, "owner.bound_from_caller_pane"),
|
|
170
|
+
(LeaderEvent::OwnerBindRefused, "owner.bind_refused"),
|
|
171
|
+
(LeaderEvent::OwnerEpochAdvanced, "owner_epoch_advanced"),
|
|
172
|
+
(LeaderEvent::LeaderSessionUuidOverride, "leader_session_uuid.override"),
|
|
173
|
+
(LeaderEvent::LeaderStart, "leader.start"),
|
|
174
|
+
(LeaderEvent::ResultWatcherRequeued, "result_watcher.requeued"),
|
|
175
|
+
(LeaderEvent::IdleTakeoverClassify, "idle_takeover.classify"),
|
|
176
|
+
(LeaderEvent::IdleTakeoverPing, "idle_takeover.ping"),
|
|
177
|
+
(LeaderEvent::IdleTakeoverReminder, "idle_takeover.reminder"),
|
|
178
|
+
(LeaderEvent::IdleTakeoverPushFailed, "idle_takeover.push_failed"),
|
|
179
|
+
];
|
|
180
|
+
for (ev, want) in cases {
|
|
181
|
+
assert_eq!(ev.name(), want, "{ev:?}");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
#[test]
|
|
4
|
+
#[serial_test::serial(env)]
|
|
5
|
+
fn d1_claim_team_owner_includes_os_user() {
|
|
6
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
7
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
8
|
+
let ws = p2_temp_ws("d1_owner_keys");
|
|
9
|
+
let mut state = serde_json::json!({"session_name": "team-agent-x"});
|
|
10
|
+
let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
|
|
11
|
+
&PaneId::new("%5"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%5"])).unwrap();
|
|
12
|
+
assert!(r.ok && r.status == LeaseStatus::Claimed, "precondition: vacant acquire");
|
|
13
|
+
let owner = state["team_owner"].as_object().expect("team_owner object");
|
|
14
|
+
assert!(owner.contains_key("os_user"),
|
|
15
|
+
"golden claim-path team_owner carries os_user even when empty; keys={:?}", owner.keys().collect::<Vec<_>>());
|
|
16
|
+
let keys: Vec<&str> = owner.keys().map(String::as_str).collect();
|
|
17
|
+
assert_eq!(keys, vec!["pane_id","provider","machine_fingerprint","leader_session_uuid","owner_epoch","claimed_at","claimed_via","os_user"],
|
|
18
|
+
"golden new_owner includes os_user");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// D2 [BLOCK] — claim-path leader_receiver is golden's 14 keys in golden order; NO
|
|
22
|
+
// fingerprint/requested_provider/warning. Golden _receiver_from_claim_target (__init__.py:861-877).
|
|
23
|
+
// Rust LeaderReceiver serializes all 17 (no skip_serializing_if) -> 3 always-null extras leak. RED.
|
|
24
|
+
// (The POPULATED tmux values session_name/window_*/pane_* come from the caller-target scan — a
|
|
25
|
+
// deferred real-tmux seam, see d2_receiver_populated_from_caller_target_seam; the KEY-SET + ORDER
|
|
26
|
+
// locked here are unchanged by that scan.)
|
|
27
|
+
#[test]
|
|
28
|
+
#[serial_test::serial(env)]
|
|
29
|
+
fn d2_claim_leader_receiver_is_fourteen_golden_keys_in_order_no_extras() {
|
|
30
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
31
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
32
|
+
let ws = p2_temp_ws("d2_recv_keys");
|
|
33
|
+
let mut state = serde_json::json!({"session_name": "team-agent-x"});
|
|
34
|
+
let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
|
|
35
|
+
&PaneId::new("%5"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%5"])).unwrap();
|
|
36
|
+
assert!(r.ok && r.status == LeaseStatus::Claimed, "precondition: vacant acquire");
|
|
37
|
+
let recv = state["leader_receiver"].as_object().expect("leader_receiver object");
|
|
38
|
+
for extra in ["fingerprint", "requested_provider", "warning"] {
|
|
39
|
+
assert!(!recv.contains_key(extra),
|
|
40
|
+
"golden leader_receiver has NO '{extra}' key (Rust must add skip_serializing_if); keys={:?}", recv.keys().collect::<Vec<_>>());
|
|
41
|
+
}
|
|
42
|
+
let keys: Vec<&str> = recv.keys().map(String::as_str).collect();
|
|
43
|
+
assert_eq!(keys, vec![
|
|
44
|
+
"mode","status","provider","pane_id","session_name","window_index","window_name",
|
|
45
|
+
"pane_index","pane_tty","pane_current_command","leader_session_uuid","owner_epoch",
|
|
46
|
+
"attached_at","discovery",
|
|
47
|
+
], "golden _receiver_from_claim_target 14-key set + ORDER (__init__.py:861-877)");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// D2 seam — the caller-target SCAN that fills session_name/window_index/window_name/pane_index/
|
|
51
|
+
// pane_tty/pane_current_command from core_list_targets (golden _receiver_from_claim_target reads
|
|
52
|
+
// target[...]). Rust make_receiver leaves them None (no scan). Real-tmux: needs core_list_targets.
|
|
53
|
+
#[test]
|
|
54
|
+
#[ignore = "real-tmux seam: leader_receiver session_name/window_*/pane_* are populated from the \
|
|
55
|
+
caller target via core_list_targets (golden _receiver_from_claim_target); Rust has no \
|
|
56
|
+
scan (values null). Porter wires the target scan; this asserts the populated values."]
|
|
57
|
+
fn d2_receiver_populated_from_caller_target_seam() {
|
|
58
|
+
// Golden contract: new_receiver.session_name == caller_target.session_name, etc. Needs a live
|
|
59
|
+
// tmux target list (core_list_targets) — out of in-process scope.
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// D3 [BLOCK] — bound_pane is RECEIVER-first (golden :624 receiver.pane_id OR owner.pane_id). PROBE-B
|
|
63
|
+
// (receiver=%9, owner=%1, caller=%9): golden ok=True already_bound. Rust derives OWNER-first
|
|
64
|
+
// (owner %1 != caller %9) -> owner live + !confirm -> refused force_confirm_required. RED.
|
|
65
|
+
#[test]
|
|
66
|
+
#[serial_test::serial(env)]
|
|
67
|
+
fn d3_bound_pane_receiver_first_reclaim_from_receiver_pane_is_already_bound() {
|
|
68
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
69
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
70
|
+
let ws = p2_temp_ws("d3_receiver_first");
|
|
71
|
+
let mut state = serde_json::json!({
|
|
72
|
+
"session_name": "team-agent-x",
|
|
73
|
+
"team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":3,"claimed_at":"t","claimed_via":"claim-leader"},
|
|
74
|
+
"leader_receiver": {"pane_id":"%9","owner_epoch":3,"leader_session_uuid":"U"},
|
|
75
|
+
});
|
|
76
|
+
let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
|
|
77
|
+
&PaneId::new("%9"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%9","%1"])).unwrap();
|
|
78
|
+
assert!(r.ok, "a re-claim from the bound RECEIVER pane (%9) is idempotent ok=true; got {r:?}");
|
|
79
|
+
assert_eq!(r.status, LeaseStatus::AlreadyBound,
|
|
80
|
+
"golden :624 receiver-first: bound_pane=receiver(%9)==caller(%9) -> already_bound, NOT force_confirm");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// D4 [WARN] — precheck_epoch uses Python truthiness: owner_epoch=0 is FALSY -> falls through to
|
|
84
|
+
// receiver_epoch=5 (golden _lease_epoch :400 int(owner.epoch or recv.epoch or 0)). Rust
|
|
85
|
+
// current_owner_epoch Some(0) stops the chain -> 0. Surfaces in the force_confirm refusal. RED.
|
|
86
|
+
#[test]
|
|
87
|
+
#[serial_test::serial(env)]
|
|
88
|
+
fn d4_precheck_epoch_zero_is_falsy_falls_through_to_receiver() {
|
|
89
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
90
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
91
|
+
let ws = p2_temp_ws("d4_epoch_falsy");
|
|
92
|
+
let mut state = serde_json::json!({
|
|
93
|
+
"session_name": "team-agent-x",
|
|
94
|
+
"team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":0,"claimed_at":"t","claimed_via":"claim-leader"},
|
|
95
|
+
"leader_receiver": {"pane_id":"%1","owner_epoch":5,"leader_session_uuid":"U"},
|
|
96
|
+
});
|
|
97
|
+
let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
|
|
98
|
+
&PaneId::new("%2"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%1","%2"])).unwrap();
|
|
99
|
+
assert_eq!(r.status, LeaseStatus::Refused, "precondition: live owner %1 + no confirm -> force_confirm");
|
|
100
|
+
assert_eq!(r.owner_epoch, Some(OwnerEpoch(5)),
|
|
101
|
+
"golden _lease_epoch: owner_epoch=0 is falsy -> receiver_epoch=5; Rust stops at Some(0)");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// D5 [BLOCK] — success path emits leader_receiver.rebind_applied + owner_epoch_advanced (golden
|
|
105
|
+
// :712-729), NOT the incident-arm leader_receiver.claim_applied (:791). RED.
|
|
106
|
+
#[test]
|
|
107
|
+
#[serial_test::serial(env)]
|
|
108
|
+
fn d5_success_emits_rebind_applied_and_owner_epoch_advanced_not_claim_applied() {
|
|
109
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
110
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
111
|
+
let ws = p2_temp_ws("d5_events");
|
|
112
|
+
let mut state = serde_json::json!({"session_name": "team-agent-x"});
|
|
113
|
+
let _ = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
|
|
114
|
+
&PaneId::new("%5"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%5"])).unwrap();
|
|
115
|
+
let names = event_names(&ws);
|
|
116
|
+
assert!(names.iter().any(|n| n == "leader_receiver.rebind_applied"),
|
|
117
|
+
"golden success emits leader_receiver.rebind_applied (:712); got {names:?}");
|
|
118
|
+
assert!(names.iter().any(|n| n == "owner_epoch_advanced"),
|
|
119
|
+
"golden success emits owner_epoch_advanced (:721); got {names:?}");
|
|
120
|
+
assert!(!names.iter().any(|n| n == "leader_receiver.claim_applied"),
|
|
121
|
+
"claim_applied is the INCIDENT-arm event, NEVER on the no-incident path; got {names:?}");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// D6 [WARN] — EVERY lease refusal writes a leader_receiver audit event (golden _emit_lease_refusal).
|
|
125
|
+
// not_in_tmux_pane ∈ _LEASE_REBIND_REQUIRED_REASONS -> "leader_receiver.rebind_required" (:481).
|
|
126
|
+
// Rust refused() takes no event_log -> writes nothing. RED.
|
|
127
|
+
#[test]
|
|
128
|
+
#[serial_test::serial(env)]
|
|
129
|
+
fn d6_refusal_emits_leader_receiver_audit_event() {
|
|
130
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
131
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
132
|
+
let ws = p2_temp_ws("d6_refusal_event");
|
|
133
|
+
let mut state = serde_json::json!({});
|
|
134
|
+
let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
|
|
135
|
+
&PaneId::new(""), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&[])).unwrap();
|
|
136
|
+
assert_eq!(r.reason, Some(LeaseReason::NotInTmuxPane), "precondition: empty caller -> not_in_tmux_pane");
|
|
137
|
+
let names = event_names(&ws);
|
|
138
|
+
assert!(names.iter().any(|n| n == "leader_receiver.rebind_required"),
|
|
139
|
+
"golden writes a leader_receiver.rebind_required audit on the not_in_tmux_pane refusal; Rust writes none. got {names:?}");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// D8 [WARN] — provider PRESERVED from the prior receiver on re-claim (golden
|
|
143
|
+
// _receiver_from_claim_target provider=previous.provider or 'codex'; new_owner=new_recv.provider or
|
|
144
|
+
// owner.provider or 'codex'). A dead-owner recover of a CLAUDE leader keeps provider='claude'. Rust
|
|
145
|
+
// make_receiver/make_owner hardcode Provider::Codex. RED.
|
|
146
|
+
#[test]
|
|
147
|
+
#[serial_test::serial(env)]
|
|
148
|
+
fn d8_provider_preserved_from_prior_receiver_on_reclaim() {
|
|
149
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
150
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
151
|
+
let ws = p2_temp_ws("d8_provider");
|
|
152
|
+
let mut state = serde_json::json!({
|
|
153
|
+
"session_name": "team-agent-x",
|
|
154
|
+
"team_owner": {"pane_id":"%1","provider":"claude","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":3,"claimed_at":"t","claimed_via":"claim-leader"},
|
|
155
|
+
"leader_receiver": {"pane_id":"%1","provider":"claude","owner_epoch":3,"leader_session_uuid":"U"},
|
|
156
|
+
});
|
|
157
|
+
let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
|
|
158
|
+
&PaneId::new("%5"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%5"])).unwrap();
|
|
159
|
+
assert!(r.ok && r.status == LeaseStatus::Claimed, "precondition: dead-owner %1 recover");
|
|
160
|
+
assert_eq!(state.pointer("/team_owner/provider").and_then(|v| v.as_str()), Some("claude"),
|
|
161
|
+
"golden preserves the prior provider (claude) on re-claim; Rust hardcodes codex");
|
|
162
|
+
assert_eq!(state.pointer("/leader_receiver/provider").and_then(|v| v.as_str()), Some("claude"),
|
|
163
|
+
"the new receiver also preserves the prior provider (golden previous.provider)");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// TOCTOU CAS [BLOCK, flagged] — golden :654-671 re-reads owner_epoch INSIDE the lock and refuses
|
|
167
|
+
// owner_epoch_advanced on a concurrent bump (no double-bind). Rust has NO locked re-read; the race
|
|
168
|
+
// silently double-binds (LeaseReason::OwnerEpochAdvanced is unreachable). Simulated: on-disk
|
|
169
|
+
// state.json (the locked-re-read source) carries epoch 5 while the passed precheck state is epoch 3.
|
|
170
|
+
// RED: today Rust ignores disk and claims. Porter: re-read select_runtime_state under the lock and
|
|
171
|
+
// refuse owner_epoch_advanced when locked_epoch != precheck_epoch.
|
|
172
|
+
#[test]
|
|
173
|
+
#[serial_test::serial(env)]
|
|
174
|
+
fn toctou_locked_reread_refuses_owner_epoch_advanced_on_concurrent_bump() {
|
|
175
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
176
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
177
|
+
let ws = p2_temp_ws("toctou_cas");
|
|
178
|
+
// disk (the locked-re-read source): a concurrent claimer bumped owner_epoch to 5.
|
|
179
|
+
crate::state::persist::save_runtime_state(&ws, &serde_json::json!({
|
|
180
|
+
"session_name": "team-agent-x",
|
|
181
|
+
"team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":5,"claimed_at":"t","claimed_via":"claim-leader"},
|
|
182
|
+
})).unwrap();
|
|
183
|
+
// precheck state passed to the claim: epoch 3, vacant (no owner pane) -> no force_confirm gate.
|
|
184
|
+
let mut state = serde_json::json!({"session_name":"team-agent-x","leader_receiver":{"owner_epoch":3}});
|
|
185
|
+
let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
|
|
186
|
+
&PaneId::new("%5"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%5"])).unwrap();
|
|
187
|
+
assert_eq!(r.status, LeaseStatus::Refused,
|
|
188
|
+
"golden TOCTOU CAS: locked epoch 5 != precheck 3 -> refused; Rust skips the re-read and claims. got {r:?}");
|
|
189
|
+
assert_eq!(r.reason, Some(LeaseReason::OwnerEpochAdvanced),
|
|
190
|
+
"the refusal reason is owner_epoch_advanced (:666)");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// attach_leader_to_state [BLOCK, flagged] — golden non-first-time path writes leader_receiver ONLY:
|
|
194
|
+
// NO team_owner write, NO epoch advance (after the strict-uuid gate). Rust stub writes BOTH and
|
|
195
|
+
// advances epoch (precheck+1), ignoring source/require_current. RED (locked before the autobind/launch
|
|
196
|
+
// wiring lands, so it does not silently overwrite the owner + bump epoch on every attach).
|
|
197
|
+
#[test]
|
|
198
|
+
#[serial_test::serial(env)]
|
|
199
|
+
fn attach_leader_to_state_nonfirst_does_not_overwrite_owner_or_advance_epoch() {
|
|
200
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
201
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
202
|
+
let ws = p2_temp_ws("attach_nonfirst");
|
|
203
|
+
let mut state = serde_json::json!({
|
|
204
|
+
"session_name": "team-agent-x",
|
|
205
|
+
"team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":3,"claimed_at":"t","claimed_via":"claim-leader"},
|
|
206
|
+
});
|
|
207
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
208
|
+
let _ = attach_leader_to_state(&ws, &mut state, Some(&PaneId::new("%2")),
|
|
209
|
+
crate::provider::Provider::Codex, &event_log, LeaseSource::Manual, false);
|
|
210
|
+
assert_eq!(state.pointer("/team_owner/pane_id").and_then(|v| v.as_str()), Some("%1"),
|
|
211
|
+
"non-first-time attach must NOT overwrite team_owner.pane_id (golden writes leader_receiver only); Rust rebinds it to %2");
|
|
212
|
+
assert_eq!(state.pointer("/team_owner/owner_epoch").and_then(|v| v.as_u64()), Some(3),
|
|
213
|
+
"non-first-time attach must NOT advance owner_epoch (golden keeps 3); Rust bumps to 4");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// C3 [WARN] — leader_identity.current_pane_id honors TEAM_AGENT_LEADER_PANE_ID priority (golden
|
|
217
|
+
// :366 = env(TEAM_AGENT_LEADER_PANE_ID) or env(TMUX_PANE) or None). Rust owner_bind.rs:32 reads
|
|
218
|
+
// TMUX_PANE only. RED: with TEAM_AGENT_LEADER_PANE_ID set + TMUX_PANE unset, current_pane_id must be
|
|
219
|
+
// the LEADER_PANE_ID. (Sibling gap, noted: last_seen_at = receiver.attached_at OR receiver.last_seen_at;
|
|
220
|
+
// Rust reads attached_at only.)
|
|
221
|
+
#[test]
|
|
222
|
+
#[serial_test::serial(env)]
|
|
223
|
+
fn c3_leader_identity_current_pane_honors_leader_pane_id_env() {
|
|
224
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
225
|
+
let _e = EnvGuard::apply(&[
|
|
226
|
+
("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
|
|
227
|
+
("TEAM_AGENT_LEADER_PANE_ID", Some("%foo")),
|
|
228
|
+
("TMUX_PANE", None),
|
|
229
|
+
]);
|
|
230
|
+
let ws = p2_temp_ws("c3_pane_env");
|
|
231
|
+
let v = leader_identity(&ws, None).unwrap();
|
|
232
|
+
assert_eq!(v["current_pane_id"], serde_json::json!("%foo"),
|
|
233
|
+
"golden :366 current_pane_id = TEAM_AGENT_LEADER_PANE_ID or TMUX_PANE or None; Rust reads TMUX_PANE only");
|
|
234
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
// =====================================================================
|
|
4
|
+
// 12. leader_identity_context — override / state uuid / derive 三源(unimplemented → RED)
|
|
5
|
+
// =====================================================================
|
|
6
|
+
|
|
7
|
+
// 无 override env、无 state record → derive(machine, ws_abspath, user, team)。
|
|
8
|
+
// 现 unimplemented → 调用即 RED;锁住返回 LeaderIdentity 且 source==Derived。
|
|
9
|
+
#[test]
|
|
10
|
+
#[serial_test::serial(env)]
|
|
11
|
+
fn leader_identity_context_derives_when_no_override_no_state() {
|
|
12
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_lic_{}", std::process::id()));
|
|
13
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
14
|
+
// 空 state(无 team_owner/leader_receiver uuid)。
|
|
15
|
+
let state = serde_json::json!({});
|
|
16
|
+
let id = leader_identity_context(&ws, None, Some(&state)).unwrap();
|
|
17
|
+
// 无 override → source 为 Derived(leader plan 侧;__init__.py:206)。
|
|
18
|
+
assert_eq!(id.leader_session_uuid_source, LeaderSessionUuidSource::Derived);
|
|
19
|
+
// uuid 是 32 hex(derive 形)。
|
|
20
|
+
assert_eq!(id.leader_session_uuid.as_str().len(), 32);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// leader_identity:CLI 直出 dict(__init__.py:355-369)。unimplemented → RED。
|
|
24
|
+
// 强化:uuid_prefix 必须 == leader_identity_context 派生 uuid 的前 12 hex(绑到真值,
|
|
25
|
+
// 而非任意 12 字符);整个 dict 的 machine_fingerprint/os_user/team_id/source 必须与 context 一致。
|
|
26
|
+
#[test]
|
|
27
|
+
#[serial_test::serial(env)]
|
|
28
|
+
fn leader_identity_dict_ties_prefix_and_fields_to_derived_context() {
|
|
29
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_lid_{}", std::process::id()));
|
|
30
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
31
|
+
// 空 state → context 走 derive(无 override/无 state uuid)。
|
|
32
|
+
let ctx = leader_identity_context(&ws, None, Some(&serde_json::json!({}))).unwrap();
|
|
33
|
+
let expected_prefix = &ctx.leader_session_uuid.as_str()[..12];
|
|
34
|
+
let v = leader_identity(&ws, None).unwrap();
|
|
35
|
+
assert_eq!(v["ok"], serde_json::json!(true));
|
|
36
|
+
// uuid_prefix 绑到派生真值的前 12 hex(错的 12 字符串会被抓)。
|
|
37
|
+
assert_eq!(v["uuid_prefix"].as_str().unwrap(), expected_prefix);
|
|
38
|
+
// 其余身份字段与 context 字节一致。
|
|
39
|
+
assert_eq!(v["machine_fingerprint"].as_str().unwrap(), ctx.machine_fingerprint);
|
|
40
|
+
assert_eq!(v["os_user"].as_str().unwrap(), ctx.os_user);
|
|
41
|
+
assert_eq!(v["team_id"].as_str().unwrap(), ctx.team_id.as_str());
|
|
42
|
+
// source == 派生侧 "derived"(无 override → 不是 "override"/"env")。
|
|
43
|
+
assert_eq!(v["source"], serde_json::json!("derived"));
|
|
44
|
+
assert_eq!(ctx.leader_session_uuid_source, LeaderSessionUuidSource::Derived);
|
|
45
|
+
// CLI 直出形态:current_pane_id / last_seen_at 在无 env/无 receiver 时为 null。
|
|
46
|
+
assert_eq!(v["last_seen_at"], serde_json::Value::Null);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// =====================================================================
|
|
50
|
+
// 13. leader_start_plan(unimplemented → RED):钉 mode 选择 + leader_env 导出键。
|
|
51
|
+
// =====================================================================
|
|
52
|
+
|
|
53
|
+
// leader_start_plan(__init__.py:82-145)。强化:钉具体 mode + plan 内容,而非 provider 回声。
|
|
54
|
+
// 确定性环境:在 TMUX 内 → exec_provider;不在 TMUX(且 tmux 可用)→ new_tmux_session,
|
|
55
|
+
// session_name==leader_session_name(Fake,ws),leader_env 携带 5 个 TEAM_AGENT_* 导出键。
|
|
56
|
+
// 注:`detached` 在 leader_start_plan 返回值里恒为 false(__init__.py:174 "detached": False);
|
|
57
|
+
// 非 tty 的 `-d` 插入发生在 start_leader 调用者层(:74-78),不在本 plan 边界 → 不在此断言 detached。
|
|
58
|
+
// unimplemented → RED。
|
|
59
|
+
#[test]
|
|
60
|
+
#[serial_test::serial(env)]
|
|
61
|
+
fn leader_start_plan_pins_mode_and_leader_env() {
|
|
62
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_lsp_{}", std::process::id()));
|
|
63
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
64
|
+
let plan = leader_start_plan(Provider::Fake, &[], &ws, false, false, None).unwrap();
|
|
65
|
+
assert_eq!(plan.provider, Provider::Fake);
|
|
66
|
+
if std::env::var_os("TMUX").is_some() {
|
|
67
|
+
// 已在 tmux 内 → exec in-place。
|
|
68
|
+
assert_eq!(plan.mode, LeaderStartMode::ExecProvider);
|
|
69
|
+
} else {
|
|
70
|
+
// 不在 tmux → 新建 tmux session(测试环境假定 tmux 可用;否则 Err(Start))。
|
|
71
|
+
assert_eq!(plan.mode, LeaderStartMode::NewTmuxSession);
|
|
72
|
+
// session_name 由派生公式确定。
|
|
73
|
+
assert_eq!(plan.session_name.as_ref(), Some(&leader_session_name(Provider::Fake, &ws)));
|
|
74
|
+
// plan 边界 detached 恒 false(`-d` 插入在 start_leader 层,非此处)。
|
|
75
|
+
assert!(!plan.detached, "leader_start_plan 返回值 detached 恒 false");
|
|
76
|
+
// leader_env 携带 5 个 TEAM_AGENT_* 导出键(_leader_provider_env)。
|
|
77
|
+
for key in [
|
|
78
|
+
"TEAM_AGENT_LEADER_PROVIDER",
|
|
79
|
+
"TEAM_AGENT_LEADER_SESSION_UUID",
|
|
80
|
+
"TEAM_AGENT_MACHINE_FINGERPRINT",
|
|
81
|
+
"TEAM_AGENT_WORKSPACE",
|
|
82
|
+
"TEAM_AGENT_TEAM_ID",
|
|
83
|
+
] {
|
|
84
|
+
assert!(plan.leader_env.contains_key(key), "leader_env 缺导出键 {key}");
|
|
85
|
+
}
|
|
86
|
+
assert_eq!(
|
|
87
|
+
plan.leader_env.get("TEAM_AGENT_LEADER_PROVIDER").map(String::as_str),
|
|
88
|
+
Some("fake")
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ═══════════════ P2 FIX-LOOP RED (复绿即对抗 cross-model findings) ═══════════════
|
|
94
|
+
// Lock CORRECT Python v0.2.11 leader-identity behavior the contracts missed.
|
|
95
|
+
// Golden re-probed via /tmp/probe_p2_leader.py vs team-agent-public @ 439bef8
|
|
96
|
+
// (leader/__init__.py:_leader_identity_context / _identity_* / _detect_dual_state_divergence).
|
|
97
|
+
|
|
98
|
+
#[test]
|
|
99
|
+
#[serial_test::serial(env)]
|
|
100
|
+
fn p2_leader_state_uuid_source_is_derived_not_env() {
|
|
101
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
102
|
+
let _e = EnvGuard::apply(&[
|
|
103
|
+
("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
|
|
104
|
+
("TEAM_AGENT_LEADER_SESSION_UUID", None),
|
|
105
|
+
]);
|
|
106
|
+
let ws = p2_temp_ws("src");
|
|
107
|
+
let state = serde_json::json!({"team_owner": {"leader_session_uuid": "STATEUUID123"}});
|
|
108
|
+
let id = leader_identity_context(&ws, None, Some(&state)).unwrap();
|
|
109
|
+
assert_eq!(id.leader_session_uuid_source, LeaderSessionUuidSource::Derived);
|
|
110
|
+
assert_eq!(id.leader_session_uuid.as_str(), "STATEUUID123");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// P1 — operator override env var is TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE (the
|
|
114
|
+
// _OVERRIDE suffix), per leader/__init__.py:197 — NOT the injected child-env var.
|
|
115
|
+
#[test]
|
|
116
|
+
#[serial_test::serial(env)]
|
|
117
|
+
fn p2_leader_override_reads_override_suffixed_env_var() {
|
|
118
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
119
|
+
let _e = EnvGuard::apply(&[
|
|
120
|
+
("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", Some("OVERRIDE_X")),
|
|
121
|
+
("TEAM_AGENT_LEADER_SESSION_UUID", None),
|
|
122
|
+
]);
|
|
123
|
+
let ws = p2_temp_ws("ovr");
|
|
124
|
+
let id = leader_identity_context(&ws, None, None).unwrap();
|
|
125
|
+
assert_eq!(id.leader_session_uuid_source, LeaderSessionUuidSource::Override);
|
|
126
|
+
assert_eq!(id.leader_session_uuid.as_str(), "OVERRIDE_X");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// P1 — derived inputs read state: machine_fingerprint = state team_owner record first
|
|
130
|
+
// (_identity_machine_fingerprint); team_id = team_state_key(state) from session_name
|
|
131
|
+
// (default 'current', not a hardcoded 'default').
|
|
132
|
+
#[test]
|
|
133
|
+
#[serial_test::serial(env)]
|
|
134
|
+
fn p2_leader_derived_inputs_read_state_record() {
|
|
135
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
136
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_MACHINE_FINGERPRINT", None)]);
|
|
137
|
+
let ws = p2_temp_ws("der");
|
|
138
|
+
let state = serde_json::json!({
|
|
139
|
+
"team_owner": {"machine_fingerprint": "RECORDED-FP-FROM-STATE"},
|
|
140
|
+
"session_name": "team-agent-myteam"
|
|
141
|
+
});
|
|
142
|
+
let id = leader_identity_context(&ws, None, Some(&state)).unwrap();
|
|
143
|
+
assert_eq!(id.machine_fingerprint, "RECORDED-FP-FROM-STATE", "state record fp beats env/hostname");
|
|
144
|
+
assert_eq!(id.team_id.as_str(), "team-agent-myteam", "team_id from state.session_name");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// P1 — os_user fallback chain = USER or USERNAME or '' (_identity_os_user), NOT
|
|
148
|
+
// USER or LOGNAME or 'unknown'. (USERNAME is the 2nd choice; empty-string fallback.)
|
|
149
|
+
#[test]
|
|
150
|
+
#[serial_test::serial(env)]
|
|
151
|
+
fn p2_leader_os_user_honors_username_then_empty() {
|
|
152
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
153
|
+
let _e = EnvGuard::apply(&[
|
|
154
|
+
("USER", None),
|
|
155
|
+
("LOGNAME", None),
|
|
156
|
+
("USERNAME", Some("winuser")),
|
|
157
|
+
]);
|
|
158
|
+
let ws = p2_temp_ws("usr");
|
|
159
|
+
let id = leader_identity_context(&ws, None, None).unwrap();
|
|
160
|
+
assert_eq!(id.os_user, "winuser", "USERNAME is the second choice (not LOGNAME)");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// P1 — detect_dual_state_divergence must catch an owner leader_session_uuid split even
|
|
164
|
+
// when panes + epoch are identical (leader/__init__.py:574).
|
|
165
|
+
#[test]
|
|
166
|
+
#[serial_test::serial(env)]
|
|
167
|
+
fn p2_leader_detect_divergence_catches_owner_uuid_split() {
|
|
168
|
+
let ws = p2_temp_ws("div");
|
|
169
|
+
let snap_dir = crate::model::paths::runtime_dir(&ws).join("teams").join("sess1");
|
|
170
|
+
std::fs::create_dir_all(&snap_dir).unwrap();
|
|
171
|
+
let snap = serde_json::json!({
|
|
172
|
+
"session_name":"sess1",
|
|
173
|
+
"team_owner":{"pane_id":"%1","leader_session_uuid":"UUID_B","owner_epoch":5},
|
|
174
|
+
"leader_receiver":{"pane_id":"%1"}
|
|
175
|
+
});
|
|
176
|
+
std::fs::write(snap_dir.join("state.json"), serde_json::to_string(&snap).unwrap()).unwrap();
|
|
177
|
+
let state = serde_json::json!({
|
|
178
|
+
"session_name":"sess1",
|
|
179
|
+
"team_owner":{"pane_id":"%1","leader_session_uuid":"UUID_A","owner_epoch":5},
|
|
180
|
+
"leader_receiver":{"pane_id":"%1"}
|
|
181
|
+
});
|
|
182
|
+
let div = detect_dual_state_divergence(&ws, &state).unwrap();
|
|
183
|
+
assert!(div.is_some(), "owner uuid split (A vs B) with matching panes/epoch must be detected");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
187
|
+
// 14. WAVE-2 Lane B CONTRACT PASS — CLI-handler-facing byte-parity for the
|
|
188
|
+
// three verbs (claim-leader / takeover / identity) + their core lease
|
|
189
|
+
// machinery (_claim_lease_no_incident outcomes / _lease_refused shapes).
|
|
190
|
+
//
|
|
191
|
+
// GOLDEN (re-probed @ team-agent-public, leader/__init__.py +
|
|
192
|
+
// runtime.py:721/791). Each test labels RED|LOCK honestly:
|
|
193
|
+
// RED = drives an unimplemented!() body (claim_lease_no_incident /
|
|
194
|
+
// attach_leader_to_state) → panics today = correct RED-first.
|
|
195
|
+
// LOCK = drives an already-implemented stub/path → green today; pins
|
|
196
|
+
// the golden so a future port cannot regress it.
|
|
197
|
+
// Deferred to later adversarial rounds (Lane-A style): the ambiguous-
|
|
198
|
+
// incident claim arm (no_caller_pane / caller_not_candidate / dry_run /
|
|
199
|
+
// lost_race) which needs a seeded event-log incident + the broadcast
|
|
200
|
+
// requeue cross-lane; the strict-uuid attach refusal string (needs a
|
|
201
|
+
// live pane resolver). Those are #[ignore]/NOTE seams below.
|
|
202
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
203
|
+
|
|
204
|
+
// ── 14a. _claim_lease_no_incident OUTCOMES (golden __init__.py:598) ──────
|
|
205
|
+
//
|
|
206
|
+
// claim_lease_no_incident is unimplemented!() → every test here PANICS today
|