@team-agent/installer 0.2.10 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +744 -0
- package/Cargo.toml +34 -0
- package/crates/team-agent/Cargo.toml +33 -0
- package/crates/team-agent/src/cli/adapters.rs +1343 -0
- package/crates/team-agent/src/cli/diagnose.rs +554 -0
- package/crates/team-agent/src/cli/emit.rs +1077 -0
- package/crates/team-agent/src/cli/helpers.rs +88 -0
- package/crates/team-agent/src/cli/leader.rs +216 -0
- package/crates/team-agent/src/cli/mod.rs +1141 -0
- package/crates/team-agent/src/cli/profile.rs +306 -0
- package/crates/team-agent/src/cli/send.rs +215 -0
- package/crates/team-agent/src/cli/status.rs +179 -0
- package/crates/team-agent/src/cli/status_port.rs +502 -0
- package/crates/team-agent/src/cli/tests/base.rs +616 -0
- package/crates/team-agent/src/cli/tests/compile.rs +96 -0
- package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
- package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
- package/crates/team-agent/src/cli/tests/mod.rs +97 -0
- package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
- package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
- package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
- package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
- package/crates/team-agent/src/cli/types.rs +605 -0
- package/crates/team-agent/src/compiler/tests.rs +701 -0
- package/crates/team-agent/src/compiler.rs +489 -0
- package/crates/team-agent/src/coordinator/backoff.rs +153 -0
- package/crates/team-agent/src/coordinator/health.rs +436 -0
- package/crates/team-agent/src/coordinator/mod.rs +80 -0
- package/crates/team-agent/src/coordinator/orphan.rs +179 -0
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
- package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
- package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
- package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
- package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
- package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
- package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
- package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
- package/crates/team-agent/src/coordinator/tick.rs +2032 -0
- package/crates/team-agent/src/coordinator/types.rs +584 -0
- package/crates/team-agent/src/db/migration.rs +716 -0
- package/crates/team-agent/src/db/mod.rs +23 -0
- package/crates/team-agent/src/db/schema.rs +378 -0
- package/crates/team-agent/src/event_log.rs +375 -0
- package/crates/team-agent/src/fake_worker.rs +253 -0
- package/crates/team-agent/src/leader/helpers.rs +190 -0
- package/crates/team-agent/src/leader/inject.rs +33 -0
- package/crates/team-agent/src/leader/lease.rs +1063 -0
- package/crates/team-agent/src/leader/mod.rs +99 -0
- package/crates/team-agent/src/leader/owner_bind.rs +292 -0
- package/crates/team-agent/src/leader/rediscover/tests.rs +525 -0
- package/crates/team-agent/src/leader/rediscover.rs +1099 -0
- package/crates/team-agent/src/leader/start.rs +273 -0
- package/crates/team-agent/src/leader/takeover.rs +235 -0
- package/crates/team-agent/src/leader/tests/basics.rs +183 -0
- package/crates/team-agent/src/leader/tests/byte_findings.rs +234 -0
- package/crates/team-agent/src/leader/tests/identity.rs +206 -0
- package/crates/team-agent/src/leader/tests/idle.rs +271 -0
- package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
- package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -0
- package/crates/team-agent/src/leader/tests/mod.rs +125 -0
- package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
- package/crates/team-agent/src/leader/types.rs +487 -0
- package/crates/team-agent/src/lib.rs +85 -0
- package/crates/team-agent/src/lifecycle/display.rs +228 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
- package/crates/team-agent/src/lifecycle/launch.rs +1833 -0
- package/crates/team-agent/src/lifecycle/mod.rs +62 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
- package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
- package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
- package/crates/team-agent/src/lifecycle/restart.rs +76 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
- package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +933 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
- package/crates/team-agent/src/lifecycle/tests.rs +27 -0
- package/crates/team-agent/src/lifecycle/types.rs +685 -0
- package/crates/team-agent/src/main.rs +41 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
- package/crates/team-agent/src/mcp_server/mod.rs +183 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
- package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
- package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
- package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +159 -0
- package/crates/team-agent/src/mcp_server/tests.rs +38 -0
- package/crates/team-agent/src/mcp_server/tools.rs +603 -0
- package/crates/team-agent/src/mcp_server/types.rs +421 -0
- package/crates/team-agent/src/mcp_server/wire.rs +388 -0
- package/crates/team-agent/src/message_store.rs +767 -0
- package/crates/team-agent/src/messaging/activity.rs +433 -0
- package/crates/team-agent/src/messaging/delivery.rs +542 -0
- package/crates/team-agent/src/messaging/helpers.rs +209 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +340 -0
- package/crates/team-agent/src/messaging/mod.rs +147 -0
- package/crates/team-agent/src/messaging/peers.rs +32 -0
- package/crates/team-agent/src/messaging/results.rs +537 -0
- package/crates/team-agent/src/messaging/scheduler.rs +344 -0
- package/crates/team-agent/src/messaging/selftest.rs +100 -0
- package/crates/team-agent/src/messaging/send.rs +582 -0
- package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
- package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
- package/crates/team-agent/src/messaging/trust.rs +192 -0
- package/crates/team-agent/src/messaging/types.rs +355 -0
- package/crates/team-agent/src/messaging/watchers.rs +591 -0
- package/crates/team-agent/src/model/enums.rs +311 -0
- package/crates/team-agent/src/model/errors.rs +17 -0
- package/crates/team-agent/src/model/ids.rs +155 -0
- package/crates/team-agent/src/model/mod.rs +22 -0
- package/crates/team-agent/src/model/paths.rs +228 -0
- package/crates/team-agent/src/model/permissions.rs +567 -0
- package/crates/team-agent/src/model/routing.rs +340 -0
- package/crates/team-agent/src/model/spec.rs +680 -0
- package/crates/team-agent/src/model/task_graph.rs +380 -0
- package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
- package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
- package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
- package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
- package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
- package/crates/team-agent/src/model/yaml/tests.rs +288 -0
- package/crates/team-agent/src/model/yaml.rs +800 -0
- package/crates/team-agent/src/packaging/install.rs +305 -0
- package/crates/team-agent/src/packaging/migrate.rs +30 -0
- package/crates/team-agent/src/packaging/mod.rs +82 -0
- package/crates/team-agent/src/packaging/repair.rs +24 -0
- package/crates/team-agent/src/packaging/tests.rs +829 -0
- package/crates/team-agent/src/packaging/types.rs +369 -0
- package/crates/team-agent/src/provider/adapter.rs +801 -0
- package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
- package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
- package/crates/team-agent/src/provider/classify.rs +456 -0
- package/crates/team-agent/src/provider/faults.rs +136 -0
- package/crates/team-agent/src/provider/helpers.rs +41 -0
- package/crates/team-agent/src/provider/mod.rs +53 -0
- package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
- package/crates/team-agent/src/provider/tests/classify.rs +240 -0
- package/crates/team-agent/src/provider/tests/faults.rs +120 -0
- package/crates/team-agent/src/provider/tests/idle.rs +208 -0
- package/crates/team-agent/src/provider/tests/wire.rs +213 -0
- package/crates/team-agent/src/provider/tests.rs +31 -0
- package/crates/team-agent/src/provider/types.rs +424 -0
- package/crates/team-agent/src/state/identity.rs +656 -0
- package/crates/team-agent/src/state/mod.rs +58 -0
- package/crates/team-agent/src/state/owner_gate.rs +423 -0
- package/crates/team-agent/src/state/persist.rs +712 -0
- package/crates/team-agent/src/state/projection.rs +657 -0
- package/crates/team-agent/src/state/selector.rs +105 -0
- package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +586 -0
- package/crates/team-agent/src/tmux_backend.rs +758 -0
- package/crates/team-agent/src/transport/test_support.rs +252 -0
- package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
- package/crates/team-agent/src/transport/tests/mod.rs +199 -0
- package/crates/team-agent/src/transport/tests/wire.rs +527 -0
- package/crates/team-agent/src/transport.rs +774 -0
- package/npm/install.mjs +90 -106
- package/package.json +15 -13
- package/crates/team-agent-core/Cargo.toml +0 -12
- package/crates/team-agent-core/src/lib.rs +0 -332
- package/crates/team-agent-core/src/main.rs +0 -152
- package/pyproject.toml +0 -18
- package/scripts/install.py +0 -88
- package/scripts/run_regression_tests.py +0 -83
- package/src/team_agent/__init__.py +0 -3
- package/src/team_agent/__main__.py +0 -5
- package/src/team_agent/_legacy_pane_discovery.py +0 -186
- package/src/team_agent/abnormal_track.py +0 -253
- package/src/team_agent/approvals/__init__.py +0 -65
- package/src/team_agent/approvals/constants.py +0 -6
- package/src/team_agent/approvals/parsing.py +0 -176
- package/src/team_agent/approvals/runtime_prompts.py +0 -171
- package/src/team_agent/approvals/status.py +0 -176
- package/src/team_agent/cli/__init__.py +0 -137
- package/src/team_agent/cli/commands.py +0 -481
- package/src/team_agent/cli/e2e.py +0 -202
- package/src/team_agent/cli/helpers.py +0 -226
- package/src/team_agent/cli/parser.py +0 -540
- package/src/team_agent/compiler.py +0 -334
- package/src/team_agent/coordinator/__init__.py +0 -53
- package/src/team_agent/coordinator/__main__.py +0 -83
- package/src/team_agent/coordinator/lifecycle.py +0 -363
- package/src/team_agent/coordinator/metadata.py +0 -61
- package/src/team_agent/coordinator/paths.py +0 -17
- package/src/team_agent/diagnose/__init__.py +0 -48
- package/src/team_agent/diagnose/checks.py +0 -101
- package/src/team_agent/diagnose/comms.py +0 -213
- package/src/team_agent/diagnose/health.py +0 -241
- package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
- package/src/team_agent/diagnose/preflight.py +0 -194
- package/src/team_agent/diagnose/quick_start.py +0 -324
- package/src/team_agent/display/__init__.py +0 -92
- package/src/team_agent/display/adaptive.py +0 -511
- package/src/team_agent/display/backend.py +0 -46
- package/src/team_agent/display/close.py +0 -154
- package/src/team_agent/display/ghostty.py +0 -77
- package/src/team_agent/display/rebuild.py +0 -102
- package/src/team_agent/display/tiling.py +0 -156
- package/src/team_agent/display/worker_window.py +0 -114
- package/src/team_agent/display/workspace.py +0 -382
- package/src/team_agent/errors.py +0 -10
- package/src/team_agent/events.py +0 -84
- package/src/team_agent/fake_worker.py +0 -80
- package/src/team_agent/idle_predicate.py +0 -200
- package/src/team_agent/idle_takeover.py +0 -59
- package/src/team_agent/idle_takeover_wiring.py +0 -111
- package/src/team_agent/launch/__init__.py +0 -41
- package/src/team_agent/launch/bootstrap.py +0 -85
- package/src/team_agent/launch/config.py +0 -106
- package/src/team_agent/launch/core.py +0 -301
- package/src/team_agent/launch/requirements.py +0 -57
- package/src/team_agent/leader/__init__.py +0 -926
- package/src/team_agent/leader_binding.py +0 -183
- package/src/team_agent/lifecycle/__init__.py +0 -5
- package/src/team_agent/lifecycle/agents.py +0 -278
- package/src/team_agent/lifecycle/operations.py +0 -411
- package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
- package/src/team_agent/lifecycle/start.py +0 -363
- package/src/team_agent/mcp_server/__init__.py +0 -42
- package/src/team_agent/mcp_server/__main__.py +0 -7
- package/src/team_agent/mcp_server/contracts.py +0 -148
- package/src/team_agent/mcp_server/normalize.py +0 -257
- package/src/team_agent/mcp_server/server.py +0 -150
- package/src/team_agent/mcp_server/tools.py +0 -352
- package/src/team_agent/message_store/__init__.py +0 -23
- package/src/team_agent/message_store/agent_health.py +0 -113
- package/src/team_agent/message_store/core.py +0 -497
- package/src/team_agent/message_store/leader_notification_log.py +0 -198
- package/src/team_agent/message_store/result_watchers.py +0 -251
- package/src/team_agent/message_store/schema.py +0 -308
- package/src/team_agent/message_store/schema_migration.py +0 -448
- package/src/team_agent/messaging/__init__.py +0 -1
- package/src/team_agent/messaging/activity_detector.py +0 -254
- package/src/team_agent/messaging/delivery.py +0 -473
- package/src/team_agent/messaging/deps.py +0 -247
- package/src/team_agent/messaging/idle_alerts.py +0 -423
- package/src/team_agent/messaging/internal_delivery.py +0 -46
- package/src/team_agent/messaging/leader.py +0 -497
- package/src/team_agent/messaging/leader_api_errors.py +0 -216
- package/src/team_agent/messaging/leader_panes.py +0 -673
- package/src/team_agent/messaging/owner_bypass.py +0 -29
- package/src/team_agent/messaging/result_delivery.py +0 -539
- package/src/team_agent/messaging/results.py +0 -447
- package/src/team_agent/messaging/scheduler.py +0 -450
- package/src/team_agent/messaging/send.py +0 -532
- package/src/team_agent/messaging/session_drift.py +0 -94
- package/src/team_agent/messaging/tmux_io.py +0 -506
- package/src/team_agent/messaging/tmux_prompt.py +0 -338
- package/src/team_agent/messaging/trust_auto_answer.py +0 -52
- package/src/team_agent/orchestrator/__init__.py +0 -376
- package/src/team_agent/orchestrator/plan.py +0 -122
- package/src/team_agent/orchestrator/state.py +0 -128
- package/src/team_agent/paths.py +0 -45
- package/src/team_agent/permissions.py +0 -123
- package/src/team_agent/profiles/__init__.py +0 -82
- package/src/team_agent/profiles/constants.py +0 -19
- package/src/team_agent/profiles/core.py +0 -407
- package/src/team_agent/profiles/helpers.py +0 -69
- package/src/team_agent/profiles/provider_env.py +0 -188
- package/src/team_agent/profiles/smoke.py +0 -201
- package/src/team_agent/provider_cli/__init__.py +0 -43
- package/src/team_agent/provider_cli/adapter.py +0 -172
- package/src/team_agent/provider_cli/base.py +0 -48
- package/src/team_agent/provider_cli/claude.py +0 -457
- package/src/team_agent/provider_cli/codex.py +0 -336
- package/src/team_agent/provider_cli/copilot.py +0 -8
- package/src/team_agent/provider_cli/fake.py +0 -39
- package/src/team_agent/provider_cli/gemini.py +0 -95
- package/src/team_agent/provider_cli/opencode.py +0 -8
- package/src/team_agent/provider_cli/prompt.py +0 -62
- package/src/team_agent/provider_cli/registry.py +0 -18
- package/src/team_agent/provider_cli/unsupported.py +0 -32
- package/src/team_agent/provider_state/README.md +0 -78
- package/src/team_agent/provider_state/__init__.py +0 -86
- package/src/team_agent/provider_state/claude.py +0 -86
- package/src/team_agent/provider_state/codex.py +0 -84
- package/src/team_agent/provider_state/common.py +0 -207
- package/src/team_agent/provider_state/registry.py +0 -118
- package/src/team_agent/providers.py +0 -163
- package/src/team_agent/quality_gates.py +0 -104
- package/src/team_agent/restart/__init__.py +0 -34
- package/src/team_agent/restart/orchestration.py +0 -554
- package/src/team_agent/restart/selection.py +0 -89
- package/src/team_agent/restart/snapshot.py +0 -70
- package/src/team_agent/routing.py +0 -84
- package/src/team_agent/runtime.py +0 -1239
- package/src/team_agent/rust_core.py +0 -327
- package/src/team_agent/sessions/__init__.py +0 -25
- package/src/team_agent/sessions/capture.py +0 -143
- package/src/team_agent/sessions/inventory.py +0 -44
- package/src/team_agent/sessions/resume.py +0 -135
- package/src/team_agent/simple_yaml.py +0 -236
- package/src/team_agent/spec.py +0 -370
- package/src/team_agent/state.py +0 -602
- package/src/team_agent/status/__init__.py +0 -63
- package/src/team_agent/status/approvals.py +0 -52
- package/src/team_agent/status/compact.py +0 -158
- package/src/team_agent/status/constants.py +0 -18
- package/src/team_agent/status/inbox.py +0 -58
- package/src/team_agent/status/peek.py +0 -117
- package/src/team_agent/status/queries.py +0 -199
- package/src/team_agent/task_graph.py +0 -80
- package/src/team_agent/terminal.py +0 -57
- package/src/team_agent/wake.py +0 -58
- package/src/team_agent/watch/__init__.py +0 -145
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
// =====================================================================
|
|
4
|
+
// 8. idle-takeover 接线 — build_idle_nodes / leader_node(unimplemented → RED)
|
|
5
|
+
// 命门:rollout_path=None → Unknown → never idle;leader path/provider 缺 → 省略;
|
|
6
|
+
// MUST-NOT-13:经 TurnStateClassifier mock,断言零 provider client 直连。
|
|
7
|
+
// =====================================================================
|
|
8
|
+
|
|
9
|
+
// build_idle_nodes:一个 worker 有 rollout_path(可读)→ classifier.classify 被调一次;
|
|
10
|
+
// 经注入分类器(零 provider client)。stopped/paused 跳过。
|
|
11
|
+
#[test]
|
|
12
|
+
fn build_idle_nodes_uses_injected_classifier_no_provider_client() {
|
|
13
|
+
// 真实 session 文件,使 _read_session_tail 读到非空 → classifier 返回注入 state。
|
|
14
|
+
let dir = std::env::temp_dir().join(format!("ta_rs_idle_{}", std::process::id()));
|
|
15
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
16
|
+
let log = dir.join("w1.jsonl");
|
|
17
|
+
std::fs::write(&log, b"{\"type\":\"turn_complete\"}\n").unwrap();
|
|
18
|
+
let state = serde_json::json!({
|
|
19
|
+
"agents": {
|
|
20
|
+
"w1": {"provider": "codex", "rollout_path": log.to_string_lossy(), "status": "running"},
|
|
21
|
+
"w_stopped": {"provider": "codex", "rollout_path": log.to_string_lossy(), "status": "stopped"},
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
let clf = CountingClassifier::new(TurnState::Idle);
|
|
25
|
+
let nodes = build_idle_nodes(&state, &clf).unwrap();
|
|
26
|
+
// stopped 被跳过(__init__ wiring:29)→ 仅 w1。
|
|
27
|
+
assert_eq!(nodes.len(), 1);
|
|
28
|
+
assert_eq!(nodes[0].node_id, "w1");
|
|
29
|
+
assert_eq!(nodes[0].role, NodeRole::Worker);
|
|
30
|
+
assert_eq!(nodes[0].state, TurnState::Idle);
|
|
31
|
+
// MUST-NOT-13:分类只经注入 classifier(此 mock 计数==1),零 provider client 直连。
|
|
32
|
+
assert_eq!(clf.calls.get(), 1, "每个 live node 恰调一次注入 classify");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// bug-085:rollout_path=None → 读到空串 → Unknown(不当 idle)。
|
|
36
|
+
#[test]
|
|
37
|
+
fn build_idle_nodes_none_rollout_path_yields_unknown_not_idle() {
|
|
38
|
+
let state = serde_json::json!({
|
|
39
|
+
"agents": {
|
|
40
|
+
"w1": {"provider": "codex", "status": "running"} // 无 rollout_path。
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
// 即使 mock 默认想返 Idle,空 session-log → classifier 返 Unknown(见 mock 逻辑)。
|
|
44
|
+
let clf = CountingClassifier::new(TurnState::Idle);
|
|
45
|
+
let nodes = build_idle_nodes(&state, &clf).unwrap();
|
|
46
|
+
assert_eq!(nodes.len(), 1);
|
|
47
|
+
assert_eq!(nodes[0].state, TurnState::Unknown, "None rollout_path → Unknown,绝不 idle");
|
|
48
|
+
assert!(
|
|
49
|
+
!nodes[0].state.is_idle_for_takeover(),
|
|
50
|
+
"unknown ≠ idle:不得对 takeover 放行"
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// _leader_node:leader path 或 provider 缺 → None(省略而非猜 idle)。
|
|
55
|
+
#[test]
|
|
56
|
+
fn leader_node_omitted_when_path_or_provider_missing() {
|
|
57
|
+
let clf = CountingClassifier::new(TurnState::Idle);
|
|
58
|
+
// 既无 leader.rollout_path 也无 receiver.rollout_path → None。
|
|
59
|
+
let state_no_path = serde_json::json!({
|
|
60
|
+
"leader": {"id": "leader", "provider": "codex"},
|
|
61
|
+
"leader_receiver": {"provider": "codex"}
|
|
62
|
+
});
|
|
63
|
+
assert!(leader_node(&state_no_path, &clf).unwrap().is_none(), "缺 path → 省略 leader 节点");
|
|
64
|
+
// 有 path 但无 provider → None。
|
|
65
|
+
let dir = std::env::temp_dir().join(format!("ta_rs_lnode_{}", std::process::id()));
|
|
66
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
67
|
+
let log = dir.join("leader.jsonl");
|
|
68
|
+
std::fs::write(&log, b"{\"type\":\"turn_open\"}\n").unwrap();
|
|
69
|
+
let state_no_provider = serde_json::json!({
|
|
70
|
+
"leader": {"id": "leader", "rollout_path": log.to_string_lossy()}
|
|
71
|
+
});
|
|
72
|
+
assert!(
|
|
73
|
+
leader_node(&state_no_provider, &clf).unwrap().is_none(),
|
|
74
|
+
"缺 provider → 省略 leader 节点(不猜 idle)"
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// _leader_node:path+provider 齐 → 经 classifier 产 role=leader 节点(C13)。
|
|
79
|
+
#[test]
|
|
80
|
+
fn leader_node_classified_via_injected_classifier() {
|
|
81
|
+
let dir = std::env::temp_dir().join(format!("ta_rs_lnode2_{}", std::process::id()));
|
|
82
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
83
|
+
let log = dir.join("leader.jsonl");
|
|
84
|
+
std::fs::write(&log, b"{\"type\":\"turn_complete\"}\n").unwrap();
|
|
85
|
+
let state = serde_json::json!({
|
|
86
|
+
"leader": {"id": "leader", "provider": "codex", "rollout_path": log.to_string_lossy()}
|
|
87
|
+
});
|
|
88
|
+
let clf = CountingClassifier::new(TurnState::Working);
|
|
89
|
+
let node = leader_node(&state, &clf).unwrap().expect("path+provider 齐 → 有 leader 节点");
|
|
90
|
+
assert_eq!(node.role, NodeRole::Leader);
|
|
91
|
+
assert_eq!(node.node_id, "leader");
|
|
92
|
+
assert_eq!(node.state, TurnState::Working);
|
|
93
|
+
assert_eq!(clf.calls.get(), 1, "leader 分类经注入 classifier 一次,零 provider client");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// =====================================================================
|
|
97
|
+
// 9. classify_provider_turn_state 门面(unimplemented → RED)
|
|
98
|
+
// unknown/abnormal 且有 event_sink → 写 idle_takeover.classify。
|
|
99
|
+
// =====================================================================
|
|
100
|
+
|
|
101
|
+
// 门面经注入 classifier 分类;空文本 → Unknown(本 mock),验证返回 TurnClassification。
|
|
102
|
+
#[test]
|
|
103
|
+
fn classify_provider_turn_state_returns_classification_via_injected_classifier() {
|
|
104
|
+
let clf = CountingClassifier::new(TurnState::Idle);
|
|
105
|
+
let c = classify_provider_turn_state(Provider::Codex, "{\"type\":\"turn_complete\"}", &clf, None).unwrap();
|
|
106
|
+
assert_eq!(c.state, TurnState::Idle);
|
|
107
|
+
assert_eq!(clf.calls.get(), 1);
|
|
108
|
+
// 空文本 → Unknown(unknown ≠ idle 命门下游)。
|
|
109
|
+
let clf2 = CountingClassifier::new(TurnState::Idle);
|
|
110
|
+
let c2 = classify_provider_turn_state(Provider::Codex, "", &clf2, None).unwrap();
|
|
111
|
+
assert_eq!(c2.state, TurnState::Unknown);
|
|
112
|
+
assert!(!c2.state.is_idle_for_takeover());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// event_sink + unknown/abnormal → 写 idle_takeover.classify(事件名字节锁)。
|
|
116
|
+
#[test]
|
|
117
|
+
fn classify_event_name_is_idle_takeover_classify() {
|
|
118
|
+
assert_eq!(LeaderEvent::IdleTakeoverClassify.name(), "idle_takeover.classify");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// =====================================================================
|
|
122
|
+
// 10. push_idle_reminder(unimplemented → RED):!should_ping → no-op。
|
|
123
|
+
// =====================================================================
|
|
124
|
+
|
|
125
|
+
#[test]
|
|
126
|
+
fn push_idle_reminder_noop_when_should_not_ping() {
|
|
127
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_push_{}", std::process::id()));
|
|
128
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
129
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
130
|
+
let state = serde_json::json!({"leader": {"id": "leader"}});
|
|
131
|
+
let result = TakeoverReminderResult {
|
|
132
|
+
should_ping: false,
|
|
133
|
+
message: None,
|
|
134
|
+
interrupted_nodes: vec![],
|
|
135
|
+
reason: Some("not_armed_no_worker_turn".into()),
|
|
136
|
+
};
|
|
137
|
+
// should_ping=false → no-op,返回 Ok(())。现 unimplemented → RED。
|
|
138
|
+
push_idle_reminder(&ws, &state, &event_log, &result).unwrap();
|
|
139
|
+
// 强化:no-op 必须真的什么都不做 —— 不写 idle_takeover.reminder 事件(EventLog 无该事件)。
|
|
140
|
+
let events = event_log.tail(50).unwrap();
|
|
141
|
+
assert!(
|
|
142
|
+
!events.iter().any(|e| e["event"] == serde_json::json!("idle_takeover.reminder")),
|
|
143
|
+
"should_ping=false 时绝不写 reminder 事件"
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// #236 nag_removal (N35) — push_idle_reminder is now a no-op shim.
|
|
148
|
+
// [OLD] assertion: should_ping=true → writes idle_takeover.reminder event (with
|
|
149
|
+
// interrupted/reason byte-locked golden payload).
|
|
150
|
+
// [NEW] assertion: even when should_ping=true, push_idle_reminder writes NO event
|
|
151
|
+
// and emits NO leader-bound message; ownership/handover happens only via explicit
|
|
152
|
+
// `claim-leader` / `takeover` commands. The function signature is preserved so
|
|
153
|
+
// existing callers (coordinator/tick.rs, lifecycle wiring) still resolve.
|
|
154
|
+
#[test]
|
|
155
|
+
fn push_idle_reminder_is_silent_no_op_under_n35_even_when_should_ping_true() {
|
|
156
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_push2_{}", std::process::id()));
|
|
157
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
158
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
159
|
+
let state = serde_json::json!({"leader": {"id": "leader"}});
|
|
160
|
+
let result = TakeoverReminderResult {
|
|
161
|
+
should_ping: true,
|
|
162
|
+
message: Some("neutral reminder body".into()),
|
|
163
|
+
interrupted_nodes: vec!["w1".into()],
|
|
164
|
+
reason: Some("armed_all_idle".into()),
|
|
165
|
+
};
|
|
166
|
+
push_idle_reminder(&ws, &state, &event_log, &result).unwrap();
|
|
167
|
+
let events = event_log.tail(50).unwrap();
|
|
168
|
+
assert!(
|
|
169
|
+
!events.iter().any(|e| e["event"] == serde_json::json!("idle_takeover.reminder")),
|
|
170
|
+
"#236 N35: push_idle_reminder must no longer emit the reminder nag event; got {events:?}"
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// idle_takeover.reminder / push_failed 事件名字节锁。
|
|
175
|
+
#[test]
|
|
176
|
+
fn idle_takeover_event_names_byte_locked() {
|
|
177
|
+
assert_eq!(LeaderEvent::IdleTakeoverReminder.name(), "idle_takeover.reminder");
|
|
178
|
+
assert_eq!(LeaderEvent::IdleTakeoverPushFailed.name(), "idle_takeover.push_failed");
|
|
179
|
+
assert_eq!(LeaderEvent::IdleTakeoverPing.name(), "idle_takeover.ping");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// =====================================================================
|
|
183
|
+
// 11. struct 构造 / 序列化形态 + key 插入序证据(纯数据,不依赖 body)
|
|
184
|
+
// =====================================================================
|
|
185
|
+
|
|
186
|
+
// LeaderReceiver:所有可选字段 Option(bug-085 半状态合法);序列化保字段名。
|
|
187
|
+
#[test]
|
|
188
|
+
fn leader_receiver_struct_serializes_with_python_field_names() {
|
|
189
|
+
let recv = LeaderReceiver {
|
|
190
|
+
mode: ReceiverMode::DirectTmux,
|
|
191
|
+
status: ReceiverStatus::Attached,
|
|
192
|
+
provider: Provider::ClaudeCode,
|
|
193
|
+
pane_id: PaneId::new("%648"),
|
|
194
|
+
session_name: Some(SessionName::new("S")),
|
|
195
|
+
window_index: Some("1".into()),
|
|
196
|
+
window_name: Some(WindowName::new("W")),
|
|
197
|
+
pane_index: Some("2".into()),
|
|
198
|
+
pane_tty: Some("/dev/ttys001".into()),
|
|
199
|
+
pane_current_command: Some("claude".into()),
|
|
200
|
+
fingerprint: Some("fp".into()),
|
|
201
|
+
leader_session_uuid: Some(uuid("fp", "/ws", "u", "default")),
|
|
202
|
+
owner_epoch: Some(OwnerEpoch(3)),
|
|
203
|
+
attached_at: Some("2026-06-02T00:00:00+00:00".into()),
|
|
204
|
+
discovery: Some(Discovery::ClaimLeader),
|
|
205
|
+
requested_provider: None,
|
|
206
|
+
warning: None,
|
|
207
|
+
};
|
|
208
|
+
let v = serde_json::to_value(&recv).unwrap();
|
|
209
|
+
assert_eq!(v["mode"], serde_json::json!("direct_tmux"));
|
|
210
|
+
assert_eq!(v["status"], serde_json::json!("attached"));
|
|
211
|
+
assert_eq!(v["provider"], serde_json::json!("claude_code"));
|
|
212
|
+
assert_eq!(v["pane_id"], serde_json::json!("%648"));
|
|
213
|
+
assert_eq!(v["owner_epoch"], serde_json::json!(3));
|
|
214
|
+
assert_eq!(v["discovery"], serde_json::json!("claim_leader"));
|
|
215
|
+
// bug-085:None 字段序列化为 null(半状态合法,不崩)。
|
|
216
|
+
assert_eq!(v["requested_provider"], serde_json::Value::Null);
|
|
217
|
+
assert_eq!(v["warning"], serde_json::Value::Null);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// TeamOwner:claimed_via kebab + owner_epoch int;os_user Option(Family A 才写)。
|
|
221
|
+
#[test]
|
|
222
|
+
fn team_owner_struct_serializes_with_python_shape() {
|
|
223
|
+
let owner = TeamOwner {
|
|
224
|
+
pane_id: PaneId::new("%9"),
|
|
225
|
+
provider: Provider::Codex,
|
|
226
|
+
machine_fingerprint: "fp".into(),
|
|
227
|
+
leader_session_uuid: Some(uuid("fp", "/ws", "u", "default")),
|
|
228
|
+
owner_epoch: OwnerEpoch(1),
|
|
229
|
+
claimed_at: "2026-06-02T00:00:00+00:00".into(),
|
|
230
|
+
claimed_via: ClaimedVia::ClaimLeader,
|
|
231
|
+
os_user: Some("alice".into()),
|
|
232
|
+
};
|
|
233
|
+
let v = serde_json::to_value(&owner).unwrap();
|
|
234
|
+
assert_eq!(v["claimed_via"], serde_json::json!("claim-leader"));
|
|
235
|
+
assert_eq!(v["owner_epoch"], serde_json::json!(1));
|
|
236
|
+
assert_eq!(v["provider"], serde_json::json!("codex"));
|
|
237
|
+
assert_eq!(v["os_user"], serde_json::json!("alice"));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// LeaderIdentity:source 用 leader-plan 枚举值(Override→"override");team_id 透明串。
|
|
241
|
+
#[test]
|
|
242
|
+
fn leader_identity_struct_serializes_with_leader_plan_source() {
|
|
243
|
+
let id = LeaderIdentity {
|
|
244
|
+
leader_session_uuid: uuid("fp", "/ws", "u", "default"),
|
|
245
|
+
leader_session_uuid_source: LeaderSessionUuidSource::Override,
|
|
246
|
+
machine_fingerprint: "fp".into(),
|
|
247
|
+
workspace_abspath: std::path::PathBuf::from("/ws"),
|
|
248
|
+
os_user: "u".into(),
|
|
249
|
+
team_id: TeamKey::new("default"),
|
|
250
|
+
};
|
|
251
|
+
let v = serde_json::to_value(&id).unwrap();
|
|
252
|
+
assert_eq!(v["leader_session_uuid_source"], serde_json::json!("override"));
|
|
253
|
+
assert_eq!(v["team_id"], serde_json::json!("default"));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// IdleNode:bug-085 rollout_path Option;state 是 TurnState(穷尽,Unknown 非 idle)。
|
|
257
|
+
#[test]
|
|
258
|
+
fn idle_node_unknown_state_is_not_idle() {
|
|
259
|
+
let n = IdleNode {
|
|
260
|
+
node_id: "w1".into(),
|
|
261
|
+
role: NodeRole::Worker,
|
|
262
|
+
state: TurnState::Unknown,
|
|
263
|
+
turn_id: None,
|
|
264
|
+
annotations: vec![],
|
|
265
|
+
provider: Some(Provider::Codex),
|
|
266
|
+
auth_mode: None,
|
|
267
|
+
rollout_path: None, // bug-085:None 合法 → 该 node Unknown。
|
|
268
|
+
};
|
|
269
|
+
assert!(!n.state.is_idle_for_takeover(), "Unknown 不当 idle");
|
|
270
|
+
assert!(n.rollout_path.is_none());
|
|
271
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
// =====================================================================
|
|
4
|
+
// 7. 五条 lease 路径签名 + 返回 LeaseResult 形态(unimplemented → RED)
|
|
5
|
+
// =====================================================================
|
|
6
|
+
|
|
7
|
+
// attach_leader:手动 CLI attach(__init__.py:19-58 → attach_leader_to_state:276 →
|
|
8
|
+
// _resolve_leader_pane)。在无 live tmux 的测试环境,指定一个不存在的 pane %1 →
|
|
9
|
+
// _resolve_leader_pane raise RuntimeError("tmux pane not found: %1")(_legacy_pane_discovery.py:153),
|
|
10
|
+
// 映射到 LeaderError::Validation。golden(probe_attach.py 已验:真跑即 raise)。
|
|
11
|
+
// 强化:钉具体的 Err 形态 + 错误串含 pane id;并断言失败时绝不留下半绑定 state(无 team_owner)。
|
|
12
|
+
// unimplemented → RED(unimplemented panic ≠ 期望的 Validation,且后续 is_err 断言不会被求值)。
|
|
13
|
+
#[test]
|
|
14
|
+
fn attach_leader_errors_when_pane_not_resolvable() {
|
|
15
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_attach_{}", std::process::id()));
|
|
16
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
17
|
+
let r = attach_leader(&ws, Some(&PaneId::new("%1")), Provider::Codex);
|
|
18
|
+
// 不可解析 pane → Err(Validation),错误串提及 pane not found。
|
|
19
|
+
match r {
|
|
20
|
+
Err(LeaderError::Validation(msg)) => {
|
|
21
|
+
assert!(msg.contains("%1"), "Validation 错误须含目标 pane id,got {msg}");
|
|
22
|
+
assert!(msg.contains("not found"), "须是 pane-not-found 形态,got {msg}");
|
|
23
|
+
}
|
|
24
|
+
Err(other) => panic!("期望 Validation(pane not found),got {other:?}"),
|
|
25
|
+
Ok(v) => panic!("无 live tmux pane 时不该成功 attach,got {v:?}"),
|
|
26
|
+
}
|
|
27
|
+
// 失败不留半绑定:state.json 无 team_owner。
|
|
28
|
+
let st = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
29
|
+
assert!(st.get("team_owner").is_none(), "resolve 失败不得落 team_owner");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// attach_leader 成功 post-state(需 live tmux pane + cross-lane _resolve_leader_pane):
|
|
33
|
+
// vacant acquire → status=Claimed、reason=vacant_acquired、owner_epoch 0→1、owner/receiver 绑同 pane、
|
|
34
|
+
// 且 workspace state.json 真被持久化。golden(_claim_lease_no_incident:81/102/139-143)。
|
|
35
|
+
// real-machine-gated(无 live tmux 无法驱动 pane resolver)。
|
|
36
|
+
#[test]
|
|
37
|
+
#[ignore = "needs a live tmux pane + cross-lane _resolve_leader_pane (step 9/11)"]
|
|
38
|
+
fn attach_leader_binds_pane_advances_epoch_and_persists() {
|
|
39
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_attach_ok_{}", std::process::id()));
|
|
40
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
41
|
+
let pane = PaneId::new("%1");
|
|
42
|
+
let r = attach_leader(&ws, Some(&pane), Provider::Codex).unwrap();
|
|
43
|
+
assert!(r.ok);
|
|
44
|
+
assert_eq!(r.status, LeaseStatus::Claimed);
|
|
45
|
+
let owner = r.owner.as_ref().expect("attach 成功必带 owner");
|
|
46
|
+
assert_eq!(owner.pane_id, pane);
|
|
47
|
+
assert_eq!(owner.owner_epoch, OwnerEpoch(1), "vacant acquire 后 epoch=1");
|
|
48
|
+
assert_eq!(owner.provider, Provider::Codex);
|
|
49
|
+
let receiver = r.receiver.as_ref().expect("attach 成功必带 receiver");
|
|
50
|
+
assert_eq!(receiver.pane_id, pane);
|
|
51
|
+
assert_eq!(r.reason, Some(LeaseReason::VacantAcquired));
|
|
52
|
+
let persisted = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
53
|
+
assert_eq!(persisted["team_owner"]["pane_id"], serde_json::json!("%1"));
|
|
54
|
+
assert_eq!(persisted["team_owner"]["owner_epoch"], serde_json::json!(1));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// autobind:$TMUX_PANE 缺 → Ok(None)(__init__.py:885-887,锁前直接返回,不开锁)。
|
|
58
|
+
// 强化:断言这是 lock-not-acquired 早退 —— 不写 state.json(无 receiver/owner 落盘),
|
|
59
|
+
// 不发任何 leader_receiver.* 事件(锁前 return,连 EventLog 都不构造)。
|
|
60
|
+
#[test]
|
|
61
|
+
fn autobind_returns_none_when_tmux_pane_missing() {
|
|
62
|
+
if std::env::var_os("TMUX_PANE").is_some() {
|
|
63
|
+
return; // 在 tmux 内:走绑定路径,本用例只验缺失分支。
|
|
64
|
+
}
|
|
65
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_auto_{}", std::process::id()));
|
|
66
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
67
|
+
let r = autobind_leader_receiver_from_env(&ws, Provider::Codex, LeaseSource::Restart).unwrap();
|
|
68
|
+
assert!(r.is_none(), "$TMUX_PANE 缺 → autobind 返回 Ok(None)");
|
|
69
|
+
// lock-not-acquired 早退:state.json 未被写(load 兜底成空 state,无 team_owner/receiver)。
|
|
70
|
+
let st = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
71
|
+
assert!(st.get("leader_receiver").is_none(), "skip 路径不应落 receiver");
|
|
72
|
+
assert!(st.get("team_owner").is_none(), "skip 路径不应落 owner");
|
|
73
|
+
// 早退发生在 EventLog 构造前 → 无任何事件文件。
|
|
74
|
+
let events = crate::event_log::EventLog::new(&ws).tail(50).unwrap();
|
|
75
|
+
assert!(events.is_empty(), "skip 路径绝不写审计事件");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// autobind 成功支:$TMUX_PANE 命中 → 锁内 attach_leader_to_state → Ok(Some(receiver))。
|
|
79
|
+
// receiver.pane_id == 注入 pane,discovery==EnvPane($TMUX_PANE 直接命中)。
|
|
80
|
+
// env 变更进程全局且与并行 test race,且依赖跨 lane 的 live pane resolver,故 real-machine-gated。
|
|
81
|
+
#[test]
|
|
82
|
+
#[ignore = "needs live $TMUX_PANE + cross-lane _resolve_leader_pane; env mutation races parallel tests"]
|
|
83
|
+
fn autobind_binds_env_pane_on_success() {
|
|
84
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_auto_ok_{}", std::process::id()));
|
|
85
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
86
|
+
// SAFETY: #[ignore]d,单独 `--ignored --test-threads=1` 跑,不与并行 test race。
|
|
87
|
+
unsafe { std::env::set_var("TMUX_PANE", "%42") };
|
|
88
|
+
let r = autobind_leader_receiver_from_env(&ws, Provider::Codex, LeaseSource::Restart).unwrap();
|
|
89
|
+
unsafe { std::env::remove_var("TMUX_PANE") };
|
|
90
|
+
let receiver = r.expect("$TMUX_PANE 命中 → Ok(Some(receiver))");
|
|
91
|
+
assert_eq!(receiver.pane_id, PaneId::new("%42"));
|
|
92
|
+
assert_eq!(receiver.discovery, Some(Discovery::EnvPane), "$TMUX_PANE 命中 → discovery=env_pane");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// claim_leader:无 ambiguous incident → 走 claim_lease_no_incident 直接 acquire/CAS。
|
|
96
|
+
// 现 unimplemented → RED;锁住返回 LeaseResult。
|
|
97
|
+
#[test]
|
|
98
|
+
fn claim_leader_returns_lease_result() {
|
|
99
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_claim_{}", std::process::id()));
|
|
100
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
101
|
+
let r = claim_leader(&ws, None, false).unwrap();
|
|
102
|
+
// 无 caller pane(测试进程无 TMUX_PANE)→ refused not_in_tmux_pane(__init__.py:616-618)。
|
|
103
|
+
if std::env::var_os("TMUX_PANE").is_none() {
|
|
104
|
+
assert!(!r.ok);
|
|
105
|
+
assert_eq!(r.status, LeaseStatus::Refused);
|
|
106
|
+
assert_eq!(r.reason, Some(LeaseReason::NotInTmuxPane));
|
|
107
|
+
assert!(r.action.is_some());
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// write_lease_dual_state:同一锁内双写(C17,__init__.py:588-596);unimplemented → RED。
|
|
112
|
+
// 强化:带 session_name 时必须落 BOTH —— workspace state.json + team/<session> snapshot,
|
|
113
|
+
// 两份 team_owner.pane_id / owner_epoch 必须一致(永不分叉)。空 body Ok(()) 会被这里抓。
|
|
114
|
+
#[test]
|
|
115
|
+
fn write_lease_dual_state_persists_both_locations_without_divergence() {
|
|
116
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_dual_{}", std::process::id()));
|
|
117
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
118
|
+
let state = serde_json::json!({
|
|
119
|
+
"session_name": "team-sess",
|
|
120
|
+
"team_owner": {"pane_id": "%1", "owner_epoch": 2, "leader_session_uuid": "uuuu"},
|
|
121
|
+
"leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
|
|
122
|
+
});
|
|
123
|
+
write_lease_dual_state(&ws, &state).unwrap();
|
|
124
|
+
// (1) workspace state.json(<ws>/.team/runtime/state.json)被写,team_owner.pane_id==%1。
|
|
125
|
+
let ws_path = crate::state::persist::runtime_state_path(&ws);
|
|
126
|
+
let ws_state: serde_json::Value =
|
|
127
|
+
serde_json::from_str(&std::fs::read_to_string(&ws_path).expect("workspace state.json 必须存在")).unwrap();
|
|
128
|
+
assert_eq!(ws_state["team_owner"]["pane_id"], serde_json::json!("%1"));
|
|
129
|
+
assert_eq!(ws_state["team_owner"]["owner_epoch"], serde_json::json!(2));
|
|
130
|
+
// (2) team-level snapshot(<ws>/.team/runtime/teams/<session>/state.json)被写。
|
|
131
|
+
let snap_path = crate::model::paths::runtime_dir(&ws)
|
|
132
|
+
.join("teams")
|
|
133
|
+
.join("team-sess")
|
|
134
|
+
.join("state.json");
|
|
135
|
+
let snap_state: serde_json::Value =
|
|
136
|
+
serde_json::from_str(&std::fs::read_to_string(&snap_path).expect("team snapshot state.json 必须存在")).unwrap();
|
|
137
|
+
// (3) 两份永不分叉:owner pane_id / owner_epoch 必须相等(C17 核心不变量)。
|
|
138
|
+
assert_eq!(
|
|
139
|
+
ws_state["team_owner"]["pane_id"], snap_state["team_owner"]["pane_id"],
|
|
140
|
+
"workspace 与 team snapshot 的 owner pane 不得分叉"
|
|
141
|
+
);
|
|
142
|
+
assert_eq!(
|
|
143
|
+
ws_state["team_owner"]["owner_epoch"], snap_state["team_owner"]["owner_epoch"],
|
|
144
|
+
"workspace 与 team snapshot 的 owner_epoch 不得分叉"
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// detect_dual_state_divergence:无 session_name → None(__init__.py:560-561);unimplemented → RED。
|
|
149
|
+
#[test]
|
|
150
|
+
fn detect_dual_state_divergence_none_without_session_name() {
|
|
151
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_div_{}", std::process::id()));
|
|
152
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
153
|
+
let state = serde_json::json!({"team_owner": {"pane_id": "%1"}}); // 无 session_name。
|
|
154
|
+
let d = detect_dual_state_divergence(&ws, &state).unwrap();
|
|
155
|
+
assert!(d.is_none(), "无 session_name → 无 snapshot 可比 → None");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// C18 核心:workspace state.json 与 team snapshot 在 owner pane 上分叉 → Some(具体分叉字段)。
|
|
159
|
+
// golden(probe_leader_strengthen.py):workspace owner=%1 / snapshot owner=%9 →
|
|
160
|
+
// {workspace_owner_pane:%1, team_owner_pane:%9, workspace_receiver_pane:%1, team_receiver_pane:%9}。
|
|
161
|
+
// unimplemented → RED。
|
|
162
|
+
#[test]
|
|
163
|
+
fn detect_dual_state_divergence_reports_diverging_panes() {
|
|
164
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_divx_{}", std::process::id()));
|
|
165
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
166
|
+
let state = serde_json::json!({
|
|
167
|
+
"session_name": "team-sess",
|
|
168
|
+
"team_owner": {"pane_id": "%1", "leader_session_uuid": "uuuu", "owner_epoch": 2},
|
|
169
|
+
"leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
|
|
170
|
+
});
|
|
171
|
+
// 写一份分叉 snapshot:owner/receiver pane 都是 %9(不同于 workspace 的 %1)。
|
|
172
|
+
let snap_dir = crate::model::paths::runtime_dir(&ws).join("teams").join("team-sess");
|
|
173
|
+
std::fs::create_dir_all(&snap_dir).unwrap();
|
|
174
|
+
let snap = serde_json::json!({
|
|
175
|
+
"session_name": "team-sess",
|
|
176
|
+
"team_owner": {"pane_id": "%9", "leader_session_uuid": "uuuu", "owner_epoch": 2},
|
|
177
|
+
"leader_receiver": {"pane_id": "%9", "owner_epoch": 2},
|
|
178
|
+
});
|
|
179
|
+
std::fs::write(snap_dir.join("state.json"), serde_json::to_string(&snap).unwrap()).unwrap();
|
|
180
|
+
let d = detect_dual_state_divergence(&ws, &state).unwrap().expect("分叉 → Some(details)");
|
|
181
|
+
assert_eq!(d["workspace_owner_pane"], serde_json::json!("%1"));
|
|
182
|
+
assert_eq!(d["team_owner_pane"], serde_json::json!("%9"));
|
|
183
|
+
assert_eq!(d["workspace_receiver_pane"], serde_json::json!("%1"));
|
|
184
|
+
assert_eq!(d["team_receiver_pane"], serde_json::json!("%9"));
|
|
185
|
+
|
|
186
|
+
// 匹配 snapshot(与 workspace 一致)→ None(无分叉)。
|
|
187
|
+
std::fs::write(snap_dir.join("state.json"), serde_json::to_string(&state).unwrap()).unwrap();
|
|
188
|
+
assert!(
|
|
189
|
+
detect_dual_state_divergence(&ws, &state).unwrap().is_none(),
|
|
190
|
+
"两份一致 → 无分叉 → None"
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// R8 D4 (c-lite offline byte-lock): the leader_receiver.requeued_exhausted_watchers payload-build,
|
|
195
|
+
// extracted from the real-tmux attach flow into a pure helper, must produce golden shape.
|
|
196
|
+
// golden leader/__init__.py:39-44: EXACTLY {watcher_ids, count, trigger:"attach_leader"}.
|
|
197
|
+
#[test]
|
|
198
|
+
fn r8_requeued_exhausted_watchers_event_payload_golden_shape() {
|
|
199
|
+
let notices = vec![crate::messaging::WatcherNotice {
|
|
200
|
+
watcher_id: "w1".to_string(),
|
|
201
|
+
result_id: Some("r1".to_string()),
|
|
202
|
+
ok: true,
|
|
203
|
+
status: Some("notify_failed".to_string()),
|
|
204
|
+
notified_message_id: None,
|
|
205
|
+
primary_watcher_id: None,
|
|
206
|
+
prior_state: Some("delivery_exhausted".to_string()),
|
|
207
|
+
error: None,
|
|
208
|
+
}];
|
|
209
|
+
let payload = crate::leader::lease::requeued_exhausted_watchers_event_payload(
|
|
210
|
+
&crate::transport::PaneId::new("%leader"),
|
|
211
|
+
&crate::model::ids::TeamKey::new("team-a"),
|
|
212
|
+
¬ices,
|
|
213
|
+
);
|
|
214
|
+
let keys: std::collections::BTreeSet<&str> =
|
|
215
|
+
payload.as_object().unwrap().keys().map(String::as_str).collect();
|
|
216
|
+
let expected: std::collections::BTreeSet<&str> = ["watcher_ids", "count", "trigger"].into_iter().collect();
|
|
217
|
+
assert_eq!(keys, expected,
|
|
218
|
+
"D4: leader_receiver.requeued_exhausted_watchers payload must be golden {{watcher_ids, count, trigger}} \
|
|
219
|
+
(leader/__init__.py:39-44), not the Rust {{pane_id, team_id, watcher_ids, requeued}}; got {keys:?}");
|
|
220
|
+
assert_eq!(
|
|
221
|
+
payload.get("watcher_ids").and_then(|v| v.as_array()).map(|a| a.iter().filter_map(|x| x.as_str()).collect::<Vec<_>>()),
|
|
222
|
+
Some(vec!["w1"]), "watcher_ids must be the string list of requeued ids");
|
|
223
|
+
assert_eq!(payload.get("count").and_then(|v| v.as_u64()), Some(1), "count == number of requeued watchers");
|
|
224
|
+
assert_eq!(payload.get("trigger").and_then(|v| v.as_str()), Some("attach_leader"));
|
|
225
|
+
}
|