agent-tower 0.4.15 → 0.4.16-beta.3

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 (279) hide show
  1. package/dist/app.test.js +2 -0
  2. package/dist/app.test.js.map +1 -1
  3. package/dist/core/event-bus.d.ts +2 -0
  4. package/dist/core/event-bus.d.ts.map +1 -1
  5. package/dist/core/event-bus.js.map +1 -1
  6. package/dist/executors/__tests__/codex.executor.test.d.ts +2 -0
  7. package/dist/executors/__tests__/codex.executor.test.d.ts.map +1 -0
  8. package/dist/executors/__tests__/codex.executor.test.js +28 -0
  9. package/dist/executors/__tests__/codex.executor.test.js.map +1 -0
  10. package/dist/executors/codex.executor.d.ts +1 -0
  11. package/dist/executors/codex.executor.d.ts.map +1 -1
  12. package/dist/executors/codex.executor.js +19 -1
  13. package/dist/executors/codex.executor.js.map +1 -1
  14. package/dist/git/git-cli.d.ts +18 -1
  15. package/dist/git/git-cli.d.ts.map +1 -1
  16. package/dist/git/git-cli.js +17 -1
  17. package/dist/git/git-cli.js.map +1 -1
  18. package/dist/git/worktree.manager.d.ts +29 -2
  19. package/dist/git/worktree.manager.d.ts.map +1 -1
  20. package/dist/git/worktree.manager.js +137 -16
  21. package/dist/git/worktree.manager.js.map +1 -1
  22. package/dist/git/worktree.manager.test.d.ts +2 -0
  23. package/dist/git/worktree.manager.test.d.ts.map +1 -0
  24. package/dist/git/worktree.manager.test.js +104 -0
  25. package/dist/git/worktree.manager.test.js.map +1 -0
  26. package/dist/mcp/context.d.ts +3 -0
  27. package/dist/mcp/context.d.ts.map +1 -1
  28. package/dist/mcp/context.js +10 -1
  29. package/dist/mcp/context.js.map +1 -1
  30. package/dist/mcp/http-client.d.ts +24 -1
  31. package/dist/mcp/http-client.d.ts.map +1 -1
  32. package/dist/mcp/http-client.js +37 -3
  33. package/dist/mcp/http-client.js.map +1 -1
  34. package/dist/mcp/server.d.ts.map +1 -1
  35. package/dist/mcp/server.js +190 -0
  36. package/dist/mcp/server.js.map +1 -1
  37. package/dist/middleware/tunnel-auth.d.ts.map +1 -1
  38. package/dist/middleware/tunnel-auth.js +2 -0
  39. package/dist/middleware/tunnel-auth.js.map +1 -1
  40. package/dist/output/__tests__/codex-parser.test.d.ts +2 -0
  41. package/dist/output/__tests__/codex-parser.test.d.ts.map +1 -0
  42. package/dist/output/__tests__/codex-parser.test.js +148 -0
  43. package/dist/output/__tests__/codex-parser.test.js.map +1 -0
  44. package/dist/output/codex-parser.d.ts +12 -0
  45. package/dist/output/codex-parser.d.ts.map +1 -1
  46. package/dist/output/codex-parser.js +129 -12
  47. package/dist/output/codex-parser.js.map +1 -1
  48. package/dist/routes/__tests__/attachments.test.d.ts +2 -0
  49. package/dist/routes/__tests__/attachments.test.d.ts.map +1 -0
  50. package/dist/routes/__tests__/attachments.test.js +86 -0
  51. package/dist/routes/__tests__/attachments.test.js.map +1 -0
  52. package/dist/routes/__tests__/filesystem.test.d.ts +2 -0
  53. package/dist/routes/__tests__/filesystem.test.d.ts.map +1 -0
  54. package/dist/routes/__tests__/filesystem.test.js +80 -0
  55. package/dist/routes/__tests__/filesystem.test.js.map +1 -0
  56. package/dist/routes/__tests__/previews.test.d.ts +2 -0
  57. package/dist/routes/__tests__/previews.test.d.ts.map +1 -0
  58. package/dist/routes/__tests__/previews.test.js +89 -0
  59. package/dist/routes/__tests__/previews.test.js.map +1 -0
  60. package/dist/routes/__tests__/tasks.test.d.ts +2 -0
  61. package/dist/routes/__tests__/tasks.test.d.ts.map +1 -0
  62. package/dist/routes/__tests__/tasks.test.js +72 -0
  63. package/dist/routes/__tests__/tasks.test.js.map +1 -0
  64. package/dist/routes/attachments.d.ts.map +1 -1
  65. package/dist/routes/attachments.js +36 -16
  66. package/dist/routes/attachments.js.map +1 -1
  67. package/dist/routes/filesystem.d.ts.map +1 -1
  68. package/dist/routes/filesystem.js +24 -3
  69. package/dist/routes/filesystem.js.map +1 -1
  70. package/dist/routes/index.d.ts.map +1 -1
  71. package/dist/routes/index.js +6 -0
  72. package/dist/routes/index.js.map +1 -1
  73. package/dist/routes/previews.d.ts +6 -0
  74. package/dist/routes/previews.d.ts.map +1 -0
  75. package/dist/routes/previews.js +413 -0
  76. package/dist/routes/previews.js.map +1 -0
  77. package/dist/routes/projects.d.ts.map +1 -1
  78. package/dist/routes/projects.js +1 -0
  79. package/dist/routes/projects.js.map +1 -1
  80. package/dist/routes/system.d.ts.map +1 -1
  81. package/dist/routes/system.js +35 -1
  82. package/dist/routes/system.js.map +1 -1
  83. package/dist/routes/tasks.js +2 -2
  84. package/dist/routes/tasks.js.map +1 -1
  85. package/dist/routes/team-runs.d.ts +11 -0
  86. package/dist/routes/team-runs.d.ts.map +1 -0
  87. package/dist/routes/team-runs.js +309 -0
  88. package/dist/routes/team-runs.js.map +1 -0
  89. package/dist/routes/tunnel.d.ts.map +1 -1
  90. package/dist/routes/tunnel.js +20 -0
  91. package/dist/routes/tunnel.js.map +1 -1
  92. package/dist/routes/workspaces.d.ts.map +1 -1
  93. package/dist/routes/workspaces.js +15 -1
  94. package/dist/routes/workspaces.js.map +1 -1
  95. package/dist/services/__tests__/preview.service.test.d.ts +2 -0
  96. package/dist/services/__tests__/preview.service.test.d.ts.map +1 -0
  97. package/dist/services/__tests__/preview.service.test.js +29 -0
  98. package/dist/services/__tests__/preview.service.test.js.map +1 -0
  99. package/dist/services/__tests__/session-manager.team-run.test.d.ts +2 -0
  100. package/dist/services/__tests__/session-manager.team-run.test.d.ts.map +1 -0
  101. package/dist/services/__tests__/session-manager.team-run.test.js +286 -0
  102. package/dist/services/__tests__/session-manager.team-run.test.js.map +1 -0
  103. package/dist/services/__tests__/task.service.test.d.ts +2 -0
  104. package/dist/services/__tests__/task.service.test.d.ts.map +1 -0
  105. package/dist/services/__tests__/task.service.test.js +65 -0
  106. package/dist/services/__tests__/task.service.test.js.map +1 -0
  107. package/dist/services/__tests__/team-lock.service.test.d.ts +2 -0
  108. package/dist/services/__tests__/team-lock.service.test.d.ts.map +1 -0
  109. package/dist/services/__tests__/team-lock.service.test.js +81 -0
  110. package/dist/services/__tests__/team-lock.service.test.js.map +1 -0
  111. package/dist/services/__tests__/team-reconciler.service.test.d.ts +2 -0
  112. package/dist/services/__tests__/team-reconciler.service.test.d.ts.map +1 -0
  113. package/dist/services/__tests__/team-reconciler.service.test.js +1536 -0
  114. package/dist/services/__tests__/team-reconciler.service.test.js.map +1 -0
  115. package/dist/services/__tests__/team-run.service.test.d.ts +2 -0
  116. package/dist/services/__tests__/team-run.service.test.d.ts.map +1 -0
  117. package/dist/services/__tests__/team-run.service.test.js +699 -0
  118. package/dist/services/__tests__/team-run.service.test.js.map +1 -0
  119. package/dist/services/__tests__/team-scheduler.service.test.d.ts +2 -0
  120. package/dist/services/__tests__/team-scheduler.service.test.d.ts.map +1 -0
  121. package/dist/services/__tests__/team-scheduler.service.test.js +1688 -0
  122. package/dist/services/__tests__/team-scheduler.service.test.js.map +1 -0
  123. package/dist/services/__tests__/tunnel.service.test.d.ts +2 -0
  124. package/dist/services/__tests__/tunnel.service.test.d.ts.map +1 -0
  125. package/dist/services/__tests__/tunnel.service.test.js +138 -0
  126. package/dist/services/__tests__/tunnel.service.test.js.map +1 -0
  127. package/dist/services/__tests__/workspace.service.test.d.ts +2 -0
  128. package/dist/services/__tests__/workspace.service.test.d.ts.map +1 -0
  129. package/dist/services/__tests__/workspace.service.test.js +695 -0
  130. package/dist/services/__tests__/workspace.service.test.js.map +1 -0
  131. package/dist/services/attachment-context.d.ts +3 -0
  132. package/dist/services/attachment-context.d.ts.map +1 -0
  133. package/dist/services/attachment-context.js +34 -0
  134. package/dist/services/attachment-context.js.map +1 -0
  135. package/dist/services/preview.service.d.ts +19 -0
  136. package/dist/services/preview.service.d.ts.map +1 -0
  137. package/dist/services/preview.service.js +147 -0
  138. package/dist/services/preview.service.js.map +1 -0
  139. package/dist/services/project.service.d.ts +2 -0
  140. package/dist/services/project.service.d.ts.map +1 -1
  141. package/dist/services/project.service.js +87 -18
  142. package/dist/services/project.service.js.map +1 -1
  143. package/dist/services/session-manager.d.ts +43 -1
  144. package/dist/services/session-manager.d.ts.map +1 -1
  145. package/dist/services/session-manager.js +110 -2
  146. package/dist/services/session-manager.js.map +1 -1
  147. package/dist/services/task.service.d.ts +6 -0
  148. package/dist/services/task.service.d.ts.map +1 -1
  149. package/dist/services/task.service.js +15 -3
  150. package/dist/services/task.service.js.map +1 -1
  151. package/dist/services/team-lock.service.d.ts +25 -0
  152. package/dist/services/team-lock.service.d.ts.map +1 -0
  153. package/dist/services/team-lock.service.js +56 -0
  154. package/dist/services/team-lock.service.js.map +1 -0
  155. package/dist/services/team-reconciler.service.d.ts +44 -0
  156. package/dist/services/team-reconciler.service.d.ts.map +1 -0
  157. package/dist/services/team-reconciler.service.js +286 -0
  158. package/dist/services/team-reconciler.service.js.map +1 -0
  159. package/dist/services/team-run-events.d.ts +13 -0
  160. package/dist/services/team-run-events.d.ts.map +1 -0
  161. package/dist/services/team-run-events.js +27 -0
  162. package/dist/services/team-run-events.js.map +1 -0
  163. package/dist/services/team-run.service.d.ts +92 -0
  164. package/dist/services/team-run.service.d.ts.map +1 -0
  165. package/dist/services/team-run.service.js +835 -0
  166. package/dist/services/team-run.service.js.map +1 -0
  167. package/dist/services/team-scheduler.service.d.ts +104 -0
  168. package/dist/services/team-scheduler.service.d.ts.map +1 -0
  169. package/dist/services/team-scheduler.service.js +843 -0
  170. package/dist/services/team-scheduler.service.js.map +1 -0
  171. package/dist/services/tunnel.service.d.ts +31 -5
  172. package/dist/services/tunnel.service.d.ts.map +1 -1
  173. package/dist/services/tunnel.service.js +293 -32
  174. package/dist/services/tunnel.service.js.map +1 -1
  175. package/dist/services/workspace.service.d.ts +161 -7
  176. package/dist/services/workspace.service.d.ts.map +1 -1
  177. package/dist/services/workspace.service.js +396 -51
  178. package/dist/services/workspace.service.js.map +1 -1
  179. package/dist/socket/events.d.ts +1 -1
  180. package/dist/socket/events.d.ts.map +1 -1
  181. package/dist/socket/events.js.map +1 -1
  182. package/dist/socket/socket-gateway.d.ts.map +1 -1
  183. package/dist/socket/socket-gateway.js +5 -1
  184. package/dist/socket/socket-gateway.js.map +1 -1
  185. package/dist/web/assets/AgentDemoPage-Bf6labVB.js +1 -0
  186. package/dist/web/assets/{DemoPage-XwuS8vNB.js → DemoPage-DlfG47rV.js} +3 -3
  187. package/dist/web/assets/{GeneralSettingsPage-CliIgpwf.js → GeneralSettingsPage-DefqwzVn.js} +1 -1
  188. package/dist/web/assets/MemberAvatar-DVw_TedB.js +1 -0
  189. package/dist/web/assets/NotificationSettingsPage-C9h1U1Za.js +1 -0
  190. package/dist/web/assets/{ProfileSettingsPage-CkU_kZKG.js → ProfileSettingsPage-BkZE2yVP.js} +1 -1
  191. package/dist/web/assets/ProjectKanbanPage-B1Ckl1uY.js +89 -0
  192. package/dist/web/assets/ProjectSettingsPage-ByZ13awb.js +2 -0
  193. package/dist/web/assets/{ProviderSettingsPage-CfvdeoEU.js → ProviderSettingsPage-DSQYe8B6.js} +12 -12
  194. package/dist/web/assets/TeamSettingsPage-DUukJ_Ih.js +1 -0
  195. package/dist/web/assets/agent-tower-logo-COx9gy77.png +0 -0
  196. package/dist/web/assets/{button-BWFTEdOr.js → button-Bpm98eOV.js} +1 -1
  197. package/dist/web/assets/{chevron-down-CuPdBAx-.js → chevron-down-DSKKXCi8.js} +1 -1
  198. package/dist/web/assets/{chevron-right-Cs8vYTMn.js → chevron-right-CZdDV9GU.js} +1 -1
  199. package/dist/web/assets/chevron-up-gnnlwvYe.js +1 -0
  200. package/dist/web/assets/{circle-check-BXZTzqw0.js → circle-check-DeD_VuLK.js} +1 -1
  201. package/dist/web/assets/{code-block-OCS4YCEC-BxUpvXK_.js → code-block-OCS4YCEC-BrGjkdjS.js} +1 -1
  202. package/dist/web/assets/{confirm-dialog-CDLHRthd.js → confirm-dialog-CEVVvAcE.js} +1 -1
  203. package/dist/web/assets/folder-picker-ZBQlFEWL.js +1 -0
  204. package/dist/web/assets/index-B5g4V0NU.js +13 -0
  205. package/dist/web/assets/index-ltjI8o6A.css +1 -0
  206. package/dist/web/assets/loader-circle-GMfBClX0.js +1 -0
  207. package/dist/web/assets/{log-adapter-CeKrvZcz.js → log-adapter-DKKM3sxS.js} +1 -1
  208. package/dist/web/assets/{mermaid-NOHMQCX5-BOSwJqP0.js → mermaid-NOHMQCX5-D5USvUiZ.js} +44 -44
  209. package/dist/web/assets/modal-JMpuh-LG.js +1 -0
  210. package/dist/web/assets/{pencil-BMxBxIhw.js → pencil-QrCW47nn.js} +1 -1
  211. package/dist/web/assets/{select-BUmRG0LY.js → select-CINRzLiE.js} +1 -1
  212. package/dist/web/assets/upload-vFxZxKHo.js +1 -0
  213. package/dist/web/assets/{use-profiles-C1vlPE-2.js → use-profiles-SrVWPYv0.js} +1 -1
  214. package/dist/web/assets/{use-providers-Cdxr4Jbz.js → use-providers-BihMydl0.js} +1 -1
  215. package/dist/web/avatars/presets/avatar-preset-01-developer.png +0 -0
  216. package/dist/web/avatars/presets/avatar-preset-02-architect.png +0 -0
  217. package/dist/web/avatars/presets/avatar-preset-03-tester.png +0 -0
  218. package/dist/web/avatars/presets/avatar-preset-04-devops.png +0 -0
  219. package/dist/web/avatars/presets/avatar-preset-05-data-scientist.png +0 -0
  220. package/dist/web/avatars/presets/avatar-preset-06-frontend.png +0 -0
  221. package/dist/web/avatars/presets/avatar-preset-07-backend.png +0 -0
  222. package/dist/web/avatars/presets/avatar-preset-08-security.png +0 -0
  223. package/dist/web/avatars/presets/avatar-preset-09-project-manager.png +0 -0
  224. package/dist/web/avatars/presets/avatar-preset-10-product-manager.png +0 -0
  225. package/dist/web/avatars/presets/avatar-preset-11-scrum-master.png +0 -0
  226. package/dist/web/avatars/presets/avatar-preset-12-tech-lead.png +0 -0
  227. package/dist/web/avatars/presets/avatar-preset-13-coordinator.png +0 -0
  228. package/dist/web/avatars/presets/avatar-preset-14-mentor.png +0 -0
  229. package/dist/web/avatars/presets/avatar-preset-15-reviewer.png +0 -0
  230. package/dist/web/avatars/presets/avatar-preset-16-ui-designer.png +0 -0
  231. package/dist/web/avatars/presets/avatar-preset-17-ux-researcher.png +0 -0
  232. package/dist/web/avatars/presets/avatar-preset-18-documenter.png +0 -0
  233. package/dist/web/avatars/presets/avatar-preset-19-translator.png +0 -0
  234. package/dist/web/avatars/presets/avatar-preset-20-analyst.png +0 -0
  235. package/dist/web/avatars/presets/avatar-preset-21-consultant.png +0 -0
  236. package/dist/web/avatars/presets/avatar-preset-22-creative-director.png +0 -0
  237. package/dist/web/avatars/presets/avatar-preset-23-support.png +0 -0
  238. package/dist/web/avatars/presets/avatar-preset-24-assistant.png +0 -0
  239. package/dist/web/avatars/presets/avatar-preset-25-robot.png +0 -0
  240. package/dist/web/avatars/presets/avatar-preset-grid.png +0 -0
  241. package/dist/web/index.html +2 -2
  242. package/node_modules/@agent-tower/shared/dist/socket/events.d.ts +10 -0
  243. package/node_modules/@agent-tower/shared/dist/socket/events.d.ts.map +1 -1
  244. package/node_modules/@agent-tower/shared/dist/socket/events.js +1 -0
  245. package/node_modules/@agent-tower/shared/dist/socket/events.js.map +1 -1
  246. package/node_modules/@agent-tower/shared/dist/types.d.ts +161 -0
  247. package/node_modules/@agent-tower/shared/dist/types.d.ts.map +1 -1
  248. package/node_modules/@agent-tower/shared/dist/types.js.map +1 -1
  249. package/node_modules/@prisma/client/.prisma/client/default.d.ts +1 -0
  250. package/node_modules/@prisma/client/.prisma/client/default.js +1 -0
  251. package/node_modules/@prisma/client/.prisma/client/edge.d.ts +1 -0
  252. package/node_modules/@prisma/client/.prisma/client/edge.js +396 -0
  253. package/node_modules/@prisma/client/.prisma/client/index-browser.js +385 -0
  254. package/node_modules/@prisma/client/.prisma/client/index.d.ts +26996 -0
  255. package/node_modules/@prisma/client/.prisma/client/index.js +421 -0
  256. package/node_modules/@prisma/client/.prisma/client/libquery_engine-darwin-arm64.dylib.node +0 -0
  257. package/node_modules/@prisma/client/.prisma/client/package.json +97 -0
  258. package/node_modules/@prisma/client/.prisma/client/query_engine-windows.dll.node +0 -0
  259. package/node_modules/@prisma/client/.prisma/client/schema.prisma +296 -0
  260. package/node_modules/@prisma/client/.prisma/client/wasm.d.ts +1 -0
  261. package/node_modules/@prisma/client/.prisma/client/wasm.js +385 -0
  262. package/node_modules/@prisma/client/package.json +3 -2
  263. package/package.json +2 -1
  264. package/prisma/migrations/20260515000000_add_workspace_preview_target/migration.sql +2 -0
  265. package/prisma/migrations/20260518000000_add_team_run_collaboration/migration.sql +150 -0
  266. package/prisma/migrations/20260522000000_add_team_member_session_policy/migration.sql +2 -0
  267. package/prisma/migrations/20260526000000_add_team_run_main_and_dedicated_workspaces/migration.sql +21 -0
  268. package/prisma/schema.prisma +147 -1
  269. package/dist/web/assets/AgentDemoPage-ClnGPAV9.js +0 -1
  270. package/dist/web/assets/NotificationSettingsPage-y3vhVgPv.js +0 -1
  271. package/dist/web/assets/ProjectKanbanPage-BddzfZRV.js +0 -87
  272. package/dist/web/assets/ProjectSettingsPage-B6xhbziO.js +0 -2
  273. package/dist/web/assets/circle-alert-EUyZcWhp.js +0 -1
  274. package/dist/web/assets/folder-picker-CUbhsnhi.js +0 -1
  275. package/dist/web/assets/index-BGvfX18x.css +0 -1
  276. package/dist/web/assets/index-CHN8jahE.js +0 -13
  277. package/dist/web/assets/loader-circle-BHzDVpxt.js +0 -1
  278. package/dist/web/assets/modal-D_AU4URz.js +0 -1
  279. package/dist/web/assets/use-projects-Bcd5hIOY.js +0 -1
