@team-agent/installer 0.2.11 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +744 -0
- package/Cargo.toml +34 -0
- package/crates/team-agent/Cargo.toml +33 -0
- package/crates/team-agent/src/cli/adapters.rs +1343 -0
- package/crates/team-agent/src/cli/diagnose.rs +554 -0
- package/crates/team-agent/src/cli/emit.rs +1077 -0
- package/crates/team-agent/src/cli/helpers.rs +88 -0
- package/crates/team-agent/src/cli/leader.rs +216 -0
- package/crates/team-agent/src/cli/mod.rs +1141 -0
- package/crates/team-agent/src/cli/profile.rs +306 -0
- package/crates/team-agent/src/cli/send.rs +215 -0
- package/crates/team-agent/src/cli/status.rs +179 -0
- package/crates/team-agent/src/cli/status_port.rs +502 -0
- package/crates/team-agent/src/cli/tests/base.rs +616 -0
- package/crates/team-agent/src/cli/tests/compile.rs +96 -0
- package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
- package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
- package/crates/team-agent/src/cli/tests/mod.rs +97 -0
- package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
- package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
- package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
- package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
- package/crates/team-agent/src/cli/types.rs +605 -0
- package/crates/team-agent/src/compiler/tests.rs +701 -0
- package/crates/team-agent/src/compiler.rs +489 -0
- package/crates/team-agent/src/coordinator/backoff.rs +153 -0
- package/crates/team-agent/src/coordinator/health.rs +436 -0
- package/crates/team-agent/src/coordinator/mod.rs +80 -0
- package/crates/team-agent/src/coordinator/orphan.rs +179 -0
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
- package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
- package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
- package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
- package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
- package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
- package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
- package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
- package/crates/team-agent/src/coordinator/tick.rs +2032 -0
- package/crates/team-agent/src/coordinator/types.rs +584 -0
- package/crates/team-agent/src/db/migration.rs +716 -0
- package/crates/team-agent/src/db/mod.rs +23 -0
- package/crates/team-agent/src/db/schema.rs +378 -0
- package/crates/team-agent/src/event_log.rs +375 -0
- package/crates/team-agent/src/fake_worker.rs +253 -0
- package/crates/team-agent/src/leader/helpers.rs +190 -0
- package/crates/team-agent/src/leader/inject.rs +33 -0
- package/crates/team-agent/src/leader/lease.rs +1063 -0
- package/crates/team-agent/src/leader/mod.rs +99 -0
- package/crates/team-agent/src/leader/owner_bind.rs +292 -0
- package/crates/team-agent/src/leader/rediscover/tests.rs +525 -0
- package/crates/team-agent/src/leader/rediscover.rs +1099 -0
- package/crates/team-agent/src/leader/start.rs +273 -0
- package/crates/team-agent/src/leader/takeover.rs +235 -0
- package/crates/team-agent/src/leader/tests/basics.rs +183 -0
- package/crates/team-agent/src/leader/tests/byte_findings.rs +234 -0
- package/crates/team-agent/src/leader/tests/identity.rs +206 -0
- package/crates/team-agent/src/leader/tests/idle.rs +271 -0
- package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
- package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -0
- package/crates/team-agent/src/leader/tests/mod.rs +125 -0
- package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
- package/crates/team-agent/src/leader/types.rs +487 -0
- package/crates/team-agent/src/lib.rs +85 -0
- package/crates/team-agent/src/lifecycle/display.rs +228 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
- package/crates/team-agent/src/lifecycle/launch.rs +1833 -0
- package/crates/team-agent/src/lifecycle/mod.rs +62 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
- package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
- package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
- package/crates/team-agent/src/lifecycle/restart.rs +76 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
- package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +933 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
- package/crates/team-agent/src/lifecycle/tests.rs +27 -0
- package/crates/team-agent/src/lifecycle/types.rs +685 -0
- package/crates/team-agent/src/main.rs +41 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
- package/crates/team-agent/src/mcp_server/mod.rs +183 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
- package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
- package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
- package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +159 -0
- package/crates/team-agent/src/mcp_server/tests.rs +38 -0
- package/crates/team-agent/src/mcp_server/tools.rs +603 -0
- package/crates/team-agent/src/mcp_server/types.rs +421 -0
- package/crates/team-agent/src/mcp_server/wire.rs +388 -0
- package/crates/team-agent/src/message_store.rs +767 -0
- package/crates/team-agent/src/messaging/activity.rs +433 -0
- package/crates/team-agent/src/messaging/delivery.rs +542 -0
- package/crates/team-agent/src/messaging/helpers.rs +209 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +340 -0
- package/crates/team-agent/src/messaging/mod.rs +147 -0
- package/crates/team-agent/src/messaging/peers.rs +32 -0
- package/crates/team-agent/src/messaging/results.rs +537 -0
- package/crates/team-agent/src/messaging/scheduler.rs +344 -0
- package/crates/team-agent/src/messaging/selftest.rs +100 -0
- package/crates/team-agent/src/messaging/send.rs +582 -0
- package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
- package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
- package/crates/team-agent/src/messaging/trust.rs +192 -0
- package/crates/team-agent/src/messaging/types.rs +355 -0
- package/crates/team-agent/src/messaging/watchers.rs +591 -0
- package/crates/team-agent/src/model/enums.rs +311 -0
- package/crates/team-agent/src/model/errors.rs +17 -0
- package/crates/team-agent/src/model/ids.rs +155 -0
- package/crates/team-agent/src/model/mod.rs +22 -0
- package/crates/team-agent/src/model/paths.rs +228 -0
- package/crates/team-agent/src/model/permissions.rs +567 -0
- package/crates/team-agent/src/model/routing.rs +340 -0
- package/crates/team-agent/src/model/spec.rs +680 -0
- package/crates/team-agent/src/model/task_graph.rs +380 -0
- package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
- package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
- package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
- package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
- package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
- package/crates/team-agent/src/model/yaml/tests.rs +288 -0
- package/crates/team-agent/src/model/yaml.rs +800 -0
- package/crates/team-agent/src/packaging/install.rs +305 -0
- package/crates/team-agent/src/packaging/migrate.rs +30 -0
- package/crates/team-agent/src/packaging/mod.rs +82 -0
- package/crates/team-agent/src/packaging/repair.rs +24 -0
- package/crates/team-agent/src/packaging/tests.rs +829 -0
- package/crates/team-agent/src/packaging/types.rs +369 -0
- package/crates/team-agent/src/provider/adapter.rs +801 -0
- package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
- package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
- package/crates/team-agent/src/provider/classify.rs +456 -0
- package/crates/team-agent/src/provider/faults.rs +136 -0
- package/crates/team-agent/src/provider/helpers.rs +41 -0
- package/crates/team-agent/src/provider/mod.rs +53 -0
- package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
- package/crates/team-agent/src/provider/tests/classify.rs +240 -0
- package/crates/team-agent/src/provider/tests/faults.rs +120 -0
- package/crates/team-agent/src/provider/tests/idle.rs +208 -0
- package/crates/team-agent/src/provider/tests/wire.rs +213 -0
- package/crates/team-agent/src/provider/tests.rs +31 -0
- package/crates/team-agent/src/provider/types.rs +424 -0
- package/crates/team-agent/src/state/identity.rs +656 -0
- package/crates/team-agent/src/state/mod.rs +58 -0
- package/crates/team-agent/src/state/owner_gate.rs +423 -0
- package/crates/team-agent/src/state/persist.rs +712 -0
- package/crates/team-agent/src/state/projection.rs +657 -0
- package/crates/team-agent/src/state/selector.rs +105 -0
- package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +586 -0
- package/crates/team-agent/src/tmux_backend.rs +758 -0
- package/crates/team-agent/src/transport/test_support.rs +252 -0
- package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
- package/crates/team-agent/src/transport/tests/mod.rs +199 -0
- package/crates/team-agent/src/transport/tests/wire.rs +527 -0
- package/crates/team-agent/src/transport.rs +774 -0
- package/npm/install.mjs +90 -106
- package/package.json +15 -13
- package/crates/team-agent-core/Cargo.toml +0 -12
- package/crates/team-agent-core/src/lib.rs +0 -332
- package/crates/team-agent-core/src/main.rs +0 -152
- package/pyproject.toml +0 -18
- package/scripts/install.py +0 -88
- package/scripts/run_regression_tests.py +0 -83
- package/src/team_agent/__init__.py +0 -3
- package/src/team_agent/__main__.py +0 -5
- package/src/team_agent/_legacy_pane_discovery.py +0 -186
- package/src/team_agent/abnormal_track.py +0 -253
- package/src/team_agent/approvals/__init__.py +0 -65
- package/src/team_agent/approvals/constants.py +0 -6
- package/src/team_agent/approvals/parsing.py +0 -176
- package/src/team_agent/approvals/runtime_prompts.py +0 -171
- package/src/team_agent/approvals/status.py +0 -176
- package/src/team_agent/cli/__init__.py +0 -137
- package/src/team_agent/cli/commands.py +0 -481
- package/src/team_agent/cli/e2e.py +0 -202
- package/src/team_agent/cli/helpers.py +0 -226
- package/src/team_agent/cli/parser.py +0 -540
- package/src/team_agent/compiler.py +0 -334
- package/src/team_agent/coordinator/__init__.py +0 -53
- package/src/team_agent/coordinator/__main__.py +0 -119
- package/src/team_agent/coordinator/lifecycle.py +0 -411
- package/src/team_agent/coordinator/metadata.py +0 -61
- package/src/team_agent/coordinator/paths.py +0 -17
- package/src/team_agent/diagnose/__init__.py +0 -48
- package/src/team_agent/diagnose/checks.py +0 -101
- package/src/team_agent/diagnose/comms.py +0 -213
- package/src/team_agent/diagnose/health.py +0 -241
- package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
- package/src/team_agent/diagnose/preflight.py +0 -194
- package/src/team_agent/diagnose/quick_start.py +0 -324
- package/src/team_agent/display/__init__.py +0 -92
- package/src/team_agent/display/adaptive.py +0 -511
- package/src/team_agent/display/backend.py +0 -46
- package/src/team_agent/display/close.py +0 -154
- package/src/team_agent/display/ghostty.py +0 -77
- package/src/team_agent/display/rebuild.py +0 -102
- package/src/team_agent/display/tiling.py +0 -156
- package/src/team_agent/display/worker_window.py +0 -114
- package/src/team_agent/display/workspace.py +0 -382
- package/src/team_agent/errors.py +0 -10
- package/src/team_agent/events.py +0 -84
- package/src/team_agent/fake_worker.py +0 -80
- package/src/team_agent/idle_predicate.py +0 -218
- package/src/team_agent/idle_takeover.py +0 -59
- package/src/team_agent/idle_takeover_wiring.py +0 -114
- package/src/team_agent/launch/__init__.py +0 -41
- package/src/team_agent/launch/bootstrap.py +0 -85
- package/src/team_agent/launch/config.py +0 -106
- package/src/team_agent/launch/core.py +0 -301
- package/src/team_agent/launch/requirements.py +0 -57
- package/src/team_agent/leader/__init__.py +0 -926
- package/src/team_agent/leader_binding.py +0 -183
- package/src/team_agent/lifecycle/__init__.py +0 -5
- package/src/team_agent/lifecycle/agents.py +0 -278
- package/src/team_agent/lifecycle/operations.py +0 -411
- package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
- package/src/team_agent/lifecycle/start.py +0 -363
- package/src/team_agent/mcp_server/__init__.py +0 -42
- package/src/team_agent/mcp_server/__main__.py +0 -7
- package/src/team_agent/mcp_server/contracts.py +0 -148
- package/src/team_agent/mcp_server/normalize.py +0 -257
- package/src/team_agent/mcp_server/server.py +0 -150
- package/src/team_agent/mcp_server/tools.py +0 -352
- package/src/team_agent/message_store/__init__.py +0 -23
- package/src/team_agent/message_store/agent_health.py +0 -113
- package/src/team_agent/message_store/core.py +0 -497
- package/src/team_agent/message_store/leader_notification_log.py +0 -198
- package/src/team_agent/message_store/result_watchers.py +0 -251
- package/src/team_agent/message_store/schema.py +0 -308
- package/src/team_agent/message_store/schema_migration.py +0 -448
- package/src/team_agent/messaging/__init__.py +0 -1
- package/src/team_agent/messaging/activity_detector.py +0 -262
- package/src/team_agent/messaging/delivery.py +0 -504
- package/src/team_agent/messaging/deps.py +0 -247
- package/src/team_agent/messaging/idle_alerts.py +0 -423
- package/src/team_agent/messaging/internal_delivery.py +0 -46
- package/src/team_agent/messaging/leader.py +0 -497
- package/src/team_agent/messaging/leader_api_errors.py +0 -216
- package/src/team_agent/messaging/leader_panes.py +0 -673
- package/src/team_agent/messaging/owner_bypass.py +0 -29
- package/src/team_agent/messaging/result_delivery.py +0 -539
- package/src/team_agent/messaging/results.py +0 -447
- package/src/team_agent/messaging/scheduler.py +0 -450
- package/src/team_agent/messaging/send.py +0 -532
- package/src/team_agent/messaging/session_drift.py +0 -94
- package/src/team_agent/messaging/tmux_io.py +0 -506
- package/src/team_agent/messaging/tmux_prompt.py +0 -338
- package/src/team_agent/messaging/trust_auto_answer.py +0 -52
- package/src/team_agent/orchestrator/__init__.py +0 -376
- package/src/team_agent/orchestrator/plan.py +0 -122
- package/src/team_agent/orchestrator/state.py +0 -128
- package/src/team_agent/paths.py +0 -45
- package/src/team_agent/permissions.py +0 -123
- package/src/team_agent/profiles/__init__.py +0 -82
- package/src/team_agent/profiles/constants.py +0 -19
- package/src/team_agent/profiles/core.py +0 -407
- package/src/team_agent/profiles/helpers.py +0 -69
- package/src/team_agent/profiles/provider_env.py +0 -188
- package/src/team_agent/profiles/smoke.py +0 -201
- package/src/team_agent/provider_cli/__init__.py +0 -43
- package/src/team_agent/provider_cli/adapter.py +0 -172
- package/src/team_agent/provider_cli/base.py +0 -48
- package/src/team_agent/provider_cli/claude.py +0 -503
- package/src/team_agent/provider_cli/codex.py +0 -336
- package/src/team_agent/provider_cli/copilot.py +0 -8
- package/src/team_agent/provider_cli/fake.py +0 -39
- package/src/team_agent/provider_cli/gemini.py +0 -95
- package/src/team_agent/provider_cli/opencode.py +0 -8
- package/src/team_agent/provider_cli/prompt.py +0 -62
- package/src/team_agent/provider_cli/registry.py +0 -18
- package/src/team_agent/provider_cli/unsupported.py +0 -32
- package/src/team_agent/provider_state/README.md +0 -78
- package/src/team_agent/provider_state/__init__.py +0 -91
- package/src/team_agent/provider_state/claude.py +0 -86
- package/src/team_agent/provider_state/codex.py +0 -84
- package/src/team_agent/provider_state/common.py +0 -207
- package/src/team_agent/provider_state/registry.py +0 -118
- package/src/team_agent/providers.py +0 -163
- package/src/team_agent/quality_gates.py +0 -104
- package/src/team_agent/restart/__init__.py +0 -34
- package/src/team_agent/restart/orchestration.py +0 -554
- package/src/team_agent/restart/selection.py +0 -89
- package/src/team_agent/restart/snapshot.py +0 -70
- package/src/team_agent/routing.py +0 -84
- package/src/team_agent/runtime.py +0 -1243
- package/src/team_agent/rust_core.py +0 -327
- package/src/team_agent/sessions/__init__.py +0 -25
- package/src/team_agent/sessions/capture.py +0 -144
- package/src/team_agent/sessions/inventory.py +0 -44
- package/src/team_agent/sessions/resume.py +0 -135
- package/src/team_agent/simple_yaml.py +0 -236
- package/src/team_agent/spec.py +0 -370
- package/src/team_agent/state.py +0 -693
- package/src/team_agent/status/__init__.py +0 -63
- package/src/team_agent/status/approvals.py +0 -52
- package/src/team_agent/status/compact.py +0 -158
- package/src/team_agent/status/constants.py +0 -18
- package/src/team_agent/status/inbox.py +0 -58
- package/src/team_agent/status/peek.py +0 -117
- package/src/team_agent/status/queries.py +0 -199
- package/src/team_agent/task_graph.py +0 -80
- package/src/team_agent/terminal.py +0 -57
- package/src/team_agent/wake.py +0 -58
- package/src/team_agent/watch/__init__.py +0 -145
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
4
|
+
// SPINE SLICE-2a RED — capture-based health-sync obligations (sync_health / refresh_statuses
|
|
5
|
+
// + capture_missing). These are bare record_step() probes today (tick.rs `TODO(spine slice 2):
|
|
6
|
+
// wire via capture seam`), so the daemon does not read live scrollback or update agent status.
|
|
7
|
+
// Golden: coordinator/lifecycle.py → approvals/status.py sync_agent_health /
|
|
8
|
+
// refresh_agent_runtime_statuses (capture pane → classify_agent_activity → activity/status +
|
|
9
|
+
// last_output) and sessions/capture.py capture_missing_sessions (no session_id + transcript →
|
|
10
|
+
// capture_session_id). §11 iron law (bug-071/077/085): an UNKNOWN scrollback is NEVER idle.
|
|
11
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
12
|
+
|
|
13
|
+
/// The CAPTURE SEAM (test side): a transport whose `capture()` returns SEEDED scrollback, so a test
|
|
14
|
+
/// can stage exactly what a worker's pane shows. The porter wires tick to call transport.capture per
|
|
15
|
+
/// agent. `has_session`→true (gate passes); `inject`→Ok (delivery may run); the rest are Ok defaults.
|
|
16
|
+
struct CapturingTransport {
|
|
17
|
+
scrollback: String,
|
|
18
|
+
}
|
|
19
|
+
impl Transport for CapturingTransport {
|
|
20
|
+
fn kind(&self) -> BackendKind {
|
|
21
|
+
BackendKind::Tmux
|
|
22
|
+
}
|
|
23
|
+
fn spawn_first(&self, _s: &SessionName, _w: &WindowName, _a: &[String], _c: &std::path::Path, _e: &std::collections::BTreeMap<String, String>) -> Result<SpawnResult, TransportError> {
|
|
24
|
+
unimplemented!("not reached")
|
|
25
|
+
}
|
|
26
|
+
fn spawn_into(&self, _s: &SessionName, _w: &WindowName, _a: &[String], _c: &std::path::Path, _e: &std::collections::BTreeMap<String, String>) -> Result<SpawnResult, TransportError> {
|
|
27
|
+
unimplemented!("not reached")
|
|
28
|
+
}
|
|
29
|
+
fn inject(&self, _t: &Target, _p: &InjectPayload, _s: Key, _b: bool) -> Result<InjectReport, TransportError> {
|
|
30
|
+
Ok(InjectReport {
|
|
31
|
+
stage_reached: crate::transport::InjectStage::Submit,
|
|
32
|
+
inject_verification: crate::transport::InjectVerification::CaptureContainsToken,
|
|
33
|
+
submit_verification: crate::transport::SubmitVerification::EnterSentWithoutPlaceholderCheck,
|
|
34
|
+
turn_verification: crate::transport::TurnVerification::NotYetObserved,
|
|
35
|
+
attempts: 1,
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
fn send_keys(&self, _t: &Target, _k: &[Key]) -> Result<(), TransportError> {
|
|
39
|
+
Ok(())
|
|
40
|
+
}
|
|
41
|
+
fn capture(&self, _t: &Target, range: CaptureRange) -> Result<CapturedText, TransportError> {
|
|
42
|
+
Ok(CapturedText { text: self.scrollback.clone(), range })
|
|
43
|
+
}
|
|
44
|
+
fn query(&self, _t: &Target, _f: PaneField) -> Result<Option<String>, TransportError> {
|
|
45
|
+
Ok(None)
|
|
46
|
+
}
|
|
47
|
+
fn liveness(&self, _p: &PaneId) -> Result<PaneLiveness, TransportError> {
|
|
48
|
+
Ok(PaneLiveness::Live)
|
|
49
|
+
}
|
|
50
|
+
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
51
|
+
Ok(Vec::new())
|
|
52
|
+
}
|
|
53
|
+
fn has_session(&self, _s: &SessionName) -> Result<bool, TransportError> {
|
|
54
|
+
Ok(true)
|
|
55
|
+
}
|
|
56
|
+
fn list_windows(&self, _s: &SessionName) -> Result<Vec<WindowName>, TransportError> {
|
|
57
|
+
Ok(vec![WindowName::new("w1")])
|
|
58
|
+
}
|
|
59
|
+
fn set_session_env(&self, _s: &SessionName, _k: &str, _v: &str) -> Result<SetEnvOutcome, TransportError> {
|
|
60
|
+
Ok(SetEnvOutcome::Applied)
|
|
61
|
+
}
|
|
62
|
+
fn kill_session(&self, _s: &SessionName) -> Result<(), TransportError> {
|
|
63
|
+
Ok(())
|
|
64
|
+
}
|
|
65
|
+
fn kill_window(&self, _t: &Target) -> Result<(), TransportError> {
|
|
66
|
+
Ok(())
|
|
67
|
+
}
|
|
68
|
+
fn attach_session(&self, _s: &SessionName) -> Result<AttachOutcome, TransportError> {
|
|
69
|
+
Ok(AttachOutcome::Attached)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Build a Coordinator over a real seeded workspace (truthy session_name + the given agents map) with
|
|
74
|
+
/// the CapturingTransport staging `scrollback` for every pane. Returns the workspace dir so the test
|
|
75
|
+
/// can load_runtime_state after the tick.
|
|
76
|
+
fn seeded_health_coord(agents: serde_json::Value, scrollback: &str) -> (Coordinator, std::path::PathBuf) {
|
|
77
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
78
|
+
static N: AtomicU64 = AtomicU64::new(0);
|
|
79
|
+
let dir = std::env::temp_dir().join(format!(
|
|
80
|
+
"ta-rs-health-{}-{}",
|
|
81
|
+
std::process::id(),
|
|
82
|
+
N.fetch_add(1, Ordering::Relaxed)
|
|
83
|
+
));
|
|
84
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
85
|
+
crate::state::persist::save_runtime_state(
|
|
86
|
+
&dir,
|
|
87
|
+
&serde_json::json!({ "session_name": "team-health", "agents": agents }),
|
|
88
|
+
)
|
|
89
|
+
.unwrap();
|
|
90
|
+
let ws = WorkspacePath::new(dir.clone());
|
|
91
|
+
let reg: Box<dyn ProviderRegistry> = Box::new(MockRegistry::new(&[], &[]));
|
|
92
|
+
let coord = Coordinator::for_test(
|
|
93
|
+
ws,
|
|
94
|
+
reg,
|
|
95
|
+
Box::new(CapturingTransport { scrollback: scrollback.to_string() }),
|
|
96
|
+
None,
|
|
97
|
+
None,
|
|
98
|
+
);
|
|
99
|
+
(coord, dir)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn agent_activity_status(dir: &std::path::Path, agent: &str) -> Option<String> {
|
|
103
|
+
let state = crate::state::persist::load_runtime_state(dir).ok()?;
|
|
104
|
+
state
|
|
105
|
+
.get("agents")?
|
|
106
|
+
.get(agent)?
|
|
107
|
+
.get("activity")?
|
|
108
|
+
.get("status")?
|
|
109
|
+
.as_str()
|
|
110
|
+
.map(str::to_string)
|
|
111
|
+
}
|
|
112
|
+
fn agent_field(dir: &std::path::Path, agent: &str, field: &str) -> Option<serde_json::Value> {
|
|
113
|
+
let state = crate::state::persist::load_runtime_state(dir).ok()?;
|
|
114
|
+
state.get("agents")?.get(agent)?.get(field).cloned()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fn one_agent(provider: &str) -> serde_json::Value {
|
|
118
|
+
serde_json::json!({ "w1": { "provider": provider, "window": "w1", "pane_id": "%1" } })
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// P0 §11 — an IDLE-prompt scrollback must classify the agent idle (golden classify_agent_activity →
|
|
122
|
+
// state.agents[w1].activity). Today the obligation is a probe → no activity written.
|
|
123
|
+
#[test]
|
|
124
|
+
fn spine2_sync_health_classifies_idle_scrollback() {
|
|
125
|
+
let (coord, dir) = seeded_health_coord(one_agent("codex"), "previous output\n❯\n");
|
|
126
|
+
coord.tick().expect("tick");
|
|
127
|
+
assert_eq!(
|
|
128
|
+
agent_activity_status(&dir, "w1").as_deref(),
|
|
129
|
+
Some("idle"),
|
|
130
|
+
"an idle-prompt scrollback must classify the agent idle (sync_health writes state.agents[w1].activity)"
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// P0 §11 IRON LAW (bug-071/077/085) — an UNKNOWN/unrecognized scrollback must classify the agent but
|
|
135
|
+
// NEVER as idle. Today: no activity written.
|
|
136
|
+
#[test]
|
|
137
|
+
fn spine2_sync_health_unknown_scrollback_never_idle() {
|
|
138
|
+
let (coord, dir) = seeded_health_coord(one_agent("codex"), "garbled noise xyz 12345 no recognizable signal");
|
|
139
|
+
coord.tick().expect("tick");
|
|
140
|
+
let status = agent_activity_status(&dir, "w1");
|
|
141
|
+
assert!(status.is_some(), "sync_health must classify the agent (write activity); today the probe writes nothing. got {status:?}");
|
|
142
|
+
assert_ne!(status.as_deref(), Some("idle"), "§11: an UNKNOWN scrollback must NEVER be classified idle");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// P0 §11 — a WORKING scrollback classifies the agent but never idle.
|
|
146
|
+
#[test]
|
|
147
|
+
fn spine2_sync_health_working_scrollback_never_idle() {
|
|
148
|
+
let (coord, dir) = seeded_health_coord(one_agent("codex"), "Working (5s · esc to interrupt)");
|
|
149
|
+
coord.tick().expect("tick");
|
|
150
|
+
let status = agent_activity_status(&dir, "w1");
|
|
151
|
+
assert!(status.is_some(), "sync_health must classify a working agent; today no activity. got {status:?}");
|
|
152
|
+
assert_ne!(status.as_deref(), Some("idle"), "§11: a WORKING scrollback must not be idle");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// P1 — sync_health records last_output_at on a pane delta (so detect_stuck / take-over downstream can
|
|
156
|
+
// use it). Golden approvals/status.py:sync_agent_health. Today: probe writes nothing.
|
|
157
|
+
#[test]
|
|
158
|
+
fn spine2_sync_health_records_last_output_at() {
|
|
159
|
+
let (coord, dir) = seeded_health_coord(one_agent("codex"), "some fresh pane output");
|
|
160
|
+
coord.tick().expect("tick");
|
|
161
|
+
assert!(
|
|
162
|
+
agent_field(&dir, "w1", "last_output_at").is_some(),
|
|
163
|
+
"sync_health must record last_output_at on a pane delta; today the probe writes nothing"
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// P1 — capture_missing: an agent with NO session_id but a discoverable transcript under spawn_cwd gets
|
|
168
|
+
// its session_id captured + persisted (real capture_session_id); an agent that already has one is
|
|
169
|
+
// untouched. Golden sessions/capture.py:capture_missing_sessions.
|
|
170
|
+
#[test]
|
|
171
|
+
fn spine2_capture_missing_captures_session_id_for_missing_agent() {
|
|
172
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
173
|
+
static N: AtomicU64 = AtomicU64::new(0);
|
|
174
|
+
let tdir = std::env::temp_dir().join(format!("ta-rs-health-tx-{}-{}", std::process::id(), N.fetch_add(1, Ordering::Relaxed)));
|
|
175
|
+
std::fs::create_dir_all(&tdir).unwrap();
|
|
176
|
+
std::fs::write(
|
|
177
|
+
tdir.join("session.jsonl"),
|
|
178
|
+
r#"{"type":"user","sessionId":"sess-found","cwd":"x","message":{"content":"hi"}}"#,
|
|
179
|
+
)
|
|
180
|
+
.unwrap();
|
|
181
|
+
let agents = serde_json::json!({
|
|
182
|
+
"w1": { "provider": "claude_code", "window": "w1", "spawn_cwd": tdir.to_string_lossy() },
|
|
183
|
+
"w2": { "provider": "claude_code", "window": "w2", "session_id": "existing-sess" },
|
|
184
|
+
});
|
|
185
|
+
let (coord, dir) = seeded_health_coord(agents, "");
|
|
186
|
+
coord.tick().expect("tick");
|
|
187
|
+
assert!(
|
|
188
|
+
agent_field(&dir, "w1", "session_id").and_then(|v| v.as_str().map(str::to_string)).is_some(),
|
|
189
|
+
"capture_missing must capture+persist a session_id for an agent with a discoverable transcript; today it's a probe"
|
|
190
|
+
);
|
|
191
|
+
assert_eq!(
|
|
192
|
+
agent_field(&dir, "w2", "session_id").and_then(|v| v.as_str().map(str::to_string)).as_deref(),
|
|
193
|
+
Some("existing-sess"),
|
|
194
|
+
"an agent that already has a session_id must be untouched"
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// CONTRACT — sync_health runs BEFORE deliver_pending, but turn-level WORKING state must not make an
|
|
199
|
+
// alive worker undeliverable. Busy delivery deferral is lifecycle-only (`state.agents[id].status=="busy"`);
|
|
200
|
+
// activity/agent_health WORKING remains diagnostic turn state.
|
|
201
|
+
#[test]
|
|
202
|
+
fn spine2_sync_health_working_status_delivers_same_tick() {
|
|
203
|
+
let (coord, dir) = seeded_health_coord(one_agent("codex"), "Working (5s · esc to interrupt)");
|
|
204
|
+
let store = MessageStore::open(&dir).unwrap();
|
|
205
|
+
let mid = store.create_message(Some("t"), "leader", "w1", "hi", None, true, None).unwrap();
|
|
206
|
+
let conn = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
207
|
+
conn.execute(
|
|
208
|
+
"insert into agent_health(owner_team_id, agent_id, status, updated_at)
|
|
209
|
+
values (?1, ?2, ?3, ?4)
|
|
210
|
+
on conflict(owner_team_id, agent_id) do update set
|
|
211
|
+
status = excluded.status,
|
|
212
|
+
updated_at = excluded.updated_at",
|
|
213
|
+
rusqlite::params!["current", "w1", "WORKING", chrono::Utc::now().to_rfc3339()],
|
|
214
|
+
)
|
|
215
|
+
.unwrap();
|
|
216
|
+
drop(store);
|
|
217
|
+
coord.tick().expect("tick");
|
|
218
|
+
let events = read_event_log_dir(&dir);
|
|
219
|
+
assert!(
|
|
220
|
+
!events.iter().any(|e| e.get("event").and_then(|v| v.as_str()) == Some("send.deferred_busy")),
|
|
221
|
+
"turn-level WORKING must not trigger lifecycle busy deferral; got {events:?}"
|
|
222
|
+
);
|
|
223
|
+
assert!(
|
|
224
|
+
events.iter().any(|e| {
|
|
225
|
+
e.get("event").and_then(|v| v.as_str()) == Some("message.delivered")
|
|
226
|
+
&& e.get("message_id").and_then(|v| v.as_str()) == Some(mid.as_str())
|
|
227
|
+
}),
|
|
228
|
+
"alive worker with WORKING turn state must still receive the pending message; got {events:?}"
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ADVERSARIAL (real-machine-driven; catches porter fix 1f97163 re-coupling): a worker classified WORKING
|
|
233
|
+
// by sync_health is STILL ALIVE (lifecycle status running) and MUST remain deliverable. golden never maps
|
|
234
|
+
// turn activity to lifecycle status (status is only running/stopped; the status=="busy" gate is vestigial/
|
|
235
|
+
// unreachable — golden delivers to alive workers regardless of activity). The porter fix (write_activity,
|
|
236
|
+
// tick.rs:858) maps activity=Working -> status="busy", re-coupling turn state into lifecycle status — it
|
|
237
|
+
// just MOVED the deferral from agent_health to status, re-introducing the regression (fake workers are
|
|
238
|
+
// permanently WORKING -> permanently status=busy -> permanently deferred -> round-trip never closes).
|
|
239
|
+
// The synthetic contract REDs seed status=running directly and skip the tick, so they don't catch this;
|
|
240
|
+
// this drives the REAL coordinator tick. (Contradicts the stale lock
|
|
241
|
+
// spine2_sync_health_busy_status_defers_delivery_same_tick, which encodes the regression behavior and
|
|
242
|
+
// must be reconciled to this contract.)
|
|
243
|
+
#[test]
|
|
244
|
+
fn contract_working_worker_stays_alive_and_deliverable_in_real_tick() {
|
|
245
|
+
let (coord, dir) = seeded_health_coord(one_agent("codex"), "Working (5s · esc to interrupt)");
|
|
246
|
+
let store = MessageStore::open(&dir).unwrap();
|
|
247
|
+
let _mid = store.create_message(Some("t"), "leader", "w1", "hi", None, true, None).unwrap();
|
|
248
|
+
drop(store);
|
|
249
|
+
coord.tick().expect("tick");
|
|
250
|
+
let status = agent_field(&dir, "w1", "status").and_then(|v| v.as_str().map(str::to_string));
|
|
251
|
+
assert_ne!(
|
|
252
|
+
status.as_deref(),
|
|
253
|
+
Some("busy"),
|
|
254
|
+
"CONTRACT: sync_health must NOT write lifecycle status='busy' from turn activity=Working (golden never \
|
|
255
|
+
maps activity->status); turn state belongs in agent['activity']/agent_health only. got status={status:?}"
|
|
256
|
+
);
|
|
257
|
+
let events = read_event_log_dir(&dir);
|
|
258
|
+
assert!(
|
|
259
|
+
!events.iter().any(|e| e.get("event").and_then(|v| v.as_str()) == Some("send.deferred_busy")),
|
|
260
|
+
"CONTRACT: an alive worker (lifecycle running) classified WORKING must still receive delivery, not \
|
|
261
|
+
deferred_busy (golden delivers; fake workers are permanently WORKING). got {events:?}"
|
|
262
|
+
);
|
|
263
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
use super::spine::{message_status, DeliveringTransport};
|
|
3
|
+
|
|
4
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
5
|
+
// P0 REGRESSION (root cause pinned) — a per-agent capture/health-check FAILURE must not abort the
|
|
6
|
+
// whole tick. rt-host-a baseline @ ea4ba97: after `stop-agent w1`, `send w2` stays status='accepted'
|
|
7
|
+
// with delivery_attempts=0 while the coordinator stays alive. Root cause: sync_agent_health
|
|
8
|
+
// (tick.rs) does `self.transport.capture(&target, ...)?` per agent — when the stopped w1's window is
|
|
9
|
+
// gone the capture ERRORS and the `?` propagates -> sync_agent_health returns Err -> the tick
|
|
10
|
+
// early-returns Err BEFORE deliver_pending_messages -> the deliver loop never runs for the active w2.
|
|
11
|
+
// Contract: the tick must SWALLOW a per-agent capture/health failure (log + continue) and still run
|
|
12
|
+
// deliver_pending_messages for the other agents. Unit repro: a transport whose capture() ERRORS
|
|
13
|
+
// (window gone) but whose session exists + inject delivers -> the tick must (a) NOT return Err, and
|
|
14
|
+
// (b) still attempt delivery to the active w2 (delivery_attempts +1 / status advances past 'accepted').
|
|
15
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
16
|
+
fn message_delivery_attempts(dir: &std::path::Path, message_id: &str) -> i64 {
|
|
17
|
+
let store = MessageStore::open(dir).unwrap();
|
|
18
|
+
let conn = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
19
|
+
conn.query_row(
|
|
20
|
+
"select delivery_attempts from messages where message_id = ?1",
|
|
21
|
+
[message_id],
|
|
22
|
+
|r| r.get::<_, i64>(0),
|
|
23
|
+
)
|
|
24
|
+
.unwrap()
|
|
25
|
+
}
|
|
26
|
+
/// A transport that DELIVERS (inject ok, session present) but whose `capture` ALWAYS fails — modelling a
|
|
27
|
+
/// stopped agent whose tmux window is gone. Exercises sync_agent_health's per-agent `capture?`: today the
|
|
28
|
+
/// error propagates and aborts the whole tick; the fix must swallow it and continue to deliver.
|
|
29
|
+
struct CaptureFailsDeliverTransport {
|
|
30
|
+
inner: DeliveringTransport,
|
|
31
|
+
}
|
|
32
|
+
impl CaptureFailsDeliverTransport {
|
|
33
|
+
fn new() -> Self {
|
|
34
|
+
Self { inner: DeliveringTransport::new() }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
impl Transport for CaptureFailsDeliverTransport {
|
|
38
|
+
fn kind(&self) -> BackendKind {
|
|
39
|
+
self.inner.kind()
|
|
40
|
+
}
|
|
41
|
+
fn spawn_first(&self, s: &SessionName, w: &WindowName, a: &[String], c: &std::path::Path, e: &std::collections::BTreeMap<String, String>) -> Result<SpawnResult, TransportError> {
|
|
42
|
+
self.inner.spawn_first(s, w, a, c, e)
|
|
43
|
+
}
|
|
44
|
+
fn spawn_into(&self, s: &SessionName, w: &WindowName, a: &[String], c: &std::path::Path, e: &std::collections::BTreeMap<String, String>) -> Result<SpawnResult, TransportError> {
|
|
45
|
+
self.inner.spawn_into(s, w, a, c, e)
|
|
46
|
+
}
|
|
47
|
+
fn inject(&self, t: &Target, p: &InjectPayload, submit: Key, bracketed: bool) -> Result<InjectReport, TransportError> {
|
|
48
|
+
self.inner.inject(t, p, submit, bracketed)
|
|
49
|
+
}
|
|
50
|
+
fn send_keys(&self, t: &Target, k: &[Key]) -> Result<(), TransportError> {
|
|
51
|
+
self.inner.send_keys(t, k)
|
|
52
|
+
}
|
|
53
|
+
fn capture(&self, _t: &Target, _r: CaptureRange) -> Result<CapturedText, TransportError> {
|
|
54
|
+
// the agent's window is gone — capture fails (tmux can't find the target).
|
|
55
|
+
Err(TransportError::TargetNotFound { target: "window gone (stopped agent)".to_string() })
|
|
56
|
+
}
|
|
57
|
+
fn query(&self, t: &Target, f: PaneField) -> Result<Option<String>, TransportError> {
|
|
58
|
+
self.inner.query(t, f)
|
|
59
|
+
}
|
|
60
|
+
fn liveness(&self, p: &PaneId) -> Result<PaneLiveness, TransportError> {
|
|
61
|
+
self.inner.liveness(p)
|
|
62
|
+
}
|
|
63
|
+
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
64
|
+
self.inner.list_targets()
|
|
65
|
+
}
|
|
66
|
+
fn has_session(&self, s: &SessionName) -> Result<bool, TransportError> {
|
|
67
|
+
self.inner.has_session(s)
|
|
68
|
+
}
|
|
69
|
+
fn list_windows(&self, s: &SessionName) -> Result<Vec<WindowName>, TransportError> {
|
|
70
|
+
self.inner.list_windows(s)
|
|
71
|
+
}
|
|
72
|
+
fn set_session_env(&self, s: &SessionName, k: &str, v: &str) -> Result<SetEnvOutcome, TransportError> {
|
|
73
|
+
self.inner.set_session_env(s, k, v)
|
|
74
|
+
}
|
|
75
|
+
fn kill_session(&self, s: &SessionName) -> Result<(), TransportError> {
|
|
76
|
+
self.inner.kill_session(s)
|
|
77
|
+
}
|
|
78
|
+
fn kill_window(&self, t: &Target) -> Result<(), TransportError> {
|
|
79
|
+
self.inner.kill_window(t)
|
|
80
|
+
}
|
|
81
|
+
fn attach_session(&self, s: &SessionName) -> Result<AttachOutcome, TransportError> {
|
|
82
|
+
self.inner.attach_session(s)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
#[test]
|
|
86
|
+
fn tick_swallows_capture_failure_and_still_delivers_to_other_agent() {
|
|
87
|
+
let dir = std::env::temp_dir().join(format!(
|
|
88
|
+
"team-agent-coord-stopreg-{}-{}",
|
|
89
|
+
std::process::id(),
|
|
90
|
+
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()
|
|
91
|
+
));
|
|
92
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
93
|
+
// w1 STOPPED (window gone -> capture fails), w2 ACTIVE — w2 keeps the tmux session alive.
|
|
94
|
+
crate::state::persist::save_runtime_state(
|
|
95
|
+
&dir,
|
|
96
|
+
&serde_json::json!({
|
|
97
|
+
"session_name": "team-spine",
|
|
98
|
+
"agents": {
|
|
99
|
+
"w1": { "provider": "codex", "status": "stopped", "window": "w1" },
|
|
100
|
+
"w2": { "provider": "codex", "status": "running", "window": "w2" }
|
|
101
|
+
}
|
|
102
|
+
}),
|
|
103
|
+
)
|
|
104
|
+
.unwrap();
|
|
105
|
+
let store = MessageStore::open(&dir).unwrap();
|
|
106
|
+
let mid = store
|
|
107
|
+
.create_message(Some("task-1"), "leader", "w2", "after stop", None, true, None)
|
|
108
|
+
.unwrap();
|
|
109
|
+
drop(store);
|
|
110
|
+
assert_eq!(message_status(&dir, &mid), "accepted", "precondition: a fresh message is 'accepted'");
|
|
111
|
+
let ws = WorkspacePath::new(dir.clone());
|
|
112
|
+
let reg: Box<dyn ProviderRegistry> = Box::new(MockRegistry::new(&[], &[]));
|
|
113
|
+
// capture() ERRORS (window gone) but inject delivers + session present.
|
|
114
|
+
let coord = Coordinator::for_test(ws, reg, Box::new(CaptureFailsDeliverTransport::new()), None, None);
|
|
115
|
+
let result = coord.tick();
|
|
116
|
+
// (a) a per-agent capture failure must NOT abort the whole tick.
|
|
117
|
+
assert!(
|
|
118
|
+
result.is_ok(),
|
|
119
|
+
"P0: sync_agent_health's per-agent `capture?` must be SWALLOWED (log + continue), not propagated — \
|
|
120
|
+
the tick must NOT early-return Err when a stopped agent's window-capture fails; got {result:?}"
|
|
121
|
+
);
|
|
122
|
+
let report = result.unwrap();
|
|
123
|
+
// (b) the deliver loop must still run for the active w2.
|
|
124
|
+
let attempts = message_delivery_attempts(&dir, &mid);
|
|
125
|
+
let delivered_reported = report.delivered.iter().any(|d| d.message_id == mid);
|
|
126
|
+
let advanced = message_status(&dir, &mid) != "accepted";
|
|
127
|
+
assert!(
|
|
128
|
+
attempts >= 1 || delivered_reported || advanced,
|
|
129
|
+
"P0: after swallowing w1's capture failure, the tick MUST still deliver to the active w2 — its \
|
|
130
|
+
message must advance past 'accepted' (delivery_attempts +1 / claimed / delivered). The regression \
|
|
131
|
+
halts before deliver_pending_messages and leaves it at delivery_attempts=0. attempts={attempts} \
|
|
132
|
+
status={} delivered={:?}",
|
|
133
|
+
message_status(&dir, &mid),
|
|
134
|
+
report.delivered
|
|
135
|
+
);
|
|
136
|
+
}
|