@team-agent/installer 0.5.61 → 0.5.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +5 -2
- package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
- package/crates/team-agent/src/cli/diagnose.rs +1 -0
- package/crates/team-agent/src/cli/emit.rs +41 -7
- package/crates/team-agent/src/cli/helpers.rs +1 -0
- package/crates/team-agent/src/cli/leader.rs +6 -1
- package/crates/team-agent/src/cli/leaders.rs +2 -0
- package/crates/team-agent/src/cli/mod.rs +33 -3
- package/crates/team-agent/src/cli/named_address.rs +8 -1
- package/crates/team-agent/src/cli/profile.rs +2 -1
- package/crates/team-agent/src/cli/send/coordinator.rs +1 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +3 -0
- package/crates/team-agent/src/cli/send/persist.rs +1 -0
- package/crates/team-agent/src/cli/send/presentation.rs +1 -0
- package/crates/team-agent/src/cli/send/resolve.rs +206 -1
- package/crates/team-agent/src/cli/send.rs +2 -1
- package/crates/team-agent/src/cli/spec.rs +1 -0
- package/crates/team-agent/src/cli/status.rs +2 -1
- package/crates/team-agent/src/cli/status_port/compact.rs +1 -0
- package/crates/team-agent/src/cli/status_port/format.rs +1 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +1 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +38 -0
- package/crates/team-agent/src/cli/types.rs +3 -2
- package/crates/team-agent/src/communication_mode/mod.rs +1 -0
- package/crates/team-agent/src/compiler.rs +1 -0
- package/crates/team-agent/src/conpty/backend.rs +1 -0
- package/crates/team-agent/src/conpty/mod.rs +1 -0
- package/crates/team-agent/src/coordinator/backoff.rs +5 -8
- package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
- package/crates/team-agent/src/coordinator/health.rs +1 -0
- package/crates/team-agent/src/coordinator/mod.rs +2 -2
- package/crates/team-agent/src/coordinator/orphan.rs +4 -116
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +6 -5
- package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
- package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
- package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +0 -9
- package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
- package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -42
- package/crates/team-agent/src/coordinator/tick.rs +24 -28
- package/crates/team-agent/src/coordinator/types.rs +3 -176
- package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
- package/crates/team-agent/src/db/message_store.rs +2 -1
- package/crates/team-agent/src/db/migration.rs +5 -4
- package/crates/team-agent/src/db/mod.rs +1 -0
- package/crates/team-agent/src/db/schema.rs +1 -0
- package/crates/team-agent/src/diagnose/comms.rs +2 -1
- package/crates/team-agent/src/diagnose/mod.rs +1 -0
- package/crates/team-agent/src/diagnose/orphans.rs +1 -0
- package/crates/team-agent/src/layout/manager.rs +3 -7
- package/crates/team-agent/src/layout/mod.rs +2 -2
- package/crates/team-agent/src/layout/overlay.rs +6 -1
- package/crates/team-agent/src/layout/placement.rs +1 -0
- package/crates/team-agent/src/layout/recovery.rs +3 -0
- package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
- package/crates/team-agent/src/layout/sessions.rs +8 -1
- package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
- package/crates/team-agent/src/layout/worker_env.rs +3 -0
- package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
- package/crates/team-agent/src/leader/helpers.rs +1 -0
- package/crates/team-agent/src/leader/incident.rs +1 -0
- package/crates/team-agent/src/leader/lease.rs +27 -22
- package/crates/team-agent/src/leader/mod.rs +2 -3
- package/crates/team-agent/src/leader/owner_bind.rs +13 -3
- package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
- package/crates/team-agent/src/leader/rediscover.rs +4 -17
- package/crates/team-agent/src/leader/registry.rs +3 -15
- package/crates/team-agent/src/leader/start.rs +373 -125
- package/crates/team-agent/src/leader/takeover.rs +10 -232
- package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
- package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
- package/crates/team-agent/src/leader/tests/identity.rs +7 -2
- package/crates/team-agent/src/leader/tests/idle.rs +0 -193
- package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
- package/crates/team-agent/src/leader/tests/mod.rs +4 -41
- package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
- package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
- package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
- package/crates/team-agent/src/leader/types.rs +1 -28
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/display.rs +7 -1
- package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
- package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -27
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch.rs +11 -12
- package/crates/team-agent/src/lifecycle/mod.rs +2 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +3 -3
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
- package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
- package/crates/team-agent/src/lifecycle/restart.rs +13 -11
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
- package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
- package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
- package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
- package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
- package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
- package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
- package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
- package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
- package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
- package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
- package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
- package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
- package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
- package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
- package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
- package/crates/team-agent/src/lifecycle/tests.rs +59 -0
- package/crates/team-agent/src/lifecycle/types.rs +0 -9
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
- package/crates/team-agent/src/mcp_server/mod.rs +1 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +4 -3
- package/crates/team-agent/src/mcp_server/tools.rs +1 -0
- package/crates/team-agent/src/mcp_server/types.rs +1 -18
- package/crates/team-agent/src/mcp_server/wire.rs +5 -5
- package/crates/team-agent/src/messaging/activity.rs +1 -0
- package/crates/team-agent/src/messaging/address.rs +1 -0
- package/crates/team-agent/src/messaging/delivery.rs +4 -3
- package/crates/team-agent/src/messaging/helpers.rs +2 -1
- package/crates/team-agent/src/messaging/leader_channel.rs +1 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +2 -1
- package/crates/team-agent/src/messaging/mod.rs +10 -14
- package/crates/team-agent/src/messaging/peers.rs +2 -0
- package/crates/team-agent/src/messaging/persist.rs +3 -1
- package/crates/team-agent/src/messaging/presentation.rs +2 -1
- package/crates/team-agent/src/messaging/results.rs +1 -0
- package/crates/team-agent/src/messaging/scheduler.rs +1 -0
- package/crates/team-agent/src/messaging/selftest.rs +1 -1
- package/crates/team-agent/src/messaging/send.rs +41 -8
- package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
- package/crates/team-agent/src/messaging/types.rs +2 -2
- package/crates/team-agent/src/messaging/watchers.rs +19 -13
- package/crates/team-agent/src/model/enums.rs +1 -0
- package/crates/team-agent/src/model/errors.rs +1 -0
- package/crates/team-agent/src/model/ids.rs +1 -0
- package/crates/team-agent/src/model/mod.rs +1 -0
- package/crates/team-agent/src/model/name_similarity.rs +1 -0
- package/crates/team-agent/src/model/pane_authority_refusal.rs +1 -0
- package/crates/team-agent/src/model/paths.rs +1 -0
- package/crates/team-agent/src/model/permissions.rs +3 -2
- package/crates/team-agent/src/model/routing.rs +1 -0
- package/crates/team-agent/src/model/spec.rs +1 -0
- package/crates/team-agent/src/model/task_graph.rs +1 -0
- package/crates/team-agent/src/model/yaml/tests.rs +1 -0
- package/crates/team-agent/src/model/yaml.rs +1 -0
- package/crates/team-agent/src/packaging/install.rs +9 -147
- package/crates/team-agent/src/packaging/migrate.rs +2 -0
- package/crates/team-agent/src/packaging/mod.rs +1 -0
- package/crates/team-agent/src/packaging/repair.rs +2 -0
- package/crates/team-agent/src/packaging/tests.rs +29 -218
- package/crates/team-agent/src/packaging/types.rs +8 -15
- package/crates/team-agent/src/platform/argv.rs +4 -0
- package/crates/team-agent/src/platform/errors.rs +10 -1
- package/crates/team-agent/src/platform/file_lock.rs +4 -157
- package/crates/team-agent/src/platform/mod.rs +0 -61
- package/crates/team-agent/src/platform/process.rs +1 -0
- package/crates/team-agent/src/provider/adapter.rs +1 -0
- package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
- package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
- package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
- package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
- package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
- package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
- package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
- package/crates/team-agent/src/provider/classify.rs +1 -0
- package/crates/team-agent/src/provider/faults.rs +1 -26
- package/crates/team-agent/src/provider/helpers.rs +1 -0
- package/crates/team-agent/src/provider/mod.rs +1 -1
- package/crates/team-agent/src/provider/session/capture.rs +1 -4
- package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
- package/crates/team-agent/src/provider/session/mod.rs +2 -4
- package/crates/team-agent/src/provider/session/resume.rs +2 -209
- package/crates/team-agent/src/provider/session_scan/claude.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
- package/crates/team-agent/src/provider/session_scan.rs +1 -42
- package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
- package/crates/team-agent/src/provider/types.rs +1 -0
- package/crates/team-agent/src/provider/wire.rs +1 -0
- package/crates/team-agent/src/state/identity.rs +16 -5
- package/crates/team-agent/src/state/identity_keys.rs +1 -0
- package/crates/team-agent/src/state/mod.rs +2 -0
- package/crates/team-agent/src/state/owner_gate.rs +5 -0
- package/crates/team-agent/src/state/ownership.rs +10 -4
- package/crates/team-agent/src/state/paths.rs +12 -0
- package/crates/team-agent/src/state/persist.rs +9 -3
- package/crates/team-agent/src/state/projection.rs +15 -2
- package/crates/team-agent/src/state/repository/intent.rs +1 -0
- package/crates/team-agent/src/state/repository/tests.rs +1 -0
- package/crates/team-agent/src/state/repository.rs +7 -0
- package/crates/team-agent/src/state/selector.rs +1 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
- package/crates/team-agent/src/tmux_backend.rs +63 -0
- package/crates/team-agent/src/transport.rs +15 -0
- package/package.json +4 -4
- package/crates/team-agent/src/leader/inject.rs +0 -33
- package/crates/team-agent/src/provider/command.rs +0 -80
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
//! RED contract — 0.5.57 typed-state car · batch C (P0 upgrade gate)
|
|
2
|
+
//! Historical-inventory upgrade gate: pre-seed a multi-status, multi-era
|
|
3
|
+
//! fixture representing the storm production snapshot; after boot/claim,
|
|
4
|
+
//! the typed contract must produce EXACTLY the expected per-prior-state
|
|
5
|
+
//! transition count, and produce a per-status audit record.
|
|
6
|
+
//!
|
|
7
|
+
//! LINEAGE:
|
|
8
|
+
//! - Baseline: 5b847e4 (0.5.56 tested tip).
|
|
9
|
+
//! - Distillation §3.3 minimum matrix items #1, #6, #7 (historical
|
|
10
|
+
//! backlog / crash cut / upgrade inventory).
|
|
11
|
+
//! - Storm snapshot production shape (
|
|
12
|
+
//! `.team/artifacts/pipeline-runs/mailbox-flush-case/storm-p0-locate.md
|
|
13
|
+
//! §A1-A2`): 346 parked + 153 blocked (leader_not_attached) +
|
|
14
|
+
//! 51 `failed/send_unverified_exhausted` (of which 23 recipient=leader) +
|
|
15
|
+
//! 5 `queued_pane_missing` + 108 null-result historical watchers +
|
|
16
|
+
//! delivered rows preserved.
|
|
17
|
+
//! - Wiki hard rule: fixture predicates MUST derive from the product typed
|
|
18
|
+
//! enum catalog (`MessageRowStatus::ALL`), so no hand-written second state
|
|
19
|
+
//! semantics can exist.
|
|
20
|
+
//!
|
|
21
|
+
//! TEETH (RED at 5b847e4):
|
|
22
|
+
//! 1. `upgrade_gate_only_blocked_rows_flip` — with a full storm-shape
|
|
23
|
+
//! inventory (parked + blocked + delivered + submitted_unverified +
|
|
24
|
+
//! queued_pane_missing + null-result watcher + worker-recipient
|
|
25
|
+
//! parked), a single claim/attach must flip ONLY the truly-blocked
|
|
26
|
+
//! leader rows. Baseline 5b847e4 already passes this thanks to
|
|
27
|
+
//! 0.5.56, so this tooth is an INVARIANT LOCK (green). It's kept
|
|
28
|
+
//! here because the typed refactor MUST preserve it.
|
|
29
|
+
//! 2. `upgrade_gate_emits_per_prior_state_audit_count` — the
|
|
30
|
+
//! `leader_receiver.blocked_messages_requeued` event must carry a
|
|
31
|
+
//! per-prior-status breakdown (e.g. `{ blocked_leader_unbound: 1,
|
|
32
|
+
//! queued_until_leader_attach: 0 }`), not just a scalar `count`.
|
|
33
|
+
//! Baseline only emits `count` → red.
|
|
34
|
+
//! 3. `null_result_historical_watchers_never_rewritten_across_boot`
|
|
35
|
+
//! — 108 null-result historical watchers must be untouched: no
|
|
36
|
+
//! status change, no event write. Baseline 5b847e4 already passes
|
|
37
|
+
//! thanks to 0.5.56's `result_id is not null` guard → invariant
|
|
38
|
+
//! lock (green).
|
|
39
|
+
//! 4. `worker_recipient_parked_rows_have_typed_recovery_owner` — the
|
|
40
|
+
//! 5 `queued_pane_missing` rows (worker-recipient face) must be
|
|
41
|
+
//! pointed to a NAMED typed recovery incident, not left in
|
|
42
|
+
//! undefined-owner limbo. Baseline: no such surface exists → red.
|
|
43
|
+
//! 5. `upgrade_inventory_covers_full_enum` — the fixture itself
|
|
44
|
+
//! exercises every status in `MessageRowStatus::ALL`. A new product
|
|
45
|
+
//! status is therefore seeded automatically rather than copied into
|
|
46
|
+
//! a test-side list.
|
|
47
|
+
//! 6. `injected_awaiting_receipt_is_open_debt_but_not_requeueable` —
|
|
48
|
+
//! the typed parked row remains visible in the undelivered inventory,
|
|
49
|
+
//! while the same row cannot win a delivery claim.
|
|
50
|
+
//!
|
|
51
|
+
//! POSITIVE CONTROL:
|
|
52
|
+
//! 7. `newly_bound_leader_still_flushes_queued_row` — the legitimate
|
|
53
|
+
//! recovery path (blocked leader row → accepted → delivered) is
|
|
54
|
+
//! preserved through the upgrade gate.
|
|
55
|
+
//!
|
|
56
|
+
//! NEGATIVE CONTROL:
|
|
57
|
+
//! 8. `non_leader_parked_rows_are_never_flipped_by_leader_claim` — a
|
|
58
|
+
//! worker-recipient parked row (recipient!=leader, status=
|
|
59
|
+
//! submitted_pending_acceptance) is unaffected by any leader
|
|
60
|
+
//! claim; scope of blocked-leader recovery stays leader-only.
|
|
61
|
+
//!
|
|
62
|
+
//! FROZEN by verifier — do NOT modify without a new SHA256 signature.
|
|
63
|
+
|
|
64
|
+
#![cfg(unix)]
|
|
65
|
+
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
|
|
66
|
+
|
|
67
|
+
use crate as team_agent;
|
|
68
|
+
|
|
69
|
+
#[path = "rehomed_env.rs"]
|
|
70
|
+
mod rehomed_env;
|
|
71
|
+
|
|
72
|
+
use std::collections::BTreeSet;
|
|
73
|
+
use std::path::PathBuf;
|
|
74
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
75
|
+
|
|
76
|
+
use rusqlite::params;
|
|
77
|
+
use serial_test::serial;
|
|
78
|
+
use team_agent::event_log::EventLog;
|
|
79
|
+
use team_agent::leader::{claim_lease_no_incident, LeaseStatus};
|
|
80
|
+
use team_agent::message_store::{MessageRowStatus, MessageStore};
|
|
81
|
+
use team_agent::messaging::enqueue_leader_mailbox_until_attach;
|
|
82
|
+
use team_agent::messaging::watchers::requeue_after_claim_leader;
|
|
83
|
+
use team_agent::model::enums::PaneLiveness;
|
|
84
|
+
use team_agent::model::ids::TeamKey;
|
|
85
|
+
use team_agent::state::owner_gate::PaneLivenessProbe;
|
|
86
|
+
use team_agent::transport::PaneId;
|
|
87
|
+
|
|
88
|
+
fn durable_status_catalog() -> BTreeSet<&'static str> {
|
|
89
|
+
MessageRowStatus::ALL
|
|
90
|
+
.iter()
|
|
91
|
+
.map(|status| status.as_str())
|
|
92
|
+
.collect()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const TEAM: &str = "current";
|
|
96
|
+
const PANE: &str = "%leader";
|
|
97
|
+
|
|
98
|
+
struct LiveCaller;
|
|
99
|
+
impl PaneLivenessProbe for LiveCaller {
|
|
100
|
+
fn liveness(&self, pane_id: &str) -> PaneLiveness {
|
|
101
|
+
if pane_id == PANE {
|
|
102
|
+
PaneLiveness::Live
|
|
103
|
+
} else {
|
|
104
|
+
PaneLiveness::Dead
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fn bound_state() -> serde_json::Value {
|
|
110
|
+
serde_json::json!({
|
|
111
|
+
"session_name": "team-upgrade-gate",
|
|
112
|
+
"team_owner": {
|
|
113
|
+
"pane_id": PANE,
|
|
114
|
+
"provider": "codex",
|
|
115
|
+
"owner_epoch": 7,
|
|
116
|
+
"claimed_at": "2026-07-23T05:13:42Z",
|
|
117
|
+
"claimed_via": "claim-leader"
|
|
118
|
+
},
|
|
119
|
+
"leader_receiver": {
|
|
120
|
+
"pane_id": PANE,
|
|
121
|
+
"provider": "codex",
|
|
122
|
+
"owner_epoch": 7,
|
|
123
|
+
"status": "attached"
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
struct InventoryCase {
|
|
129
|
+
_env: rehomed_env::RehomedTestEnv,
|
|
130
|
+
workspace: PathBuf,
|
|
131
|
+
store: MessageStore,
|
|
132
|
+
event_log: EventLog,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
impl InventoryCase {
|
|
136
|
+
fn new(tag: &str) -> Self {
|
|
137
|
+
static N: AtomicU64 = AtomicU64::new(0);
|
|
138
|
+
let env = rehomed_env::RehomedTestEnv::enter(tag);
|
|
139
|
+
let workspace = env.workspace(&format!(
|
|
140
|
+
"057c-inv-{tag}-{}",
|
|
141
|
+
N.fetch_add(1, Ordering::Relaxed)
|
|
142
|
+
));
|
|
143
|
+
let store = MessageStore::open(&workspace).unwrap();
|
|
144
|
+
let event_log = EventLog::new(&workspace);
|
|
145
|
+
Self {
|
|
146
|
+
_env: env,
|
|
147
|
+
workspace,
|
|
148
|
+
store,
|
|
149
|
+
event_log,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fn seed_leader_row(&self, content: &str, status: &str, error: Option<&str>) -> String {
|
|
154
|
+
let mid = enqueue_leader_mailbox_until_attach(
|
|
155
|
+
&self.workspace,
|
|
156
|
+
TEAM,
|
|
157
|
+
content,
|
|
158
|
+
None,
|
|
159
|
+
"leader",
|
|
160
|
+
&self.event_log,
|
|
161
|
+
)
|
|
162
|
+
.unwrap()
|
|
163
|
+
.message_id
|
|
164
|
+
.expect("mailbox enqueue returns id");
|
|
165
|
+
let conn = team_agent::db::schema::open_db(self.store.db_path()).unwrap();
|
|
166
|
+
conn.execute(
|
|
167
|
+
"update messages set status = ?2, error = ?3 where message_id = ?1",
|
|
168
|
+
params![mid, status, error],
|
|
169
|
+
)
|
|
170
|
+
.unwrap();
|
|
171
|
+
mid
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fn seed_worker_row(&self, content: &str, status: &str, error: Option<&str>) -> String {
|
|
175
|
+
let mid = self.seed_leader_row(content, "accepted", None);
|
|
176
|
+
let conn = team_agent::db::schema::open_db(self.store.db_path()).unwrap();
|
|
177
|
+
conn.execute(
|
|
178
|
+
"update messages set recipient = 'worker-a', status = ?2, error = ?3
|
|
179
|
+
where message_id = ?1",
|
|
180
|
+
params![mid, status, error],
|
|
181
|
+
)
|
|
182
|
+
.unwrap();
|
|
183
|
+
mid
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fn seed_null_result_watcher(&self, i: usize) -> String {
|
|
187
|
+
let wid = format!("watch-null-hist-{i}");
|
|
188
|
+
let conn = team_agent::db::schema::open_db(self.store.db_path()).unwrap();
|
|
189
|
+
conn.execute(
|
|
190
|
+
"insert into result_watchers
|
|
191
|
+
(watcher_id, owner_team_id, leader_id, status, created_at, completed_at,
|
|
192
|
+
result_id, notified_message_id)
|
|
193
|
+
values (?1, ?2, 'leader', 'notify_failed', '2026-07-21T00:00:00Z',
|
|
194
|
+
'2026-07-21T00:00:00Z', null, null)",
|
|
195
|
+
params![wid, TEAM],
|
|
196
|
+
)
|
|
197
|
+
.unwrap();
|
|
198
|
+
wid
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
fn row_status(&self, mid: &str) -> String {
|
|
202
|
+
let conn = team_agent::db::schema::open_db(self.store.db_path()).unwrap();
|
|
203
|
+
conn.query_row(
|
|
204
|
+
"select status from messages where message_id = ?1",
|
|
205
|
+
params![mid],
|
|
206
|
+
|row| row.get(0),
|
|
207
|
+
)
|
|
208
|
+
.unwrap()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
fn watcher_status(&self, wid: &str) -> String {
|
|
212
|
+
let conn = team_agent::db::schema::open_db(self.store.db_path()).unwrap();
|
|
213
|
+
conn.query_row(
|
|
214
|
+
"select status from result_watchers where watcher_id = ?1",
|
|
215
|
+
params![wid],
|
|
216
|
+
|row| row.get(0),
|
|
217
|
+
)
|
|
218
|
+
.unwrap()
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fn distinct_message_statuses(&self) -> BTreeSet<String> {
|
|
222
|
+
let conn = team_agent::db::schema::open_db(self.store.db_path()).unwrap();
|
|
223
|
+
let mut statement = conn
|
|
224
|
+
.prepare("select distinct status from messages order by status")
|
|
225
|
+
.unwrap();
|
|
226
|
+
statement
|
|
227
|
+
.query_map([], |row| row.get::<_, String>(0))
|
|
228
|
+
.unwrap()
|
|
229
|
+
.map(|row| row.unwrap())
|
|
230
|
+
.collect()
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
fn last_blocked_requeue_event(&self) -> Option<serde_json::Value> {
|
|
234
|
+
let path = self
|
|
235
|
+
.workspace
|
|
236
|
+
.join(".team")
|
|
237
|
+
.join("logs")
|
|
238
|
+
.join("events.jsonl");
|
|
239
|
+
let text = std::fs::read_to_string(path).ok()?;
|
|
240
|
+
text.lines()
|
|
241
|
+
.rev()
|
|
242
|
+
.filter_map(|l| serde_json::from_str::<serde_json::Value>(l).ok())
|
|
243
|
+
.find(|e| {
|
|
244
|
+
e.get("event").and_then(serde_json::Value::as_str)
|
|
245
|
+
== Some("leader_receiver.blocked_messages_requeued")
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
fn claim(&self, state: &mut serde_json::Value) -> LeaseStatus {
|
|
250
|
+
let team = TeamKey::new(TEAM.to_string());
|
|
251
|
+
let pane = PaneId::new(PANE);
|
|
252
|
+
let result = claim_lease_no_incident(
|
|
253
|
+
&self.workspace,
|
|
254
|
+
state,
|
|
255
|
+
None,
|
|
256
|
+
&team,
|
|
257
|
+
&pane,
|
|
258
|
+
true,
|
|
259
|
+
&self.event_log,
|
|
260
|
+
&LiveCaller,
|
|
261
|
+
)
|
|
262
|
+
.unwrap();
|
|
263
|
+
if result.ok {
|
|
264
|
+
let _ = requeue_after_claim_leader(
|
|
265
|
+
&self.workspace,
|
|
266
|
+
&self.store,
|
|
267
|
+
&self.event_log,
|
|
268
|
+
&team,
|
|
269
|
+
result.bound_pane_id.as_ref().expect("pane"),
|
|
270
|
+
None,
|
|
271
|
+
)
|
|
272
|
+
.unwrap();
|
|
273
|
+
}
|
|
274
|
+
result.status
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/// Seed a full storm-shape inventory and return the id map keyed by the
|
|
279
|
+
/// prior-state name so teeth can address rows by role.
|
|
280
|
+
fn seed_full_inventory(case: &InventoryCase) -> std::collections::BTreeMap<String, Vec<String>> {
|
|
281
|
+
let mut m: std::collections::BTreeMap<String, Vec<String>> = Default::default();
|
|
282
|
+
// parked (leader, submitted_pending_acceptance) ×3 — represents 346
|
|
283
|
+
for i in 0..3 {
|
|
284
|
+
m.entry("submitted_pending_acceptance".into())
|
|
285
|
+
.or_default()
|
|
286
|
+
.push(case.seed_leader_row(
|
|
287
|
+
&format!("parked history {i}"),
|
|
288
|
+
"submitted_pending_acceptance",
|
|
289
|
+
None,
|
|
290
|
+
));
|
|
291
|
+
}
|
|
292
|
+
// blocked (failed + leader_not_attached) ×2 — represents 153
|
|
293
|
+
for i in 0..2 {
|
|
294
|
+
m.entry("failed:leader_not_attached".into())
|
|
295
|
+
.or_default()
|
|
296
|
+
.push(case.seed_leader_row(
|
|
297
|
+
&format!("blocked {i}"),
|
|
298
|
+
"failed",
|
|
299
|
+
Some("leader_not_attached"),
|
|
300
|
+
));
|
|
301
|
+
}
|
|
302
|
+
// delivered ×1 — must never flip
|
|
303
|
+
m.entry("delivered".into())
|
|
304
|
+
.or_default()
|
|
305
|
+
.push(case.seed_leader_row("delivered history", "delivered", None));
|
|
306
|
+
// submitted_unverified ×1 — represents 51 (or the leader-subset 23)
|
|
307
|
+
m.entry("submitted_unverified".into())
|
|
308
|
+
.or_default()
|
|
309
|
+
.push(case.seed_leader_row("unverified history", "submitted_unverified", None));
|
|
310
|
+
// queued_pane_missing ×1 (worker-recipient face) — represents 5
|
|
311
|
+
m.entry("queued_pane_missing".into())
|
|
312
|
+
.or_default()
|
|
313
|
+
.push(case.seed_worker_row(
|
|
314
|
+
"worker pane missing",
|
|
315
|
+
"queued_pane_missing",
|
|
316
|
+
Some("tmux_target_missing"),
|
|
317
|
+
));
|
|
318
|
+
// acknowledged ×1 — terminal, must not flip
|
|
319
|
+
m.entry("acknowledged".into())
|
|
320
|
+
.or_default()
|
|
321
|
+
.push(case.seed_leader_row("acked", "acknowledged", None));
|
|
322
|
+
// consumed ×1 — terminal, must not flip
|
|
323
|
+
m.entry("consumed".into())
|
|
324
|
+
.or_default()
|
|
325
|
+
.push(case.seed_leader_row("consumed", "consumed", None));
|
|
326
|
+
// stored_only ×1 — not an eligible transition
|
|
327
|
+
m.entry("stored_only".into())
|
|
328
|
+
.or_default()
|
|
329
|
+
.push(case.seed_leader_row("stored", "stored_only", None));
|
|
330
|
+
// queued_coordinator_unavailable ×1 — same, blocked but not by leader
|
|
331
|
+
m.entry("queued_coordinator_unavailable".into())
|
|
332
|
+
.or_default()
|
|
333
|
+
.push(case.seed_leader_row("coord-unavail", "queued_coordinator_unavailable", None));
|
|
334
|
+
// target_resolved ×1 — in-flight, must not flip
|
|
335
|
+
m.entry("target_resolved".into())
|
|
336
|
+
.or_default()
|
|
337
|
+
.push(case.seed_leader_row("resolved", "target_resolved", None));
|
|
338
|
+
// Non-leader parked (negative control body)
|
|
339
|
+
m.entry("worker:submitted_pending_acceptance".into())
|
|
340
|
+
.or_default()
|
|
341
|
+
.push(case.seed_worker_row("worker parked", "submitted_pending_acceptance", None));
|
|
342
|
+
// Add a queued_until_leader_attach positive control ×1
|
|
343
|
+
m.entry("queued_until_leader_attach".into())
|
|
344
|
+
.or_default()
|
|
345
|
+
.push(case.seed_leader_row("queued mailbox", "queued_until_leader_attach", None));
|
|
346
|
+
// accepted ×1 — freshly created, must remain accepted (not touched by claim)
|
|
347
|
+
m.entry("accepted".into())
|
|
348
|
+
.or_default()
|
|
349
|
+
.push(case.seed_leader_row("just accepted", "accepted", None));
|
|
350
|
+
// Drive the historical fixture from the product's unique typed directory.
|
|
351
|
+
// Any new durable wire status is seeded automatically, without a copied
|
|
352
|
+
// test-side list that can silently lag the product catalog.
|
|
353
|
+
let already_seeded = case.distinct_message_statuses();
|
|
354
|
+
for status in durable_status_catalog() {
|
|
355
|
+
if !already_seeded.contains(status) {
|
|
356
|
+
m.entry(status.to_string())
|
|
357
|
+
.or_default()
|
|
358
|
+
.push(case.seed_leader_row(
|
|
359
|
+
&format!("catalog-derived historical {status}"),
|
|
360
|
+
status,
|
|
361
|
+
None,
|
|
362
|
+
));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
m
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
// Tooth 1 — only truly-blocked rows flip (invariant lock at 5b847e4)
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
#[test]
|
|
373
|
+
#[serial(env)]
|
|
374
|
+
fn upgrade_gate_only_blocked_rows_flip() {
|
|
375
|
+
let case = InventoryCase::new("only-blocked-flip");
|
|
376
|
+
let map = seed_full_inventory(&case);
|
|
377
|
+
let mut state = bound_state();
|
|
378
|
+
let status = case.claim(&mut state);
|
|
379
|
+
assert_eq!(status, LeaseStatus::AlreadyBound);
|
|
380
|
+
// Blocked and queued_until_leader_attach must be accepted; everything
|
|
381
|
+
// else byte-stable at its seeded status.
|
|
382
|
+
for id in &map["failed:leader_not_attached"] {
|
|
383
|
+
assert_eq!(
|
|
384
|
+
case.row_status(id),
|
|
385
|
+
"accepted",
|
|
386
|
+
"blocked leader row must flip"
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
for id in &map["queued_until_leader_attach"] {
|
|
390
|
+
assert_eq!(
|
|
391
|
+
case.row_status(id),
|
|
392
|
+
"accepted",
|
|
393
|
+
"queued_until_leader_attach must flip (0.5.56 preserved)"
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
for (key, ids) in &map {
|
|
397
|
+
if key == "failed:leader_not_attached" || key == "queued_until_leader_attach" {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
// Determine the expected preserved status.
|
|
401
|
+
let expected = key.split(':').next().unwrap();
|
|
402
|
+
// Special case: "worker:X" seeded status is X.
|
|
403
|
+
let expected = expected
|
|
404
|
+
.strip_prefix("worker")
|
|
405
|
+
.unwrap_or(expected)
|
|
406
|
+
.trim_start_matches(':');
|
|
407
|
+
let expected = if expected.is_empty() {
|
|
408
|
+
key.split(':').nth(1).unwrap()
|
|
409
|
+
} else {
|
|
410
|
+
expected
|
|
411
|
+
};
|
|
412
|
+
for id in ids {
|
|
413
|
+
assert_eq!(
|
|
414
|
+
case.row_status(id),
|
|
415
|
+
expected,
|
|
416
|
+
"row {id} (prior={key}) must be byte-stable across claim; upgrade gate leaks"
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
// Tooth 2 — per-prior-state audit count (RED at 5b847e4)
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
|
|
426
|
+
#[test]
|
|
427
|
+
#[serial(env)]
|
|
428
|
+
fn upgrade_gate_emits_per_prior_state_audit_count() {
|
|
429
|
+
let case = InventoryCase::new("audit-count");
|
|
430
|
+
let _map = seed_full_inventory(&case);
|
|
431
|
+
let mut state = bound_state();
|
|
432
|
+
let _ = case.claim(&mut state);
|
|
433
|
+
let event = case
|
|
434
|
+
.last_blocked_requeue_event()
|
|
435
|
+
.expect("blocked_messages_requeued event must exist");
|
|
436
|
+
// Distillation §D3 / §2.2#2 target: per-prior-status breakdown.
|
|
437
|
+
// The typed refactor must add a `by_prior_state` map keyed by typed
|
|
438
|
+
// variant. Baseline only has scalar `count` → red.
|
|
439
|
+
let by_prior = event.get("by_prior_state").and_then(|v| v.as_object());
|
|
440
|
+
assert!(
|
|
441
|
+
by_prior.is_some(),
|
|
442
|
+
"event missing `by_prior_state` breakdown; only scalar count exposed. \
|
|
443
|
+
audit-side conflation is what let the 494 batch hide 346/148 splits."
|
|
444
|
+
);
|
|
445
|
+
let by_prior = by_prior.unwrap();
|
|
446
|
+
// Every prior-status the fixture actually touched must be represented,
|
|
447
|
+
// even if 0 rows in that state were flipped.
|
|
448
|
+
for expected_key in ["blocked_leader_unbound", "queued_until_leader_attach"] {
|
|
449
|
+
assert!(
|
|
450
|
+
by_prior.contains_key(expected_key),
|
|
451
|
+
"audit map missing typed key `{expected_key}`; the typed refactor must \
|
|
452
|
+
expose 0-count entries too (so audits are self-describing)."
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ---------------------------------------------------------------------------
|
|
458
|
+
// Tooth 3 — null-result historical watchers are untouched (invariant lock)
|
|
459
|
+
// ---------------------------------------------------------------------------
|
|
460
|
+
|
|
461
|
+
#[test]
|
|
462
|
+
#[serial(env)]
|
|
463
|
+
fn null_result_historical_watchers_never_rewritten_across_boot() {
|
|
464
|
+
let case = InventoryCase::new("watcher-null");
|
|
465
|
+
let watchers: Vec<String> = (0..3).map(|i| case.seed_null_result_watcher(i)).collect();
|
|
466
|
+
let _map = seed_full_inventory(&case);
|
|
467
|
+
let before: Vec<String> = watchers.iter().map(|w| case.watcher_status(w)).collect();
|
|
468
|
+
let mut state = bound_state();
|
|
469
|
+
let _ = case.claim(&mut state);
|
|
470
|
+
let after: Vec<String> = watchers.iter().map(|w| case.watcher_status(w)).collect();
|
|
471
|
+
assert_eq!(
|
|
472
|
+
before, after,
|
|
473
|
+
"null-result historical watchers must never be rewritten (108-row write \
|
|
474
|
+
amplification of 0.5.55 storm). §2.3 watcher fan-in."
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ---------------------------------------------------------------------------
|
|
479
|
+
// Tooth 4 — worker-recipient parked rows must have a typed recovery owner
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
|
|
482
|
+
#[test]
|
|
483
|
+
#[serial(env)]
|
|
484
|
+
fn worker_recipient_parked_rows_have_typed_recovery_owner() {
|
|
485
|
+
// We probe by source scan: after refactor, a file inside
|
|
486
|
+
// src/messaging/ or src/recovery/ (name at implementer's discretion)
|
|
487
|
+
// must expose a typed API for pane-available recovery whose target
|
|
488
|
+
// is `BlockedWorkerPaneMissing` (§3.2 last-two-rows).
|
|
489
|
+
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
490
|
+
let src = manifest.join("src");
|
|
491
|
+
// Search for a symbol name pattern: `BlockedWorkerPaneMissing` variant
|
|
492
|
+
// + a recovery function whose name mentions pane_available.
|
|
493
|
+
let mut has_variant = false;
|
|
494
|
+
let mut has_recovery = false;
|
|
495
|
+
fn walk(dir: &std::path::Path, has_variant: &mut bool, has_recovery: &mut bool) {
|
|
496
|
+
let Ok(entries) = std::fs::read_dir(dir) else {
|
|
497
|
+
return;
|
|
498
|
+
};
|
|
499
|
+
for e in entries.flatten() {
|
|
500
|
+
let p = e.path();
|
|
501
|
+
if p.is_dir() {
|
|
502
|
+
walk(&p, has_variant, has_recovery);
|
|
503
|
+
} else if p.extension().and_then(|s| s.to_str()) == Some("rs") {
|
|
504
|
+
if let Ok(t) = std::fs::read_to_string(&p) {
|
|
505
|
+
if t.contains("BlockedWorkerPaneMissing") {
|
|
506
|
+
*has_variant = true;
|
|
507
|
+
}
|
|
508
|
+
if t.contains("pane_available")
|
|
509
|
+
&& (t.contains("fn recover") || t.contains("fn requeue"))
|
|
510
|
+
{
|
|
511
|
+
*has_recovery = true;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
walk(&src, &mut has_variant, &mut has_recovery);
|
|
518
|
+
assert!(
|
|
519
|
+
has_variant && has_recovery,
|
|
520
|
+
"worker-recipient parked rows (queued_pane_missing, §3.2 last-two-rows) \
|
|
521
|
+
have no typed recovery owner. Need: `BlockedWorkerPaneMissing` variant + \
|
|
522
|
+
a recovery function keyed on pane_available incident. Baseline: variant={} \
|
|
523
|
+
recovery={}. Currently these rows sit in undefined-owner limbo (locate \
|
|
524
|
+
§1.3 '23+5' subset).",
|
|
525
|
+
has_variant,
|
|
526
|
+
has_recovery
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
// Tooth 5 — fixture stays in lock-step with batch A's enum catalog
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
|
|
534
|
+
#[test]
|
|
535
|
+
#[serial(env)]
|
|
536
|
+
fn upgrade_inventory_covers_full_enum() {
|
|
537
|
+
let case = InventoryCase::new("catalog-lockstep");
|
|
538
|
+
let _map = seed_full_inventory(&case);
|
|
539
|
+
let seeded = case.distinct_message_statuses();
|
|
540
|
+
let catalog: BTreeSet<String> = durable_status_catalog()
|
|
541
|
+
.into_iter()
|
|
542
|
+
.map(str::to_string)
|
|
543
|
+
.collect();
|
|
544
|
+
assert_eq!(
|
|
545
|
+
seeded, catalog,
|
|
546
|
+
"historical fixture status coverage must be exactly the product \
|
|
547
|
+
MessageRowStatus::ALL wire catalog; no hand-copied subset may drift"
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ---------------------------------------------------------------------------
|
|
552
|
+
// Tooth 6 — parked delivery debt remains open but cannot requeue
|
|
553
|
+
// ---------------------------------------------------------------------------
|
|
554
|
+
|
|
555
|
+
#[test]
|
|
556
|
+
#[serial(env)]
|
|
557
|
+
fn injected_awaiting_receipt_is_open_debt_but_not_requeueable() {
|
|
558
|
+
let case = InventoryCase::new("injected-open-debt");
|
|
559
|
+
let status = durable_status_catalog()
|
|
560
|
+
.into_iter()
|
|
561
|
+
.find(|status| *status == "injected_awaiting_receipt")
|
|
562
|
+
.expect("product MessageRowStatus::ALL must expose injected_awaiting_receipt");
|
|
563
|
+
let message_id = case.seed_leader_row("receipt still owed", status, None);
|
|
564
|
+
|
|
565
|
+
let runtime_status =
|
|
566
|
+
team_agent::cli::status_port::status(&case.workspace, false, true).unwrap();
|
|
567
|
+
let undelivered = runtime_status
|
|
568
|
+
.pointer("/runtime/undelivered")
|
|
569
|
+
.and_then(serde_json::Value::as_i64);
|
|
570
|
+
let claim_won = case.store.claim_for_delivery(&message_id).unwrap();
|
|
571
|
+
assert!(
|
|
572
|
+
undelivered == Some(1) && !claim_won,
|
|
573
|
+
"injected_awaiting_receipt must be classified in two directions at once: \
|
|
574
|
+
nonterminal-for-delivery (runtime undelivered inventory=1) and \
|
|
575
|
+
terminal-for-requeue (claim_won=false). observed \
|
|
576
|
+
undelivered={undelivered:?} claim_won={claim_won}"
|
|
577
|
+
);
|
|
578
|
+
assert_eq!(
|
|
579
|
+
case.row_status(&message_id),
|
|
580
|
+
"injected_awaiting_receipt",
|
|
581
|
+
"a rejected claim must leave the typed parked fact unchanged"
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
// Tooth 7 — positive control: legitimate recovery path preserved
|
|
587
|
+
// ---------------------------------------------------------------------------
|
|
588
|
+
|
|
589
|
+
#[test]
|
|
590
|
+
#[serial(env)]
|
|
591
|
+
fn newly_bound_leader_still_flushes_queued_row() {
|
|
592
|
+
let case = InventoryCase::new("pos-newly-bound");
|
|
593
|
+
let queued = case.seed_leader_row("queued mailbox", "queued_until_leader_attach", None);
|
|
594
|
+
let mut state = serde_json::json!({ "session_name": "team-upgrade-pos-new" });
|
|
595
|
+
let status = case.claim(&mut state);
|
|
596
|
+
assert_eq!(
|
|
597
|
+
status,
|
|
598
|
+
LeaseStatus::Claimed,
|
|
599
|
+
"positive control exercises Claimed enum branch"
|
|
600
|
+
);
|
|
601
|
+
assert_eq!(
|
|
602
|
+
case.row_status(&queued),
|
|
603
|
+
"accepted",
|
|
604
|
+
"newly-bound path must still flush queued mailbox row"
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// ---------------------------------------------------------------------------
|
|
609
|
+
// Tooth 8 — negative control: non-leader parked rows unaffected
|
|
610
|
+
// ---------------------------------------------------------------------------
|
|
611
|
+
|
|
612
|
+
#[test]
|
|
613
|
+
#[serial(env)]
|
|
614
|
+
fn non_leader_parked_rows_are_never_flipped_by_leader_claim() {
|
|
615
|
+
let case = InventoryCase::new("neg-worker-parked");
|
|
616
|
+
let worker_parked = case.seed_worker_row("worker parked", "submitted_pending_acceptance", None);
|
|
617
|
+
let mut state = bound_state();
|
|
618
|
+
let _ = case.claim(&mut state);
|
|
619
|
+
assert_eq!(
|
|
620
|
+
case.row_status(&worker_parked),
|
|
621
|
+
"submitted_pending_acceptance",
|
|
622
|
+
"recipient!=leader parked row must be unaffected by leader claim"
|
|
623
|
+
);
|
|
624
|
+
}
|
|
@@ -94,8 +94,13 @@ fn leader_start_plan_pins_mode_and_leader_env() {
|
|
|
94
94
|
);
|
|
95
95
|
assert!(!plan.is_external_leader);
|
|
96
96
|
assert!(
|
|
97
|
-
plan.argv.
|
|
98
|
-
|
|
97
|
+
plan.argv.len() >= 5
|
|
98
|
+
&& plan.argv[0] == "tmux"
|
|
99
|
+
&& plan.argv[1] == "-L"
|
|
100
|
+
&& plan.argv[2].starts_with("ta-")
|
|
101
|
+
&& plan.argv.iter().any(|arg| arg == "attach-session")
|
|
102
|
+
&& plan.argv.iter().any(|arg| arg.ends_with(":fake")),
|
|
103
|
+
"no-tmux managed launch uses the workspace socket and attaches the user client to the leader window: {:?}",
|
|
99
104
|
plan.argv
|
|
100
105
|
);
|
|
101
106
|
// plan 边界 detached 恒 false(`-d` 插入在 start_leader 层,非此处)。
|