agent-tower 0.4.16-beta.0 → 0.4.16-beta.4
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.
- package/dist/app.test.js +2 -0
- package/dist/app.test.js.map +1 -1
- package/dist/git/git-cli.d.ts +18 -1
- package/dist/git/git-cli.d.ts.map +1 -1
- package/dist/git/git-cli.js +17 -1
- package/dist/git/git-cli.js.map +1 -1
- package/dist/git/worktree.manager.d.ts +29 -2
- package/dist/git/worktree.manager.d.ts.map +1 -1
- package/dist/git/worktree.manager.js +137 -16
- package/dist/git/worktree.manager.js.map +1 -1
- package/dist/git/worktree.manager.test.d.ts +2 -0
- package/dist/git/worktree.manager.test.d.ts.map +1 -0
- package/dist/git/worktree.manager.test.js +104 -0
- package/dist/git/worktree.manager.test.js.map +1 -0
- package/dist/mcp/http-client.d.ts +5 -1
- package/dist/mcp/http-client.d.ts.map +1 -1
- package/dist/mcp/http-client.js +13 -3
- package/dist/mcp/http-client.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +33 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/middleware/tunnel-auth.d.ts.map +1 -1
- package/dist/middleware/tunnel-auth.js +2 -0
- package/dist/middleware/tunnel-auth.js.map +1 -1
- package/dist/output/__tests__/codex-parser.test.d.ts +2 -0
- package/dist/output/__tests__/codex-parser.test.d.ts.map +1 -0
- package/dist/output/__tests__/codex-parser.test.js +148 -0
- package/dist/output/__tests__/codex-parser.test.js.map +1 -0
- package/dist/output/codex-parser.d.ts +12 -0
- package/dist/output/codex-parser.d.ts.map +1 -1
- package/dist/output/codex-parser.js +129 -12
- package/dist/output/codex-parser.js.map +1 -1
- package/dist/routes/__tests__/attachments.test.d.ts +2 -0
- package/dist/routes/__tests__/attachments.test.d.ts.map +1 -0
- package/dist/routes/__tests__/attachments.test.js +86 -0
- package/dist/routes/__tests__/attachments.test.js.map +1 -0
- package/dist/routes/__tests__/filesystem.test.d.ts +2 -0
- package/dist/routes/__tests__/filesystem.test.d.ts.map +1 -0
- package/dist/routes/__tests__/filesystem.test.js +80 -0
- package/dist/routes/__tests__/filesystem.test.js.map +1 -0
- package/dist/routes/__tests__/previews.test.d.ts +2 -0
- package/dist/routes/__tests__/previews.test.d.ts.map +1 -0
- package/dist/routes/__tests__/previews.test.js +89 -0
- package/dist/routes/__tests__/previews.test.js.map +1 -0
- package/dist/routes/__tests__/tasks.test.d.ts +2 -0
- package/dist/routes/__tests__/tasks.test.d.ts.map +1 -0
- package/dist/routes/__tests__/tasks.test.js +72 -0
- package/dist/routes/__tests__/tasks.test.js.map +1 -0
- package/dist/routes/attachments.d.ts.map +1 -1
- package/dist/routes/attachments.js +36 -16
- package/dist/routes/attachments.js.map +1 -1
- package/dist/routes/filesystem.d.ts.map +1 -1
- package/dist/routes/filesystem.js +24 -3
- package/dist/routes/filesystem.js.map +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +3 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/previews.d.ts +6 -0
- package/dist/routes/previews.d.ts.map +1 -0
- package/dist/routes/previews.js +413 -0
- package/dist/routes/previews.js.map +1 -0
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +1 -0
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/tasks.js +2 -2
- package/dist/routes/tasks.js.map +1 -1
- package/dist/routes/team-runs.d.ts.map +1 -1
- package/dist/routes/team-runs.js +36 -9
- package/dist/routes/team-runs.js.map +1 -1
- package/dist/routes/tunnel.d.ts.map +1 -1
- package/dist/routes/tunnel.js +20 -0
- package/dist/routes/tunnel.js.map +1 -1
- package/dist/routes/workspaces.d.ts.map +1 -1
- package/dist/routes/workspaces.js +15 -1
- package/dist/routes/workspaces.js.map +1 -1
- package/dist/services/__tests__/preview.service.test.d.ts +2 -0
- package/dist/services/__tests__/preview.service.test.d.ts.map +1 -0
- package/dist/services/__tests__/preview.service.test.js +29 -0
- package/dist/services/__tests__/preview.service.test.js.map +1 -0
- package/dist/services/__tests__/task.service.test.d.ts +2 -0
- package/dist/services/__tests__/task.service.test.d.ts.map +1 -0
- package/dist/services/__tests__/task.service.test.js +65 -0
- package/dist/services/__tests__/task.service.test.js.map +1 -0
- package/dist/services/__tests__/team-reconciler.service.test.js +720 -28
- package/dist/services/__tests__/team-reconciler.service.test.js.map +1 -1
- package/dist/services/__tests__/team-run.service.test.js +416 -0
- package/dist/services/__tests__/team-run.service.test.js.map +1 -1
- package/dist/services/__tests__/team-scheduler.service.test.js +680 -26
- package/dist/services/__tests__/team-scheduler.service.test.js.map +1 -1
- package/dist/services/__tests__/tunnel.service.test.d.ts +2 -0
- package/dist/services/__tests__/tunnel.service.test.d.ts.map +1 -0
- package/dist/services/__tests__/tunnel.service.test.js +138 -0
- package/dist/services/__tests__/tunnel.service.test.js.map +1 -0
- package/dist/services/__tests__/workspace.service.test.d.ts +2 -0
- package/dist/services/__tests__/workspace.service.test.d.ts.map +1 -0
- package/dist/services/__tests__/workspace.service.test.js +695 -0
- package/dist/services/__tests__/workspace.service.test.js.map +1 -0
- package/dist/services/attachment-context.d.ts +3 -0
- package/dist/services/attachment-context.d.ts.map +1 -0
- package/dist/services/attachment-context.js +34 -0
- package/dist/services/attachment-context.js.map +1 -0
- package/dist/services/preview.service.d.ts +19 -0
- package/dist/services/preview.service.d.ts.map +1 -0
- package/dist/services/preview.service.js +147 -0
- package/dist/services/preview.service.js.map +1 -0
- package/dist/services/project.service.d.ts +2 -0
- package/dist/services/project.service.d.ts.map +1 -1
- package/dist/services/project.service.js +87 -18
- package/dist/services/project.service.js.map +1 -1
- package/dist/services/session-manager.d.ts +12 -0
- package/dist/services/session-manager.d.ts.map +1 -1
- package/dist/services/task.service.d.ts +6 -0
- package/dist/services/task.service.d.ts.map +1 -1
- package/dist/services/task.service.js +15 -3
- package/dist/services/task.service.js.map +1 -1
- package/dist/services/team-lock.service.d.ts +3 -0
- package/dist/services/team-lock.service.d.ts.map +1 -1
- package/dist/services/team-lock.service.js +11 -0
- package/dist/services/team-lock.service.js.map +1 -1
- package/dist/services/team-run.service.d.ts +34 -1
- package/dist/services/team-run.service.d.ts.map +1 -1
- package/dist/services/team-run.service.js +370 -30
- package/dist/services/team-run.service.js.map +1 -1
- package/dist/services/team-scheduler.service.d.ts +22 -1
- package/dist/services/team-scheduler.service.d.ts.map +1 -1
- package/dist/services/team-scheduler.service.js +148 -33
- package/dist/services/team-scheduler.service.js.map +1 -1
- package/dist/services/tunnel.service.d.ts +31 -5
- package/dist/services/tunnel.service.d.ts.map +1 -1
- package/dist/services/tunnel.service.js +293 -32
- package/dist/services/tunnel.service.js.map +1 -1
- package/dist/services/workspace.service.d.ts +161 -7
- package/dist/services/workspace.service.d.ts.map +1 -1
- package/dist/services/workspace.service.js +396 -51
- package/dist/services/workspace.service.js.map +1 -1
- package/dist/web/assets/{AgentDemoPage-p9YI4_l4.js → AgentDemoPage-BhDnxdmh.js} +1 -1
- package/dist/web/assets/{DemoPage-B5DTSEbS.js → DemoPage-CJBc0NZf.js} +1 -1
- package/dist/web/assets/{GeneralSettingsPage-Cspr7Vol.js → GeneralSettingsPage-CEjDPmtD.js} +1 -1
- package/dist/web/assets/MemberAvatar-DxRCLAoK.js +1 -0
- package/dist/web/assets/NotificationSettingsPage-i77lmSic.js +1 -0
- package/dist/web/assets/{ProfileSettingsPage-CNugU40a.js → ProfileSettingsPage-DcGLD5O-.js} +1 -1
- package/dist/web/assets/ProjectKanbanPage-CX-NY7hx.js +89 -0
- package/dist/web/assets/ProjectSettingsPage-BBfroQPA.js +2 -0
- package/dist/web/assets/{ProviderSettingsPage-D_KWkgRM.js → ProviderSettingsPage-BdxbO1E9.js} +12 -12
- package/dist/web/assets/TeamSettingsPage-CTWGO79W.js +1 -0
- package/dist/web/assets/agent-tower-logo-COx9gy77.png +0 -0
- package/dist/web/assets/{button-B6JaSbDB.js → button-RDdre_kF.js} +1 -1
- package/dist/web/assets/{chevron-down-CACy4UFq.js → chevron-down-Cfeapk0v.js} +1 -1
- package/dist/web/assets/{chevron-right-DFWfnDJY.js → chevron-right-BE6LVCii.js} +1 -1
- package/dist/web/assets/{chevron-up-CGlf6jzw.js → chevron-up-CEEz4jJv.js} +1 -1
- package/dist/web/assets/{circle-check-DMK8auwb.js → circle-check-0imI5gEL.js} +1 -1
- package/dist/web/assets/{code-block-OCS4YCEC-Hn75KHRK.js → code-block-OCS4YCEC-D4c9zDcq.js} +1 -1
- package/dist/web/assets/{confirm-dialog-DHI2f7Ni.js → confirm-dialog--HDqEa-R.js} +1 -1
- package/dist/web/assets/folder-picker-CKfogW-o.js +1 -0
- package/dist/web/assets/index-BHmOCKAn.css +1 -0
- package/dist/web/assets/{index-BFAA3PTl.js → index-DbGCpy8E.js} +9 -9
- package/dist/web/assets/loader-circle-BAUFMewp.js +1 -0
- package/dist/web/assets/{log-adapter-CeKrvZcz.js → log-adapter-DKKM3sxS.js} +1 -1
- package/dist/web/assets/{mermaid-NOHMQCX5-DJFgrXPd.js → mermaid-NOHMQCX5-BkaKG_2K.js} +4 -4
- package/dist/web/assets/{modal-B5IRN7QI.js → modal-3rdeMVPn.js} +1 -1
- package/dist/web/assets/{pencil-CJY6Ahn7.js → pencil-CqWv0WcO.js} +1 -1
- package/dist/web/assets/{select-BPZZlla1.js → select-DHVfUr22.js} +1 -1
- package/dist/web/assets/upload-D3aqtSCY.js +1 -0
- package/dist/web/assets/{use-profiles-C2k04ICZ.js → use-profiles-BznmWvqM.js} +1 -1
- package/dist/web/assets/{use-providers-C7fIDWzP.js → use-providers-DIHUIuEZ.js} +1 -1
- package/dist/web/avatars/presets/avatar-preset-01-developer.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-02-architect.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-03-tester.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-04-devops.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-05-data-scientist.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-06-frontend.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-07-backend.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-08-security.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-09-project-manager.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-10-product-manager.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-11-scrum-master.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-12-tech-lead.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-13-coordinator.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-14-mentor.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-15-reviewer.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-16-ui-designer.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-17-ux-researcher.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-18-documenter.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-19-translator.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-20-analyst.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-21-consultant.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-22-creative-director.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-23-support.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-24-assistant.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-25-robot.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-grid.png +0 -0
- package/dist/web/index.html +2 -2
- package/node_modules/@agent-tower/shared/dist/types.d.ts +13 -1
- package/node_modules/@agent-tower/shared/dist/types.d.ts.map +1 -1
- package/node_modules/@agent-tower/shared/dist/types.js.map +1 -1
- package/node_modules/@prisma/client/.prisma/client/edge.js +11 -5
- package/node_modules/@prisma/client/.prisma/client/index-browser.js +6 -0
- package/node_modules/@prisma/client/.prisma/client/index.d.ts +1309 -14
- package/node_modules/@prisma/client/.prisma/client/index.js +11 -5
- package/node_modules/@prisma/client/.prisma/client/package.json +1 -1
- package/node_modules/@prisma/client/.prisma/client/schema.prisma +70 -52
- package/node_modules/@prisma/client/.prisma/client/wasm.js +6 -0
- package/package.json +1 -1
- package/prisma/migrations/20260515000000_add_workspace_preview_target/migration.sql +2 -0
- package/prisma/migrations/20260526000000_add_team_run_main_and_dedicated_workspaces/migration.sql +21 -0
- package/prisma/migrations/20260529000000_add_team_member_queue_management_policy/migration.sql +2 -0
- package/prisma/schema.prisma +29 -11
- package/dist/web/assets/NotificationSettingsPage-C9VfrRr-.js +0 -1
- package/dist/web/assets/ProjectKanbanPage-CkGNuqxq.js +0 -87
- package/dist/web/assets/ProjectSettingsPage-f1dg0XMf.js +0 -2
- package/dist/web/assets/TeamSettingsPage-B6WciZyi.js +0 -1
- package/dist/web/assets/circle-alert-BSAUEd9O.js +0 -1
- package/dist/web/assets/folder-picker-CtQkbWfa.js +0 -1
- package/dist/web/assets/index-mBCb67dB.css +0 -1
- package/dist/web/assets/loader-circle-CkDnf8ST.js +0 -1
- package/dist/web/assets/use-projects-BxuE-ulT.js +0 -1
|
@@ -134,7 +134,7 @@ function createRouteSchedulerMock() {
|
|
|
134
134
|
where: { id: workRequestId },
|
|
135
135
|
data: { status: 'REJECTED' },
|
|
136
136
|
}).then(asWorkRequest));
|
|
137
|
-
scheduler.cancelWorkRequest = vi.fn(async (workRequestId) => prisma.workRequest.update({
|
|
137
|
+
scheduler.cancelWorkRequest = vi.fn(async (workRequestId, _options) => prisma.workRequest.update({
|
|
138
138
|
where: { id: workRequestId },
|
|
139
139
|
data: { status: 'CANCELLED' },
|
|
140
140
|
}).then(asWorkRequest));
|
|
@@ -188,12 +188,30 @@ async function createFixture(options = {}) {
|
|
|
188
188
|
workspacePolicy: 'shared',
|
|
189
189
|
triggerPolicy: options.triggerPolicies?.[index] ?? 'MENTION_ONLY',
|
|
190
190
|
sessionPolicy: 'new_per_request',
|
|
191
|
+
queueManagementPolicy: 'own_only',
|
|
191
192
|
avatar: null,
|
|
192
193
|
},
|
|
193
194
|
}));
|
|
194
195
|
}
|
|
195
196
|
return { project, task, workspace, teamRun, members };
|
|
196
197
|
}
|
|
198
|
+
async function createMemberPreset(options = {}) {
|
|
199
|
+
const name = options.name ?? 'Leader';
|
|
200
|
+
return prisma.memberPreset.create({
|
|
201
|
+
data: {
|
|
202
|
+
name,
|
|
203
|
+
aliases: stringifyJson([name.toLowerCase()]),
|
|
204
|
+
providerId: `provider-${name.toLowerCase()}`,
|
|
205
|
+
rolePrompt: `${name} role`,
|
|
206
|
+
capabilities: stringifyJson(capabilities),
|
|
207
|
+
workspacePolicy: 'shared',
|
|
208
|
+
triggerPolicy: options.triggerPolicy ?? 'USER_MESSAGES',
|
|
209
|
+
sessionPolicy: 'new_per_request',
|
|
210
|
+
queueManagementPolicy: 'own_only',
|
|
211
|
+
avatar: null,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
}
|
|
197
215
|
async function createWorkRequest(options) {
|
|
198
216
|
return prisma.workRequest.create({
|
|
199
217
|
data: {
|
|
@@ -279,6 +297,9 @@ describe('TeamReconcilerService', () => {
|
|
|
279
297
|
await prisma.roomMessage.deleteMany();
|
|
280
298
|
await prisma.teamMember.deleteMany();
|
|
281
299
|
await prisma.teamRun.deleteMany();
|
|
300
|
+
await prisma.teamTemplateMember.deleteMany();
|
|
301
|
+
await prisma.teamTemplate.deleteMany();
|
|
302
|
+
await prisma.memberPreset.deleteMany();
|
|
282
303
|
await prisma.session.deleteMany();
|
|
283
304
|
await prisma.workspace.deleteMany();
|
|
284
305
|
await prisma.task.deleteMany();
|
|
@@ -588,16 +609,30 @@ describe('TeamReconcilerService', () => {
|
|
|
588
609
|
senderId: members[0].id,
|
|
589
610
|
senderInvocationId: invocation.id,
|
|
590
611
|
});
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
612
|
+
let createdRequestId = '';
|
|
613
|
+
await vi.waitFor(async () => {
|
|
614
|
+
const createdRequest = await prisma.workRequest.findFirst({
|
|
615
|
+
where: {
|
|
616
|
+
teamRunId: teamRun.id,
|
|
617
|
+
triggerMessageId: message.id,
|
|
618
|
+
targetMemberId: members[1].id,
|
|
619
|
+
},
|
|
620
|
+
});
|
|
621
|
+
expect(createdRequest).toMatchObject({
|
|
622
|
+
requesterType: 'agent',
|
|
623
|
+
requesterMemberId: members[0].id,
|
|
624
|
+
status: 'STARTED',
|
|
625
|
+
});
|
|
626
|
+
createdRequestId = createdRequest.id;
|
|
627
|
+
});
|
|
628
|
+
await vi.waitFor(async () => {
|
|
629
|
+
await expect(prisma.agentInvocation.findFirst({
|
|
630
|
+
where: { workRequestId: createdRequestId },
|
|
631
|
+
})).resolves.toMatchObject({
|
|
632
|
+
memberId: members[1].id,
|
|
633
|
+
sessionId: null,
|
|
634
|
+
status: 'FAILED',
|
|
635
|
+
});
|
|
601
636
|
});
|
|
602
637
|
}
|
|
603
638
|
finally {
|
|
@@ -693,16 +728,30 @@ describe('TeamReconcilerService', () => {
|
|
|
693
728
|
senderId: members[0].id,
|
|
694
729
|
senderInvocationId: invocation.id,
|
|
695
730
|
});
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
731
|
+
let createdRequestId = '';
|
|
732
|
+
await vi.waitFor(async () => {
|
|
733
|
+
const createdRequest = await prisma.workRequest.findFirst({
|
|
734
|
+
where: {
|
|
735
|
+
teamRunId: teamRun.id,
|
|
736
|
+
triggerMessageId: message.id,
|
|
737
|
+
targetMemberId: members[1].id,
|
|
738
|
+
},
|
|
739
|
+
});
|
|
740
|
+
expect(createdRequest).toMatchObject({
|
|
741
|
+
requesterType: 'agent',
|
|
742
|
+
requesterMemberId: members[0].id,
|
|
743
|
+
status: 'STARTED',
|
|
744
|
+
});
|
|
745
|
+
createdRequestId = createdRequest.id;
|
|
746
|
+
});
|
|
747
|
+
await vi.waitFor(async () => {
|
|
748
|
+
await expect(prisma.agentInvocation.findFirst({
|
|
749
|
+
where: { workRequestId: createdRequestId },
|
|
750
|
+
})).resolves.toMatchObject({
|
|
751
|
+
memberId: members[1].id,
|
|
752
|
+
sessionId: null,
|
|
753
|
+
status: 'FAILED',
|
|
754
|
+
});
|
|
706
755
|
});
|
|
707
756
|
}
|
|
708
757
|
finally {
|
|
@@ -736,7 +785,19 @@ describe('TeamReconcilerService', () => {
|
|
|
736
785
|
it('lists TeamRun members through MCP without exposing role prompts', async () => {
|
|
737
786
|
const previousTeamRunId = process.env.AGENT_TOWER_TEAM_RUN_ID;
|
|
738
787
|
const previousMemberId = process.env.AGENT_TOWER_MEMBER_ID;
|
|
739
|
-
const { teamRun, members } = await createFixture({ memberCount: 2 });
|
|
788
|
+
const { workspace, teamRun, members } = await createFixture({ memberCount: 2 });
|
|
789
|
+
const runningRequest = await createWorkRequest({
|
|
790
|
+
teamRunId: teamRun.id,
|
|
791
|
+
targetMemberId: members[0].id,
|
|
792
|
+
status: 'STARTED',
|
|
793
|
+
});
|
|
794
|
+
await createRunningInvocation({
|
|
795
|
+
teamRunId: teamRun.id,
|
|
796
|
+
workRequestId: runningRequest.id,
|
|
797
|
+
memberId: members[0].id,
|
|
798
|
+
workspaceId: workspace.id,
|
|
799
|
+
status: 'RUNNING',
|
|
800
|
+
});
|
|
740
801
|
const app = Fastify({ logger: false });
|
|
741
802
|
try {
|
|
742
803
|
process.env.AGENT_TOWER_TEAM_RUN_ID = teamRun.id;
|
|
@@ -771,7 +832,7 @@ describe('TeamReconcilerService', () => {
|
|
|
771
832
|
id: members[0].id,
|
|
772
833
|
name: 'Member 1',
|
|
773
834
|
aliases: ['member-1'],
|
|
774
|
-
status: '
|
|
835
|
+
status: 'RUNNING',
|
|
775
836
|
workspacePolicy: 'shared',
|
|
776
837
|
triggerPolicy: 'MENTION_ONLY',
|
|
777
838
|
sessionPolicy: 'new_per_request',
|
|
@@ -860,6 +921,12 @@ describe('TeamReconcilerService', () => {
|
|
|
860
921
|
team_run_id: other.teamRun.id,
|
|
861
922
|
},
|
|
862
923
|
},
|
|
924
|
+
{
|
|
925
|
+
name: 'list_member_work_requests',
|
|
926
|
+
arguments: {
|
|
927
|
+
team_run_id: other.teamRun.id,
|
|
928
|
+
},
|
|
929
|
+
},
|
|
863
930
|
{
|
|
864
931
|
name: 'stop_member_work',
|
|
865
932
|
arguments: {
|
|
@@ -868,6 +935,13 @@ describe('TeamReconcilerService', () => {
|
|
|
868
935
|
cancel_queued: true,
|
|
869
936
|
},
|
|
870
937
|
},
|
|
938
|
+
{
|
|
939
|
+
name: 'cancel_work_request',
|
|
940
|
+
arguments: {
|
|
941
|
+
team_run_id: other.teamRun.id,
|
|
942
|
+
work_request_id: request.id,
|
|
943
|
+
},
|
|
944
|
+
},
|
|
871
945
|
];
|
|
872
946
|
for (const toolCall of cases) {
|
|
873
947
|
const result = await client.callTool(toolCall);
|
|
@@ -887,6 +961,113 @@ describe('TeamReconcilerService', () => {
|
|
|
887
961
|
await app.close();
|
|
888
962
|
}
|
|
889
963
|
});
|
|
964
|
+
it('lists and cancels current member queued WorkRequests through MCP', async () => {
|
|
965
|
+
const previousEnv = captureTeamRunEnv();
|
|
966
|
+
const { teamRun, members } = await createFixture({ memberCount: 2 });
|
|
967
|
+
const triggerMessage = await prisma.roomMessage.create({
|
|
968
|
+
data: {
|
|
969
|
+
teamRunId: teamRun.id,
|
|
970
|
+
senderType: 'user',
|
|
971
|
+
senderId: null,
|
|
972
|
+
senderInvocationId: null,
|
|
973
|
+
kind: 'chat',
|
|
974
|
+
content: 'Please handle this queued request',
|
|
975
|
+
mentions: '[]',
|
|
976
|
+
workRequestIds: '[]',
|
|
977
|
+
artifactRefs: '[]',
|
|
978
|
+
attachmentIds: '[]',
|
|
979
|
+
},
|
|
980
|
+
});
|
|
981
|
+
const ownRequest = await createWorkRequest({
|
|
982
|
+
teamRunId: teamRun.id,
|
|
983
|
+
targetMemberId: members[0].id,
|
|
984
|
+
status: 'QUEUED',
|
|
985
|
+
instruction: 'Own queued request',
|
|
986
|
+
});
|
|
987
|
+
await prisma.workRequest.update({
|
|
988
|
+
where: { id: ownRequest.id },
|
|
989
|
+
data: { triggerMessageId: triggerMessage.id },
|
|
990
|
+
});
|
|
991
|
+
const otherRequest = await createWorkRequest({
|
|
992
|
+
teamRunId: teamRun.id,
|
|
993
|
+
targetMemberId: members[1].id,
|
|
994
|
+
status: 'QUEUED',
|
|
995
|
+
instruction: 'Other queued request',
|
|
996
|
+
});
|
|
997
|
+
const app = Fastify({ logger: false });
|
|
998
|
+
try {
|
|
999
|
+
setTeamRunEnv({
|
|
1000
|
+
AGENT_TOWER_TEAM_RUN_ID: teamRun.id,
|
|
1001
|
+
AGENT_TOWER_MEMBER_ID: members[0].id,
|
|
1002
|
+
AGENT_TOWER_INVOCATION_ID: undefined,
|
|
1003
|
+
AGENT_TOWER_SESSION_ID: undefined,
|
|
1004
|
+
});
|
|
1005
|
+
await app.register(teamRunRoutes, { prefix: '/api' });
|
|
1006
|
+
await app.listen({ port: 0, host: '127.0.0.1' });
|
|
1007
|
+
const address = app.server.address();
|
|
1008
|
+
if (!address || typeof address === 'string') {
|
|
1009
|
+
throw new Error('Failed to start test server');
|
|
1010
|
+
}
|
|
1011
|
+
const server = await createMcpServer(`http://127.0.0.1:${address.port}`);
|
|
1012
|
+
const client = new Client({ name: 'team-run-queue-test-client', version: '0.1.0' });
|
|
1013
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
1014
|
+
await server.connect(serverTransport);
|
|
1015
|
+
await client.connect(clientTransport);
|
|
1016
|
+
try {
|
|
1017
|
+
const listResult = await client.callTool({
|
|
1018
|
+
name: 'list_member_work_requests',
|
|
1019
|
+
arguments: {},
|
|
1020
|
+
});
|
|
1021
|
+
expect(listResult.isError, getMcpToolText(listResult)).not.toBe(true);
|
|
1022
|
+
const queue = JSON.parse(getMcpToolText(listResult));
|
|
1023
|
+
expect(queue).toMatchObject({
|
|
1024
|
+
currentMemberId: members[0].id,
|
|
1025
|
+
queueManagementPolicy: 'own_only',
|
|
1026
|
+
canManageTeamRunQueue: false,
|
|
1027
|
+
});
|
|
1028
|
+
expect(queue.workRequests).toHaveLength(1);
|
|
1029
|
+
expect(queue.workRequests[0]).toMatchObject({
|
|
1030
|
+
id: ownRequest.id,
|
|
1031
|
+
targetMemberId: members[0].id,
|
|
1032
|
+
targetMember: {
|
|
1033
|
+
id: members[0].id,
|
|
1034
|
+
name: members[0].name,
|
|
1035
|
+
label: members[0].name,
|
|
1036
|
+
},
|
|
1037
|
+
triggerMessage: { contentPreview: 'Please handle this queued request' },
|
|
1038
|
+
});
|
|
1039
|
+
const cancelResult = await client.callTool({
|
|
1040
|
+
name: 'cancel_work_request',
|
|
1041
|
+
arguments: { work_request_id: ownRequest.id },
|
|
1042
|
+
});
|
|
1043
|
+
expect(cancelResult.isError, getMcpToolText(cancelResult)).not.toBe(true);
|
|
1044
|
+
expect(JSON.parse(getMcpToolText(cancelResult))).toMatchObject({
|
|
1045
|
+
id: ownRequest.id,
|
|
1046
|
+
status: 'CANCELLED',
|
|
1047
|
+
});
|
|
1048
|
+
const forbiddenCancel = await client.callTool({
|
|
1049
|
+
name: 'cancel_work_request',
|
|
1050
|
+
arguments: { work_request_id: otherRequest.id },
|
|
1051
|
+
});
|
|
1052
|
+
expect(forbiddenCancel.isError).toBe(true);
|
|
1053
|
+
expect(getMcpToolText(forbiddenCancel)).toContain('FORBIDDEN');
|
|
1054
|
+
}
|
|
1055
|
+
finally {
|
|
1056
|
+
await client.close();
|
|
1057
|
+
await server.close();
|
|
1058
|
+
}
|
|
1059
|
+
await expect(prisma.workRequest.findUnique({ where: { id: ownRequest.id } })).resolves.toMatchObject({
|
|
1060
|
+
status: 'CANCELLED',
|
|
1061
|
+
});
|
|
1062
|
+
await expect(prisma.workRequest.findUnique({ where: { id: otherRequest.id } })).resolves.toMatchObject({
|
|
1063
|
+
status: 'QUEUED',
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
finally {
|
|
1067
|
+
restoreTeamRunEnv(previousEnv);
|
|
1068
|
+
await app.close();
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
890
1071
|
it('posts MCP room messages as user when bound TeamRun identity is partial', async () => {
|
|
891
1072
|
const previousEnv = captureTeamRunEnv();
|
|
892
1073
|
const { teamRun, members } = await createFixture({ memberCount: 2 });
|
|
@@ -941,6 +1122,78 @@ describe('TeamReconcilerService', () => {
|
|
|
941
1122
|
await app.close();
|
|
942
1123
|
}
|
|
943
1124
|
});
|
|
1125
|
+
it('enforces requester member scope for REST WorkRequest cancellation', async () => {
|
|
1126
|
+
const { teamRun, members } = await createFixture({ memberCount: 3 });
|
|
1127
|
+
await prisma.teamMember.update({
|
|
1128
|
+
where: { id: members[2].id },
|
|
1129
|
+
data: { queueManagementPolicy: 'team_pending' },
|
|
1130
|
+
});
|
|
1131
|
+
const ownQueued = await createWorkRequest({
|
|
1132
|
+
teamRunId: teamRun.id,
|
|
1133
|
+
targetMemberId: members[0].id,
|
|
1134
|
+
status: 'QUEUED',
|
|
1135
|
+
instruction: 'Own queued request',
|
|
1136
|
+
});
|
|
1137
|
+
const otherQueued = await createWorkRequest({
|
|
1138
|
+
teamRunId: teamRun.id,
|
|
1139
|
+
targetMemberId: members[1].id,
|
|
1140
|
+
status: 'QUEUED',
|
|
1141
|
+
instruction: 'Other queued request',
|
|
1142
|
+
});
|
|
1143
|
+
const managerQueued = await createWorkRequest({
|
|
1144
|
+
teamRunId: teamRun.id,
|
|
1145
|
+
targetMemberId: members[1].id,
|
|
1146
|
+
status: 'QUEUED',
|
|
1147
|
+
instruction: 'Manager cancellable request',
|
|
1148
|
+
});
|
|
1149
|
+
const app = Fastify({ logger: false });
|
|
1150
|
+
try {
|
|
1151
|
+
await app.register(teamRunRoutes, { prefix: '/api' });
|
|
1152
|
+
const anonymousCancel = await app.inject({
|
|
1153
|
+
method: 'POST',
|
|
1154
|
+
url: `/api/team-runs/work-requests/${ownQueued.id}/cancel`,
|
|
1155
|
+
payload: {},
|
|
1156
|
+
});
|
|
1157
|
+
expect(anonymousCancel.statusCode).toBe(400);
|
|
1158
|
+
expect(anonymousCancel.json()).toMatchObject({ code: 'VALIDATION_ERROR' });
|
|
1159
|
+
const ownCancel = await app.inject({
|
|
1160
|
+
method: 'POST',
|
|
1161
|
+
url: `/api/team-runs/work-requests/${ownQueued.id}/cancel`,
|
|
1162
|
+
payload: {
|
|
1163
|
+
teamRunId: teamRun.id,
|
|
1164
|
+
requesterMemberId: members[0].id,
|
|
1165
|
+
},
|
|
1166
|
+
});
|
|
1167
|
+
expect(ownCancel.statusCode).toBe(200);
|
|
1168
|
+
expect(ownCancel.json()).toMatchObject({ id: ownQueued.id, status: 'CANCELLED' });
|
|
1169
|
+
const forbiddenCancel = await app.inject({
|
|
1170
|
+
method: 'POST',
|
|
1171
|
+
url: `/api/team-runs/work-requests/${otherQueued.id}/cancel`,
|
|
1172
|
+
payload: {
|
|
1173
|
+
teamRunId: teamRun.id,
|
|
1174
|
+
requesterMemberId: members[0].id,
|
|
1175
|
+
},
|
|
1176
|
+
});
|
|
1177
|
+
expect(forbiddenCancel.statusCode).toBe(403);
|
|
1178
|
+
expect(forbiddenCancel.json()).toMatchObject({ code: 'FORBIDDEN' });
|
|
1179
|
+
const managerCancel = await app.inject({
|
|
1180
|
+
method: 'POST',
|
|
1181
|
+
url: `/api/team-runs/work-requests/${managerQueued.id}/cancel`,
|
|
1182
|
+
payload: {
|
|
1183
|
+
teamRunId: teamRun.id,
|
|
1184
|
+
requesterMemberId: members[2].id,
|
|
1185
|
+
},
|
|
1186
|
+
});
|
|
1187
|
+
expect(managerCancel.statusCode).toBe(200);
|
|
1188
|
+
expect(managerCancel.json()).toMatchObject({ id: managerQueued.id, status: 'CANCELLED' });
|
|
1189
|
+
await expect(prisma.workRequest.findUnique({ where: { id: otherQueued.id } })).resolves.toMatchObject({
|
|
1190
|
+
status: 'QUEUED',
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
finally {
|
|
1194
|
+
await app.close();
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
944
1197
|
it('auto-starts USER_MESSAGES work when a user posts an unmentioned room message', async () => {
|
|
945
1198
|
const { teamRun, members } = await createFixture({
|
|
946
1199
|
memberCount: 2,
|
|
@@ -948,6 +1201,11 @@ describe('TeamReconcilerService', () => {
|
|
|
948
1201
|
triggerPolicies: ['USER_MESSAGES', 'MENTION_ONLY'],
|
|
949
1202
|
});
|
|
950
1203
|
const routeScheduler = createRouteSchedulerMock();
|
|
1204
|
+
const startedTeamRunIds = [];
|
|
1205
|
+
routeScheduler.startNextSessions = vi.fn(async (teamRunId) => {
|
|
1206
|
+
startedTeamRunIds.push(teamRunId);
|
|
1207
|
+
return await new Promise(() => { });
|
|
1208
|
+
});
|
|
951
1209
|
const app = Fastify({ logger: false });
|
|
952
1210
|
try {
|
|
953
1211
|
await app.register(teamRunRoutes, { prefix: '/api', scheduler: routeScheduler });
|
|
@@ -963,22 +1221,456 @@ describe('TeamReconcilerService', () => {
|
|
|
963
1221
|
const message = response.json();
|
|
964
1222
|
expect(message.mentions).toEqual([]);
|
|
965
1223
|
expect(message.workRequestIds).toHaveLength(1);
|
|
966
|
-
expect(routeScheduler.startNextSessions).
|
|
967
|
-
expect(routeScheduler.startedTeamRunIds).toEqual([teamRun.id]);
|
|
1224
|
+
expect(routeScheduler.startNextSessions).not.toHaveBeenCalled();
|
|
968
1225
|
await expect(prisma.workRequest.findUnique({ where: { id: message.workRequestIds[0] } })).resolves.toMatchObject({
|
|
969
1226
|
targetMemberId: members[0].id,
|
|
970
1227
|
instruction: '普通用户消息',
|
|
971
|
-
status: '
|
|
1228
|
+
status: 'QUEUED',
|
|
972
1229
|
});
|
|
973
1230
|
await expect(prisma.agentInvocation.count({
|
|
974
1231
|
where: {
|
|
975
1232
|
teamRunId: teamRun.id,
|
|
976
1233
|
memberId: members[0].id,
|
|
977
|
-
status: 'RUNNING',
|
|
978
1234
|
},
|
|
979
|
-
})).resolves.toBe(
|
|
1235
|
+
})).resolves.toBe(0);
|
|
1236
|
+
await vi.waitFor(() => {
|
|
1237
|
+
expect(routeScheduler.startNextSessions).toHaveBeenCalledWith(teamRun.id);
|
|
1238
|
+
expect(startedTeamRunIds).toEqual([teamRun.id]);
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
finally {
|
|
1242
|
+
await app.close();
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
it('creates an initial Team Room message without WorkRequests for unmentioned MENTION_ONLY members', async () => {
|
|
1246
|
+
const project = await prisma.project.create({
|
|
1247
|
+
data: {
|
|
1248
|
+
name: 'Initial message project',
|
|
1249
|
+
repoPath: testDir,
|
|
1250
|
+
},
|
|
1251
|
+
});
|
|
1252
|
+
const task = await prisma.task.create({
|
|
1253
|
+
data: {
|
|
1254
|
+
title: 'Build CSV import',
|
|
1255
|
+
description: 'Parse uploaded files and show validation errors.',
|
|
1256
|
+
projectId: project.id,
|
|
1257
|
+
},
|
|
1258
|
+
});
|
|
1259
|
+
const implementerPreset = await createMemberPreset({ name: 'Implementer', triggerPolicy: 'MENTION_ONLY' });
|
|
1260
|
+
const reviewerPreset = await createMemberPreset({ name: 'Reviewer', triggerPolicy: 'MENTION_ONLY' });
|
|
1261
|
+
const routeScheduler = createRouteSchedulerMock();
|
|
1262
|
+
const app = Fastify({ logger: false });
|
|
1263
|
+
try {
|
|
1264
|
+
await app.register(teamRunRoutes, { prefix: '/api', scheduler: routeScheduler });
|
|
1265
|
+
const response = await app.inject({
|
|
1266
|
+
method: 'POST',
|
|
1267
|
+
url: `/api/tasks/${task.id}/team-runs`,
|
|
1268
|
+
payload: {
|
|
1269
|
+
mode: 'AUTO',
|
|
1270
|
+
memberPresetIds: [implementerPreset.id, reviewerPreset.id],
|
|
1271
|
+
},
|
|
1272
|
+
});
|
|
1273
|
+
expect(response.statusCode).toBe(201);
|
|
1274
|
+
const teamRun = response.json();
|
|
1275
|
+
const initialContent = 'Build CSV import\n\nParse uploaded files and show validation errors.';
|
|
1276
|
+
expect(teamRun.messages).toHaveLength(1);
|
|
1277
|
+
expect(teamRun.messages[0]).toMatchObject({
|
|
1278
|
+
senderType: 'user',
|
|
1279
|
+
kind: 'chat',
|
|
1280
|
+
content: initialContent,
|
|
1281
|
+
mentions: [],
|
|
1282
|
+
workRequestIds: [],
|
|
1283
|
+
});
|
|
1284
|
+
expect(teamRun.workRequests).toEqual([]);
|
|
1285
|
+
const messagesResponse = await app.inject({
|
|
1286
|
+
method: 'GET',
|
|
1287
|
+
url: `/api/team-runs/${teamRun.id}/messages`,
|
|
1288
|
+
});
|
|
1289
|
+
expect(messagesResponse.statusCode).toBe(200);
|
|
1290
|
+
const messages = messagesResponse.json();
|
|
1291
|
+
expect(messages).toEqual([
|
|
1292
|
+
expect.objectContaining({
|
|
1293
|
+
id: teamRun.messages[0].id,
|
|
1294
|
+
content: initialContent,
|
|
1295
|
+
mentions: [],
|
|
1296
|
+
workRequestIds: [],
|
|
1297
|
+
}),
|
|
1298
|
+
]);
|
|
1299
|
+
const workRequestsResponse = await app.inject({
|
|
1300
|
+
method: 'GET',
|
|
1301
|
+
url: `/api/team-runs/${teamRun.id}/work-requests`,
|
|
1302
|
+
});
|
|
1303
|
+
expect(workRequestsResponse.statusCode).toBe(200);
|
|
1304
|
+
const workRequests = workRequestsResponse.json();
|
|
1305
|
+
expect(workRequests).toEqual([]);
|
|
1306
|
+
await expect(prisma.roomMessage.count({ where: { teamRunId: teamRun.id } })).resolves.toBe(1);
|
|
1307
|
+
await expect(prisma.workRequest.count({ where: { teamRunId: teamRun.id } })).resolves.toBe(0);
|
|
1308
|
+
expect(routeScheduler.startNextSessions).not.toHaveBeenCalled();
|
|
1309
|
+
await expect(prisma.agentInvocation.count({ where: { teamRunId: teamRun.id } })).resolves.toBe(0);
|
|
1310
|
+
}
|
|
1311
|
+
finally {
|
|
1312
|
+
await app.close();
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
it('creates USER_MESSAGES WorkRequests from a title-only initial message and auto-starts it', async () => {
|
|
1316
|
+
const project = await prisma.project.create({
|
|
1317
|
+
data: {
|
|
1318
|
+
name: 'Title-only project',
|
|
1319
|
+
repoPath: testDir,
|
|
1320
|
+
},
|
|
1321
|
+
});
|
|
1322
|
+
const task = await prisma.task.create({
|
|
1323
|
+
data: {
|
|
1324
|
+
title: 'Investigate slow dashboard',
|
|
1325
|
+
projectId: project.id,
|
|
1326
|
+
},
|
|
1327
|
+
});
|
|
1328
|
+
const preset = await createMemberPreset({ name: 'Leader', triggerPolicy: 'USER_MESSAGES' });
|
|
1329
|
+
const routeScheduler = createRouteSchedulerMock();
|
|
1330
|
+
const app = Fastify({ logger: false });
|
|
1331
|
+
try {
|
|
1332
|
+
await app.register(teamRunRoutes, { prefix: '/api', scheduler: routeScheduler });
|
|
1333
|
+
const response = await app.inject({
|
|
1334
|
+
method: 'POST',
|
|
1335
|
+
url: `/api/tasks/${task.id}/team-runs`,
|
|
1336
|
+
payload: {
|
|
1337
|
+
mode: 'AUTO',
|
|
1338
|
+
memberPresetIds: [preset.id],
|
|
1339
|
+
},
|
|
1340
|
+
});
|
|
1341
|
+
expect(response.statusCode).toBe(201);
|
|
1342
|
+
const teamRun = response.json();
|
|
1343
|
+
expect(teamRun.messages).toHaveLength(1);
|
|
1344
|
+
expect(teamRun.messages[0]).toMatchObject({
|
|
1345
|
+
content: 'Investigate slow dashboard',
|
|
1346
|
+
workRequestIds: [teamRun.workRequests[0].id],
|
|
1347
|
+
});
|
|
1348
|
+
expect(teamRun.workRequests[0]).toMatchObject({
|
|
1349
|
+
instruction: 'Investigate slow dashboard',
|
|
1350
|
+
status: 'QUEUED',
|
|
1351
|
+
});
|
|
1352
|
+
await vi.waitFor(() => {
|
|
1353
|
+
expect(routeScheduler.startNextSessions).toHaveBeenCalledWith(teamRun.id);
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
finally {
|
|
1357
|
+
await app.close();
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
it('creates an initial WorkRequest only for a mentioned MENTION_ONLY member', async () => {
|
|
1361
|
+
const project = await prisma.project.create({
|
|
1362
|
+
data: {
|
|
1363
|
+
name: 'Mentioned initial message project',
|
|
1364
|
+
repoPath: testDir,
|
|
1365
|
+
},
|
|
1366
|
+
});
|
|
1367
|
+
const task = await prisma.task.create({
|
|
1368
|
+
data: {
|
|
1369
|
+
title: 'Investigate import failures',
|
|
1370
|
+
description: 'Please @Reviewer check the error handling.',
|
|
1371
|
+
projectId: project.id,
|
|
1372
|
+
},
|
|
1373
|
+
});
|
|
1374
|
+
const implementerPreset = await createMemberPreset({ name: 'Implementer', triggerPolicy: 'MENTION_ONLY' });
|
|
1375
|
+
const reviewerPreset = await createMemberPreset({ name: 'Reviewer', triggerPolicy: 'MENTION_ONLY' });
|
|
1376
|
+
const routeScheduler = createRouteSchedulerMock();
|
|
1377
|
+
const app = Fastify({ logger: false });
|
|
1378
|
+
try {
|
|
1379
|
+
await app.register(teamRunRoutes, { prefix: '/api', scheduler: routeScheduler });
|
|
1380
|
+
const response = await app.inject({
|
|
1381
|
+
method: 'POST',
|
|
1382
|
+
url: `/api/tasks/${task.id}/team-runs`,
|
|
1383
|
+
payload: {
|
|
1384
|
+
mode: 'AUTO',
|
|
1385
|
+
memberPresetIds: [implementerPreset.id, reviewerPreset.id],
|
|
1386
|
+
},
|
|
1387
|
+
});
|
|
1388
|
+
expect(response.statusCode).toBe(201);
|
|
1389
|
+
const teamRun = response.json();
|
|
1390
|
+
const reviewer = teamRun.members.find((member) => member.name === 'Reviewer');
|
|
1391
|
+
const implementer = teamRun.members.find((member) => member.name === 'Implementer');
|
|
1392
|
+
const initialContent = 'Investigate import failures\n\nPlease @Reviewer check the error handling.';
|
|
1393
|
+
expect(reviewer).toBeDefined();
|
|
1394
|
+
expect(implementer).toBeDefined();
|
|
1395
|
+
expect(teamRun.messages).toHaveLength(1);
|
|
1396
|
+
expect(teamRun.messages[0]).toMatchObject({
|
|
1397
|
+
content: initialContent,
|
|
1398
|
+
mentions: [{ memberId: reviewer.id, label: 'Reviewer' }],
|
|
1399
|
+
});
|
|
1400
|
+
expect(teamRun.workRequests).toEqual([
|
|
1401
|
+
expect.objectContaining({
|
|
1402
|
+
id: teamRun.messages[0].workRequestIds[0],
|
|
1403
|
+
targetMemberId: reviewer.id,
|
|
1404
|
+
instruction: initialContent,
|
|
1405
|
+
status: 'QUEUED',
|
|
1406
|
+
}),
|
|
1407
|
+
]);
|
|
1408
|
+
expect(teamRun.workRequests[0].targetMemberId).not.toBe(implementer.id);
|
|
1409
|
+
await vi.waitFor(() => {
|
|
1410
|
+
expect(routeScheduler.startNextSessions).toHaveBeenCalledWith(teamRun.id);
|
|
1411
|
+
});
|
|
1412
|
+
await expect(prisma.agentInvocation.count({ where: { teamRunId: teamRun.id } })).resolves.toBe(1);
|
|
1413
|
+
}
|
|
1414
|
+
finally {
|
|
1415
|
+
await app.close();
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
it('matches initial @mention tokens exactly instead of matching name prefixes', async () => {
|
|
1419
|
+
const project = await prisma.project.create({
|
|
1420
|
+
data: {
|
|
1421
|
+
name: 'Mention prefix project',
|
|
1422
|
+
repoPath: testDir,
|
|
1423
|
+
},
|
|
1424
|
+
});
|
|
1425
|
+
const task = await prisma.task.create({
|
|
1426
|
+
data: {
|
|
1427
|
+
title: 'Route QA review',
|
|
1428
|
+
description: 'Please @QA-Lead verify the release notes.',
|
|
1429
|
+
projectId: project.id,
|
|
1430
|
+
},
|
|
1431
|
+
});
|
|
1432
|
+
const qaPreset = await createMemberPreset({ name: 'QA', triggerPolicy: 'MENTION_ONLY' });
|
|
1433
|
+
const qaLeadPreset = await createMemberPreset({ name: 'QA-Lead', triggerPolicy: 'MENTION_ONLY' });
|
|
1434
|
+
const routeScheduler = createRouteSchedulerMock();
|
|
1435
|
+
const app = Fastify({ logger: false });
|
|
1436
|
+
try {
|
|
1437
|
+
await app.register(teamRunRoutes, { prefix: '/api', scheduler: routeScheduler });
|
|
1438
|
+
const response = await app.inject({
|
|
1439
|
+
method: 'POST',
|
|
1440
|
+
url: `/api/tasks/${task.id}/team-runs`,
|
|
1441
|
+
payload: {
|
|
1442
|
+
mode: 'AUTO',
|
|
1443
|
+
memberPresetIds: [qaPreset.id, qaLeadPreset.id],
|
|
1444
|
+
},
|
|
1445
|
+
});
|
|
1446
|
+
expect(response.statusCode).toBe(201);
|
|
1447
|
+
const teamRun = response.json();
|
|
1448
|
+
const qa = teamRun.members.find((member) => member.name === 'QA');
|
|
1449
|
+
const qaLead = teamRun.members.find((member) => member.name === 'QA-Lead');
|
|
1450
|
+
expect(qa).toBeDefined();
|
|
1451
|
+
expect(qaLead).toBeDefined();
|
|
1452
|
+
expect(teamRun.messages[0].mentions).toEqual([{ memberId: qaLead.id, label: 'QA-Lead' }]);
|
|
1453
|
+
expect(teamRun.workRequests).toEqual([
|
|
1454
|
+
expect.objectContaining({
|
|
1455
|
+
id: teamRun.messages[0].workRequestIds[0],
|
|
1456
|
+
targetMemberId: qaLead.id,
|
|
1457
|
+
}),
|
|
1458
|
+
]);
|
|
1459
|
+
expect(teamRun.workRequests[0].targetMemberId).not.toBe(qa.id);
|
|
1460
|
+
}
|
|
1461
|
+
finally {
|
|
1462
|
+
await app.close();
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
it('does not create initial WorkRequests for ambiguous @mention aliases', async () => {
|
|
1466
|
+
const project = await prisma.project.create({
|
|
1467
|
+
data: {
|
|
1468
|
+
name: 'Ambiguous mention project',
|
|
1469
|
+
repoPath: testDir,
|
|
1470
|
+
},
|
|
1471
|
+
});
|
|
1472
|
+
const task = await prisma.task.create({
|
|
1473
|
+
data: {
|
|
1474
|
+
title: 'Review ambiguous owner',
|
|
1475
|
+
description: 'Please @reviewer check this.',
|
|
1476
|
+
projectId: project.id,
|
|
1477
|
+
},
|
|
1478
|
+
});
|
|
1479
|
+
const firstPreset = await createMemberPreset({ name: 'Frontend Reviewer', triggerPolicy: 'MENTION_ONLY' });
|
|
1480
|
+
const secondPreset = await createMemberPreset({ name: 'Backend Reviewer', triggerPolicy: 'MENTION_ONLY' });
|
|
1481
|
+
await prisma.memberPreset.updateMany({
|
|
1482
|
+
where: { id: { in: [firstPreset.id, secondPreset.id] } },
|
|
1483
|
+
data: { aliases: stringifyJson(['reviewer']) },
|
|
1484
|
+
});
|
|
1485
|
+
const routeScheduler = createRouteSchedulerMock();
|
|
1486
|
+
const app = Fastify({ logger: false });
|
|
1487
|
+
try {
|
|
1488
|
+
await app.register(teamRunRoutes, { prefix: '/api', scheduler: routeScheduler });
|
|
1489
|
+
const response = await app.inject({
|
|
1490
|
+
method: 'POST',
|
|
1491
|
+
url: `/api/tasks/${task.id}/team-runs`,
|
|
1492
|
+
payload: {
|
|
1493
|
+
mode: 'AUTO',
|
|
1494
|
+
memberPresetIds: [firstPreset.id, secondPreset.id],
|
|
1495
|
+
},
|
|
1496
|
+
});
|
|
1497
|
+
expect(response.statusCode).toBe(201);
|
|
1498
|
+
const teamRun = response.json();
|
|
1499
|
+
expect(teamRun.messages[0]).toMatchObject({
|
|
1500
|
+
mentions: [],
|
|
1501
|
+
workRequestIds: [],
|
|
1502
|
+
});
|
|
1503
|
+
expect(teamRun.workRequests).toEqual([]);
|
|
1504
|
+
await expect(prisma.workRequest.count({ where: { teamRunId: teamRun.id } })).resolves.toBe(0);
|
|
1505
|
+
expect(routeScheduler.startNextSessions).not.toHaveBeenCalled();
|
|
1506
|
+
}
|
|
1507
|
+
finally {
|
|
1508
|
+
await app.close();
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1511
|
+
it('does not fall back to USER_MESSAGES when initial @mention aliases are ambiguous', async () => {
|
|
1512
|
+
const project = await prisma.project.create({
|
|
1513
|
+
data: {
|
|
1514
|
+
name: 'Mixed ambiguous mention project',
|
|
1515
|
+
repoPath: testDir,
|
|
1516
|
+
},
|
|
1517
|
+
});
|
|
1518
|
+
const task = await prisma.task.create({
|
|
1519
|
+
data: {
|
|
1520
|
+
title: 'Route ambiguous review',
|
|
1521
|
+
description: 'Please @reviewer decide who owns this.',
|
|
1522
|
+
projectId: project.id,
|
|
1523
|
+
},
|
|
1524
|
+
});
|
|
1525
|
+
const firstPreset = await createMemberPreset({ name: 'Frontend Reviewer', triggerPolicy: 'MENTION_ONLY' });
|
|
1526
|
+
const secondPreset = await createMemberPreset({ name: 'Backend Reviewer', triggerPolicy: 'MENTION_ONLY' });
|
|
1527
|
+
const observerPreset = await createMemberPreset({ name: 'Observer', triggerPolicy: 'USER_MESSAGES' });
|
|
1528
|
+
await prisma.memberPreset.updateMany({
|
|
1529
|
+
where: { id: { in: [firstPreset.id, secondPreset.id] } },
|
|
1530
|
+
data: { aliases: stringifyJson(['reviewer']) },
|
|
1531
|
+
});
|
|
1532
|
+
const routeScheduler = createRouteSchedulerMock();
|
|
1533
|
+
const app = Fastify({ logger: false });
|
|
1534
|
+
try {
|
|
1535
|
+
await app.register(teamRunRoutes, { prefix: '/api', scheduler: routeScheduler });
|
|
1536
|
+
const response = await app.inject({
|
|
1537
|
+
method: 'POST',
|
|
1538
|
+
url: `/api/tasks/${task.id}/team-runs`,
|
|
1539
|
+
payload: {
|
|
1540
|
+
mode: 'AUTO',
|
|
1541
|
+
memberPresetIds: [firstPreset.id, secondPreset.id, observerPreset.id],
|
|
1542
|
+
},
|
|
1543
|
+
});
|
|
1544
|
+
expect(response.statusCode).toBe(201);
|
|
1545
|
+
const teamRun = response.json();
|
|
1546
|
+
expect(teamRun.messages[0]).toMatchObject({
|
|
1547
|
+
mentions: [],
|
|
1548
|
+
workRequestIds: [],
|
|
1549
|
+
});
|
|
1550
|
+
expect(teamRun.workRequests).toEqual([]);
|
|
1551
|
+
await expect(prisma.workRequest.count({ where: { teamRunId: teamRun.id } })).resolves.toBe(0);
|
|
1552
|
+
expect(routeScheduler.startNextSessions).not.toHaveBeenCalled();
|
|
1553
|
+
}
|
|
1554
|
+
finally {
|
|
1555
|
+
await app.close();
|
|
1556
|
+
}
|
|
1557
|
+
});
|
|
1558
|
+
it('rejects creating a TeamRun for a blank-title task before writing TeamRun data', async () => {
|
|
1559
|
+
const project = await prisma.project.create({
|
|
1560
|
+
data: {
|
|
1561
|
+
name: 'Blank title project',
|
|
1562
|
+
repoPath: testDir,
|
|
1563
|
+
},
|
|
1564
|
+
});
|
|
1565
|
+
const task = await prisma.task.create({
|
|
1566
|
+
data: {
|
|
1567
|
+
title: ' ',
|
|
1568
|
+
projectId: project.id,
|
|
1569
|
+
},
|
|
1570
|
+
});
|
|
1571
|
+
const preset = await createMemberPreset({ name: 'Leader', triggerPolicy: 'USER_MESSAGES' });
|
|
1572
|
+
const routeScheduler = createRouteSchedulerMock();
|
|
1573
|
+
const app = Fastify({ logger: false });
|
|
1574
|
+
try {
|
|
1575
|
+
await app.register(teamRunRoutes, { prefix: '/api', scheduler: routeScheduler });
|
|
1576
|
+
const response = await app.inject({
|
|
1577
|
+
method: 'POST',
|
|
1578
|
+
url: `/api/tasks/${task.id}/team-runs`,
|
|
1579
|
+
payload: {
|
|
1580
|
+
mode: 'AUTO',
|
|
1581
|
+
memberPresetIds: [preset.id],
|
|
1582
|
+
},
|
|
1583
|
+
});
|
|
1584
|
+
expect(response.statusCode).toBe(400);
|
|
1585
|
+
expect(response.json()).toMatchObject({ code: 'VALIDATION_ERROR' });
|
|
1586
|
+
expect(routeScheduler.startNextSessions).not.toHaveBeenCalled();
|
|
1587
|
+
await expect(prisma.teamRun.count({ where: { taskId: task.id } })).resolves.toBe(0);
|
|
1588
|
+
await expect(prisma.roomMessage.count()).resolves.toBe(0);
|
|
1589
|
+
await expect(prisma.workRequest.count()).resolves.toBe(0);
|
|
1590
|
+
}
|
|
1591
|
+
finally {
|
|
1592
|
+
await app.close();
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1595
|
+
it('rolls back TeamRun creation when initial WorkRequest creation fails and allows retry', async () => {
|
|
1596
|
+
const project = await prisma.project.create({
|
|
1597
|
+
data: {
|
|
1598
|
+
name: 'Atomic TeamRun project',
|
|
1599
|
+
repoPath: testDir,
|
|
1600
|
+
},
|
|
1601
|
+
});
|
|
1602
|
+
const task = await prisma.task.create({
|
|
1603
|
+
data: {
|
|
1604
|
+
title: 'Start atomically',
|
|
1605
|
+
projectId: project.id,
|
|
1606
|
+
},
|
|
1607
|
+
});
|
|
1608
|
+
const preset = await createMemberPreset({ name: 'Implementer', triggerPolicy: 'USER_MESSAGES' });
|
|
1609
|
+
const routeScheduler = createRouteSchedulerMock();
|
|
1610
|
+
const originalTransaction = prisma.$transaction.bind(prisma);
|
|
1611
|
+
const transactionSpy = vi.spyOn(prisma, '$transaction');
|
|
1612
|
+
transactionSpy.mockImplementationOnce(async (arg, ...rest) => {
|
|
1613
|
+
if (typeof arg !== 'function') {
|
|
1614
|
+
return originalTransaction(arg, ...rest);
|
|
1615
|
+
}
|
|
1616
|
+
return originalTransaction(async (tx) => {
|
|
1617
|
+
const failingTx = new Proxy(tx, {
|
|
1618
|
+
get(target, property, receiver) {
|
|
1619
|
+
if (property !== 'workRequest') {
|
|
1620
|
+
return Reflect.get(target, property, receiver);
|
|
1621
|
+
}
|
|
1622
|
+
const delegate = Reflect.get(target, property, receiver);
|
|
1623
|
+
return new Proxy(delegate, {
|
|
1624
|
+
get(delegateTarget, delegateProperty, delegateReceiver) {
|
|
1625
|
+
if (delegateProperty === 'create') {
|
|
1626
|
+
return async () => {
|
|
1627
|
+
throw new Error('injected initial WorkRequest failure');
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
return Reflect.get(delegateTarget, delegateProperty, delegateReceiver);
|
|
1631
|
+
},
|
|
1632
|
+
});
|
|
1633
|
+
},
|
|
1634
|
+
});
|
|
1635
|
+
return arg(failingTx);
|
|
1636
|
+
}, ...rest);
|
|
1637
|
+
});
|
|
1638
|
+
const app = Fastify({ logger: false });
|
|
1639
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
1640
|
+
try {
|
|
1641
|
+
await app.register(teamRunRoutes, { prefix: '/api', scheduler: routeScheduler });
|
|
1642
|
+
const failedResponse = await app.inject({
|
|
1643
|
+
method: 'POST',
|
|
1644
|
+
url: `/api/tasks/${task.id}/team-runs`,
|
|
1645
|
+
payload: {
|
|
1646
|
+
mode: 'AUTO',
|
|
1647
|
+
memberPresetIds: [preset.id],
|
|
1648
|
+
},
|
|
1649
|
+
});
|
|
1650
|
+
expect(failedResponse.statusCode).toBe(500);
|
|
1651
|
+
expect(routeScheduler.startNextSessions).not.toHaveBeenCalled();
|
|
1652
|
+
await expect(prisma.teamRun.count({ where: { taskId: task.id } })).resolves.toBe(0);
|
|
1653
|
+
await expect(prisma.teamMember.count()).resolves.toBe(0);
|
|
1654
|
+
await expect(prisma.roomMessage.count()).resolves.toBe(0);
|
|
1655
|
+
await expect(prisma.workRequest.count()).resolves.toBe(0);
|
|
1656
|
+
const retryResponse = await app.inject({
|
|
1657
|
+
method: 'POST',
|
|
1658
|
+
url: `/api/tasks/${task.id}/team-runs`,
|
|
1659
|
+
payload: {
|
|
1660
|
+
mode: 'AUTO',
|
|
1661
|
+
memberPresetIds: [preset.id],
|
|
1662
|
+
},
|
|
1663
|
+
});
|
|
1664
|
+
expect(retryResponse.statusCode).toBe(201);
|
|
1665
|
+
const teamRun = retryResponse.json();
|
|
1666
|
+
expect(teamRun.messages[0].workRequestIds).toEqual([teamRun.workRequests[0].id]);
|
|
1667
|
+
expect(teamRun.workRequests[0]).toMatchObject({ status: 'QUEUED' });
|
|
1668
|
+
await vi.waitFor(() => {
|
|
1669
|
+
expect(routeScheduler.startNextSessions).toHaveBeenCalledWith(teamRun.id);
|
|
1670
|
+
});
|
|
980
1671
|
}
|
|
981
1672
|
finally {
|
|
1673
|
+
consoleErrorSpy.mockRestore();
|
|
982
1674
|
await app.close();
|
|
983
1675
|
}
|
|
984
1676
|
});
|