@team-agent/installer 0.5.42 → 0.5.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +129 -54
- package/crates/team-agent/src/cli/diagnose.rs +1 -2
- package/crates/team-agent/src/cli/emit.rs +1 -7
- package/crates/team-agent/src/cli/helpers.rs +3 -1
- package/crates/team-agent/src/cli/leader.rs +2 -1
- package/crates/team-agent/src/cli/mod.rs +126 -16
- package/crates/team-agent/src/cli/named_address.rs +14 -5
- package/crates/team-agent/src/cli/profile.rs +19 -7
- package/crates/team-agent/src/cli/send.rs +9 -3
- package/crates/team-agent/src/cli/status.rs +8 -30
- package/crates/team-agent/src/cli/status_port.rs +1341 -1272
- package/crates/team-agent/src/cli/tests/base.rs +738 -660
- package/crates/team-agent/src/cli/tests/compile.rs +45 -18
- package/crates/team-agent/src/cli/tests/divergence.rs +462 -444
- package/crates/team-agent/src/cli/tests/lane_c.rs +365 -282
- package/crates/team-agent/src/cli/tests/leader_watch.rs +356 -329
- package/crates/team-agent/src/cli/tests/main_preserved.rs +672 -564
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +284 -224
- package/crates/team-agent/src/cli/tests/mod.rs +17 -7
- package/crates/team-agent/src/cli/tests/named_address.rs +8 -2
- package/crates/team-agent/src/cli/tests/peer_allow.rs +10 -2
- package/crates/team-agent/src/cli/tests/run_delegation.rs +314 -273
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +26 -15
- package/crates/team-agent/src/cli/tests/status_send.rs +707 -629
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +20 -4
- package/crates/team-agent/src/cli/tests/verb_profile.rs +46 -17
- package/crates/team-agent/src/cli/tests/verb_validate.rs +15 -3
- package/crates/team-agent/src/codex_app_server.rs +2 -5
- package/crates/team-agent/src/compiler/tests.rs +139 -33
- package/crates/team-agent/src/compiler.rs +55 -22
- package/crates/team-agent/src/conpty/backend.rs +23 -33
- package/crates/team-agent/src/coordinator/backoff.rs +2 -7
- package/crates/team-agent/src/coordinator/conpty_shim.rs +55 -67
- package/crates/team-agent/src/coordinator/health.rs +46 -31
- package/crates/team-agent/src/coordinator/mod.rs +3 -3
- package/crates/team-agent/src/coordinator/orphan.rs +22 -10
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +51 -56
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +55 -19
- package/crates/team-agent/src/coordinator/tests/basics.rs +179 -41
- package/crates/team-agent/src/coordinator/tests/daemon.rs +53 -13
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +78 -19
- package/crates/team-agent/src/coordinator/tests/main_preserved.rs +61 -11
- package/crates/team-agent/src/coordinator/tests/mod.rs +33 -39
- package/crates/team-agent/src/coordinator/tests/spine.rs +52 -12
- package/crates/team-agent/src/coordinator/tests/takeover.rs +73 -15
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +50 -15
- package/crates/team-agent/src/coordinator/tests/watch.rs +74 -20
- package/crates/team-agent/src/db/message_store.rs +138 -30
- package/crates/team-agent/src/db/migration.rs +249 -61
- package/crates/team-agent/src/db/schema.rs +303 -82
- package/crates/team-agent/src/diagnose/comms.rs +9 -2
- package/crates/team-agent/src/diagnose/mod.rs +1 -3
- package/crates/team-agent/src/diagnose/orphans.rs +79 -61
- package/crates/team-agent/src/event_log.rs +70 -16
- package/crates/team-agent/src/layout/manager.rs +15 -4
- package/crates/team-agent/src/layout/mod.rs +4 -4
- package/crates/team-agent/src/layout/overlay.rs +10 -3
- package/crates/team-agent/src/layout/placement.rs +5 -1
- package/crates/team-agent/src/layout/recovery.rs +4 -2
- package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
- package/crates/team-agent/src/layout/sessions.rs +17 -9
- package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
- package/crates/team-agent/src/layout/worker_env.rs +87 -19
- package/crates/team-agent/src/leader/helpers.rs +7 -1
- package/crates/team-agent/src/leader/lease.rs +199 -89
- package/crates/team-agent/src/leader/owner_bind.rs +55 -22
- package/crates/team-agent/src/leader/provider_attribution.rs +25 -6
- package/crates/team-agent/src/leader/rediscover/tests.rs +88 -24
- package/crates/team-agent/src/leader/rediscover.rs +74 -25
- package/crates/team-agent/src/leader/registry.rs +1 -1
- package/crates/team-agent/src/leader/start.rs +75 -54
- package/crates/team-agent/src/leader/takeover.rs +46 -11
- package/crates/team-agent/src/leader/tests/basics.rs +320 -167
- package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
- package/crates/team-agent/src/leader/tests/identity.rs +428 -356
- package/crates/team-agent/src/leader/tests/idle.rs +285 -254
- package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
- package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
- package/crates/team-agent/src/leader/tests/mod.rs +115 -99
- package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
- package/crates/team-agent/src/lib.rs +4 -4
- package/crates/team-agent/src/lifecycle/display.rs +7 -3
- package/crates/team-agent/src/lifecycle/launch.rs +55 -15
- package/crates/team-agent/src/lifecycle/mod.rs +9 -1
- package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
- package/crates/team-agent/src/lifecycle/restart/common.rs +6 -2
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
- package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
- package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests.rs +2 -2
- package/crates/team-agent/src/main.rs +4 -4
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
- package/crates/team-agent/src/mcp_server/mod.rs +11 -2
- package/crates/team-agent/src/mcp_server/types.rs +14 -3
- package/crates/team-agent/src/mcp_server/wire.rs +230 -68
- package/crates/team-agent/src/messaging/delivery.rs +20 -18
- package/crates/team-agent/src/messaging/helpers.rs +25 -4
- package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
- package/crates/team-agent/src/messaging/mod.rs +2 -3
- package/crates/team-agent/src/messaging/selftest.rs +46 -26
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
- package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
- package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
- package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
- package/crates/team-agent/src/messaging/trust.rs +25 -2
- package/crates/team-agent/src/messaging/watchers.rs +37 -9
- package/crates/team-agent/src/model/enums.rs +65 -16
- package/crates/team-agent/src/model/ids.rs +12 -3
- package/crates/team-agent/src/model/paths.rs +28 -7
- package/crates/team-agent/src/model/permissions.rs +176 -33
- package/crates/team-agent/src/model/routing.rs +66 -20
- package/crates/team-agent/src/model/spec.rs +365 -69
- package/crates/team-agent/src/model/task_graph.rs +36 -9
- package/crates/team-agent/src/model/yaml/tests.rs +24 -6
- package/crates/team-agent/src/model/yaml.rs +7 -6
- package/crates/team-agent/src/packaging/install.rs +23 -9
- package/crates/team-agent/src/packaging/migrate.rs +5 -7
- package/crates/team-agent/src/packaging/mod.rs +9 -1
- package/crates/team-agent/src/packaging/repair.rs +13 -6
- package/crates/team-agent/src/packaging/tests.rs +63 -16
- package/crates/team-agent/src/packaging/types.rs +22 -7
- package/crates/team-agent/src/platform/argv.rs +4 -1
- package/crates/team-agent/src/platform/file_lock.rs +22 -8
- package/crates/team-agent/src/platform/process.rs +54 -24
- package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
- package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
- package/crates/team-agent/src/provider/classify.rs +127 -42
- package/crates/team-agent/src/provider/faults.rs +17 -5
- package/crates/team-agent/src/provider/helpers.rs +6 -5
- package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
- package/crates/team-agent/src/state/persist.rs +28 -0
- package/crates/team-agent/src/state/repository.rs +8 -3
- package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
- package/crates/team-agent/src/tmux_backend.rs +80 -44
- package/crates/team-agent/src/topology.rs +40 -20
- package/crates/team-agent/src/transport/test_support.rs +20 -23
- package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
- package/crates/team-agent/src/transport/tests/mod.rs +178 -187
- package/crates/team-agent/src/transport/tests/wire.rs +561 -525
- package/crates/team-agent/src/transport.rs +12 -17
- package/crates/team-agent/src/transport_factory.rs +29 -14
- package/package.json +4 -4
|
@@ -1,292 +1,330 @@
|
|
|
1
1
|
use super::*;
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
// =====================================================================
|
|
4
|
+
// 7. 五条 lease 路径签名 + 返回 LeaseResult 形态(unimplemented → RED)
|
|
5
|
+
// =====================================================================
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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, None, Some(&PaneId::new("%1")), Provider::Codex);
|
|
18
|
+
// 不可解析 pane → Err(Validation),错误串提及 pane not found。
|
|
19
|
+
match r {
|
|
20
|
+
Err(LeaderError::Validation(msg)) => {
|
|
21
|
+
assert!(
|
|
22
|
+
msg.contains("%1"),
|
|
23
|
+
"Validation 错误须含目标 pane id,got {msg}"
|
|
24
|
+
);
|
|
25
|
+
assert!(
|
|
26
|
+
msg.contains("not found"),
|
|
27
|
+
"须是 pane-not-found 形态,got {msg}"
|
|
28
|
+
);
|
|
26
29
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
assert!(st.get("team_owner").is_none(), "resolve 失败不得落 team_owner");
|
|
30
|
+
Err(other) => panic!("期望 Validation(pane not found),got {other:?}"),
|
|
31
|
+
Ok(v) => panic!("无 live tmux pane 时不该成功 attach,got {v:?}"),
|
|
30
32
|
}
|
|
33
|
+
// 失败不留半绑定:state.json 无 team_owner。
|
|
34
|
+
let st = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
35
|
+
assert!(
|
|
36
|
+
st.get("team_owner").is_none(),
|
|
37
|
+
"resolve 失败不得落 team_owner"
|
|
38
|
+
);
|
|
39
|
+
}
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
41
|
+
// attach_leader 成功 post-state(需 live tmux pane + cross-lane _resolve_leader_pane):
|
|
42
|
+
// vacant acquire → status=Claimed、reason=vacant_acquired、owner_epoch 0→1、owner/receiver 绑同 pane、
|
|
43
|
+
// 且 workspace state.json 真被持久化。golden(_claim_lease_no_incident:81/102/139-143)。
|
|
44
|
+
// real-machine-gated(无 live tmux 无法驱动 pane resolver)。
|
|
45
|
+
#[test]
|
|
46
|
+
#[ignore = "needs a live tmux pane + cross-lane _resolve_leader_pane (step 9/11)"]
|
|
47
|
+
fn attach_leader_binds_pane_advances_epoch_and_persists() {
|
|
48
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_attach_ok_{}", std::process::id()));
|
|
49
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
50
|
+
let pane = PaneId::new("%1");
|
|
51
|
+
let r = attach_leader(&ws, None, Some(&pane), Provider::Codex).unwrap();
|
|
52
|
+
assert!(r.ok);
|
|
53
|
+
assert_eq!(r.status, LeaseStatus::Claimed);
|
|
54
|
+
let owner = r.owner.as_ref().expect("attach 成功必带 owner");
|
|
55
|
+
assert_eq!(owner.pane_id, pane);
|
|
56
|
+
assert_eq!(
|
|
57
|
+
owner.owner_epoch,
|
|
58
|
+
OwnerEpoch(1),
|
|
59
|
+
"vacant acquire 后 epoch=1"
|
|
60
|
+
);
|
|
61
|
+
assert_eq!(owner.provider, Provider::Codex);
|
|
62
|
+
let receiver = r.receiver.as_ref().expect("attach 成功必带 receiver");
|
|
63
|
+
assert_eq!(receiver.pane_id, pane);
|
|
64
|
+
assert_eq!(r.reason, Some(LeaseReason::VacantAcquired));
|
|
65
|
+
let persisted = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
66
|
+
assert_eq!(persisted["team_owner"]["pane_id"], serde_json::json!("%1"));
|
|
67
|
+
assert_eq!(persisted["team_owner"]["owner_epoch"], serde_json::json!(1));
|
|
68
|
+
}
|
|
56
69
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 路径绝不写审计事件");
|
|
70
|
+
// autobind:$TMUX_PANE 缺 → Ok(None)(__init__.py:885-887,锁前直接返回,不开锁)。
|
|
71
|
+
// 强化:断言这是 lock-not-acquired 早退 —— 不写 state.json(无 receiver/owner 落盘),
|
|
72
|
+
// 不发任何 leader_receiver.* 事件(锁前 return,连 EventLog 都不构造)。
|
|
73
|
+
#[test]
|
|
74
|
+
fn autobind_returns_none_when_tmux_pane_missing() {
|
|
75
|
+
if std::env::var_os("TMUX_PANE").is_some() {
|
|
76
|
+
return; // 在 tmux 内:走绑定路径,本用例只验缺失分支。
|
|
76
77
|
}
|
|
78
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_auto_{}", std::process::id()));
|
|
79
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
80
|
+
let r = autobind_leader_receiver_from_env(&ws, Provider::Codex, LeaseSource::Restart).unwrap();
|
|
81
|
+
assert!(r.is_none(), "$TMUX_PANE 缺 → autobind 返回 Ok(None)");
|
|
82
|
+
// lock-not-acquired 早退:state.json 未被写(load 兜底成空 state,无 team_owner/receiver)。
|
|
83
|
+
let st = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
84
|
+
assert!(
|
|
85
|
+
st.get("leader_receiver").is_none(),
|
|
86
|
+
"skip 路径不应落 receiver"
|
|
87
|
+
);
|
|
88
|
+
assert!(st.get("team_owner").is_none(), "skip 路径不应落 owner");
|
|
89
|
+
// 早退发生在 EventLog 构造前 → 无任何事件文件。
|
|
90
|
+
let events = crate::event_log::EventLog::new(&ws).tail(50).unwrap();
|
|
91
|
+
assert!(events.is_empty(), "skip 路径绝不写审计事件");
|
|
92
|
+
}
|
|
77
93
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
// autobind 成功支:$TMUX_PANE 命中 → 锁内 attach_leader_to_state → Ok(Some(receiver))。
|
|
95
|
+
// receiver.pane_id == 注入 pane,discovery==EnvPane($TMUX_PANE 直接命中)。
|
|
96
|
+
// env 变更进程全局且与并行 test race,且依赖跨 lane 的 live pane resolver,故 real-machine-gated。
|
|
97
|
+
#[test]
|
|
98
|
+
#[ignore = "needs live $TMUX_PANE + cross-lane _resolve_leader_pane; env mutation races parallel tests"]
|
|
99
|
+
fn autobind_binds_env_pane_on_success() {
|
|
100
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_auto_ok_{}", std::process::id()));
|
|
101
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
102
|
+
// SAFETY: #[ignore]d,单独 `--ignored --test-threads=1` 跑,不与并行 test race。
|
|
103
|
+
unsafe { std::env::set_var("TMUX_PANE", "%42") };
|
|
104
|
+
let r = autobind_leader_receiver_from_env(&ws, Provider::Codex, LeaseSource::Restart).unwrap();
|
|
105
|
+
unsafe { std::env::remove_var("TMUX_PANE") };
|
|
106
|
+
let receiver = r.expect("$TMUX_PANE 命中 → Ok(Some(receiver))");
|
|
107
|
+
assert_eq!(receiver.pane_id, PaneId::new("%42"));
|
|
108
|
+
assert_eq!(
|
|
109
|
+
receiver.discovery,
|
|
110
|
+
Some(Discovery::EnvPane),
|
|
111
|
+
"$TMUX_PANE 命中 → discovery=env_pane"
|
|
112
|
+
);
|
|
113
|
+
}
|
|
94
114
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
115
|
+
// claim_leader:无 ambiguous incident → 走 claim_lease_no_incident 直接 acquire/CAS。
|
|
116
|
+
// 现 unimplemented → RED;锁住返回 LeaseResult。
|
|
117
|
+
#[test]
|
|
118
|
+
fn claim_leader_returns_lease_result() {
|
|
119
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_claim_{}", std::process::id()));
|
|
120
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
121
|
+
let r = claim_leader(&ws, None, false).unwrap();
|
|
122
|
+
// 无 caller pane(测试进程无 TMUX_PANE)→ refused not_in_tmux_pane(__init__.py:616-618)。
|
|
123
|
+
if std::env::var_os("TMUX_PANE").is_none() {
|
|
124
|
+
assert!(!r.ok);
|
|
125
|
+
assert_eq!(r.status, LeaseStatus::Refused);
|
|
126
|
+
assert_eq!(r.reason, Some(LeaseReason::NotInTmuxPane));
|
|
127
|
+
assert!(r.action.is_some());
|
|
109
128
|
}
|
|
129
|
+
}
|
|
110
130
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
131
|
+
// write_lease_dual_state:同一锁内双写(C17,__init__.py:588-596);unimplemented → RED。
|
|
132
|
+
// 强化:带 session_name 时必须落 BOTH —— workspace state.json + team/<session> snapshot,
|
|
133
|
+
// 两份 team_owner.pane_id / owner_epoch 必须一致(永不分叉)。空 body Ok(()) 会被这里抓。
|
|
134
|
+
#[test]
|
|
135
|
+
// Foundation-0 F0-2 conversion of the C17 dual-write invariant test:
|
|
136
|
+
// legacy per-session snapshot dual-write has been retired
|
|
137
|
+
// (`.team/artifacts/foundation-0-slice-design.md` §§4-5). The lease
|
|
138
|
+
// helper now persists ONLY the canonical root state; a legacy
|
|
139
|
+
// snapshot is diagnostic-only and never re-appears as a side effect
|
|
140
|
+
// of `write_lease_dual_state`. This test enforces exactly that:
|
|
141
|
+
// canonical root gets the owner tuple, and the legacy per-session
|
|
142
|
+
// path stays absent unless a diagnostic path explicitly writes it.
|
|
143
|
+
fn write_lease_dual_state_persists_canonical_root_only_no_legacy_side_effect() {
|
|
144
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_dual_{}", std::process::id()));
|
|
145
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
146
|
+
let state = serde_json::json!({
|
|
147
|
+
"session_name": "team-sess",
|
|
148
|
+
"team_owner": {"pane_id": "%1", "owner_epoch": 2, "leader_session_uuid": "uuuu"},
|
|
149
|
+
"leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
|
|
150
|
+
});
|
|
151
|
+
write_lease_dual_state(&ws, &state).unwrap();
|
|
152
|
+
// Canonical root state.json IS written; F0-2 keeps this as the
|
|
153
|
+
// single product authority.
|
|
154
|
+
let ws_path = crate::state::persist::runtime_state_path(&ws);
|
|
155
|
+
let ws_state: serde_json::Value = serde_json::from_str(
|
|
156
|
+
&std::fs::read_to_string(&ws_path).expect("workspace state.json 必须存在"),
|
|
157
|
+
)
|
|
158
|
+
.unwrap();
|
|
159
|
+
assert_eq!(ws_state["team_owner"]["pane_id"], serde_json::json!("%1"));
|
|
160
|
+
assert_eq!(ws_state["team_owner"]["owner_epoch"], serde_json::json!(2));
|
|
161
|
+
// Legacy per-session snapshot MUST NOT be a side effect of the
|
|
162
|
+
// lease save path any more — this is the RED2 authority-write
|
|
163
|
+
// guard shape at the unit-test layer.
|
|
164
|
+
let legacy_snap = crate::lifecycle::helpers::team_snapshot_path(&ws, "team-sess");
|
|
165
|
+
assert!(
|
|
166
|
+
!legacy_snap.exists(),
|
|
167
|
+
"F0-2: write_lease_dual_state must not write the legacy per-session snapshot; found {}",
|
|
168
|
+
legacy_snap.display(),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
149
171
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
#[test]
|
|
173
|
+
fn lease_divergence_reads_restart_snapshot_for_special_session_names() {
|
|
174
|
+
// Foundation-0 F0-2 conversion: `write_lease_dual_state` no longer
|
|
175
|
+
// side-effects a legacy snapshot; the divergence detector is a
|
|
176
|
+
// diagnostic reader that fires only when a diagnostic writer
|
|
177
|
+
// (e.g. `lifecycle::save_team_runtime_snapshot`) has produced
|
|
178
|
+
// the sidecar. Sequence exercised here: canonical root save →
|
|
179
|
+
// diagnostic snapshot save with a different pane → divergence
|
|
180
|
+
// reader observes the drift.
|
|
181
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_dual_special_{}", std::process::id()));
|
|
182
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
183
|
+
let state = serde_json::json!({
|
|
184
|
+
"session_name": "team:proj-中文",
|
|
185
|
+
"team_owner": {"pane_id": "%1", "owner_epoch": 2, "leader_session_uuid": "uuuu"},
|
|
186
|
+
"leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
|
|
187
|
+
});
|
|
188
|
+
write_lease_dual_state(&ws, &state).unwrap();
|
|
189
|
+
let safe_path = crate::lifecycle::helpers::team_snapshot_path(&ws, "team:proj-中文");
|
|
190
|
+
let raw_path = crate::model::paths::runtime_dir(&ws)
|
|
191
|
+
.join("teams")
|
|
192
|
+
.join("team:proj-中文")
|
|
193
|
+
.join("state.json");
|
|
194
|
+
// F0-2: canonical save must not create either the restart-safe
|
|
195
|
+
// or the raw legacy snapshot as a side effect.
|
|
196
|
+
assert!(
|
|
175
197
|
!safe_path.exists(),
|
|
176
198
|
"F0-2: canonical write_lease_dual_state must not create the restart-safe legacy snapshot: {}",
|
|
177
199
|
safe_path.display()
|
|
178
200
|
);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
201
|
+
assert!(
|
|
202
|
+
!raw_path.exists(),
|
|
203
|
+
"F0-2: canonical write_lease_dual_state must not create the raw legacy snapshot: {}",
|
|
204
|
+
raw_path.display()
|
|
205
|
+
);
|
|
184
206
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
207
|
+
// Diagnostic write path with a divergent pane so we can exercise
|
|
208
|
+
// the divergence reader below.
|
|
209
|
+
let restart_state = serde_json::json!({
|
|
210
|
+
"session_name": "team:proj-中文",
|
|
211
|
+
"team_owner": {"pane_id": "%9", "owner_epoch": 2, "leader_session_uuid": "uuuu"},
|
|
212
|
+
"leader_receiver": {"pane_id": "%9", "owner_epoch": 2},
|
|
213
|
+
});
|
|
214
|
+
let restart_path = crate::lifecycle::save_team_runtime_snapshot(&ws, &restart_state).unwrap();
|
|
215
|
+
assert_eq!(restart_path, safe_path);
|
|
216
|
+
assert!(
|
|
217
|
+
restart_path.exists(),
|
|
218
|
+
"diagnostic snapshot must be written to a real file: {}",
|
|
219
|
+
restart_path.display()
|
|
220
|
+
);
|
|
199
221
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
222
|
+
// F0-2 RED1 diagnostic marker is present on the on-disk file.
|
|
223
|
+
let snap_state: serde_json::Value =
|
|
224
|
+
serde_json::from_str(&std::fs::read_to_string(&restart_path).unwrap()).unwrap();
|
|
225
|
+
assert_eq!(snap_state["_not_authoritative"], serde_json::json!(true));
|
|
204
226
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
227
|
+
let d = detect_dual_state_divergence(&ws, &state)
|
|
228
|
+
.unwrap()
|
|
229
|
+
.expect("divergence detector must read the diagnostic snapshot");
|
|
230
|
+
assert_eq!(d["workspace_owner_pane"], serde_json::json!("%1"));
|
|
231
|
+
assert_eq!(d["team_owner_pane"], serde_json::json!("%9"));
|
|
232
|
+
assert_eq!(d["workspace_receiver_pane"], serde_json::json!("%1"));
|
|
233
|
+
assert_eq!(d["team_receiver_pane"], serde_json::json!("%9"));
|
|
234
|
+
}
|
|
213
235
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
236
|
+
#[test]
|
|
237
|
+
fn detect_dual_state_divergence_reads_legacy_raw_snapshot_when_safe_absent() {
|
|
238
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_dual_legacy_{}", std::process::id()));
|
|
239
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
240
|
+
let state = serde_json::json!({
|
|
241
|
+
"session_name": "team:legacy-中文",
|
|
242
|
+
"team_owner": {"pane_id": "%1", "leader_session_uuid": "uuuu", "owner_epoch": 2},
|
|
243
|
+
"leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
|
|
244
|
+
});
|
|
245
|
+
let safe_path = crate::lifecycle::helpers::team_snapshot_path(&ws, "team:legacy-中文");
|
|
246
|
+
assert!(!safe_path.exists(), "test setup requires safe path absent");
|
|
247
|
+
let raw_dir = crate::model::paths::runtime_dir(&ws)
|
|
248
|
+
.join("teams")
|
|
249
|
+
.join("team:legacy-中文");
|
|
250
|
+
std::fs::create_dir_all(&raw_dir).unwrap();
|
|
251
|
+
let snap = serde_json::json!({
|
|
252
|
+
"session_name": "team:legacy-中文",
|
|
253
|
+
"team_owner": {"pane_id": "%8", "leader_session_uuid": "uuuu", "owner_epoch": 2},
|
|
254
|
+
"leader_receiver": {"pane_id": "%8", "owner_epoch": 2},
|
|
255
|
+
});
|
|
256
|
+
std::fs::write(
|
|
257
|
+
raw_dir.join("state.json"),
|
|
258
|
+
serde_json::to_string(&snap).unwrap(),
|
|
259
|
+
)
|
|
260
|
+
.unwrap();
|
|
235
261
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
262
|
+
let d = detect_dual_state_divergence(&ws, &state)
|
|
263
|
+
.unwrap()
|
|
264
|
+
.expect("legacy raw snapshot fallback must preserve old teams");
|
|
265
|
+
assert_eq!(d["workspace_owner_pane"], serde_json::json!("%1"));
|
|
266
|
+
assert_eq!(d["team_owner_pane"], serde_json::json!("%8"));
|
|
267
|
+
assert_eq!(d["workspace_receiver_pane"], serde_json::json!("%1"));
|
|
268
|
+
assert_eq!(d["team_receiver_pane"], serde_json::json!("%8"));
|
|
269
|
+
}
|
|
244
270
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
271
|
+
// detect_dual_state_divergence:无 session_name → None(__init__.py:560-561);unimplemented → RED。
|
|
272
|
+
#[test]
|
|
273
|
+
fn detect_dual_state_divergence_none_without_session_name() {
|
|
274
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_div_{}", std::process::id()));
|
|
275
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
276
|
+
let state = serde_json::json!({"team_owner": {"pane_id": "%1"}}); // 无 session_name。
|
|
277
|
+
let d = detect_dual_state_divergence(&ws, &state).unwrap();
|
|
278
|
+
assert!(d.is_none(), "无 session_name → 无 snapshot 可比 → None");
|
|
279
|
+
}
|
|
254
280
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
281
|
+
// C18 核心:workspace state.json 与 team snapshot 在 owner pane 上分叉 → Some(具体分叉字段)。
|
|
282
|
+
// golden(probe_leader_strengthen.py):workspace owner=%1 / snapshot owner=%9 →
|
|
283
|
+
// {workspace_owner_pane:%1, team_owner_pane:%9, workspace_receiver_pane:%1, team_receiver_pane:%9}。
|
|
284
|
+
// unimplemented → RED。
|
|
285
|
+
#[test]
|
|
286
|
+
fn detect_dual_state_divergence_reports_diverging_panes() {
|
|
287
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_divx_{}", std::process::id()));
|
|
288
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
289
|
+
let state = serde_json::json!({
|
|
290
|
+
"session_name": "team-sess",
|
|
291
|
+
"team_owner": {"pane_id": "%1", "leader_session_uuid": "uuuu", "owner_epoch": 2},
|
|
292
|
+
"leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
|
|
293
|
+
});
|
|
294
|
+
// 写一份分叉 snapshot:owner/receiver pane 都是 %9(不同于 workspace 的 %1)。
|
|
295
|
+
let snap_dir = crate::model::paths::runtime_dir(&ws)
|
|
296
|
+
.join("teams")
|
|
297
|
+
.join("team-sess");
|
|
298
|
+
std::fs::create_dir_all(&snap_dir).unwrap();
|
|
299
|
+
let snap = serde_json::json!({
|
|
300
|
+
"session_name": "team-sess",
|
|
301
|
+
"team_owner": {"pane_id": "%9", "leader_session_uuid": "uuuu", "owner_epoch": 2},
|
|
302
|
+
"leader_receiver": {"pane_id": "%9", "owner_epoch": 2},
|
|
303
|
+
});
|
|
304
|
+
std::fs::write(
|
|
305
|
+
snap_dir.join("state.json"),
|
|
306
|
+
serde_json::to_string(&snap).unwrap(),
|
|
307
|
+
)
|
|
308
|
+
.unwrap();
|
|
309
|
+
let d = detect_dual_state_divergence(&ws, &state)
|
|
310
|
+
.unwrap()
|
|
311
|
+
.expect("分叉 → Some(details)");
|
|
312
|
+
assert_eq!(d["workspace_owner_pane"], serde_json::json!("%1"));
|
|
313
|
+
assert_eq!(d["team_owner_pane"], serde_json::json!("%9"));
|
|
314
|
+
assert_eq!(d["workspace_receiver_pane"], serde_json::json!("%1"));
|
|
315
|
+
assert_eq!(d["team_receiver_pane"], serde_json::json!("%9"));
|
|
282
316
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
317
|
+
// 匹配 snapshot(与 workspace 一致)→ None(无分叉)。
|
|
318
|
+
std::fs::write(
|
|
319
|
+
snap_dir.join("state.json"),
|
|
320
|
+
serde_json::to_string(&state).unwrap(),
|
|
321
|
+
)
|
|
322
|
+
.unwrap();
|
|
323
|
+
assert!(
|
|
324
|
+
detect_dual_state_divergence(&ws, &state).unwrap().is_none(),
|
|
325
|
+
"两份一致 → 无分叉 → None"
|
|
326
|
+
);
|
|
327
|
+
}
|
|
290
328
|
|
|
291
329
|
// R8 D4 (c-lite offline byte-lock): the leader_receiver.requeued_exhausted_watchers payload-build,
|
|
292
330
|
// extracted from the real-tmux attach flow into a pure helper, must produce golden shape.
|
|
@@ -308,17 +346,34 @@ fn r8_requeued_exhausted_watchers_event_payload_golden_shape() {
|
|
|
308
346
|
&crate::model::ids::TeamKey::new("team-a"),
|
|
309
347
|
¬ices,
|
|
310
348
|
);
|
|
311
|
-
let keys: std::collections::BTreeSet<&str> =
|
|
312
|
-
|
|
313
|
-
|
|
349
|
+
let keys: std::collections::BTreeSet<&str> = payload
|
|
350
|
+
.as_object()
|
|
351
|
+
.unwrap()
|
|
352
|
+
.keys()
|
|
353
|
+
.map(String::as_str)
|
|
354
|
+
.collect();
|
|
355
|
+
let expected: std::collections::BTreeSet<&str> =
|
|
356
|
+
["watcher_ids", "count", "trigger"].into_iter().collect();
|
|
314
357
|
assert_eq!(keys, expected,
|
|
315
358
|
"D4: leader_receiver.requeued_exhausted_watchers payload must be golden {{watcher_ids, count, trigger}} \
|
|
316
359
|
(leader/__init__.py:39-44), not the Rust {{pane_id, team_id, watcher_ids, requeued}}; got {keys:?}");
|
|
317
360
|
assert_eq!(
|
|
318
|
-
payload
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
361
|
+
payload
|
|
362
|
+
.get("watcher_ids")
|
|
363
|
+
.and_then(|v| v.as_array())
|
|
364
|
+
.map(|a| a.iter().filter_map(|x| x.as_str()).collect::<Vec<_>>()),
|
|
365
|
+
Some(vec!["w1"]),
|
|
366
|
+
"watcher_ids must be the string list of requeued ids"
|
|
367
|
+
);
|
|
368
|
+
assert_eq!(
|
|
369
|
+
payload.get("count").and_then(|v| v.as_u64()),
|
|
370
|
+
Some(1),
|
|
371
|
+
"count == number of requeued watchers"
|
|
372
|
+
);
|
|
373
|
+
assert_eq!(
|
|
374
|
+
payload.get("trigger").and_then(|v| v.as_str()),
|
|
375
|
+
Some("attach_leader")
|
|
376
|
+
);
|
|
322
377
|
}
|
|
323
378
|
|
|
324
379
|
// 0.5.x Windows portability Batch 5: `FakeAppServer` uses UNIX domain
|
|
@@ -355,8 +410,14 @@ fn app_server_attach_writes_transport_kind_tuple_and_advances_epoch() {
|
|
|
355
410
|
.expect("app-server attach should succeed");
|
|
356
411
|
|
|
357
412
|
assert_eq!(out["ok"], serde_json::json!(true));
|
|
358
|
-
assert_eq!(
|
|
359
|
-
|
|
413
|
+
assert_eq!(
|
|
414
|
+
out["leader_receiver"]["transport_kind"],
|
|
415
|
+
serde_json::json!("codex_app_server")
|
|
416
|
+
);
|
|
417
|
+
assert_eq!(
|
|
418
|
+
out["leader_receiver"]["mode"],
|
|
419
|
+
serde_json::json!("codex_app_server")
|
|
420
|
+
);
|
|
360
421
|
assert_eq!(
|
|
361
422
|
out["leader_receiver"]["app_server"]["thread_id"],
|
|
362
423
|
serde_json::json!("thread-live")
|
|
@@ -376,7 +437,10 @@ fn app_server_attach_writes_transport_kind_tuple_and_advances_epoch() {
|
|
|
376
437
|
serde_json::json!("thread-live")
|
|
377
438
|
);
|
|
378
439
|
assert_eq!(saved["leader_receiver"].get("pane_id"), None);
|
|
379
|
-
assert_eq!(
|
|
440
|
+
assert_eq!(
|
|
441
|
+
saved["team_owner"]["transport_kind"],
|
|
442
|
+
serde_json::json!("codex_app_server")
|
|
443
|
+
);
|
|
380
444
|
assert_eq!(saved["owner_epoch"], serde_json::json!(1));
|
|
381
445
|
}
|
|
382
446
|
|