@team-agent/installer 0.2.10 → 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.
Files changed (326) hide show
  1. package/Cargo.lock +744 -0
  2. package/Cargo.toml +34 -0
  3. package/crates/team-agent/Cargo.toml +33 -0
  4. package/crates/team-agent/src/cli/adapters.rs +1343 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +554 -0
  6. package/crates/team-agent/src/cli/emit.rs +1077 -0
  7. package/crates/team-agent/src/cli/helpers.rs +88 -0
  8. package/crates/team-agent/src/cli/leader.rs +216 -0
  9. package/crates/team-agent/src/cli/mod.rs +1141 -0
  10. package/crates/team-agent/src/cli/profile.rs +306 -0
  11. package/crates/team-agent/src/cli/send.rs +215 -0
  12. package/crates/team-agent/src/cli/status.rs +179 -0
  13. package/crates/team-agent/src/cli/status_port.rs +502 -0
  14. package/crates/team-agent/src/cli/tests/base.rs +616 -0
  15. package/crates/team-agent/src/cli/tests/compile.rs +96 -0
  16. package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
  21. package/crates/team-agent/src/cli/tests/mod.rs +97 -0
  22. package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
  23. package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
  25. package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
  27. package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
  28. package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
  29. package/crates/team-agent/src/cli/types.rs +605 -0
  30. package/crates/team-agent/src/compiler/tests.rs +701 -0
  31. package/crates/team-agent/src/compiler.rs +489 -0
  32. package/crates/team-agent/src/coordinator/backoff.rs +153 -0
  33. package/crates/team-agent/src/coordinator/health.rs +436 -0
  34. package/crates/team-agent/src/coordinator/mod.rs +80 -0
  35. package/crates/team-agent/src/coordinator/orphan.rs +179 -0
  36. package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
  37. package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
  38. package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
  39. package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
  40. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
  41. package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
  42. package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
  43. package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
  44. package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
  45. package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
  46. package/crates/team-agent/src/coordinator/tick.rs +2032 -0
  47. package/crates/team-agent/src/coordinator/types.rs +584 -0
  48. package/crates/team-agent/src/db/migration.rs +716 -0
  49. package/crates/team-agent/src/db/mod.rs +23 -0
  50. package/crates/team-agent/src/db/schema.rs +378 -0
  51. package/crates/team-agent/src/event_log.rs +375 -0
  52. package/crates/team-agent/src/fake_worker.rs +253 -0
  53. package/crates/team-agent/src/leader/helpers.rs +190 -0
  54. package/crates/team-agent/src/leader/inject.rs +33 -0
  55. package/crates/team-agent/src/leader/lease.rs +1063 -0
  56. package/crates/team-agent/src/leader/mod.rs +99 -0
  57. package/crates/team-agent/src/leader/owner_bind.rs +292 -0
  58. package/crates/team-agent/src/leader/rediscover/tests.rs +525 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1099 -0
  60. package/crates/team-agent/src/leader/start.rs +273 -0
  61. package/crates/team-agent/src/leader/takeover.rs +235 -0
  62. package/crates/team-agent/src/leader/tests/basics.rs +183 -0
  63. package/crates/team-agent/src/leader/tests/byte_findings.rs +234 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +271 -0
  66. package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
  67. package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -0
  68. package/crates/team-agent/src/leader/tests/mod.rs +125 -0
  69. package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
  70. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
  71. package/crates/team-agent/src/leader/types.rs +487 -0
  72. package/crates/team-agent/src/lib.rs +85 -0
  73. package/crates/team-agent/src/lifecycle/display.rs +228 -0
  74. package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
  75. package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
  76. package/crates/team-agent/src/lifecycle/launch.rs +1833 -0
  77. package/crates/team-agent/src/lifecycle/mod.rs +62 -0
  78. package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
  79. package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
  80. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
  81. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
  82. package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
  83. package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
  84. package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
  85. package/crates/team-agent/src/lifecycle/restart.rs +76 -0
  86. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
  88. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
  89. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +933 -0
  90. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
  91. package/crates/team-agent/src/lifecycle/tests.rs +27 -0
  92. package/crates/team-agent/src/lifecycle/types.rs +685 -0
  93. package/crates/team-agent/src/main.rs +41 -0
  94. package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
  95. package/crates/team-agent/src/mcp_server/mod.rs +183 -0
  96. package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
  97. package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
  98. package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
  99. package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
  100. package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
  101. package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
  102. package/crates/team-agent/src/mcp_server/tests/wire.rs +159 -0
  103. package/crates/team-agent/src/mcp_server/tests.rs +38 -0
  104. package/crates/team-agent/src/mcp_server/tools.rs +603 -0
  105. package/crates/team-agent/src/mcp_server/types.rs +421 -0
  106. package/crates/team-agent/src/mcp_server/wire.rs +388 -0
  107. package/crates/team-agent/src/message_store.rs +767 -0
  108. package/crates/team-agent/src/messaging/activity.rs +433 -0
  109. package/crates/team-agent/src/messaging/delivery.rs +542 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +340 -0
  112. package/crates/team-agent/src/messaging/mod.rs +147 -0
  113. package/crates/team-agent/src/messaging/peers.rs +32 -0
  114. package/crates/team-agent/src/messaging/results.rs +537 -0
  115. package/crates/team-agent/src/messaging/scheduler.rs +344 -0
  116. package/crates/team-agent/src/messaging/selftest.rs +100 -0
  117. package/crates/team-agent/src/messaging/send.rs +582 -0
  118. package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
  119. package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
  121. package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
  122. package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
  123. package/crates/team-agent/src/messaging/trust.rs +192 -0
  124. package/crates/team-agent/src/messaging/types.rs +355 -0
  125. package/crates/team-agent/src/messaging/watchers.rs +591 -0
  126. package/crates/team-agent/src/model/enums.rs +311 -0
  127. package/crates/team-agent/src/model/errors.rs +17 -0
  128. package/crates/team-agent/src/model/ids.rs +155 -0
  129. package/crates/team-agent/src/model/mod.rs +22 -0
  130. package/crates/team-agent/src/model/paths.rs +228 -0
  131. package/crates/team-agent/src/model/permissions.rs +567 -0
  132. package/crates/team-agent/src/model/routing.rs +340 -0
  133. package/crates/team-agent/src/model/spec.rs +680 -0
  134. package/crates/team-agent/src/model/task_graph.rs +380 -0
  135. package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
  136. package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
  137. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
  138. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
  139. package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
  140. package/crates/team-agent/src/model/yaml/tests.rs +288 -0
  141. package/crates/team-agent/src/model/yaml.rs +800 -0
  142. package/crates/team-agent/src/packaging/install.rs +305 -0
  143. package/crates/team-agent/src/packaging/migrate.rs +30 -0
  144. package/crates/team-agent/src/packaging/mod.rs +82 -0
  145. package/crates/team-agent/src/packaging/repair.rs +24 -0
  146. package/crates/team-agent/src/packaging/tests.rs +829 -0
  147. package/crates/team-agent/src/packaging/types.rs +369 -0
  148. package/crates/team-agent/src/provider/adapter.rs +801 -0
  149. package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
  150. package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
  151. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
  152. package/crates/team-agent/src/provider/classify.rs +456 -0
  153. package/crates/team-agent/src/provider/faults.rs +136 -0
  154. package/crates/team-agent/src/provider/helpers.rs +41 -0
  155. package/crates/team-agent/src/provider/mod.rs +53 -0
  156. package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
  157. package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
  158. package/crates/team-agent/src/provider/tests/classify.rs +240 -0
  159. package/crates/team-agent/src/provider/tests/faults.rs +120 -0
  160. package/crates/team-agent/src/provider/tests/idle.rs +208 -0
  161. package/crates/team-agent/src/provider/tests/wire.rs +213 -0
  162. package/crates/team-agent/src/provider/tests.rs +31 -0
  163. package/crates/team-agent/src/provider/types.rs +424 -0
  164. package/crates/team-agent/src/state/identity.rs +656 -0
  165. package/crates/team-agent/src/state/mod.rs +58 -0
  166. package/crates/team-agent/src/state/owner_gate.rs +423 -0
  167. package/crates/team-agent/src/state/persist.rs +712 -0
  168. package/crates/team-agent/src/state/projection.rs +657 -0
  169. package/crates/team-agent/src/state/selector.rs +105 -0
  170. package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
  171. package/crates/team-agent/src/tmux_backend/tests.rs +586 -0
  172. package/crates/team-agent/src/tmux_backend.rs +758 -0
  173. package/crates/team-agent/src/transport/test_support.rs +252 -0
  174. package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
  175. package/crates/team-agent/src/transport/tests/mod.rs +199 -0
  176. package/crates/team-agent/src/transport/tests/wire.rs +527 -0
  177. package/crates/team-agent/src/transport.rs +774 -0
  178. package/npm/install.mjs +90 -106
  179. package/package.json +15 -13
  180. package/crates/team-agent-core/Cargo.toml +0 -12
  181. package/crates/team-agent-core/src/lib.rs +0 -332
  182. package/crates/team-agent-core/src/main.rs +0 -152
  183. package/pyproject.toml +0 -18
  184. package/scripts/install.py +0 -88
  185. package/scripts/run_regression_tests.py +0 -83
  186. package/src/team_agent/__init__.py +0 -3
  187. package/src/team_agent/__main__.py +0 -5
  188. package/src/team_agent/_legacy_pane_discovery.py +0 -186
  189. package/src/team_agent/abnormal_track.py +0 -253
  190. package/src/team_agent/approvals/__init__.py +0 -65
  191. package/src/team_agent/approvals/constants.py +0 -6
  192. package/src/team_agent/approvals/parsing.py +0 -176
  193. package/src/team_agent/approvals/runtime_prompts.py +0 -171
  194. package/src/team_agent/approvals/status.py +0 -176
  195. package/src/team_agent/cli/__init__.py +0 -137
  196. package/src/team_agent/cli/commands.py +0 -481
  197. package/src/team_agent/cli/e2e.py +0 -202
  198. package/src/team_agent/cli/helpers.py +0 -226
  199. package/src/team_agent/cli/parser.py +0 -540
  200. package/src/team_agent/compiler.py +0 -334
  201. package/src/team_agent/coordinator/__init__.py +0 -53
  202. package/src/team_agent/coordinator/__main__.py +0 -83
  203. package/src/team_agent/coordinator/lifecycle.py +0 -363
  204. package/src/team_agent/coordinator/metadata.py +0 -61
  205. package/src/team_agent/coordinator/paths.py +0 -17
  206. package/src/team_agent/diagnose/__init__.py +0 -48
  207. package/src/team_agent/diagnose/checks.py +0 -101
  208. package/src/team_agent/diagnose/comms.py +0 -213
  209. package/src/team_agent/diagnose/health.py +0 -241
  210. package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
  211. package/src/team_agent/diagnose/preflight.py +0 -194
  212. package/src/team_agent/diagnose/quick_start.py +0 -324
  213. package/src/team_agent/display/__init__.py +0 -92
  214. package/src/team_agent/display/adaptive.py +0 -511
  215. package/src/team_agent/display/backend.py +0 -46
  216. package/src/team_agent/display/close.py +0 -154
  217. package/src/team_agent/display/ghostty.py +0 -77
  218. package/src/team_agent/display/rebuild.py +0 -102
  219. package/src/team_agent/display/tiling.py +0 -156
  220. package/src/team_agent/display/worker_window.py +0 -114
  221. package/src/team_agent/display/workspace.py +0 -382
  222. package/src/team_agent/errors.py +0 -10
  223. package/src/team_agent/events.py +0 -84
  224. package/src/team_agent/fake_worker.py +0 -80
  225. package/src/team_agent/idle_predicate.py +0 -200
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -111
  228. package/src/team_agent/launch/__init__.py +0 -41
  229. package/src/team_agent/launch/bootstrap.py +0 -85
  230. package/src/team_agent/launch/config.py +0 -106
  231. package/src/team_agent/launch/core.py +0 -301
  232. package/src/team_agent/launch/requirements.py +0 -57
  233. package/src/team_agent/leader/__init__.py +0 -926
  234. package/src/team_agent/leader_binding.py +0 -183
  235. package/src/team_agent/lifecycle/__init__.py +0 -5
  236. package/src/team_agent/lifecycle/agents.py +0 -278
  237. package/src/team_agent/lifecycle/operations.py +0 -411
  238. package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
  239. package/src/team_agent/lifecycle/start.py +0 -363
  240. package/src/team_agent/mcp_server/__init__.py +0 -42
  241. package/src/team_agent/mcp_server/__main__.py +0 -7
  242. package/src/team_agent/mcp_server/contracts.py +0 -148
  243. package/src/team_agent/mcp_server/normalize.py +0 -257
  244. package/src/team_agent/mcp_server/server.py +0 -150
  245. package/src/team_agent/mcp_server/tools.py +0 -352
  246. package/src/team_agent/message_store/__init__.py +0 -23
  247. package/src/team_agent/message_store/agent_health.py +0 -113
  248. package/src/team_agent/message_store/core.py +0 -497
  249. package/src/team_agent/message_store/leader_notification_log.py +0 -198
  250. package/src/team_agent/message_store/result_watchers.py +0 -251
  251. package/src/team_agent/message_store/schema.py +0 -308
  252. package/src/team_agent/message_store/schema_migration.py +0 -448
  253. package/src/team_agent/messaging/__init__.py +0 -1
  254. package/src/team_agent/messaging/activity_detector.py +0 -254
  255. package/src/team_agent/messaging/delivery.py +0 -473
  256. package/src/team_agent/messaging/deps.py +0 -247
  257. package/src/team_agent/messaging/idle_alerts.py +0 -423
  258. package/src/team_agent/messaging/internal_delivery.py +0 -46
  259. package/src/team_agent/messaging/leader.py +0 -497
  260. package/src/team_agent/messaging/leader_api_errors.py +0 -216
  261. package/src/team_agent/messaging/leader_panes.py +0 -673
  262. package/src/team_agent/messaging/owner_bypass.py +0 -29
  263. package/src/team_agent/messaging/result_delivery.py +0 -539
  264. package/src/team_agent/messaging/results.py +0 -447
  265. package/src/team_agent/messaging/scheduler.py +0 -450
  266. package/src/team_agent/messaging/send.py +0 -532
  267. package/src/team_agent/messaging/session_drift.py +0 -94
  268. package/src/team_agent/messaging/tmux_io.py +0 -506
  269. package/src/team_agent/messaging/tmux_prompt.py +0 -338
  270. package/src/team_agent/messaging/trust_auto_answer.py +0 -52
  271. package/src/team_agent/orchestrator/__init__.py +0 -376
  272. package/src/team_agent/orchestrator/plan.py +0 -122
  273. package/src/team_agent/orchestrator/state.py +0 -128
  274. package/src/team_agent/paths.py +0 -45
  275. package/src/team_agent/permissions.py +0 -123
  276. package/src/team_agent/profiles/__init__.py +0 -82
  277. package/src/team_agent/profiles/constants.py +0 -19
  278. package/src/team_agent/profiles/core.py +0 -407
  279. package/src/team_agent/profiles/helpers.py +0 -69
  280. package/src/team_agent/profiles/provider_env.py +0 -188
  281. package/src/team_agent/profiles/smoke.py +0 -201
  282. package/src/team_agent/provider_cli/__init__.py +0 -43
  283. package/src/team_agent/provider_cli/adapter.py +0 -172
  284. package/src/team_agent/provider_cli/base.py +0 -48
  285. package/src/team_agent/provider_cli/claude.py +0 -457
  286. package/src/team_agent/provider_cli/codex.py +0 -336
  287. package/src/team_agent/provider_cli/copilot.py +0 -8
  288. package/src/team_agent/provider_cli/fake.py +0 -39
  289. package/src/team_agent/provider_cli/gemini.py +0 -95
  290. package/src/team_agent/provider_cli/opencode.py +0 -8
  291. package/src/team_agent/provider_cli/prompt.py +0 -62
  292. package/src/team_agent/provider_cli/registry.py +0 -18
  293. package/src/team_agent/provider_cli/unsupported.py +0 -32
  294. package/src/team_agent/provider_state/README.md +0 -78
  295. package/src/team_agent/provider_state/__init__.py +0 -86
  296. package/src/team_agent/provider_state/claude.py +0 -86
  297. package/src/team_agent/provider_state/codex.py +0 -84
  298. package/src/team_agent/provider_state/common.py +0 -207
  299. package/src/team_agent/provider_state/registry.py +0 -118
  300. package/src/team_agent/providers.py +0 -163
  301. package/src/team_agent/quality_gates.py +0 -104
  302. package/src/team_agent/restart/__init__.py +0 -34
  303. package/src/team_agent/restart/orchestration.py +0 -554
  304. package/src/team_agent/restart/selection.py +0 -89
  305. package/src/team_agent/restart/snapshot.py +0 -70
  306. package/src/team_agent/routing.py +0 -84
  307. package/src/team_agent/runtime.py +0 -1239
  308. package/src/team_agent/rust_core.py +0 -327
  309. package/src/team_agent/sessions/__init__.py +0 -25
  310. package/src/team_agent/sessions/capture.py +0 -143
  311. package/src/team_agent/sessions/inventory.py +0 -44
  312. package/src/team_agent/sessions/resume.py +0 -135
  313. package/src/team_agent/simple_yaml.py +0 -236
  314. package/src/team_agent/spec.py +0 -370
  315. package/src/team_agent/state.py +0 -602
  316. package/src/team_agent/status/__init__.py +0 -63
  317. package/src/team_agent/status/approvals.py +0 -52
  318. package/src/team_agent/status/compact.py +0 -158
  319. package/src/team_agent/status/constants.py +0 -18
  320. package/src/team_agent/status/inbox.py +0 -58
  321. package/src/team_agent/status/peek.py +0 -117
  322. package/src/team_agent/status/queries.py +0 -199
  323. package/src/team_agent/task_graph.py +0 -80
  324. package/src/team_agent/terminal.py +0 -57
  325. package/src/team_agent/wake.py +0 -58
  326. package/src/team_agent/watch/__init__.py +0 -145