@@ -0,0 +1,695 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
7
+ import { TeamLockService } from '../team-lock.service.js';
8
+ const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-tower-workspace-service-'));
9
+ const dbPath = path.join(testDir, 'test.db');
10
+ process.env.AGENT_TOWER_DATABASE_URL = `file:${dbPath}`;
11
+ const { createWorktreeMock, ensureWorktreeExistsMock, removeWorktreeMock, mergeWorktreeMock, mergeIntoWorktreeMock, execGitMock, } = vi.hoisted(() => ({
12
+ createWorktreeMock: vi.fn(),
13
+ ensureWorktreeExistsMock: vi.fn(),
14
+ removeWorktreeMock: vi.fn(),
15
+ mergeWorktreeMock: vi.fn(),
16
+ mergeIntoWorktreeMock: vi.fn(),
17
+ execGitMock: vi.fn(),
18
+ }));
19
+ vi.mock('../../git/worktree.manager.js', () => ({
20
+ WorktreeManager: vi.fn().mockImplementation(function () {
21
+ return {
22
+ create: createWorktreeMock,
23
+ ensureWorktreeExists: ensureWorktreeExistsMock,
24
+ remove: removeWorktreeMock,
25
+ getDiff: vi.fn(),
26
+ rebase: vi.fn(),
27
+ getGitOperationStatus: vi.fn(),
28
+ abortOperation: vi.fn(),
29
+ merge: mergeWorktreeMock,
30
+ mergeIntoWorktree: mergeIntoWorktreeMock,
31
+ prune: vi.fn(),
32
+ };
33
+ }),
34
+ }));
35
+ vi.mock('../../git/git-cli.js', () => ({
36
+ execGit: execGitMock,
37
+ MergeConflictError: class MergeConflictError extends Error {
38
+ },
39
+ }));
40
+ execGitMock.mockImplementation(async (_repoPath, args) => {
41
+ if (args[0] === 'rev-parse' && args.includes('--abbrev-ref')) {
42
+ return 'main\n';
43
+ }
44
+ if (args[0] === 'status') {
45
+ return '';
46
+ }
47
+ return '';
48
+ });
49
+ vi.mock('../copy-files.service.js', () => ({
50
+ copyProjectFiles: vi.fn(),
51
+ }));
52
+ const __filename = fileURLToPath(import.meta.url);
53
+ const __dirname = path.dirname(__filename);
54
+ const serverRoot = path.resolve(__dirname, '../../..');
55
+ const schemaPath = path.join(serverRoot, 'prisma/schema.prisma');
56
+ let prisma;
57
+ let WorkspaceService;
58
+ async function createTask(title = 'Workspace service task') {
59
+ const project = await prisma.project.create({
60
+ data: {
61
+ name: `${title} project`,
62
+ repoPath: testDir,
63
+ },
64
+ });
65
+ const task = await prisma.task.create({
66
+ data: {
67
+ title,
68
+ projectId: project.id,
69
+ },
70
+ });
71
+ return { project, task };
72
+ }
73
+ async function createTeamRunWithMember(options = {}) {
74
+ const { project, task } = await createTask();
75
+ const teamRun = await prisma.teamRun.create({
76
+ data: {
77
+ taskId: task.id,
78
+ mode: 'AUTO',
79
+ },
80
+ });
81
+ const member = await prisma.teamMember.create({
82
+ data: {
83
+ teamRunId: teamRun.id,
84
+ presetId: null,
85
+ name: 'Member 1',
86
+ aliases: '["member-1"]',
87
+ providerId: 'provider-1',
88
+ rolePrompt: 'Role 1',
89
+ capabilities: '{}',
90
+ workspacePolicy: options.workspacePolicy ?? 'dedicated',
91
+ triggerPolicy: 'MENTION_ONLY',
92
+ sessionPolicy: 'new_per_request',
93
+ avatar: null,
94
+ },
95
+ });
96
+ return { project, task, teamRun, member };
97
+ }
98
+ function mockCreatedWorktreePath(branchName) {
99
+ const worktreePath = path.join(testDir, 'created-worktrees', branchName);
100
+ fs.mkdirSync(path.join(worktreePath, '.git'), { recursive: true });
101
+ return worktreePath;
102
+ }
103
+ function mockRestoredWorktreePath(branchName) {
104
+ const worktreePath = path.join(testDir, 'restored-worktrees', branchName);
105
+ fs.mkdirSync(path.join(worktreePath, '.git'), { recursive: true });
106
+ return worktreePath;
107
+ }
108
+ describe('WorkspaceService TeamRun workspace lifecycle', () => {
109
+ let service;
110
+ beforeAll(async () => {
111
+ execFileSync('pnpm', ['exec', 'prisma', 'db', 'push', '--skip-generate', `--schema=${schemaPath}`], {
112
+ cwd: serverRoot,
113
+ env: { ...process.env, AGENT_TOWER_DATABASE_URL: `file:${dbPath}` },
114
+ stdio: 'pipe',
115
+ });
116
+ const utilsModule = await import('../../utils/index.js');
117
+ const serviceModule = await import('../workspace.service.js');
118
+ prisma = utilsModule.prisma;
119
+ WorkspaceService = serviceModule.WorkspaceService;
120
+ });
121
+ beforeEach(async () => {
122
+ vi.clearAllMocks();
123
+ execGitMock.mockImplementation(async (_repoPath, args) => {
124
+ if (args[0] === 'rev-parse' && args.includes('--abbrev-ref')) {
125
+ return 'main\n';
126
+ }
127
+ if (args[0] === 'status') {
128
+ return '';
129
+ }
130
+ return '';
131
+ });
132
+ createWorktreeMock.mockImplementation(async (branchName) => mockCreatedWorktreePath(branchName));
133
+ ensureWorktreeExistsMock.mockImplementation(async (branchName) => mockRestoredWorktreePath(branchName));
134
+ removeWorktreeMock.mockImplementation(async (worktreePath) => ({
135
+ status: 'removed',
136
+ path: worktreePath,
137
+ managed: true,
138
+ }));
139
+ mergeWorktreeMock.mockResolvedValue({ sha: 'root-merge-sha', taskBranch: 'team-main' });
140
+ mergeIntoWorktreeMock.mockResolvedValue({
141
+ sha: 'child-merge-sha',
142
+ sourceBranch: 'dedicated-child',
143
+ targetBranch: 'team-main',
144
+ });
145
+ service = new WorkspaceService();
146
+ await prisma.agentInvocation.deleteMany();
147
+ await prisma.workRequest.deleteMany();
148
+ await prisma.roomMessage.deleteMany();
149
+ await prisma.teamMember.deleteMany();
150
+ await prisma.teamRun.deleteMany();
151
+ await prisma.session.deleteMany();
152
+ await prisma.workspace.deleteMany();
153
+ await prisma.task.deleteMany();
154
+ await prisma.project.deleteMany();
155
+ });
156
+ afterAll(async () => {
157
+ vi.restoreAllMocks();
158
+ await prisma.$disconnect();
159
+ fs.rmSync(testDir, { recursive: true, force: true });
160
+ });
161
+ it('creates and binds a main workspace without reusing child or inactive task workspaces', async () => {
162
+ const { task, teamRun, member } = await createTeamRunWithMember();
163
+ const inactiveRoot = await prisma.workspace.create({
164
+ data: {
165
+ taskId: task.id,
166
+ branchName: 'inactive-root',
167
+ worktreePath: '',
168
+ status: 'HIBERNATED',
169
+ hibernatedAt: new Date(),
170
+ },
171
+ });
172
+ await prisma.workspace.create({
173
+ data: {
174
+ taskId: task.id,
175
+ parentWorkspaceId: inactiveRoot.id,
176
+ ownerMemberId: member.id,
177
+ branchName: 'child-active',
178
+ worktreePath: path.join(testDir, 'child-active'),
179
+ status: 'ACTIVE',
180
+ },
181
+ });
182
+ await prisma.teamRun.update({
183
+ where: { id: teamRun.id },
184
+ data: { mainWorkspaceId: inactiveRoot.id },
185
+ });
186
+ const mainWorkspace = await service.getOrCreateMainWorkspace(teamRun.id);
187
+ expect(mainWorkspace.id).not.toBe(inactiveRoot.id);
188
+ expect(mainWorkspace.parentWorkspaceId).toBeNull();
189
+ expect(mainWorkspace.ownerMemberId).toBeNull();
190
+ expect(mainWorkspace.status).toBe('ACTIVE');
191
+ expect(mainWorkspace.branchName).toMatch(new RegExp(`^at/team/${teamRun.id.slice(0, 8)}/main/`));
192
+ expect(createWorktreeMock).toHaveBeenCalledTimes(1);
193
+ expect(ensureWorktreeExistsMock).not.toHaveBeenCalledWith('inactive-root');
194
+ await expect(prisma.teamRun.findUnique({ where: { id: teamRun.id } })).resolves.toMatchObject({
195
+ mainWorkspaceId: mainWorkspace.id,
196
+ });
197
+ });
198
+ it('does not bind an orphan dedicated child as the TeamRun main workspace', async () => {
199
+ const { task, teamRun, member } = await createTeamRunWithMember();
200
+ const orphanPath = path.join(testDir, 'orphan-child');
201
+ fs.mkdirSync(path.join(orphanPath, '.git'), { recursive: true });
202
+ const orphanChild = await prisma.workspace.create({
203
+ data: {
204
+ taskId: task.id,
205
+ parentWorkspaceId: null,
206
+ ownerMemberId: member.id,
207
+ branchName: 'orphan-child',
208
+ worktreePath: orphanPath,
209
+ status: 'ACTIVE',
210
+ },
211
+ });
212
+ await prisma.teamRun.update({
213
+ where: { id: teamRun.id },
214
+ data: { mainWorkspaceId: orphanChild.id },
215
+ });
216
+ const mainWorkspace = await service.getOrCreateMainWorkspace(teamRun.id);
217
+ expect(mainWorkspace.id).not.toBe(orphanChild.id);
218
+ expect(mainWorkspace.parentWorkspaceId).toBeNull();
219
+ expect(mainWorkspace.ownerMemberId).toBeNull();
220
+ expect(mainWorkspace.status).toBe('ACTIVE');
221
+ expect(mainWorkspace.branchName).toMatch(new RegExp(`^at/team/${teamRun.id.slice(0, 8)}/main/`));
222
+ expect(createWorktreeMock).toHaveBeenCalledTimes(1);
223
+ await expect(prisma.teamRun.findUnique({ where: { id: teamRun.id } })).resolves.toMatchObject({
224
+ mainWorkspaceId: mainWorkspace.id,
225
+ });
226
+ });
227
+ it('reuses an active root workspace as the TeamRun main workspace', async () => {
228
+ const { task, teamRun } = await createTeamRunWithMember();
229
+ const rootPath = path.join(testDir, 'active-root');
230
+ fs.mkdirSync(path.join(rootPath, '.git'), { recursive: true });
231
+ const activeRoot = await prisma.workspace.create({
232
+ data: {
233
+ taskId: task.id,
234
+ branchName: 'active-root',
235
+ worktreePath: rootPath,
236
+ status: 'ACTIVE',
237
+ },
238
+ });
239
+ const mainWorkspace = await service.getOrCreateMainWorkspace(teamRun.id);
240
+ expect(mainWorkspace.id).toBe(activeRoot.id);
241
+ expect(createWorktreeMock).not.toHaveBeenCalled();
242
+ expect(ensureWorktreeExistsMock).not.toHaveBeenCalled();
243
+ await expect(prisma.teamRun.findUnique({ where: { id: teamRun.id } })).resolves.toMatchObject({
244
+ mainWorkspaceId: activeRoot.id,
245
+ });
246
+ });
247
+ it('creates and reuses a dedicated child workspace for the same main workspace and member', async () => {
248
+ const { teamRun, member } = await createTeamRunWithMember();
249
+ const first = await service.getOrCreateDedicatedWorkspace(teamRun.id, member.id);
250
+ const second = await service.getOrCreateDedicatedWorkspace(teamRun.id, member.id);
251
+ const reloadedTeamRun = await prisma.teamRun.findUniqueOrThrow({ where: { id: teamRun.id } });
252
+ const mainWorkspace = await prisma.workspace.findUniqueOrThrow({
253
+ where: { id: reloadedTeamRun.mainWorkspaceId ?? '' },
254
+ });
255
+ expect(first.id).toBe(second.id);
256
+ expect(first.parentWorkspaceId).toBe(reloadedTeamRun.mainWorkspaceId);
257
+ expect(first.ownerMemberId).toBe(member.id);
258
+ expect(first.baseBranch).toBe(mainWorkspace.branchName);
259
+ expect(first.branchName).toMatch(new RegExp(`^at/team/${teamRun.id.slice(0, 8)}/member-${member.id.slice(0, 8)}/`));
260
+ expect(createWorktreeMock).toHaveBeenCalledWith(first.branchName, mainWorkspace.branchName);
261
+ await expect(prisma.workspace.count({
262
+ where: {
263
+ parentWorkspaceId: reloadedTeamRun.mainWorkspaceId,
264
+ ownerMemberId: member.id,
265
+ },
266
+ })).resolves.toBe(1);
267
+ });
268
+ it('reactivates a hibernated dedicated child workspace', async () => {
269
+ const { task, teamRun, member } = await createTeamRunWithMember();
270
+ const mainWorkspace = await prisma.workspace.create({
271
+ data: {
272
+ taskId: task.id,
273
+ branchName: 'team-main',
274
+ worktreePath: path.join(testDir, 'team-main'),
275
+ status: 'ACTIVE',
276
+ },
277
+ });
278
+ await prisma.teamRun.update({
279
+ where: { id: teamRun.id },
280
+ data: { mainWorkspaceId: mainWorkspace.id },
281
+ });
282
+ const child = await prisma.workspace.create({
283
+ data: {
284
+ taskId: task.id,
285
+ parentWorkspaceId: mainWorkspace.id,
286
+ ownerMemberId: member.id,
287
+ branchName: 'dedicated-child',
288
+ worktreePath: '',
289
+ status: 'HIBERNATED',
290
+ hibernatedAt: new Date(),
291
+ },
292
+ });
293
+ const workspace = await service.getOrCreateDedicatedWorkspace(teamRun.id, member.id);
294
+ expect(workspace.id).toBe(child.id);
295
+ expect(workspace.status).toBe('ACTIVE');
296
+ expect(workspace.hibernatedAt).toBeNull();
297
+ expect(workspace.worktreePath).toBe(mockRestoredWorktreePath('dedicated-child'));
298
+ expect(ensureWorktreeExistsMock).toHaveBeenCalledWith('dedicated-child');
299
+ expect(createWorktreeMock).not.toHaveBeenCalledWith(expect.stringContaining('member-'));
300
+ });
301
+ it('merges a dedicated child into the TeamRun main workspace without marking the task DONE', async () => {
302
+ const { task, teamRun, member } = await createTeamRunWithMember();
303
+ await prisma.task.update({
304
+ where: { id: task.id },
305
+ data: { status: 'IN_REVIEW' },
306
+ });
307
+ const mainWorkspace = await prisma.workspace.create({
308
+ data: {
309
+ taskId: task.id,
310
+ branchName: 'team-main',
311
+ baseBranch: 'main',
312
+ worktreePath: path.join(testDir, 'team-main'),
313
+ status: 'ACTIVE',
314
+ },
315
+ });
316
+ await prisma.teamRun.update({
317
+ where: { id: teamRun.id },
318
+ data: { mainWorkspaceId: mainWorkspace.id },
319
+ });
320
+ const childWorkspace = await prisma.workspace.create({
321
+ data: {
322
+ taskId: task.id,
323
+ parentWorkspaceId: mainWorkspace.id,
324
+ ownerMemberId: member.id,
325
+ branchName: 'dedicated-child',
326
+ baseBranch: mainWorkspace.branchName,
327
+ worktreePath: path.join(testDir, 'dedicated-child'),
328
+ status: 'ACTIVE',
329
+ },
330
+ });
331
+ const sha = await service.merge(childWorkspace.id, 'merge child');
332
+ expect(sha).toBe('child-merge-sha');
333
+ expect(mergeIntoWorktreeMock).toHaveBeenCalledWith(childWorkspace.worktreePath, mainWorkspace.worktreePath, { commitMessage: 'merge child' });
334
+ expect(mergeWorktreeMock).not.toHaveBeenCalled();
335
+ await expect(prisma.workspace.findUnique({ where: { id: childWorkspace.id } })).resolves.toMatchObject({
336
+ status: 'MERGED',
337
+ });
338
+ await expect(prisma.task.findUnique({ where: { id: task.id } })).resolves.toMatchObject({
339
+ status: 'IN_REVIEW',
340
+ });
341
+ });
342
+ it('rejects child merge when the parent workspace has an active write session', async () => {
343
+ const { task, teamRun, member } = await createTeamRunWithMember();
344
+ const mainWorkspace = await prisma.workspace.create({
345
+ data: {
346
+ taskId: task.id,
347
+ branchName: 'team-main',
348
+ worktreePath: path.join(testDir, 'team-main'),
349
+ status: 'ACTIVE',
350
+ },
351
+ });
352
+ await prisma.teamRun.update({
353
+ where: { id: teamRun.id },
354
+ data: { mainWorkspaceId: mainWorkspace.id },
355
+ });
356
+ const childWorkspace = await prisma.workspace.create({
357
+ data: {
358
+ taskId: task.id,
359
+ parentWorkspaceId: mainWorkspace.id,
360
+ ownerMemberId: member.id,
361
+ branchName: 'dedicated-child',
362
+ worktreePath: path.join(testDir, 'dedicated-child'),
363
+ status: 'ACTIVE',
364
+ },
365
+ });
366
+ await prisma.session.create({
367
+ data: {
368
+ workspaceId: mainWorkspace.id,
369
+ agentType: 'CODEX',
370
+ prompt: 'write on main',
371
+ status: 'RUNNING',
372
+ },
373
+ });
374
+ await expect(service.merge(childWorkspace.id)).rejects.toMatchObject({
375
+ code: 'PARENT_WORKSPACE_HAS_ACTIVE_SESSION',
376
+ });
377
+ expect(mergeIntoWorktreeMock).not.toHaveBeenCalled();
378
+ });
379
+ it('rejects final TeamRun main workspace merge while dedicated children are not final', async () => {
380
+ const { task, teamRun, member } = await createTeamRunWithMember();
381
+ await prisma.task.update({
382
+ where: { id: task.id },
383
+ data: { status: 'IN_REVIEW' },
384
+ });
385
+ const mainWorkspace = await prisma.workspace.create({
386
+ data: {
387
+ taskId: task.id,
388
+ branchName: 'team-main',
389
+ baseBranch: 'main',
390
+ worktreePath: path.join(testDir, 'team-main'),
391
+ status: 'ACTIVE',
392
+ },
393
+ });
394
+ await prisma.teamRun.update({
395
+ where: { id: teamRun.id },
396
+ data: { mainWorkspaceId: mainWorkspace.id },
397
+ });
398
+ await prisma.workspace.create({
399
+ data: {
400
+ taskId: task.id,
401
+ parentWorkspaceId: mainWorkspace.id,
402
+ ownerMemberId: member.id,
403
+ branchName: 'dedicated-child',
404
+ baseBranch: mainWorkspace.branchName,
405
+ worktreePath: path.join(testDir, 'dedicated-child'),
406
+ status: 'ACTIVE',
407
+ },
408
+ });
409
+ await expect(service.merge(mainWorkspace.id)).rejects.toMatchObject({
410
+ code: 'TEAM_RUN_CHILD_WORKSPACES_NOT_FINAL',
411
+ });
412
+ expect(mergeWorktreeMock).not.toHaveBeenCalled();
413
+ await expect(prisma.task.findUnique({ where: { id: task.id } })).resolves.toMatchObject({
414
+ status: 'IN_REVIEW',
415
+ });
416
+ });
417
+ it('rejects final merge from a non-main root workspace on a TeamRun task', async () => {
418
+ const { task, teamRun, member } = await createTeamRunWithMember();
419
+ await prisma.task.update({
420
+ where: { id: task.id },
421
+ data: { status: 'IN_REVIEW' },
422
+ });
423
+ const mainWorkspace = await prisma.workspace.create({
424
+ data: {
425
+ taskId: task.id,
426
+ branchName: 'team-main',
427
+ baseBranch: 'main',
428
+ worktreePath: path.join(testDir, 'team-main'),
429
+ status: 'ACTIVE',
430
+ },
431
+ });
432
+ await prisma.teamRun.update({
433
+ where: { id: teamRun.id },
434
+ data: { mainWorkspaceId: mainWorkspace.id },
435
+ });
436
+ await prisma.workspace.create({
437
+ data: {
438
+ taskId: task.id,
439
+ parentWorkspaceId: mainWorkspace.id,
440
+ ownerMemberId: member.id,
441
+ branchName: 'dedicated-child',
442
+ baseBranch: mainWorkspace.branchName,
443
+ worktreePath: path.join(testDir, 'dedicated-child'),
444
+ status: 'ACTIVE',
445
+ },
446
+ });
447
+ const extraRootWorkspace = await prisma.workspace.create({
448
+ data: {
449
+ taskId: task.id,
450
+ branchName: 'extra-root',
451
+ baseBranch: 'main',
452
+ worktreePath: path.join(testDir, 'extra-root'),
453
+ status: 'ACTIVE',
454
+ },
455
+ });
456
+ await expect(service.merge(extraRootWorkspace.id)).rejects.toMatchObject({
457
+ code: 'TEAM_RUN_NON_MAIN_WORKSPACE_FINAL_MERGE_FORBIDDEN',
458
+ });
459
+ expect(mergeWorktreeMock).not.toHaveBeenCalled();
460
+ await expect(prisma.task.findUnique({ where: { id: task.id } })).resolves.toMatchObject({
461
+ status: 'IN_REVIEW',
462
+ });
463
+ });
464
+ it('rejects final TeamRun main workspace merge when ownerless children are not final', async () => {
465
+ const { task, teamRun } = await createTeamRunWithMember();
466
+ await prisma.task.update({
467
+ where: { id: task.id },
468
+ data: { status: 'IN_REVIEW' },
469
+ });
470
+ const mainWorkspace = await prisma.workspace.create({
471
+ data: {
472
+ taskId: task.id,
473
+ branchName: 'team-main',
474
+ baseBranch: 'main',
475
+ worktreePath: path.join(testDir, 'team-main'),
476
+ status: 'ACTIVE',
477
+ },
478
+ });
479
+ await prisma.teamRun.update({
480
+ where: { id: teamRun.id },
481
+ data: { mainWorkspaceId: mainWorkspace.id },
482
+ });
483
+ await prisma.workspace.createMany({
484
+ data: [
485
+ {
486
+ taskId: task.id,
487
+ parentWorkspaceId: mainWorkspace.id,
488
+ ownerMemberId: null,
489
+ branchName: 'ownerless-active-child',
490
+ baseBranch: mainWorkspace.branchName,
491
+ worktreePath: path.join(testDir, 'ownerless-active-child'),
492
+ status: 'ACTIVE',
493
+ },
494
+ {
495
+ taskId: task.id,
496
+ parentWorkspaceId: mainWorkspace.id,
497
+ ownerMemberId: null,
498
+ branchName: 'ownerless-hibernated-child',
499
+ baseBranch: mainWorkspace.branchName,
500
+ worktreePath: '',
501
+ status: 'HIBERNATED',
502
+ hibernatedAt: new Date(),
503
+ },
504
+ ],
505
+ });
506
+ await expect(service.merge(mainWorkspace.id)).rejects.toMatchObject({
507
+ code: 'TEAM_RUN_CHILD_WORKSPACES_NOT_FINAL',
508
+ });
509
+ expect(mergeWorktreeMock).not.toHaveBeenCalled();
510
+ await expect(prisma.task.findUnique({ where: { id: task.id } })).resolves.toMatchObject({
511
+ status: 'IN_REVIEW',
512
+ });
513
+ });
514
+ it('marks the task DONE only after the TeamRun main workspace final merge succeeds', async () => {
515
+ const { task, teamRun, member } = await createTeamRunWithMember();
516
+ await prisma.task.update({
517
+ where: { id: task.id },
518
+ data: { status: 'IN_REVIEW' },
519
+ });
520
+ const mainWorkspace = await prisma.workspace.create({
521
+ data: {
522
+ taskId: task.id,
523
+ branchName: 'team-main',
524
+ baseBranch: 'main',
525
+ worktreePath: path.join(testDir, 'team-main'),
526
+ status: 'ACTIVE',
527
+ },
528
+ });
529
+ await prisma.teamRun.update({
530
+ where: { id: teamRun.id },
531
+ data: { mainWorkspaceId: mainWorkspace.id },
532
+ });
533
+ await prisma.workspace.create({
534
+ data: {
535
+ taskId: task.id,
536
+ parentWorkspaceId: mainWorkspace.id,
537
+ ownerMemberId: member.id,
538
+ branchName: 'dedicated-child',
539
+ baseBranch: mainWorkspace.branchName,
540
+ worktreePath: path.join(testDir, 'dedicated-child'),
541
+ status: 'MERGED',
542
+ },
543
+ });
544
+ const sha = await service.merge(mainWorkspace.id, 'final merge');
545
+ expect(sha).toBe('root-merge-sha');
546
+ expect(mergeWorktreeMock).toHaveBeenCalledWith(mainWorkspace.worktreePath, 'main', { commitMessage: 'final merge' });
547
+ await expect(prisma.workspace.findUnique({ where: { id: mainWorkspace.id } })).resolves.toMatchObject({
548
+ status: 'MERGED',
549
+ });
550
+ await expect(prisma.task.findUnique({ where: { id: task.id } })).resolves.toMatchObject({
551
+ status: 'DONE',
552
+ });
553
+ });
554
+ it('uses the shared project merge lock and allows the current invocation owner to merge', async () => {
555
+ const lockService = new TeamLockService();
556
+ const lockedService = new WorkspaceService(lockService);
557
+ const { project, task } = await createTask('locked merge task');
558
+ const workspace = await prisma.workspace.create({
559
+ data: {
560
+ taskId: task.id,
561
+ branchName: 'locked-main',
562
+ baseBranch: 'main',
563
+ worktreePath: path.join(testDir, 'locked-main'),
564
+ status: 'ACTIVE',
565
+ },
566
+ });
567
+ expect(lockService.acquire('external-owner', [`project:${project.id}:merge`])).toBe(true);
568
+ await expect(lockedService.merge(workspace.id)).rejects.toMatchObject({
569
+ code: 'PROJECT_MERGE_LOCKED',
570
+ });
571
+ const sha = await lockedService.merge(workspace.id, { lockOwnerId: 'external-owner' });
572
+ expect(sha).toBe('root-merge-sha');
573
+ });
574
+ it('cleans up stale managed worktree records and deletes their branches', async () => {
575
+ const { project, task } = await createTask('cleanup stale workspace task');
576
+ await prisma.task.update({
577
+ where: { id: task.id },
578
+ data: { status: 'DONE' },
579
+ });
580
+ const workspace = await prisma.workspace.create({
581
+ data: {
582
+ taskId: task.id,
583
+ branchName: 'at/12345678',
584
+ worktreePath: path.join(project.repoPath, '..', '.worktrees', 'at', '12345678'),
585
+ status: 'MERGED',
586
+ },
587
+ });
588
+ removeWorktreeMock.mockResolvedValueOnce({
589
+ status: 'stale_removed',
590
+ path: workspace.worktreePath,
591
+ managed: true,
592
+ });
593
+ const cleaned = await service.cleanup();
594
+ expect(cleaned).toBe(1);
595
+ expect(removeWorktreeMock).toHaveBeenCalledWith(workspace.worktreePath);
596
+ expect(execGitMock).toHaveBeenCalledWith(project.repoPath, ['branch', '-D', workspace.branchName]);
597
+ await expect(prisma.workspace.findUnique({ where: { id: workspace.id } })).resolves.toBeNull();
598
+ });
599
+ it('cleans up stale managed TeamRun nested worktree records', async () => {
600
+ const { project, task, teamRun } = await createTeamRunWithMember();
601
+ await prisma.task.update({
602
+ where: { id: task.id },
603
+ data: { status: 'DONE' },
604
+ });
605
+ const workspace = await prisma.workspace.create({
606
+ data: {
607
+ taskId: task.id,
608
+ branchName: `at/team/${teamRun.id.slice(0, 8)}/main/87654321`,
609
+ worktreePath: path.join(project.repoPath, '..', '.worktrees', 'at', 'team', teamRun.id.slice(0, 8), 'main', '87654321'),
610
+ status: 'MERGED',
611
+ },
612
+ });
613
+ removeWorktreeMock.mockResolvedValueOnce({
614
+ status: 'stale_removed',
615
+ path: workspace.worktreePath,
616
+ managed: true,
617
+ });
618
+ const cleaned = await service.cleanup();
619
+ expect(cleaned).toBe(1);
620
+ expect(removeWorktreeMock).toHaveBeenCalledWith(workspace.worktreePath);
621
+ expect(execGitMock).toHaveBeenCalledWith(project.repoPath, ['branch', '-D', workspace.branchName]);
622
+ await expect(prisma.workspace.findUnique({ where: { id: workspace.id } })).resolves.toBeNull();
623
+ });
624
+ it('keeps non-managed unregistered worktree records for retry', async () => {
625
+ const { task } = await createTask('cleanup unmanaged workspace task');
626
+ await prisma.task.update({
627
+ where: { id: task.id },
628
+ data: { status: 'DONE' },
629
+ });
630
+ const workspace = await prisma.workspace.create({
631
+ data: {
632
+ taskId: task.id,
633
+ branchName: 'at/unmanaged',
634
+ worktreePath: path.join(testDir, 'outside-managed-worktree'),
635
+ status: 'MERGED',
636
+ },
637
+ });
638
+ removeWorktreeMock.mockResolvedValueOnce({
639
+ status: 'unregistered',
640
+ path: workspace.worktreePath,
641
+ managed: false,
642
+ });
643
+ const cleaned = await service.cleanup();
644
+ expect(cleaned).toBe(0);
645
+ await expect(prisma.workspace.findUnique({ where: { id: workspace.id } })).resolves.toMatchObject({
646
+ id: workspace.id,
647
+ });
648
+ });
649
+ it('keeps managed unregistered ancestor worktree records for retry', async () => {
650
+ const { project, task, teamRun } = await createTeamRunWithMember();
651
+ await prisma.task.update({
652
+ where: { id: task.id },
653
+ data: { status: 'DONE' },
654
+ });
655
+ const ancestorPath = path.join(project.repoPath, '..', '.worktrees', 'at', 'team', teamRun.id.slice(0, 8));
656
+ const workspace = await prisma.workspace.create({
657
+ data: {
658
+ taskId: task.id,
659
+ branchName: `at/team/${teamRun.id.slice(0, 8)}`,
660
+ worktreePath: ancestorPath,
661
+ status: 'MERGED',
662
+ },
663
+ });
664
+ removeWorktreeMock.mockResolvedValueOnce({
665
+ status: 'unregistered',
666
+ path: ancestorPath,
667
+ managed: true,
668
+ });
669
+ const cleaned = await service.cleanup();
670
+ expect(cleaned).toBe(0);
671
+ expect(execGitMock).not.toHaveBeenCalledWith(project.repoPath, ['branch', '-D', workspace.branchName]);
672
+ await expect(prisma.workspace.findUnique({ where: { id: workspace.id } })).resolves.toMatchObject({
673
+ id: workspace.id,
674
+ });
675
+ });
676
+ it('does not cleanup active workspaces', async () => {
677
+ const { task } = await createTask('cleanup active workspace task');
678
+ await prisma.task.update({
679
+ where: { id: task.id },
680
+ data: { status: 'DONE' },
681
+ });
682
+ await prisma.workspace.create({
683
+ data: {
684
+ taskId: task.id,
685
+ branchName: 'at/active',
686
+ worktreePath: path.join(testDir, 'active-worktree'),
687
+ status: 'ACTIVE',
688
+ },
689
+ });
690
+ const cleaned = await service.cleanup();
691
+ expect(cleaned).toBe(0);
692
+ expect(removeWorktreeMock).not.toHaveBeenCalled();
693
+ });
694
+ });
695
+ //# sourceMappingURL=workspace.service.test.js.map