@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.
Files changed (250) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +5 -2
  4. package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +1 -0
  6. package/crates/team-agent/src/cli/emit.rs +41 -7
  7. package/crates/team-agent/src/cli/helpers.rs +1 -0
  8. package/crates/team-agent/src/cli/leader.rs +6 -1
  9. package/crates/team-agent/src/cli/leaders.rs +2 -0
  10. package/crates/team-agent/src/cli/mod.rs +33 -3
  11. package/crates/team-agent/src/cli/named_address.rs +8 -1
  12. package/crates/team-agent/src/cli/profile.rs +2 -1
  13. package/crates/team-agent/src/cli/send/coordinator.rs +1 -0
  14. package/crates/team-agent/src/cli/send/mailbox.rs +3 -0
  15. package/crates/team-agent/src/cli/send/persist.rs +1 -0
  16. package/crates/team-agent/src/cli/send/presentation.rs +1 -0
  17. package/crates/team-agent/src/cli/send/resolve.rs +206 -1
  18. package/crates/team-agent/src/cli/send.rs +2 -1
  19. package/crates/team-agent/src/cli/spec.rs +1 -0
  20. package/crates/team-agent/src/cli/status.rs +2 -1
  21. package/crates/team-agent/src/cli/status_port/compact.rs +1 -0
  22. package/crates/team-agent/src/cli/status_port/format.rs +1 -0
  23. package/crates/team-agent/src/cli/status_port/inbox.rs +1 -0
  24. package/crates/team-agent/src/cli/tests/verb_profile.rs +38 -0
  25. package/crates/team-agent/src/cli/types.rs +3 -2
  26. package/crates/team-agent/src/communication_mode/mod.rs +1 -0
  27. package/crates/team-agent/src/compiler.rs +1 -0
  28. package/crates/team-agent/src/conpty/backend.rs +1 -0
  29. package/crates/team-agent/src/conpty/mod.rs +1 -0
  30. package/crates/team-agent/src/coordinator/backoff.rs +5 -8
  31. package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
  32. package/crates/team-agent/src/coordinator/health.rs +1 -0
  33. package/crates/team-agent/src/coordinator/mod.rs +2 -2
  34. package/crates/team-agent/src/coordinator/orphan.rs +4 -116
  35. package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
  36. package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
  37. package/crates/team-agent/src/coordinator/steps/abnormal.rs +6 -5
  38. package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
  39. package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
  40. package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
  41. package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
  42. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
  43. package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
  44. package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
  45. package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +0 -9
  46. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
  47. package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
  48. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -42
  49. package/crates/team-agent/src/coordinator/tick.rs +24 -28
  50. package/crates/team-agent/src/coordinator/types.rs +3 -176
  51. package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
  52. package/crates/team-agent/src/db/message_store.rs +2 -1
  53. package/crates/team-agent/src/db/migration.rs +5 -4
  54. package/crates/team-agent/src/db/mod.rs +1 -0
  55. package/crates/team-agent/src/db/schema.rs +1 -0
  56. package/crates/team-agent/src/diagnose/comms.rs +2 -1
  57. package/crates/team-agent/src/diagnose/mod.rs +1 -0
  58. package/crates/team-agent/src/diagnose/orphans.rs +1 -0
  59. package/crates/team-agent/src/layout/manager.rs +3 -7
  60. package/crates/team-agent/src/layout/mod.rs +2 -2
  61. package/crates/team-agent/src/layout/overlay.rs +6 -1
  62. package/crates/team-agent/src/layout/placement.rs +1 -0
  63. package/crates/team-agent/src/layout/recovery.rs +3 -0
  64. package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
  65. package/crates/team-agent/src/layout/sessions.rs +8 -1
  66. package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
  67. package/crates/team-agent/src/layout/worker_env.rs +3 -0
  68. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  69. package/crates/team-agent/src/leader/helpers.rs +1 -0
  70. package/crates/team-agent/src/leader/incident.rs +1 -0
  71. package/crates/team-agent/src/leader/lease.rs +27 -22
  72. package/crates/team-agent/src/leader/mod.rs +2 -3
  73. package/crates/team-agent/src/leader/owner_bind.rs +13 -3
  74. package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
  75. package/crates/team-agent/src/leader/rediscover.rs +4 -17
  76. package/crates/team-agent/src/leader/registry.rs +3 -15
  77. package/crates/team-agent/src/leader/start.rs +373 -125
  78. package/crates/team-agent/src/leader/takeover.rs +10 -232
  79. package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
  80. package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
  81. package/crates/team-agent/src/leader/tests/identity.rs +7 -2
  82. package/crates/team-agent/src/leader/tests/idle.rs +0 -193
  83. package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
  84. package/crates/team-agent/src/leader/tests/mod.rs +4 -41
  85. package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
  86. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
  87. package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
  88. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
  89. package/crates/team-agent/src/leader/types.rs +1 -28
  90. package/crates/team-agent/src/lib.rs +3 -0
  91. package/crates/team-agent/src/lifecycle/display.rs +7 -1
  92. package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
  93. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
  94. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
  95. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
  96. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
  97. package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
  98. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
  99. package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
  100. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -27
  101. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
  102. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
  103. package/crates/team-agent/src/lifecycle/launch.rs +11 -12
  104. package/crates/team-agent/src/lifecycle/mod.rs +2 -1
  105. package/crates/team-agent/src/lifecycle/restart/agent.rs +3 -3
  106. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
  107. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
  108. package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
  109. package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
  110. package/crates/team-agent/src/lifecycle/restart.rs +13 -11
  111. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
  112. package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
  113. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
  114. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
  115. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
  116. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
  117. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
  118. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
  119. package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
  120. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
  121. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
  122. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
  123. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
  124. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
  125. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
  126. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
  127. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
  128. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
  129. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
  130. package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
  131. package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
  132. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
  133. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
  134. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
  135. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
  136. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
  137. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
  138. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
  139. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
  140. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
  141. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
  142. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
  143. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
  144. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
  145. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
  146. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
  147. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
  148. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
  149. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
  150. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
  151. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
  152. package/crates/team-agent/src/lifecycle/tests.rs +59 -0
  153. package/crates/team-agent/src/lifecycle/types.rs +0 -9
  154. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  155. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
  156. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
  157. package/crates/team-agent/src/mcp_server/mod.rs +1 -0
  158. package/crates/team-agent/src/mcp_server/normalize.rs +4 -3
  159. package/crates/team-agent/src/mcp_server/tools.rs +1 -0
  160. package/crates/team-agent/src/mcp_server/types.rs +1 -18
  161. package/crates/team-agent/src/mcp_server/wire.rs +5 -5
  162. package/crates/team-agent/src/messaging/activity.rs +1 -0
  163. package/crates/team-agent/src/messaging/address.rs +1 -0
  164. package/crates/team-agent/src/messaging/delivery.rs +4 -3
  165. package/crates/team-agent/src/messaging/helpers.rs +2 -1
  166. package/crates/team-agent/src/messaging/leader_channel.rs +1 -0
  167. package/crates/team-agent/src/messaging/leader_receiver.rs +2 -1
  168. package/crates/team-agent/src/messaging/mod.rs +10 -14
  169. package/crates/team-agent/src/messaging/peers.rs +2 -0
  170. package/crates/team-agent/src/messaging/persist.rs +3 -1
  171. package/crates/team-agent/src/messaging/presentation.rs +2 -1
  172. package/crates/team-agent/src/messaging/results.rs +1 -0
  173. package/crates/team-agent/src/messaging/scheduler.rs +1 -0
  174. package/crates/team-agent/src/messaging/selftest.rs +1 -1
  175. package/crates/team-agent/src/messaging/send.rs +41 -8
  176. package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
  177. package/crates/team-agent/src/messaging/types.rs +2 -2
  178. package/crates/team-agent/src/messaging/watchers.rs +19 -13
  179. package/crates/team-agent/src/model/enums.rs +1 -0
  180. package/crates/team-agent/src/model/errors.rs +1 -0
  181. package/crates/team-agent/src/model/ids.rs +1 -0
  182. package/crates/team-agent/src/model/mod.rs +1 -0
  183. package/crates/team-agent/src/model/name_similarity.rs +1 -0
  184. package/crates/team-agent/src/model/pane_authority_refusal.rs +1 -0
  185. package/crates/team-agent/src/model/paths.rs +1 -0
  186. package/crates/team-agent/src/model/permissions.rs +3 -2
  187. package/crates/team-agent/src/model/routing.rs +1 -0
  188. package/crates/team-agent/src/model/spec.rs +1 -0
  189. package/crates/team-agent/src/model/task_graph.rs +1 -0
  190. package/crates/team-agent/src/model/yaml/tests.rs +1 -0
  191. package/crates/team-agent/src/model/yaml.rs +1 -0
  192. package/crates/team-agent/src/packaging/install.rs +9 -147
  193. package/crates/team-agent/src/packaging/migrate.rs +2 -0
  194. package/crates/team-agent/src/packaging/mod.rs +1 -0
  195. package/crates/team-agent/src/packaging/repair.rs +2 -0
  196. package/crates/team-agent/src/packaging/tests.rs +29 -218
  197. package/crates/team-agent/src/packaging/types.rs +8 -15
  198. package/crates/team-agent/src/platform/argv.rs +4 -0
  199. package/crates/team-agent/src/platform/errors.rs +10 -1
  200. package/crates/team-agent/src/platform/file_lock.rs +4 -157
  201. package/crates/team-agent/src/platform/mod.rs +0 -61
  202. package/crates/team-agent/src/platform/process.rs +1 -0
  203. package/crates/team-agent/src/provider/adapter.rs +1 -0
  204. package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
  205. package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
  206. package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
  207. package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
  208. package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
  209. package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
  210. package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
  211. package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
  212. package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
  213. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
  214. package/crates/team-agent/src/provider/classify.rs +1 -0
  215. package/crates/team-agent/src/provider/faults.rs +1 -26
  216. package/crates/team-agent/src/provider/helpers.rs +1 -0
  217. package/crates/team-agent/src/provider/mod.rs +1 -1
  218. package/crates/team-agent/src/provider/session/capture.rs +1 -4
  219. package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
  220. package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
  221. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
  222. package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
  223. package/crates/team-agent/src/provider/session/mod.rs +2 -4
  224. package/crates/team-agent/src/provider/session/resume.rs +2 -209
  225. package/crates/team-agent/src/provider/session_scan/claude.rs +1 -0
  226. package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
  227. package/crates/team-agent/src/provider/session_scan/common.rs +1 -0
  228. package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
  229. package/crates/team-agent/src/provider/session_scan.rs +1 -42
  230. package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
  231. package/crates/team-agent/src/provider/types.rs +1 -0
  232. package/crates/team-agent/src/provider/wire.rs +1 -0
  233. package/crates/team-agent/src/state/identity.rs +16 -5
  234. package/crates/team-agent/src/state/identity_keys.rs +1 -0
  235. package/crates/team-agent/src/state/mod.rs +2 -0
  236. package/crates/team-agent/src/state/owner_gate.rs +5 -0
  237. package/crates/team-agent/src/state/ownership.rs +10 -4
  238. package/crates/team-agent/src/state/paths.rs +12 -0
  239. package/crates/team-agent/src/state/persist.rs +9 -3
  240. package/crates/team-agent/src/state/projection.rs +15 -2
  241. package/crates/team-agent/src/state/repository/intent.rs +1 -0
  242. package/crates/team-agent/src/state/repository/tests.rs +1 -0
  243. package/crates/team-agent/src/state/repository.rs +7 -0
  244. package/crates/team-agent/src/state/selector.rs +1 -0
  245. package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
  246. package/crates/team-agent/src/tmux_backend.rs +63 -0
  247. package/crates/team-agent/src/transport.rs +15 -0
  248. package/package.json +4 -4
  249. package/crates/team-agent/src/leader/inject.rs +0 -33
  250. package/crates/team-agent/src/provider/command.rs +0 -80