@@ -0,0 +1,716 @@
1
+ //! `team.db` 列布局迁移(Gap 46;真相源 `message_store/schema_migration.py`)。
2
+ //!
3
+ //! 检测**物理列序漂移**(`pragma table_info` 顺序 vs canonical),在**一个原子事务**
4
+ //! (`BEGIN IMMEDIATE`)内整表 rebuild(temp 复制 → rename → drop),保证「崩溃只留迁移前
5
+ //! 或迁移后,绝不半成品」(事务回滚提供该不变量)。rebuild 前先写
6
+ //! `team.db.pre-migration-<utc>-from-v<N>.bak` 备份。行数前后必须不变(否则报错)。
7
+
8
+ use std::path::{Path, PathBuf};
9
+
10
+ use rusqlite::Connection;
11
+
12
+ use crate::db::schema::{ensure_schema_indexes, table_layout, SCHEMA_VERSION};
13
+ use crate::db::DbError;
14
+
15
+ /// canonical 列序(`schema_migration.py:MANAGED_TABLE_LAYOUTS`)。
16
+ pub const MANAGED_TABLE_LAYOUTS: &[(&str, &[&str])] = &[
17
+ ("messages", &[
18
+ "message_id", "owner_team_id", "task_id", "sender", "recipient", "reply_to", "requires_ack",
19
+ "status", "content", "artifact_refs", "created_at", "updated_at", "delivered_at",
20
+ "acknowledged_at", "error", "delivery_attempts",
21
+ ]),
22
+ ("results", &["result_id", "owner_team_id", "task_id", "agent_id", "envelope", "status", "created_at"]),
23
+ ("scheduled_events", &[
24
+ "id", "owner_team_id", "due_at", "target", "kind", "payload_json", "status", "created_at",
25
+ "fired_at", "result_json",
26
+ ]),
27
+ ("delivery_tokens", &[
28
+ "message_id", "unique_token", "injected_at", "visible_at", "consumed_at", "failed_at",
29
+ "failure_reason",
30
+ ]),
31
+ ("agent_health", &[
32
+ "owner_team_id", "agent_id", "status", "last_output_at", "context_usage_pct", "current_task_id",
33
+ "updated_at",
34
+ ]),
35
+ ("peer_allowlist", &["a", "b", "created_at"]),
36
+ ("result_watchers", &[
37
+ "watcher_id", "owner_team_id", "task_id", "agent_id", "message_id", "leader_id", "status",
38
+ "created_at", "completed_at", "result_id", "notified_message_id", "error",
39
+ ]),
40
+ ("leader_notification_log", &[
41
+ "result_id", "owner_team_id", "owner_epoch", "leader_session_uuid", "notified_message_id",
42
+ "notified_at", "leader_pane_id_at_notify", "envelope_content_hash",
43
+ ]),
44
+ ];
45
+
46
+ /// rebuild / 建缺表用的 DDL 模板(`schema_migration.py:CREATE_TABLE_SQL`,`__TABLE__` 占位)。
47
+ const CREATE_TABLE_TEMPLATES: &[(&str, &str)] = &[
48
+ ("messages", "create table if not exists __TABLE__ (\n message_id text primary key,\n owner_team_id text,\n task_id text,\n sender text,\n recipient text,\n reply_to text,\n requires_ack integer,\n status text,\n content text,\n artifact_refs text,\n created_at text,\n updated_at text,\n delivered_at text,\n acknowledged_at text,\n error text,\n delivery_attempts integer not null default 0\n )"),
49
+ ("results", "create table if not exists __TABLE__ (\n result_id text primary key,\n owner_team_id text,\n task_id text not null,\n agent_id text not null,\n envelope text not null,\n status text not null,\n created_at text not null\n )"),
50
+ ("scheduled_events", "create table if not exists __TABLE__ (\n id integer primary key,\n owner_team_id text,\n due_at text not null,\n target text not null,\n kind text not null,\n payload_json text not null,\n status text not null,\n created_at text not null,\n fired_at text,\n result_json text\n )"),
51
+ ("delivery_tokens", "create table if not exists __TABLE__ (\n message_id text primary key,\n unique_token text not null,\n injected_at text not null,\n visible_at text,\n consumed_at text,\n failed_at text,\n failure_reason text\n )"),
52
+ ("agent_health", "create table if not exists __TABLE__ (\n owner_team_id text,\n agent_id text not null,\n status text not null,\n last_output_at text,\n context_usage_pct integer,\n current_task_id text,\n updated_at text not null,\n unique(owner_team_id, agent_id)\n )"),
53
+ ("peer_allowlist", "create table if not exists __TABLE__ (\n a text not null,\n b text not null,\n created_at text not null,\n primary key (a, b)\n )"),
54
+ ("result_watchers", "create table if not exists __TABLE__ (\n watcher_id text primary key,\n owner_team_id text,\n task_id text,\n agent_id text,\n message_id text,\n leader_id text not null,\n status text not null,\n created_at text not null,\n completed_at text,\n result_id text,\n notified_message_id text,\n error text\n )"),
55
+ ("leader_notification_log", "create table if not exists __TABLE__ (\n result_id text not null,\n owner_team_id text not null default '',\n owner_epoch integer not null default 0,\n leader_session_uuid text,\n notified_message_id text not null,\n notified_at text not null,\n leader_pane_id_at_notify text,\n envelope_content_hash text,\n primary key (result_id, owner_team_id, owner_epoch)\n )"),
56
+ ];
57
+
58
+ fn create_table_ddl(table: &str, name: &str) -> Option<String> {
59
+ CREATE_TABLE_TEMPLATES
60
+ .iter()
61
+ .find(|(t, _)| *t == table)
62
+ .map(|(_, ddl)| ddl.replace("__TABLE__", name))
63
+ }
64
+
65
+ /// 一张表的布局 diff。
66
+ #[derive(Debug, Clone)]
67
+ pub struct Diff {
68
+ pub table: &'static str,
69
+ pub expected: Vec<&'static str>,
70
+ pub actual: Vec<String>,
71
+ pub missing: bool,
72
+ }
73
+
74
+ /// 一次表 rebuild 的审计事件(对应 Python `schema.layout_rebuild`)。
75
+ #[derive(Debug, Clone)]
76
+ pub struct RebuildEvent {
77
+ pub table: &'static str,
78
+ pub from_layout_columns: Vec<String>,
79
+ pub to_layout_columns: Vec<String>,
80
+ pub backup_path: String,
81
+ pub row_count_before: i64,
82
+ pub row_count_after: i64,
83
+ pub missing: bool,
84
+ }
85
+
86
+ /// `schema_diagnosis` 的只读结论。
87
+ #[derive(Debug, Clone, PartialEq, Eq)]
88
+ pub struct Diagnosis {
89
+ pub ok: bool,
90
+ pub status: String,
91
+ pub user_version: i64,
92
+ pub layout_diffs: Vec<String>,
93
+ }
94
+
95
+ fn table_exists(conn: &Connection, table: &str) -> Result<bool, DbError> {
96
+ let n: i64 = conn.query_row(
97
+ "select count(*) from sqlite_master where type = 'table' and name = ?1",
98
+ [table],
99
+ |row| row.get(0),
100
+ )?;
101
+ Ok(n > 0)
102
+ }
103
+
104
+ fn table_count(conn: &Connection, table: &str) -> Result<i64, DbError> {
105
+ // table 来自 MANAGED_TABLE_LAYOUTS 固定常量名,非用户输入 → format 安全。
106
+ Ok(conn.query_row(&format!("select count(*) from {table}"), [], |r| r.get(0))?)
107
+ }
108
+
109
+ pub fn pragma_user_version(conn: &Connection) -> Result<i64, DbError> {
110
+ Ok(conn.query_row("pragma user_version", [], |r| r.get(0))?)
111
+ }
112
+
113
+ /// `schema_migration.py:_db_path_from_conn`:从 `pragma database_list` 的 main 行解析文件路径。
114
+ /// in-memory(file 列为空)→ `None`。
115
+ fn db_path_from_conn(conn: &Connection) -> Option<PathBuf> {
116
+ conn.query_row("pragma database_list", [], |r| r.get::<_, String>(2))
117
+ .ok()
118
+ .filter(|f| !f.is_empty())
119
+ .map(PathBuf::from)
120
+ }
121
+
122
+ // 原子性探针(对抗检查 high gap):测试态下在 rebuild 的 insert 之后注入 Err,
123
+ // 验证 ensure_table_layout 的 BEGIN IMMEDIATE 事务**回滚到迁移前态、不留半成品**
124
+ // (Gap-46 唯一存在理由)。对应 Python `_maybe_fault_after_insert`(env→os._exit)。
125
+ #[cfg(test)]
126
+ thread_local! {
127
+ static FAULT_AFTER_INSERT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
128
+ }
129
+ fn maybe_test_fault_after_insert() -> Result<(), DbError> {
130
+ #[cfg(test)]
131
+ {
132
+ if FAULT_AFTER_INSERT.with(std::cell::Cell::get) {
133
+ return Err(DbError::Schema("injected test fault after insert (atomicity probe)".to_string()));
134
+ }
135
+ }
136
+ Ok(())
137
+ }
138
+
139
+ /// `schema_migration.py:_run_version_migrations`:current→schema_version 链式(v1/2/3 均 no-op)。
140
+ fn run_version_migrations(conn: &Connection, schema_version: i64) -> Result<(), DbError> {
141
+ let current = pragma_user_version(conn)?;
142
+ for _version in current..=schema_version {
143
+ // SCHEMA_MIGRATIONS:目前 1/2/3 全 no-op(`schema_migration.py:161-165`)。
144
+ }
145
+ Ok(())
146
+ }
147
+
148
+ /// `schema_migration.py:_layout_diffs`:按 `table_info` 物理列序检测漂移。
149
+ /// 无任何 managed 表存在(fresh DB)→ 空(initialize 随后建表)。
150
+ pub fn layout_diffs(conn: &Connection) -> Result<Vec<Diff>, DbError> {
151
+ let mut any = false;
152
+ for (t, _) in MANAGED_TABLE_LAYOUTS {
153
+ if table_exists(conn, t)? {
154
+ any = true;
155
+ break;
156
+ }
157
+ }
158
+ if !any {
159
+ return Ok(Vec::new());
160
+ }
161
+ let mut diffs = Vec::new();
162
+ for (table, expected) in MANAGED_TABLE_LAYOUTS {
163
+ if !table_exists(conn, table)? {
164
+ diffs.push(Diff { table, expected: expected.to_vec(), actual: vec![], missing: true });
165
+ continue;
166
+ }
167
+ let actual = table_layout(conn, table)?;
168
+ if !actual.iter().map(String::as_str).eq(expected.iter().copied()) {
169
+ diffs.push(Diff { table, expected: expected.to_vec(), actual, missing: false });
170
+ }
171
+ }
172
+ Ok(diffs)
173
+ }
174
+
175
+ /// `schema_migration.py:_rebuild_tables`:missing → 建表;漂移 → temp 复制 common 列 → rename。
176
+ /// 行数前后不变(否则报错)。须在调用方的事务内执行。
177
+ fn rebuild_tables(
178
+ conn: &Connection,
179
+ diffs: &[Diff],
180
+ backup_path: &Path,
181
+ ) -> Result<Vec<RebuildEvent>, DbError> {
182
+ let backup = backup_path.display().to_string();
183
+ let mut events = Vec::new();
184
+ for diff in diffs {
185
+ let table = diff.table;
186
+ let expected = MANAGED_TABLE_LAYOUTS
187
+ .iter()
188
+ .find(|(t, _)| *t == table)
189
+ .map(|(_, e)| *e)
190
+ .ok_or_else(|| DbError::Schema(format!("unknown managed table {table}")))?;
191
+ let ddl = |name: &str| {
192
+ create_table_ddl(table, name)
193
+ .ok_or_else(|| DbError::Schema(format!("no DDL template for {table}")))
194
+ };
195
+ if diff.missing {
196
+ let before = 0;
197
+ conn.execute(&ddl(table)?, [])?;
198
+ let after = table_count(conn, table)?;
199
+ if after != before {
200
+ return Err(DbError::Schema(format!(
201
+ "schema rebuild row count changed for {table}: {before} != {after}"
202
+ )));
203
+ }
204
+ events.push(RebuildEvent {
205
+ table,
206
+ from_layout_columns: vec![],
207
+ to_layout_columns: expected.iter().map(|s| s.to_string()).collect(),
208
+ backup_path: backup.clone(),
209
+ row_count_before: before,
210
+ row_count_after: after,
211
+ missing: true,
212
+ });
213
+ continue;
214
+ }
215
+ let temp = format!("__team_agent_rebuild_{table}");
216
+ let old = format!("__team_agent_old_{table}");
217
+ let before = table_count(conn, table)?;
218
+ conn.execute(&format!("drop table if exists {temp}"), [])?;
219
+ conn.execute(&format!("drop table if exists {old}"), [])?;
220
+ conn.execute(&ddl(&temp)?, [])?;
221
+ // common = expected ∩ actual,按 expected 顺序。
222
+ let common: Vec<&str> = expected
223
+ .iter()
224
+ .copied()
225
+ .filter(|c| diff.actual.iter().any(|a| a == c))
226
+ .collect();
227
+ let column_sql = common.join(", ");
228
+ conn.execute(&format!("insert into {temp}({column_sql}) select {column_sql} from {table}"), [])?;
229
+ maybe_test_fault_after_insert()?; // insert 后、rename 前注入点(原子性探针)
230
+ conn.execute(&format!("alter table {table} rename to {old}"), [])?;
231
+ conn.execute(&format!("alter table {temp} rename to {table}"), [])?;
232
+ conn.execute(&format!("drop table {old}"), [])?;
233
+ let after = table_count(conn, table)?;
234
+ if before != after {
235
+ return Err(DbError::Schema(format!(
236
+ "schema rebuild row count changed for {table}: {before} != {after}"
237
+ )));
238
+ }
239
+ events.push(RebuildEvent {
240
+ table,
241
+ from_layout_columns: diff.actual.clone(),
242
+ to_layout_columns: expected.iter().map(|s| s.to_string()).collect(),
243
+ backup_path: backup.clone(),
244
+ row_count_before: before,
245
+ row_count_after: after,
246
+ missing: false,
247
+ });
248
+ }
249
+ ensure_schema_indexes(conn)?;
250
+ Ok(events)
251
+ }
252
+
253
+ fn backup_path(db_path: &Path, user_version: i64) -> PathBuf {
254
+ let stamp = chrono::Utc::now().format("%Y%m%dT%H%M%SZ");
255
+ db_path.with_file_name(format!("team.db.pre-migration-{stamp}-from-v{user_version}.bak"))
256
+ }
257
+
258
+ /// `schema_migration.py:ensure_table_layout`:版本迁移 + (有漂移则)备份 + 整表 rebuild,
259
+ /// 全程一个 `BEGIN IMMEDIATE` 事务(崩溃 → 回滚到迁移前;成功 → 迁移后;绝不半成品)。
260
+ /// fresh DB(无 managed 表)→ 无 diff → 无需 db_path/备份,返回空。
261
+ pub fn ensure_table_layout(
262
+ conn: &Connection,
263
+ schema_version: i64,
264
+ db_path: Option<&Path>,
265
+ ) -> Result<Vec<RebuildEvent>, DbError> {
266
+ run_version_migrations(conn, schema_version)?;
267
+ let started_tx = conn.is_autocommit();
268
+ if started_tx {
269
+ conn.execute_batch("BEGIN IMMEDIATE")?;
270
+ }
271
+ let work = || -> Result<Vec<RebuildEvent>, DbError> {
272
+ let diffs = layout_diffs(conn)?;
273
+ if diffs.is_empty() {
274
+ return Ok(Vec::new());
275
+ }
276
+ // db_path=None 时经 `pragma database_list` 回退解析真实文件路径(`_db_path_from_conn`);
277
+ // in-memory(file 空)→ 仍无路径 → Err。修复对抗检查发现的 divergence。
278
+ let dbp = match db_path {
279
+ Some(p) => p.to_path_buf(),
280
+ None => db_path_from_conn(conn).ok_or_else(|| {
281
+ DbError::Schema("cannot rebuild team.db layout without a database path".to_string())
282
+ })?,
283
+ };
284
+ let uv = pragma_user_version(conn)?;
285
+ let backup = backup_path(&dbp, uv);
286
+ if let Some(parent) = backup.parent() {
287
+ std::fs::create_dir_all(parent).map_err(|e| DbError::Schema(format!("backup mkdir: {e}")))?;
288
+ }
289
+ std::fs::copy(dbp, &backup).map_err(|e| DbError::Schema(format!("backup copy: {e}")))?;
290
+ rebuild_tables(conn, &diffs, &backup)
291
+ };
292
+ match work() {
293
+ Ok(events) => {
294
+ if started_tx {
295
+ conn.execute_batch("COMMIT")?;
296
+ }
297
+ // Gap 46 契约:每次 rebuild 在 COMMIT 后发射 schema.layout_rebuild 事件(step 3↔4 集成点)。
298
+ // 回滚的 rebuild 不发(post-commit 语义)。best-effort,不让审计失败拖垮迁移。
299
+ if !events.is_empty() {
300
+ let resolved = db_path.map(Path::to_path_buf).or_else(|| db_path_from_conn(conn));
301
+ if let Some(p) = resolved {
302
+ emit_rebuild_events(&p, &events);
303
+ }
304
+ }
305
+ Ok(events)
306
+ }
307
+ Err(e) => {
308
+ if started_tx && !conn.is_autocommit() {
309
+ let _ = conn.execute_batch("ROLLBACK");
310
+ }
311
+ Err(e)
312
+ }
313
+ }
314
+ }
315
+
316
+ /// `schema_migration.py:schema_diagnosis`:只读判定(不变更 DB)。
317
+ pub fn schema_diagnosis(db_path: &Path, schema_version: i64) -> Result<Diagnosis, DbError> {
318
+ if !db_path.exists() {
319
+ return Ok(Diagnosis {
320
+ ok: true,
321
+ status: "missing".to_string(),
322
+ user_version: 0,
323
+ layout_diffs: vec![],
324
+ });
325
+ }
326
+ let conn = Connection::open(db_path)?;
327
+ let uv = pragma_user_version(&conn)?;
328
+ let diffs = layout_diffs(&conn)?;
329
+ let diff_tables: Vec<String> = diffs.iter().map(|d| d.table.to_string()).collect();
330
+ let ok = diffs.is_empty() && uv == schema_version;
331
+ Ok(Diagnosis {
332
+ ok,
333
+ status: if ok { "ok".to_string() } else { "schema_repair_available".to_string() },
334
+ user_version: uv,
335
+ layout_diffs: diff_tables,
336
+ })
337
+ }
338
+
339
+ /// 便捷:对一个 workspace 的 `.team/runtime/team.db` 跑 diagnosis(对应 Python 签名)。
340
+ pub fn schema_diagnosis_workspace(workspace: &Path) -> Result<Diagnosis, DbError> {
341
+ schema_diagnosis(&workspace.join(".team").join("runtime").join("team.db"), SCHEMA_VERSION)
342
+ }
343
+
344
+ /// `schema_migration.py:_workspace_from_db_path`:`<ws>/.team/runtime/team.db` → `<ws>`。
345
+ fn workspace_from_db_path(db_path: &Path) -> Option<PathBuf> {
346
+ let parts: Vec<_> = db_path.iter().collect();
347
+ let n = parts.len();
348
+ if n >= 3 && parts[n - 3] == ".team" && parts[n - 2] == "runtime" && parts[n - 1] == "team.db" {
349
+ db_path.parent()?.parent()?.parent().map(Path::to_path_buf)
350
+ } else {
351
+ None
352
+ }
353
+ }
354
+
355
+ /// `schema_migration.py:_emit_rebuild_events`:每个 rebuild 事件写 `schema.layout_rebuild`(step 4 EventLog)。
356
+ /// best-effort:审计写失败不影响已提交的迁移。
357
+ fn emit_rebuild_events(db_path: &Path, events: &[RebuildEvent]) {
358
+ let Some(workspace) = workspace_from_db_path(db_path) else {
359
+ return;
360
+ };
361
+ let log = crate::event_log::EventLog::new(&workspace);
362
+ for e in events {
363
+ let fields = serde_json::json!({
364
+ "table": e.table,
365
+ "from_layout_columns": e.from_layout_columns,
366
+ "to_layout_columns": e.to_layout_columns,
367
+ "backup_path": e.backup_path,
368
+ "row_count_before": e.row_count_before,
369
+ "row_count_after": e.row_count_after,
370
+ "missing": e.missing,
371
+ });
372
+ let _ = log.write("schema.layout_rebuild", fields);
373
+ }
374
+ }
375
+
376
+ /// `fix_schema_layout` 的结构化结果(对应 Python dict)。
377
+ /// `schema.layout_rebuild` / `schema.layout_rebuild_blocked` 事件发射 → step 4 EventLog 接线。
378
+ #[derive(Debug, Clone)]
379
+ pub enum FixResult {
380
+ /// db 不存在 → 等价 diagnosis(missing)。
381
+ Missing(Diagnosis),
382
+ /// 撞活跃锁 → 拒绝且**不写备份**(`status:'blocked', reason:'active_lock'`)。
383
+ Blocked { reason: String },
384
+ /// 已修复 → diagnosis(ok)+ rebuild 事件。
385
+ Fixed { diagnosis: Diagnosis, rebuilds: Vec<RebuildEvent> },
386
+ }
387
+
388
+ /// `schema_migration.py:_db_lock_status`:0-timeout `BEGIN IMMEDIATE` 探锁;locked/busy → `active_lock`。
389
+ fn db_lock_status(db_path: &Path) -> Result<Option<String>, DbError> {
390
+ let conn = Connection::open(db_path)?;
391
+ conn.busy_timeout(std::time::Duration::from_secs(0))?;
392
+ match conn.execute_batch("BEGIN IMMEDIATE") {
393
+ Ok(()) => {
394
+ let _ = conn.execute_batch("ROLLBACK");
395
+ Ok(None)
396
+ }
397
+ Err(e) => {
398
+ let msg = e.to_string().to_lowercase();
399
+ if msg.contains("locked") || msg.contains("busy") {
400
+ Ok(Some("active_lock".to_string()))
401
+ } else {
402
+ Err(DbError::Sqlite(e))
403
+ }
404
+ }
405
+ }
406
+ }
407
+
408
+ /// `schema_migration.py:fix_schema_layout`:`doctor --fix-schema` 的迁移实体。
409
+ /// 先探锁(撞锁则 blocked 且不写备份),否则备份先于破坏性写 + 原子 rebuild + 重诊断。
410
+ pub fn fix_schema_layout(workspace: &Path, schema_version: i64) -> Result<FixResult, DbError> {
411
+ let db_path = workspace.join(".team").join("runtime").join("team.db");
412
+ if !db_path.exists() {
413
+ return Ok(FixResult::Missing(schema_diagnosis(&db_path, schema_version)?));
414
+ }
415
+ if let Some(reason) = db_lock_status(&db_path)? {
416
+ let _ = crate::event_log::EventLog::new(workspace).write(
417
+ "schema.layout_rebuild_blocked",
418
+ serde_json::json!({"reason": reason, "db_path": db_path.display().to_string()}),
419
+ );
420
+ return Ok(FixResult::Blocked { reason });
421
+ }
422
+ let conn = Connection::open(&db_path)?;
423
+ conn.busy_timeout(std::time::Duration::from_secs(30))?;
424
+ let rebuilds = ensure_table_layout(&conn, schema_version, Some(&db_path))?;
425
+ conn.execute_batch(&format!("pragma user_version = {schema_version}"))?;
426
+ drop(conn);
427
+ let diagnosis = schema_diagnosis(&db_path, schema_version)?;
428
+ Ok(FixResult::Fixed { diagnosis, rebuilds })
429
+ }
430
+
431
+ #[cfg(test)]
432
+ mod tests {
433
+ #![allow(clippy::unwrap_used, clippy::panic, clippy::expect_used)]
434
+ use super::*;
435
+ use crate::db::schema::initialize_schema;
436
+ use std::sync::atomic::{AtomicU32, Ordering};
437
+
438
+ static SEQ: AtomicU32 = AtomicU32::new(0);
439
+
440
+ fn temp_db() -> PathBuf {
441
+ let n = SEQ.fetch_add(1, Ordering::Relaxed);
442
+ let db = std::env::temp_dir()
443
+ .join(format!("ta_rs_db_{}_{}", std::process::id(), n))
444
+ .join(".team")
445
+ .join("runtime")
446
+ .join("team.db");
447
+ std::fs::create_dir_all(db.parent().unwrap()).unwrap();
448
+ db
449
+ }
450
+
451
+ /// 造 legacy DB:4 表 owner_team_id 在末列(列序漂移),各 2 行,user_version=1。
452
+ fn build_legacy(path: &Path) {
453
+ std::fs::create_dir_all(path.parent().unwrap()).unwrap();
454
+ let conn = Connection::open(path).unwrap();
455
+ conn.execute_batch(
456
+ "create table messages (message_id text primary key, task_id text, sender text, recipient text, reply_to text, requires_ack integer, status text, content text, artifact_refs text, created_at text, updated_at text, delivered_at text, acknowledged_at text, error text, delivery_attempts integer not null default 0, owner_team_id text);
457
+ create table results (result_id text primary key, task_id text not null, agent_id text not null, envelope text not null, status text not null, created_at text not null, owner_team_id text);
458
+ create table scheduled_events (id integer primary key, due_at text not null, target text not null, kind text not null, payload_json text not null, status text not null, created_at text not null, fired_at text, result_json text, owner_team_id text);
459
+ create table agent_health (agent_id text not null, status text not null, last_output_at text, context_usage_pct integer, current_task_id text, updated_at text not null, owner_team_id text);",
460
+ ).unwrap();
461
+ conn.execute("insert into messages(message_id, status, owner_team_id) values ('m1','sent','t'),('m2','sent','t')", []).unwrap();
462
+ conn.execute("insert into results(result_id, task_id, agent_id, envelope, status, created_at, owner_team_id) values ('r1','t1','a','{}','success','now','t'),('r2','t2','a','{}','success','now','t')", []).unwrap();
463
+ conn.execute("insert into scheduled_events(due_at,target,kind,payload_json,status,created_at) values ('d','x','k','{}','pending','now'),('d','x','k','{}','pending','now')", []).unwrap();
464
+ conn.execute("insert into agent_health(agent_id,status,updated_at,owner_team_id) values ('a','idle','now','t'),('b','idle','now','t')", []).unwrap();
465
+ conn.execute_batch("pragma user_version = 1").unwrap();
466
+ }
467
+
468
+ fn layout(conn: &Connection, t: &str) -> Vec<String> {
469
+ table_layout(conn, t).unwrap()
470
+ }
471
+
472
+ // golden:Python build_legacy_workspace + fix_schema_layout(team-agent-public@439bef8)。
473
+ #[test]
474
+ fn legacy_db_migrates_to_canonical_layout_preserving_rows() {
475
+ let path = temp_db();
476
+ build_legacy(&path);
477
+
478
+ // 迁移前:diagnosis = schema_repair_available,全 8 表 diff(4 漂移 + 4 缺失)。
479
+ let before = schema_diagnosis(&path, SCHEMA_VERSION).unwrap();
480
+ assert_eq!(before.status, "schema_repair_available");
481
+ assert!(!before.ok);
482
+ assert_eq!(before.user_version, 1);
483
+ assert_eq!(before.layout_diffs.len(), 8);
484
+
485
+ // initialize_schema 走 ensure_table_layout(rebuild)+ 建表 + user_version=3。
486
+ let conn = crate::db::schema::open_db(&path).unwrap();
487
+ initialize_schema(&conn, Some(&path)).unwrap();
488
+
489
+ // 迁移后:4 表 canonical 列序 + 行数保全。
490
+ assert_eq!(layout(&conn, "messages")[..3], ["message_id", "owner_team_id", "task_id"]);
491
+ assert_eq!(layout(&conn, "agent_health")[..2], ["owner_team_id", "agent_id"]);
492
+ assert_eq!(table_count(&conn, "messages").unwrap(), 2);
493
+ assert_eq!(table_count(&conn, "results").unwrap(), 2);
494
+ assert_eq!(table_count(&conn, "scheduled_events").unwrap(), 2);
495
+ assert_eq!(table_count(&conn, "agent_health").unwrap(), 2);
496
+ // 行内容保全(owner_team_id 迁移后仍可读)。
497
+ let m1: String = conn.query_row("select owner_team_id from messages where message_id='m1'", [], |r| r.get(0)).unwrap();
498
+ assert_eq!(m1, "t");
499
+ drop(conn);
500
+
501
+ // 迁移后 diagnosis = ok / user_version=3。
502
+ let after = schema_diagnosis(&path, SCHEMA_VERSION).unwrap();
503
+ assert!(after.ok);
504
+ assert_eq!(after.status, "ok");
505
+ assert_eq!(after.user_version, 3);
506
+
507
+ // 备份文件已写(team.db.pre-migration-*-from-v1.bak)。
508
+ let runtime = path.parent().unwrap();
509
+ let baks: Vec<_> = std::fs::read_dir(runtime).unwrap()
510
+ .filter_map(|e| e.ok())
511
+ .filter(|e| {
512
+ let n = e.file_name().to_string_lossy().to_string();
513
+ n.starts_with("team.db.pre-migration-") && n.ends_with("-from-v1.bak")
514
+ })
515
+ .collect();
516
+ assert_eq!(baks.len(), 1, "应有且仅有一个 from-v1 备份");
517
+ }
518
+
519
+ #[test]
520
+ fn fresh_db_no_diffs_and_diagnosis_ok() {
521
+ let path = temp_db();
522
+ let conn = crate::db::schema::open_db(&path).unwrap();
523
+ initialize_schema(&conn, Some(&path)).unwrap(); // fresh:ensure_table_layout no-op
524
+ assert!(layout_diffs(&conn).unwrap().is_empty());
525
+ drop(conn);
526
+ let d = schema_diagnosis(&path, SCHEMA_VERSION).unwrap();
527
+ assert!(d.ok);
528
+ assert_eq!(d.status, "ok");
529
+ assert_eq!(d.user_version, 3);
530
+ }
531
+
532
+ #[test]
533
+ fn diagnosis_missing_db() {
534
+ let path = temp_db(); // 未创建文件
535
+ let d = schema_diagnosis(&path, SCHEMA_VERSION).unwrap();
536
+ assert!(d.ok);
537
+ assert_eq!(d.status, "missing");
538
+ assert_eq!(d.user_version, 0);
539
+ }
540
+
541
+ #[test]
542
+ fn rebuild_preserves_row_count_invariant() {
543
+ // 再迁移一次(已 canonical)→ 无 diff → 无 rebuild。
544
+ let path = temp_db();
545
+ build_legacy(&path);
546
+ let conn = crate::db::schema::open_db(&path).unwrap();
547
+ let events1 = ensure_table_layout(&conn, SCHEMA_VERSION, Some(&path)).unwrap();
548
+ assert_eq!(events1.len(), 8); // 4 漂移 rebuild + 4 缺失建表
549
+ for e in &events1 {
550
+ assert_eq!(e.row_count_before, e.row_count_after);
551
+ }
552
+ let events2 = ensure_table_layout(&conn, SCHEMA_VERSION, Some(&path)).unwrap();
553
+ assert!(events2.is_empty(), "已 canonical,二次 ensure 无 rebuild");
554
+ }
555
+
556
+ // 对抗检查 HIGH:崩溃原子性 —— insert 后、rename 前注入故障,验事务回滚到迁移前态、无半成品。
557
+ #[test]
558
+ fn crash_after_insert_rolls_back_to_pre_migration_state() {
559
+ let path = temp_db();
560
+ build_legacy(&path);
561
+ let conn = crate::db::schema::open_db(&path).unwrap();
562
+ FAULT_AFTER_INSERT.with(|c| c.set(true));
563
+ let r = ensure_table_layout(&conn, SCHEMA_VERSION, Some(&path));
564
+ FAULT_AFTER_INSERT.with(|c| c.set(false));
565
+ assert!(r.is_err(), "注入故障后 ensure_table_layout 必须 Err");
566
+ // 重开 DB:必须是迁移前态 —— messages 仍 owner_team_id 末列,无残留 temp/old 表,行数不变。
567
+ drop(conn);
568
+ let conn2 = Connection::open(&path).unwrap();
569
+ assert_eq!(
570
+ table_layout(&conn2, "messages").unwrap().last().map(String::as_str),
571
+ Some("owner_team_id"),
572
+ "回滚后 messages 应仍是 legacy 布局(owner_team_id 末列)"
573
+ );
574
+ let residue: i64 = conn2
575
+ .query_row(
576
+ "select count(*) from sqlite_master where type='table' and (name like '__team_agent_rebuild_%' or name like '__team_agent_old_%')",
577
+ [], |r| r.get(0),
578
+ )
579
+ .unwrap();
580
+ assert_eq!(residue, 0, "不得残留 rebuild/old 临时表");
581
+ assert_eq!(table_count(&conn2, "messages").unwrap(), 2);
582
+ }
583
+
584
+ // db_path=None + 文件 conn + 有 diff → 经 pragma database_list 回退解析路径并成功(修复的 divergence)。
585
+ #[test]
586
+ fn ensure_table_layout_none_db_path_falls_back_to_conn_path() {
587
+ let path = temp_db();
588
+ build_legacy(&path);
589
+ let conn = crate::db::schema::open_db(&path).unwrap();
590
+ let events = ensure_table_layout(&conn, SCHEMA_VERSION, None).unwrap(); // None 不再 Err
591
+ assert_eq!(events.len(), 8);
592
+ assert_eq!(table_layout(&conn, "messages").unwrap()[..2], ["message_id", "owner_team_id"]);
593
+ }
594
+
595
+ // agent_health 完全缺 owner_team_id 列 → generic rebuild 补 NULL 列、行保全。
596
+ #[test]
597
+ fn agent_health_missing_owner_team_id_column_rebuilds_with_null() {
598
+ let path = temp_db();
599
+ std::fs::create_dir_all(path.parent().unwrap()).unwrap();
600
+ let c = Connection::open(&path).unwrap();
601
+ c.execute_batch(
602
+ "create table agent_health (agent_id text not null, status text not null, last_output_at text, context_usage_pct integer, current_task_id text, updated_at text not null, unique(agent_id));
603
+ insert into agent_health(agent_id,status,updated_at) values ('a','idle','now');
604
+ pragma user_version = 1;",
605
+ ).unwrap();
606
+ drop(c);
607
+ let conn = crate::db::schema::open_db(&path).unwrap();
608
+ initialize_schema(&conn, Some(&path)).unwrap();
609
+ assert_eq!(table_layout(&conn, "agent_health").unwrap()[0], "owner_team_id");
610
+ assert_eq!(table_count(&conn, "agent_health").unwrap(), 1);
611
+ let owner: Option<String> = conn
612
+ .query_row("select owner_team_id from agent_health where agent_id='a'", [], |r| r.get(0))
613
+ .unwrap();
614
+ assert_eq!(owner, None, "补的 owner_team_id 列应为 NULL");
615
+ }
616
+
617
+ // legacy 多出 expected 之外的废列 → rebuild 丢弃(common = expected ∩ actual)。
618
+ #[test]
619
+ fn extra_legacy_column_is_dropped_on_rebuild() {
620
+ let path = temp_db();
621
+ std::fs::create_dir_all(path.parent().unwrap()).unwrap();
622
+ let c = Connection::open(&path).unwrap();
623
+ // messages canonical 列 + 末尾一个废列 legacy_junk → 触发列序 diff。
624
+ c.execute_batch(
625
+ "create table messages (message_id text primary key, owner_team_id text, task_id text, sender text, recipient text, reply_to text, requires_ack integer, status text, content text, artifact_refs text, created_at text, updated_at text, delivered_at text, acknowledged_at text, error text, delivery_attempts integer not null default 0, legacy_junk text);
626
+ insert into messages(message_id, legacy_junk) values ('m1','junk');
627
+ pragma user_version = 1;",
628
+ ).unwrap();
629
+ drop(c);
630
+ let conn = crate::db::schema::open_db(&path).unwrap();
631
+ initialize_schema(&conn, Some(&path)).unwrap();
632
+ let cols = table_layout(&conn, "messages").unwrap();
633
+ assert!(!cols.iter().any(|c| c == "legacy_junk"), "废列应被丢弃");
634
+ assert_eq!(cols.len(), 16);
635
+ assert_eq!(table_count(&conn, "messages").unwrap(), 1);
636
+ }
637
+
638
+ // unicode / NULL 行内容经 rebuild 字节保全。
639
+ #[test]
640
+ fn unicode_and_null_content_preserved_through_rebuild() {
641
+ let path = temp_db();
642
+ build_legacy(&path); // messages 漂移(owner_team_id 末列)
643
+ let c = Connection::open(&path).unwrap();
644
+ c.execute("insert into messages(message_id, content, status, owner_team_id) values ('u1', 'héllo🦀\n世界', NULL, 't')", []).unwrap();
645
+ drop(c);
646
+ let conn = crate::db::schema::open_db(&path).unwrap();
647
+ initialize_schema(&conn, Some(&path)).unwrap();
648
+ let (content, status): (String, Option<String>) = conn
649
+ .query_row("select content, status from messages where message_id='u1'", [], |r| Ok((r.get(0)?, r.get(1)?)))
650
+ .unwrap();
651
+ assert_eq!(content, "héllo🦀\n世界");
652
+ assert_eq!(status, None);
653
+ }
654
+
655
+ #[test]
656
+ fn fix_schema_layout_missing_blocked_fixed() {
657
+ // missing:无 db。
658
+ let ws_missing = temp_db().parent().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
659
+ match fix_schema_layout(&ws_missing, SCHEMA_VERSION).unwrap() {
660
+ FixResult::Missing(d) => assert_eq!(d.status, "missing"),
661
+ other => panic!("expected Missing, got {other:?}"),
662
+ }
663
+
664
+ // fixed:legacy db。workspace = .../<dir>(db 在 ws/.team/runtime/team.db)。
665
+ let db = temp_db();
666
+ build_legacy(&db);
667
+ let ws = db.parent().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
668
+ match fix_schema_layout(&ws, SCHEMA_VERSION).unwrap() {
669
+ FixResult::Fixed { diagnosis, rebuilds } => {
670
+ assert!(diagnosis.ok);
671
+ assert_eq!(diagnosis.user_version, 3);
672
+ assert_eq!(rebuilds.len(), 8);
673
+ }
674
+ other => panic!("expected Fixed, got {other:?}"),
675
+ }
676
+
677
+ // blocked:另一个连接持 BEGIN IMMEDIATE 写锁时 fix 应拒绝(不写备份)。
678
+ let db2 = temp_db();
679
+ build_legacy(&db2);
680
+ let ws2 = db2.parent().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
681
+ let holder = Connection::open(&db2).unwrap();
682
+ holder.busy_timeout(std::time::Duration::from_secs(5)).unwrap();
683
+ holder.execute_batch("BEGIN IMMEDIATE").unwrap(); // 占写锁
684
+ match fix_schema_layout(&ws2, SCHEMA_VERSION).unwrap() {
685
+ FixResult::Blocked { reason } => assert_eq!(reason, "active_lock"),
686
+ other => panic!("expected Blocked, got {other:?}"),
687
+ }
688
+ // 撞锁时不应写任何备份。
689
+ let baks = std::fs::read_dir(db2.parent().unwrap()).unwrap()
690
+ .filter_map(|e| e.ok())
691
+ .filter(|e| e.file_name().to_string_lossy().contains("pre-migration"))
692
+ .count();
693
+ assert_eq!(baks, 0, "blocked 路径不得写备份");
694
+ holder.execute_batch("ROLLBACK").unwrap();
695
+ }
696
+
697
+ // step 3↔4 集成:rebuild 后 events.jsonl 发射每表一条 schema.layout_rebuild(row_count 前后相等)。
698
+ #[test]
699
+ fn rebuild_emits_schema_layout_rebuild_events() {
700
+ let path = temp_db(); // <ws>/.team/runtime/team.db
701
+ build_legacy(&path);
702
+ let conn = crate::db::schema::open_db(&path).unwrap();
703
+ initialize_schema(&conn, Some(&path)).unwrap();
704
+ let ws = path.parent().unwrap().parent().unwrap().parent().unwrap();
705
+ let events = crate::event_log::EventLog::new(ws).tail(50).unwrap();
706
+ let rebuilds: Vec<_> = events.iter().filter(|e| e["event"] == serde_json::json!("schema.layout_rebuild")).collect();
707
+ assert_eq!(rebuilds.len(), 8, "8 张 managed 表各一条 schema.layout_rebuild");
708
+ for e in &rebuilds {
709
+ assert_eq!(e["row_count_before"], e["row_count_after"], "事件须 row_count 前后相等");
710
+ assert!(e["backup_path"].as_str().is_some());
711
+ }
712
+ // 含 messages 那条,to_layout_columns 是 canonical。
713
+ let m = rebuilds.iter().find(|e| e["table"] == serde_json::json!("messages")).unwrap();
714
+ assert_eq!(m["to_layout_columns"][1], serde_json::json!("owner_team_id"));
715
+ }
716
+ }