@team-agent/installer 0.5.77 → 0.5.80
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 +161 -29
- package/crates/team-agent/src/cli/diagnose.rs +333 -68
- package/crates/team-agent/src/cli/emit.rs +24 -8
- package/crates/team-agent/src/cli/mod.rs +543 -98
- package/crates/team-agent/src/cli/models.rs +393 -32
- package/crates/team-agent/src/cli/named_address.rs +19 -5
- package/crates/team-agent/src/cli/profile.rs +35 -4
- package/crates/team-agent/src/cli/spec.rs +7 -2
- package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
- package/crates/team-agent/src/cli/status_port/runtime.rs +3 -1
- package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
- package/crates/team-agent/src/cli/status_port/store.rs +1 -1
- package/crates/team-agent/src/cli/status_port/tests.rs +37 -1
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
- package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +122 -2
- package/crates/team-agent/src/leader/lease.rs +492 -33
- package/crates/team-agent/src/leader/owner_bind.rs +40 -12
- package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
- package/crates/team-agent/src/leader/rediscover.rs +36 -6
- package/crates/team-agent/src/leader/start.rs +197 -88
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +86 -23
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
- package/crates/team-agent/src/lifecycle/launch.rs +30 -7
- package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
- package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
- package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +996 -66
- package/crates/team-agent/src/lifecycle/restart.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -4
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_spaced_ids.stdout.txt +9 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +54 -9
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +71 -0
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- package/crates/team-agent/src/mcp_server/wire.rs +140 -11
- package/crates/team-agent/src/messaging/delivery.rs +221 -80
- package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +127 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
- package/crates/team-agent/src/provider/session_scan/common/tests.rs +204 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +24 -2
- package/crates/team-agent/src/state/abnormal_watch/tests/mod.rs +248 -0
- package/crates/team-agent/src/state/abnormal_watch.rs +144 -0
- package/crates/team-agent/src/state/mod.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +102 -4
- package/crates/team-agent/src/state/projection.rs +7 -1
- package/crates/team-agent/src/state/repository.rs +29 -0
- package/crates/team-agent/src/test_support/models_timeout_fixture.rs +43 -21
- package/crates/team-agent/src/tmux_backend/tests.rs +149 -4
- package/crates/team-agent/src/tmux_backend.rs +239 -127
- package/crates/team-agent/src/transport.rs +78 -0
- package/crates/team-agent/src/transport_factory.rs +53 -0
- package/package.json +4 -5
- package/skills/team-agent/SKILL.md +45 -17
- package/skills/team-agent/command-coverage.json +4 -29
- package/npm/check-skill-links.mjs +0 -88
- package/skills/team-agent/references/team-agent-operator.md +0 -672
|
@@ -159,6 +159,11 @@ pub(crate) struct CommandSpec {
|
|
|
159
159
|
pub(crate) governance: Option<GovernanceNote>,
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
/// Visible default-help contract:
|
|
163
|
+
/// - d40 / v0.5.77 published 15 names including `models` and excluding `results`.
|
|
164
|
+
/// - `results` remains a public handler and must stay discoverable.
|
|
165
|
+
/// - Exact composed set is d40 ∪ {results}. This is a frozen name set, not a
|
|
166
|
+
/// numeric slack threshold.
|
|
162
167
|
#[rustfmt::skip]
|
|
163
168
|
pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
164
169
|
CommandSpec { name: "quick-start", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::QuickStart), summary: "start or attach a team from TEAM.md", usage: "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
@@ -166,7 +171,7 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
|
166
171
|
CommandSpec { name: "status", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Status), summary: "show current team status", usage: "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
167
172
|
CommandSpec { name: "models", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Models), summary: "list exact provider model ids", usage: "usage: team-agent models --provider pi [--search TEXT] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
168
173
|
CommandSpec { name: "collect", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Collect), summary: "collect reported results", usage: "usage: team-agent collect [--workspace WORKSPACE] [--team TEAM] [--result-file FILE] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
169
|
-
CommandSpec { name: "results", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Results), summary: "read reported results for a case", usage: "usage: team-agent results --case CASE_ID [--workspace WORKSPACE] [--team TEAM] [--json]", default_help:
|
|
174
|
+
CommandSpec { name: "results", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Results), summary: "read reported results for a case", usage: "usage: team-agent results --case CASE_ID [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
170
175
|
CommandSpec { name: "wait", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Wait), summary: "block until a task result is stored", usage: "usage: team-agent wait --task TASK [--workspace WORKSPACE] [--json]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
171
176
|
CommandSpec { name: "restart", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Restart), summary: "restart the selected team", usage: "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
172
177
|
CommandSpec { name: "shutdown", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Shutdown), summary: "stop the selected team", usage: "usage: team-agent shutdown [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
@@ -201,7 +206,7 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
|
201
206
|
CommandSpec { name: "clone-agent", tier: CommandTier::Secondary, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::CloneAgent), summary: "clone a worker role into a fresh session", usage: "usage: team-agent clone-agent SOURCE_AGENT --as AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
202
207
|
CommandSpec { name: "allow-peer-talk", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::AllowPeerTalk), summary: "allow direct worker peer talk", usage: "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "capability/config surface" }) },
|
|
203
208
|
CommandSpec { name: "approvals", tier: CommandTier::Secondary, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Approvals), summary: "inspect provider approval state", usage: "usage: team-agent approvals [AGENT] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "approval diagnostics" }) },
|
|
204
|
-
CommandSpec { name: "profile", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::Profile), summary: "manage provider profiles", usage: "usage: team-agent profile COMMAND NAME [--workspace WORKSPACE] [--team TEAM] [--auth-mode MODE] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "provider/profile setup" }) },
|
|
209
|
+
CommandSpec { name: "profile", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::Profile), summary: "manage provider profiles", usage: "usage: team-agent profile COMMAND NAME [--workspace WORKSPACE] [--team TEAM] [--auth-mode MODE] [--proxy-mode direct|inherit] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "provider/profile setup" }) },
|
|
205
210
|
CommandSpec { name: "install-skill", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::InstallSkill), summary: "install or uninstall Team Agent skills", usage: "usage: team-agent install-skill (--source DIR | --uninstall) [--target codex|claude|copilot|all] [--dest DIR] [--dry-run] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "capability setup" }) },
|
|
206
211
|
CommandSpec { name: "inbox", tier: CommandTier::Secondary, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Inbox), summary: "inspect queued messages", usage: "usage: team-agent inbox AGENT [--workspace WORKSPACE] [--team TEAM] [--limit N] [--since CURSOR] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
207
212
|
|
|
@@ -60,19 +60,23 @@ pub(super) fn compact_not_ready(full: &Value) -> Value {
|
|
|
60
60
|
})
|
|
61
61
|
.unwrap_or_default();
|
|
62
62
|
let mut obj = Map::new();
|
|
63
|
-
let channel_unbound = reasons
|
|
64
|
-
.iter()
|
|
65
|
-
.any(|reason| reason == "leader_receiver_unbound");
|
|
66
63
|
obj.insert(
|
|
67
64
|
"reasons".to_string(),
|
|
68
65
|
Value::Array(reasons.into_iter().map(Value::String).collect()),
|
|
69
66
|
);
|
|
70
67
|
obj.insert("agents".to_string(), Value::Array(agents));
|
|
71
|
-
if
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
)
|
|
68
|
+
if let Some(next_action) = full
|
|
69
|
+
.pointer("/readiness/next_action")
|
|
70
|
+
.and_then(Value::as_str)
|
|
71
|
+
{
|
|
72
|
+
let truly_unbound = full.pointer("/readiness/state").and_then(Value::as_str)
|
|
73
|
+
== Some("leader_receiver_unbound");
|
|
74
|
+
if !next_action.contains("claim-leader") || truly_unbound {
|
|
75
|
+
obj.insert(
|
|
76
|
+
"next_action".to_string(),
|
|
77
|
+
Value::String(next_action.to_string()),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
76
80
|
}
|
|
77
81
|
Value::Object(obj)
|
|
78
82
|
}
|
|
@@ -58,7 +58,9 @@ pub(crate) fn compute_runtime_freshness(
|
|
|
58
58
|
for (agent_id, entry) in watch {
|
|
59
59
|
let exited =
|
|
60
60
|
entry.get("worker_provider_exited").and_then(Value::as_bool) == Some(true);
|
|
61
|
-
if exited
|
|
61
|
+
if exited
|
|
62
|
+
&& crate::state::abnormal_watch::matches_observation(state, agent_id, entry)
|
|
63
|
+
{
|
|
62
64
|
provider_exited_agents.insert(agent_id.clone());
|
|
63
65
|
}
|
|
64
66
|
}
|
|
@@ -95,18 +95,21 @@ impl RuntimeSnapshot {
|
|
|
95
95
|
.filter(|team| !team.is_empty())
|
|
96
96
|
.map(str::to_string)
|
|
97
97
|
.unwrap_or_else(|| crate::state::projection::team_state_key(state));
|
|
98
|
-
if !team_key.is_empty()
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
98
|
+
if !team_key.is_empty() {
|
|
99
|
+
let class = crate::lifecycle::launch::classify_leader_binding(workspace, &team_key);
|
|
100
|
+
if class != crate::lifecycle::launch::LeaderBindingClass::Attached {
|
|
101
|
+
if let Some(object) = readiness.as_object_mut() {
|
|
102
|
+
object.insert("all_attached_receiver".to_string(), json!(false));
|
|
103
|
+
object.insert("ready".to_string(), json!(false));
|
|
104
|
+
object.insert("state".to_string(), json!(class.issue_id()));
|
|
105
|
+
object.insert("reason".to_string(), json!(class.issue_id()));
|
|
106
|
+
if class == crate::lifecycle::launch::LeaderBindingClass::Unbound {
|
|
107
|
+
object.insert(
|
|
108
|
+
"next_action".to_string(),
|
|
109
|
+
json!(class.repair()),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
110
113
|
}
|
|
111
114
|
}
|
|
112
115
|
let grok_slot = crate::cli::grok_slot_report(workspace, state).to_json();
|
|
@@ -230,7 +230,7 @@ pub(super) fn pending_leader_notifications(
|
|
|
230
230
|
"created_at": row.get::<_, Option<String>>(4)?,
|
|
231
231
|
"delivery_attempts": row.get::<_, i64>(5)?,
|
|
232
232
|
"channel": channel,
|
|
233
|
-
"action": "
|
|
233
|
+
"action": "historical leader mailbox debt; not proof of current unbound owner",
|
|
234
234
|
}))
|
|
235
235
|
};
|
|
236
236
|
let rows = match owner_team_id {
|
|
@@ -28,7 +28,12 @@ fn health_fixture() -> crate::coordinator::HealthReport {
|
|
|
28
28
|
|
|
29
29
|
#[test]
|
|
30
30
|
fn runtime_freshness_provider_exit_requires_typed_positive_fact() {
|
|
31
|
-
let state = serde_json::json!({
|
|
31
|
+
let mut state = serde_json::json!({
|
|
32
|
+
"active_team_key": "status-team",
|
|
33
|
+
"agents": {
|
|
34
|
+
"pane_only": {"provider": "codex", "rollout_path": "/fixture/pane", "spawn_epoch": 1},
|
|
35
|
+
"typed_exit": {"provider": "codex", "rollout_path": "/fixture/typed", "spawn_epoch": 1}
|
|
36
|
+
},
|
|
32
37
|
"coordinator": {
|
|
33
38
|
"abnormal_exit_watch": {
|
|
34
39
|
"pane_only": {
|
|
@@ -43,6 +48,10 @@ fn runtime_freshness_provider_exit_requires_typed_positive_fact() {
|
|
|
43
48
|
}
|
|
44
49
|
});
|
|
45
50
|
|
|
51
|
+
for id in ["pane_only", "typed_exit"] {
|
|
52
|
+
state["coordinator"]["abnormal_exit_watch"][id]["watch_identity"] =
|
|
53
|
+
crate::state::abnormal_watch::observation_identity(&state, id).unwrap();
|
|
54
|
+
}
|
|
46
55
|
let freshness = compute_runtime_freshness(
|
|
47
56
|
Path::new("/nonexistent/status-port-typed-provider-exit-test"),
|
|
48
57
|
&state,
|
|
@@ -52,3 +61,30 @@ fn runtime_freshness_provider_exit_requires_typed_positive_fact() {
|
|
|
52
61
|
assert!(!freshness.provider_exited_agents.contains("pane_only"));
|
|
53
62
|
assert!(freshness.provider_exited_agents.contains("typed_exit"));
|
|
54
63
|
}
|
|
64
|
+
|
|
65
|
+
#[test]
|
|
66
|
+
fn runtime_freshness_rejects_foreign_legacy_and_unknown_watch_identity() {
|
|
67
|
+
let mut state = json!({
|
|
68
|
+
"active_team_key": "a",
|
|
69
|
+
"agents": {"w": {"provider": "codex", "rollout_path": "/fixture/w", "spawn_epoch": 1}},
|
|
70
|
+
"coordinator": {"abnormal_exit_watch": {"w": {"worker_provider_exited": true}}}
|
|
71
|
+
});
|
|
72
|
+
let path = Path::new("/nonexistent/status-watch-188");
|
|
73
|
+
assert!(compute_runtime_freshness(path, &state, &health_fixture())
|
|
74
|
+
.provider_exited_agents
|
|
75
|
+
.is_empty());
|
|
76
|
+
let identity = crate::state::abnormal_watch::observation_identity(&state, "w").unwrap();
|
|
77
|
+
state["coordinator"]["abnormal_exit_watch"]["w"]["watch_identity"] = identity;
|
|
78
|
+
for (pointer, value) in [
|
|
79
|
+
("/active_team_key", json!("b")),
|
|
80
|
+
("/agents/w/provider", json!("claude")),
|
|
81
|
+
("/agents/w/spawn_epoch", json!(2)),
|
|
82
|
+
("/agents/w/rollout_path", json!(null)),
|
|
83
|
+
] {
|
|
84
|
+
let mut changed = state.clone();
|
|
85
|
+
*changed.pointer_mut(pointer).unwrap() = value;
|
|
86
|
+
assert!(compute_runtime_freshness(path, &changed, &health_fixture())
|
|
87
|
+
.provider_exited_agents
|
|
88
|
+
.is_empty());
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -90,6 +90,44 @@ fn seed_self_signed_attached_leader(ws: &std::path::Path) {
|
|
|
90
90
|
.unwrap();
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
fn seed_canonical_healthy_leader(ws: &std::path::Path, endpoint: &str) {
|
|
94
|
+
crate::state::persist::save_runtime_state(
|
|
95
|
+
ws,
|
|
96
|
+
&json!({
|
|
97
|
+
"team_key": "current",
|
|
98
|
+
"workspace": ws,
|
|
99
|
+
"teams": {
|
|
100
|
+
"current": {
|
|
101
|
+
"team_owner": {
|
|
102
|
+
"pane_id": "%1",
|
|
103
|
+
"owner_epoch": 1,
|
|
104
|
+
"provider": "codex"
|
|
105
|
+
},
|
|
106
|
+
"leader_receiver": {
|
|
107
|
+
"mode": "direct_tmux",
|
|
108
|
+
"status": "attached",
|
|
109
|
+
"pane_id": "%1",
|
|
110
|
+
"owner_epoch": 1,
|
|
111
|
+
"provider": "codex",
|
|
112
|
+
"tmux_socket": endpoint
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}),
|
|
117
|
+
)
|
|
118
|
+
.unwrap();
|
|
119
|
+
assert_eq!(
|
|
120
|
+
crate::leader::registry::register_binding_from_state_best_effort(
|
|
121
|
+
ws,
|
|
122
|
+
Some("current"),
|
|
123
|
+
"diagnose-healthy-fixture",
|
|
124
|
+
)
|
|
125
|
+
.as_ref()
|
|
126
|
+
.map(|outcome| outcome.status),
|
|
127
|
+
Some("registered")
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
93
131
|
fn seed_leader_registry_entry(ws: &std::path::Path) {
|
|
94
132
|
let entry = crate::leader::registry::build_entry(
|
|
95
133
|
ws,
|
|
@@ -345,17 +383,33 @@ fn dispatch_routes_diagnose_healthy_leader() {
|
|
|
345
383
|
]);
|
|
346
384
|
let _home = IsolatedHome::enter("diagnose-healthy");
|
|
347
385
|
let ws = tmp_workspace();
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
let
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
386
|
+
let endpoint = "/tmp/ta-diagnose-healthy.sock";
|
|
387
|
+
seed_canonical_healthy_leader(&ws, endpoint);
|
|
388
|
+
let transport = crate::transport::test_support::OfflineTransport::default()
|
|
389
|
+
.with_tmux_endpoint(endpoint)
|
|
390
|
+
.with_targets(vec![crate::transport::PaneInfo {
|
|
391
|
+
pane_id: crate::transport::PaneId::new("%1"),
|
|
392
|
+
session: crate::transport::SessionName::new("s"),
|
|
393
|
+
window_index: None,
|
|
394
|
+
window_name: None,
|
|
395
|
+
pane_index: None,
|
|
396
|
+
tty: None,
|
|
397
|
+
current_command: Some("codex".to_string()),
|
|
398
|
+
current_path: Some(ws.clone()),
|
|
399
|
+
active: true,
|
|
400
|
+
pane_pid: None,
|
|
401
|
+
leader_env: Default::default(),
|
|
402
|
+
}]);
|
|
403
|
+
let code = crate::transport_factory::with_leader_endpoint_transport(endpoint, transport, || {
|
|
404
|
+
run(
|
|
405
|
+
&cli_argv(&["diagnose", "--workspace", &ws.to_string_lossy(), "--json"]),
|
|
406
|
+
&ws,
|
|
407
|
+
)
|
|
408
|
+
});
|
|
354
409
|
assert_eq!(
|
|
355
410
|
code,
|
|
356
411
|
ExitCode::Ok,
|
|
357
|
-
"`diagnose` must be Ok when
|
|
358
|
-
has a matching attached row; got {code:?}"
|
|
412
|
+
"`diagnose` must be Ok when canonical owner/receiver, production registry, and live align; got {code:?}"
|
|
359
413
|
);
|
|
360
414
|
let _ = std::fs::remove_dir_all(&ws);
|
|
361
415
|
}
|
|
@@ -234,6 +234,76 @@ fn resolve_name_team_disambiguated() {
|
|
|
234
234
|
let _ = std::fs::remove_dir_all(&ws);
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
#[test]
|
|
238
|
+
fn resolve_qualified_name_prefers_unique_state_pane_over_same_window_idle_pane() {
|
|
239
|
+
let ws = named_ws("qualified-state-pane-disambiguation");
|
|
240
|
+
seed_state(
|
|
241
|
+
&ws,
|
|
242
|
+
state_with_teams(json!({
|
|
243
|
+
"team-a": worker_team("team-a", "qa", "%worker", "qa")
|
|
244
|
+
})),
|
|
245
|
+
);
|
|
246
|
+
let transport = OfflineTransport::new().with_targets(vec![
|
|
247
|
+
pane("team-a", "qa", "%worker"),
|
|
248
|
+
pane("team-a", "qa", "%idle"),
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
let resolved = resolve_name_with_transport(
|
|
252
|
+
&ws,
|
|
253
|
+
&format!("{}::team-a/qa", ws.display()),
|
|
254
|
+
&transport,
|
|
255
|
+
)
|
|
256
|
+
.expect("a qualified worker address should use its unique state pane");
|
|
257
|
+
assert_eq!(resolved.pane_id, "%worker");
|
|
258
|
+
assert_eq!(resolved.state_pane_id.as_deref(), Some("%worker"));
|
|
259
|
+
assert!(!resolved.state_pane_stale);
|
|
260
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#[test]
|
|
264
|
+
fn resolve_qualified_name_state_pane_must_uniquely_match_scoped_candidates() {
|
|
265
|
+
for scenario in ["missing", "stale", "cross-session", "duplicate"] {
|
|
266
|
+
let ws = named_ws(&format!("qualified-state-pane-{scenario}"));
|
|
267
|
+
let mut team = worker_team("team-a", "qa", "%worker", "qa");
|
|
268
|
+
let targets = match scenario {
|
|
269
|
+
"missing" => {
|
|
270
|
+
team["agents"]["qa"]
|
|
271
|
+
.as_object_mut()
|
|
272
|
+
.unwrap()
|
|
273
|
+
.remove("pane_id");
|
|
274
|
+
vec![pane("team-a", "qa", "%worker"), pane("team-a", "qa", "%idle")]
|
|
275
|
+
}
|
|
276
|
+
"stale" => {
|
|
277
|
+
team["agents"]["qa"]["pane_id"] = json!("%stale");
|
|
278
|
+
vec![pane("team-a", "qa", "%worker"), pane("team-a", "qa", "%idle")]
|
|
279
|
+
}
|
|
280
|
+
"cross-session" => vec![
|
|
281
|
+
pane("old-team-a", "qa", "%worker"),
|
|
282
|
+
pane("team-a", "qa", "%current-one"),
|
|
283
|
+
pane("team-a", "qa", "%current-two"),
|
|
284
|
+
],
|
|
285
|
+
"duplicate" => {
|
|
286
|
+
team["agents"]["qa"]["pane_id"] = json!("%duplicate");
|
|
287
|
+
vec![
|
|
288
|
+
pane("team-a", "qa", "%duplicate"),
|
|
289
|
+
pane("team-a", "qa", "%duplicate"),
|
|
290
|
+
]
|
|
291
|
+
}
|
|
292
|
+
_ => unreachable!(),
|
|
293
|
+
};
|
|
294
|
+
seed_state(&ws, state_with_teams(json!({"team-a": team})));
|
|
295
|
+
let transport = OfflineTransport::new().with_targets(targets);
|
|
296
|
+
let err = resolve_name_with_transport(
|
|
297
|
+
&ws,
|
|
298
|
+
&format!("{}::team-a/qa", ws.display()),
|
|
299
|
+
&transport,
|
|
300
|
+
)
|
|
301
|
+
.expect_err("non-unique state pane evidence must remain fail-closed");
|
|
302
|
+
assert_eq!(err.kind, NamedAddressErrorKind::NameAmbiguous, "{scenario}");
|
|
303
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
237
307
|
#[test]
|
|
238
308
|
fn resolve_qualified_name_canonicalizes_legacy_session_alias() {
|
|
239
309
|
let ws = named_ws("legacy-session-alias");
|
|
@@ -35,7 +35,7 @@ fn seed_official_api_profile(ws: &std::path::Path) {
|
|
|
35
35
|
|
|
36
36
|
// Golden source:
|
|
37
37
|
// - cli/parser.py:131-150 registers `profile {init,doctor,show}`:
|
|
38
|
-
// * init NAME --workspace . --team TEAM --auth-mode choices(sorted AUTH_MODES) --json
|
|
38
|
+
// * init NAME --workspace . --team TEAM --auth-mode choices(sorted AUTH_MODES) --proxy-mode direct|inherit --json
|
|
39
39
|
// * doctor NAME --workspace . --team TEAM --json
|
|
40
40
|
// * show NAME --workspace . --team TEAM --json
|
|
41
41
|
// - cli/commands.py:47-67 resolves scope then delegates to profiles.init_profile/
|
|
@@ -52,7 +52,7 @@ fn seed_official_api_profile(ws: &std::path::Path) {
|
|
|
52
52
|
// PYTHONPATH=/Users/alauda/Documents/code/team-agent-public/src \
|
|
53
53
|
// python3 /tmp/probe_profile_cli.py
|
|
54
54
|
// profile init codex_sub --auth-mode subscription --json rc=0 and creates:
|
|
55
|
-
// codex_sub.env = "AUTH_MODE=subscription\nPROFILE_NAME=codex_sub\n" (mode 0600),
|
|
55
|
+
// codex_sub.env = "AUTH_MODE=subscription\nPROFILE_NAME=codex_sub\nPROXY_MODE=inherit\n" (mode 0600),
|
|
56
56
|
// codex_sub.example.env with same body, plus AGENTS.md/CLAUDE.md secret boundary files.
|
|
57
57
|
#[test]
|
|
58
58
|
fn profile_init_routes_and_creates_secret_boundary_files() {
|
|
@@ -87,12 +87,12 @@ fn profile_init_routes_and_creates_secret_boundary_files() {
|
|
|
87
87
|
);
|
|
88
88
|
assert_eq!(
|
|
89
89
|
std::fs::read_to_string(dir.join("codex_sub.env")).unwrap(),
|
|
90
|
-
"AUTH_MODE=subscription\nPROFILE_NAME=codex_sub\n",
|
|
90
|
+
"AUTH_MODE=subscription\nPROFILE_NAME=codex_sub\nPROXY_MODE=inherit\n",
|
|
91
91
|
"subscription profile template body must match golden"
|
|
92
92
|
);
|
|
93
93
|
assert_eq!(
|
|
94
94
|
std::fs::read_to_string(dir.join("codex_sub.example.env")).unwrap(),
|
|
95
|
-
"AUTH_MODE=subscription\nPROFILE_NAME=codex_sub\n",
|
|
95
|
+
"AUTH_MODE=subscription\nPROFILE_NAME=codex_sub\nPROXY_MODE=inherit\n",
|
|
96
96
|
"subscription example template body must match golden"
|
|
97
97
|
);
|
|
98
98
|
|
|
@@ -116,7 +116,7 @@ fn profile_init_routes_and_creates_secret_boundary_files() {
|
|
|
116
116
|
);
|
|
117
117
|
assert_eq!(
|
|
118
118
|
std::fs::read_to_string(dir.join("codex_sub.env")).unwrap(),
|
|
119
|
-
"AUTH_MODE=subscription\nPROFILE_NAME=codex_sub\n",
|
|
119
|
+
"AUTH_MODE=subscription\nPROFILE_NAME=codex_sub\nPROXY_MODE=inherit\n",
|
|
120
120
|
"idempotent init must not rewrite existing profile bytes"
|
|
121
121
|
);
|
|
122
122
|
let _ = std::fs::remove_dir_all(&ws);
|
|
@@ -145,7 +145,7 @@ fn compatible_api_profile_init_template_contains_common_five_fields() {
|
|
|
145
145
|
);
|
|
146
146
|
|
|
147
147
|
let expected =
|
|
148
|
-
"AUTH_MODE=compatible_api\nPROFILE_NAME=compat\nBASE_URL=\nAPI_KEY=\nMODEL=\n";
|
|
148
|
+
"AUTH_MODE=compatible_api\nPROFILE_NAME=compat\nPROXY_MODE=inherit\nBASE_URL=\nAPI_KEY=\nMODEL=\n";
|
|
149
149
|
let dir = profiles_dir(&ws);
|
|
150
150
|
assert_eq!(
|
|
151
151
|
std::fs::read_to_string(dir.join("compat.env")).unwrap(),
|
|
@@ -160,6 +160,81 @@ fn compatible_api_profile_init_template_contains_common_five_fields() {
|
|
|
160
160
|
let _ = std::fs::remove_dir_all(&ws);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
#[test]
|
|
164
|
+
fn profile_init_accepts_local_proxy_mode_and_rejects_unknown_values() {
|
|
165
|
+
let ws = tmp_workspace();
|
|
166
|
+
let direct = run(
|
|
167
|
+
&profile_argv(&[
|
|
168
|
+
"profile",
|
|
169
|
+
"init",
|
|
170
|
+
"cursor_sub",
|
|
171
|
+
"--workspace",
|
|
172
|
+
".",
|
|
173
|
+
"--auth-mode",
|
|
174
|
+
"subscription",
|
|
175
|
+
"--proxy-mode=direct",
|
|
176
|
+
"--json",
|
|
177
|
+
]),
|
|
178
|
+
&ws,
|
|
179
|
+
);
|
|
180
|
+
assert_eq!(direct, ExitCode::Ok, "direct proxy mode must be accepted");
|
|
181
|
+
let body = std::fs::read_to_string(profiles_dir(&ws).join("cursor_sub.env")).unwrap();
|
|
182
|
+
assert!(body.contains("PROXY_MODE=direct\n"));
|
|
183
|
+
|
|
184
|
+
let invalid = run(
|
|
185
|
+
&profile_argv(&[
|
|
186
|
+
"profile",
|
|
187
|
+
"init",
|
|
188
|
+
"bad_proxy",
|
|
189
|
+
"--workspace",
|
|
190
|
+
".",
|
|
191
|
+
"--proxy-mode",
|
|
192
|
+
"sideways",
|
|
193
|
+
"--json",
|
|
194
|
+
]),
|
|
195
|
+
&ws,
|
|
196
|
+
);
|
|
197
|
+
assert_eq!(invalid, ExitCode::Error, "unknown proxy mode must fail");
|
|
198
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#[test]
|
|
202
|
+
fn profile_init_existing_profile_reports_actual_mode_and_show_matches() {
|
|
203
|
+
let ws = tmp_workspace();
|
|
204
|
+
let make_args = |command: &str, mode: Option<&str>| ProfileArgs {
|
|
205
|
+
command: command.to_string(),
|
|
206
|
+
name: "cursor_sub".to_string(),
|
|
207
|
+
workspace: ws.clone(),
|
|
208
|
+
team: None,
|
|
209
|
+
auth_mode: Some("subscription".to_string()),
|
|
210
|
+
proxy_mode: mode.map(str::to_string),
|
|
211
|
+
json: true,
|
|
212
|
+
};
|
|
213
|
+
let first = cmd_profile(&make_args("init", Some("direct"))).unwrap();
|
|
214
|
+
let first = match first.output {
|
|
215
|
+
CmdOutput::Json(value) => value,
|
|
216
|
+
other => panic!("expected json output, got {other:?}"),
|
|
217
|
+
};
|
|
218
|
+
assert_eq!(first["proxy_mode"], "direct");
|
|
219
|
+
assert_eq!(first["proxy_mode_changed"], true);
|
|
220
|
+
|
|
221
|
+
let repeat = cmd_profile(&make_args("init", Some("inherit"))).unwrap();
|
|
222
|
+
let repeat = match repeat.output {
|
|
223
|
+
CmdOutput::Json(value) => value,
|
|
224
|
+
other => panic!("expected json output, got {other:?}"),
|
|
225
|
+
};
|
|
226
|
+
assert_eq!(repeat["proxy_mode"], "direct");
|
|
227
|
+
assert_eq!(repeat["proxy_mode_changed"], false);
|
|
228
|
+
|
|
229
|
+
let shown = cmd_profile(&make_args("show", None)).unwrap();
|
|
230
|
+
let shown = match shown.output {
|
|
231
|
+
CmdOutput::Json(value) => value,
|
|
232
|
+
other => panic!("expected json output, got {other:?}"),
|
|
233
|
+
};
|
|
234
|
+
assert_eq!(shown["proxy_mode"], "direct");
|
|
235
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
236
|
+
}
|
|
237
|
+
|
|
163
238
|
// Golden source:
|
|
164
239
|
// - profiles/core.py:95-119 doctor_profile returns ok=true for existing profiles and
|
|
165
240
|
// ok=false for missing profiles; parser.py:506-508 maps result.ok false to exit 1.
|
|
@@ -73,6 +73,8 @@ pub(crate) fn detect_abnormal_exits(
|
|
|
73
73
|
let team = crate::state::projection::team_state_key(&snapshot);
|
|
74
74
|
let session_name = snapshot.get("session_name").and_then(Value::as_str);
|
|
75
75
|
for agent in abnormal_watch_agents(&snapshot) {
|
|
76
|
+
// Reject untrusted history without skipping a fresh provider/path observation.
|
|
77
|
+
crate::state::abnormal_watch::begin_observation(state, &agent.agent_id);
|
|
76
78
|
// Pane/process liveness is independent of transcript content. A
|
|
77
79
|
// frozen rollout is common after pane death, so probe before the
|
|
78
80
|
// metadata dedupe gate; only the expensive tail scan stays deduped.
|
|
@@ -109,7 +111,7 @@ pub(crate) fn detect_abnormal_exits(
|
|
|
109
111
|
// re-read below.
|
|
110
112
|
if let (Some(mtime), Some(stored)) = (
|
|
111
113
|
mtime_ns,
|
|
112
|
-
abnormal_watch_stored_metadata(
|
|
114
|
+
abnormal_watch_stored_metadata(state, &agent.agent_id),
|
|
113
115
|
) {
|
|
114
116
|
if stored == (size, mtime) {
|
|
115
117
|
refresh_abnormal_watch_liveness(state, &agent.agent_id, &liveness);
|
|
@@ -149,7 +151,7 @@ pub(crate) fn detect_abnormal_exits(
|
|
|
149
151
|
.map(|fact| abnormal_error_observation_key(&agent, fact));
|
|
150
152
|
let error_observation_cohort = fact.as_ref().map(|_| abnormal_error_cohort_key(&agent));
|
|
151
153
|
let error_recency = abnormal_error_recency(
|
|
152
|
-
|
|
154
|
+
state,
|
|
153
155
|
&agent,
|
|
154
156
|
error_observation_key.as_deref(),
|
|
155
157
|
error_observation_cohort.as_deref(),
|
|
@@ -954,6 +956,9 @@ fn abnormal_watch_payload(
|
|
|
954
956
|
}
|
|
955
957
|
|
|
956
958
|
fn upsert_abnormal_watch(state: &mut Value, agent_id: &str, mut payload: Value) {
|
|
959
|
+
if let Some(identity) = crate::state::abnormal_watch::observation_identity(state, agent_id) {
|
|
960
|
+
payload["watch_identity"] = identity;
|
|
961
|
+
}
|
|
957
962
|
let preserved = [
|
|
958
963
|
"last_notified_key",
|
|
959
964
|
"last_notified_at",
|
|
@@ -1413,6 +1418,121 @@ mod tests {
|
|
|
1413
1418
|
crate::provider::get_adapter(provider)
|
|
1414
1419
|
}
|
|
1415
1420
|
}
|
|
1421
|
+
#[test]
|
|
1422
|
+
fn resource188_foreign_metadata_and_dedupe_cannot_skip_new_baseline() {
|
|
1423
|
+
let dir = temp_abnormal_dir("188-foreign-watch");
|
|
1424
|
+
let rollout = dir.join("rollout-w1.jsonl");
|
|
1425
|
+
let error = "{\"method\":\"turn/completed\",\"params\":{\"turn\":{\"id\":\"old\",\"status\":\"failed\"}}}\n";
|
|
1426
|
+
std::fs::write(&rollout, error).unwrap();
|
|
1427
|
+
seed_abnormal_state(&dir, &rollout, "alive", 1);
|
|
1428
|
+
let mut state = crate::state::persist::load_runtime_state(&dir).unwrap();
|
|
1429
|
+
let metadata = std::fs::metadata(&rollout).unwrap();
|
|
1430
|
+
let mut foreign = crate::state::abnormal_watch::observation_identity(&state, "w1").unwrap();
|
|
1431
|
+
foreign["team_key"] = serde_json::json!("another-team");
|
|
1432
|
+
state["coordinator"] = serde_json::json!({"abnormal_exit_watch": {"w1": {
|
|
1433
|
+
"watch_identity": foreign,
|
|
1434
|
+
"size": metadata.len(), "mtime_ns": metadata_mtime_ns(&metadata),
|
|
1435
|
+
"last_notified_key": "foreign", "last_check_key": "foreign",
|
|
1436
|
+
"last_error_observation_key": "foreign", "last_error_observation_cohort": "foreign"
|
|
1437
|
+
}}});
|
|
1438
|
+
let log = EventLog::new(&dir);
|
|
1439
|
+
let transport = crate::transport::test_support::OfflineTransport::new();
|
|
1440
|
+
detect_abnormal_exits(&dir, &transport, &mut state, &log, &[]).unwrap();
|
|
1441
|
+
let baseline = state["coordinator"]["abnormal_exit_watch"]["w1"].clone();
|
|
1442
|
+
assert_eq!(baseline["error_recency"], "stale");
|
|
1443
|
+
assert!(baseline.get("last_notified_key").is_none());
|
|
1444
|
+
assert!(crate::state::abnormal_watch::matches_observation(
|
|
1445
|
+
&state, "w1", &baseline
|
|
1446
|
+
));
|
|
1447
|
+
let events = log.tail(0).unwrap();
|
|
1448
|
+
detect_abnormal_exits(&dir, &transport, &mut state, &log, &[]).unwrap();
|
|
1449
|
+
assert_eq!(
|
|
1450
|
+
log.tail(0).unwrap(),
|
|
1451
|
+
events,
|
|
1452
|
+
"unchanged baseline must not re-alarm"
|
|
1453
|
+
);
|
|
1454
|
+
assert!(find_event(&events, "worker.abnormal_exit").is_none());
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
#[test]
|
|
1458
|
+
fn resource188_legacy_missing_cohort_still_observes_fresh_errors_and_dedupes() {
|
|
1459
|
+
let dir = temp_abnormal_dir("188-legacy-cohort");
|
|
1460
|
+
let rollout = dir.join("rollout-w1.jsonl");
|
|
1461
|
+
let error = "{\"method\":\"turn/completed\",\"params\":{\"turn\":{\"id\":\"old\",\"status\":\"failed\"}}}\n";
|
|
1462
|
+
std::fs::write(&rollout, error).unwrap();
|
|
1463
|
+
seed_abnormal_state(&dir, &rollout, "alive", 1);
|
|
1464
|
+
let mut state = crate::state::persist::load_runtime_state(&dir).unwrap();
|
|
1465
|
+
state["agents"]["w1"]
|
|
1466
|
+
.as_object_mut()
|
|
1467
|
+
.unwrap()
|
|
1468
|
+
.remove("spawn_epoch");
|
|
1469
|
+
let log = EventLog::new(&dir);
|
|
1470
|
+
let transport = crate::transport::test_support::OfflineTransport::new();
|
|
1471
|
+
detect_abnormal_exits(&dir, &transport, &mut state, &log, &[]).unwrap();
|
|
1472
|
+
assert_eq!(
|
|
1473
|
+
state["coordinator"]["abnormal_exit_watch"]["w1"]["error_recency"],
|
|
1474
|
+
"stale"
|
|
1475
|
+
);
|
|
1476
|
+
assert!(find_event(&log.tail(0).unwrap(), "worker.abnormal_exit").is_none());
|
|
1477
|
+
std::fs::OpenOptions::new()
|
|
1478
|
+
.append(true)
|
|
1479
|
+
.open(&rollout)
|
|
1480
|
+
.unwrap()
|
|
1481
|
+
.write_all(error.replace("old", "new-error").as_bytes())
|
|
1482
|
+
.unwrap();
|
|
1483
|
+
detect_abnormal_exits(&dir, &transport, &mut state, &log, &[]).unwrap();
|
|
1484
|
+
let events = log.tail(0).unwrap();
|
|
1485
|
+
assert_eq!(
|
|
1486
|
+
find_event(&events, "worker.abnormal_exit").unwrap()["error_recency"],
|
|
1487
|
+
"fresh"
|
|
1488
|
+
);
|
|
1489
|
+
let notified =
|
|
1490
|
+
state["coordinator"]["abnormal_exit_watch"]["w1"]["last_notified_key"].clone();
|
|
1491
|
+
assert!(notified.is_string());
|
|
1492
|
+
detect_abnormal_exits(&dir, &transport, &mut state, &log, &[]).unwrap();
|
|
1493
|
+
assert_eq!(log.tail(0).unwrap(), events);
|
|
1494
|
+
assert_eq!(
|
|
1495
|
+
state["coordinator"]["abnormal_exit_watch"]["w1"]["last_notified_key"],
|
|
1496
|
+
notified
|
|
1497
|
+
);
|
|
1498
|
+
state["agents"]["w1"]["spawn_epoch"] = serde_json::json!(2);
|
|
1499
|
+
detect_abnormal_exits(&dir, &transport, &mut state, &log, &[]).unwrap();
|
|
1500
|
+
let watch = &state["coordinator"]["abnormal_exit_watch"]["w1"];
|
|
1501
|
+
assert_eq!(
|
|
1502
|
+
watch["error_recency"], "stale",
|
|
1503
|
+
"a known generation must rebaseline unchanged bytes"
|
|
1504
|
+
);
|
|
1505
|
+
assert!(watch.get("last_notified_key").is_none());
|
|
1506
|
+
assert_eq!(
|
|
1507
|
+
log.tail(0)
|
|
1508
|
+
.unwrap()
|
|
1509
|
+
.iter()
|
|
1510
|
+
.filter(|event| event["event"] == "worker.abnormal_exit")
|
|
1511
|
+
.count(),
|
|
1512
|
+
1
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
#[test]
|
|
1517
|
+
fn resource188_unknown_team_still_records_fresh_liveness_without_reusing_cache() {
|
|
1518
|
+
let dir = temp_abnormal_dir("188-unknown-team");
|
|
1519
|
+
let rollout = dir.join("rollout-w1.jsonl");
|
|
1520
|
+
std::fs::write(&rollout, "").unwrap();
|
|
1521
|
+
seed_abnormal_state(&dir, &rollout, "dead", 1);
|
|
1522
|
+
let mut state = crate::state::persist::load_runtime_state(&dir).unwrap();
|
|
1523
|
+
state.as_object_mut().unwrap().remove("active_team_key");
|
|
1524
|
+
let log = EventLog::new(&dir);
|
|
1525
|
+
let transport = crate::transport::test_support::OfflineTransport::new();
|
|
1526
|
+
detect_abnormal_exits(&dir, &transport, &mut state, &log, &[]).unwrap();
|
|
1527
|
+
assert_eq!(
|
|
1528
|
+
state["coordinator"]["abnormal_exit_watch"]["w1"]["last_liveness"],
|
|
1529
|
+
"dead"
|
|
1530
|
+
);
|
|
1531
|
+
assert!(state["coordinator"]["abnormal_exit_watch"]["w1"]
|
|
1532
|
+
.get("watch_identity")
|
|
1533
|
+
.is_none());
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1416
1536
|
#[test]
|
|
1417
1537
|
fn abnormal_stale_error_baselines_then_fresh_alive_error_notifies() {
|
|
1418
1538
|
let dir = temp_abnormal_dir("alive-fresh");
|