@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
|
@@ -18,7 +18,10 @@ fn tick_never_panics_returns_ok_tickreport_on_clean_state() {
|
|
|
18
18
|
let report = report.expect("tick must not Err on clean state");
|
|
19
19
|
assert!(report.ok, "clean tick is ok=true");
|
|
20
20
|
assert!(!report.stop, "clean tick does not stop the main loop");
|
|
21
|
-
assert_eq!(
|
|
21
|
+
assert_eq!(
|
|
22
|
+
report.reason, None,
|
|
23
|
+
"clean tick has no degraded/stop reason"
|
|
24
|
+
);
|
|
22
25
|
assert_eq!(report.persisted, Some(true), "clean tick persisted state");
|
|
23
26
|
}
|
|
24
27
|
|
|
@@ -29,15 +32,23 @@ fn tick_save_failure_returns_degraded_not_panic_not_err() {
|
|
|
29
32
|
// returned as Ok — NOT a panic, NOT an Err (the main loop must NOT catch+backoff this).
|
|
30
33
|
let (coord, _calls) = coord_for_test(true, Some(failing_save_hook()), None);
|
|
31
34
|
let report = coord.tick();
|
|
32
|
-
let report =
|
|
35
|
+
let report =
|
|
36
|
+
report.expect("save failure is a DEGRADED Ok, NOT an Err (main loop must not backoff)");
|
|
33
37
|
assert!(!report.ok, "bug-084: degraded => ok=false");
|
|
34
38
|
assert_eq!(
|
|
35
39
|
report.reason,
|
|
36
40
|
Some(TickStopReason::PersistenceDegraded),
|
|
37
41
|
"bug-084: reason=persistence_degraded"
|
|
38
42
|
);
|
|
39
|
-
assert_eq!(
|
|
40
|
-
|
|
43
|
+
assert_eq!(
|
|
44
|
+
report.persisted,
|
|
45
|
+
Some(false),
|
|
46
|
+
"bug-084: persisted=Some(false)"
|
|
47
|
+
);
|
|
48
|
+
assert!(
|
|
49
|
+
!report.stop,
|
|
50
|
+
"bug-084: stop=false (degrade, do NOT exit main loop)"
|
|
51
|
+
);
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
#[test]
|
|
@@ -49,9 +60,14 @@ fn tick_tmux_session_missing_returns_stop_true() {
|
|
|
49
60
|
// p2_tick_skips_tmux_gate_when_session_name_absent.)
|
|
50
61
|
let (coord, _calls) =
|
|
51
62
|
coord_for_test_with_session(/*session_present=*/ false, "team-missing");
|
|
52
|
-
let report = coord
|
|
63
|
+
let report = coord
|
|
64
|
+
.tick()
|
|
65
|
+
.expect("session-missing is a typed report, not an Err");
|
|
53
66
|
assert!(!report.ok, "session missing => ok=false");
|
|
54
|
-
assert!(
|
|
67
|
+
assert!(
|
|
68
|
+
report.stop,
|
|
69
|
+
"session missing => stop=true (break main loop)"
|
|
70
|
+
);
|
|
55
71
|
assert_eq!(
|
|
56
72
|
report.reason,
|
|
57
73
|
Some(TickStopReason::TmuxSessionMissing),
|
|
@@ -68,8 +84,7 @@ fn tick_side_effect_order_is_the_fixed_sequence() {
|
|
|
68
84
|
// detect_deadlocks -> detect_compaction -> detect_drift -> detect_api_errors ->
|
|
69
85
|
// ATOMIC_save (bug-084 wrap) -> collect_results -> prune_dedupe_log.
|
|
70
86
|
// The porter must push each step name into the injected recorder at its call site.
|
|
71
|
-
let recorder: OrderRecorder =
|
|
72
|
-
std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
|
87
|
+
let recorder: OrderRecorder = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
|
73
88
|
let (coord, _calls) = coord_for_test(true, None, Some(std::sync::Arc::clone(&recorder)));
|
|
74
89
|
let _ = coord.tick().expect("tick ok");
|
|
75
90
|
let order = recorder.lock().unwrap().clone();
|
|
@@ -101,11 +116,17 @@ fn tick_side_effect_order_is_the_fixed_sequence() {
|
|
|
101
116
|
"collect_results",
|
|
102
117
|
"prune_dedupe_log",
|
|
103
118
|
];
|
|
104
|
-
assert_eq!(
|
|
119
|
+
assert_eq!(
|
|
120
|
+
order, expected,
|
|
121
|
+
"tick side-effect ORDER must match the fixed sequence"
|
|
122
|
+
);
|
|
105
123
|
// ATOMIC save is the LAST mutation before read-only collect/prune (bug-084 wrap point).
|
|
106
124
|
let save_idx = order.iter().position(|s| *s == "atomic_save").unwrap();
|
|
107
125
|
let collect_idx = order.iter().position(|s| *s == "collect_results").unwrap();
|
|
108
|
-
assert!(
|
|
126
|
+
assert!(
|
|
127
|
+
save_idx < collect_idx,
|
|
128
|
+
"save precedes collect (bug-084 wrap is the last mutation)"
|
|
129
|
+
);
|
|
109
130
|
}
|
|
110
131
|
|
|
111
132
|
#[test]
|
|
@@ -132,7 +153,11 @@ fn health_ok_is_conjunction_of_running_metadata_ok_and_schema_ok() {
|
|
|
132
153
|
let (coord, _calls) = coord_for_test(true, None, None);
|
|
133
154
|
let h = coord.health().expect("health is a typed report");
|
|
134
155
|
assert!(!h.ok, "no pid file => not running => ok=false");
|
|
135
|
-
assert_eq!(
|
|
156
|
+
assert_eq!(
|
|
157
|
+
h.status,
|
|
158
|
+
CoordinatorHealthStatus::Missing,
|
|
159
|
+
"no pid => status=missing"
|
|
160
|
+
);
|
|
136
161
|
assert!(!h.metadata_ok, "no metadata => metadata_ok=false");
|
|
137
162
|
}
|
|
138
163
|
|
|
@@ -193,7 +218,10 @@ fn read_coordinator_metadata_missing_file_is_none() {
|
|
|
193
218
|
fn p2_tick_skips_tmux_gate_when_session_name_absent() {
|
|
194
219
|
let (coord, _calls) = coord_for_test(false, None, None);
|
|
195
220
|
let report = coord.tick().unwrap();
|
|
196
|
-
assert!(
|
|
221
|
+
assert!(
|
|
222
|
+
!report.stop,
|
|
223
|
+
"missing/null session_name must skip the gate, not stop the daemon"
|
|
224
|
+
);
|
|
197
225
|
assert_ne!(report.reason, Some(TickStopReason::TmuxSessionMissing));
|
|
198
226
|
}
|
|
199
227
|
|
|
@@ -223,8 +251,14 @@ fn p2_render_result_received_truncates_summary_to_80_chars() {
|
|
|
223
251
|
let long = "x".repeat(200);
|
|
224
252
|
let e = serde_json::json!({"event":"result_received","agent_id":"w1","summary": long});
|
|
225
253
|
let line = render_event_line(&e).expect("renders");
|
|
226
|
-
assert!(
|
|
227
|
-
|
|
254
|
+
assert!(
|
|
255
|
+
line.contains(&"x".repeat(80)),
|
|
256
|
+
"first 80 summary chars are kept"
|
|
257
|
+
);
|
|
258
|
+
assert!(
|
|
259
|
+
!line.contains(&"x".repeat(81)),
|
|
260
|
+
"summary must be truncated to 80 chars"
|
|
261
|
+
);
|
|
228
262
|
}
|
|
229
263
|
|
|
230
264
|
// P1 — idle_takeover.unknown_persistent must carry the auth_mode field (null when absent)
|
|
@@ -252,7 +286,8 @@ fn p2_unknown_persistent_event_serializes_auth_mode_key() {
|
|
|
252
286
|
#[test]
|
|
253
287
|
fn p2_abnormal_matches_lowercased_signature_too() {
|
|
254
288
|
let reg = MockRegistry::new(&[], &["timeout"]);
|
|
255
|
-
let records =
|
|
289
|
+
let records =
|
|
290
|
+
vec![serde_json::json!({"raw":"nothing here","signature":"TimeoutError","kind":"error"})];
|
|
256
291
|
let out = process_abnormal_records(
|
|
257
292
|
&records,
|
|
258
293
|
®,
|
|
@@ -9,38 +9,64 @@ use super::*;
|
|
|
9
9
|
fn render_result_received_collapses_whitespace_and_defaults() {
|
|
10
10
|
// watch.py:48-49 + _result_line — "result_received: <agent|-> -> <clean summary>".
|
|
11
11
|
let e = serde_json::json!({"event": "result_received", "agent_id": "w1", "summary": "did the\nthing"});
|
|
12
|
-
assert_eq!(
|
|
12
|
+
assert_eq!(
|
|
13
|
+
render_event_line(&e),
|
|
14
|
+
Some("result_received: w1 -> did the thing".to_string())
|
|
15
|
+
);
|
|
13
16
|
// missing agent + summary → both '-'.
|
|
14
17
|
let bare = serde_json::json!({"event": "result_received"});
|
|
15
|
-
assert_eq!(
|
|
18
|
+
assert_eq!(
|
|
19
|
+
render_event_line(&bare),
|
|
20
|
+
Some("result_received: - -> -".to_string())
|
|
21
|
+
);
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
#[test]
|
|
19
25
|
fn render_injected_and_submitted_share_prefix_and_truncate_message_id() {
|
|
20
26
|
// watch.py:50-51 — both render as "leader_receiver.injected"; message_id truncated to 12.
|
|
21
27
|
let inj = serde_json::json!({"event": "leader_receiver.injected", "message_id": "abcdef0123456789", "recipient": "w2"});
|
|
22
|
-
assert_eq!(
|
|
28
|
+
assert_eq!(
|
|
29
|
+
render_event_line(&inj),
|
|
30
|
+
Some("leader_receiver.injected: abcdef012345 -> w2".to_string())
|
|
31
|
+
);
|
|
23
32
|
// submitted uses same label, msg_id fallback, `to` recipient fallback.
|
|
24
33
|
let sub = serde_json::json!({"event": "leader_receiver.submitted", "msg_id": "xy", "to": "w3"});
|
|
25
|
-
assert_eq!(
|
|
34
|
+
assert_eq!(
|
|
35
|
+
render_event_line(&sub),
|
|
36
|
+
Some("leader_receiver.injected: xy -> w3".to_string())
|
|
37
|
+
);
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
#[test]
|
|
29
41
|
fn render_send_failed_uses_reason_then_error_fallback() {
|
|
30
42
|
// watch.py:52-53 — reason || error || '-', recipient || to || target || '-', whitespace-cleaned.
|
|
31
|
-
let with_reason =
|
|
32
|
-
|
|
43
|
+
let with_reason =
|
|
44
|
+
serde_json::json!({"event": "send.failed", "recipient": "w4", "reason": " pane gone "});
|
|
45
|
+
assert_eq!(
|
|
46
|
+
render_event_line(&with_reason),
|
|
47
|
+
Some("send.failed: w4 reason=pane gone".to_string())
|
|
48
|
+
);
|
|
33
49
|
let with_error = serde_json::json!({"event": "send.failed", "target": "w5", "error": "boom"});
|
|
34
|
-
assert_eq!(
|
|
50
|
+
assert_eq!(
|
|
51
|
+
render_event_line(&with_error),
|
|
52
|
+
Some("send.failed: w5 reason=boom".to_string())
|
|
53
|
+
);
|
|
35
54
|
}
|
|
36
55
|
|
|
37
56
|
#[test]
|
|
38
57
|
fn render_rebind_required_uses_pane_and_reason_fallbacks() {
|
|
39
58
|
// watch.py:54-57 — old_pane_id || pane_id || target || '-'; reason || rediscovery_status || '-'.
|
|
40
59
|
let e = serde_json::json!({"event": "leader_receiver.rebind_required", "old_pane_id": "%9", "reason": "lost"});
|
|
41
|
-
assert_eq!(
|
|
42
|
-
|
|
43
|
-
|
|
60
|
+
assert_eq!(
|
|
61
|
+
render_event_line(&e),
|
|
62
|
+
Some("leader_receiver.rebind_required: pane=%9 reason=lost".to_string())
|
|
63
|
+
);
|
|
64
|
+
let no_reason =
|
|
65
|
+
serde_json::json!({"event": "leader_receiver.rebind_required", "pane_id": "%7"});
|
|
66
|
+
assert_eq!(
|
|
67
|
+
render_event_line(&no_reason),
|
|
68
|
+
Some("leader_receiver.rebind_required: pane=%7 reason=-".to_string())
|
|
69
|
+
);
|
|
44
70
|
}
|
|
45
71
|
|
|
46
72
|
#[test]
|
|
@@ -49,16 +75,32 @@ fn render_api_error_defaults_unknown_class_and_dash() {
|
|
|
49
75
|
let error_class = ["Over", "loaded"].concat();
|
|
50
76
|
let code = (500 + 29).to_string();
|
|
51
77
|
let e = serde_json::json!({"event": "leader.api_error", "error_class": error_class, "provider": "claude_code", "matched_pattern_snippet": format!("{code} too many")});
|
|
52
|
-
assert_eq!(
|
|
78
|
+
assert_eq!(
|
|
79
|
+
render_event_line(&e),
|
|
80
|
+
Some(format!(
|
|
81
|
+
"leader.api_error: {} provider=claude_code snippet={} too many",
|
|
82
|
+
["Over", "loaded"].concat(),
|
|
83
|
+
code
|
|
84
|
+
))
|
|
85
|
+
);
|
|
53
86
|
let bare = serde_json::json!({"event": "leader.api_error"});
|
|
54
|
-
assert_eq!(
|
|
87
|
+
assert_eq!(
|
|
88
|
+
render_event_line(&bare),
|
|
89
|
+
Some("leader.api_error: Unknown provider=- snippet=-".to_string())
|
|
90
|
+
);
|
|
55
91
|
}
|
|
56
92
|
|
|
57
93
|
#[test]
|
|
58
94
|
fn render_non_renderable_events_return_none() {
|
|
59
95
|
// watch.py:63 — coordinator.* / unknown events → None.
|
|
60
|
-
assert_eq!(
|
|
61
|
-
|
|
96
|
+
assert_eq!(
|
|
97
|
+
render_event_line(&serde_json::json!({"event": "coordinator.boot"})),
|
|
98
|
+
None
|
|
99
|
+
);
|
|
100
|
+
assert_eq!(
|
|
101
|
+
render_event_line(&serde_json::json!({"event": "unknown.thing"})),
|
|
102
|
+
None
|
|
103
|
+
);
|
|
62
104
|
}
|
|
63
105
|
|
|
64
106
|
// ═════════════════════════════════════════════════════════════════════════
|
|
@@ -88,7 +130,8 @@ fn seed_watch_workspace(event_summary: &str, archive_bytes: Option<&[u8]>) -> Wo
|
|
|
88
130
|
));
|
|
89
131
|
let logs = crate::model::paths::logs_dir(&dir);
|
|
90
132
|
std::fs::create_dir_all(&logs).unwrap();
|
|
91
|
-
let line =
|
|
133
|
+
let line =
|
|
134
|
+
serde_json::json!({"event": "result_received", "agent_id": "w1", "summary": event_summary});
|
|
92
135
|
std::fs::write(
|
|
93
136
|
logs.join("events.jsonl"),
|
|
94
137
|
format!("{}\n", serde_json::to_string(&line).unwrap()),
|
|
@@ -125,14 +168,20 @@ fn watch_rotation_emits_marker_once_resets_offset_and_does_not_replay() {
|
|
|
125
168
|
"first segment line is rendered exactly once"
|
|
126
169
|
);
|
|
127
170
|
let offset_after_first = cursor.event_offset;
|
|
128
|
-
assert!(
|
|
171
|
+
assert!(
|
|
172
|
+
offset_after_first > 0,
|
|
173
|
+
"offset advanced past the consumed segment"
|
|
174
|
+
);
|
|
129
175
|
|
|
130
176
|
// Simulate rotation: an archive segment now appears (archive_signature changes from
|
|
131
177
|
// None -> Some), and the live events.jsonl is replaced with a fresh, SHORTER file
|
|
132
178
|
// whose new content must NOT include the archived "first segment".
|
|
133
179
|
let logs = crate::model::paths::logs_dir(ws.as_path());
|
|
134
|
-
std::fs::write(
|
|
135
|
-
.
|
|
180
|
+
std::fs::write(
|
|
181
|
+
logs.join("events.jsonl.1"),
|
|
182
|
+
b"archived old bytes that must never replay\n",
|
|
183
|
+
)
|
|
184
|
+
.unwrap();
|
|
136
185
|
let fresh = serde_json::json!({"event": "result_received", "agent_id": "w1", "summary": "post rotation"});
|
|
137
186
|
std::fs::write(
|
|
138
187
|
logs.join("events.jsonl"),
|
|
@@ -142,13 +191,18 @@ fn watch_rotation_emits_marker_once_resets_offset_and_does_not_replay() {
|
|
|
142
191
|
|
|
143
192
|
let second = collect_watch_lines(&ws, &mut cursor, &store, None).unwrap();
|
|
144
193
|
let marker_count = second.iter().filter(|l| **l == ROTATION_MARKER).count();
|
|
145
|
-
assert_eq!(
|
|
194
|
+
assert_eq!(
|
|
195
|
+
marker_count, 1,
|
|
196
|
+
"ROTATION_MARKER emitted exactly once on rotation"
|
|
197
|
+
);
|
|
146
198
|
assert!(
|
|
147
199
|
!second.iter().any(|l| l.contains("first segment")),
|
|
148
200
|
"archived segment is NOT replayed"
|
|
149
201
|
);
|
|
150
202
|
assert!(
|
|
151
|
-
!second
|
|
203
|
+
!second
|
|
204
|
+
.iter()
|
|
205
|
+
.any(|l| l.contains("old bytes that must never replay")),
|
|
152
206
|
"archive file contents are NEVER read/replayed"
|
|
153
207
|
);
|
|
154
208
|
assert!(
|
|
@@ -230,7 +230,12 @@ impl MessageStore {
|
|
|
230
230
|
|
|
231
231
|
/// `mark` (`core.py:116-138`) — the messages.status state machine (this slice
|
|
232
232
|
/// excludes the `delivery_tokens` side-effect, which is deferred).
|
|
233
|
-
pub fn mark(
|
|
233
|
+
pub fn mark(
|
|
234
|
+
&self,
|
|
235
|
+
message_id: &str,
|
|
236
|
+
status: &str,
|
|
237
|
+
error: Option<&str>,
|
|
238
|
+
) -> Result<(), MessageStoreError> {
|
|
234
239
|
let conn = crate::db::schema::open_db(&self.path)?;
|
|
235
240
|
let now = now_ts();
|
|
236
241
|
conn.execute(
|
|
@@ -594,7 +599,15 @@ mod tests {
|
|
|
594
599
|
fn create_message_inserts_accepted_row() {
|
|
595
600
|
let s = store();
|
|
596
601
|
let mid = s
|
|
597
|
-
.create_message(
|
|
602
|
+
.create_message(
|
|
603
|
+
Some("task_1"),
|
|
604
|
+
"leader",
|
|
605
|
+
"alice",
|
|
606
|
+
"hello",
|
|
607
|
+
None,
|
|
608
|
+
true,
|
|
609
|
+
Some("team_A"),
|
|
610
|
+
)
|
|
598
611
|
.unwrap();
|
|
599
612
|
assert!(mid.starts_with("msg_"), "id: {mid}");
|
|
600
613
|
assert_eq!(mid.len(), 16, "msg_ + 12 hex");
|
|
@@ -604,7 +617,10 @@ mod tests {
|
|
|
604
617
|
assert_eq!(col_str(&c, &mid, "task_id").as_deref(), Some("task_1"));
|
|
605
618
|
assert_eq!(col_str(&c, &mid, "sender").as_deref(), Some("leader"));
|
|
606
619
|
assert_eq!(col_str(&c, &mid, "recipient").as_deref(), Some("alice"));
|
|
607
|
-
assert_eq!(
|
|
620
|
+
assert_eq!(
|
|
621
|
+
col_str(&c, &mid, "owner_team_id").as_deref(),
|
|
622
|
+
Some("team_A")
|
|
623
|
+
);
|
|
608
624
|
assert_eq!(col_str(&c, &mid, "content").as_deref(), Some("hello"));
|
|
609
625
|
assert_eq!(col_str(&c, &mid, "artifact_refs").as_deref(), Some("[]"));
|
|
610
626
|
assert_eq!(col_str(&c, &mid, "reply_to"), None);
|
|
@@ -618,7 +634,9 @@ mod tests {
|
|
|
618
634
|
#[test]
|
|
619
635
|
fn create_message_no_task_no_owner_ack_false() {
|
|
620
636
|
let s = store();
|
|
621
|
-
let mid = s
|
|
637
|
+
let mid = s
|
|
638
|
+
.create_message(None, "leader", "bob", "hi", None, false, None)
|
|
639
|
+
.unwrap();
|
|
622
640
|
let c = read(&s);
|
|
623
641
|
assert_eq!(col_str(&c, &mid, "task_id"), None);
|
|
624
642
|
assert_eq!(col_str(&c, &mid, "owner_team_id"), None);
|
|
@@ -631,9 +649,14 @@ mod tests {
|
|
|
631
649
|
#[test]
|
|
632
650
|
fn claim_for_delivery_first_caller_wins() {
|
|
633
651
|
let s = store();
|
|
634
|
-
let mid = s
|
|
652
|
+
let mid = s
|
|
653
|
+
.create_message(Some("t"), "s", "r", "c", None, true, None)
|
|
654
|
+
.unwrap();
|
|
635
655
|
|
|
636
|
-
assert!(
|
|
656
|
+
assert!(
|
|
657
|
+
s.claim_for_delivery(&mid).unwrap(),
|
|
658
|
+
"accepted is eligible → claim wins"
|
|
659
|
+
);
|
|
637
660
|
let c = read(&s);
|
|
638
661
|
assert_eq!(status_of(&c, &mid), "target_resolved");
|
|
639
662
|
assert_eq!(col_i64(&c, &mid, "delivery_attempts"), 1);
|
|
@@ -644,9 +667,14 @@ mod tests {
|
|
|
644
667
|
// Atomic single-winner: once target_resolved, a re-claim returns false and
|
|
645
668
|
// must NOT bump delivery_attempts again.
|
|
646
669
|
let s = store();
|
|
647
|
-
let mid = s
|
|
670
|
+
let mid = s
|
|
671
|
+
.create_message(Some("t"), "s", "r", "c", None, true, None)
|
|
672
|
+
.unwrap();
|
|
648
673
|
assert!(s.claim_for_delivery(&mid).unwrap());
|
|
649
|
-
assert!(
|
|
674
|
+
assert!(
|
|
675
|
+
!s.claim_for_delivery(&mid).unwrap(),
|
|
676
|
+
"already target_resolved → no second winner"
|
|
677
|
+
);
|
|
650
678
|
|
|
651
679
|
let c = read(&s);
|
|
652
680
|
assert_eq!(status_of(&c, &mid), "target_resolved");
|
|
@@ -663,7 +691,9 @@ mod tests {
|
|
|
663
691
|
fn claim_for_delivery_ineligible_status_is_false() {
|
|
664
692
|
// 'failed' is not in the eligible set → claim returns false, status unchanged.
|
|
665
693
|
let s = store();
|
|
666
|
-
let mid = s
|
|
694
|
+
let mid = s
|
|
695
|
+
.create_message(Some("t"), "s", "r", "c", None, true, None)
|
|
696
|
+
.unwrap();
|
|
667
697
|
s.mark(&mid, "failed", Some("boom")).unwrap();
|
|
668
698
|
assert!(!s.claim_for_delivery(&mid).unwrap());
|
|
669
699
|
assert_eq!(status_of(&read(&s), &mid), "failed");
|
|
@@ -674,11 +704,16 @@ mod tests {
|
|
|
674
704
|
#[test]
|
|
675
705
|
fn mark_injected_sets_status_and_delivered_at() {
|
|
676
706
|
let s = store();
|
|
677
|
-
let mid = s
|
|
707
|
+
let mid = s
|
|
708
|
+
.create_message(Some("t"), "s", "r", "c", None, true, None)
|
|
709
|
+
.unwrap();
|
|
678
710
|
s.mark(&mid, "injected", None).unwrap();
|
|
679
711
|
let c = read(&s);
|
|
680
712
|
assert_eq!(status_of(&c, &mid), "injected");
|
|
681
|
-
assert!(
|
|
713
|
+
assert!(
|
|
714
|
+
col_str(&c, &mid, "delivered_at").is_some(),
|
|
715
|
+
"delivered_at set for injected"
|
|
716
|
+
);
|
|
682
717
|
assert_eq!(col_str(&c, &mid, "acknowledged_at"), None);
|
|
683
718
|
}
|
|
684
719
|
|
|
@@ -686,22 +721,34 @@ mod tests {
|
|
|
686
721
|
fn mark_acknowledged_is_sticky_against_delivery_statuses() {
|
|
687
722
|
// CASE guard: once acknowledged, marks to injected/visible/... are ignored.
|
|
688
723
|
let s = store();
|
|
689
|
-
let mid = s
|
|
724
|
+
let mid = s
|
|
725
|
+
.create_message(Some("t"), "s", "r", "c", None, true, None)
|
|
726
|
+
.unwrap();
|
|
690
727
|
s.mark(&mid, "acknowledged", None).unwrap();
|
|
691
728
|
assert_eq!(status_of(&read(&s), &mid), "acknowledged");
|
|
692
729
|
assert!(col_str(&read(&s), &mid, "acknowledged_at").is_some());
|
|
693
730
|
|
|
694
731
|
s.mark(&mid, "injected", None).unwrap();
|
|
695
|
-
assert_eq!(
|
|
732
|
+
assert_eq!(
|
|
733
|
+
status_of(&read(&s), &mid),
|
|
734
|
+
"acknowledged",
|
|
735
|
+
"injected ignored after ack"
|
|
736
|
+
);
|
|
696
737
|
s.mark(&mid, "visible", None).unwrap();
|
|
697
|
-
assert_eq!(
|
|
738
|
+
assert_eq!(
|
|
739
|
+
status_of(&read(&s), &mid),
|
|
740
|
+
"acknowledged",
|
|
741
|
+
"visible ignored after ack"
|
|
742
|
+
);
|
|
698
743
|
}
|
|
699
744
|
|
|
700
745
|
#[test]
|
|
701
746
|
fn mark_acknowledged_then_failed_overwrites() {
|
|
702
747
|
// 'failed' is NOT in the guarded delivery set → it overwrites acknowledged.
|
|
703
748
|
let s = store();
|
|
704
|
-
let mid = s
|
|
749
|
+
let mid = s
|
|
750
|
+
.create_message(Some("t"), "s", "r", "c", None, true, None)
|
|
751
|
+
.unwrap();
|
|
705
752
|
s.mark(&mid, "acknowledged", None).unwrap();
|
|
706
753
|
s.mark(&mid, "failed", Some("x")).unwrap();
|
|
707
754
|
assert_eq!(status_of(&read(&s), &mid), "failed");
|
|
@@ -710,7 +757,9 @@ mod tests {
|
|
|
710
757
|
#[test]
|
|
711
758
|
fn mark_overwrites_for_non_acknowledged() {
|
|
712
759
|
let s = store();
|
|
713
|
-
let mid = s
|
|
760
|
+
let mid = s
|
|
761
|
+
.create_message(Some("t"), "s", "r", "c", None, true, None)
|
|
762
|
+
.unwrap();
|
|
714
763
|
s.mark(&mid, "injected", None).unwrap();
|
|
715
764
|
s.mark(&mid, "visible", None).unwrap();
|
|
716
765
|
assert_eq!(status_of(&read(&s), &mid), "visible");
|
|
@@ -742,16 +791,31 @@ mod tests {
|
|
|
742
791
|
// and proposed id → second is deduped to the first winner.
|
|
743
792
|
let s = store();
|
|
744
793
|
let r1 = s
|
|
745
|
-
.claim_leader_notification_delivery(params(
|
|
794
|
+
.claim_leader_notification_delivery(params(
|
|
795
|
+
"res_1",
|
|
796
|
+
Some("team_A"),
|
|
797
|
+
Some(7),
|
|
798
|
+
Some("uuid-AAA"),
|
|
799
|
+
"msg_notif_1",
|
|
800
|
+
))
|
|
746
801
|
.unwrap();
|
|
747
802
|
assert_eq!(r1.status, "claimed_by_you");
|
|
748
803
|
assert_eq!(r1.notified_message_id, "msg_notif_1");
|
|
749
804
|
|
|
750
805
|
let r2 = s
|
|
751
|
-
.claim_leader_notification_delivery(params(
|
|
806
|
+
.claim_leader_notification_delivery(params(
|
|
807
|
+
"res_1",
|
|
808
|
+
Some("team_A"),
|
|
809
|
+
Some(7),
|
|
810
|
+
Some("uuid-BBB"),
|
|
811
|
+
"msg_notif_2",
|
|
812
|
+
))
|
|
752
813
|
.unwrap();
|
|
753
814
|
assert_eq!(r2.status, "already_notified_by");
|
|
754
|
-
assert_eq!(
|
|
815
|
+
assert_eq!(
|
|
816
|
+
r2.notified_message_id, "msg_notif_1",
|
|
817
|
+
"loser sees first winner's id, not its own"
|
|
818
|
+
);
|
|
755
819
|
|
|
756
820
|
// Exactly ONE row — carrying the FIRST caller's uuid (uuid not in the key).
|
|
757
821
|
assert_eq!(
|
|
@@ -769,12 +833,27 @@ mod tests {
|
|
|
769
833
|
#[test]
|
|
770
834
|
fn leader_notification_different_epoch_is_a_new_claim() {
|
|
771
835
|
let s = store();
|
|
772
|
-
s.claim_leader_notification_delivery(params(
|
|
773
|
-
|
|
836
|
+
s.claim_leader_notification_delivery(params(
|
|
837
|
+
"res_1",
|
|
838
|
+
Some("team_A"),
|
|
839
|
+
Some(7),
|
|
840
|
+
Some("uuid-AAA"),
|
|
841
|
+
"msg_notif_1",
|
|
842
|
+
))
|
|
843
|
+
.unwrap();
|
|
774
844
|
let r3 = s
|
|
775
|
-
.claim_leader_notification_delivery(params(
|
|
845
|
+
.claim_leader_notification_delivery(params(
|
|
846
|
+
"res_1",
|
|
847
|
+
Some("team_A"),
|
|
848
|
+
Some(8),
|
|
849
|
+
Some("uuid-AAA"),
|
|
850
|
+
"msg_notif_3",
|
|
851
|
+
))
|
|
776
852
|
.unwrap();
|
|
777
|
-
assert_eq!(
|
|
853
|
+
assert_eq!(
|
|
854
|
+
r3.status, "claimed_by_you",
|
|
855
|
+
"different owner_epoch → different PK → new claim"
|
|
856
|
+
);
|
|
778
857
|
|
|
779
858
|
let rows = notif_rows(&read(&s));
|
|
780
859
|
assert_eq!(rows.len(), 2);
|
|
@@ -789,20 +868,41 @@ mod tests {
|
|
|
789
868
|
// yields a different derived epoch → a new claim.
|
|
790
869
|
let s = store();
|
|
791
870
|
let a1 = s
|
|
792
|
-
.claim_leader_notification_delivery(params(
|
|
871
|
+
.claim_leader_notification_delivery(params(
|
|
872
|
+
"res_2",
|
|
873
|
+
Some("team_B"),
|
|
874
|
+
None,
|
|
875
|
+
Some("uuid-AAA"),
|
|
876
|
+
"m1",
|
|
877
|
+
))
|
|
793
878
|
.unwrap();
|
|
794
879
|
assert_eq!(a1.status, "claimed_by_you");
|
|
795
880
|
|
|
796
881
|
let a2 = s
|
|
797
|
-
.claim_leader_notification_delivery(params(
|
|
882
|
+
.claim_leader_notification_delivery(params(
|
|
883
|
+
"res_2",
|
|
884
|
+
Some("team_B"),
|
|
885
|
+
None,
|
|
886
|
+
Some("uuid-AAA"),
|
|
887
|
+
"m2",
|
|
888
|
+
))
|
|
798
889
|
.unwrap();
|
|
799
890
|
assert_eq!(a2.status, "already_notified_by");
|
|
800
891
|
assert_eq!(a2.notified_message_id, "m1");
|
|
801
892
|
|
|
802
893
|
let a3 = s
|
|
803
|
-
.claim_leader_notification_delivery(params(
|
|
894
|
+
.claim_leader_notification_delivery(params(
|
|
895
|
+
"res_2",
|
|
896
|
+
Some("team_B"),
|
|
897
|
+
None,
|
|
898
|
+
Some("uuid-BBB"),
|
|
899
|
+
"m3",
|
|
900
|
+
))
|
|
804
901
|
.unwrap();
|
|
805
|
-
assert_eq!(
|
|
902
|
+
assert_eq!(
|
|
903
|
+
a3.status, "claimed_by_you",
|
|
904
|
+
"different uuid → different derived epoch → new claim"
|
|
905
|
+
);
|
|
806
906
|
|
|
807
907
|
// Stored epochs equal the crc32 derivation of each uuid.
|
|
808
908
|
let rows = notif_rows(&read(&s));
|
|
@@ -829,7 +929,9 @@ mod tests {
|
|
|
829
929
|
let mut rows = c
|
|
830
930
|
.prepare("select a, b from peer_allowlist order by a, b")
|
|
831
931
|
.unwrap()
|
|
832
|
-
.query_map([], |row|
|
|
932
|
+
.query_map([], |row| {
|
|
933
|
+
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
|
934
|
+
})
|
|
833
935
|
.unwrap()
|
|
834
936
|
.collect::<Result<Vec<_>, _>>()
|
|
835
937
|
.unwrap();
|
|
@@ -851,7 +953,9 @@ mod tests {
|
|
|
851
953
|
#[test]
|
|
852
954
|
fn fix_b1_mark_preserves_existing_error_when_none_given() {
|
|
853
955
|
let s = store();
|
|
854
|
-
let mid = s
|
|
956
|
+
let mid = s
|
|
957
|
+
.create_message(Some("t"), "a", "b", "c", None, true, None)
|
|
958
|
+
.unwrap();
|
|
855
959
|
|
|
856
960
|
s.mark(&mid, "failed", Some("boom")).unwrap();
|
|
857
961
|
assert_eq!(col_str(&read(&s), &mid, "error").as_deref(), Some("boom"));
|
|
@@ -861,7 +965,11 @@ mod tests {
|
|
|
861
965
|
s.mark(&mid, "injected", None).unwrap();
|
|
862
966
|
let c = read(&s);
|
|
863
967
|
assert_eq!(status_of(&c, &mid), "injected");
|
|
864
|
-
assert_eq!(
|
|
968
|
+
assert_eq!(
|
|
969
|
+
col_str(&c, &mid, "error").as_deref(),
|
|
970
|
+
Some("boom"),
|
|
971
|
+
"existing error must survive a no-error mark"
|
|
972
|
+
);
|
|
865
973
|
|
|
866
974
|
// A NEW error overwrites.
|
|
867
975
|
s.mark(&mid, "failed", Some("second")).unwrap();
|