@team-agent/installer 0.2.11 โ†’ 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +1204 -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 +1207 -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 +557 -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 +1084 -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 +526 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1101 -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 +237 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +272 -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 +410 -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 +489 -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 +2109 -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 +985 -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 +710 -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 +187 -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 +468 -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 +743 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +329 -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 +553 -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 +578 -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 +659 -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 +765 -0
  172. package/crates/team-agent/src/tmux_backend.rs +810 -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 +118 -112
  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 -119
  203. package/src/team_agent/coordinator/lifecycle.py +0 -411
  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 -218
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -114
  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 -262
  255. package/src/team_agent/messaging/delivery.py +0 -504
  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 -503
  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 -91
  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 -1243
  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 -144
  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 -693
  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,829 @@
1
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
2
+ #![allow(non_snake_case)]
3
+ use super::*;
4
+ use std::path::{Path, PathBuf};
5
+
6
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
7
+ // ยง19 ๆ•ฃๅญ—็ฌฆไธฒ โ†’ enum:SkillTargetโ†’Provider ๅ…ณ่” (skeleton:112 codexโ†’Codex,
8
+ // claudeโ†’ClaudeCode;allโ†’None)ใ€‚Python _skill_dest_dir ๆฎ target ้€‰ dirใ€‚
9
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
10
+
11
+ #[test]
12
+ fn skill_target_codex_maps_to_provider_codex() {
13
+ // skeleton line 112: codexโ†’Codex.
14
+ assert_eq!(SkillTarget::Codex.provider(), Some(Provider::Codex));
15
+ }
16
+
17
+ #[test]
18
+ fn skill_target_claude_maps_to_provider_claude_code() {
19
+ // skeleton line 112: claudeโ†’ClaudeCode (NOT bare Claude โ€” ยง3 claude vs claude_code ไธ่ƒฝๆผๅฝ’ไธ€).
20
+ assert_eq!(SkillTarget::Claude.provider(), Some(Provider::ClaudeCode));
21
+ }
22
+
23
+ #[test]
24
+ fn skill_target_all_has_no_single_provider() {
25
+ // `All` fan-out ไธค่€… โ†’ ๆ— ๅ•ไธ€ provider.
26
+ assert_eq!(SkillTarget::All.provider(), None);
27
+ }
28
+
29
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
30
+ // _skill_dest_dir (commands.py:467-472):claudeโ†’~/.claude/skills/team-agent,
31
+ // ๅ…ถไฝ™(ๅซ codex)โ†’~/.codex/skills/team-agent;Allโ†’None(้žๅ• dir,fan-out)ใ€‚
32
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
33
+
34
+ #[test]
35
+ fn dest_dir_codex_resolves_to_dot_codex() {
36
+ // commands.py:471 โ€” codex (else branch) โ†’ ~/.codex/skills/team-agent.
37
+ let home = Path::new("/home/testuser");
38
+ let got = SkillTarget::Codex.dest_dir(home);
39
+ assert_eq!(
40
+ got,
41
+ Some(SkillDestDir(PathBuf::from(
42
+ "/home/testuser/.codex/skills/team-agent"
43
+ )))
44
+ );
45
+ }
46
+
47
+ #[test]
48
+ fn dest_dir_claude_resolves_to_dot_claude() {
49
+ // commands.py:469 โ€” claude โ†’ ~/.claude/skills/team-agent.
50
+ let home = Path::new("/home/testuser");
51
+ let got = SkillTarget::Claude.dest_dir(home);
52
+ assert_eq!(
53
+ got,
54
+ Some(SkillDestDir(PathBuf::from(
55
+ "/home/testuser/.claude/skills/team-agent"
56
+ )))
57
+ );
58
+ }
59
+
60
+ #[test]
61
+ fn dest_dir_all_is_none_not_single_dir() {
62
+ // `All` ๅบ” fan-out ๅˆฐไธค่€… โ†’ ้žๅ• dir โ†’ None (skeleton:116).
63
+ let home = Path::new("/home/testuser");
64
+ assert_eq!(SkillTarget::All.dest_dir(home), None);
65
+ }
66
+
67
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
68
+ // Version ๅ•ไธ€็œŸ็›ธๆบ:env!("CARGO_PKG_VERSION") โ€” ไฟฎๅŒๆบๆผ‚็งป
69
+ // (pyproject 0.1.4 vs package.json 0.2.11)ใ€‚current() == Cargo.toml ็‰ˆๆœฌ,
70
+ // ็ฆๆ‰‹ๆŠ„็ฌฌไบŒๅค„ใ€‚as_str() ๅณ้€ไผ ใ€‚
71
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
72
+
73
+ #[test]
74
+ fn version_current_equals_cargo_pkg_version() {
75
+ // ๅ•ไธ€็œŸ็›ธๆบ โ€” ็ฆๆ‰‹ๆŠ„ใ€‚current() ๅฟ…้กป == ็ผ–่ฏ‘ๆœŸ CARGO_PKG_VERSION.
76
+ assert_eq!(Version::current().as_str(), env!("CARGO_PKG_VERSION"));
77
+ }
78
+
79
+ #[test]
80
+ fn version_current_is_not_a_hand_copied_python_drift_literal() {
81
+ // STRENGTHENED (gate w59ds828k): the old test only asserted !is_empty() && != "dev",
82
+ // which a buggy impl returning a hardcoded "0.2.11" copied from package.json would PASS โ€”
83
+ // exactly the double-source-drift bug the subsystem forbids. Now CONCRETE & falsifiable.
84
+ //
85
+ // CONCRETE golden: workspace Cargo.toml version == "0.0.0" (Phase 0; team-agent-rs
86
+ // Cargo.toml:12). CARGO_PKG_VERSION therefore resolves to "0.0.0" โ€” which differs from
87
+ // BOTH Python drift sources (pyproject.toml 0.1.4 / package.json 0.2.11). So a porter who
88
+ // hand-copies either Python literal instead of using env!("CARGO_PKG_VERSION") FAILS here.
89
+ let v = Version::current();
90
+ assert_eq!(
91
+ v.as_str(),
92
+ env!("CARGO_PKG_VERSION"),
93
+ "single source of truth = CARGO_PKG_VERSION"
94
+ );
95
+ assert_ne!(v.as_str(), "0.1.4", "must not hand-copy pyproject.toml drift source");
96
+ assert_ne!(v.as_str(), "0.2.11", "must not hand-copy package.json drift source");
97
+ assert_ne!(v.as_str(), "dev", "must not be install.mjs:54 'dev' fallback");
98
+ }
99
+
100
+ #[test]
101
+ fn no_literal_version_string_hardcoded_in_packaging_code() {
102
+ // STRENGTHENED (gate w59ds828k): grep-assert the production CODE (comments stripped)
103
+ // contains no hand-copied semver literal โ€” only env!("CARGO_PKG_VERSION") may supply the
104
+ // version. Reads the file at test time via CARGO_MANIFEST_DIR. The Python drift literals
105
+ // 0.1.4 / 0.2.11 legitimately appear in doc/line comments documenting the bug, so we strip
106
+ // comment text first and scan only executable code. This is the one place where
107
+ // "double-source-drift forbidden" is statically checked against the source itself.
108
+ let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/packaging/types.rs"))
109
+ .expect("read own source");
110
+ // Production region only (the #[cfg(test)] mod uses these literals as golden anti-examples):
111
+ let prod = match src.find("#[cfg(test)]") {
112
+ Some(i) => &src[..i],
113
+ None => &src[..],
114
+ };
115
+ // Strip everything from the first `//` on each line (covers `//!` doc + `//` line comments).
116
+ let code: String = prod
117
+ .lines()
118
+ .map(|line| match line.find("//") {
119
+ Some(i) => &line[..i],
120
+ None => line,
121
+ })
122
+ .collect::<Vec<_>>()
123
+ .join("\n");
124
+ assert!(
125
+ !code.contains("0.1.4"),
126
+ "packaging.rs code must not hand-copy pyproject 0.1.4 (use env!(CARGO_PKG_VERSION))"
127
+ );
128
+ assert!(
129
+ !code.contains("0.2.11"),
130
+ "packaging.rs code must not hand-copy package.json 0.2.11 (use env!(CARGO_PKG_VERSION))"
131
+ );
132
+ // The ONLY version source allowed is env!("CARGO_PKG_VERSION") โ€” assert it is the source.
133
+ assert!(
134
+ code.contains("CARGO_PKG_VERSION"),
135
+ "Version::current() must derive from env!(\"CARGO_PKG_VERSION\")"
136
+ );
137
+ }
138
+
139
+ #[test]
140
+ fn version_serde_transparent_roundtrip() {
141
+ // #[serde(transparent)] โ€” ๅบๅˆ—ๅŒ–ไธบ่ฃธๅญ—็ฌฆไธฒ,้ž {"0":"..."}.
142
+ let v = Version("1.2.3".to_string());
143
+ let json = serde_json::to_string(&v).unwrap();
144
+ assert_eq!(json, "\"1.2.3\"");
145
+ let back: Version = serde_json::from_str(&json).unwrap();
146
+ assert_eq!(back, v);
147
+ }
148
+
149
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
150
+ // platform_support (ยง8 ๅฆ‚ๅฎžๅฃฐๆ˜Ž):macOS/Linux ๅŽŸ็”Ÿ;Windows ๅŽŸ็”Ÿไธ€็ญ‰
151
+ // (WezTerm/ConPTY,่ง transport-backend-design)ใ€‚ไธๅ‡่ฃ…ๅ…ผๅฎนใ€‚
152
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
153
+
154
+ #[test]
155
+ fn macos_aarch64_is_native() {
156
+ assert_eq!(
157
+ platform_support(ReleaseTarget::MacosAarch64),
158
+ PlatformSupport::Native
159
+ );
160
+ }
161
+
162
+ #[test]
163
+ fn macos_x8664_is_native() {
164
+ assert_eq!(
165
+ platform_support(ReleaseTarget::MacosX8664),
166
+ PlatformSupport::Native
167
+ );
168
+ }
169
+
170
+ #[test]
171
+ fn linux_x8664_is_native() {
172
+ assert_eq!(
173
+ platform_support(ReleaseTarget::LinuxX8664),
174
+ PlatformSupport::Native
175
+ );
176
+ }
177
+
178
+ #[test]
179
+ fn linux_aarch64_is_native() {
180
+ assert_eq!(
181
+ platform_support(ReleaseTarget::LinuxAarch64),
182
+ PlatformSupport::Native
183
+ );
184
+ }
185
+
186
+ #[test]
187
+ fn windows_x8664_is_native_per_transport_design() {
188
+ // skeleton:203/211 โ€” Windows ๅŽŸ็”Ÿไธ€็ญ‰ (WezTerm/ConPTY,้ž tmux)ใ€‚
189
+ // ไธๆ˜ฏ Unsupported,ไธๆ˜ฏ RequiresWslTmux โ€” ๆ˜ฏ Native.
190
+ assert_eq!(
191
+ platform_support(ReleaseTarget::WindowsX8664),
192
+ PlatformSupport::Native
193
+ );
194
+ }
195
+
196
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
197
+ // doctor โ€” typed DoctorStatus (commands.py:218-260)ใ€‚
198
+ // error ่ทฏๅพ„ + ็ฒพ็กฎๆถˆๆฏ:
199
+ // - --fix ๆ—  --gate โ†’ TeamAgentError("--fix requires --gate") (commands.py:221)
200
+ // - unknown doctor gate โ†’ "unknown doctor gate: <g>" (commands.py:235)
201
+ // - schema layout drift โ†’ HasBlockers{SchemaLayoutDrift} (commands.py:242-250)
202
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
203
+
204
+ fn doctor_opts(workspace: &Path) -> DoctorOptions {
205
+ DoctorOptions {
206
+ workspace: workspace.to_path_buf(),
207
+ gate: None,
208
+ fix: false,
209
+ cleanup_orphans: false,
210
+ confirm: false,
211
+ }
212
+ }
213
+
214
+ #[test]
215
+ fn doctor_fix_without_gate_is_invalid_options() {
216
+ // commands.py:220-221 โ€” `--fix requires --gate`.
217
+ let ws = PathBuf::from("/tmp/ws-doctor-fix");
218
+ let mut opts = doctor_opts(&ws);
219
+ opts.fix = true;
220
+ opts.gate = None;
221
+ let err = doctor(&opts).expect_err("fix without gate must error");
222
+ match err {
223
+ PackagingError::InvalidOptions(msg) => {
224
+ assert!(
225
+ msg.contains("--fix requires --gate"),
226
+ "expected '--fix requires --gate', got: {msg}"
227
+ );
228
+ }
229
+ other => panic!("expected InvalidOptions, got {other:?}"),
230
+ }
231
+ }
232
+
233
+ /// TEST-SUPPORT seed helper (real impl โ€” pure test scaffolding, uses rusqlite directly):
234
+ /// build a workspace whose `.team/runtime/team.db` has the LEGACY drifted layout
235
+ /// (owner_team_id appended as the last column on the 4 managed tables, user_version=1).
236
+ /// This is the exact fixture migration.rs::build_legacy uses; schema_diagnosis on it yields
237
+ /// non-empty layout_diffs โ†’ doctor() must surface SchemaLayoutDrift. Returns the workspace.
238
+ fn seed_workspace_with_drifted_db(tag: &str) -> PathBuf {
239
+ use rusqlite::Connection;
240
+ let ws = std::env::temp_dir().join(format!("ta-doctor-drift-{}-{}", std::process::id(), tag));
241
+ let db = ws.join(".team").join("runtime").join("team.db");
242
+ std::fs::create_dir_all(db.parent().unwrap()).expect("seed runtime dir");
243
+ let conn = Connection::open(&db).expect("open drifted db");
244
+ // Legacy layout: owner_team_id is the LAST column โ†’ physical column-order drift vs canonical.
245
+ conn.execute_batch(
246
+ "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);
247
+ 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);
248
+ 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);
249
+ 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);
250
+ pragma user_version = 1;",
251
+ )
252
+ .expect("seed legacy schema");
253
+ drop(conn);
254
+ ws
255
+ }
256
+
257
+ #[test]
258
+ fn doctor_on_clean_workspace_no_drift_is_ok() {
259
+ // ๆ—  schema layout drift (็ฉบ/ๆ—  db) โ†’ DoctorStatus::Ok.
260
+ // schema_diagnosis(missing db) โ†’ layout_diffs ็ฉบ โ†’ ้ž HasBlockers(SchemaLayoutDrift).
261
+ // NOTE (gate w59ds828k): cleanโ†’Ok cannot distinguish "gates ran & passed" from "gates not
262
+ // wired" because step 11/12 gate entities live elsewhere. The positive drift test below
263
+ // (doctor_drifted_db_emits_schema_layout_drift_blocker) is what proves doctor() actually
264
+ // READS schema_diagnosis and emits the typed SchemaLayoutDrift blocker on real drift.
265
+ let dir = std::env::temp_dir().join(format!("ta-doctor-clean-{}", std::process::id()));
266
+ let _ = std::fs::create_dir_all(&dir);
267
+ let opts = doctor_opts(&dir);
268
+ let status = doctor(&opts).expect("clean workspace doctor should succeed");
269
+ assert_eq!(status, DoctorStatus::Ok);
270
+ }
271
+
272
+ #[test]
273
+ fn doctor_drifted_db_emits_schema_layout_drift_blocker() {
274
+ // STRENGTHENED (gate w59ds828k): the ONLY drift assertion previously
275
+ // (doctor_status_has_blockers_carries_typed_source) was a pure serde test that hand-built
276
+ // the Blocker โ€” it never proved doctor() EMITS SchemaLayoutDrift from a real drifted db.
277
+ // This drives real doctor() on a SEEDED drifted team.db and pins the CONCRETE golden:
278
+ // commands.py:242-250 โ†’ schema layout drift โ†’ coordinator.schema_error ==
279
+ // "team.db physical layout drift detected" (EXACT string, commands.py:248).
280
+ let ws = seed_workspace_with_drifted_db("blocker");
281
+ let opts = doctor_opts(&ws);
282
+ let status = doctor(&opts).expect("doctor on drifted workspace should succeed (returns blockers)");
283
+ match status {
284
+ DoctorStatus::HasBlockers { blockers } => {
285
+ let drift = blockers
286
+ .iter()
287
+ .find(|b| b.source == BlockerSource::SchemaLayoutDrift)
288
+ .expect("must surface a SchemaLayoutDrift blocker");
289
+ // EXACT golden string from commands.py:248.
290
+ assert_eq!(
291
+ drift.detail, "team.db physical layout drift detected",
292
+ "schema_error golden must match commands.py:248 verbatim"
293
+ );
294
+ }
295
+ DoctorStatus::Ok => panic!("drifted team.db must NOT report Ok โ€” layout_diffs non-empty"),
296
+ }
297
+ }
298
+
299
+ #[test]
300
+ fn doctor_status_ok_serializes_with_status_tag() {
301
+ // #[serde(tag = "status")] โ€” Ok โ†’ {"status":"ok"}.
302
+ let json = serde_json::to_string(&DoctorStatus::Ok).unwrap();
303
+ assert_eq!(json, "{\"status\":\"ok\"}");
304
+ }
305
+
306
+ #[test]
307
+ fn doctor_status_has_blockers_carries_typed_source() {
308
+ // HasBlockers serde:tag status=has_blockers + blockers[].source snake_case.
309
+ let status = DoctorStatus::HasBlockers {
310
+ blockers: vec![Blocker {
311
+ source: BlockerSource::SchemaLayoutDrift,
312
+ detail: "team.db physical layout drift detected".to_string(),
313
+ }],
314
+ };
315
+ let json = serde_json::to_string(&status).unwrap();
316
+ assert!(json.contains("\"status\":\"has_blockers\""), "got: {json}");
317
+ assert!(json.contains("\"source\":\"schema_layout_drift\""), "got: {json}");
318
+ // detail ็ฒพ็กฎ == commands.py:248 schema_error ๆ–‡ๆœฌ.
319
+ assert!(
320
+ json.contains("team.db physical layout drift detected"),
321
+ "got: {json}"
322
+ );
323
+ }
324
+
325
+ #[test]
326
+ fn blocker_source_serde_exact_snake_case_strings() {
327
+ // ยง19 ็ฉทๅฐฝ enum ๅบๅˆ—ๅŒ– == Python ๆ•ฃๅญ—็ฌฆไธฒ็ญ‰ไปท (้€ variant ้’‰).
328
+ let cases: &[(BlockerSource, &str)] = &[
329
+ (BlockerSource::SchemaLayoutDrift, "\"schema_layout_drift\""),
330
+ (BlockerSource::OrphanCoordinator, "\"orphan_coordinator\""),
331
+ (BlockerSource::CommsGate, "\"comms_gate\""),
332
+ (BlockerSource::PathNotConfigured, "\"path_not_configured\""),
333
+ ];
334
+ for (src, want) in cases {
335
+ assert_eq!(&serde_json::to_string(src).unwrap(), want);
336
+ }
337
+ }
338
+
339
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
340
+ // repair_schema โ€” ่ฝฌ่ฐƒ step 3 fix_schema_layout(workspace, SCHEMA_VERSION=3)ใ€‚
341
+ // (commands.py:239-240 doctor --fix-schema)ใ€‚
342
+ // ๅ†ณ็ญ–:
343
+ // - db ไธๅญ˜ๅœจ โ†’ Missing โ†’ ๆ—  drift โ†’ UpToDate (่ฝฌ่ฐƒๅค–ๅฃณ)ใ€‚
344
+ // - ๆ’žๆดป่ทƒ้” โ†’ Blocked{reason:"active_lock"} ไธ”ไธๅ†™ๅค‡ไปฝใ€‚
345
+ // ็ ดๅๆ€ง rebuild ็œŸ่ทฏๅพ„ #[ignore](้œ€็œŸ db fixture / ็œŸๆœบ)ใ€‚
346
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
347
+
348
+ #[test]
349
+ fn repair_schema_missing_db_is_up_to_date() {
350
+ // db ไธๅญ˜ๅœจ (.team/runtime/team.db) โ†’ fix_schema_layout ่ฟ” Missing โ†’
351
+ // packaging ๅŒ…ๆˆ UpToDate (ๆ—  drift ้œ€่ฟ็งป)ใ€‚
352
+ let dir = std::env::temp_dir().join(format!("ta-repair-missing-{}", std::process::id()));
353
+ let _ = std::fs::create_dir_all(&dir);
354
+ let outcome = repair_schema(&dir).expect("missing db repair should succeed");
355
+ match outcome {
356
+ MigrationOutcome::UpToDate { diagnosis } => {
357
+ // schema_diagnosis(missing) โ†’ ok=true, layout_diffs ็ฉบ.
358
+ assert!(diagnosis.layout_diffs.is_empty(), "missing db has no drift");
359
+ }
360
+ other => panic!("expected UpToDate for missing db, got {other:?}"),
361
+ }
362
+ }
363
+
364
+ #[test]
365
+ #[ignore = "REAL-MACHINE-E2E: needs real drifted team.db fixture + destructive rebuild"]
366
+ fn repair_schema_drifted_db_migrates() {
367
+ // ็œŸ drift fixture โ†’ Migrated{fix: FixResult::Fixed{rebuilds non-empty}}.
368
+ let ws = PathBuf::from("/nonexistent-fixture");
369
+ let outcome = repair_schema(&ws).expect("drift repair");
370
+ assert!(matches!(outcome, MigrationOutcome::Migrated { .. }));
371
+ }
372
+
373
+ #[test]
374
+ #[ignore = "REAL-MACHINE-E2E: needs a held active lock on team.db (concurrent BEGIN IMMEDIATE)"]
375
+ fn repair_schema_active_lock_is_blocked_no_backup() {
376
+ // ๆ’žๆดป่ทƒ้” โ†’ Blocked{reason:"active_lock"};ไธ”ไธๅ†™ๅค‡ไปฝ (db/migration.rs:db_lock_status).
377
+ let ws = PathBuf::from("/nonexistent-locked-fixture");
378
+ let outcome = repair_schema(&ws).expect("blocked repair returns Ok wrapper");
379
+ match outcome {
380
+ MigrationOutcome::Blocked { reason } => assert_eq!(reason, "active_lock"),
381
+ other => panic!("expected Blocked, got {other:?}"),
382
+ }
383
+ }
384
+
385
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
386
+ // diagnose_path โ€” bincheck.mjs printMissingBinDiagnostic ็ญ‰ไปท (typed PathDiagnostic)ใ€‚
387
+ // bin ๅœจ PATH โ†’ OnPath;ไธๅœจ โ†’ NotOnPath{diagnostic}ใ€‚Rust ๆ—  npm โ†’ npmrc_prefix=Noneใ€‚
388
+ // ็บฏ้€ป่พ‘ๅฏๅ•ๆต‹ (็œŸๆŽข PATH ้ƒจๅˆ† #[ignore])ใ€‚
389
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
390
+
391
+ #[test]
392
+ fn diagnose_path_when_bin_on_path_reports_on_path() {
393
+ // bin_dir ๅœจๅฝ“ๅ‰ PATH โ†’ OnPath{bin_dir}ใ€‚ๆž„้€ :ๆŠŠ bin_dir ไธดๆ—ถๅกž่ฟ› PATHใ€‚
394
+ // ๅ– PATH ้ฆ–ไธช็œŸๅฎžๆก็›ฎไฝœไธบ bin_dir,ไฟ่ฏใ€Œๅœจ PATH ไธŠใ€ใ€‚
395
+ let path_var = std::env::var("PATH").unwrap_or_default();
396
+ let first = path_var
397
+ .split(':')
398
+ .find(|p| !p.is_empty())
399
+ .expect("PATH has at least one entry");
400
+ let bin = BinDir(PathBuf::from(first));
401
+ let hint = diagnose_path(&bin).expect("diagnose on-path bin");
402
+ match hint {
403
+ PathHint::OnPath { bin_dir } => assert_eq!(bin_dir, PathBuf::from(first)),
404
+ PathHint::NotOnPath { .. } => panic!("PATH entry should report OnPath"),
405
+ }
406
+ }
407
+
408
+ #[test]
409
+ fn diagnose_path_not_on_path_npmrc_prefix_is_none_no_npm() {
410
+ // Rust ็‰ˆๆ—  npm ่ทฏๅพ„ โ†’ npmrc_prefix == None (skeleton:257).
411
+ // ็”จไธ€ไธช็ปไธๅœจ PATH ็š„็›ฎๅฝ•ใ€‚
412
+ let bin = BinDir(PathBuf::from("/zzz-definitely-not-on-path-9f3a"));
413
+ let hint = diagnose_path(&bin).expect("diagnose off-path bin");
414
+ match hint {
415
+ PathHint::NotOnPath { bin_dir, diagnostic } => {
416
+ assert_eq!(bin_dir, PathBuf::from("/zzz-definitely-not-on-path-9f3a"));
417
+ // Rust ๆ—  npm โ†’ ็ปไธ้‡ๆ–ฐๅผ•ๅ…ฅ .npmrc ่งฃๆž.
418
+ assert_eq!(diagnostic.npmrc_prefix, None);
419
+ // path_entries == ๅฝ“ๅ‰ PATH ๆก็›ฎๆ•ฐ (bincheck.mjs:43).
420
+ let want = std::env::var("PATH")
421
+ .unwrap_or_default()
422
+ .split(':')
423
+ .filter(|p| !p.is_empty())
424
+ .count();
425
+ assert_eq!(diagnostic.path_entries, want);
426
+ }
427
+ PathHint::OnPath { .. } => panic!("bogus dir must be NotOnPath"),
428
+ }
429
+ }
430
+
431
+ #[test]
432
+ fn path_hint_serde_tag_kind() {
433
+ // #[serde(tag = "kind")] โ€” OnPath โ†’ {"kind":"on_path",...}.
434
+ let h = PathHint::OnPath {
435
+ bin_dir: PathBuf::from("/home/u/.local/bin"),
436
+ };
437
+ let json = serde_json::to_string(&h).unwrap();
438
+ assert!(json.contains("\"kind\":\"on_path\""), "got: {json}");
439
+ }
440
+
441
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
442
+ // install_skill โ€” cmd_install_skill (commands.py:451-481)ใ€‚
443
+ // error ่ทฏๅพ„ + ็ฒพ็กฎๆถˆๆฏ:
444
+ // - --dest + --target all โ†’ InvalidOptions("--dest cannot be combined with --target all")
445
+ // (commands.py:453-454)
446
+ // dry-run ๅ†ณ็ญ– (ๆ— ๅ‰ฏไฝœ็”จ,ๅฏๅ•ๆต‹):
447
+ // - dry_run=true โ†’ SkillInstallOutcome{dry_run:true, removed_stale:[]} ไธ่ฝๅœฐ.
448
+ // ็œŸๆ‹ท / removed_stale #[ignore] (ๆ–‡ไปถ็ณป็ปŸๅ‰ฏไฝœ็”จ)ใ€‚
449
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
450
+
451
+ fn skill_opts(target: SkillTarget, dest: Option<PathBuf>, dry_run: bool) -> SkillInstallOptions {
452
+ SkillInstallOptions {
453
+ target,
454
+ dest,
455
+ dry_run,
456
+ source: PathBuf::from("/repo/skills/team-agent"),
457
+ }
458
+ }
459
+
460
+ #[test]
461
+ fn install_skill_dest_with_target_all_is_invalid() {
462
+ // commands.py:453-454 โ€” `--dest cannot be combined with --target all`.
463
+ let opts = skill_opts(
464
+ SkillTarget::All,
465
+ Some(PathBuf::from("/custom/dest")),
466
+ false,
467
+ );
468
+ let err = install_skill(&opts).expect_err("dest + all must error");
469
+ match err {
470
+ PackagingError::InvalidOptions(msg) => assert!(
471
+ msg.contains("--dest cannot be combined with --target all"),
472
+ "got: {msg}"
473
+ ),
474
+ other => panic!("expected InvalidOptions, got {other:?}"),
475
+ }
476
+ }
477
+
478
+ #[test]
479
+ fn install_skill_dry_run_single_target_reports_plan_no_side_effects() {
480
+ // commands.py:477-478 dry_run โ†’ {ok, source, dest, dry_run:true} ไธ่ฝๅœฐ.
481
+ let opts = skill_opts(SkillTarget::Codex, None, true);
482
+ let outcomes = install_skill(&opts).expect("dry-run install-skill");
483
+ assert_eq!(outcomes.len(), 1, "single target โ†’ 1 outcome");
484
+ let o = &outcomes[0];
485
+ assert_eq!(o.target, SkillTarget::Codex);
486
+ assert!(o.dry_run, "dry_run flag preserved");
487
+ // dry-run ็ปไธๆธ…็†ๆฎ‹็•™ (ๆ— ๅ‰ฏไฝœ็”จ).
488
+ assert!(o.removed_stale.is_empty(), "dry-run touches nothing");
489
+ // dest == codex skill dir (HOME ไพ่ต–,็”จ dirs ่งฃๆž;ๅชๆ–ญๅŽ็ผ€็จณๅฎš้ƒจๅˆ†).
490
+ assert!(
491
+ o.dest
492
+ .0
493
+ .ends_with(PathBuf::from(".codex/skills/team-agent")),
494
+ "got: {:?}",
495
+ o.dest
496
+ );
497
+ }
498
+
499
+ #[test]
500
+ fn install_skill_dry_run_target_all_fans_out_to_two() {
501
+ // commands.py:458-463 โ€” target all โ†’ ไธคไธช outcome (codex + claude),้กบๅบๅ›บๅฎš.
502
+ let opts = skill_opts(SkillTarget::All, None, true);
503
+ let outcomes = install_skill(&opts).expect("dry-run install-skill all");
504
+ assert_eq!(outcomes.len(), 2, "all โ†’ fan-out codex+claude");
505
+ // KEY ORDER:commands.py:460-461 codex first, claude second.
506
+ assert_eq!(outcomes[0].target, SkillTarget::Codex);
507
+ assert_eq!(outcomes[1].target, SkillTarget::Claude);
508
+ assert!(outcomes.iter().all(|o| o.dry_run));
509
+ }
510
+
511
+ #[test]
512
+ fn install_skill_dry_run_explicit_dest_single_target() {
513
+ // commands.py:455-457 โ€” --dest ๆ˜พๅผ็›ฎๅฝ•,่ฆ†็›– target ่ทฏๅพ„่งฃๆž,ๅ• outcome.
514
+ let dest = PathBuf::from("/custom/skills/team-agent");
515
+ let opts = skill_opts(SkillTarget::Codex, Some(dest.clone()), true);
516
+ let outcomes = install_skill(&opts).expect("dry-run explicit dest");
517
+ assert_eq!(outcomes.len(), 1);
518
+ assert_eq!(outcomes[0].dest, SkillDestDir(dest));
519
+ assert!(outcomes[0].dry_run);
520
+ }
521
+
522
+ #[test]
523
+ #[ignore = "REAL-MACHINE-E2E: real copytree + stale diff removal (fixes dirs_exist_ok=True residue)"]
524
+ fn install_skill_real_copy_removes_stale_files() {
525
+ // ไฟฎ commands.py:480 dirs_exist_ok ๆฎ‹็•™:Rust ๆ‹ทๅ‰ๆธ…ๆ—ง SKILL,่ฎฐๅฝ• removed_stale.
526
+ let opts = skill_opts(SkillTarget::Codex, Some(PathBuf::from("/tmp/ta-skill-real")), false);
527
+ let outcomes = install_skill(&opts).expect("real install-skill");
528
+ assert!(!outcomes[0].dry_run);
529
+ // ็œŸ่ทฏๅพ„ไธ‹่‹ฅๆœ‰ๆ—งๆฎ‹็•™,removed_stale ้ž็ฉบ (ๅ…ทไฝ“ๅ€ผไพ fixture).
530
+ }
531
+
532
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
533
+ // uninstall โ€” install.mjs:109-130ใ€‚ๅฎ‰ๅ…จๆŠคๆ :
534
+ // - ้ป˜่ฎค purge_runtime=false โ†’ purged_runtime=false,ไธๅˆ  workspace/.team.
535
+ // - purge_runtime=true ไธ”ๆฃ€ๆต‹ๆœ‰ team ๅœจ่ท‘ โ†’ ๆ‹’็ป:purge_refused_team_running=true
536
+ // (ๆˆ–่ฟ” PurgeRefusedTeamRunning err โ€” ๅ–ๅฎž็Žฐ็บชๅพ‹,่ฟ™้‡Œ้’‰ใ€Œไธ็œŸๅˆ ใ€)ใ€‚
537
+ // ็œŸๅˆ  / team-running ๅˆคๅฎš #[ignore]ใ€‚
538
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
539
+
540
+ /// TEST-SUPPORT seed helper: build a workspace whose state.json projects a RUNNING team
541
+ /// (status "running" + a live coordinator pid). The team-running guard reads this projection.
542
+ /// Real impl (pure scaffolding, not a production fn) so the NEW-CONTRACT guard test has a
543
+ /// concrete fixture instead of a bare nonexistent path.
544
+ fn seed_workspace_with_running_team(tag: &str) -> PathBuf {
545
+ let ws = std::env::temp_dir().join(format!("ta-ws-running-{}-{}", std::process::id(), tag));
546
+ let team_dir = ws.join(".team");
547
+ std::fs::create_dir_all(&team_dir).expect("seed .team dir");
548
+ // state.json shape mirrors step-5 state projection: a team marked running.
549
+ let state = serde_json::json!({
550
+ "teams": {
551
+ "demo": { "status": "running", "coordinator_pid": std::process::id() }
552
+ }
553
+ });
554
+ std::fs::write(
555
+ team_dir.join("state.json"),
556
+ serde_json::to_vec_pretty(&state).unwrap(),
557
+ )
558
+ .expect("seed state.json");
559
+ ws
560
+ }
561
+
562
+ /// TEST-SUPPORT seed helper: an idle workspace (state.json present, no running team).
563
+ fn seed_workspace_idle(tag: &str) -> PathBuf {
564
+ let ws = std::env::temp_dir().join(format!("ta-ws-idle-{}-{}", std::process::id(), tag));
565
+ let team_dir = ws.join(".team");
566
+ std::fs::create_dir_all(&team_dir).expect("seed .team dir");
567
+ std::fs::write(
568
+ team_dir.join("state.json"),
569
+ serde_json::to_vec_pretty(&serde_json::json!({ "teams": {} })).unwrap(),
570
+ )
571
+ .expect("seed state.json");
572
+ ws
573
+ }
574
+
575
+ #[test]
576
+ #[serial_test::serial(env)]
577
+ fn uninstall_default_does_not_purge_runtime() {
578
+ // PORT-GOLDEN (install.mjs:127-129): default (no --purge-runtime) leaves runtime; the
579
+ // "runtime directories are left ... for rollback" branch. Default must NOT purge
580
+ // workspace/.team. With workspace seeded, default must STILL leave it untouched on disk.
581
+ // Isolate HOME: uninstall() now removes ~/.codex|.claude skill dirs (reads HOME). Without an
582
+ // isolated empty HOME this test would (a) delete the real user's skill dir and (b) race
583
+ // p2_uninstall_removes_both_skill_dirs' HOME mutation โ†’ remove_dir_all NotFound flake under
584
+ // parallel cargo. Shared ENV_LOCK_PKG serializes the two HOME-touching uninstall tests.
585
+ let _g = ENV_LOCK_PKG.lock().unwrap_or_else(|p| p.into_inner());
586
+ let home = std::env::temp_dir().join(format!("ta-uninst-default-home-{}", std::process::id()));
587
+ std::fs::create_dir_all(&home).unwrap();
588
+ let _h = HomeGuard::set(&home);
589
+ let ws = seed_workspace_idle("default-nopurge");
590
+ let opts = UninstallOptions {
591
+ prefix: Prefix(std::env::temp_dir().join(format!("ta-uninst-{}", std::process::id()))),
592
+ purge_runtime: false,
593
+ workspace: Some(ws.clone()),
594
+ };
595
+ let outcome = uninstall(&opts).expect("default uninstall");
596
+ assert!(!outcome.purged_runtime, "default must NOT purge runtime");
597
+ assert!(
598
+ !outcome.purge_refused_team_running,
599
+ "no purge requested โ†’ no refusal"
600
+ );
601
+ // SAFETY INVARIANT (card ยงuninstall ็ปไธ้ป˜่ฎคๅˆ  workspace/.team): the seeded workspace
602
+ // .team must still exist after a default uninstall.
603
+ assert!(
604
+ ws.join(".team").join("state.json").exists(),
605
+ "default uninstall must NEVER delete workspace/.team"
606
+ );
607
+ }
608
+
609
+ #[test]
610
+ #[ignore = "NEW-CONTRACT (Rust hardening, NOT a Python port-golden): real team-running guard \
611
+ needs live state projection (step 5). Python install.mjs:123-128 has NO such guard \
612
+ โ€” it purges unconditionally on --purge-runtime. Confirmed with gate w59ds828k: this \
613
+ is intentional hardening backed by card ยงuninstall prose 'pass --purge-runtime only \
614
+ when no teams are running.' Marked NEW-CONTRACT, not PORT."]
615
+ fn uninstall_purge_refused_when_team_running_NEW_CONTRACT() {
616
+ // NEW-CONTRACT safety guard: purge_runtime=true but workspace projects a RUNNING team โ†’
617
+ // REFUSE purge (purge_refused_team_running=true OR PurgeRefusedTeamRunning err). The
618
+ // seeded workspace .team MUST survive regardless. This behavior is NOT in install.mjs.
619
+ let ws = seed_workspace_with_running_team("guard");
620
+ let opts = UninstallOptions {
621
+ prefix: Prefix(std::env::temp_dir().join("ta-uninst-guard")),
622
+ purge_runtime: true,
623
+ workspace: Some(ws.clone()),
624
+ };
625
+ match uninstall(&opts) {
626
+ Ok(o) => {
627
+ assert!(!o.purged_runtime, "must not purge while team running");
628
+ assert!(o.purge_refused_team_running, "must set refusal flag");
629
+ }
630
+ Err(PackagingError::PurgeRefusedTeamRunning(refused_ws)) => {
631
+ assert_eq!(refused_ws, ws, "refusal must name the running workspace");
632
+ }
633
+ Err(other) => panic!("expected refusal, got {other:?}"),
634
+ }
635
+ // Hard invariant: refused purge must leave the workspace fully intact.
636
+ assert!(
637
+ ws.join(".team").join("state.json").exists(),
638
+ "refused purge must NEVER delete workspace/.team"
639
+ );
640
+ }
641
+
642
+ #[test]
643
+ #[ignore = "REAL-MACHINE-E2E: PORT-GOLDEN โ€” --purge-runtime with NO running team really removes \
644
+ the runtime root (install.mjs:123-126 unconditional rmSync). File-system side effect."]
645
+ fn uninstall_purge_runtime_idle_workspace_purges_PORT_GOLDEN() {
646
+ // PORT-GOLDEN (install.mjs:123-126): --purge-runtime DOES purge when no team is running.
647
+ // This is the faithful Python behavior (the guard above is the only Rust addition).
648
+ let ws = seed_workspace_idle("port-purge");
649
+ let opts = UninstallOptions {
650
+ prefix: Prefix(std::env::temp_dir().join("ta-uninst-port-purge")),
651
+ purge_runtime: true,
652
+ workspace: Some(ws.clone()),
653
+ };
654
+ let outcome = uninstall(&opts).expect("purge on idle workspace");
655
+ assert!(outcome.purged_runtime, "idle + --purge-runtime โ†’ purged");
656
+ assert!(!outcome.purge_refused_team_running, "no team โ†’ no refusal");
657
+ }
658
+
659
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
660
+ // install / update โ€” install.mjs:48-95ใ€‚
661
+ // - install ้ฆ–่ฃ…:InstallReport.replace == None (ๆ— ไบŒ่ฟ›ๅˆถๆ›ฟๆข).
662
+ // - update:replace == Some(..) (ๆœ‰ๅŽŸๅญๆ›ฟๆข;ๅคฑ่ดฅๅ›žๆปšๅˆฐ .previous,bug-084 ๅŒๆบ).
663
+ // - installer ้ป˜่ฎค skill_target = All (install.mjs:74 `--target all`).
664
+ // ๅ…จๅ‰ฏไฝœ็”จ #[ignore] (ๅ†™ bin / ๆ‹ท skill / ๆŽข PATH / ่ท‘ doctor) โ€” clean-install E2E.
665
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
666
+
667
+ fn install_opts(skill_target: SkillTarget) -> InstallOptions {
668
+ InstallOptions {
669
+ prefix: Prefix(PathBuf::from("/home/u/.local")),
670
+ self_binary: PathBuf::from("/proc/self/exe"),
671
+ skill_target,
672
+ }
673
+ }
674
+
675
+ #[test]
676
+ #[ignore = "REAL-MACHINE-E2E: clean-install writes bin + copies skill + runs doctor + probes PATH"]
677
+ fn install_first_time_has_no_binary_replace() {
678
+ // install.mjs:48 install ๅ…ฅๅฃ โ€” ้ฆ–่ฃ…ๆ—  replace.
679
+ let opts = install_opts(SkillTarget::All);
680
+ let report = install(&opts).expect("clean install");
681
+ assert!(report.replace.is_none(), "first install must NOT replace");
682
+ // installer ้ป˜่ฎค่ฃ…ไธคไธช skill (--target all).
683
+ assert_eq!(report.skills.len(), 2);
684
+ // ็‰ˆๆœฌ == ๅ•ไธ€็œŸ็›ธๆบ.
685
+ assert_eq!(report.version, Version::current());
686
+ }
687
+
688
+ #[test]
689
+ #[ignore = "REAL-MACHINE-E2E: atomic binary replace + .previous backup + rollback (bug-084 ๅŒๆบ)"]
690
+ fn update_performs_atomic_binary_replace() {
691
+ // install.mjs:60-66 โ€” update ๆœ‰ destโ†’backup + tmpโ†’dest ๅŽŸๅญๆ›ฟๆข.
692
+ let opts = install_opts(SkillTarget::All);
693
+ let report = update(&opts).expect("update");
694
+ match report.replace {
695
+ Some(AtomicReplaceOutcome::Replaced { .. })
696
+ | Some(AtomicReplaceOutcome::ReplacedCrossDevice { .. }) => {}
697
+ other => panic!("update must replace binary, got {other:?}"),
698
+ }
699
+ }
700
+
701
+ #[test]
702
+ fn atomic_replace_outcome_serde_tag_outcome() {
703
+ // #[serde(tag = "outcome")] โ€” RolledBack carries restored_from + error.
704
+ let o = AtomicReplaceOutcome::RolledBack {
705
+ restored_from: PathBuf::from("/home/u/.local/bin/.previous"),
706
+ error: "EXDEV".to_string(),
707
+ };
708
+ let json = serde_json::to_string(&o).unwrap();
709
+ assert!(json.contains("\"outcome\":\"rolled_back\""), "got: {json}");
710
+ assert!(json.contains("\"error\":\"EXDEV\""), "got: {json}");
711
+ }
712
+
713
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
714
+ // ยง84 โ€” packaging ็ปไธ่งฆๅ‘ provider client / prompt / token.
715
+ // install_skill ๅชๆ‹ทๆ–‡ไปถ (provider ่ฐƒ็”จ่ฎกๆ•ฐ = 0)ใ€‚ๆญคๅค„ไปฅใ€Œๆ—  provider ไพ่ต–ใ€
716
+ // ็š„็ป“ๆž„ๆ€งๆ–ญ่จ€ไปฃๆ›ฟ่ฟ่กŒๆ—ถ่ฎกๆ•ฐ:dry-run install-skill ไธๅบ”้œ€่ฆไปปไฝ• provider optsใ€‚
717
+ // (็œŸ mock-provider-call-count==0 ๆ–ญ่จ€ๅฝ’้›†ๆˆๅฑ‚,่ฟ™้‡Œ้’‰ dry-run ไธ่ฏป provider.)
718
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
719
+
720
+ #[test]
721
+ fn install_skill_dry_run_is_pure_no_provider_state() {
722
+ // ยง84:install-skill ๅชๆ‹ทๆ–‡ไปถ;dry-run ่ฟžๆ–‡ไปถ้ƒฝไธๅŠจ โ†’ ็บฏๅ‡ฝๆ•ฐๅผๅฏ้‡ๅค.
723
+ let opts = skill_opts(SkillTarget::Claude, None, true);
724
+ let first = install_skill(&opts).expect("dry-run 1");
725
+ let second = install_skill(&opts).expect("dry-run 2");
726
+ assert_eq!(first, second, "dry-run install-skill must be deterministic & side-effect free");
727
+ }
728
+
729
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
730
+ // CRLF / platform โ€” wrapper ๅ†…ๅฎนๅœจ Python ๆ˜ฏ sh wrapper (LF)ใ€‚Rust ๅ•ไบŒ่ฟ›ๅˆถ
731
+ // ๅŽๆ—  sh wrapper;ไฝ† PATH ่ฏŠๆ–ญ็š„ path_entries ๅˆ†้š”ๅœจ Windows ็”จ ';'ใ€‚
732
+ // ๆญคๅค„้’‰ diagnose_path ๅœจ็ฉบ PATH ๆ—ถ path_entries==0 (bincheck.mjs:43 ไธ‰ๅ…ƒ)ใ€‚
733
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
734
+
735
+ #[test]
736
+ #[ignore = "REAL-MACHINE-E2E: needs to override process PATH env to empty/Windows-delimited"]
737
+ fn diagnose_path_empty_path_has_zero_entries() {
738
+ // bincheck.mjs:43 โ€” searchPath ? split.length : 0;็ฉบ PATH โ†’ 0 entries.
739
+ // (็œŸๆ”น process env PATH ๅฝฑๅ“ๅนถ่กŒๆต‹่ฏ•,ๆ•… ignore;ๅฎž็Žฐๅฑ‚ๅบ”ๆ”ฏๆŒๆณจๅ…ฅ PATH.)
740
+ let bin = BinDir(PathBuf::from("/anything"));
741
+ let hint = diagnose_path(&bin).expect("diagnose empty path");
742
+ if let PathHint::NotOnPath { diagnostic, .. } = hint {
743
+ assert_eq!(diagnostic.path_entries, 0);
744
+ } else {
745
+ panic!("empty PATH โ†’ NotOnPath");
746
+ }
747
+ }
748
+
749
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• P2 FIX-LOOP RED (ๅค็ปฟๅณๅฏนๆŠ— cross-model findings) โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
750
+
751
+ static ENV_LOCK_PKG: std::sync::Mutex<()> = std::sync::Mutex::new(());
752
+ struct HomeGuard {
753
+ prev: Option<String>,
754
+ }
755
+ impl HomeGuard {
756
+ fn set(home: &Path) -> Self {
757
+ let prev = std::env::var("HOME").ok();
758
+ std::env::set_var("HOME", home);
759
+ Self { prev }
760
+ }
761
+ }
762
+ impl Drop for HomeGuard {
763
+ fn drop(&mut self) {
764
+ match &self.prev {
765
+ Some(v) => std::env::set_var("HOME", v),
766
+ None => std::env::remove_var("HOME"),
767
+ }
768
+ }
769
+ }
770
+
771
+ // P1 โ€” update() must perform a REAL atomic replace (rename destโ†’.previous), not fabricate
772
+ // a Replaced outcome whose backup file never exists (install.mjs:60-66; bug-084).
773
+ #[test]
774
+ fn p2_update_creates_real_atomic_replace_backup() {
775
+ let base = std::env::temp_dir().join(format!("ta-p2-update-{}", std::process::id()));
776
+ let prefix = base.join("prefix");
777
+ std::fs::create_dir_all(prefix.join("bin")).unwrap();
778
+ let dest = prefix.join("bin").join("team-agent");
779
+ std::fs::write(&dest, b"OLD BINARY").unwrap(); // pre-existing bin to back up
780
+ let self_bin = base.join("team-agent-new");
781
+ std::fs::write(&self_bin, b"NEW BINARY").unwrap();
782
+
783
+ let opts = InstallOptions {
784
+ prefix: Prefix(prefix.clone()),
785
+ self_binary: self_bin,
786
+ skill_target: SkillTarget::All,
787
+ };
788
+ let report = update(&opts).unwrap();
789
+ let backup = match report.replace {
790
+ Some(AtomicReplaceOutcome::Replaced { backup }) => backup,
791
+ other => panic!("update must report a Replaced atomic replace, got {other:?}"),
792
+ };
793
+ assert!(
794
+ backup.exists(),
795
+ "update() must actually rename destโ†’.previous; the claimed backup file must exist on disk"
796
+ );
797
+ }
798
+
799
+ // P1 โ€” uninstall() must remove BOTH ~/.codex/skills/team-agent and ~/.claude/skills/team-agent
800
+ // and record them (install.mjs:115-122). Current returns removed_skill_dirs empty and leaves
801
+ // the dirs on disk.
802
+ #[test]
803
+ #[serial_test::serial(env)]
804
+ fn p2_uninstall_removes_both_skill_dirs() {
805
+ let _g = ENV_LOCK_PKG.lock().unwrap_or_else(|p| p.into_inner());
806
+ let base = std::env::temp_dir().join(format!("ta-p2-uninst-{}", std::process::id()));
807
+ let home = base.join("home");
808
+ let codex = home.join(".codex").join("skills").join("team-agent");
809
+ let claude = home.join(".claude").join("skills").join("team-agent");
810
+ std::fs::create_dir_all(&codex).unwrap();
811
+ std::fs::create_dir_all(&claude).unwrap();
812
+ std::fs::write(codex.join("SKILL.md"), b"x").unwrap();
813
+ std::fs::write(claude.join("SKILL.md"), b"x").unwrap();
814
+ let _h = HomeGuard::set(&home);
815
+
816
+ let opts = UninstallOptions {
817
+ prefix: Prefix(base.join("prefix")),
818
+ purge_runtime: false,
819
+ workspace: None,
820
+ };
821
+ let out = uninstall(&opts).unwrap();
822
+ assert_eq!(
823
+ out.removed_skill_dirs.len(),
824
+ 2,
825
+ "uninstall must remove BOTH ~/.codex and ~/.claude skill dirs"
826
+ );
827
+ assert!(!codex.exists(), "~/.codex skill dir must be removed");
828
+ assert!(!claude.exists(), "~/.claude skill dir must be removed");
829
+ }