@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,227 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
4
|
+
// SPINE SLICE-2b RED — take-over / stuck / deadlock obligations. record_unknown_idle +
|
|
5
|
+
// evaluate_takeover are still record_step() probes; detect_stuck / detect_deadlocks are
|
|
6
|
+
// slice-1 placeholders (detect_stuck reads agent_health status='stuck'; detect_deadlocks
|
|
7
|
+
// returns []). Golden: coordinator/lifecycle.py (_detect_stuck_agents, _record_unknown_idle,
|
|
8
|
+
// _evaluate_takeover), messaging/idle_alerts.py (detect_cross_worker_deadlocks),
|
|
9
|
+
// idle_takeover_wiring.py (build_idle_nodes via read_turn_state on rollout_path; push_idle_reminder
|
|
10
|
+
// → idle_takeover.reminder), leader evaluate_takeover_reminder. IRON LAWS: §121 arm-from-real-
|
|
11
|
+
// delivery only (never from thin air); §84 zero-injection (only push_idle_reminder, only should_ping);
|
|
12
|
+
// unknown != idle persists into take-over (unknown_persistent path, not an idle ping).
|
|
13
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
14
|
+
|
|
15
|
+
/// CapturingTransport + an inject COUNTER (for §84: assert zero injects from the take-over path).
|
|
16
|
+
struct CountingCaptureTransport {
|
|
17
|
+
scrollback: String,
|
|
18
|
+
injects: std::sync::Arc<std::sync::atomic::AtomicUsize>,
|
|
19
|
+
}
|
|
20
|
+
impl Transport for CountingCaptureTransport {
|
|
21
|
+
fn kind(&self) -> BackendKind {
|
|
22
|
+
BackendKind::Tmux
|
|
23
|
+
}
|
|
24
|
+
fn spawn_first(&self, _s: &SessionName, _w: &WindowName, _a: &[String], _c: &std::path::Path, _e: &std::collections::BTreeMap<String, String>) -> Result<SpawnResult, TransportError> {
|
|
25
|
+
unimplemented!("not reached")
|
|
26
|
+
}
|
|
27
|
+
fn spawn_into(&self, _s: &SessionName, _w: &WindowName, _a: &[String], _c: &std::path::Path, _e: &std::collections::BTreeMap<String, String>) -> Result<SpawnResult, TransportError> {
|
|
28
|
+
unimplemented!("not reached")
|
|
29
|
+
}
|
|
30
|
+
fn inject(&self, _t: &Target, _p: &InjectPayload, _s: Key, _b: bool) -> Result<InjectReport, TransportError> {
|
|
31
|
+
self.injects.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
32
|
+
Ok(InjectReport {
|
|
33
|
+
stage_reached: crate::transport::InjectStage::Submit,
|
|
34
|
+
inject_verification: crate::transport::InjectVerification::CaptureContainsToken,
|
|
35
|
+
submit_verification: crate::transport::SubmitVerification::EnterSentWithoutPlaceholderCheck,
|
|
36
|
+
turn_verification: crate::transport::TurnVerification::NotYetObserved,
|
|
37
|
+
attempts: 1,
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
fn send_keys(&self, _t: &Target, _k: &[Key]) -> Result<(), TransportError> {
|
|
41
|
+
Ok(())
|
|
42
|
+
}
|
|
43
|
+
fn capture(&self, _t: &Target, range: CaptureRange) -> Result<CapturedText, TransportError> {
|
|
44
|
+
Ok(CapturedText { text: self.scrollback.clone(), range })
|
|
45
|
+
}
|
|
46
|
+
fn query(&self, _t: &Target, _f: PaneField) -> Result<Option<String>, TransportError> {
|
|
47
|
+
Ok(None)
|
|
48
|
+
}
|
|
49
|
+
fn liveness(&self, _p: &PaneId) -> Result<PaneLiveness, TransportError> {
|
|
50
|
+
Ok(PaneLiveness::Live)
|
|
51
|
+
}
|
|
52
|
+
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
53
|
+
Ok(Vec::new())
|
|
54
|
+
}
|
|
55
|
+
fn has_session(&self, _s: &SessionName) -> Result<bool, TransportError> {
|
|
56
|
+
Ok(true)
|
|
57
|
+
}
|
|
58
|
+
fn list_windows(&self, _s: &SessionName) -> Result<Vec<WindowName>, TransportError> {
|
|
59
|
+
Ok(Vec::new())
|
|
60
|
+
}
|
|
61
|
+
fn set_session_env(&self, _s: &SessionName, _k: &str, _v: &str) -> Result<SetEnvOutcome, TransportError> {
|
|
62
|
+
Ok(SetEnvOutcome::Applied)
|
|
63
|
+
}
|
|
64
|
+
fn kill_session(&self, _s: &SessionName) -> Result<(), TransportError> {
|
|
65
|
+
Ok(())
|
|
66
|
+
}
|
|
67
|
+
fn kill_window(&self, _t: &Target) -> Result<(), TransportError> {
|
|
68
|
+
Ok(())
|
|
69
|
+
}
|
|
70
|
+
fn attach_session(&self, _s: &SessionName) -> Result<AttachOutcome, TransportError> {
|
|
71
|
+
Ok(AttachOutcome::Attached)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn slice2b_dir() -> std::path::PathBuf {
|
|
76
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
77
|
+
static N: AtomicU64 = AtomicU64::new(0);
|
|
78
|
+
let dir = std::env::temp_dir().join(format!("ta-rs-2b-{}-{}", std::process::id(), N.fetch_add(1, Ordering::Relaxed)));
|
|
79
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
80
|
+
dir
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Build a coordinator over a workspace seeded with `state` (verbatim) + a counting transport.
|
|
84
|
+
fn takeover_coord(state: serde_json::Value) -> (Coordinator, std::path::PathBuf, std::sync::Arc<std::sync::atomic::AtomicUsize>) {
|
|
85
|
+
let dir = slice2b_dir();
|
|
86
|
+
crate::state::persist::save_runtime_state(&dir, &state).unwrap();
|
|
87
|
+
let injects = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
|
88
|
+
let transport = CountingCaptureTransport { scrollback: String::new(), injects: std::sync::Arc::clone(&injects) };
|
|
89
|
+
let ws = WorkspacePath::new(dir.clone());
|
|
90
|
+
let reg: Box<dyn ProviderRegistry> = Box::new(MockRegistry::new(&[], &[]));
|
|
91
|
+
let coord = Coordinator::for_test(ws, reg, Box::new(transport), None, None);
|
|
92
|
+
(coord, dir, injects)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Seed an agent_health row (real DB) — what sync_health writes and detect_stuck/detect_deadlocks read.
|
|
96
|
+
fn seed_agent_health(dir: &std::path::Path, agent_id: &str, status: &str, last_output_at: &str, owner_team_id: Option<&str>) {
|
|
97
|
+
let store = MessageStore::open(dir).unwrap();
|
|
98
|
+
let conn = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
99
|
+
conn.execute(
|
|
100
|
+
"insert into agent_health(owner_team_id, agent_id, status, last_output_at, context_usage_pct, current_task_id, updated_at) \
|
|
101
|
+
values (?1, ?2, ?3, ?4, null, null, ?4)",
|
|
102
|
+
rusqlite::params![owner_team_id, agent_id, status, last_output_at],
|
|
103
|
+
)
|
|
104
|
+
.unwrap();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fn has_event(dir: &std::path::Path, name: &str) -> bool {
|
|
108
|
+
read_event_log_dir(dir).iter().any(|e| e.get("event").and_then(|v| v.as_str()) == Some(name))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// #236 nag_removal (N35) — detect_stuck no longer synthesized in coordinator tick.
|
|
112
|
+
// [OLD] assertion: a RUNNING agent with stale last_output + inbound work → report.stuck non-empty.
|
|
113
|
+
// [NEW] assertion: report.stuck stays empty — the framework no longer infers "stuck" from
|
|
114
|
+
// time/state. Delivery primitives (report_result / send / funnel / request_human / broadcast)
|
|
115
|
+
// still flow; only the proactive nag output is gone.
|
|
116
|
+
#[test]
|
|
117
|
+
fn slice2b_detect_stuck_no_longer_synthesized_by_framework_n35() {
|
|
118
|
+
let stale = (chrono::Utc::now() - chrono::Duration::seconds(1000)).to_rfc3339();
|
|
119
|
+
let (coord, dir, _inj) = takeover_coord(serde_json::json!({
|
|
120
|
+
"session_name": "team-h",
|
|
121
|
+
"agents": { "w1": { "provider": "codex" } },
|
|
122
|
+
}));
|
|
123
|
+
seed_agent_health(&dir, "w1", "RUNNING", &stale, Some("team-h"));
|
|
124
|
+
let store = MessageStore::open(&dir).unwrap();
|
|
125
|
+
let _ = store.create_message(Some("t"), "leader", "w1", "do work", None, true, None).unwrap();
|
|
126
|
+
drop(store);
|
|
127
|
+
|
|
128
|
+
let report = coord.tick().expect("tick");
|
|
129
|
+
assert!(
|
|
130
|
+
report.stuck.is_empty(),
|
|
131
|
+
"#236 N35: tick no longer manufactures stuck nag; got {:?}",
|
|
132
|
+
report.stuck
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// #236 nag_removal (N35) — cross_worker_deadlock detection removed from tick.
|
|
137
|
+
// [OLD] assertion: idle recipient + undelivered message → report.deadlock_alerts non-empty.
|
|
138
|
+
// [NEW] assertion: report.deadlock_alerts stays empty — the framework no longer manufactures
|
|
139
|
+
// "deadlock" alerts from idle+pending inference. The actual delivery row still exists and is
|
|
140
|
+
// retried/handled by the delivery primitives; only the nag wrapper is gone.
|
|
141
|
+
#[test]
|
|
142
|
+
fn slice2b_detect_deadlocks_no_longer_synthesized_by_framework_n35() {
|
|
143
|
+
let now = chrono::Utc::now().to_rfc3339();
|
|
144
|
+
let (coord, dir, _inj) = takeover_coord(serde_json::json!({
|
|
145
|
+
"session_name": "team-h",
|
|
146
|
+
"agents": { "w1": { "provider": "codex" }, "w2": { "provider": "codex" } },
|
|
147
|
+
}));
|
|
148
|
+
seed_agent_health(&dir, "w1", "IDLE", &now, Some("team-h"));
|
|
149
|
+
let store = MessageStore::open(&dir).unwrap();
|
|
150
|
+
let _ = store.create_message(Some("t"), "w2", "w1", "are you done?", None, true, None).unwrap();
|
|
151
|
+
drop(store);
|
|
152
|
+
|
|
153
|
+
let report = coord.tick().expect("tick");
|
|
154
|
+
assert!(
|
|
155
|
+
report.deadlock_alerts.is_empty(),
|
|
156
|
+
"#236 N35: tick no longer manufactures cross_worker_deadlock alerts; got {:?}",
|
|
157
|
+
report.deadlock_alerts
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// #236 nag_removal (N35) — the take-over reminder injection is gone from
|
|
162
|
+
// leader::inject::push_idle_reminder (now a no-op shim). tick.rs still runs the
|
|
163
|
+
// evaluate_takeover_reminder / push_idle_reminder pipeline (developer-b will land
|
|
164
|
+
// the tick.rs cleanup separately), but the helper that emitted the reminder event
|
|
165
|
+
// no longer does so — handover requires explicit `claim-leader` / `takeover` now.
|
|
166
|
+
// [OLD] assertion: ARMED + idle worker → exactly one idle_takeover.reminder event.
|
|
167
|
+
// [NEW] assertion: no reminder event is ever emitted (the helper that wrote it is
|
|
168
|
+
// no-op'd at the leader::inject layer).
|
|
169
|
+
#[test]
|
|
170
|
+
fn slice2b_takeover_armed_idle_worker_no_longer_emits_reminder_n35() {
|
|
171
|
+
let rollout = slice2b_dir().join("w1.jsonl");
|
|
172
|
+
std::fs::write(&rollout, r#"{"type":"assistant","requestId":"r1","message":{"stop_reason":"end_turn","content":[]}}"#).unwrap();
|
|
173
|
+
let (coord, dir, _inj) = takeover_coord(serde_json::json!({
|
|
174
|
+
"session_name": "team-h",
|
|
175
|
+
"agents": { "w1": { "provider": "claude_code", "rollout_path": rollout.to_string_lossy() } },
|
|
176
|
+
"coordinator": {
|
|
177
|
+
"idle_takeover_monitor": { "opened_worker_turn_since_ack": true, "all_idle_since": -1.0e9, "suppressed": false }
|
|
178
|
+
}
|
|
179
|
+
}));
|
|
180
|
+
coord.tick().expect("tick");
|
|
181
|
+
assert!(
|
|
182
|
+
!has_event(&dir, "idle_takeover.reminder"),
|
|
183
|
+
"#236 N35: push_idle_reminder is a no-op shim; reminder nag must not be emitted; got {:?}",
|
|
184
|
+
read_event_log_dir(&dir)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// P0 §121/§84 — NEVER arm from thin air: an idle worker whose monitor is NOT armed
|
|
189
|
+
// (no opened_worker_turn_since_ack) is NEVER pinged → no idle_takeover.reminder AND zero injects from
|
|
190
|
+
// the take-over path. Guard (correct today and after wiring); a future arm-from-thin-air regression fails it.
|
|
191
|
+
#[test]
|
|
192
|
+
fn slice2b_takeover_unarmed_idle_worker_is_never_pinged() {
|
|
193
|
+
let rollout = slice2b_dir().join("w1.jsonl");
|
|
194
|
+
std::fs::write(&rollout, r#"{"type":"assistant","requestId":"r1","message":{"stop_reason":"end_turn","content":[]}}"#).unwrap();
|
|
195
|
+
let (coord, dir, injects) = takeover_coord(serde_json::json!({
|
|
196
|
+
"session_name": "team-h",
|
|
197
|
+
"agents": { "w1": { "provider": "claude_code", "rollout_path": rollout.to_string_lossy() } },
|
|
198
|
+
// NO coordinator.idle_takeover_monitor → not armed → reason not_armed_no_worker_turn.
|
|
199
|
+
}));
|
|
200
|
+
coord.tick().expect("tick");
|
|
201
|
+
assert!(!has_event(&dir, "idle_takeover.reminder"), "a NOT-armed worker must never be pinged (§121)");
|
|
202
|
+
assert_eq!(
|
|
203
|
+
injects.load(std::sync::atomic::Ordering::Relaxed),
|
|
204
|
+
0,
|
|
205
|
+
"§84 zero-injection: with no should_ping (and no pending messages), the tick injects NOTHING"
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// #236 nag_removal (N35) — record_unknown_idle removed; unknown_persistent nag deleted.
|
|
210
|
+
// [OLD] assertion: unknown_ticks reaching the 60th tick fires `idle_takeover.unknown_persistent`
|
|
211
|
+
// (every-12-tick cadence after the 60-tick threshold).
|
|
212
|
+
// [NEW] assertion: tick no longer emits the threshold event. Operators don't get time-windowed
|
|
213
|
+
// prods; explicit `claim-leader` / `takeover` is the only ownership-change path.
|
|
214
|
+
#[test]
|
|
215
|
+
fn slice2b_unknown_persistent_no_longer_emitted_at_threshold_tick_n35() {
|
|
216
|
+
let (coord, dir, _inj) = takeover_coord(serde_json::json!({
|
|
217
|
+
"session_name": "team-h",
|
|
218
|
+
"agents": { "w1": { "provider": "codex" } },
|
|
219
|
+
"coordinator": { "unknown_ticks": { "w1": 59 } }
|
|
220
|
+
}));
|
|
221
|
+
coord.tick().expect("tick");
|
|
222
|
+
assert!(
|
|
223
|
+
!has_event(&dir, "idle_takeover.unknown_persistent"),
|
|
224
|
+
"#236 N35: tick no longer emits unknown-persistent nag; got {:?}",
|
|
225
|
+
read_event_log_dir(&dir)
|
|
226
|
+
);
|
|
227
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
4
|
+
// GROUP I/J — tick orchestration (§10 no-panic + bug-084 degraded + tick ORDER
|
|
5
|
+
// + §84 zero-injection) and health/start/stop outcomes — RED via unimplemented!().
|
|
6
|
+
// These are this lane's #1 invariants. `coord_for_test` constructs a real
|
|
7
|
+
// `Coordinator` over a temp workspace with an injected MockTransport + MockRegistry
|
|
8
|
+
// (+ optional save-failure hook + ORDER recorder), so the contracts below ASSERT
|
|
9
|
+
// concrete golden against the unimplemented production `tick`/`health`/`start`/`stop`.
|
|
10
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
11
|
+
|
|
12
|
+
#[test]
|
|
13
|
+
fn tick_never_panics_returns_ok_tickreport_on_clean_state() {
|
|
14
|
+
// §10: daemon-path tick(..) -> Result<TickReport, TickError>; a clean, no-obligation
|
|
15
|
+
// workspace with a live tmux session must yield Ok(TickReport) — never panic, never Err.
|
|
16
|
+
let (coord, _calls) = coord_for_test(/*session_present=*/ true, None, None);
|
|
17
|
+
let report = coord.tick();
|
|
18
|
+
let report = report.expect("tick must not Err on clean state");
|
|
19
|
+
assert!(report.ok, "clean tick is ok=true");
|
|
20
|
+
assert!(!report.stop, "clean tick does not stop the main loop");
|
|
21
|
+
assert_eq!(report.reason, None, "clean tick has no degraded/stop reason");
|
|
22
|
+
assert_eq!(report.persisted, Some(true), "clean tick persisted state");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[test]
|
|
26
|
+
fn tick_save_failure_returns_degraded_not_panic_not_err() {
|
|
27
|
+
// bug-084 (lifecycle.py:345-363): tick-end save_runtime_state failure => degraded
|
|
28
|
+
// TickReport{ok:false, reason:PersistenceDegraded, persisted:Some(false), stop:false}
|
|
29
|
+
// returned as Ok — NOT a panic, NOT an Err (the main loop must NOT catch+backoff this).
|
|
30
|
+
let (coord, _calls) = coord_for_test(true, Some(failing_save_hook()), None);
|
|
31
|
+
let report = coord.tick();
|
|
32
|
+
let report = report.expect("save failure is a DEGRADED Ok, NOT an Err (main loop must not backoff)");
|
|
33
|
+
assert!(!report.ok, "bug-084: degraded => ok=false");
|
|
34
|
+
assert_eq!(
|
|
35
|
+
report.reason,
|
|
36
|
+
Some(TickStopReason::PersistenceDegraded),
|
|
37
|
+
"bug-084: reason=persistence_degraded"
|
|
38
|
+
);
|
|
39
|
+
assert_eq!(report.persisted, Some(false), "bug-084: persisted=Some(false)");
|
|
40
|
+
assert!(!report.stop, "bug-084: stop=false (degrade, do NOT exit main loop)");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[test]
|
|
44
|
+
fn tick_tmux_session_missing_returns_stop_true() {
|
|
45
|
+
// lifecycle.py:277-279 — a TRUTHY session_name whose tmux session is gone =>
|
|
46
|
+
// {ok:false, stop:true, reason:tmux_session_missing} (triggers main-loop break). The
|
|
47
|
+
// gate is the SECOND step after load_runtime_state, BEFORE any capture/refresh/prompt
|
|
48
|
+
// side-effects. (A null/empty session_name skips the gate entirely — see
|
|
49
|
+
// p2_tick_skips_tmux_gate_when_session_name_absent.)
|
|
50
|
+
let (coord, _calls) =
|
|
51
|
+
coord_for_test_with_session(/*session_present=*/ false, "team-missing");
|
|
52
|
+
let report = coord.tick().expect("session-missing is a typed report, not an Err");
|
|
53
|
+
assert!(!report.ok, "session missing => ok=false");
|
|
54
|
+
assert!(report.stop, "session missing => stop=true (break main loop)");
|
|
55
|
+
assert_eq!(
|
|
56
|
+
report.reason,
|
|
57
|
+
Some(TickStopReason::TmuxSessionMissing),
|
|
58
|
+
"reason=tmux_session_missing"
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#[test]
|
|
63
|
+
fn tick_side_effect_order_is_the_fixed_sequence() {
|
|
64
|
+
// lifecycle.py:273-372 — the tick chained side-effect ORDER is fixed:
|
|
65
|
+
// load_state -> tmux_session_gate -> capture_missing -> refresh_statuses ->
|
|
66
|
+
// startup_prompts -> runtime_prompts -> sync_health -> deliver_pending ->
|
|
67
|
+
// fire_scheduled -> detect_stuck -> record_unknown_idle -> evaluate_takeover ->
|
|
68
|
+
// detect_deadlocks -> detect_compaction -> detect_drift -> detect_api_errors ->
|
|
69
|
+
// ATOMIC_save (bug-084 wrap) -> collect_results -> prune_dedupe_log.
|
|
70
|
+
// 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()));
|
|
73
|
+
let (coord, _calls) = coord_for_test(true, None, Some(std::sync::Arc::clone(&recorder)));
|
|
74
|
+
let _ = coord.tick().expect("tick ok");
|
|
75
|
+
let order = recorder.lock().unwrap().clone();
|
|
76
|
+
let expected = vec![
|
|
77
|
+
"load_state",
|
|
78
|
+
"tmux_session_gate",
|
|
79
|
+
"capture_missing",
|
|
80
|
+
"refresh_statuses",
|
|
81
|
+
"startup_prompts",
|
|
82
|
+
"runtime_prompts",
|
|
83
|
+
"sync_health",
|
|
84
|
+
"deliver_pending",
|
|
85
|
+
"fire_scheduled",
|
|
86
|
+
"detect_stuck",
|
|
87
|
+
"record_unknown_idle",
|
|
88
|
+
"evaluate_takeover",
|
|
89
|
+
"detect_deadlocks",
|
|
90
|
+
"detect_compaction",
|
|
91
|
+
"detect_drift",
|
|
92
|
+
"detect_api_errors",
|
|
93
|
+
"atomic_save",
|
|
94
|
+
"collect_results",
|
|
95
|
+
"prune_dedupe_log",
|
|
96
|
+
];
|
|
97
|
+
assert_eq!(order, expected, "tick side-effect ORDER must match the fixed sequence");
|
|
98
|
+
// ATOMIC save is the LAST mutation before read-only collect/prune (bug-084 wrap point).
|
|
99
|
+
let save_idx = order.iter().position(|s| *s == "atomic_save").unwrap();
|
|
100
|
+
let collect_idx = order.iter().position(|s| *s == "collect_results").unwrap();
|
|
101
|
+
assert!(save_idx < collect_idx, "save precedes collect (bug-084 wrap is the last mutation)");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#[test]
|
|
105
|
+
fn tick_zero_provider_sdk_across_full_tick() {
|
|
106
|
+
// §84 / MUST-NOT-13: a full no-obligation tick injects ZERO exploratory prompts and
|
|
107
|
+
// touches NO provider client. MockTransport::inject is unimplemented!() (would panic if
|
|
108
|
+
// reached); a clean tick must therefore NEVER call inject. (The MockRegistry adapter
|
|
109
|
+
// count is asserted via the GROUP E abnormal path; here the no-inject Transport guards
|
|
110
|
+
// the tick-level §84 obligation.)
|
|
111
|
+
let (coord, calls) = coord_for_test(true, None, None);
|
|
112
|
+
let _ = coord.tick().expect("clean tick ok");
|
|
113
|
+
let names = calls.lock().unwrap().clone();
|
|
114
|
+
assert!(
|
|
115
|
+
!names.contains(&"inject"),
|
|
116
|
+
"§84: no exploratory prompt injected across a no-obligation tick"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#[test]
|
|
121
|
+
fn health_ok_is_conjunction_of_running_metadata_ok_and_schema_ok() {
|
|
122
|
+
// lifecycle.py:38 — ok = running ∧ metadata_ok ∧ schema_ok. A fresh temp workspace has
|
|
123
|
+
// NO coordinator.pid => status Missing, ok=false (not running). health() must return a
|
|
124
|
+
// typed HealthReport, never panic.
|
|
125
|
+
let (coord, _calls) = coord_for_test(true, None, None);
|
|
126
|
+
let h = coord.health().expect("health is a typed report");
|
|
127
|
+
assert!(!h.ok, "no pid file => not running => ok=false");
|
|
128
|
+
assert_eq!(h.status, CoordinatorHealthStatus::Missing, "no pid => status=missing");
|
|
129
|
+
assert!(!h.metadata_ok, "no metadata => metadata_ok=false");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#[test]
|
|
133
|
+
fn stop_of_missing_coordinator_returns_missing_outcome() {
|
|
134
|
+
// lifecycle.py:230-232 — no coordinator.pid => StopOutcome::Missing, ok=true (nothing to do).
|
|
135
|
+
let (coord, _calls) = coord_for_test(true, None, None);
|
|
136
|
+
let r = coord.stop().expect("stop is a typed report");
|
|
137
|
+
assert_eq!(r.status, StopOutcome::Missing, "no pid => missing");
|
|
138
|
+
assert_eq!(r.pid, None);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
142
|
+
// GROUP K — pid_is_running (metadata.py:16) — zombie detection — RED
|
|
143
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
144
|
+
|
|
145
|
+
#[test]
|
|
146
|
+
fn pid_is_running_false_for_impossible_pid() {
|
|
147
|
+
// metadata.py:19-21 — os.kill(pid, 0) raises → False. pid 0/巨大 pid 不存在。
|
|
148
|
+
let alive = pid_is_running(Pid(2_000_000_000)).expect("probe returns Result");
|
|
149
|
+
assert!(!alive, "non-existent pid → not running");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[test]
|
|
153
|
+
fn pid_is_running_true_for_self() {
|
|
154
|
+
// current process is alive & not zombie → true.
|
|
155
|
+
let me = std::process::id();
|
|
156
|
+
let alive = pid_is_running(Pid(me)).expect("probe self");
|
|
157
|
+
assert!(alive, "self pid is running");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
161
|
+
// GROUP L — resolve_tick_interval fallback (__main__.py:104) — RED
|
|
162
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
163
|
+
|
|
164
|
+
#[test]
|
|
165
|
+
fn resolve_tick_interval_defaults_to_five() {
|
|
166
|
+
// __main__.py:110-115 — missing/erroring spec → DEFAULT_TICK_INTERVAL_SEC (5.0).
|
|
167
|
+
let w = ws();
|
|
168
|
+
let interval = resolve_tick_interval(&w).expect("returns Result");
|
|
169
|
+
assert_eq!(interval, DEFAULT_TICK_INTERVAL_SEC);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#[test]
|
|
173
|
+
fn read_coordinator_metadata_missing_file_is_none() {
|
|
174
|
+
// metadata.py:30-34 — OSError/JSONDecodeError/非 dict → None.
|
|
175
|
+
let w = WorkspacePath::new("/tmp/team-agent-NONEXISTENT-meta-read-xyz");
|
|
176
|
+
assert_eq!(read_coordinator_metadata(&w), None);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ═══════════════ P2 FIX-LOOP RED (复绿即对抗 cross-model findings) ═══════════════
|
|
180
|
+
// Golden re-probed vs team-agent-public @ 439bef8 (lifecycle/metadata/watch/abnormal_track).
|
|
181
|
+
|
|
182
|
+
// P0 — a fresh state has session_name:null. Python truthiness skips the tmux gate entirely
|
|
183
|
+
// (only probes when session_name is a non-empty string); the daemon proceeds. Current
|
|
184
|
+
// defaults to "team-agent" and, with the session absent, stops the daemon (stop:true).
|
|
185
|
+
#[test]
|
|
186
|
+
fn p2_tick_skips_tmux_gate_when_session_name_absent() {
|
|
187
|
+
let (coord, _calls) = coord_for_test(false, None, None);
|
|
188
|
+
let report = coord.tick().unwrap();
|
|
189
|
+
assert!(!report.stop, "missing/null session_name must skip the gate, not stop the daemon");
|
|
190
|
+
assert_ne!(report.reason, Some(TickStopReason::TmuxSessionMissing));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// P1 — pid_is_running must use os.kill(pid,0) first: a pid owned by another user (pid 1 /
|
|
194
|
+
// launchd, root) is EPERM → not signalable → False. Current only `ps -p` (rc=0) → True.
|
|
195
|
+
#[test]
|
|
196
|
+
fn p2_pid_is_running_false_for_cross_user_pid() {
|
|
197
|
+
// The cross-user semantics only hold for a non-root caller (root CAN signal pid 1).
|
|
198
|
+
if unsafe { libc::geteuid() } != 0 {
|
|
199
|
+
assert!(
|
|
200
|
+
!pid_is_running(Pid(1)).expect("probe pid 1"),
|
|
201
|
+
"pid 1 (root-owned) must read as not-running for a non-root caller (kill EPERM)"
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// P1 — render_event_line(result_received) truncates the summary to 80 chars
|
|
207
|
+
// (watch.py:115-116 `_clean(summary)[:80]`).
|
|
208
|
+
#[test]
|
|
209
|
+
fn p2_render_result_received_truncates_summary_to_80_chars() {
|
|
210
|
+
let long = "x".repeat(200);
|
|
211
|
+
let e = serde_json::json!({"event":"result_received","agent_id":"w1","summary": long});
|
|
212
|
+
let line = render_event_line(&e).expect("renders");
|
|
213
|
+
assert!(line.contains(&"x".repeat(80)), "first 80 summary chars are kept");
|
|
214
|
+
assert!(!line.contains(&"x".repeat(81)), "summary must be truncated to 80 chars");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// P1 — idle_takeover.unknown_persistent must carry the auth_mode field (null when absent)
|
|
218
|
+
// (lifecycle.py:401-408). NOTE: the variant currently lacks the field; the porter adds
|
|
219
|
+
// `auth_mode` between provider and consecutive_ticks and updates this construction.
|
|
220
|
+
#[test]
|
|
221
|
+
fn p2_unknown_persistent_event_serializes_auth_mode_key() {
|
|
222
|
+
let evt = CoordinatorEvent::IdleTakeoverUnknownPersistent {
|
|
223
|
+
node_id: "w7".into(),
|
|
224
|
+
provider: None,
|
|
225
|
+
auth_mode: None,
|
|
226
|
+
consecutive_ticks: 72,
|
|
227
|
+
rollout_path: None,
|
|
228
|
+
};
|
|
229
|
+
let json = serde_json::to_value(&evt).unwrap();
|
|
230
|
+
assert!(
|
|
231
|
+
json.get("auth_mode").is_some(),
|
|
232
|
+
"idle_takeover.unknown_persistent must serialize an auth_mode key (null when absent)"
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// P1 — process_abnormal_records matches a LOWERCASED signature too (abnormal_track.py:49,198):
|
|
237
|
+
// raw has no needle but signature 'TimeoutError' matches blacklist 'timeout'. Current is
|
|
238
|
+
// case-sensitive on `raw` only and ignores the signature → NotifyDefault.
|
|
239
|
+
#[test]
|
|
240
|
+
fn p2_abnormal_matches_lowercased_signature_too() {
|
|
241
|
+
let reg = MockRegistry::new(&[], &["timeout"]);
|
|
242
|
+
let records = vec![serde_json::json!({"raw":"nothing here","signature":"TimeoutError","kind":"error"})];
|
|
243
|
+
let out = process_abnormal_records(
|
|
244
|
+
&records,
|
|
245
|
+
®,
|
|
246
|
+
Provider::Codex,
|
|
247
|
+
&AbnormalNotificationState::default(),
|
|
248
|
+
)
|
|
249
|
+
.unwrap();
|
|
250
|
+
assert_eq!(out.notifications.len(), 1);
|
|
251
|
+
assert_eq!(
|
|
252
|
+
out.notifications[0].decision,
|
|
253
|
+
AbnormalDecision::NotifyBlacklist,
|
|
254
|
+
"blacklist 'timeout' must match the lowercased signature 'TimeoutError'"
|
|
255
|
+
);
|
|
256
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
4
|
+
// GROUP G — watch render_event_line (watch.py:46) — golden text — RED
|
|
5
|
+
// exact strings captured via PYTHONPATH probe against v0.2.11.
|
|
6
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
7
|
+
|
|
8
|
+
#[test]
|
|
9
|
+
fn render_result_received_collapses_whitespace_and_defaults() {
|
|
10
|
+
// watch.py:48-49 + _result_line — "result_received: <agent|-> -> <clean summary>".
|
|
11
|
+
let e = serde_json::json!({"event": "result_received", "agent_id": "w1", "summary": "did the\nthing"});
|
|
12
|
+
assert_eq!(render_event_line(&e), Some("result_received: w1 -> did the thing".to_string()));
|
|
13
|
+
// missing agent + summary → both '-'.
|
|
14
|
+
let bare = serde_json::json!({"event": "result_received"});
|
|
15
|
+
assert_eq!(render_event_line(&bare), Some("result_received: - -> -".to_string()));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[test]
|
|
19
|
+
fn render_injected_and_submitted_share_prefix_and_truncate_message_id() {
|
|
20
|
+
// watch.py:50-51 — both render as "leader_receiver.injected"; message_id truncated to 12.
|
|
21
|
+
let inj = serde_json::json!({"event": "leader_receiver.injected", "message_id": "abcdef0123456789", "recipient": "w2"});
|
|
22
|
+
assert_eq!(render_event_line(&inj), Some("leader_receiver.injected: abcdef012345 -> w2".to_string()));
|
|
23
|
+
// submitted uses same label, msg_id fallback, `to` recipient fallback.
|
|
24
|
+
let sub = serde_json::json!({"event": "leader_receiver.submitted", "msg_id": "xy", "to": "w3"});
|
|
25
|
+
assert_eq!(render_event_line(&sub), Some("leader_receiver.injected: xy -> w3".to_string()));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#[test]
|
|
29
|
+
fn render_send_failed_uses_reason_then_error_fallback() {
|
|
30
|
+
// watch.py:52-53 — reason || error || '-', recipient || to || target || '-', whitespace-cleaned.
|
|
31
|
+
let with_reason = serde_json::json!({"event": "send.failed", "recipient": "w4", "reason": " pane gone "});
|
|
32
|
+
assert_eq!(render_event_line(&with_reason), Some("send.failed: w4 reason=pane gone".to_string()));
|
|
33
|
+
let with_error = serde_json::json!({"event": "send.failed", "target": "w5", "error": "boom"});
|
|
34
|
+
assert_eq!(render_event_line(&with_error), Some("send.failed: w5 reason=boom".to_string()));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#[test]
|
|
38
|
+
fn render_rebind_required_uses_pane_and_reason_fallbacks() {
|
|
39
|
+
// watch.py:54-57 — old_pane_id || pane_id || target || '-'; reason || rediscovery_status || '-'.
|
|
40
|
+
let e = serde_json::json!({"event": "leader_receiver.rebind_required", "old_pane_id": "%9", "reason": "lost"});
|
|
41
|
+
assert_eq!(render_event_line(&e), Some("leader_receiver.rebind_required: pane=%9 reason=lost".to_string()));
|
|
42
|
+
let no_reason = serde_json::json!({"event": "leader_receiver.rebind_required", "pane_id": "%7"});
|
|
43
|
+
assert_eq!(render_event_line(&no_reason), Some("leader_receiver.rebind_required: pane=%7 reason=-".to_string()));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#[test]
|
|
47
|
+
fn render_api_error_defaults_unknown_class_and_dash() {
|
|
48
|
+
// watch.py:58-62 — error_class || "Unknown"; provider || '-'; snippet || '-' cleaned.
|
|
49
|
+
let e = serde_json::json!({"event": "leader.api_error", "error_class": "Overloaded", "provider": "claude_code", "matched_pattern_snippet": "529 too many"});
|
|
50
|
+
assert_eq!(render_event_line(&e), Some("leader.api_error: Overloaded provider=claude_code snippet=529 too many".to_string()));
|
|
51
|
+
let bare = serde_json::json!({"event": "leader.api_error"});
|
|
52
|
+
assert_eq!(render_event_line(&bare), Some("leader.api_error: Unknown provider=- snippet=-".to_string()));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[test]
|
|
56
|
+
fn render_non_renderable_events_return_none() {
|
|
57
|
+
// watch.py:63 — coordinator.* / unknown events → None.
|
|
58
|
+
assert_eq!(render_event_line(&serde_json::json!({"event": "coordinator.boot"})), None);
|
|
59
|
+
assert_eq!(render_event_line(&serde_json::json!({"event": "unknown.thing"})), None);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
63
|
+
// GROUP H — WatchCursor rotation invariants (watch.py:66-97) — RED via collect_watch_lines
|
|
64
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
65
|
+
|
|
66
|
+
#[test]
|
|
67
|
+
fn watch_cursor_default_is_uninitialized() {
|
|
68
|
+
let c = WatchCursor::default();
|
|
69
|
+
assert_eq!(c.event_offset, 0);
|
|
70
|
+
assert!(!c.initialized);
|
|
71
|
+
assert!(c.archive_signature.is_none());
|
|
72
|
+
assert!(c.seen_result_ids.is_empty());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// seed a workspace with an `events.jsonl` (+ optional archived `events.jsonl.1`).
|
|
76
|
+
/// Returns the resolved `WorkspacePath`. The events file holds one renderable
|
|
77
|
+
/// `result_received` line so `collect_watch_lines` has observable non-marker output.
|
|
78
|
+
fn seed_watch_workspace(event_summary: &str, archive_bytes: Option<&[u8]>) -> WorkspacePath {
|
|
79
|
+
let dir = std::env::temp_dir().join(format!(
|
|
80
|
+
"team-agent-watch-rotate-{}-{}",
|
|
81
|
+
std::process::id(),
|
|
82
|
+
std::time::SystemTime::now()
|
|
83
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
84
|
+
.unwrap()
|
|
85
|
+
.as_nanos()
|
|
86
|
+
));
|
|
87
|
+
let logs = crate::model::paths::logs_dir(&dir);
|
|
88
|
+
std::fs::create_dir_all(&logs).unwrap();
|
|
89
|
+
let line = serde_json::json!({"event": "result_received", "agent_id": "w1", "summary": event_summary});
|
|
90
|
+
std::fs::write(
|
|
91
|
+
logs.join("events.jsonl"),
|
|
92
|
+
format!("{}\n", serde_json::to_string(&line).unwrap()),
|
|
93
|
+
)
|
|
94
|
+
.unwrap();
|
|
95
|
+
if let Some(bytes) = archive_bytes {
|
|
96
|
+
std::fs::write(logs.join("events.jsonl.1"), bytes).unwrap();
|
|
97
|
+
}
|
|
98
|
+
// MessageStore::open creates the schema so collect_watch_lines' result tail works.
|
|
99
|
+
let _ = MessageStore::open(&dir).unwrap();
|
|
100
|
+
WorkspacePath::new(dir)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#[test]
|
|
104
|
+
fn watch_rotation_emits_marker_once_resets_offset_and_does_not_replay() {
|
|
105
|
+
// watch/__init__.py:66-97 — archive_signature change OR offset>size => ROTATION_MARKER
|
|
106
|
+
// (once), event_offset reset to 0, archived segment NOT replayed (only the current
|
|
107
|
+
// events.jsonl is read from the new offset forward). The doc-comment invariant
|
|
108
|
+
// (coordinator.rs lines 42-43 / 532-533) is now ASSERTED, not commented.
|
|
109
|
+
let ws = seed_watch_workspace("first segment", None);
|
|
110
|
+
let store = MessageStore::open(ws.as_path()).unwrap();
|
|
111
|
+
let mut cursor = WatchCursor::default();
|
|
112
|
+
|
|
113
|
+
// First call: initializes the cursor (no marker even though no archive yet),
|
|
114
|
+
// renders the seeded result line, advances offset past EOF.
|
|
115
|
+
let first = collect_watch_lines(&ws, &mut cursor, &store, None).unwrap();
|
|
116
|
+
assert!(cursor.initialized, "first call initializes the cursor");
|
|
117
|
+
assert!(
|
|
118
|
+
!first.iter().any(|l| l == ROTATION_MARKER),
|
|
119
|
+
"no rotation marker on first/initializing call"
|
|
120
|
+
);
|
|
121
|
+
assert!(
|
|
122
|
+
first.iter().any(|l| l.contains("first segment")),
|
|
123
|
+
"first segment line is rendered exactly once"
|
|
124
|
+
);
|
|
125
|
+
let offset_after_first = cursor.event_offset;
|
|
126
|
+
assert!(offset_after_first > 0, "offset advanced past the consumed segment");
|
|
127
|
+
|
|
128
|
+
// Simulate rotation: an archive segment now appears (archive_signature changes from
|
|
129
|
+
// None -> Some), and the live events.jsonl is replaced with a fresh, SHORTER file
|
|
130
|
+
// whose new content must NOT include the archived "first segment".
|
|
131
|
+
let logs = crate::model::paths::logs_dir(ws.as_path());
|
|
132
|
+
std::fs::write(logs.join("events.jsonl.1"), b"archived old bytes that must never replay\n")
|
|
133
|
+
.unwrap();
|
|
134
|
+
let fresh = serde_json::json!({"event": "result_received", "agent_id": "w1", "summary": "post rotation"});
|
|
135
|
+
std::fs::write(
|
|
136
|
+
logs.join("events.jsonl"),
|
|
137
|
+
format!("{}\n", serde_json::to_string(&fresh).unwrap()),
|
|
138
|
+
)
|
|
139
|
+
.unwrap();
|
|
140
|
+
|
|
141
|
+
let second = collect_watch_lines(&ws, &mut cursor, &store, None).unwrap();
|
|
142
|
+
let marker_count = second.iter().filter(|l| **l == ROTATION_MARKER).count();
|
|
143
|
+
assert_eq!(marker_count, 1, "ROTATION_MARKER emitted exactly once on rotation");
|
|
144
|
+
assert!(
|
|
145
|
+
!second.iter().any(|l| l.contains("first segment")),
|
|
146
|
+
"archived segment is NOT replayed"
|
|
147
|
+
);
|
|
148
|
+
assert!(
|
|
149
|
+
!second.iter().any(|l| l.contains("old bytes that must never replay")),
|
|
150
|
+
"archive file contents are NEVER read/replayed"
|
|
151
|
+
);
|
|
152
|
+
assert!(
|
|
153
|
+
second.iter().any(|l| l.contains("post rotation")),
|
|
154
|
+
"post-rotation live segment IS rendered (read from reset offset forward)"
|
|
155
|
+
);
|
|
156
|
+
// offset was reset to 0 by the rotation branch, then re-advanced over the fresh
|
|
157
|
+
// (shorter) file — so it must be < the pre-rotation offset, proving the reset.
|
|
158
|
+
assert!(
|
|
159
|
+
cursor.event_offset <= offset_after_first,
|
|
160
|
+
"event_offset reset on rotation (re-advanced over the shorter fresh file)"
|
|
161
|
+
);
|
|
162
|
+
assert_eq!(
|
|
163
|
+
cursor.archive_signature.map(|(sz, _)| sz),
|
|
164
|
+
Some(b"archived old bytes that must never replay\n".len() as u64),
|
|
165
|
+
"archive_signature updated to the new archived segment's (size, mtime_ns)"
|
|
166
|
+
);
|
|
167
|
+
}
|