@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,310 @@
|
|
|
1
|
+
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
2
|
+
use super::*;
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// helpers — concrete golden fixtures (Python v0.2.11 @ 439bef8)
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
/// `MessageStore.SCHEMA_VERSION == 3` (both Python & Rust db/schema.rs:13). metadata 三元之一。
|
|
9
|
+
const GOLDEN_SCHEMA_VERSION: i64 = 3;
|
|
10
|
+
|
|
11
|
+
fn ws() -> WorkspacePath {
|
|
12
|
+
WorkspacePath::new("/tmp/team-agent-coord-test")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fn meta(pid: u32, proto: u32, schema: i64) -> CoordinatorMetadata {
|
|
16
|
+
CoordinatorMetadata {
|
|
17
|
+
pid: Pid(pid),
|
|
18
|
+
protocol_version: proto,
|
|
19
|
+
message_store_schema_version: schema,
|
|
20
|
+
source: MetadataSource::Boot,
|
|
21
|
+
updated_at: "2026-06-02T00:00:00+00:00".to_string(),
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// `ProviderRegistry` mock — 断言**零** provider-client 调用 (MUST-NOT-13 / §84).
|
|
26
|
+
/// `adapter_for`/`error_lists` 各记一次调用计数;abnormal-track 只允许触碰 `error_lists`,
|
|
27
|
+
/// 绝不触碰 `adapter_for`(那会走真实 provider client crate)。
|
|
28
|
+
struct MockRegistry {
|
|
29
|
+
whitelist: Vec<String>,
|
|
30
|
+
blacklist: Vec<String>,
|
|
31
|
+
adapter_calls: std::cell::Cell<u32>,
|
|
32
|
+
error_list_calls: std::cell::Cell<u32>,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
impl MockRegistry {
|
|
36
|
+
fn new(whitelist: &[&str], blacklist: &[&str]) -> Self {
|
|
37
|
+
Self {
|
|
38
|
+
whitelist: whitelist.iter().map(|s| s.to_string()).collect(),
|
|
39
|
+
blacklist: blacklist.iter().map(|s| s.to_string()).collect(),
|
|
40
|
+
adapter_calls: std::cell::Cell::new(0),
|
|
41
|
+
error_list_calls: std::cell::Cell::new(0),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
impl ProviderRegistry for MockRegistry {
|
|
47
|
+
fn adapter_for(&self, _provider: Provider) -> Box<dyn ProviderAdapter> {
|
|
48
|
+
// 任何对它的调用都违反 §84 zero-injection;计数,断言保持 0。
|
|
49
|
+
self.adapter_calls.set(self.adapter_calls.get() + 1);
|
|
50
|
+
crate::provider::get_adapter(_provider)
|
|
51
|
+
}
|
|
52
|
+
fn error_lists(&self, _provider: Provider) -> ErrorLists {
|
|
53
|
+
self.error_list_calls.set(self.error_list_calls.get() + 1);
|
|
54
|
+
ErrorLists {
|
|
55
|
+
whitelist: self.whitelist.clone(),
|
|
56
|
+
blacklist: self.blacklist.clone(),
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// in-memory `MarkerStore` —— durable marker 落盘探针。
|
|
62
|
+
struct MapMarkerStore {
|
|
63
|
+
markers: std::collections::BTreeMap<String, Value>,
|
|
64
|
+
fail: bool,
|
|
65
|
+
}
|
|
66
|
+
impl MapMarkerStore {
|
|
67
|
+
fn ok() -> Self {
|
|
68
|
+
Self { markers: Default::default(), fail: false }
|
|
69
|
+
}
|
|
70
|
+
fn failing() -> Self {
|
|
71
|
+
Self { markers: Default::default(), fail: true }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
impl MarkerStore for MapMarkerStore {
|
|
75
|
+
fn set_marker(&mut self, name: &str, value: Value) -> bool {
|
|
76
|
+
if self.fail {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
self.markers.insert(name.to_string(), value);
|
|
80
|
+
true
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
85
|
+
// Mock `Transport` — records every method call (name) + returns canned values.
|
|
86
|
+
// The §84/no-tmux/no-panic tick contracts inject THIS so a Coordinator can be
|
|
87
|
+
// constructed in tests. `has_session` answer drives the tmux-session-missing gate.
|
|
88
|
+
// Recorded `calls` let a future tick-order test assert which control-plane probes
|
|
89
|
+
// ran. capture/query return canned text so tick's readonly detectors stay
|
|
90
|
+
// provider-neutral (no real subprocess, no provider client). MUST-NOT-13: this
|
|
91
|
+
// mock NEVER reaches a provider client crate.
|
|
92
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
93
|
+
use crate::transport::{
|
|
94
|
+
test_support::OfflineTransport, AttachOutcome, BackendKind, CaptureRange, CapturedText,
|
|
95
|
+
InjectPayload, InjectReport, Key, PaneField, PaneId, PaneInfo, PaneLiveness, SessionName,
|
|
96
|
+
SetEnvOutcome, SpawnResult, Target, Transport, TransportError, WindowName,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
struct MockTransport {
|
|
100
|
+
inner: OfflineTransport,
|
|
101
|
+
calls: std::sync::Arc<std::sync::Mutex<Vec<&'static str>>>,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
impl MockTransport {
|
|
105
|
+
fn new(session_present: bool) -> Self {
|
|
106
|
+
Self {
|
|
107
|
+
inner: OfflineTransport::new().with_session_present(session_present),
|
|
108
|
+
calls: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
fn record(&self, name: &'static str) {
|
|
112
|
+
self.calls.lock().unwrap().push(name);
|
|
113
|
+
}
|
|
114
|
+
fn calls(&self) -> Vec<&'static str> {
|
|
115
|
+
self.calls.lock().unwrap().clone()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
impl Transport for MockTransport {
|
|
120
|
+
fn kind(&self) -> BackendKind {
|
|
121
|
+
self.inner.kind()
|
|
122
|
+
}
|
|
123
|
+
fn spawn_first(
|
|
124
|
+
&self,
|
|
125
|
+
s: &SessionName,
|
|
126
|
+
w: &WindowName,
|
|
127
|
+
argv: &[String],
|
|
128
|
+
cwd: &std::path::Path,
|
|
129
|
+
env: &std::collections::BTreeMap<String, String>,
|
|
130
|
+
) -> Result<SpawnResult, TransportError> {
|
|
131
|
+
self.record("spawn_first");
|
|
132
|
+
self.inner.spawn_first(s, w, argv, cwd, env)
|
|
133
|
+
}
|
|
134
|
+
fn spawn_into(
|
|
135
|
+
&self,
|
|
136
|
+
s: &SessionName,
|
|
137
|
+
w: &WindowName,
|
|
138
|
+
argv: &[String],
|
|
139
|
+
cwd: &std::path::Path,
|
|
140
|
+
env: &std::collections::BTreeMap<String, String>,
|
|
141
|
+
) -> Result<SpawnResult, TransportError> {
|
|
142
|
+
self.record("spawn_into");
|
|
143
|
+
self.inner.spawn_into(s, w, argv, cwd, env)
|
|
144
|
+
}
|
|
145
|
+
fn inject(
|
|
146
|
+
&self,
|
|
147
|
+
t: &Target,
|
|
148
|
+
p: &InjectPayload,
|
|
149
|
+
submit: Key,
|
|
150
|
+
bracketed: bool,
|
|
151
|
+
) -> Result<InjectReport, TransportError> {
|
|
152
|
+
self.record("inject");
|
|
153
|
+
self.inner.inject(t, p, submit, bracketed)
|
|
154
|
+
}
|
|
155
|
+
fn send_keys(&self, t: &Target, keys: &[Key]) -> Result<(), TransportError> {
|
|
156
|
+
self.record("send_keys");
|
|
157
|
+
self.inner.send_keys(t, keys)
|
|
158
|
+
}
|
|
159
|
+
fn capture(
|
|
160
|
+
&self,
|
|
161
|
+
t: &Target,
|
|
162
|
+
range: CaptureRange,
|
|
163
|
+
) -> Result<CapturedText, TransportError> {
|
|
164
|
+
self.record("capture");
|
|
165
|
+
self.inner.capture(t, range)
|
|
166
|
+
}
|
|
167
|
+
fn query(
|
|
168
|
+
&self,
|
|
169
|
+
t: &Target,
|
|
170
|
+
f: PaneField,
|
|
171
|
+
) -> Result<Option<String>, TransportError> {
|
|
172
|
+
self.record("query");
|
|
173
|
+
self.inner.query(t, f)
|
|
174
|
+
}
|
|
175
|
+
fn liveness(&self, pane: &PaneId) -> Result<PaneLiveness, TransportError> {
|
|
176
|
+
self.record("liveness");
|
|
177
|
+
self.inner.liveness(pane)
|
|
178
|
+
}
|
|
179
|
+
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
180
|
+
self.record("list_targets");
|
|
181
|
+
self.inner.list_targets()
|
|
182
|
+
}
|
|
183
|
+
fn has_session(&self, s: &SessionName) -> Result<bool, TransportError> {
|
|
184
|
+
self.record("has_session");
|
|
185
|
+
self.inner.has_session(s)
|
|
186
|
+
}
|
|
187
|
+
fn list_windows(
|
|
188
|
+
&self,
|
|
189
|
+
s: &SessionName,
|
|
190
|
+
) -> Result<Vec<WindowName>, TransportError> {
|
|
191
|
+
self.record("list_windows");
|
|
192
|
+
self.inner.list_windows(s)
|
|
193
|
+
}
|
|
194
|
+
fn set_session_env(
|
|
195
|
+
&self,
|
|
196
|
+
s: &SessionName,
|
|
197
|
+
k: &str,
|
|
198
|
+
v: &str,
|
|
199
|
+
) -> Result<SetEnvOutcome, TransportError> {
|
|
200
|
+
self.record("set_session_env");
|
|
201
|
+
self.inner.set_session_env(s, k, v)
|
|
202
|
+
}
|
|
203
|
+
fn kill_session(&self, s: &SessionName) -> Result<(), TransportError> {
|
|
204
|
+
self.record("kill_session");
|
|
205
|
+
self.inner.kill_session(s)
|
|
206
|
+
}
|
|
207
|
+
fn kill_window(&self, t: &Target) -> Result<(), TransportError> {
|
|
208
|
+
self.record("kill_window");
|
|
209
|
+
self.inner.kill_window(t)
|
|
210
|
+
}
|
|
211
|
+
fn attach_session(
|
|
212
|
+
&self,
|
|
213
|
+
s: &SessionName,
|
|
214
|
+
) -> Result<AttachOutcome, TransportError> {
|
|
215
|
+
self.record("attach_session");
|
|
216
|
+
self.inner.attach_session(s)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// Construct a `Coordinator` over a fresh temp workspace with injected mocks.
|
|
221
|
+
/// Returns `(coord, transport_calls_handle)` so tick tests can assert control-plane
|
|
222
|
+
/// call order and that `inject` (an exploratory prompt) never fired (§84).
|
|
223
|
+
/// `session_present` drives the tmux-session-missing gate; `save_hook` injects a
|
|
224
|
+
/// forced save failure (bug-084); `recorder` captures tick side-effect ORDER.
|
|
225
|
+
fn coord_for_test(
|
|
226
|
+
session_present: bool,
|
|
227
|
+
save_hook: Option<SaveHook>,
|
|
228
|
+
recorder: Option<OrderRecorder>,
|
|
229
|
+
) -> (Coordinator, std::sync::Arc<std::sync::Mutex<Vec<&'static str>>>) {
|
|
230
|
+
let dir = std::env::temp_dir().join(format!(
|
|
231
|
+
"team-agent-coord-tick-{}-{}",
|
|
232
|
+
std::process::id(),
|
|
233
|
+
std::time::SystemTime::now()
|
|
234
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
235
|
+
.unwrap()
|
|
236
|
+
.as_nanos()
|
|
237
|
+
));
|
|
238
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
239
|
+
let ws = WorkspacePath::new(dir);
|
|
240
|
+
let reg: Box<dyn ProviderRegistry> = Box::new(MockRegistry::new(&[], &[]));
|
|
241
|
+
let transport = MockTransport::new(session_present);
|
|
242
|
+
let calls = std::sync::Arc::clone(&transport.calls);
|
|
243
|
+
let coord = Coordinator::for_test(
|
|
244
|
+
ws,
|
|
245
|
+
reg,
|
|
246
|
+
Box::new(transport),
|
|
247
|
+
save_hook,
|
|
248
|
+
recorder,
|
|
249
|
+
);
|
|
250
|
+
(coord, calls)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/// Like [`coord_for_test`] but seeds a TRUTHY `session_name` into the workspace
|
|
254
|
+
/// state.json first, so the tmux-session gate actually runs. The session-missing
|
|
255
|
+
/// STOP path requires a truthy session_name (Python lifecycle.py:276
|
|
256
|
+
/// `if session_name and not _tmux_session_exists(...)`); a null/empty name skips
|
|
257
|
+
/// the gate entirely (see `p2_tick_skips_tmux_gate_when_session_name_absent`).
|
|
258
|
+
fn coord_for_test_with_session(
|
|
259
|
+
session_present: bool,
|
|
260
|
+
session_name: &str,
|
|
261
|
+
) -> (Coordinator, std::sync::Arc<std::sync::Mutex<Vec<&'static str>>>) {
|
|
262
|
+
let dir = std::env::temp_dir().join(format!(
|
|
263
|
+
"team-agent-coord-tick-sess-{}-{}",
|
|
264
|
+
std::process::id(),
|
|
265
|
+
std::time::SystemTime::now()
|
|
266
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
267
|
+
.unwrap()
|
|
268
|
+
.as_nanos()
|
|
269
|
+
));
|
|
270
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
271
|
+
crate::state::persist::save_runtime_state(
|
|
272
|
+
&dir,
|
|
273
|
+
&serde_json::json!({ "session_name": session_name }),
|
|
274
|
+
)
|
|
275
|
+
.unwrap();
|
|
276
|
+
let ws = WorkspacePath::new(dir);
|
|
277
|
+
let reg: Box<dyn ProviderRegistry> = Box::new(MockRegistry::new(&[], &[]));
|
|
278
|
+
let transport = MockTransport::new(session_present);
|
|
279
|
+
let calls = std::sync::Arc::clone(&transport.calls);
|
|
280
|
+
let coord = Coordinator::for_test(ws, reg, Box::new(transport), None, None);
|
|
281
|
+
(coord, calls)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/// A save hook that always fails (bug-084 forced persistence failure).
|
|
285
|
+
fn failing_save_hook() -> SaveHook {
|
|
286
|
+
Box::new(|_ws, _state| {
|
|
287
|
+
Err(crate::state::StateError::SaveFailed(
|
|
288
|
+
"injected tick-end save failure".to_string(),
|
|
289
|
+
))
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
fn read_event_log_dir(dir: &std::path::Path) -> Vec<serde_json::Value> {
|
|
294
|
+
let path = crate::model::paths::logs_dir(dir).join("events.jsonl");
|
|
295
|
+
match std::fs::read_to_string(&path) {
|
|
296
|
+
Ok(text) => text.lines().filter_map(|l| serde_json::from_str(l).ok()).collect(),
|
|
297
|
+
Err(_) => Vec::new(),
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
mod basics;
|
|
303
|
+
mod abnormal;
|
|
304
|
+
mod watch;
|
|
305
|
+
mod tick_core;
|
|
306
|
+
mod spine;
|
|
307
|
+
mod health_sync;
|
|
308
|
+
mod takeover;
|
|
309
|
+
mod daemon;
|
|
310
|
+
mod main_preserved;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
4
|
+
// SPINE — Coordinator::tick() must drive REAL cross-subsystem side-effects
|
|
5
|
+
// (orchestration port, sub-phase 1, P0). Golden: coordinator/lifecycle.py:250-385
|
|
6
|
+
// (deliver_pending → fire_scheduled → … → save_runtime_state LAST). Today the 14
|
|
7
|
+
// obligation steps are bare `record_step` probes (tick.rs:171) and base_tick_report
|
|
8
|
+
// (tick.rs:345) fabricates empty delivered/scheduled/stuck/results vecs, so these
|
|
9
|
+
// integration tests — wiring REAL state.json + team.db, only the OS edge mocked —
|
|
10
|
+
// FAIL (RED) and pass once the porter wires tick to the real messaging fns.
|
|
11
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
12
|
+
|
|
13
|
+
/// Non-panicking transport: mirrors `MockTransport` but `inject` RECORDS and returns Ok
|
|
14
|
+
/// (a real `deliver_pending` obligation legitimately injects; the §84-guard `MockTransport`
|
|
15
|
+
/// panics on inject, which is correct for the no-obligation tick but not for a delivery test).
|
|
16
|
+
pub(super) struct DeliveringTransport {
|
|
17
|
+
inner: MockTransport,
|
|
18
|
+
}
|
|
19
|
+
impl DeliveringTransport {
|
|
20
|
+
pub(super) fn new() -> Self {
|
|
21
|
+
Self { inner: MockTransport::new(true) }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
impl Transport for DeliveringTransport {
|
|
25
|
+
fn kind(&self) -> BackendKind {
|
|
26
|
+
self.inner.kind()
|
|
27
|
+
}
|
|
28
|
+
fn spawn_first(
|
|
29
|
+
&self,
|
|
30
|
+
s: &SessionName,
|
|
31
|
+
w: &WindowName,
|
|
32
|
+
a: &[String],
|
|
33
|
+
c: &std::path::Path,
|
|
34
|
+
e: &std::collections::BTreeMap<String, String>,
|
|
35
|
+
) -> Result<SpawnResult, TransportError> {
|
|
36
|
+
self.inner.spawn_first(s, w, a, c, e)
|
|
37
|
+
}
|
|
38
|
+
fn spawn_into(
|
|
39
|
+
&self,
|
|
40
|
+
s: &SessionName,
|
|
41
|
+
w: &WindowName,
|
|
42
|
+
a: &[String],
|
|
43
|
+
c: &std::path::Path,
|
|
44
|
+
e: &std::collections::BTreeMap<String, String>,
|
|
45
|
+
) -> Result<SpawnResult, TransportError> {
|
|
46
|
+
self.inner.spawn_into(s, w, a, c, e)
|
|
47
|
+
}
|
|
48
|
+
fn inject(
|
|
49
|
+
&self,
|
|
50
|
+
_t: &Target,
|
|
51
|
+
_p: &InjectPayload,
|
|
52
|
+
_submit: Key,
|
|
53
|
+
_bracketed: bool,
|
|
54
|
+
) -> Result<InjectReport, TransportError> {
|
|
55
|
+
self.inner.record("inject");
|
|
56
|
+
Ok(InjectReport {
|
|
57
|
+
stage_reached: crate::transport::InjectStage::Submit,
|
|
58
|
+
inject_verification: crate::transport::InjectVerification::CaptureContainsToken,
|
|
59
|
+
submit_verification: crate::transport::SubmitVerification::EnterSentWithoutPlaceholderCheck,
|
|
60
|
+
turn_verification: crate::transport::TurnVerification::NotYetObserved,
|
|
61
|
+
attempts: 1,
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
fn send_keys(&self, t: &Target, k: &[Key]) -> Result<(), TransportError> {
|
|
65
|
+
self.inner.send_keys(t, k)
|
|
66
|
+
}
|
|
67
|
+
fn capture(&self, t: &Target, r: CaptureRange) -> Result<CapturedText, TransportError> {
|
|
68
|
+
self.inner.capture(t, r)
|
|
69
|
+
}
|
|
70
|
+
fn query(&self, t: &Target, f: PaneField) -> Result<Option<String>, TransportError> {
|
|
71
|
+
self.inner.query(t, f)
|
|
72
|
+
}
|
|
73
|
+
fn liveness(&self, p: &PaneId) -> Result<PaneLiveness, TransportError> {
|
|
74
|
+
self.inner.liveness(p)
|
|
75
|
+
}
|
|
76
|
+
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
77
|
+
self.inner.list_targets()
|
|
78
|
+
}
|
|
79
|
+
fn has_session(&self, s: &SessionName) -> Result<bool, TransportError> {
|
|
80
|
+
self.inner.has_session(s)
|
|
81
|
+
}
|
|
82
|
+
fn list_windows(&self, s: &SessionName) -> Result<Vec<WindowName>, TransportError> {
|
|
83
|
+
self.inner.list_windows(s)
|
|
84
|
+
}
|
|
85
|
+
fn set_session_env(&self, s: &SessionName, k: &str, v: &str) -> Result<SetEnvOutcome, TransportError> {
|
|
86
|
+
self.inner.set_session_env(s, k, v)
|
|
87
|
+
}
|
|
88
|
+
fn kill_session(&self, s: &SessionName) -> Result<(), TransportError> {
|
|
89
|
+
self.inner.kill_session(s)
|
|
90
|
+
}
|
|
91
|
+
fn kill_window(&self, t: &Target) -> Result<(), TransportError> {
|
|
92
|
+
self.inner.kill_window(t)
|
|
93
|
+
}
|
|
94
|
+
fn attach_session(&self, s: &SessionName) -> Result<AttachOutcome, TransportError> {
|
|
95
|
+
self.inner.attach_session(s)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Build a `Coordinator` over a freshly-seeded REAL workspace: state.json carries a truthy
|
|
100
|
+
/// `session_name` (so the gate runs) + a worker agents map. Returns the workspace dir so the
|
|
101
|
+
/// test can seed/inspect the REAL team.db. `transport` is the injected OS edge; `save_hook`
|
|
102
|
+
/// injects the bug-084 forced save failure.
|
|
103
|
+
fn seeded_spine_coord(
|
|
104
|
+
transport: Box<dyn Transport>,
|
|
105
|
+
save_hook: Option<SaveHook>,
|
|
106
|
+
) -> (Coordinator, std::path::PathBuf) {
|
|
107
|
+
let dir = std::env::temp_dir().join(format!(
|
|
108
|
+
"team-agent-coord-spine-{}-{}",
|
|
109
|
+
std::process::id(),
|
|
110
|
+
std::time::SystemTime::now()
|
|
111
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
112
|
+
.unwrap()
|
|
113
|
+
.as_nanos()
|
|
114
|
+
));
|
|
115
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
116
|
+
crate::state::persist::save_runtime_state(
|
|
117
|
+
&dir,
|
|
118
|
+
&serde_json::json!({
|
|
119
|
+
"session_name": "team-spine",
|
|
120
|
+
"agents": { "w1": { "provider": "codex" } },
|
|
121
|
+
}),
|
|
122
|
+
)
|
|
123
|
+
.unwrap();
|
|
124
|
+
let ws = WorkspacePath::new(dir.clone());
|
|
125
|
+
let reg: Box<dyn ProviderRegistry> = Box::new(MockRegistry::new(&[], &[]));
|
|
126
|
+
let coord = Coordinator::for_test(ws, reg, transport, save_hook, None);
|
|
127
|
+
(coord, dir)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Insert ONE due `health_ping` scheduled_event (status pending, far-past due) into the REAL
|
|
131
|
+
/// team.db. `fire_due_scheduled_events` fully handles `health_ping` (logs, no transport inject)
|
|
132
|
+
/// and marks the row `done`. Returns the autoincrement id.
|
|
133
|
+
fn seed_due_health_ping(dir: &std::path::Path) -> i64 {
|
|
134
|
+
let store = MessageStore::open(dir).unwrap();
|
|
135
|
+
let conn = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
136
|
+
conn.execute(
|
|
137
|
+
"insert into scheduled_events(owner_team_id, due_at, target, kind, payload_json, status, created_at) \
|
|
138
|
+
values (null, '2000-01-01T00:00:00+00:00', 'w1', 'health_ping', '{}', 'pending', '2000-01-01T00:00:00+00:00')",
|
|
139
|
+
[],
|
|
140
|
+
)
|
|
141
|
+
.unwrap();
|
|
142
|
+
conn.last_insert_rowid()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fn scheduled_status(dir: &std::path::Path, id: i64) -> String {
|
|
146
|
+
let store = MessageStore::open(dir).unwrap();
|
|
147
|
+
let conn = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
148
|
+
conn.query_row("select status from scheduled_events where id = ?1", [id], |r| r.get::<_, String>(0))
|
|
149
|
+
.unwrap()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
pub(super) fn message_status(dir: &std::path::Path, message_id: &str) -> String {
|
|
153
|
+
let store = MessageStore::open(dir).unwrap();
|
|
154
|
+
let conn = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
155
|
+
conn.query_row(
|
|
156
|
+
"select status from messages where message_id = ?1",
|
|
157
|
+
[message_id],
|
|
158
|
+
|r| r.get::<_, String>(0),
|
|
159
|
+
)
|
|
160
|
+
.unwrap()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// P0 — tick must FIRE a due scheduled event against the REAL team.db (fire_due_scheduled_events,
|
|
164
|
+
// lifecycle.py:286). Observable: report.scheduled carries the id AND the db row is marked 'done'.
|
|
165
|
+
#[test]
|
|
166
|
+
fn spine_tick_fires_due_scheduled_event_and_marks_db_done() {
|
|
167
|
+
let (coord, dir) = seeded_spine_coord(Box::new(MockTransport::new(true)), None);
|
|
168
|
+
let id = seed_due_health_ping(&dir);
|
|
169
|
+
assert_eq!(scheduled_status(&dir, id), "pending", "precondition: the seeded row starts pending");
|
|
170
|
+
|
|
171
|
+
let report = coord.tick().expect("tick returns a typed report");
|
|
172
|
+
|
|
173
|
+
assert!(
|
|
174
|
+
report.scheduled.iter().any(|e| e.id == id),
|
|
175
|
+
"tick must report the fired scheduled event id {id} (today base_tick_report fabricates []); got {:?}",
|
|
176
|
+
report.scheduled
|
|
177
|
+
);
|
|
178
|
+
assert_eq!(
|
|
179
|
+
scheduled_status(&dir, id),
|
|
180
|
+
"done",
|
|
181
|
+
"fire_due_scheduled_events must mark the scheduled_events row 'done' in the REAL team.db"
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// P1 — bug-084 save-LAST ordering with a REAL side-effect: a failing save_hook must NOT undo the
|
|
186
|
+
// scheduled fire. The db mutation commits BEFORE atomic_save (lifecycle.py:286 then :346), and the
|
|
187
|
+
// degraded report still carries `scheduled` (lifecycle.py:356). Proves save is genuinely the LAST
|
|
188
|
+
// mutation — AFTER the real obligation side-effects, not after no-op probes.
|
|
189
|
+
#[test]
|
|
190
|
+
fn spine_tick_save_failure_still_persists_real_scheduled_mutation() {
|
|
191
|
+
let (coord, dir) = seeded_spine_coord(Box::new(MockTransport::new(true)), Some(failing_save_hook()));
|
|
192
|
+
let id = seed_due_health_ping(&dir);
|
|
193
|
+
|
|
194
|
+
let report = coord.tick().expect("a degraded tick is Ok(TickReport), not Err");
|
|
195
|
+
|
|
196
|
+
// degraded report shape (bug-084: save failure → ok=false / persisted=false, NOT Err).
|
|
197
|
+
assert!(!report.ok, "save failure → ok=false");
|
|
198
|
+
assert_eq!(report.persisted, Some(false), "save failure → persisted=Some(false)");
|
|
199
|
+
assert_eq!(report.reason, Some(TickStopReason::PersistenceDegraded));
|
|
200
|
+
// the REAL db side-effect happened BEFORE the (failed) save.
|
|
201
|
+
assert_eq!(
|
|
202
|
+
scheduled_status(&dir, id),
|
|
203
|
+
"done",
|
|
204
|
+
"the scheduled fire must commit to the db BEFORE save — save is the LAST mutation (bug-084)"
|
|
205
|
+
);
|
|
206
|
+
// and the degraded report still carries the fired event.
|
|
207
|
+
assert!(
|
|
208
|
+
report.scheduled.iter().any(|e| e.id == id),
|
|
209
|
+
"the degraded report must still carry the fired scheduled event (lifecycle.py:356)"
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// P0 — tick must DELIVER a pending message against the REAL team.db (deliver_pending_messages,
|
|
214
|
+
// lifecycle.py:285). Observable: report.delivered carries the id OR the message row advances past
|
|
215
|
+
// its created 'accepted' state (claimed/delivered). NOTE: this also needs deliver_pending_messages
|
|
216
|
+
// (messaging/delivery.rs:126, currently a stub returning []) to be implemented — it is RED both
|
|
217
|
+
// because tick does not call it AND because the fn is a stub; it greens when both land.
|
|
218
|
+
#[test]
|
|
219
|
+
fn spine_tick_delivers_pending_message_drives_real_db_or_report() {
|
|
220
|
+
let (coord, dir) = seeded_spine_coord(Box::new(DeliveringTransport::new()), None);
|
|
221
|
+
let store = MessageStore::open(&dir).unwrap();
|
|
222
|
+
let mid = store
|
|
223
|
+
.create_message(Some("task-1"), "leader", "w1", "do the thing", None, true, None)
|
|
224
|
+
.unwrap();
|
|
225
|
+
drop(store);
|
|
226
|
+
assert_eq!(message_status(&dir, &mid), "accepted", "precondition: a fresh message is 'accepted'");
|
|
227
|
+
|
|
228
|
+
let report = coord.tick().expect("tick returns a typed report");
|
|
229
|
+
|
|
230
|
+
let delivered_reported = report.delivered.iter().any(|d| d.message_id == mid);
|
|
231
|
+
let db_advanced = message_status(&dir, &mid) != "accepted";
|
|
232
|
+
assert!(
|
|
233
|
+
delivered_reported || db_advanced,
|
|
234
|
+
"tick must deliver the pending message: report.delivered should carry {mid} OR its db status \
|
|
235
|
+
must advance past 'accepted' (claimed/delivered). report.delivered={:?} db_status={}",
|
|
236
|
+
report.delivered,
|
|
237
|
+
message_status(&dir, &mid)
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
242
|
+
// SPINE-WIRING (③ review→fix) RED — tick tmux-session-missing gate observability.
|
|
243
|
+
// Golden lifecycle.py:277-279: emit a `coordinator.session_missing` event (session=name)
|
|
244
|
+
// BEFORE the stop report. /tmp/spine_divergences.md #5.
|
|
245
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
246
|
+
|
|
247
|
+
#[test]
|
|
248
|
+
fn spine_tick_session_missing_emits_event() {
|
|
249
|
+
// seeded_spine_coord seeds a truthy session_name ("team-spine"); MockTransport(false) makes the
|
|
250
|
+
// tmux gate fire → the tick stops. The gate must ALSO emit coordinator.session_missing.
|
|
251
|
+
let (coord, dir) = seeded_spine_coord(Box::new(MockTransport::new(false)), None);
|
|
252
|
+
let report = coord.tick().expect("tick returns a typed report");
|
|
253
|
+
assert!(report.stop, "precondition: a missing session stops the tick");
|
|
254
|
+
let events = read_event_log_dir(&dir);
|
|
255
|
+
assert!(
|
|
256
|
+
events
|
|
257
|
+
.iter()
|
|
258
|
+
.any(|e| e.get("event").and_then(|v| v.as_str()) == Some("coordinator.session_missing")),
|
|
259
|
+
"the tmux-missing gate must emit a coordinator.session_missing event before the stop report; got {events:?}"
|
|
260
|
+
);
|
|
261
|
+
}
|