agent-tower 0.4.16-beta.0 → 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 (211) hide show
  1. package/dist/app.test.js +2 -0
  2. package/dist/app.test.js.map +1 -1
  3. package/dist/git/git-cli.d.ts +18 -1
  4. package/dist/git/git-cli.d.ts.map +1 -1
  5. package/dist/git/git-cli.js +17 -1
  6. package/dist/git/git-cli.js.map +1 -1
  7. package/dist/git/worktree.manager.d.ts +29 -2
  8. package/dist/git/worktree.manager.d.ts.map +1 -1
  9. package/dist/git/worktree.manager.js +137 -16
  10. package/dist/git/worktree.manager.js.map +1 -1
  11. package/dist/git/worktree.manager.test.d.ts +2 -0
  12. package/dist/git/worktree.manager.test.d.ts.map +1 -0
  13. package/dist/git/worktree.manager.test.js +104 -0
  14. package/dist/git/worktree.manager.test.js.map +1 -0
  15. package/dist/mcp/http-client.d.ts.map +1 -1
  16. package/dist/mcp/http-client.js +8 -1
  17. package/dist/mcp/http-client.js.map +1 -1
  18. package/dist/middleware/tunnel-auth.d.ts.map +1 -1
  19. package/dist/middleware/tunnel-auth.js +2 -0
  20. package/dist/middleware/tunnel-auth.js.map +1 -1
  21. package/dist/output/__tests__/codex-parser.test.d.ts +2 -0
  22. package/dist/output/__tests__/codex-parser.test.d.ts.map +1 -0
  23. package/dist/output/__tests__/codex-parser.test.js +148 -0
  24. package/dist/output/__tests__/codex-parser.test.js.map +1 -0
  25. package/dist/output/codex-parser.d.ts +12 -0
  26. package/dist/output/codex-parser.d.ts.map +1 -1
  27. package/dist/output/codex-parser.js +129 -12
  28. package/dist/output/codex-parser.js.map +1 -1
  29. package/dist/routes/__tests__/attachments.test.d.ts +2 -0
  30. package/dist/routes/__tests__/attachments.test.d.ts.map +1 -0
  31. package/dist/routes/__tests__/attachments.test.js +86 -0
  32. package/dist/routes/__tests__/attachments.test.js.map +1 -0
  33. package/dist/routes/__tests__/filesystem.test.d.ts +2 -0
  34. package/dist/routes/__tests__/filesystem.test.d.ts.map +1 -0
  35. package/dist/routes/__tests__/filesystem.test.js +80 -0
  36. package/dist/routes/__tests__/filesystem.test.js.map +1 -0
  37. package/dist/routes/__tests__/previews.test.d.ts +2 -0
  38. package/dist/routes/__tests__/previews.test.d.ts.map +1 -0
  39. package/dist/routes/__tests__/previews.test.js +89 -0
  40. package/dist/routes/__tests__/previews.test.js.map +1 -0
  41. package/dist/routes/__tests__/tasks.test.d.ts +2 -0
  42. package/dist/routes/__tests__/tasks.test.d.ts.map +1 -0
  43. package/dist/routes/__tests__/tasks.test.js +72 -0
  44. package/dist/routes/__tests__/tasks.test.js.map +1 -0
  45. package/dist/routes/attachments.d.ts.map +1 -1
  46. package/dist/routes/attachments.js +36 -16
  47. package/dist/routes/attachments.js.map +1 -1
  48. package/dist/routes/filesystem.d.ts.map +1 -1
  49. package/dist/routes/filesystem.js +24 -3
  50. package/dist/routes/filesystem.js.map +1 -1
  51. package/dist/routes/index.d.ts.map +1 -1
  52. package/dist/routes/index.js +3 -0
  53. package/dist/routes/index.js.map +1 -1
  54. package/dist/routes/previews.d.ts +6 -0
  55. package/dist/routes/previews.d.ts.map +1 -0
  56. package/dist/routes/previews.js +413 -0
  57. package/dist/routes/previews.js.map +1 -0
  58. package/dist/routes/projects.d.ts.map +1 -1
  59. package/dist/routes/projects.js +1 -0
  60. package/dist/routes/projects.js.map +1 -1
  61. package/dist/routes/tasks.js +2 -2
  62. package/dist/routes/tasks.js.map +1 -1
  63. package/dist/routes/team-runs.d.ts.map +1 -1
  64. package/dist/routes/team-runs.js +18 -8
  65. package/dist/routes/team-runs.js.map +1 -1
  66. package/dist/routes/tunnel.d.ts.map +1 -1
  67. package/dist/routes/tunnel.js +20 -0
  68. package/dist/routes/tunnel.js.map +1 -1
  69. package/dist/routes/workspaces.d.ts.map +1 -1
  70. package/dist/routes/workspaces.js +15 -1
  71. package/dist/routes/workspaces.js.map +1 -1
  72. package/dist/services/__tests__/preview.service.test.d.ts +2 -0
  73. package/dist/services/__tests__/preview.service.test.d.ts.map +1 -0
  74. package/dist/services/__tests__/preview.service.test.js +29 -0
  75. package/dist/services/__tests__/preview.service.test.js.map +1 -0
  76. package/dist/services/__tests__/task.service.test.d.ts +2 -0
  77. package/dist/services/__tests__/task.service.test.d.ts.map +1 -0
  78. package/dist/services/__tests__/task.service.test.js +65 -0
  79. package/dist/services/__tests__/task.service.test.js.map +1 -0
  80. package/dist/services/__tests__/team-reconciler.service.test.js +525 -27
  81. package/dist/services/__tests__/team-reconciler.service.test.js.map +1 -1
  82. package/dist/services/__tests__/team-run.service.test.js +252 -0
  83. package/dist/services/__tests__/team-run.service.test.js.map +1 -1
  84. package/dist/services/__tests__/team-scheduler.service.test.js +551 -21
  85. package/dist/services/__tests__/team-scheduler.service.test.js.map +1 -1
  86. package/dist/services/__tests__/tunnel.service.test.d.ts +2 -0
  87. package/dist/services/__tests__/tunnel.service.test.d.ts.map +1 -0
  88. package/dist/services/__tests__/tunnel.service.test.js +138 -0
  89. package/dist/services/__tests__/tunnel.service.test.js.map +1 -0
  90. package/dist/services/__tests__/workspace.service.test.d.ts +2 -0
  91. package/dist/services/__tests__/workspace.service.test.d.ts.map +1 -0
  92. package/dist/services/__tests__/workspace.service.test.js +695 -0
  93. package/dist/services/__tests__/workspace.service.test.js.map +1 -0
  94. package/dist/services/attachment-context.d.ts +3 -0
  95. package/dist/services/attachment-context.d.ts.map +1 -0
  96. package/dist/services/attachment-context.js +34 -0
  97. package/dist/services/attachment-context.js.map +1 -0
  98. package/dist/services/preview.service.d.ts +19 -0
  99. package/dist/services/preview.service.d.ts.map +1 -0
  100. package/dist/services/preview.service.js +147 -0
  101. package/dist/services/preview.service.js.map +1 -0
  102. package/dist/services/project.service.d.ts +2 -0
  103. package/dist/services/project.service.d.ts.map +1 -1
  104. package/dist/services/project.service.js +87 -18
  105. package/dist/services/project.service.js.map +1 -1
  106. package/dist/services/session-manager.d.ts +12 -0
  107. package/dist/services/session-manager.d.ts.map +1 -1
  108. package/dist/services/task.service.d.ts +6 -0
  109. package/dist/services/task.service.d.ts.map +1 -1
  110. package/dist/services/task.service.js +15 -3
  111. package/dist/services/task.service.js.map +1 -1
  112. package/dist/services/team-lock.service.d.ts +3 -0
  113. package/dist/services/team-lock.service.d.ts.map +1 -1
  114. package/dist/services/team-lock.service.js +11 -0
  115. package/dist/services/team-lock.service.js.map +1 -1
  116. package/dist/services/team-run.service.d.ts +3 -0
  117. package/dist/services/team-run.service.d.ts.map +1 -1
  118. package/dist/services/team-run.service.js +288 -30
  119. package/dist/services/team-run.service.js.map +1 -1
  120. package/dist/services/team-scheduler.service.d.ts +15 -0
  121. package/dist/services/team-scheduler.service.d.ts.map +1 -1
  122. package/dist/services/team-scheduler.service.js +125 -32
  123. package/dist/services/team-scheduler.service.js.map +1 -1
  124. package/dist/services/tunnel.service.d.ts +31 -5
  125. package/dist/services/tunnel.service.d.ts.map +1 -1
  126. package/dist/services/tunnel.service.js +293 -32
  127. package/dist/services/tunnel.service.js.map +1 -1
  128. package/dist/services/workspace.service.d.ts +161 -7
  129. package/dist/services/workspace.service.d.ts.map +1 -1
  130. package/dist/services/workspace.service.js +396 -51
  131. package/dist/services/workspace.service.js.map +1 -1
  132. package/dist/web/assets/{AgentDemoPage-p9YI4_l4.js → AgentDemoPage-Bf6labVB.js} +1 -1
  133. package/dist/web/assets/{DemoPage-B5DTSEbS.js → DemoPage-DlfG47rV.js} +1 -1
  134. package/dist/web/assets/{GeneralSettingsPage-Cspr7Vol.js → GeneralSettingsPage-DefqwzVn.js} +1 -1
  135. package/dist/web/assets/MemberAvatar-DVw_TedB.js +1 -0
  136. package/dist/web/assets/NotificationSettingsPage-C9h1U1Za.js +1 -0
  137. package/dist/web/assets/{ProfileSettingsPage-CNugU40a.js → ProfileSettingsPage-BkZE2yVP.js} +1 -1
  138. package/dist/web/assets/ProjectKanbanPage-B1Ckl1uY.js +89 -0
  139. package/dist/web/assets/ProjectSettingsPage-ByZ13awb.js +2 -0
  140. package/dist/web/assets/{ProviderSettingsPage-D_KWkgRM.js → ProviderSettingsPage-DSQYe8B6.js} +12 -12
  141. package/dist/web/assets/TeamSettingsPage-DUukJ_Ih.js +1 -0
  142. package/dist/web/assets/agent-tower-logo-COx9gy77.png +0 -0
  143. package/dist/web/assets/{button-B6JaSbDB.js → button-Bpm98eOV.js} +1 -1
  144. package/dist/web/assets/{chevron-down-CACy4UFq.js → chevron-down-DSKKXCi8.js} +1 -1
  145. package/dist/web/assets/{chevron-right-DFWfnDJY.js → chevron-right-CZdDV9GU.js} +1 -1
  146. package/dist/web/assets/{chevron-up-CGlf6jzw.js → chevron-up-gnnlwvYe.js} +1 -1
  147. package/dist/web/assets/{circle-check-DMK8auwb.js → circle-check-DeD_VuLK.js} +1 -1
  148. package/dist/web/assets/{code-block-OCS4YCEC-Hn75KHRK.js → code-block-OCS4YCEC-BrGjkdjS.js} +1 -1
  149. package/dist/web/assets/{confirm-dialog-DHI2f7Ni.js → confirm-dialog-CEVVvAcE.js} +1 -1
  150. package/dist/web/assets/folder-picker-ZBQlFEWL.js +1 -0
  151. package/dist/web/assets/{index-BFAA3PTl.js → index-B5g4V0NU.js} +6 -6
  152. package/dist/web/assets/index-ltjI8o6A.css +1 -0
  153. package/dist/web/assets/loader-circle-GMfBClX0.js +1 -0
  154. package/dist/web/assets/{log-adapter-CeKrvZcz.js → log-adapter-DKKM3sxS.js} +1 -1
  155. package/dist/web/assets/{mermaid-NOHMQCX5-DJFgrXPd.js → mermaid-NOHMQCX5-D5USvUiZ.js} +4 -4
  156. package/dist/web/assets/{modal-B5IRN7QI.js → modal-JMpuh-LG.js} +1 -1
  157. package/dist/web/assets/{pencil-CJY6Ahn7.js → pencil-QrCW47nn.js} +1 -1
  158. package/dist/web/assets/{select-BPZZlla1.js → select-CINRzLiE.js} +1 -1
  159. package/dist/web/assets/upload-vFxZxKHo.js +1 -0
  160. package/dist/web/assets/{use-profiles-C2k04ICZ.js → use-profiles-SrVWPYv0.js} +1 -1
  161. package/dist/web/assets/{use-providers-C7fIDWzP.js → use-providers-BihMydl0.js} +1 -1
  162. package/dist/web/avatars/presets/avatar-preset-01-developer.png +0 -0
  163. package/dist/web/avatars/presets/avatar-preset-02-architect.png +0 -0
  164. package/dist/web/avatars/presets/avatar-preset-03-tester.png +0 -0
  165. package/dist/web/avatars/presets/avatar-preset-04-devops.png +0 -0
  166. package/dist/web/avatars/presets/avatar-preset-05-data-scientist.png +0 -0
  167. package/dist/web/avatars/presets/avatar-preset-06-frontend.png +0 -0
  168. package/dist/web/avatars/presets/avatar-preset-07-backend.png +0 -0
  169. package/dist/web/avatars/presets/avatar-preset-08-security.png +0 -0
  170. package/dist/web/avatars/presets/avatar-preset-09-project-manager.png +0 -0
  171. package/dist/web/avatars/presets/avatar-preset-10-product-manager.png +0 -0
  172. package/dist/web/avatars/presets/avatar-preset-11-scrum-master.png +0 -0
  173. package/dist/web/avatars/presets/avatar-preset-12-tech-lead.png +0 -0
  174. package/dist/web/avatars/presets/avatar-preset-13-coordinator.png +0 -0
  175. package/dist/web/avatars/presets/avatar-preset-14-mentor.png +0 -0
  176. package/dist/web/avatars/presets/avatar-preset-15-reviewer.png +0 -0
  177. package/dist/web/avatars/presets/avatar-preset-16-ui-designer.png +0 -0
  178. package/dist/web/avatars/presets/avatar-preset-17-ux-researcher.png +0 -0
  179. package/dist/web/avatars/presets/avatar-preset-18-documenter.png +0 -0
  180. package/dist/web/avatars/presets/avatar-preset-19-translator.png +0 -0
  181. package/dist/web/avatars/presets/avatar-preset-20-analyst.png +0 -0
  182. package/dist/web/avatars/presets/avatar-preset-21-consultant.png +0 -0
  183. package/dist/web/avatars/presets/avatar-preset-22-creative-director.png +0 -0
  184. package/dist/web/avatars/presets/avatar-preset-23-support.png +0 -0
  185. package/dist/web/avatars/presets/avatar-preset-24-assistant.png +0 -0
  186. package/dist/web/avatars/presets/avatar-preset-25-robot.png +0 -0
  187. package/dist/web/avatars/presets/avatar-preset-grid.png +0 -0
  188. package/dist/web/index.html +2 -2
  189. package/node_modules/@agent-tower/shared/dist/types.d.ts +9 -1
  190. package/node_modules/@agent-tower/shared/dist/types.d.ts.map +1 -1
  191. package/node_modules/@agent-tower/shared/dist/types.js.map +1 -1
  192. package/node_modules/@prisma/client/.prisma/client/edge.js +9 -5
  193. package/node_modules/@prisma/client/.prisma/client/index-browser.js +4 -0
  194. package/node_modules/@prisma/client/.prisma/client/index.d.ts +1242 -14
  195. package/node_modules/@prisma/client/.prisma/client/index.js +9 -5
  196. package/node_modules/@prisma/client/.prisma/client/package.json +1 -1
  197. package/node_modules/@prisma/client/.prisma/client/schema.prisma +47 -31
  198. package/node_modules/@prisma/client/.prisma/client/wasm.js +4 -0
  199. package/package.json +1 -1
  200. package/prisma/migrations/20260515000000_add_workspace_preview_target/migration.sql +2 -0
  201. package/prisma/migrations/20260526000000_add_team_run_main_and_dedicated_workspaces/migration.sql +21 -0
  202. package/prisma/schema.prisma +27 -11
  203. package/dist/web/assets/NotificationSettingsPage-C9VfrRr-.js +0 -1
  204. package/dist/web/assets/ProjectKanbanPage-CkGNuqxq.js +0 -87
  205. package/dist/web/assets/ProjectSettingsPage-f1dg0XMf.js +0 -2
  206. package/dist/web/assets/TeamSettingsPage-B6WciZyi.js +0 -1
  207. package/dist/web/assets/circle-alert-BSAUEd9O.js +0 -1
  208. package/dist/web/assets/folder-picker-CtQkbWfa.js +0 -1
  209. package/dist/web/assets/index-mBCb67dB.css +0 -1
  210. package/dist/web/assets/loader-circle-CkDnf8ST.js +0 -1
  211. package/dist/web/assets/use-projects-BxuE-ulT.js +0 -1