@@ -0,0 +1,683 @@
1
+ //! 0.5.54 P0 · Codex weak-window session attribution RED (verifier).
2
+ //!
3
+ //! From gate4-c-attribution-locate.md + defect-weak-window-attribution.md
4
+ //! (2/2 real-machine reproduction: original 140 collisions / fresh case1 811
5
+ //! collisions, worker session null before AND after restart while message
6
+ //! delivery stayed healthy) — NOT from any proposed fix. Baseline `a909876`.
7
+ //!
8
+ //! Phenomenon (event-chain reconstruction, locate §Event-chain):
9
+ //! 1. `parent_peer` and `worker_a` spawn as one same-workspace codex cohort.
10
+ //! 2. An early scan exposes exactly ONE candidate — the worker's rollout in a
11
+ //! weak, identity-not-yet-parsed view.
12
+ //! 3. The weak `Any` fallback first-binds that candidate to `parent_peer`.
13
+ //! 4. The rollout later proves the exact embedded `worker_a` identity, but the
14
+ //! session key is already claimed: `worker_a` repeats
15
+ //! `attribution_ambiguous(reason=claimed_collision)` forever.
16
+ //! 5. Restart cannot prove same-session preservation: worker fresh-starts and
17
+ //! loses its context.
18
+ //!
19
+ //! Required invariant (locate §Single-truth-source): a candidate with positive
20
+ //! embedded `agent_id` proof belongs to that agent; weak fallback cannot
21
+ //! preempt that identity during the bounded cohort convergence window.
22
+ //! Collision handling stays fail-closed — never steal a positively or durably
23
+ //! claimed foreign session.
24
+ //!
25
+ //! CONTRACT LINEAGE (frozen-SHA evolution, leader ruling msg_ab2a1fecc308):
26
+ //! - 484d593c…afee7 = short-record form, four assertions (w1-w4), frozen at
27
+ //! 9c41975 on baseline a909876.
28
+ //! - THIS revision (re-frozen, same file, new SHA) adds w5/w5b/w5c after the
29
+ //! Gate-4 ROUND-2 real-machine ground truth (gate4-round2-evidence.md +
30
+ //! locate Round 2 addendum): the real worker rollout carries its embedded
31
+ //! identity inside a 44357-byte record whose terminating newline lands past
32
+ //! the CAPTURE_HEAD_BYTES=65536 head cap; `read_head_text` truncates to the
33
+ //! last complete newline, so the WHOLE record is discarded and the rollout
34
+ //! scans as weak forever — 00d63cc's shared-candidate deferral then never
35
+ //! converges (both agents terminally ambiguous, nobody binds). The w1
36
+ //! short-record fixture was a false premise for that real shape (sister
37
+ //! discipline: contract fixture data shapes must take real-machine samples
38
+ //! as ground truth). w1-w4 assertion semantics are byte-untouched.
39
+ //!
40
+ //! DETERMINISTIC TIMING SEAM (no real-provider race, no sleeps, no waiting):
41
+ //! scan visibility/order is pinned by fixture-controlled rollout FILE CONTENT
42
+ //! between explicit one-shot `capture_missing_provider_sessions_once` calls —
43
+ //! tick 1 sees the weak view (no embedded identity yet), tick 2 sees the same
44
+ //! rollout with the exact embedded `worker_a` identity. This drives the REAL
45
+ //! canonical scanner + allocator (`scan_session_candidates_once` →
46
+ //! `allocate_session_candidates`) — the surfaces the locate names — with zero
47
+ //! dependence on host load or wall-clock windows. `spawned_at` is a fixed
48
+ //! past timestamp so the spawn-time filters pass deterministically.
49
+ //! Hermeticity: HOME is redirected to a per-test root (HomeGuard, serial(env));
50
+ //! no ambient provider home, no real tmux, no network.
51
+
52
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
53
+
54
+ #[path = "../../../tests/support/hermetic.rs"]
55
+ mod hermetic_guard;
56
+ #[allow(dead_code)]
57
+ fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
58
+
59
+ use std::path::{Path, PathBuf};
60
+ use std::sync::atomic::{AtomicU64, Ordering};
61
+
62
+ use serde_json::{json, Value};
63
+ use serial_test::serial;
64
+ use team_agent::lifecycle::{classify_restart_plan, StartMode};
65
+ use team_agent::model::enums::Provider;
66
+ use team_agent::provider::get_adapter;
67
+ use team_agent::provider::session::capture::{
68
+ capture_missing_provider_sessions_once, CapturePassReport,
69
+ };
70
+
71
+ const PARENT: &str = "parent_peer";
72
+ const WORKER: &str = "worker_a";
73
+ /// Fixed past spawn boundary — both cohort members spawned before the rollout
74
+ /// was created, so `apply_spawned_at_filter` passes deterministically. NOTE:
75
+ /// the boundary must precede the session ids' uuid-v7 embedded timestamp
76
+ /// (`created_at_from_rollout_uuid` takes precedence over file mtime).
77
+ const SPAWNED_AT: &str = "2026-01-01T00:00:00Z";
78
+ const WORKER_SESSION: &str = "019f0054-70aa-7bbb-8ccc-000000000001";
79
+ const PARENT_SESSION: &str = "019f0054-70aa-7bbb-8ccc-000000000002";
80
+
81
+ static COUNTER: AtomicU64 = AtomicU64::new(0);
82
+
83
+ struct Fixture {
84
+ root: PathBuf,
85
+ spawn_cwd: PathBuf,
86
+ _home: HomeGuard,
87
+ }
88
+
89
+ impl Fixture {
90
+ fn new(tag: &str) -> Self {
91
+ let raw_root = std::env::temp_dir().join(format!(
92
+ "ta-weakwin-{tag}-{}-{}",
93
+ std::process::id(),
94
+ COUNTER.fetch_add(1, Ordering::Relaxed)
95
+ ));
96
+ let _ = std::fs::remove_dir_all(&raw_root);
97
+ std::fs::create_dir_all(&raw_root).unwrap();
98
+ // Canonicalize BEFORE deriving HOME/spawn_cwd so every path the
99
+ // scanner records (rooted at $HOME) is byte-comparable with the
100
+ // fixture's expectations (macOS /var vs /private/var symlink).
101
+ let root = std::fs::canonicalize(&raw_root).unwrap();
102
+ let spawn_cwd = root.join("ws");
103
+ std::fs::create_dir_all(&spawn_cwd).unwrap();
104
+ let guard = HomeGuard::with_home(root.join("home"));
105
+ Self {
106
+ root,
107
+ spawn_cwd,
108
+ _home: guard,
109
+ }
110
+ }
111
+
112
+ fn home(&self) -> PathBuf {
113
+ self.root.join("home")
114
+ }
115
+
116
+ fn sessions_dir(&self) -> PathBuf {
117
+ let dir = self.home().join(".codex/sessions/2026/07/22");
118
+ std::fs::create_dir_all(&dir).unwrap();
119
+ dir
120
+ }
121
+
122
+ fn rollout_path(&self, session_id: &str) -> PathBuf {
123
+ self.sessions_dir()
124
+ .join(format!("rollout-2026-07-22T10-00-00-{session_id}.jsonl"))
125
+ }
126
+
127
+ /// WEAK VIEW — the rollout as an early scan sees it: session meta + cwd +
128
+ /// conversation, identity NOT yet present. This is the locate's
129
+ /// "one-candidate weak-window view".
130
+ fn write_weak_rollout(&self, session_id: &str) -> PathBuf {
131
+ let path = self.rollout_path(session_id);
132
+ std::fs::write(&path, self.rollout_text(session_id, None)).unwrap();
133
+ path
134
+ }
135
+
136
+ /// STRONG VIEW — the same rollout once the exact embedded Team Agent
137
+ /// worker identity has been written by the provider.
138
+ fn write_identity_rollout(&self, session_id: &str, agent_id: &str) -> PathBuf {
139
+ let path = self.rollout_path(session_id);
140
+ std::fs::write(&path, self.rollout_text(session_id, Some(agent_id))).unwrap();
141
+ path
142
+ }
143
+
144
+ fn rollout_text(&self, session_id: &str, embedded_agent_id: Option<&str>) -> String {
145
+ let mut text = format!(
146
+ "{}\n{}\n",
147
+ json!({
148
+ "type": "session_meta",
149
+ "payload": {
150
+ "id": session_id,
151
+ "cwd": self.spawn_cwd.to_string_lossy().to_string()
152
+ }
153
+ }),
154
+ json!({
155
+ "type": "response_item",
156
+ "payload": {
157
+ "content": [{"type": "input_text", "text": "cohort conversation record"}]
158
+ }
159
+ }),
160
+ );
161
+ if let Some(agent_id) = embedded_agent_id {
162
+ text.push_str(&format!(
163
+ "{}\n",
164
+ json!({
165
+ "type": "response_item",
166
+ "payload": {
167
+ "content": [{
168
+ "type": "input_text",
169
+ "text": format!(
170
+ "You are Team Agent worker `{agent_id}` with role `fixture`."
171
+ )
172
+ }]
173
+ }
174
+ })
175
+ ));
176
+ }
177
+ text
178
+ }
179
+
180
+ fn agent_row(&self) -> Value {
181
+ json!({
182
+ "status": "running",
183
+ "provider": "codex",
184
+ "spawn_cwd": self.spawn_cwd.to_string_lossy().to_string(),
185
+ "spawned_at": SPAWNED_AT,
186
+ })
187
+ }
188
+
189
+ /// Two-member spawn cohort: parent_peer + worker_a, same workspace,
190
+ /// neither has a session yet (locate §Event-chain step 1).
191
+ fn cohort_state(&self) -> Value {
192
+ json!({
193
+ "agents": {
194
+ PARENT: self.agent_row(),
195
+ WORKER: self.agent_row(),
196
+ }
197
+ })
198
+ }
199
+ }
200
+
201
+ impl Drop for Fixture {
202
+ fn drop(&mut self) {
203
+ let _ = std::fs::remove_dir_all(&self.root);
204
+ }
205
+ }
206
+
207
+ struct HomeGuard {
208
+ previous_home: Option<String>,
209
+ }
210
+
211
+ impl HomeGuard {
212
+ fn with_home(home: PathBuf) -> Self {
213
+ std::fs::create_dir_all(&home).unwrap();
214
+ let previous_home = std::env::var("HOME").ok();
215
+ unsafe {
216
+ std::env::set_var("HOME", home);
217
+ }
218
+ Self { previous_home }
219
+ }
220
+ }
221
+
222
+ impl Drop for HomeGuard {
223
+ fn drop(&mut self) {
224
+ unsafe {
225
+ if let Some(value) = self.previous_home.take() {
226
+ std::env::set_var("HOME", value);
227
+ } else {
228
+ std::env::remove_var("HOME");
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ fn capture_tick(state: &mut Value) -> CapturePassReport {
235
+ capture_missing_provider_sessions_once(state, &mut |p: Provider| get_adapter(p), false, 0)
236
+ .expect("capture pass must not error")
237
+ }
238
+
239
+ fn agent_field<'a>(state: &'a Value, agent: &str, field: &str) -> Option<&'a str> {
240
+ state
241
+ .get("agents")
242
+ .and_then(|a| a.get(agent))
243
+ .and_then(|a| a.get(field))
244
+ .and_then(Value::as_str)
245
+ }
246
+
247
+ /// W1 (RED core) — positive embedded identity must reclaim / must never be
248
+ /// preempted by the weak-window first binding.
249
+ ///
250
+ /// Tick 1: only the worker's rollout is visible, in the weak
251
+ /// identity-not-yet-parsed view (single candidate). Tick 2..3: the same
252
+ /// rollout now carries the exact embedded `worker_a` identity — the bounded
253
+ /// cohort convergence window.
254
+ ///
255
+ /// Baseline red (locate steps 3-4): at tick 1 the weak `Any` fallback binds
256
+ /// the sole candidate to `parent_peer`; the worker's later positive proof
257
+ /// collides with the claimed key and `worker_a` never converges.
258
+ #[test]
259
+ #[serial(env)]
260
+ fn w1_weak_window_first_bind_must_not_preempt_positive_worker_identity() {
261
+ let fx = Fixture::new("w1");
262
+ let rollout = fx.write_weak_rollout(WORKER_SESSION);
263
+ let mut state = fx.cohort_state();
264
+
265
+ // Tick 1 — weak single-candidate window.
266
+ let _ = capture_tick(&mut state);
267
+ // Tick 2..3 — the rollout now proves the exact worker identity; the
268
+ // cohort convergence window is bounded (two ticks is plenty for a
269
+ // deterministic allocator — no wait widening allowed).
270
+ fx.write_identity_rollout(WORKER_SESSION, WORKER);
271
+ let _ = capture_tick(&mut state);
272
+ let final_report = capture_tick(&mut state);
273
+
274
+ let parent_session = agent_field(&state, PARENT, "session_id");
275
+ let worker_session = agent_field(&state, WORKER, "session_id");
276
+ let worker_rollout = agent_field(&state, WORKER, "rollout_path");
277
+
278
+ assert_ne!(
279
+ parent_session,
280
+ Some(WORKER_SESSION),
281
+ "the parent must NEVER commit the worker's session: the rollout's embedded identity \
282
+ proves it belongs to `{WORKER}`; a weak identity-unproven first binding must not be \
283
+ irreversible (locate required invariant). state={state}"
284
+ );
285
+ assert_eq!(
286
+ worker_session,
287
+ Some(WORKER_SESSION),
288
+ "`{WORKER}` must converge to its OWN session tuple once its positive embedded identity \
289
+ is scannable within the bounded cohort window; baseline leaves it null forever \
290
+ (2/2 real-machine: 140/811 claimed_collision rows). state={state}"
291
+ );
292
+ assert_eq!(
293
+ worker_rollout,
294
+ Some(rollout.to_string_lossy().as_ref()),
295
+ "`{WORKER}` must converge to the exact rollout path (tuple atomicity). state={state}"
296
+ );
297
+ assert!(
298
+ !final_report
299
+ .ambiguous
300
+ .iter()
301
+ .any(|entry| entry.agent_id == WORKER),
302
+ "after convergence there must be ZERO residual ambiguity for `{WORKER}` (the coordinator \
303
+ relabels this exact shape as `claimed_collision` and repeats it every tick). \
304
+ ambiguous={:?}",
305
+ final_report.ambiguous
306
+ );
307
+ }
308
+
309
+ /// W2 (positive control, naturally green) — the NORMAL window: the very first
310
+ /// scan already exposes the embedded `worker_a` identity. The worker binds its
311
+ /// own tuple positively, the parent binds nothing (its rollout does not exist),
312
+ /// and there is zero ambiguity. Guards against a fix that "solves" W1 by
313
+ /// rejecting or deferring correct attribution wholesale (constant-behavior
314
+ /// false-green).
315
+ #[test]
316
+ #[serial(env)]
317
+ fn w2_normal_window_same_fixture_attributes_worker_correctly() {
318
+ let fx = Fixture::new("w2");
319
+ let rollout = fx.write_identity_rollout(WORKER_SESSION, WORKER);
320
+ let mut state = fx.cohort_state();
321
+
322
+ let report = capture_tick(&mut state);
323
+
324
+ assert_eq!(
325
+ agent_field(&state, WORKER, "session_id"),
326
+ Some(WORKER_SESSION),
327
+ "positive control: with the identity visible from the first scan the worker must bind \
328
+ its own session immediately. state={state}"
329
+ );
330
+ assert_eq!(
331
+ agent_field(&state, WORKER, "rollout_path"),
332
+ Some(rollout.to_string_lossy().as_ref()),
333
+ "positive control: exact tuple. state={state}"
334
+ );
335
+ assert_eq!(
336
+ agent_field(&state, PARENT, "session_id"),
337
+ None,
338
+ "positive control: the parent has no backing rollout and must stay unbound — binding it \
339
+ would be the defect itself. state={state}"
340
+ );
341
+ assert!(
342
+ report.ambiguous.is_empty(),
343
+ "positive control: zero ambiguity/collision in the normal window. ambiguous={:?}",
344
+ report.ambiguous
345
+ );
346
+ }
347
+
348
+ /// W3 (negative control, fail-closed face — must stay green) — a durably
349
+ /// claimed FOREIGN session is never stolen. The parent already owns its own
350
+ /// completed tuple (PARENT_SESSION); the worker's positive identity binds
351
+ /// WORKER_SESSION only. Locate red line: "Collision handling remains
352
+ /// fail-closed — never steal a positively or durably claimed foreign session."
353
+ #[test]
354
+ #[serial(env)]
355
+ fn w3_durably_claimed_foreign_session_is_never_stolen() {
356
+ let fx = Fixture::new("w3");
357
+ let parent_rollout = fx.write_identity_rollout(PARENT_SESSION, PARENT);
358
+ let worker_rollout = fx.write_identity_rollout(WORKER_SESSION, WORKER);
359
+ let mut state = fx.cohort_state();
360
+ {
361
+ let parent = state
362
+ .get_mut("agents")
363
+ .and_then(|a| a.get_mut(PARENT))
364
+ .and_then(Value::as_object_mut)
365
+ .unwrap();
366
+ parent.insert("session_id".into(), json!(PARENT_SESSION));
367
+ parent.insert(
368
+ "rollout_path".into(),
369
+ json!(parent_rollout.to_string_lossy().to_string()),
370
+ );
371
+ }
372
+
373
+ let _ = capture_tick(&mut state);
374
+
375
+ assert_eq!(
376
+ agent_field(&state, PARENT, "session_id"),
377
+ Some(PARENT_SESSION),
378
+ "negative control: the parent's durably owned session must remain untouched. state={state}"
379
+ );
380
+ assert_eq!(
381
+ agent_field(&state, WORKER, "session_id"),
382
+ Some(WORKER_SESSION),
383
+ "negative control: the worker binds its own positively identified session only. \
384
+ state={state}"
385
+ );
386
+ assert_eq!(
387
+ agent_field(&state, WORKER, "rollout_path"),
388
+ Some(worker_rollout.to_string_lossy().as_ref()),
389
+ "negative control: exact tuple, never the foreign rollout. state={state}"
390
+ );
391
+ }
392
+
393
+ /// W4 (RED consequence face) — the externally observed failure is real
394
+ /// session-preservation loss at restart (locate §Event-chain step 5; defect
395
+ /// dossier: worker session null before AND after restart). After the weak
396
+ /// window and the bounded convergence window (same seam as W1), the restart
397
+ /// plan must resume `worker_a` on its OWN exact session; the baseline
398
+ /// mis-binding instead leaves the worker sessionless (fresh start = context
399
+ /// loss) — and no OTHER agent may resume the worker's session.
400
+ #[test]
401
+ #[serial(env)]
402
+ fn w4_restart_must_preserve_worker_session_after_weak_window() {
403
+ let fx = Fixture::new("w4");
404
+ let _rollout = fx.write_weak_rollout(WORKER_SESSION);
405
+ let mut state = fx.cohort_state();
406
+
407
+ let _ = capture_tick(&mut state);
408
+ fx.write_identity_rollout(WORKER_SESSION, WORKER);
409
+ let _ = capture_tick(&mut state);
410
+ let _ = capture_tick(&mut state);
411
+
412
+ let plan = classify_restart_plan(&state, true).expect("restart plan must classify");
413
+
414
+ let worker_decision = plan
415
+ .decisions
416
+ .iter()
417
+ .find(|d| d.agent_id.as_str() == WORKER)
418
+ .expect("worker must appear in the restart plan");
419
+ assert_eq!(
420
+ worker_decision.restart_mode,
421
+ StartMode::Resumed,
422
+ "restart must PRESERVE the worker's session (Resumed), not fresh-start it into context \
423
+ loss; baseline: weak-window first-bind left the worker sessionless. decision={:?} \
424
+ unresumable={:?}",
425
+ worker_decision,
426
+ plan.unresumable
427
+ );
428
+ assert_eq!(
429
+ worker_decision.session_id.as_ref().map(|s| s.as_str()),
430
+ Some(WORKER_SESSION),
431
+ "restart must resume the EXACT worker session id. decision={worker_decision:?}"
432
+ );
433
+ for decision in &plan.decisions {
434
+ if decision.agent_id.as_str() == WORKER {
435
+ continue;
436
+ }
437
+ assert!(
438
+ !(decision.restart_mode == StartMode::Resumed
439
+ && decision.session_id.as_ref().map(|s| s.as_str()) == Some(WORKER_SESSION)),
440
+ "no other agent may resume the WORKER's session (parent resuming a foreign session \
441
+ is the other face of the same defect). decision={decision:?}"
442
+ );
443
+ }
444
+ }
445
+
446
+ // ===========================================================================
447
+ // w5 family — LONG-RECORD ground truth (Gate-4 round 2). Fixture byte shape is
448
+ // modelled 1:1 on the real rollout sample (gate4-round2-evidence.md): line1 =
449
+ // 22068B session_meta, line2 = 233B, line3 = 44357B carrying the embedded
450
+ // identity, line3's newline past the 65536B head cap. The fixture SELF-ASSERTS
451
+ // its byte invariants so the shape can never silently drift.
452
+ // ===========================================================================
453
+
454
+ /// Head cap observed at the locate surface (`common.rs CAPTURE_HEAD_BYTES`).
455
+ /// The contract pins BEHAVIOR relative to this boundary, not the constant
456
+ /// itself: identity bytes inside the first HEAD_CAP raw bytes must be seen;
457
+ /// identity bytes entirely beyond it must not be (bounded I/O red line —
458
+ /// no whole-file reads, no unbounded cap growth).
459
+ const HEAD_CAP: u64 = 65_536;
460
+
461
+ fn marker_text(agent_id: &str) -> String {
462
+ format!("You are Team Agent worker `{agent_id}` with role `fixture`.")
463
+ }
464
+
465
+ fn padded_record(pad_before: usize, marker: Option<&str>, pad_after: usize) -> String {
466
+ let text = format!(
467
+ "{}{}{}",
468
+ "x".repeat(pad_before),
469
+ marker.unwrap_or(""),
470
+ "x".repeat(pad_after)
471
+ );
472
+ serde_json::to_string(&json!({
473
+ "type": "response_item",
474
+ "payload": {"content": [{"type": "input_text", "text": text}]}
475
+ }))
476
+ .unwrap()
477
+ }
478
+
479
+ impl Fixture {
480
+ /// Write the round-2 real-machine rollout shape. Returns (path,
481
+ /// marker_start_offset, line3_end_offset) with the invariants asserted:
482
+ /// line1+line2 complete under the cap; line3's terminating newline beyond
483
+ /// the cap; marker (when inside_cap) strictly under the cap.
484
+ fn write_long_record_rollout(
485
+ &self,
486
+ session_id: &str,
487
+ marker: Option<&str>,
488
+ marker_inside_cap: bool,
489
+ ) -> PathBuf {
490
+ // line1: session_meta padded to ~22068 bytes (real sample size).
491
+ let meta_core = json!({
492
+ "type": "session_meta",
493
+ "payload": {
494
+ "id": session_id,
495
+ "cwd": self.spawn_cwd.to_string_lossy().to_string(),
496
+ "pad": ""
497
+ }
498
+ });
499
+ let overhead = serde_json::to_string(&meta_core).unwrap().len();
500
+ let line1 = serde_json::to_string(&json!({
501
+ "type": "session_meta",
502
+ "payload": {
503
+ "id": session_id,
504
+ "cwd": self.spawn_cwd.to_string_lossy().to_string(),
505
+ "pad": "x".repeat(22_068usize.saturating_sub(overhead))
506
+ }
507
+ }))
508
+ .unwrap();
509
+ // line2: small complete conversation record (~233B real sample).
510
+ let line2 = padded_record(160, None, 0);
511
+ // line3: ~44357B record. marker_inside_cap=true → marker ~500B into
512
+ // the record (real sample: col 548 / absolute 22848); false → marker
513
+ // at the record tail, starting beyond the cap.
514
+ let line3 = match (marker, marker_inside_cap) {
515
+ (Some(m), true) => padded_record(500, Some(m), 43_800 - m.len()),
516
+ (Some(m), false) => padded_record(44_300 - m.len(), Some(m), 40),
517
+ (None, _) => padded_record(500, None, 43_800),
518
+ };
519
+ let body = format!("{line1}\n{line2}\n{line3}\n");
520
+
521
+ // Self-asserted byte invariants (ground-truth shape can never drift):
522
+ let line3_start = line1.len() + 1 + line2.len() + 1;
523
+ let line3_end = line3_start + line3.len() + 1;
524
+ assert!(
525
+ (line3_start as u64) < HEAD_CAP,
526
+ "fixture invariant: lines 1-2 must be complete under the head cap; line3_start={line3_start}"
527
+ );
528
+ assert!(
529
+ (line3_end as u64) > HEAD_CAP,
530
+ "fixture invariant: line3's newline must land beyond the head cap; line3_end={line3_end}"
531
+ );
532
+ if let Some(m) = marker {
533
+ let marker_start = line3_start + body[line3_start..].find(m).expect("marker present");
534
+ if marker_inside_cap {
535
+ assert!(
536
+ (marker_start as u64) < HEAD_CAP,
537
+ "fixture invariant: marker must START under the cap (real sample offset 22848); \
538
+ marker_start={marker_start}"
539
+ );
540
+ } else {
541
+ assert!(
542
+ (marker_start as u64) > HEAD_CAP,
543
+ "fixture invariant: marker must start BEYOND the cap; marker_start={marker_start}"
544
+ );
545
+ }
546
+ }
547
+
548
+ let path = self.rollout_path(session_id);
549
+ std::fs::write(&path, body).unwrap();
550
+ path
551
+ }
552
+ }
553
+
554
+ /// w5 (RED core, round 2) — an embedded identity whose BYTES lie inside the
555
+ /// bounded head window must remain visible even when its RECORD's terminating
556
+ /// newline lands beyond the cap. Real-machine ground truth: the only rollout
557
+ /// carries `worker_a` at absolute offset 22848 inside a 44357-byte record
558
+ /// ending at byte 66658; the head parser discards the incomplete record, the
559
+ /// rollout scans weak forever, and 00d63cc's shared-candidate deferral makes
560
+ /// BOTH cohort agents terminally ambiguous — nobody binds, restart loses the
561
+ /// session (run release-054-preship-fixed-20260723T001516Z).
562
+ ///
563
+ /// Post-fix: the worker converges to its exact tuple within the bounded
564
+ /// window; the parent never binds it; zero residual ambiguity. The structural
565
+ /// JSONL parser may still consume complete lines only (session_meta from
566
+ /// line1 remains authoritative for the session id) — this assertion passes
567
+ /// exactly when the identity is honored AND the tuple is intact.
568
+ #[test]
569
+ #[serial(env)]
570
+ fn w5_identity_inside_cap_within_cross_cap_record_must_converge() {
571
+ let fx = Fixture::new("w5");
572
+ let rollout = fx.write_long_record_rollout(WORKER_SESSION, Some(&marker_text(WORKER)), true);
573
+ let mut state = fx.cohort_state();
574
+
575
+ let _ = capture_tick(&mut state);
576
+ let _ = capture_tick(&mut state);
577
+ let final_report = capture_tick(&mut state);
578
+
579
+ assert_ne!(
580
+ agent_field(&state, PARENT, "session_id"),
581
+ Some(WORKER_SESSION),
582
+ "the parent must never commit the worker's session (unchanged w1 invariant). state={state}"
583
+ );
584
+ assert_eq!(
585
+ agent_field(&state, WORKER, "session_id"),
586
+ Some(WORKER_SESSION),
587
+ "`{WORKER}` must converge although its embedded identity sits inside a record whose \
588
+ newline lands beyond the {HEAD_CAP}-byte head cap — the identity BYTES are inside the \
589
+ bounded window (real sample: offset 22848) and must not be discarded with the \
590
+ incomplete record. Baseline 00d63cc: rollout scans weak forever, deferral never \
591
+ converges, nobody binds. state={state}"
592
+ );
593
+ assert_eq!(
594
+ agent_field(&state, WORKER, "rollout_path"),
595
+ Some(rollout.to_string_lossy().as_ref()),
596
+ "exact tuple (structural parser stays line-complete: session_meta on line1 is \
597
+ authoritative). state={state}"
598
+ );
599
+ assert!(
600
+ !final_report
601
+ .ambiguous
602
+ .iter()
603
+ .any(|entry| entry.agent_id == WORKER),
604
+ "zero residual ambiguity for `{WORKER}` after convergence. ambiguous={:?}",
605
+ final_report.ambiguous
606
+ );
607
+ }
608
+
609
+ /// w5b (bounded-read face — must stay green) — an identity whose bytes start
610
+ /// ENTIRELY BEYOND the head cap must NOT be extracted: the scan stays weak,
611
+ /// the shared-candidate deferral holds, and nobody binds. This pins the
612
+ /// OTHER face of "bounded read without identity loss": a fix that reads the
613
+ /// whole file (or grows the cap unboundedly) to find this marker turns this
614
+ /// form red. (Bounded per-file I/O is the locate's stated design intent;
615
+ /// heuristic attribution — cwd/nearest-spawn/newest — is refused elsewhere.)
616
+ #[test]
617
+ #[serial(env)]
618
+ fn w5b_identity_entirely_beyond_cap_stays_weak_and_deferred() {
619
+ let fx = Fixture::new("w5b");
620
+ let _rollout = fx.write_long_record_rollout(WORKER_SESSION, Some(&marker_text(WORKER)), false);
621
+ let mut state = fx.cohort_state();
622
+
623
+ let _ = capture_tick(&mut state);
624
+ let report = capture_tick(&mut state);
625
+
626
+ assert_eq!(
627
+ agent_field(&state, PARENT, "session_id"),
628
+ None,
629
+ "bounded-read face: beyond-cap identity is invisible; the shared weak candidate must \
630
+ not be bound to the parent. state={state}"
631
+ );
632
+ assert_eq!(
633
+ agent_field(&state, WORKER, "session_id"),
634
+ None,
635
+ "bounded-read face: the worker must not be POSITIVELY bound off identity bytes that \
636
+ live beyond the bounded head window (whole-file reads are forbidden). state={state}"
637
+ );
638
+ assert!(
639
+ report
640
+ .ambiguous
641
+ .iter()
642
+ .any(|entry| entry.agent_id == WORKER),
643
+ "the shared weak candidate stays deferred (ambiguous), not silently dropped. \
644
+ ambiguous={:?}",
645
+ report.ambiguous
646
+ );
647
+ }
648
+
649
+ /// w5c (fail-closed negative control — must stay green) — a cross-cap long
650
+ /// rollout with NO embedded identity anywhere must keep the shared-candidate
651
+ /// deferral: nobody binds, the worker stays deferred. Guards against a fix
652
+ /// that treats "record unreadable/truncated" as a positive identity signal.
653
+ #[test]
654
+ #[serial(env)]
655
+ fn w5c_no_identity_cross_cap_rollout_stays_deferred_for_cohort() {
656
+ let fx = Fixture::new("w5c");
657
+ let _rollout = fx.write_long_record_rollout(WORKER_SESSION, None, true);
658
+ let mut state = fx.cohort_state();
659
+
660
+ let _ = capture_tick(&mut state);
661
+ let report = capture_tick(&mut state);
662
+
663
+ assert_eq!(
664
+ agent_field(&state, PARENT, "session_id"),
665
+ None,
666
+ "fail-closed: no identity anywhere → the shared weak candidate binds nobody. state={state}"
667
+ );
668
+ assert_eq!(
669
+ agent_field(&state, WORKER, "session_id"),
670
+ None,
671
+ "fail-closed: truncation/absence of identity must never be treated as positive proof. \
672
+ state={state}"
673
+ );
674
+ assert!(
675
+ report
676
+ .ambiguous
677
+ .iter()
678
+ .any(|entry| entry.agent_id == WORKER),
679
+ "the cohort deferral holds (worker stays ambiguous for bounded convergence). \
680
+ ambiguous={:?}",
681
+ report.ambiguous
682
+ );
683
+ }