@team-agent/installer 0.4.11 → 0.5.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 +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +1 -0
- package/crates/team-agent/src/cli/diagnose.rs +2 -15
- package/crates/team-agent/src/cli/emit.rs +62 -4
- package/crates/team-agent/src/compiler.rs +11 -20
- package/crates/team-agent/src/coordinator/orphan.rs +2 -2
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +5 -4
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +1776 -2
- package/crates/team-agent/src/coordinator/steps/mod.rs +19 -0
- package/crates/team-agent/src/coordinator/tests/a0_lostupdate.rs +78 -7
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +195 -0
- package/crates/team-agent/src/coordinator/tick.rs +31 -932
- package/crates/team-agent/src/diagnose/orphans.rs +66 -8
- package/crates/team-agent/src/layout/worker_window_helpers.rs +5 -7
- package/crates/team-agent/src/leader/provider_attribution.rs +7 -5
- package/crates/team-agent/src/leader/rediscover.rs +1 -11
- package/crates/team-agent/src/lifecycle/launch/plan.rs +19 -8
- package/crates/team-agent/src/lifecycle/launch.rs +135 -58
- package/crates/team-agent/src/lifecycle/lock.rs +302 -0
- package/crates/team-agent/src/lifecycle/mod.rs +1 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +1 -11
- package/crates/team-agent/src/lifecycle/restart/agent.rs +169 -113
- package/crates/team-agent/src/lifecycle/restart/common.rs +108 -17
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -32
- package/crates/team-agent/src/lifecycle/restart/remove.rs +16 -2
- package/crates/team-agent/src/lifecycle/restart/selection.rs +2 -1
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +6 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +321 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +35 -5
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +802 -0
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +648 -0
- package/crates/team-agent/src/lifecycle/tests.rs +3 -0
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +24 -9
- package/crates/team-agent/src/mcp_server/tools.rs +157 -68
- package/crates/team-agent/src/messaging/activity.rs +43 -18
- package/crates/team-agent/src/messaging/delivery.rs +161 -97
- package/crates/team-agent/src/messaging/helpers.rs +1 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +22 -1
- package/crates/team-agent/src/messaging/results.rs +34 -9
- package/crates/team-agent/src/messaging/scheduler.rs +52 -9
- package/crates/team-agent/src/model/spec.rs +10 -6
- package/crates/team-agent/src/provider/adapter.rs +10 -1038
- package/crates/team-agent/src/provider/adapters/claude.rs +3 -8
- package/crates/team-agent/src/provider/adapters/copilot.rs +2 -5
- package/crates/team-agent/src/provider/classify.rs +23 -48
- package/crates/team-agent/src/provider/command.rs +16 -7
- package/crates/team-agent/src/provider/faults.rs +93 -53
- package/crates/team-agent/src/provider/session/capture.rs +4 -15
- package/crates/team-agent/src/provider/session_scan/claude.rs +309 -0
- package/crates/team-agent/src/provider/session_scan/codex.rs +40 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +350 -0
- package/crates/team-agent/src/provider/session_scan/copilot.rs +202 -0
- package/crates/team-agent/src/provider/session_scan.rs +65 -27
- package/crates/team-agent/src/provider/tests/faults.rs +80 -0
- package/crates/team-agent/src/provider/types.rs +33 -1
- package/crates/team-agent/src/provider/wire.rs +171 -1
- package/crates/team-agent/src/state/identity.rs +242 -57
- package/crates/team-agent/src/state/identity_keys.rs +9 -12
- package/crates/team-agent/src/state/mod.rs +5 -1
- package/crates/team-agent/src/state/owner_gate.rs +59 -15
- package/crates/team-agent/src/state/ownership.rs +12 -11
- package/crates/team-agent/src/state/paths.rs +13 -6
- package/crates/team-agent/src/state/persist.rs +671 -128
- package/crates/team-agent/src/state/projection.rs +240 -49
- package/crates/team-agent/src/state/selector.rs +11 -5
- package/crates/team-agent/src/tmux_backend/tests.rs +51 -2
- package/crates/team-agent/src/tmux_backend.rs +42 -5
- package/crates/team-agent/src/transport/test_support.rs +55 -6
- package/package.json +4 -4
|
@@ -80,4 +80,23 @@ mod tests {
|
|
|
80
80
|
assert!(g.as_str().starts_with("tick."));
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
+
|
|
84
|
+
#[test]
|
|
85
|
+
fn ordered_group_labels_are_byte_stable() {
|
|
86
|
+
let labels = TickStepGroup::ordered()
|
|
87
|
+
.iter()
|
|
88
|
+
.map(|group| group.as_str())
|
|
89
|
+
.collect::<Vec<_>>();
|
|
90
|
+
assert_eq!(
|
|
91
|
+
labels,
|
|
92
|
+
vec![
|
|
93
|
+
"tick.session_gate",
|
|
94
|
+
"tick.health_sync",
|
|
95
|
+
"tick.delivery",
|
|
96
|
+
"tick.runtime_prompts",
|
|
97
|
+
"tick.abnormal",
|
|
98
|
+
"tick.persist",
|
|
99
|
+
]
|
|
100
|
+
);
|
|
101
|
+
}
|
|
83
102
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
use super::*;
|
|
2
2
|
|
|
3
3
|
// ═════════════════════════════════════════════════════════════════════════
|
|
4
|
-
//
|
|
4
|
+
// Phase C stale-save contract, integration tier (save_hook window injection).
|
|
5
5
|
// Locate doc: .team/artifacts/a0-rs-lostupdate-locate.md §5.4.
|
|
6
6
|
//
|
|
7
7
|
// Python 0.2.11 A0: the coordinator tick loads state (tick window opens), mutates in
|
|
8
8
|
// memory for seconds, then whole-file-saves with no merge — an add-agent registration
|
|
9
|
-
// landing inside that window is permanently overwritten (state.py:493).
|
|
10
|
-
//
|
|
9
|
+
// landing inside that window is permanently overwritten (state.py:493). Phase C keeps
|
|
10
|
+
// the no-corruption guarantee by refusing stale live topology instead of cloning rows.
|
|
11
11
|
//
|
|
12
12
|
// This test pins the END-TO-END guard through the REAL tick: the `save_hook` seam
|
|
13
13
|
// (tick.rs save point) lets us deterministically land a concurrent registration on disk
|
|
@@ -17,7 +17,7 @@ use super::*;
|
|
|
17
17
|
// ═════════════════════════════════════════════════════════════════════════
|
|
18
18
|
|
|
19
19
|
#[test]
|
|
20
|
-
fn
|
|
20
|
+
fn a0_phase_c_tick_save_degrades_on_live_topology_landed_after_tick_load() {
|
|
21
21
|
let dir = std::env::temp_dir().join(format!(
|
|
22
22
|
"team-agent-coord-a0-{}-{}",
|
|
23
23
|
std::process::id(),
|
|
@@ -46,6 +46,7 @@ fn a0_green_lock_tick_save_preserves_registration_landed_after_tick_load() {
|
|
|
46
46
|
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
|
47
47
|
latest["agents"]["joiner"] = serde_json::json!({
|
|
48
48
|
"provider": "codex", "status": "running", "agent_id": "joiner", "window": "joiner",
|
|
49
|
+
"pane_id": "%9", "spawned_at": "2026-06-01T00:00:00Z", "spawn_epoch": 1,
|
|
49
50
|
});
|
|
50
51
|
std::fs::write(&path, serde_json::to_string_pretty(&latest).unwrap()).unwrap();
|
|
51
52
|
crate::state::projection::save_team_scoped_state(ws.as_path(), tick_state)
|
|
@@ -56,7 +57,10 @@ fn a0_green_lock_tick_save_preserves_registration_landed_after_tick_load() {
|
|
|
56
57
|
let transport = MockTransport::new(true);
|
|
57
58
|
let coord = Coordinator::for_test(ws, reg, Box::new(transport), Some(hook), None);
|
|
58
59
|
let report = coord.tick().expect("tick should complete");
|
|
59
|
-
assert!(
|
|
60
|
+
assert!(
|
|
61
|
+
!report.ok && matches!(report.reason, Some(TickStopReason::PersistenceDegraded)),
|
|
62
|
+
"stale live topology must degrade this tick and retry from fresh state later; report={report:?}"
|
|
63
|
+
);
|
|
60
64
|
|
|
61
65
|
let saved: serde_json::Value = serde_json::from_str(
|
|
62
66
|
&std::fs::read_to_string(crate::state::persist::runtime_state_path(&dir)).unwrap(),
|
|
@@ -66,8 +70,7 @@ fn a0_green_lock_tick_save_preserves_registration_landed_after_tick_load() {
|
|
|
66
70
|
saved
|
|
67
71
|
.pointer("/agents/joiner")
|
|
68
72
|
.is_some_and(serde_json::Value::is_object),
|
|
69
|
-
"
|
|
70
|
-
survive the tick's save (in-lock reload+merge, persist.rs:272-313); saved={saved}"
|
|
73
|
+
"Phase C: concurrent live registration stays on disk; stale tick must not overwrite it; saved={saved}"
|
|
71
74
|
);
|
|
72
75
|
// 0.3.5 integration re-anchor (P3 / perf C-P3-1): the tick iteration counter moved
|
|
73
76
|
// OUT of persistent state into .team/runtime/coordinator_tick.json — state.json is
|
|
@@ -85,3 +88,71 @@ survive the tick's save (in-lock reload+merge, persist.rs:272-313); saved={saved
|
|
|
85
88
|
"the tick really ran (its iteration counter metadata landed); tick_meta={tick_meta}"
|
|
86
89
|
);
|
|
87
90
|
}
|
|
91
|
+
|
|
92
|
+
#[test]
|
|
93
|
+
fn a0_phase_c_tick_preserves_typed_non_live_roster_stub_only() {
|
|
94
|
+
let dir = std::env::temp_dir().join(format!(
|
|
95
|
+
"team-agent-coord-a0-roster-{}-{}",
|
|
96
|
+
std::process::id(),
|
|
97
|
+
std::time::SystemTime::now()
|
|
98
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
99
|
+
.unwrap()
|
|
100
|
+
.as_nanos()
|
|
101
|
+
));
|
|
102
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
103
|
+
crate::state::persist::save_runtime_state(
|
|
104
|
+
&dir,
|
|
105
|
+
&serde_json::json!({
|
|
106
|
+
"session_name": "team-a0",
|
|
107
|
+
"active_team_key": "team-a0",
|
|
108
|
+
"agents": { "w1": { "provider": "codex", "status": "running", "agent_id": "w1", "window": "w1" } },
|
|
109
|
+
}),
|
|
110
|
+
)
|
|
111
|
+
.unwrap();
|
|
112
|
+
|
|
113
|
+
let hook: SaveHook = Box::new(|ws, tick_state| {
|
|
114
|
+
let path = crate::state::persist::runtime_state_path(ws.as_path());
|
|
115
|
+
let mut latest: serde_json::Value =
|
|
116
|
+
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
|
117
|
+
latest["agents"]["typed"] = serde_json::json!({
|
|
118
|
+
"provider": "codex",
|
|
119
|
+
"role": "Developer",
|
|
120
|
+
"agent_id": "typed",
|
|
121
|
+
"status": "running",
|
|
122
|
+
"spawn_cwd": "/tmp/stale",
|
|
123
|
+
"session_id": "old-session"
|
|
124
|
+
});
|
|
125
|
+
std::fs::write(&path, serde_json::to_string_pretty(&latest).unwrap()).unwrap();
|
|
126
|
+
crate::state::projection::save_team_scoped_state(ws.as_path(), tick_state)
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
let ws = WorkspacePath::new(dir.clone());
|
|
130
|
+
let reg: Box<dyn ProviderRegistry> = Box::new(MockRegistry::new(&[], &[]));
|
|
131
|
+
let transport = MockTransport::new(true);
|
|
132
|
+
let coord = Coordinator::for_test(ws, reg, Box::new(transport), Some(hook), None);
|
|
133
|
+
let report = coord.tick().expect("tick should complete");
|
|
134
|
+
assert!(
|
|
135
|
+
report.ok,
|
|
136
|
+
"non-live roster stub must not degrade tick; report={report:?}"
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
let saved: serde_json::Value = serde_json::from_str(
|
|
140
|
+
&std::fs::read_to_string(crate::state::persist::runtime_state_path(&dir)).unwrap(),
|
|
141
|
+
)
|
|
142
|
+
.unwrap();
|
|
143
|
+
let typed = saved.pointer("/agents/typed").expect("typed roster stub");
|
|
144
|
+
assert_eq!(
|
|
145
|
+
typed.get("provider").and_then(serde_json::Value::as_str),
|
|
146
|
+
Some("codex")
|
|
147
|
+
);
|
|
148
|
+
assert_eq!(
|
|
149
|
+
typed.get("role").and_then(serde_json::Value::as_str),
|
|
150
|
+
Some("Developer")
|
|
151
|
+
);
|
|
152
|
+
for forbidden in ["status", "spawn_cwd", "session_id"] {
|
|
153
|
+
assert!(
|
|
154
|
+
typed.get(forbidden).is_none(),
|
|
155
|
+
"typed roster stub must not preserve forbidden field {forbidden}; typed={typed}"
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -150,6 +150,201 @@ fn abnormal_preseeded_seen_suppresses_duplicate_across_calls() {
|
|
|
150
150
|
assert_eq!(out.notifications.len(), 0, "pre-seeded seen suppresses (probe: 0)");
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
#[test]
|
|
154
|
+
fn abnormal_explicit_error_classifier_boundaries_stay_stable() {
|
|
155
|
+
let claude_api = line(serde_json::json!({
|
|
156
|
+
"type": "system",
|
|
157
|
+
"subtype": "api_error",
|
|
158
|
+
"level": "error",
|
|
159
|
+
"sessionId": "sess-1"
|
|
160
|
+
}));
|
|
161
|
+
let claude_api_fact =
|
|
162
|
+
crate::provider::latest_explicit_error_fact(Provider::ClaudeCode, &claude_api)
|
|
163
|
+
.expect("Claude system api_error level=error counts as explicit");
|
|
164
|
+
assert_eq!(claude_api_fact.signature.as_str(), "api_error");
|
|
165
|
+
assert_eq!(
|
|
166
|
+
claude_api_fact.turn_id.as_ref().map(|id| id.as_str()),
|
|
167
|
+
Some("sess-1")
|
|
168
|
+
);
|
|
169
|
+
assert_eq!(claude_api_fact.api_error_status, None);
|
|
170
|
+
assert_eq!(claude_api_fact.error.as_deref(), None);
|
|
171
|
+
assert_eq!(claude_api_fact.request_id.as_deref(), None);
|
|
172
|
+
assert_eq!(claude_api_fact.assistant_uuid.as_deref(), None);
|
|
173
|
+
|
|
174
|
+
let claude_400 = line(serde_json::json!({
|
|
175
|
+
"type": "assistant",
|
|
176
|
+
"parentUuid": "parent-400",
|
|
177
|
+
"uuid": "assistant-400",
|
|
178
|
+
"message": {"role": "assistant", "content": [
|
|
179
|
+
{"type": "text", "text": "API Error: 400 Team Agent truthsource forced invalid_request_error"}
|
|
180
|
+
]},
|
|
181
|
+
"error": "unknown",
|
|
182
|
+
"isApiErrorMessage": true,
|
|
183
|
+
"apiErrorStatus": 400,
|
|
184
|
+
"sessionId": "session-400",
|
|
185
|
+
"version": "2.1.181"
|
|
186
|
+
}));
|
|
187
|
+
let claude_400_fact =
|
|
188
|
+
crate::provider::latest_explicit_error_fact(Provider::ClaudeCode, &claude_400)
|
|
189
|
+
.expect("Claude assistant api error counts as explicit");
|
|
190
|
+
assert_claude_api_error_fact(
|
|
191
|
+
&claude_400_fact,
|
|
192
|
+
"assistant-400",
|
|
193
|
+
Some(400),
|
|
194
|
+
Some("unknown"),
|
|
195
|
+
None,
|
|
196
|
+
Some("assistant-400"),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
let claude_404 = line(serde_json::json!({
|
|
200
|
+
"type": "assistant",
|
|
201
|
+
"parentUuid": "parent-404",
|
|
202
|
+
"uuid": "assistant-404",
|
|
203
|
+
"message": {"role": "assistant", "content": [
|
|
204
|
+
{"type": "text", "text": "There's an issue with the selected model."}
|
|
205
|
+
]},
|
|
206
|
+
"error": "model_not_found",
|
|
207
|
+
"isApiErrorMessage": true,
|
|
208
|
+
"apiErrorStatus": 404,
|
|
209
|
+
"sessionId": "session-404",
|
|
210
|
+
"version": "2.1.181"
|
|
211
|
+
}));
|
|
212
|
+
let claude_404_fact =
|
|
213
|
+
crate::provider::latest_explicit_error_fact(Provider::ClaudeCode, &claude_404)
|
|
214
|
+
.expect("Claude assistant model_not_found counts as explicit");
|
|
215
|
+
assert_claude_api_error_fact(
|
|
216
|
+
&claude_404_fact,
|
|
217
|
+
"assistant-404",
|
|
218
|
+
Some(404),
|
|
219
|
+
Some("model_not_found"),
|
|
220
|
+
None,
|
|
221
|
+
Some("assistant-404"),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
let claude_real_request = line(serde_json::json!({
|
|
225
|
+
"type": "assistant",
|
|
226
|
+
"parentUuid": "parent-real",
|
|
227
|
+
"uuid": "assistant-real",
|
|
228
|
+
"requestId": "req_011CceNfWj2aPY5gtCdakULt",
|
|
229
|
+
"message": {"role": "assistant", "content": [
|
|
230
|
+
{"type": "text", "text": "There's an issue with the selected model."}
|
|
231
|
+
]},
|
|
232
|
+
"error": "model_not_found",
|
|
233
|
+
"isApiErrorMessage": true,
|
|
234
|
+
"apiErrorStatus": 404,
|
|
235
|
+
"sessionId": "session-real",
|
|
236
|
+
"version": "2.1.181"
|
|
237
|
+
}));
|
|
238
|
+
let claude_real_fact =
|
|
239
|
+
crate::provider::latest_explicit_error_fact(Provider::ClaudeCode, &claude_real_request)
|
|
240
|
+
.expect("Claude assistant api error with requestId counts as explicit");
|
|
241
|
+
assert_claude_api_error_fact(
|
|
242
|
+
&claude_real_fact,
|
|
243
|
+
"assistant-real",
|
|
244
|
+
Some(404),
|
|
245
|
+
Some("model_not_found"),
|
|
246
|
+
Some("req_011CceNfWj2aPY5gtCdakULt"),
|
|
247
|
+
Some("assistant-real"),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
let claude_tool_result = line(serde_json::json!({
|
|
251
|
+
"type": "user",
|
|
252
|
+
"message": {
|
|
253
|
+
"content": [
|
|
254
|
+
{"type": "tool_result", "is_error": true, "content": "permission denied"}
|
|
255
|
+
]
|
|
256
|
+
}
|
|
257
|
+
}));
|
|
258
|
+
assert!(
|
|
259
|
+
crate::provider::latest_explicit_error_fact(Provider::ClaudeCode, &claude_tool_result)
|
|
260
|
+
.is_none(),
|
|
261
|
+
"Claude tool_result is_error is an abnormal fact, but not an explicit provider-exit error"
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
let ordinary_assistant_text = line(serde_json::json!({
|
|
265
|
+
"type": "assistant",
|
|
266
|
+
"uuid": "assistant-text",
|
|
267
|
+
"message": {"role": "assistant", "content": [
|
|
268
|
+
{"type": "text", "text": "The command returned an error string."}
|
|
269
|
+
]},
|
|
270
|
+
"sessionId": "session-text"
|
|
271
|
+
}));
|
|
272
|
+
assert!(
|
|
273
|
+
crate::provider::latest_explicit_error_fact(Provider::ClaudeCode, &ordinary_assistant_text)
|
|
274
|
+
.is_none(),
|
|
275
|
+
"ordinary assistant text containing error is not a structured API error"
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
let interrupted = line(serde_json::json!({
|
|
279
|
+
"type": "user",
|
|
280
|
+
"uuid": "user-interrupt",
|
|
281
|
+
"message": {"content": [{"type": "text", "text": "[Request interrupted by user]"}]}
|
|
282
|
+
}));
|
|
283
|
+
assert!(
|
|
284
|
+
crate::provider::latest_explicit_error_fact(Provider::ClaudeCode, &interrupted).is_none(),
|
|
285
|
+
"user interruption is not a provider API error"
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
let empty_api_error_shape = line(serde_json::json!({
|
|
289
|
+
"type": "assistant",
|
|
290
|
+
"uuid": "assistant-empty",
|
|
291
|
+
"message": {"role": "assistant", "content": [
|
|
292
|
+
{"type": "text", "text": "API Error:"}
|
|
293
|
+
]},
|
|
294
|
+
"isApiErrorMessage": true,
|
|
295
|
+
"sessionId": "session-empty"
|
|
296
|
+
}));
|
|
297
|
+
assert!(
|
|
298
|
+
crate::provider::latest_explicit_error_fact(Provider::ClaudeCode, &empty_api_error_shape)
|
|
299
|
+
.is_none(),
|
|
300
|
+
"isApiErrorMessage without status/error/requestId is not enough"
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
let codex_failed = line(serde_json::json!({
|
|
304
|
+
"jsonrpc": "2.0",
|
|
305
|
+
"method": "turn/completed",
|
|
306
|
+
"params": {"turn": {"id": "turn-1", "status": "failed"}}
|
|
307
|
+
}));
|
|
308
|
+
let codex_fact = crate::provider::latest_explicit_error_fact(Provider::Codex, &codex_failed)
|
|
309
|
+
.expect("Codex failed turn/completed counts as explicit");
|
|
310
|
+
assert_eq!(codex_fact.signature.as_str(), "turn_failed");
|
|
311
|
+
assert_eq!(
|
|
312
|
+
codex_fact.turn_id.as_ref().map(|id| id.as_str()),
|
|
313
|
+
Some("turn-1")
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
let non_error = line(serde_json::json!({
|
|
317
|
+
"jsonrpc": "2.0",
|
|
318
|
+
"method": "turn/completed",
|
|
319
|
+
"params": {"turn": {"id": "turn-2", "status": "completed"}}
|
|
320
|
+
}));
|
|
321
|
+
assert!(
|
|
322
|
+
crate::provider::latest_explicit_error_fact(Provider::Codex, &non_error).is_none(),
|
|
323
|
+
"non-error tail must not count as latest explicit error"
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
fn line(value: serde_json::Value) -> String {
|
|
328
|
+
format!("{value}\n")
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
fn assert_claude_api_error_fact(
|
|
332
|
+
fact: &crate::provider::FaultFact,
|
|
333
|
+
turn_id: &str,
|
|
334
|
+
status: Option<i64>,
|
|
335
|
+
error: Option<&str>,
|
|
336
|
+
request_id: Option<&str>,
|
|
337
|
+
assistant_uuid: Option<&str>,
|
|
338
|
+
) {
|
|
339
|
+
assert_eq!(fact.signature.as_str(), "api_error");
|
|
340
|
+
assert_eq!(fact.kind, crate::provider::FactKind::Error);
|
|
341
|
+
assert_eq!(fact.turn_id.as_ref().map(|id| id.as_str()), Some(turn_id));
|
|
342
|
+
assert_eq!(fact.api_error_status, status);
|
|
343
|
+
assert_eq!(fact.error.as_deref(), error);
|
|
344
|
+
assert_eq!(fact.request_id.as_deref(), request_id);
|
|
345
|
+
assert_eq!(fact.assistant_uuid.as_deref(), assistant_uuid);
|
|
346
|
+
}
|
|
347
|
+
|
|
153
348
|
// ═════════════════════════════════════════════════════════════════════════
|
|
154
349
|
// GROUP F — detect_whole_team_gone (abnormal_track.py:91) — clean vs unexpected — RED
|
|
155
350
|
// ═════════════════════════════════════════════════════════════════════════
|