@@ -194,6 +194,22 @@ async function createFixture(options = {}) {
194
194
  }
195
195
  return { project, task, workspace, teamRun, members };
196
196
  }
197
+ async function createMemberPreset(options = {}) {
198
+ const name = options.name ?? 'Leader';
199
+ return prisma.memberPreset.create({
200
+ data: {
201
+ name,
202
+ aliases: stringifyJson([name.toLowerCase()]),
203
+ providerId: `provider-${name.toLowerCase()}`,
204
+ rolePrompt: `${name} role`,
205
+ capabilities: stringifyJson(capabilities),
206
+ workspacePolicy: 'shared',
207
+ triggerPolicy: options.triggerPolicy ?? 'USER_MESSAGES',
208
+ sessionPolicy: 'new_per_request',
209
+ avatar: null,
210
+ },
211
+ });
212
+ }
197
213
  async function createWorkRequest(options) {
198
214
  return prisma.workRequest.create({
199
215
  data: {
@@ -279,6 +295,9 @@ describe('TeamReconcilerService', () => {
279
295
  await prisma.roomMessage.deleteMany();
280
296
  await prisma.teamMember.deleteMany();
281
297
  await prisma.teamRun.deleteMany();
298
+ await prisma.teamTemplateMember.deleteMany();
299
+ await prisma.teamTemplate.deleteMany();
300
+ await prisma.memberPreset.deleteMany();
282
301
  await prisma.session.deleteMany();
283
302
  await prisma.workspace.deleteMany();
284
303
  await prisma.task.deleteMany();
@@ -588,16 +607,30 @@ describe('TeamReconcilerService', () => {
588
607
  senderId: members[0].id,
589
608
  senderInvocationId: invocation.id,
590
609
  });
591
- await expect(prisma.workRequest.findFirst({
592
- where: {
593
- teamRunId: teamRun.id,
594
- triggerMessageId: message.id,
595
- targetMemberId: members[1].id,
596
- },
597
- })).resolves.toMatchObject({
598
- requesterType: 'agent',
599
- requesterMemberId: members[0].id,
600
- status: 'QUEUED',
610
+ let createdRequestId = '';
611
+ await vi.waitFor(async () => {
612
+ const createdRequest = await prisma.workRequest.findFirst({
613
+ where: {
614
+ teamRunId: teamRun.id,
615
+ triggerMessageId: message.id,
616
+ targetMemberId: members[1].id,
617
+ },
618
+ });
619
+ expect(createdRequest).toMatchObject({
620
+ requesterType: 'agent',
621
+ requesterMemberId: members[0].id,
622
+ status: 'STARTED',
623
+ });
624
+ createdRequestId = createdRequest.id;
625
+ });
626
+ await vi.waitFor(async () => {
627
+ await expect(prisma.agentInvocation.findFirst({
628
+ where: { workRequestId: createdRequestId },
629
+ })).resolves.toMatchObject({
630
+ memberId: members[1].id,
631
+ sessionId: null,
632
+ status: 'FAILED',
633
+ });
601
634
  });
602
635
  }
603
636
  finally {
@@ -693,16 +726,30 @@ describe('TeamReconcilerService', () => {
693
726
  senderId: members[0].id,
694
727
  senderInvocationId: invocation.id,
695
728
  });
696
- await expect(prisma.workRequest.findFirst({
697
- where: {
698
- teamRunId: teamRun.id,
699
- triggerMessageId: message.id,
700
- targetMemberId: members[1].id,
701
- },
702
- })).resolves.toMatchObject({
703
- requesterType: 'agent',
704
- requesterMemberId: members[0].id,
705
- status: 'QUEUED',
729
+ let createdRequestId = '';
730
+ await vi.waitFor(async () => {
731
+ const createdRequest = await prisma.workRequest.findFirst({
732
+ where: {
733
+ teamRunId: teamRun.id,
734
+ triggerMessageId: message.id,
735
+ targetMemberId: members[1].id,
736
+ },
737
+ });
738
+ expect(createdRequest).toMatchObject({
739
+ requesterType: 'agent',
740
+ requesterMemberId: members[0].id,
741
+ status: 'STARTED',
742
+ });
743
+ createdRequestId = createdRequest.id;
744
+ });
745
+ await vi.waitFor(async () => {
746
+ await expect(prisma.agentInvocation.findFirst({
747
+ where: { workRequestId: createdRequestId },
748
+ })).resolves.toMatchObject({
749
+ memberId: members[1].id,
750
+ sessionId: null,
751
+ status: 'FAILED',
752
+ });
706
753
  });
707
754
  }
708
755
  finally {
@@ -736,7 +783,19 @@ describe('TeamReconcilerService', () => {
736
783
  it('lists TeamRun members through MCP without exposing role prompts', async () => {
737
784
  const previousTeamRunId = process.env.AGENT_TOWER_TEAM_RUN_ID;
738
785
  const previousMemberId = process.env.AGENT_TOWER_MEMBER_ID;
739
- const { teamRun, members } = await createFixture({ memberCount: 2 });
786
+ const { workspace, teamRun, members } = await createFixture({ memberCount: 2 });
787
+ const runningRequest = await createWorkRequest({
788
+ teamRunId: teamRun.id,
789
+ targetMemberId: members[0].id,
790
+ status: 'STARTED',
791
+ });
792
+ await createRunningInvocation({
793
+ teamRunId: teamRun.id,
794
+ workRequestId: runningRequest.id,
795
+ memberId: members[0].id,
796
+ workspaceId: workspace.id,
797
+ status: 'RUNNING',
798
+ });
740
799
  const app = Fastify({ logger: false });
741
800
  try {
742
801
  process.env.AGENT_TOWER_TEAM_RUN_ID = teamRun.id;
@@ -771,7 +830,7 @@ describe('TeamReconcilerService', () => {
771
830
  id: members[0].id,
772
831
  name: 'Member 1',
773
832
  aliases: ['member-1'],
774
- status: 'IDLE',
833
+ status: 'RUNNING',
775
834
  workspacePolicy: 'shared',
776
835
  triggerPolicy: 'MENTION_ONLY',
777
836
  sessionPolicy: 'new_per_request',
@@ -948,6 +1007,11 @@ describe('TeamReconcilerService', () => {
948
1007
  triggerPolicies: ['USER_MESSAGES', 'MENTION_ONLY'],
949
1008
  });
950
1009
  const routeScheduler = createRouteSchedulerMock();
1010
+ const startedTeamRunIds = [];
1011
+ routeScheduler.startNextSessions = vi.fn(async (teamRunId) => {
1012
+ startedTeamRunIds.push(teamRunId);
1013
+ return await new Promise(() => { });
1014
+ });
951
1015
  const app = Fastify({ logger: false });
952
1016
  try {
953
1017
  await app.register(teamRunRoutes, { prefix: '/api', scheduler: routeScheduler });
@@ -963,22 +1027,456 @@ describe('TeamReconcilerService', () => {
963
1027
  const message = response.json();
964
1028
  expect(message.mentions).toEqual([]);
965
1029
  expect(message.workRequestIds).toHaveLength(1);
966
- expect(routeScheduler.startNextSessions).toHaveBeenCalledWith(teamRun.id);
967
- expect(routeScheduler.startedTeamRunIds).toEqual([teamRun.id]);
1030
+ expect(routeScheduler.startNextSessions).not.toHaveBeenCalled();
968
1031
  await expect(prisma.workRequest.findUnique({ where: { id: message.workRequestIds[0] } })).resolves.toMatchObject({
969
1032
  targetMemberId: members[0].id,
970
1033
  instruction: '普通用户消息',
971
- status: 'STARTED',
1034
+ status: 'QUEUED',
972
1035
  });
973
1036
  await expect(prisma.agentInvocation.count({
974
1037
  where: {
975
1038
  teamRunId: teamRun.id,
976
1039
  memberId: members[0].id,
977
- status: 'RUNNING',
978
1040
  },
979
- })).resolves.toBe(1);
1041
+ })).resolves.toBe(0);
1042
+ await vi.waitFor(() => {
1043
+ expect(routeScheduler.startNextSessions).toHaveBeenCalledWith(teamRun.id);
1044
+ expect(startedTeamRunIds).toEqual([teamRun.id]);
1045
+ });
1046
+ }
1047
+ finally {
1048
+ await app.close();
1049
+ }
1050
+ });
1051
+ it('creates an initial Team Room message without WorkRequests for unmentioned MENTION_ONLY members', async () => {
1052
+ const project = await prisma.project.create({
1053
+ data: {
1054
+ name: 'Initial message project',
1055
+ repoPath: testDir,
1056
+ },
1057
+ });
1058
+ const task = await prisma.task.create({
1059
+ data: {
1060
+ title: 'Build CSV import',
1061
+ description: 'Parse uploaded files and show validation errors.',
1062
+ projectId: project.id,
1063
+ },
1064
+ });
1065
+ const implementerPreset = await createMemberPreset({ name: 'Implementer', triggerPolicy: 'MENTION_ONLY' });
1066
+ const reviewerPreset = await createMemberPreset({ name: 'Reviewer', triggerPolicy: 'MENTION_ONLY' });
1067
+ const routeScheduler = createRouteSchedulerMock();
1068
+ const app = Fastify({ logger: false });
1069
+ try {
1070
+ await app.register(teamRunRoutes, { prefix: '/api', scheduler: routeScheduler });
1071
+ const response = await app.inject({
1072
+ method: 'POST',
1073
+ url: `/api/tasks/${task.id}/team-runs`,
1074
+ payload: {
1075
+ mode: 'AUTO',
1076
+ memberPresetIds: [implementerPreset.id, reviewerPreset.id],
1077
+ },
1078
+ });
1079
+ expect(response.statusCode).toBe(201);
1080
+ const teamRun = response.json();
1081
+ const initialContent = 'Build CSV import\n\nParse uploaded files and show validation errors.';
1082
+ expect(teamRun.messages).toHaveLength(1);
1083
+ expect(teamRun.messages[0]).toMatchObject({
1084
+ senderType: 'user',
1085
+ kind: 'chat',
1086
+ content: initialContent,
1087
+ mentions: [],
1088
+ workRequestIds: [],
1089
+ });
1090
+ expect(teamRun.workRequests).toEqual([]);
1091
+ const messagesResponse = await app.inject({
1092
+ method: 'GET',
1093
+ url: `/api/team-runs/${teamRun.id}/messages`,
1094
+ });
1095
+ expect(messagesResponse.statusCode).toBe(200);
1096
+ const messages = messagesResponse.json();
1097
+ expect(messages).toEqual([
1098
+ expect.objectContaining({
1099
+ id: teamRun.messages[0].id,
1100
+ content: initialContent,
1101
+ mentions: [],
1102
+ workRequestIds: [],
1103
+ }),
1104
+ ]);
1105
+ const workRequestsResponse = await app.inject({
1106
+ method: 'GET',
1107
+ url: `/api/team-runs/${teamRun.id}/work-requests`,
1108
+ });
1109
+ expect(workRequestsResponse.statusCode).toBe(200);
1110
+ const workRequests = workRequestsResponse.json();
1111
+ expect(workRequests).toEqual([]);
1112
+ await expect(prisma.roomMessage.count({ where: { teamRunId: teamRun.id } })).resolves.toBe(1);
1113
+ await expect(prisma.workRequest.count({ where: { teamRunId: teamRun.id } })).resolves.toBe(0);
1114
+ expect(routeScheduler.startNextSessions).not.toHaveBeenCalled();
1115
+ await expect(prisma.agentInvocation.count({ where: { teamRunId: teamRun.id } })).resolves.toBe(0);
1116
+ }
1117
+ finally {
1118
+ await app.close();
1119
+ }
1120
+ });
1121
+ it('creates USER_MESSAGES WorkRequests from a title-only initial message and auto-starts it', async () => {
1122
+ const project = await prisma.project.create({
1123
+ data: {
1124
+ name: 'Title-only project',
1125
+ repoPath: testDir,
1126
+ },
1127
+ });
1128
+ const task = await prisma.task.create({
1129
+ data: {
1130
+ title: 'Investigate slow dashboard',
1131
+ projectId: project.id,
1132
+ },
1133
+ });
1134
+ const preset = await createMemberPreset({ name: 'Leader', triggerPolicy: 'USER_MESSAGES' });
1135
+ const routeScheduler = createRouteSchedulerMock();
1136
+ const app = Fastify({ logger: false });
1137
+ try {
1138
+ await app.register(teamRunRoutes, { prefix: '/api', scheduler: routeScheduler });
1139
+ const response = await app.inject({
1140
+ method: 'POST',
1141
+ url: `/api/tasks/${task.id}/team-runs`,
1142
+ payload: {
1143
+ mode: 'AUTO',
1144
+ memberPresetIds: [preset.id],
1145
+ },
1146
+ });
1147
+ expect(response.statusCode).toBe(201);
1148
+ const teamRun = response.json();
1149
+ expect(teamRun.messages).toHaveLength(1);
1150
+ expect(teamRun.messages[0]).toMatchObject({
1151
+ content: 'Investigate slow dashboard',
1152
+ workRequestIds: [teamRun.workRequests[0].id],
1153
+ });
1154
+ expect(teamRun.workRequests[0]).toMatchObject({
1155
+ instruction: 'Investigate slow dashboard',
1156
+ status: 'QUEUED',
1157
+ });
1158
+ await vi.waitFor(() => {
1159
+ expect(routeScheduler.startNextSessions).toHaveBeenCalledWith(teamRun.id);
1160
+ });
1161
+ }
1162
+ finally {
1163
+ await app.close();
1164
+ }
1165
+ });
1166
+ it('creates an initial WorkRequest only for a mentioned MENTION_ONLY member', async () => {
1167
+ const project = await prisma.project.create({
1168
+ data: {
1169
+ name: 'Mentioned initial message project',
1170
+ repoPath: testDir,
1171
+ },
1172
+ });
1173
+ const task = await prisma.task.create({
1174
+ data: {
1175
+ title: 'Investigate import failures',
1176
+ description: 'Please @Reviewer check the error handling.',
1177
+ projectId: project.id,
1178
+ },
1179
+ });
1180
+ const implementerPreset = await createMemberPreset({ name: 'Implementer', triggerPolicy: 'MENTION_ONLY' });
1181
+ const reviewerPreset = await createMemberPreset({ name: 'Reviewer', triggerPolicy: 'MENTION_ONLY' });
1182
+ const routeScheduler = createRouteSchedulerMock();
1183
+ const app = Fastify({ logger: false });
1184
+ try {
1185
+ await app.register(teamRunRoutes, { prefix: '/api', scheduler: routeScheduler });
1186
+ const response = await app.inject({
1187
+ method: 'POST',
1188
+ url: `/api/tasks/${task.id}/team-runs`,
1189
+ payload: {
1190
+ mode: 'AUTO',
1191
+ memberPresetIds: [implementerPreset.id, reviewerPreset.id],
1192
+ },
1193
+ });
1194
+ expect(response.statusCode).toBe(201);
1195
+ const teamRun = response.json();
1196
+ const reviewer = teamRun.members.find((member) => member.name === 'Reviewer');
1197
+ const implementer = teamRun.members.find((member) => member.name === 'Implementer');
1198
+ const initialContent = 'Investigate import failures\n\nPlease @Reviewer check the error handling.';
1199
+ expect(reviewer).toBeDefined();
1200
+ expect(implementer).toBeDefined();
1201
+ expect(teamRun.messages).toHaveLength(1);
1202
+ expect(teamRun.messages[0]).toMatchObject({
1203
+ content: initialContent,
1204
+ mentions: [{ memberId: reviewer.id, label: 'Reviewer' }],
1205
+ });
1206
+ expect(teamRun.workRequests).toEqual([
1207
+ expect.objectContaining({
1208
+ id: teamRun.messages[0].workRequestIds[0],
1209
+ targetMemberId: reviewer.id,
1210
+ instruction: initialContent,
1211
+ status: 'QUEUED',
1212
+ }),
1213
+ ]);
1214
+ expect(teamRun.workRequests[0].targetMemberId).not.toBe(implementer.id);
1215
+ await vi.waitFor(() => {
1216
+ expect(routeScheduler.startNextSessions).toHaveBeenCalledWith(teamRun.id);
1217
+ });
1218
+ await expect(prisma.agentInvocation.count({ where: { teamRunId: teamRun.id } })).resolves.toBe(1);
1219
+ }
1220
+ finally {
1221
+ await app.close();
1222
+ }
1223
+ });
1224
+ it('matches initial @mention tokens exactly instead of matching name prefixes', async () => {
1225
+ const project = await prisma.project.create({
1226
+ data: {
1227
+ name: 'Mention prefix project',
1228
+ repoPath: testDir,
1229
+ },
1230
+ });
1231
+ const task = await prisma.task.create({
1232
+ data: {
1233
+ title: 'Route QA review',
1234
+ description: 'Please @QA-Lead verify the release notes.',
1235
+ projectId: project.id,
1236
+ },
1237
+ });
1238
+ const qaPreset = await createMemberPreset({ name: 'QA', triggerPolicy: 'MENTION_ONLY' });
1239
+ const qaLeadPreset = await createMemberPreset({ name: 'QA-Lead', triggerPolicy: 'MENTION_ONLY' });
1240
+ const routeScheduler = createRouteSchedulerMock();
1241
+ const app = Fastify({ logger: false });
1242
+ try {
1243
+ await app.register(teamRunRoutes, { prefix: '/api', scheduler: routeScheduler });
1244
+ const response = await app.inject({
1245
+ method: 'POST',
1246
+ url: `/api/tasks/${task.id}/team-runs`,
1247
+ payload: {
1248
+ mode: 'AUTO',
1249
+ memberPresetIds: [qaPreset.id, qaLeadPreset.id],
1250
+ },
1251
+ });
1252
+ expect(response.statusCode).toBe(201);
1253
+ const teamRun = response.json();
1254
+ const qa = teamRun.members.find((member) => member.name === 'QA');
1255
+ const qaLead = teamRun.members.find((member) => member.name === 'QA-Lead');
1256
+ expect(qa).toBeDefined();
1257
+ expect(qaLead).toBeDefined();
1258
+ expect(teamRun.messages[0].mentions).toEqual([{ memberId: qaLead.id, label: 'QA-Lead' }]);
1259
+ expect(teamRun.workRequests).toEqual([
1260
+ expect.objectContaining({
1261
+ id: teamRun.messages[0].workRequestIds[0],
1262
+ targetMemberId: qaLead.id,
1263
+ }),
1264
+ ]);
1265
+ expect(teamRun.workRequests[0].targetMemberId).not.toBe(qa.id);
1266
+ }
1267
+ finally {
1268
+ await app.close();
1269
+ }
1270
+ });
1271
+ it('does not create initial WorkRequests for ambiguous @mention aliases', async () => {
1272
+ const project = await prisma.project.create({
1273
+ data: {
1274
+ name: 'Ambiguous mention project',
1275
+ repoPath: testDir,
1276
+ },
1277
+ });
1278
+ const task = await prisma.task.create({
1279
+ data: {
1280
+ title: 'Review ambiguous owner',
1281
+ description: 'Please @reviewer check this.',
1282
+ projectId: project.id,
1283
+ },
1284
+ });
1285
+ const firstPreset = await createMemberPreset({ name: 'Frontend Reviewer', triggerPolicy: 'MENTION_ONLY' });
1286
+ const secondPreset = await createMemberPreset({ name: 'Backend Reviewer', triggerPolicy: 'MENTION_ONLY' });
1287
+ await prisma.memberPreset.updateMany({
1288
+ where: { id: { in: [firstPreset.id, secondPreset.id] } },
1289
+ data: { aliases: stringifyJson(['reviewer']) },
1290
+ });
1291
+ const routeScheduler = createRouteSchedulerMock();
1292
+ const app = Fastify({ logger: false });
1293
+ try {
1294
+ await app.register(teamRunRoutes, { prefix: '/api', scheduler: routeScheduler });
1295
+ const response = await app.inject({
1296
+ method: 'POST',
1297
+ url: `/api/tasks/${task.id}/team-runs`,
1298
+ payload: {
1299
+ mode: 'AUTO',
1300
+ memberPresetIds: [firstPreset.id, secondPreset.id],
1301
+ },
1302
+ });
1303
+ expect(response.statusCode).toBe(201);
1304
+ const teamRun = response.json();
1305
+ expect(teamRun.messages[0]).toMatchObject({
1306
+ mentions: [],
1307
+ workRequestIds: [],
1308
+ });
1309
+ expect(teamRun.workRequests).toEqual([]);
1310
+ await expect(prisma.workRequest.count({ where: { teamRunId: teamRun.id } })).resolves.toBe(0);
1311
+ expect(routeScheduler.startNextSessions).not.toHaveBeenCalled();
1312
+ }
1313
+ finally {
1314
+ await app.close();
1315
+ }
1316
+ });
1317
+ it('does not fall back to USER_MESSAGES when initial @mention aliases are ambiguous', async () => {
1318
+ const project = await prisma.project.create({
1319
+ data: {
1320
+ name: 'Mixed ambiguous mention project',
1321
+ repoPath: testDir,
1322
+ },
1323
+ });
1324
+ const task = await prisma.task.create({
1325
+ data: {
1326
+ title: 'Route ambiguous review',
1327
+ description: 'Please @reviewer decide who owns this.',
1328
+ projectId: project.id,
1329
+ },
1330
+ });
1331
+ const firstPreset = await createMemberPreset({ name: 'Frontend Reviewer', triggerPolicy: 'MENTION_ONLY' });
1332
+ const secondPreset = await createMemberPreset({ name: 'Backend Reviewer', triggerPolicy: 'MENTION_ONLY' });
1333
+ const observerPreset = await createMemberPreset({ name: 'Observer', triggerPolicy: 'USER_MESSAGES' });
1334
+ await prisma.memberPreset.updateMany({
1335
+ where: { id: { in: [firstPreset.id, secondPreset.id] } },
1336
+ data: { aliases: stringifyJson(['reviewer']) },
1337
+ });
1338
+ const routeScheduler = createRouteSchedulerMock();
1339
+ const app = Fastify({ logger: false });
1340
+ try {
1341
+ await app.register(teamRunRoutes, { prefix: '/api', scheduler: routeScheduler });
1342
+ const response = await app.inject({
1343
+ method: 'POST',
1344
+ url: `/api/tasks/${task.id}/team-runs`,
1345
+ payload: {
1346
+ mode: 'AUTO',
1347
+ memberPresetIds: [firstPreset.id, secondPreset.id, observerPreset.id],
1348
+ },
1349
+ });
1350
+ expect(response.statusCode).toBe(201);
1351
+ const teamRun = response.json();
1352
+ expect(teamRun.messages[0]).toMatchObject({
1353
+ mentions: [],
1354
+ workRequestIds: [],
1355
+ });
1356
+ expect(teamRun.workRequests).toEqual([]);
1357
+ await expect(prisma.workRequest.count({ where: { teamRunId: teamRun.id } })).resolves.toBe(0);
1358
+ expect(routeScheduler.startNextSessions).not.toHaveBeenCalled();
1359
+ }
1360
+ finally {
1361
+ await app.close();
1362
+ }
1363
+ });
1364
+ it('rejects creating a TeamRun for a blank-title task before writing TeamRun data', async () => {
1365
+ const project = await prisma.project.create({
1366
+ data: {
1367
+ name: 'Blank title project',
1368
+ repoPath: testDir,
1369
+ },
1370
+ });
1371
+ const task = await prisma.task.create({
1372
+ data: {
1373
+ title: ' ',
1374
+ projectId: project.id,
1375
+ },
1376
+ });
1377
+ const preset = await createMemberPreset({ name: 'Leader', triggerPolicy: 'USER_MESSAGES' });
1378
+ const routeScheduler = createRouteSchedulerMock();
1379
+ const app = Fastify({ logger: false });
1380
+ try {
1381
+ await app.register(teamRunRoutes, { prefix: '/api', scheduler: routeScheduler });
1382
+ const response = await app.inject({
1383
+ method: 'POST',
1384
+ url: `/api/tasks/${task.id}/team-runs`,
1385
+ payload: {
1386
+ mode: 'AUTO',
1387
+ memberPresetIds: [preset.id],
1388
+ },
1389
+ });
1390
+ expect(response.statusCode).toBe(400);
1391
+ expect(response.json()).toMatchObject({ code: 'VALIDATION_ERROR' });
1392
+ expect(routeScheduler.startNextSessions).not.toHaveBeenCalled();
1393
+ await expect(prisma.teamRun.count({ where: { taskId: task.id } })).resolves.toBe(0);
1394
+ await expect(prisma.roomMessage.count()).resolves.toBe(0);
1395
+ await expect(prisma.workRequest.count()).resolves.toBe(0);
1396
+ }
1397
+ finally {
1398
+ await app.close();
1399
+ }
1400
+ });
1401
+ it('rolls back TeamRun creation when initial WorkRequest creation fails and allows retry', async () => {
1402
+ const project = await prisma.project.create({
1403
+ data: {
1404
+ name: 'Atomic TeamRun project',
1405
+ repoPath: testDir,
1406
+ },
1407
+ });
1408
+ const task = await prisma.task.create({
1409
+ data: {
1410
+ title: 'Start atomically',
1411
+ projectId: project.id,
1412
+ },
1413
+ });
1414
+ const preset = await createMemberPreset({ name: 'Implementer', triggerPolicy: 'USER_MESSAGES' });
1415
+ const routeScheduler = createRouteSchedulerMock();
1416
+ const originalTransaction = prisma.$transaction.bind(prisma);
1417
+ const transactionSpy = vi.spyOn(prisma, '$transaction');
1418
+ transactionSpy.mockImplementationOnce(async (arg, ...rest) => {
1419
+ if (typeof arg !== 'function') {
1420
+ return originalTransaction(arg, ...rest);
1421
+ }
1422
+ return originalTransaction(async (tx) => {
1423
+ const failingTx = new Proxy(tx, {
1424
+ get(target, property, receiver) {
1425
+ if (property !== 'workRequest') {
1426
+ return Reflect.get(target, property, receiver);
1427
+ }
1428
+ const delegate = Reflect.get(target, property, receiver);
1429
+ return new Proxy(delegate, {
1430
+ get(delegateTarget, delegateProperty, delegateReceiver) {
1431
+ if (delegateProperty === 'create') {
1432
+ return async () => {
1433
+ throw new Error('injected initial WorkRequest failure');
1434
+ };
1435
+ }
1436
+ return Reflect.get(delegateTarget, delegateProperty, delegateReceiver);
1437
+ },
1438
+ });
1439
+ },
1440
+ });
1441
+ return arg(failingTx);
1442
+ }, ...rest);
1443
+ });
1444
+ const app = Fastify({ logger: false });
1445
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
1446
+ try {
1447
+ await app.register(teamRunRoutes, { prefix: '/api', scheduler: routeScheduler });
1448
+ const failedResponse = await app.inject({
1449
+ method: 'POST',
1450
+ url: `/api/tasks/${task.id}/team-runs`,
1451
+ payload: {
1452
+ mode: 'AUTO',
1453
+ memberPresetIds: [preset.id],
1454
+ },
1455
+ });
1456
+ expect(failedResponse.statusCode).toBe(500);
1457
+ expect(routeScheduler.startNextSessions).not.toHaveBeenCalled();
1458
+ await expect(prisma.teamRun.count({ where: { taskId: task.id } })).resolves.toBe(0);
1459
+ await expect(prisma.teamMember.count()).resolves.toBe(0);
1460
+ await expect(prisma.roomMessage.count()).resolves.toBe(0);
1461
+ await expect(prisma.workRequest.count()).resolves.toBe(0);
1462
+ const retryResponse = await app.inject({
1463
+ method: 'POST',
1464
+ url: `/api/tasks/${task.id}/team-runs`,
1465
+ payload: {
1466
+ mode: 'AUTO',
1467
+ memberPresetIds: [preset.id],
1468
+ },
1469
+ });
1470
+ expect(retryResponse.statusCode).toBe(201);
1471
+ const teamRun = retryResponse.json();
1472
+ expect(teamRun.messages[0].workRequestIds).toEqual([teamRun.workRequests[0].id]);
1473
+ expect(teamRun.workRequests[0]).toMatchObject({ status: 'QUEUED' });
1474
+ await vi.waitFor(() => {
1475
+ expect(routeScheduler.startNextSessions).toHaveBeenCalledWith(teamRun.id);
1476
+ });
980
1477
  }
981
1478
  finally {
1479
+ consoleErrorSpy.mockRestore();
982
1480
  await app.close();
983
1481
  }
984
1482
  });