@team-agent/installer 0.2.11 → 0.3.1
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 +1204 -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 +1207 -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 +557 -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 +1084 -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 +526 -0
- package/crates/team-agent/src/leader/rediscover.rs +1101 -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 +237 -0
- package/crates/team-agent/src/leader/tests/identity.rs +206 -0
- package/crates/team-agent/src/leader/tests/idle.rs +272 -0
- package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
- package/crates/team-agent/src/leader/tests/lease_claim.rs +410 -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 +489 -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 +2109 -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 +985 -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 +710 -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 +187 -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 +468 -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 +743 -0
- package/crates/team-agent/src/messaging/helpers.rs +209 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +329 -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 +553 -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 +578 -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 +659 -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 +765 -0
- package/crates/team-agent/src/tmux_backend.rs +810 -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 +118 -112
- 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,675 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
fn cli_argv(items: &[&str]) -> Vec<String> {
|
|
4
|
+
items.iter().map(|s| (*s).to_string()).collect()
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const FAKE_SPEC_YAML: &str = r#"version: 1
|
|
8
|
+
team:
|
|
9
|
+
name: "fake-e2e"
|
|
10
|
+
mode: "supervisor_worker"
|
|
11
|
+
objective: "Exercise fake provider orchestration."
|
|
12
|
+
workspace: "/WS"
|
|
13
|
+
leader:
|
|
14
|
+
id: "leader"
|
|
15
|
+
role: "leader"
|
|
16
|
+
provider: "fake"
|
|
17
|
+
model: null
|
|
18
|
+
tools:
|
|
19
|
+
- "fs_read"
|
|
20
|
+
- "fs_list"
|
|
21
|
+
- "mcp_team"
|
|
22
|
+
context_policy:
|
|
23
|
+
keep_user_thread: true
|
|
24
|
+
receive_worker_outputs: "structured_only"
|
|
25
|
+
max_worker_result_tokens: 2000
|
|
26
|
+
agents:
|
|
27
|
+
- id: "fake_impl"
|
|
28
|
+
role: "implementation_engineer"
|
|
29
|
+
provider: "fake"
|
|
30
|
+
model: null
|
|
31
|
+
working_directory: "/WS"
|
|
32
|
+
system_prompt:
|
|
33
|
+
inline: "Handle fake implementation tasks."
|
|
34
|
+
file: null
|
|
35
|
+
tools:
|
|
36
|
+
- "fs_read"
|
|
37
|
+
- "fs_write"
|
|
38
|
+
- "fs_list"
|
|
39
|
+
- "execute_bash"
|
|
40
|
+
- "git_diff"
|
|
41
|
+
- "mcp_team"
|
|
42
|
+
- "provider_builtin"
|
|
43
|
+
permission_mode: "restricted"
|
|
44
|
+
preferred_for:
|
|
45
|
+
- "implementation"
|
|
46
|
+
avoid_for: []
|
|
47
|
+
output_contract:
|
|
48
|
+
format: "result_envelope_v1"
|
|
49
|
+
required_fields:
|
|
50
|
+
- "task_id"
|
|
51
|
+
- "status"
|
|
52
|
+
- "summary"
|
|
53
|
+
- "artifacts"
|
|
54
|
+
routing:
|
|
55
|
+
default_assignee: "leader"
|
|
56
|
+
rules:
|
|
57
|
+
- id: "implementation-to-fake"
|
|
58
|
+
match:
|
|
59
|
+
type:
|
|
60
|
+
- "implementation"
|
|
61
|
+
assign_to: "fake_impl"
|
|
62
|
+
priority: 10
|
|
63
|
+
communication:
|
|
64
|
+
protocol: "mcp_inbox"
|
|
65
|
+
topology: "leader_centered"
|
|
66
|
+
worker_to_worker: true
|
|
67
|
+
ack_timeout_sec: 2
|
|
68
|
+
result_format: "result_envelope_v1"
|
|
69
|
+
message_store:
|
|
70
|
+
sqlite: ".team/runtime/team.db"
|
|
71
|
+
mirror_files: ".team/messages"
|
|
72
|
+
runtime:
|
|
73
|
+
backend: "tmux"
|
|
74
|
+
display_backend: "none"
|
|
75
|
+
session_name: "team-agent-fake-e2e"
|
|
76
|
+
auto_launch: true
|
|
77
|
+
require_user_approval_before_launch: false
|
|
78
|
+
max_active_agents: 1
|
|
79
|
+
startup_order:
|
|
80
|
+
- "fake_impl"
|
|
81
|
+
context:
|
|
82
|
+
state_file: "team_state.md"
|
|
83
|
+
artifact_dir: ".team/artifacts"
|
|
84
|
+
log_dir: ".team/logs"
|
|
85
|
+
summarization:
|
|
86
|
+
worker_full_logs: "retain_outside_leader_context"
|
|
87
|
+
state_update: "after_each_result"
|
|
88
|
+
tasks:
|
|
89
|
+
- id: "task_impl"
|
|
90
|
+
title: "Fake implementation"
|
|
91
|
+
type: "implementation"
|
|
92
|
+
assignee: null
|
|
93
|
+
deps: []
|
|
94
|
+
acceptance:
|
|
95
|
+
- "fake result collected"
|
|
96
|
+
status: "pending"
|
|
97
|
+
requires_tools:
|
|
98
|
+
- "fs_write"
|
|
99
|
+
- "execute_bash"
|
|
100
|
+
files:
|
|
101
|
+
- "src/example.py"
|
|
102
|
+
risk: "low"
|
|
103
|
+
"#;
|
|
104
|
+
|
|
105
|
+
fn seed_team_spec(ws: &std::path::Path) {
|
|
106
|
+
let spec = FAKE_SPEC_YAML.replace("/WS", &ws.to_string_lossy());
|
|
107
|
+
std::fs::write(ws.join("team.spec.yaml"), spec).unwrap();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── ACK-CRACK [P1 byte-shape] — acknowledge_idle must write golden's TTL suppression shape ───────
|
|
111
|
+
// golden runtime.py:680-688: manual-acknowledge persists
|
|
112
|
+
// coordinator.idle_acknowledged[team] = {acknowledged_at, expires_at, ttl_seconds}
|
|
113
|
+
// coordinator.suppressed_idle_alerts[team][worker].idle_fallback =
|
|
114
|
+
// {suppressed_at, suppressed_by:"manual_acknowledge", manual_acknowledge:true, expires_at, ttl_seconds}
|
|
115
|
+
// The clear logic does datetime.fromisoformat(entry["expires_at"]); a MISSING expires_at -> ValueError
|
|
116
|
+
// -> "invalid_suppression_timestamp" -> immediate self-clear (latent crack once detect_idle_fallbacks
|
|
117
|
+
// is ported). So BOTH idle_acknowledged and the entry MUST carry a non-empty expires_at.
|
|
118
|
+
#[test]
|
|
119
|
+
fn acknowledge_idle_writes_golden_ttl_suppression_shape() {
|
|
120
|
+
let ws = tmp_workspace();
|
|
121
|
+
crate::state::persist::save_runtime_state(
|
|
122
|
+
&ws,
|
|
123
|
+
&serde_json::json!({
|
|
124
|
+
"active_team_key": "teamX",
|
|
125
|
+
"agents": {"w1": {"status": "running", "provider": "codex"}}
|
|
126
|
+
}),
|
|
127
|
+
)
|
|
128
|
+
.unwrap();
|
|
129
|
+
let _ = lifecycle_port::acknowledge_idle(&ws, None).expect("acknowledge_idle ok");
|
|
130
|
+
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
131
|
+
let ack = &state["coordinator"]["idle_acknowledged"]["teamX"];
|
|
132
|
+
assert!(
|
|
133
|
+
ack.get("expires_at").and_then(serde_json::Value::as_str).is_some_and(|s| !s.is_empty()),
|
|
134
|
+
"ACK-CRACK: idle_acknowledged[team] must carry a non-empty expires_at (golden); got {ack}"
|
|
135
|
+
);
|
|
136
|
+
assert!(ack.get("ttl_seconds").is_some(), "idle_acknowledged[team] must carry ttl_seconds; got {ack}");
|
|
137
|
+
let entry = &state["coordinator"]["suppressed_idle_alerts"]["teamX"]["w1"]["idle_fallback"];
|
|
138
|
+
assert!(
|
|
139
|
+
entry.get("expires_at").and_then(serde_json::Value::as_str).is_some_and(|s| !s.is_empty()),
|
|
140
|
+
"ACK-CRACK: the manual-ack suppression entry must carry expires_at (else clear logic ValueErrors \
|
|
141
|
+
-> instant self-clear); got {entry}"
|
|
142
|
+
);
|
|
143
|
+
assert_eq!(entry["suppressed_by"], serde_json::json!("manual_acknowledge"), "golden suppressed_by; got {entry}");
|
|
144
|
+
assert_eq!(entry["manual_acknowledge"], serde_json::json!(true), "golden manual_acknowledge:true; got {entry}");
|
|
145
|
+
}
|
|
146
|
+
// ── ACK return-shape [P1 byte-parity] — acknowledge_idle must RETURN golden's keys ───────────────
|
|
147
|
+
// golden runtime.py:691: return {ok, team, agent_id, acknowledged_at, expires_at, ttl_seconds}.
|
|
148
|
+
// Rust (cli/mod.rs) returns only {ok, team, ttl_seconds} -> missing agent_id, acknowledged_at,
|
|
149
|
+
// expires_at. RED. (acknowledged_at/expires_at are the same values written into idle_acknowledged.)
|
|
150
|
+
#[test]
|
|
151
|
+
fn acknowledge_idle_return_carries_golden_keys() {
|
|
152
|
+
let ws = tmp_workspace();
|
|
153
|
+
crate::state::persist::save_runtime_state(
|
|
154
|
+
&ws,
|
|
155
|
+
&serde_json::json!({ "active_team_key": "teamX", "agents": {"w1": {"status": "running", "provider": "codex"}} }),
|
|
156
|
+
)
|
|
157
|
+
.unwrap();
|
|
158
|
+
let r = lifecycle_port::acknowledge_idle(&ws, None).expect("acknowledge_idle ok");
|
|
159
|
+
let obj = r.as_object().expect("ack returns a dict");
|
|
160
|
+
for key in ["ok", "team", "agent_id", "acknowledged_at", "expires_at", "ttl_seconds"] {
|
|
161
|
+
assert!(
|
|
162
|
+
obj.contains_key(key),
|
|
163
|
+
"ACK return-shape: golden return carries `{key}` (runtime.py:691: ok/team/agent_id/\
|
|
164
|
+
acknowledged_at/expires_at/ttl_seconds); Rust omits it. got keys {:?}",
|
|
165
|
+
obj.keys().collect::<Vec<_>>()
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
assert!(
|
|
169
|
+
obj.get("expires_at").and_then(serde_json::Value::as_str).is_some_and(|s| !s.is_empty()),
|
|
170
|
+
"ACK return-shape: expires_at must be a non-empty timestamp; got {r}"
|
|
171
|
+
);
|
|
172
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
173
|
+
}
|
|
174
|
+
// ── BUG-2 [real bug] — inbox must RETURN the stored messages, not a hardcoded []. ────────────────
|
|
175
|
+
// Golden status/inbox.py:35-38 -> MessageStore.inbox(agent_id) (core.py:242, owner_team_id=None):
|
|
176
|
+
// select <MESSAGE_SELECT> from messages where sender = ? or recipient = ? order by created_at desc
|
|
177
|
+
// limit ? -> then reversed(rows) (chronological asc). At THIS call site owner_team_id is None, so
|
|
178
|
+
// there is NO team filter — a sent-and-stored message (recipient=w1, status='accepted') must show
|
|
179
|
+
// in `inbox w1`. Rust mod.rs:144 is a stub: `let _=(workspace,limit,as_json); "messages":[]`. So
|
|
180
|
+
// the row is in team.db but inbox always returns [] -> RED. The shape test above only proves the
|
|
181
|
+
// empty-state envelope; THIS proves the message actually surfaces.
|
|
182
|
+
#[test]
|
|
183
|
+
fn inbox_returns_stored_message_for_recipient() {
|
|
184
|
+
let ws = tmp_workspace();
|
|
185
|
+
let store = crate::message_store::MessageStore::open(&ws).unwrap();
|
|
186
|
+
let mid = store
|
|
187
|
+
.create_message(None, "leader", "w1", "hello w1", None, true, None)
|
|
188
|
+
.unwrap();
|
|
189
|
+
let v = status_port::inbox(&ws, "w1", 20, None, true).expect("inbox");
|
|
190
|
+
let messages = v["messages"].as_array().expect("messages array");
|
|
191
|
+
assert_eq!(
|
|
192
|
+
messages.len(),
|
|
193
|
+
1,
|
|
194
|
+
"golden inbox(w1) must return the stored recipient=w1 row; the stub returns [] -> RED. got {v}"
|
|
195
|
+
);
|
|
196
|
+
let m = &messages[0];
|
|
197
|
+
assert_eq!(m["message_id"], json!(mid), "the returned row is the message we stored");
|
|
198
|
+
assert_eq!(m["recipient"], json!("w1"));
|
|
199
|
+
assert_eq!(m["sender"], json!("leader"));
|
|
200
|
+
assert_eq!(m["content"], json!("hello w1"));
|
|
201
|
+
assert_eq!(m["status"], json!("accepted"), "create_message persists status='accepted'");
|
|
202
|
+
// NULL owner_team_id semantics: status.inbox() calls MessageStore.inbox(agent) with
|
|
203
|
+
// owner_team_id=None (no team clause), so a NULL-owner message MUST surface for its recipient.
|
|
204
|
+
assert_eq!(m["owner_team_id"], json!(null), "the stored message's owner_team_id is NULL and still returned");
|
|
205
|
+
// byte-faithful raw-row columns: requires_ack is the 0/1 INT; artifact_refs the literal text "[]".
|
|
206
|
+
assert_eq!(m["requires_ack"], json!(1), "requires_ack is the 0/1 int, not a bool");
|
|
207
|
+
assert_eq!(m["artifact_refs"], json!("[]"), "artifact_refs is the raw text column, not parsed");
|
|
208
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
209
|
+
}
|
|
210
|
+
// ── BUG-2 (match scope) — inbox(agent) returns rows where sender==agent OR recipient==agent, and
|
|
211
|
+
// EXCLUDES messages for other agents. Membership+exclusion form (not strict index order) so the
|
|
212
|
+
// test is deterministic regardless of created_at sub-second ties; golden order is chronological asc. ─
|
|
213
|
+
#[test]
|
|
214
|
+
fn inbox_matches_sender_or_recipient_and_excludes_others() {
|
|
215
|
+
let ws = tmp_workspace();
|
|
216
|
+
let store = crate::message_store::MessageStore::open(&ws).unwrap();
|
|
217
|
+
store.create_message(None, "leader", "w1", "to w1", None, true, None).unwrap();
|
|
218
|
+
store.create_message(None, "w1", "leader", "from w1", None, true, None).unwrap();
|
|
219
|
+
store.create_message(None, "leader", "w2", "unrelated to w2", None, true, None).unwrap();
|
|
220
|
+
let v = status_port::inbox(&ws, "w1", 20, None, true).expect("inbox");
|
|
221
|
+
let messages = v["messages"].as_array().expect("messages array");
|
|
222
|
+
let mut contents: Vec<String> =
|
|
223
|
+
messages.iter().map(|m| m["content"].as_str().unwrap().to_string()).collect();
|
|
224
|
+
contents.sort();
|
|
225
|
+
assert_eq!(
|
|
226
|
+
contents,
|
|
227
|
+
vec!["from w1".to_string(), "to w1".to_string()],
|
|
228
|
+
"inbox(w1) must return BOTH the recipient=w1 and sender=w1 rows and EXCLUDE the w2 message; \
|
|
229
|
+
the stub returns [] -> RED. got {contents:?}"
|
|
230
|
+
);
|
|
231
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
232
|
+
}
|
|
233
|
+
// ── BUG-4 [real bug] — peek must resolve the agent terminal via state `session:window` (golden),
|
|
234
|
+
// NOT a stored `pane_id` field. A live worker present in state (with a `window`, on the team's `-L`
|
|
235
|
+
// socket) must NOT be fabricated as "agent pane not found". ──────────────────────────────────────
|
|
236
|
+
// Golden status/peek.py:35-44: agent = state["agents"][id]; window = agent.get("window", id);
|
|
237
|
+
// if not session_name or not _tmux_window_exists(session_name, window): raise "agent terminal is
|
|
238
|
+
// not available: <id>"; else `tmux capture-pane -t session:window`. It NEVER reads a stored pane_id.
|
|
239
|
+
// Probed live (/tmp/probe_peek.py): a present worker whose window is absent on the socket raises
|
|
240
|
+
// `agent terminal is not available: w1`; a missing agent raises `unknown agent id: <id>`. Rust
|
|
241
|
+
// cmd_peek (adapters.rs:231 + agent_pane_id:279) keys off agent_state pane_id/pane/tmux_pane_id and
|
|
242
|
+
// returns {ok:false,error:"agent pane not found"} when absent — so a NORMAL live worker (window in
|
|
243
|
+
// state, no pane_id field) is mis-reported as not found. That is the CP-1 pane-resolution divergence.
|
|
244
|
+
//
|
|
245
|
+
// Deterministic without real tmux: on a host with no live session, the golden-correct peek resolves
|
|
246
|
+
// session:window, finds the window absent on the socket, and yields "agent terminal is not available:
|
|
247
|
+
// w1" — NOT "agent pane not found". (The window-on-socket -> real raw-screen capture positive case is
|
|
248
|
+
// real-machine; see the #[ignore] dispatch_routes_peek_real_machine.)
|
|
249
|
+
#[test]
|
|
250
|
+
fn peek_resolves_live_worker_via_session_window_not_pane_id_field() {
|
|
251
|
+
let ws = tmp_workspace();
|
|
252
|
+
// a live worker: present in state with a `window`, session_name set, but NO stored pane_id field.
|
|
253
|
+
// session name is unique so a real tmux session on the dev host can't accidentally satisfy it.
|
|
254
|
+
crate::state::persist::save_runtime_state(
|
|
255
|
+
&ws,
|
|
256
|
+
&json!({
|
|
257
|
+
"session_name": "team-peek-red-probe-x9q",
|
|
258
|
+
"agents": {"w1": {"status": "running", "provider": "codex", "window": "w1"}}
|
|
259
|
+
}),
|
|
260
|
+
)
|
|
261
|
+
.unwrap();
|
|
262
|
+
let args = PeekArgs {
|
|
263
|
+
agent: "w1".to_string(),
|
|
264
|
+
workspace: ws.clone(),
|
|
265
|
+
tail: 20,
|
|
266
|
+
allow_raw_screen: true,
|
|
267
|
+
json: true,
|
|
268
|
+
};
|
|
269
|
+
let text = outcome_text(cmd_peek(&args));
|
|
270
|
+
assert!(
|
|
271
|
+
!text.contains("agent pane not found"),
|
|
272
|
+
"peek keys off a stored pane_id field and fabricates 'agent pane not found' for a live worker \
|
|
273
|
+
that has a `window` in state; golden resolves session:window and never reads pane_id. got: {text}"
|
|
274
|
+
);
|
|
275
|
+
assert!(
|
|
276
|
+
text.contains("agent terminal is not available: w1"),
|
|
277
|
+
"golden status/peek.py: a worker whose window is not on the socket yields \
|
|
278
|
+
'agent terminal is not available: w1' (window-existence via session:window), NOT a \
|
|
279
|
+
pane_id-keyed error. got: {text}"
|
|
280
|
+
);
|
|
281
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
282
|
+
}
|
|
283
|
+
#[test]
|
|
284
|
+
fn ux_doctor_secret_scan_is_present_and_non_triggering_for_normal_paths() {
|
|
285
|
+
let ws = tmp_workspace();
|
|
286
|
+
std::fs::write(ws.join("normal-role.md"), "---\nname: worker\nprovider: codex\n---\nUse /tmp/team-agent.\n").unwrap();
|
|
287
|
+
let value = json_output(cmd_doctor(&DoctorArgs {
|
|
288
|
+
spec: None,
|
|
289
|
+
workspace: ws.clone(),
|
|
290
|
+
gate: None,
|
|
291
|
+
comms: false,
|
|
292
|
+
team: None,
|
|
293
|
+
fix: false,
|
|
294
|
+
fix_schema: false,
|
|
295
|
+
cleanup_orphans: false,
|
|
296
|
+
confirm: false,
|
|
297
|
+
json: true,
|
|
298
|
+
}).expect("doctor"));
|
|
299
|
+
assert_eq!(value.pointer("/secret_scan/ok"), Some(&json!(true)));
|
|
300
|
+
assert_eq!(value.pointer("/secret_scan/findings"), Some(&json!([])));
|
|
301
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
302
|
+
}
|
|
303
|
+
#[test]
|
|
304
|
+
fn ux_doctor_secret_scan_findings_name_the_exact_trigger() {
|
|
305
|
+
let ws = tmp_workspace();
|
|
306
|
+
std::fs::write(ws.join("leaky-role.md"), "OPENAI_API_KEY=sk-test-red-contract\n").unwrap();
|
|
307
|
+
let value = json_output(cmd_doctor(&DoctorArgs {
|
|
308
|
+
spec: None,
|
|
309
|
+
workspace: ws.clone(),
|
|
310
|
+
gate: None,
|
|
311
|
+
comms: false,
|
|
312
|
+
team: None,
|
|
313
|
+
fix: false,
|
|
314
|
+
fix_schema: false,
|
|
315
|
+
cleanup_orphans: false,
|
|
316
|
+
confirm: false,
|
|
317
|
+
json: true,
|
|
318
|
+
}).expect("doctor"));
|
|
319
|
+
let finding = value
|
|
320
|
+
.pointer("/secret_scan/findings/0")
|
|
321
|
+
.and_then(serde_json::Value::as_object)
|
|
322
|
+
.expect("secret-scan must report the concrete trigger");
|
|
323
|
+
for key in ["path", "line", "rule", "match_excerpt"] {
|
|
324
|
+
assert!(finding.contains_key(key), "secret-scan finding missing `{key}`: {finding:?}");
|
|
325
|
+
}
|
|
326
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
327
|
+
}
|
|
328
|
+
#[test]
|
|
329
|
+
fn ux_wait_ready_does_not_report_ready_true_without_ready_runtime_state() {
|
|
330
|
+
let ws = tmp_workspace();
|
|
331
|
+
crate::state::persist::save_runtime_state(
|
|
332
|
+
&ws,
|
|
333
|
+
&json!({
|
|
334
|
+
"agents": {"w1": {"status": "starting"}},
|
|
335
|
+
"tasks": [{"id": "t1", "assignee": "w1", "status": "pending"}],
|
|
336
|
+
"leader_receiver": {"status": "attached"},
|
|
337
|
+
}),
|
|
338
|
+
)
|
|
339
|
+
.unwrap();
|
|
340
|
+
let value = json_output(cmd_wait_ready(&WaitReadyArgs {
|
|
341
|
+
workspace: ws.clone(),
|
|
342
|
+
timeout: 0.0,
|
|
343
|
+
json: true,
|
|
344
|
+
}).expect("wait-ready"));
|
|
345
|
+
assert_eq!(value["ok"], json!(false), "wait-ready must not fake success before workers are ready");
|
|
346
|
+
assert_eq!(value.pointer("/readiness/ready"), Some(&json!(false)));
|
|
347
|
+
assert!(
|
|
348
|
+
value["summary"].as_str().unwrap_or("").contains("not ready"),
|
|
349
|
+
"wait-ready false state should explain not-ready status, got {value:?}"
|
|
350
|
+
);
|
|
351
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
352
|
+
}
|
|
353
|
+
#[test]
|
|
354
|
+
fn wait_ready_fake_quick_start_counts_mcp_config_and_task_prompt_delivery() {
|
|
355
|
+
let ws = tmp_workspace();
|
|
356
|
+
let mcp_config = ws.join(".team").join("runtime").join("agents").join("fake_impl").join("mcp_config.json");
|
|
357
|
+
std::fs::create_dir_all(mcp_config.parent().unwrap()).unwrap();
|
|
358
|
+
std::fs::write(&mcp_config, r#"{"mcpServers":{"team-agent":{}}}"#).unwrap();
|
|
359
|
+
crate::state::persist::save_runtime_state(
|
|
360
|
+
&ws,
|
|
361
|
+
&json!({
|
|
362
|
+
"session_name": "team-fake-ready",
|
|
363
|
+
"agents": {
|
|
364
|
+
"fake_impl": {
|
|
365
|
+
"status": "running",
|
|
366
|
+
"provider": "fake",
|
|
367
|
+
"mcp_config": mcp_config.to_string_lossy(),
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
"tasks": [{
|
|
371
|
+
"id": "task_impl",
|
|
372
|
+
"assignee": "fake_impl",
|
|
373
|
+
"status": "pending",
|
|
374
|
+
}],
|
|
375
|
+
"leader_receiver": {"status": "attached"},
|
|
376
|
+
}),
|
|
377
|
+
)
|
|
378
|
+
.unwrap();
|
|
379
|
+
let store = crate::message_store::MessageStore::open(&ws).unwrap();
|
|
380
|
+
store
|
|
381
|
+
.create_message(Some("task_impl"), "leader", "fake_impl", "initial task prompt", None, true, None)
|
|
382
|
+
.unwrap();
|
|
383
|
+
let value = json_output(cmd_wait_ready(&WaitReadyArgs {
|
|
384
|
+
workspace: ws.clone(),
|
|
385
|
+
timeout: 0.0,
|
|
386
|
+
json: true,
|
|
387
|
+
}).expect("wait-ready"));
|
|
388
|
+
assert_eq!(
|
|
389
|
+
value.pointer("/readiness/mcp_ready"),
|
|
390
|
+
Some(&json!(true)),
|
|
391
|
+
"fake quick-start readiness must treat an existing per-agent mcp_config file as mcp_ready"
|
|
392
|
+
);
|
|
393
|
+
assert_eq!(
|
|
394
|
+
value.pointer("/readiness/task_prompt_delivered"),
|
|
395
|
+
Some(&json!(true)),
|
|
396
|
+
"fake quick-start readiness must treat message_counts>0 / persisted initial task prompt as task_prompt_delivered"
|
|
397
|
+
);
|
|
398
|
+
assert_eq!(
|
|
399
|
+
value.pointer("/readiness/ready"),
|
|
400
|
+
Some(&json!(true)),
|
|
401
|
+
"process_started + cli_prompt_ready alone is incomplete; mcp_ready and task_prompt_delivered must also be satisfied"
|
|
402
|
+
);
|
|
403
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
404
|
+
}
|
|
405
|
+
fn valid_result_envelope() -> serde_json::Value {
|
|
406
|
+
json!({
|
|
407
|
+
"schema_version": "result_envelope_v1",
|
|
408
|
+
"task_id": "task_impl",
|
|
409
|
+
"agent_id": "fake_impl",
|
|
410
|
+
"status": "success",
|
|
411
|
+
"summary": "done",
|
|
412
|
+
"artifacts": [],
|
|
413
|
+
"changes": [],
|
|
414
|
+
"tests": [{"command": "cargo test", "status": "passed"}],
|
|
415
|
+
"risks": [],
|
|
416
|
+
"next_actions": []
|
|
417
|
+
})
|
|
418
|
+
}
|
|
419
|
+
fn seed_collect_state(ws: &std::path::Path) {
|
|
420
|
+
seed_team_spec(ws);
|
|
421
|
+
crate::state::persist::save_runtime_state(
|
|
422
|
+
ws,
|
|
423
|
+
&json!({
|
|
424
|
+
"agents": {"fake_impl": {"status": "idle"}},
|
|
425
|
+
"tasks": [{
|
|
426
|
+
"id": "task_impl",
|
|
427
|
+
"title": "Fake implementation",
|
|
428
|
+
"type": "implementation",
|
|
429
|
+
"assignee": "fake_impl",
|
|
430
|
+
"deps": [],
|
|
431
|
+
"acceptance": ["fake result collected"],
|
|
432
|
+
"status": "pending",
|
|
433
|
+
"requires_tools": [],
|
|
434
|
+
"files": [],
|
|
435
|
+
"risk": "low"
|
|
436
|
+
}],
|
|
437
|
+
"session_name": Value::Null,
|
|
438
|
+
"active_team_key": Value::Null,
|
|
439
|
+
"spec_path": ws.join("team.spec.yaml").to_string_lossy()
|
|
440
|
+
}),
|
|
441
|
+
)
|
|
442
|
+
.unwrap();
|
|
443
|
+
}
|
|
444
|
+
fn seed_uncollected_result(ws: &std::path::Path, result_id: &str) {
|
|
445
|
+
let store = crate::message_store::MessageStore::open(ws).unwrap();
|
|
446
|
+
let conn = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
447
|
+
conn.execute(
|
|
448
|
+
"insert into results(
|
|
449
|
+
result_id, owner_team_id, task_id, agent_id, envelope, status, created_at
|
|
450
|
+
) values (?1, null, 'task_impl', 'fake_impl', ?2, 'success', '2026-06-02T10:00:00+00:00')",
|
|
451
|
+
rusqlite::params![result_id, valid_result_envelope().to_string()],
|
|
452
|
+
)
|
|
453
|
+
.unwrap();
|
|
454
|
+
}
|
|
455
|
+
fn read_state(ws: &std::path::Path) -> serde_json::Value {
|
|
456
|
+
serde_json::from_str(
|
|
457
|
+
&std::fs::read_to_string(crate::state::persist::runtime_state_path(ws)).unwrap(),
|
|
458
|
+
)
|
|
459
|
+
.unwrap()
|
|
460
|
+
}
|
|
461
|
+
fn read_events(ws: &std::path::Path) -> Vec<serde_json::Value> {
|
|
462
|
+
crate::event_log::EventLog::new(ws).tail(50).unwrap()
|
|
463
|
+
}
|
|
464
|
+
fn seeded_team_key(ws: &std::path::Path) -> String {
|
|
465
|
+
ws.file_name().unwrap().to_string_lossy().to_string()
|
|
466
|
+
}
|
|
467
|
+
fn json_output(result: CmdResult) -> serde_json::Value {
|
|
468
|
+
match result.output {
|
|
469
|
+
CmdOutput::Json(v) => v,
|
|
470
|
+
other => panic!("expected JSON output, got {other:?}"),
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
#[test]
|
|
474
|
+
fn validate_result_file_good_and_inline_garbage_are_distinct() {
|
|
475
|
+
let ws = tmp_workspace();
|
|
476
|
+
let envelope_path = ws.join("result.json");
|
|
477
|
+
std::fs::write(&envelope_path, valid_result_envelope().to_string()).unwrap();
|
|
478
|
+
let good = run(
|
|
479
|
+
&cli_argv(&["validate-result", "--file", &envelope_path.to_string_lossy(), "--json"]),
|
|
480
|
+
&ws,
|
|
481
|
+
);
|
|
482
|
+
assert_eq!(
|
|
483
|
+
good,
|
|
484
|
+
ExitCode::Ok,
|
|
485
|
+
"Python cmd_validate_result accepts --file and returns {{ok:true,task_id,agent_id,status}} for a valid envelope"
|
|
486
|
+
);
|
|
487
|
+
let garbage = run(&cli_argv(&["validate-result", "{garbage", "--json"]), &ws);
|
|
488
|
+
assert_eq!(
|
|
489
|
+
garbage,
|
|
490
|
+
ExitCode::Error,
|
|
491
|
+
"garbage JSON must be invalid, not indistinguishable from the good-envelope path"
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
#[test]
|
|
495
|
+
fn collect_uncollected_result_marks_db_and_outputs_result() {
|
|
496
|
+
let ws = tmp_workspace();
|
|
497
|
+
seed_collect_state(&ws);
|
|
498
|
+
seed_uncollected_result(&ws, "res_collect_red");
|
|
499
|
+
let out = json_output(
|
|
500
|
+
cmd_collect(&CollectArgs {
|
|
501
|
+
workspace: ws.clone(),
|
|
502
|
+
result_file: None,
|
|
503
|
+
json: true,
|
|
504
|
+
})
|
|
505
|
+
.unwrap(),
|
|
506
|
+
);
|
|
507
|
+
assert_eq!(out["ok"], json!(true));
|
|
508
|
+
assert_eq!(out["collected_results"][0]["result_id"], json!("res_collect_red"));
|
|
509
|
+
assert_eq!(out["collected_results"][0]["scope"], json!("task"));
|
|
510
|
+
assert_eq!(
|
|
511
|
+
out["results"],
|
|
512
|
+
json!({"total": 1, "uncollected": 0, "collected": 1, "invalid": 0, "by_status": {}})
|
|
513
|
+
);
|
|
514
|
+
let store = crate::message_store::MessageStore::open(&ws).unwrap();
|
|
515
|
+
let conn = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
516
|
+
let status: String = conn
|
|
517
|
+
.query_row(
|
|
518
|
+
"select status from results where result_id = 'res_collect_red'",
|
|
519
|
+
[],
|
|
520
|
+
|row| row.get(0),
|
|
521
|
+
)
|
|
522
|
+
.unwrap();
|
|
523
|
+
assert_eq!(status, "collected");
|
|
524
|
+
let state = read_state(&ws);
|
|
525
|
+
assert_eq!(state["tasks"][0]["status"], json!("done"));
|
|
526
|
+
assert_eq!(state["tasks"][0]["accepted_result_id"], json!("res_collect_red"));
|
|
527
|
+
assert!(
|
|
528
|
+
read_events(&ws)
|
|
529
|
+
.iter()
|
|
530
|
+
.any(|e| e["event"] == json!("collect.result") && e["result_id"] == json!("res_collect_red")),
|
|
531
|
+
"collect must emit collect.result for the stored result"
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
#[test]
|
|
535
|
+
fn stuck_cancel_persists_suppression_and_stuck_list_reads_state() {
|
|
536
|
+
let ws = tmp_workspace();
|
|
537
|
+
seed_collect_state(&ws);
|
|
538
|
+
let out = json_output(
|
|
539
|
+
cmd_stuck_cancel(&StuckCancelArgs {
|
|
540
|
+
agent: "fake_impl".to_string(),
|
|
541
|
+
workspace: ws.clone(),
|
|
542
|
+
alert_type: None,
|
|
543
|
+
json: true,
|
|
544
|
+
})
|
|
545
|
+
.unwrap(),
|
|
546
|
+
);
|
|
547
|
+
let team_key = seeded_team_key(&ws);
|
|
548
|
+
assert_eq!(out["ok"], json!(true));
|
|
549
|
+
assert_eq!(out["alert_types"], json!(["cross_worker_deadlock", "idle_fallback", "stuck"]));
|
|
550
|
+
assert!(out["suppressed"]["idle_fallback"]["snapshot"]["assigned_task_ids"]
|
|
551
|
+
.as_array()
|
|
552
|
+
.unwrap()
|
|
553
|
+
.contains(&json!("task_impl")));
|
|
554
|
+
let state = read_state(&ws);
|
|
555
|
+
assert_eq!(
|
|
556
|
+
state["coordinator"]["suppressed_idle_alerts"][&team_key]["fake_impl"]["idle_fallback"]["suppressed_by"],
|
|
557
|
+
json!("leader")
|
|
558
|
+
);
|
|
559
|
+
assert!(
|
|
560
|
+
read_events(&ws)
|
|
561
|
+
.iter()
|
|
562
|
+
.any(|e| e["event"] == json!("coordinator.idle_alert_suppressed")
|
|
563
|
+
&& e["agent_id"] == json!("fake_impl")),
|
|
564
|
+
"stuck_cancel must write coordinator.idle_alert_suppressed"
|
|
565
|
+
);
|
|
566
|
+
let listed = json_output(
|
|
567
|
+
cmd_stuck_list(&StuckListArgs {
|
|
568
|
+
workspace: ws.clone(),
|
|
569
|
+
json: true,
|
|
570
|
+
})
|
|
571
|
+
.unwrap(),
|
|
572
|
+
);
|
|
573
|
+
assert_eq!(
|
|
574
|
+
listed["suppressed_idle_alerts"]["fake_impl"]["stuck"]["suppressed_by"],
|
|
575
|
+
json!("leader"),
|
|
576
|
+
"stuck-list must read the persisted state mirror, not return a hard-coded empty list"
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
#[test]
|
|
580
|
+
fn stuck_cancel_invalid_alert_type_is_rejected() {
|
|
581
|
+
let ws = tmp_workspace();
|
|
582
|
+
seed_collect_state(&ws);
|
|
583
|
+
let code = run(
|
|
584
|
+
&cli_argv(&[
|
|
585
|
+
"stuck-cancel",
|
|
586
|
+
"fake_impl",
|
|
587
|
+
"--workspace",
|
|
588
|
+
&ws.to_string_lossy(),
|
|
589
|
+
"--alert-type",
|
|
590
|
+
"bogus",
|
|
591
|
+
"--json",
|
|
592
|
+
]),
|
|
593
|
+
&ws,
|
|
594
|
+
);
|
|
595
|
+
assert_eq!(
|
|
596
|
+
code,
|
|
597
|
+
ExitCode::Error,
|
|
598
|
+
"Python rejects alert_type outside stuck/idle_fallback/cross_worker_deadlock/all; Rust must not silently coerce bogus to stuck"
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
#[test]
|
|
602
|
+
fn acknowledge_idle_records_manual_idle_fallback_suppression_and_event() {
|
|
603
|
+
let ws = tmp_workspace();
|
|
604
|
+
seed_collect_state(&ws);
|
|
605
|
+
let out = json_output(
|
|
606
|
+
cmd_acknowledge_idle(&AcknowledgeIdleArgs {
|
|
607
|
+
team: None,
|
|
608
|
+
workspace: ws.clone(),
|
|
609
|
+
json: true,
|
|
610
|
+
})
|
|
611
|
+
.unwrap(),
|
|
612
|
+
);
|
|
613
|
+
let team_key = seeded_team_key(&ws);
|
|
614
|
+
assert_eq!(out["ok"], json!(true));
|
|
615
|
+
assert_eq!(out["team"], json!(team_key));
|
|
616
|
+
assert_eq!(out["ttl_seconds"], json!(1800));
|
|
617
|
+
let state = read_state(&ws);
|
|
618
|
+
let ack = &state["coordinator"]["idle_acknowledged"][&team_key];
|
|
619
|
+
assert_eq!(ack["ttl_seconds"], json!(1800));
|
|
620
|
+
assert!(ack["acknowledged_at"].as_str().is_some());
|
|
621
|
+
assert_eq!(
|
|
622
|
+
state["coordinator"]["suppressed_idle_alerts"][&team_key]["fake_impl"]["idle_fallback"]["suppressed_by"],
|
|
623
|
+
json!("manual_acknowledge")
|
|
624
|
+
);
|
|
625
|
+
assert_eq!(
|
|
626
|
+
state["coordinator"]["suppressed_idle_alerts"][&team_key]["fake_impl"]["idle_fallback"]["manual_acknowledge"],
|
|
627
|
+
json!(true)
|
|
628
|
+
);
|
|
629
|
+
assert!(
|
|
630
|
+
read_events(&ws)
|
|
631
|
+
.iter()
|
|
632
|
+
.any(|e| e["event"] == json!("coordinator.idle_acknowledged")
|
|
633
|
+
&& e["team"] == json!(team_key)
|
|
634
|
+
&& e["ttl_seconds"] == json!(1800)),
|
|
635
|
+
"acknowledge-idle must emit coordinator.idle_acknowledged"
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
#[test]
|
|
639
|
+
fn repair_state_done_persists_after_status_and_summary() {
|
|
640
|
+
let ws = tmp_workspace();
|
|
641
|
+
seed_collect_state(&ws);
|
|
642
|
+
let out = json_output(
|
|
643
|
+
cmd_repair_state(&RepairStateArgs {
|
|
644
|
+
workspace: ws.clone(),
|
|
645
|
+
task_id: "task_impl".to_string(),
|
|
646
|
+
assignee: None,
|
|
647
|
+
status: "done".to_string(),
|
|
648
|
+
summary: Some("manual repair accepted".to_string()),
|
|
649
|
+
json: true,
|
|
650
|
+
})
|
|
651
|
+
.expect("repair-state should not fail on a valid runtime state"),
|
|
652
|
+
);
|
|
653
|
+
assert_eq!(out["ok"], json!(true));
|
|
654
|
+
assert_eq!(
|
|
655
|
+
out["after"]["status"],
|
|
656
|
+
json!("done"),
|
|
657
|
+
"repair-state --status done must return after.status=done; ok:true with null after fields is a false success"
|
|
658
|
+
);
|
|
659
|
+
assert_eq!(out["after"]["assignee"], json!("fake_impl"));
|
|
660
|
+
assert_eq!(out["after"]["last_result_summary"], json!("manual repair accepted"));
|
|
661
|
+
let state = read_state(&ws);
|
|
662
|
+
let task = state["tasks"]
|
|
663
|
+
.as_array()
|
|
664
|
+
.unwrap()
|
|
665
|
+
.iter()
|
|
666
|
+
.find(|task| task["id"] == json!("task_impl"))
|
|
667
|
+
.unwrap();
|
|
668
|
+
assert_eq!(
|
|
669
|
+
task["status"],
|
|
670
|
+
json!("done"),
|
|
671
|
+
"repair-state --status done must persist the task status, not only emit a success envelope"
|
|
672
|
+
);
|
|
673
|
+
assert_eq!(task["last_result_summary"], json!("manual repair accepted"));
|
|
674
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
675
|
+
}
|