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.
- 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.map +1 -1
- package/dist/mcp/http-client.js +8 -1
- package/dist/mcp/http-client.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 +18 -8
- 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 +525 -27
- package/dist/services/__tests__/team-reconciler.service.test.js.map +1 -1
- package/dist/services/__tests__/team-run.service.test.js +252 -0
- package/dist/services/__tests__/team-run.service.test.js.map +1 -1
- package/dist/services/__tests__/team-scheduler.service.test.js +551 -21
- 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 +3 -0
- package/dist/services/team-run.service.d.ts.map +1 -1
- package/dist/services/team-run.service.js +288 -30
- package/dist/services/team-run.service.js.map +1 -1
- package/dist/services/team-scheduler.service.d.ts +15 -0
- package/dist/services/team-scheduler.service.d.ts.map +1 -1
- package/dist/services/team-scheduler.service.js +125 -32
- 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-Bf6labVB.js} +1 -1
- package/dist/web/assets/{DemoPage-B5DTSEbS.js → DemoPage-DlfG47rV.js} +1 -1
- package/dist/web/assets/{GeneralSettingsPage-Cspr7Vol.js → GeneralSettingsPage-DefqwzVn.js} +1 -1
- package/dist/web/assets/MemberAvatar-DVw_TedB.js +1 -0
- package/dist/web/assets/NotificationSettingsPage-C9h1U1Za.js +1 -0
- package/dist/web/assets/{ProfileSettingsPage-CNugU40a.js → ProfileSettingsPage-BkZE2yVP.js} +1 -1
- package/dist/web/assets/ProjectKanbanPage-B1Ckl1uY.js +89 -0
- package/dist/web/assets/ProjectSettingsPage-ByZ13awb.js +2 -0
- package/dist/web/assets/{ProviderSettingsPage-D_KWkgRM.js → ProviderSettingsPage-DSQYe8B6.js} +12 -12
- package/dist/web/assets/TeamSettingsPage-DUukJ_Ih.js +1 -0
- package/dist/web/assets/agent-tower-logo-COx9gy77.png +0 -0
- package/dist/web/assets/{button-B6JaSbDB.js → button-Bpm98eOV.js} +1 -1
- package/dist/web/assets/{chevron-down-CACy4UFq.js → chevron-down-DSKKXCi8.js} +1 -1
- package/dist/web/assets/{chevron-right-DFWfnDJY.js → chevron-right-CZdDV9GU.js} +1 -1
- package/dist/web/assets/{chevron-up-CGlf6jzw.js → chevron-up-gnnlwvYe.js} +1 -1
- package/dist/web/assets/{circle-check-DMK8auwb.js → circle-check-DeD_VuLK.js} +1 -1
- package/dist/web/assets/{code-block-OCS4YCEC-Hn75KHRK.js → code-block-OCS4YCEC-BrGjkdjS.js} +1 -1
- package/dist/web/assets/{confirm-dialog-DHI2f7Ni.js → confirm-dialog-CEVVvAcE.js} +1 -1
- package/dist/web/assets/folder-picker-ZBQlFEWL.js +1 -0
- package/dist/web/assets/{index-BFAA3PTl.js → index-B5g4V0NU.js} +6 -6
- package/dist/web/assets/index-ltjI8o6A.css +1 -0
- package/dist/web/assets/loader-circle-GMfBClX0.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-D5USvUiZ.js} +4 -4
- package/dist/web/assets/{modal-B5IRN7QI.js → modal-JMpuh-LG.js} +1 -1
- package/dist/web/assets/{pencil-CJY6Ahn7.js → pencil-QrCW47nn.js} +1 -1
- package/dist/web/assets/{select-BPZZlla1.js → select-CINRzLiE.js} +1 -1
- package/dist/web/assets/upload-vFxZxKHo.js +1 -0
- package/dist/web/assets/{use-profiles-C2k04ICZ.js → use-profiles-SrVWPYv0.js} +1 -1
- package/dist/web/assets/{use-providers-C7fIDWzP.js → use-providers-BihMydl0.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 +9 -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 +9 -5
- package/node_modules/@prisma/client/.prisma/client/index-browser.js +4 -0
- package/node_modules/@prisma/client/.prisma/client/index.d.ts +1242 -14
- package/node_modules/@prisma/client/.prisma/client/index.js +9 -5
- package/node_modules/@prisma/client/.prisma/client/package.json +1 -1
- package/node_modules/@prisma/client/.prisma/client/schema.prisma +47 -31
- package/node_modules/@prisma/client/.prisma/client/wasm.js +4 -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/schema.prisma +27 -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
|
@@ -4,7 +4,7 @@ import os from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
-
import { AgentType } from '../../types/index.js';
|
|
7
|
+
import { AgentType, TaskStatus } from '../../types/index.js';
|
|
8
8
|
import { TeamLockService } from '../team-lock.service.js';
|
|
9
9
|
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-tower-team-scheduler-'));
|
|
10
10
|
const dbPath = path.join(testDir, 'test.db');
|
|
@@ -40,7 +40,7 @@ const commandCapabilities = {
|
|
|
40
40
|
function stringifyJson(value) {
|
|
41
41
|
return JSON.stringify(value);
|
|
42
42
|
}
|
|
43
|
-
async function createTask(title = 'Team scheduler task') {
|
|
43
|
+
async function createTask(title = 'Team scheduler task', status = TaskStatus.TODO) {
|
|
44
44
|
const project = await prisma.project.create({
|
|
45
45
|
data: {
|
|
46
46
|
name: `${title} project`,
|
|
@@ -51,12 +51,13 @@ async function createTask(title = 'Team scheduler task') {
|
|
|
51
51
|
data: {
|
|
52
52
|
title,
|
|
53
53
|
projectId: project.id,
|
|
54
|
+
status,
|
|
54
55
|
},
|
|
55
56
|
});
|
|
56
57
|
return { project, task };
|
|
57
58
|
}
|
|
58
59
|
async function createTeamRunFixture(options = {}) {
|
|
59
|
-
const { project, task } = await createTask();
|
|
60
|
+
const { project, task } = await createTask('Team scheduler task', options.taskStatus);
|
|
60
61
|
const teamRun = await prisma.teamRun.create({
|
|
61
62
|
data: {
|
|
62
63
|
taskId: task.id,
|
|
@@ -100,7 +101,7 @@ async function createWorkRequest(options) {
|
|
|
100
101
|
requesterMemberId: null,
|
|
101
102
|
requesterType: 'user',
|
|
102
103
|
targetMemberId: options.targetMemberId,
|
|
103
|
-
triggerMessageId: `message-${Math.random().toString(16).slice(2)}`,
|
|
104
|
+
triggerMessageId: options.triggerMessageId ?? `message-${Math.random().toString(16).slice(2)}`,
|
|
104
105
|
instruction: options.instruction ?? 'Please do the work',
|
|
105
106
|
ifBusy: options.ifBusy ?? 'queue',
|
|
106
107
|
cancelQueued: options.cancelQueued ?? false,
|
|
@@ -617,26 +618,124 @@ describe('TeamSchedulerService', () => {
|
|
|
617
618
|
}),
|
|
618
619
|
]);
|
|
619
620
|
});
|
|
620
|
-
it('
|
|
621
|
-
const { teamRun, members } = await createTeamRunFixture({
|
|
622
|
-
|
|
621
|
+
it('starts dedicated members in child workspaces without shared workspace locks', async () => {
|
|
622
|
+
const { workspace: mainWorkspace, teamRun, members } = await createTeamRunFixture({
|
|
623
|
+
memberCapabilities: [writeCapabilities, writeCapabilities],
|
|
624
|
+
workspacePolicies: ['dedicated', 'dedicated'],
|
|
623
625
|
});
|
|
624
|
-
const
|
|
626
|
+
const first = await createWorkRequest({
|
|
625
627
|
teamRunId: teamRun.id,
|
|
626
628
|
targetMemberId: members[0].id,
|
|
627
629
|
});
|
|
630
|
+
const second = await createWorkRequest({
|
|
631
|
+
teamRunId: teamRun.id,
|
|
632
|
+
targetMemberId: members[1].id,
|
|
633
|
+
});
|
|
634
|
+
const childByMemberId = new Map();
|
|
635
|
+
const workspaceService = {
|
|
636
|
+
create: vi.fn(),
|
|
637
|
+
getOrCreateMainWorkspace: vi.fn(async () => mainWorkspace),
|
|
638
|
+
getOrCreateDedicatedWorkspace: vi.fn(async (_teamRunId, memberId) => {
|
|
639
|
+
const existing = childByMemberId.get(memberId);
|
|
640
|
+
if (existing) {
|
|
641
|
+
return existing;
|
|
642
|
+
}
|
|
643
|
+
const workspace = await prisma.workspace.create({
|
|
644
|
+
data: {
|
|
645
|
+
taskId: teamRun.taskId,
|
|
646
|
+
parentWorkspaceId: mainWorkspace.id,
|
|
647
|
+
ownerMemberId: memberId,
|
|
648
|
+
branchName: `dedicated-${memberId.slice(0, 8)}`,
|
|
649
|
+
worktreePath: path.join(testDir, `dedicated-${memberId}`),
|
|
650
|
+
status: 'ACTIVE',
|
|
651
|
+
},
|
|
652
|
+
});
|
|
653
|
+
childByMemberId.set(memberId, workspace);
|
|
654
|
+
return workspace;
|
|
655
|
+
}),
|
|
656
|
+
};
|
|
657
|
+
service = new TeamSchedulerService(lockService, {
|
|
658
|
+
workspaceService,
|
|
659
|
+
sessionManager: createSessionManagerMock(),
|
|
660
|
+
getProviderById: createProviderLookup(),
|
|
661
|
+
});
|
|
628
662
|
await expect(service.planNext(teamRun.id)).resolves.toEqual([
|
|
629
663
|
expect.objectContaining({
|
|
630
|
-
workRequestId:
|
|
631
|
-
canStart:
|
|
632
|
-
|
|
664
|
+
workRequestId: first.id,
|
|
665
|
+
canStart: true,
|
|
666
|
+
lockKeys: [],
|
|
667
|
+
workspaceId: null,
|
|
668
|
+
}),
|
|
669
|
+
expect.objectContaining({
|
|
670
|
+
workRequestId: second.id,
|
|
671
|
+
canStart: true,
|
|
672
|
+
lockKeys: [],
|
|
673
|
+
workspaceId: null,
|
|
633
674
|
}),
|
|
634
675
|
]);
|
|
635
|
-
await
|
|
636
|
-
|
|
637
|
-
|
|
676
|
+
const invocations = await service.startNextSessions(teamRun.id);
|
|
677
|
+
expect(invocations).toHaveLength(2);
|
|
678
|
+
expect(new Set(invocations.map((invocation) => invocation.workspaceId)).size).toBe(2);
|
|
679
|
+
expect(new Set(invocations.map((invocation) => invocation.workRequestId))).toEqual(new Set([
|
|
680
|
+
first.id,
|
|
681
|
+
second.id,
|
|
682
|
+
]));
|
|
683
|
+
expect(workspaceService.getOrCreateDedicatedWorkspace).toHaveBeenCalledTimes(2);
|
|
684
|
+
expect(workspaceService.create).not.toHaveBeenCalled();
|
|
685
|
+
expect(lockService.listLocks()).toEqual([]);
|
|
686
|
+
const childWorkspaces = await prisma.workspace.findMany({
|
|
687
|
+
where: { parentWorkspaceId: mainWorkspace.id },
|
|
688
|
+
orderBy: { ownerMemberId: 'asc' },
|
|
689
|
+
});
|
|
690
|
+
expect(childWorkspaces).toHaveLength(2);
|
|
691
|
+
expect(new Set(childWorkspaces.map((workspace) => workspace.ownerMemberId))).toEqual(new Set([
|
|
692
|
+
members[0].id,
|
|
693
|
+
members[1].id,
|
|
694
|
+
]));
|
|
695
|
+
});
|
|
696
|
+
it('records the dedicated child workspace for queued no-session invocations', async () => {
|
|
697
|
+
const { workspace: mainWorkspace, teamRun, members } = await createTeamRunFixture({
|
|
698
|
+
memberCapabilities: [writeCapabilities],
|
|
699
|
+
workspacePolicies: ['dedicated'],
|
|
700
|
+
});
|
|
701
|
+
const request = await createWorkRequest({
|
|
702
|
+
teamRunId: teamRun.id,
|
|
703
|
+
targetMemberId: members[0].id,
|
|
704
|
+
});
|
|
705
|
+
const childWorkspace = await prisma.workspace.create({
|
|
706
|
+
data: {
|
|
707
|
+
taskId: teamRun.taskId,
|
|
708
|
+
parentWorkspaceId: mainWorkspace.id,
|
|
709
|
+
ownerMemberId: members[0].id,
|
|
710
|
+
branchName: 'dedicated-queued',
|
|
711
|
+
worktreePath: path.join(testDir, 'dedicated-queued'),
|
|
712
|
+
status: 'ACTIVE',
|
|
713
|
+
},
|
|
714
|
+
});
|
|
715
|
+
const workspaceService = {
|
|
716
|
+
create: vi.fn(),
|
|
717
|
+
getOrCreateMainWorkspace: vi.fn(async () => mainWorkspace),
|
|
718
|
+
getOrCreateDedicatedWorkspace: vi.fn(async () => childWorkspace),
|
|
719
|
+
};
|
|
720
|
+
service = new TeamSchedulerService(lockService, {
|
|
721
|
+
workspaceService,
|
|
722
|
+
sessionManager: createSessionManagerMock(),
|
|
723
|
+
getProviderById: createProviderLookup(),
|
|
724
|
+
});
|
|
725
|
+
const invocations = await service.startNext(teamRun.id);
|
|
726
|
+
expect(invocations).toHaveLength(1);
|
|
727
|
+
expect(invocations[0]).toMatchObject({
|
|
728
|
+
workRequestId: request.id,
|
|
729
|
+
memberId: members[0].id,
|
|
730
|
+
workspaceId: childWorkspace.id,
|
|
731
|
+
sessionId: null,
|
|
638
732
|
status: 'QUEUED',
|
|
639
733
|
});
|
|
734
|
+
expect(lockService.listLocks()).toEqual([]);
|
|
735
|
+
expect(workspaceService.getOrCreateDedicatedWorkspace).toHaveBeenCalledWith(teamRun.id, members[0].id);
|
|
736
|
+
await expect(prisma.agentInvocation.findUnique({ where: { id: invocations[0].id } })).resolves.toMatchObject({
|
|
737
|
+
workspaceId: childWorkspace.id,
|
|
738
|
+
});
|
|
640
739
|
});
|
|
641
740
|
it('cancels only queued requests for the same member when cancelQueued is set', async () => {
|
|
642
741
|
const { teamRun, members } = await createTeamRunFixture();
|
|
@@ -702,6 +801,88 @@ describe('TeamSchedulerService', () => {
|
|
|
702
801
|
status: 'RUNNING',
|
|
703
802
|
});
|
|
704
803
|
});
|
|
804
|
+
it('builds session prompt attachment context from the trigger RoomMessage attachmentIds', async () => {
|
|
805
|
+
const { task, teamRun, members } = await createTeamRunFixture({ withWorkspace: false });
|
|
806
|
+
const attachment = await prisma.attachment.create({
|
|
807
|
+
data: {
|
|
808
|
+
originalName: 'reference.png',
|
|
809
|
+
mimeType: 'image/png',
|
|
810
|
+
sizeBytes: 256,
|
|
811
|
+
storagePath: path.join(testDir, 'reference.png'),
|
|
812
|
+
hash: 'scheduler-attachment-context-hash',
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
const message = await prisma.roomMessage.create({
|
|
816
|
+
data: {
|
|
817
|
+
teamRunId: teamRun.id,
|
|
818
|
+
senderType: 'user',
|
|
819
|
+
kind: 'work_request',
|
|
820
|
+
content: 'Use this reference',
|
|
821
|
+
mentions: stringifyJson([{ memberId: members[0].id, label: 'Member 1' }]),
|
|
822
|
+
workRequestIds: stringifyJson([]),
|
|
823
|
+
artifactRefs: stringifyJson([]),
|
|
824
|
+
attachmentIds: stringifyJson([attachment.id]),
|
|
825
|
+
},
|
|
826
|
+
});
|
|
827
|
+
await createWorkRequest({
|
|
828
|
+
teamRunId: teamRun.id,
|
|
829
|
+
targetMemberId: members[0].id,
|
|
830
|
+
instruction: 'Use this reference',
|
|
831
|
+
triggerMessageId: message.id,
|
|
832
|
+
});
|
|
833
|
+
const workspaceService = createWorkspaceServiceMock();
|
|
834
|
+
const sessionManager = createSessionManagerMock();
|
|
835
|
+
service = new TeamSchedulerService(lockService, {
|
|
836
|
+
workspaceService,
|
|
837
|
+
sessionManager,
|
|
838
|
+
getProviderById: createProviderLookup(),
|
|
839
|
+
});
|
|
840
|
+
await service.startNextSessions(teamRun.id);
|
|
841
|
+
expect(workspaceService.create).toHaveBeenCalledWith(task.id);
|
|
842
|
+
expect(sessionManager.create).toHaveBeenCalledWith(expect.any(String), AgentType.CODEX, `Role 1\n\nTask:\nUse this reference\n\nAttachments:\n`, 'DEFAULT', members[0].providerId);
|
|
843
|
+
});
|
|
844
|
+
it('does not duplicate session prompt attachment context when the WorkRequest instruction already includes the storage path', async () => {
|
|
845
|
+
const { task, teamRun, members } = await createTeamRunFixture({ withWorkspace: false });
|
|
846
|
+
const attachment = await prisma.attachment.create({
|
|
847
|
+
data: {
|
|
848
|
+
originalName: 'reference.png',
|
|
849
|
+
mimeType: 'image/png',
|
|
850
|
+
sizeBytes: 256,
|
|
851
|
+
storagePath: path.join(testDir, 'reference-dedup.png'),
|
|
852
|
+
hash: 'scheduler-attachment-context-dedup-hash',
|
|
853
|
+
},
|
|
854
|
+
});
|
|
855
|
+
const instruction = `Use this reference\n\n`;
|
|
856
|
+
const message = await prisma.roomMessage.create({
|
|
857
|
+
data: {
|
|
858
|
+
teamRunId: teamRun.id,
|
|
859
|
+
senderType: 'user',
|
|
860
|
+
kind: 'work_request',
|
|
861
|
+
content: instruction,
|
|
862
|
+
mentions: stringifyJson([{ memberId: members[0].id, label: 'Member 1' }]),
|
|
863
|
+
workRequestIds: stringifyJson([]),
|
|
864
|
+
artifactRefs: stringifyJson([]),
|
|
865
|
+
attachmentIds: stringifyJson([attachment.id]),
|
|
866
|
+
},
|
|
867
|
+
});
|
|
868
|
+
await createWorkRequest({
|
|
869
|
+
teamRunId: teamRun.id,
|
|
870
|
+
targetMemberId: members[0].id,
|
|
871
|
+
instruction,
|
|
872
|
+
triggerMessageId: message.id,
|
|
873
|
+
});
|
|
874
|
+
const workspaceService = createWorkspaceServiceMock();
|
|
875
|
+
const sessionManager = createSessionManagerMock();
|
|
876
|
+
service = new TeamSchedulerService(lockService, {
|
|
877
|
+
workspaceService,
|
|
878
|
+
sessionManager,
|
|
879
|
+
getProviderById: createProviderLookup(),
|
|
880
|
+
});
|
|
881
|
+
await service.startNextSessions(teamRun.id);
|
|
882
|
+
expect(workspaceService.create).toHaveBeenCalledWith(task.id);
|
|
883
|
+
expect(sessionManager.create).toHaveBeenCalledWith(expect.any(String), AgentType.CODEX, `Role 1\n\nTask:\n${instruction}`, 'DEFAULT', members[0].providerId);
|
|
884
|
+
expect(sessionManager.create.mock.calls[0]?.[2]).not.toContain('Attachments:');
|
|
885
|
+
});
|
|
705
886
|
it('starts resume_last members with executor resume context while keeping a new Tower session and invocation', async () => {
|
|
706
887
|
const { workspace, teamRun, members } = await createTeamRunFixture({
|
|
707
888
|
sessionPolicies: ['resume_last'],
|
|
@@ -766,6 +947,202 @@ describe('TeamSchedulerService', () => {
|
|
|
766
947
|
status: 'RUNNING',
|
|
767
948
|
});
|
|
768
949
|
});
|
|
950
|
+
it('starts new_per_request members without resuming previous native context', async () => {
|
|
951
|
+
const { workspace, teamRun, members } = await createTeamRunFixture({
|
|
952
|
+
sessionPolicies: ['new_per_request'],
|
|
953
|
+
});
|
|
954
|
+
const previousRequest = await createWorkRequest({
|
|
955
|
+
teamRunId: teamRun.id,
|
|
956
|
+
targetMemberId: members[0].id,
|
|
957
|
+
status: 'STARTED',
|
|
958
|
+
instruction: 'Previous work',
|
|
959
|
+
});
|
|
960
|
+
const previousSession = await prisma.session.create({
|
|
961
|
+
data: {
|
|
962
|
+
workspaceId: workspace.id,
|
|
963
|
+
agentType: AgentType.CODEX,
|
|
964
|
+
providerId: members[0].providerId,
|
|
965
|
+
prompt: 'previous prompt',
|
|
966
|
+
status: 'COMPLETED',
|
|
967
|
+
logSnapshot: JSON.stringify({ sessionId: 'agent-native-session-1', entries: [] }),
|
|
968
|
+
},
|
|
969
|
+
});
|
|
970
|
+
await prisma.agentInvocation.create({
|
|
971
|
+
data: {
|
|
972
|
+
teamRunId: teamRun.id,
|
|
973
|
+
workRequestId: previousRequest.id,
|
|
974
|
+
memberId: members[0].id,
|
|
975
|
+
workspaceId: workspace.id,
|
|
976
|
+
sessionId: previousSession.id,
|
|
977
|
+
status: 'COMPLETED',
|
|
978
|
+
},
|
|
979
|
+
});
|
|
980
|
+
const nextRequest = await createWorkRequest({
|
|
981
|
+
teamRunId: teamRun.id,
|
|
982
|
+
targetMemberId: members[0].id,
|
|
983
|
+
instruction: 'Fresh request',
|
|
984
|
+
});
|
|
985
|
+
const sessionManager = createSessionManagerMock();
|
|
986
|
+
service = new TeamSchedulerService(lockService, {
|
|
987
|
+
workspaceService: createWorkspaceServiceMock(),
|
|
988
|
+
sessionManager,
|
|
989
|
+
getProviderById: createProviderLookup(),
|
|
990
|
+
});
|
|
991
|
+
const invocations = await service.startNextSessions(teamRun.id);
|
|
992
|
+
expect(invocations).toHaveLength(1);
|
|
993
|
+
expect(invocations[0]).toMatchObject({
|
|
994
|
+
workRequestId: nextRequest.id,
|
|
995
|
+
memberId: members[0].id,
|
|
996
|
+
sessionId: expect.any(String),
|
|
997
|
+
status: 'RUNNING',
|
|
998
|
+
});
|
|
999
|
+
expect(invocations[0].sessionId).not.toBe(previousSession.id);
|
|
1000
|
+
expect(sessionManager.start).toHaveBeenCalledWith(invocations[0].sessionId);
|
|
1001
|
+
expect(sessionManager.startFollowUp).not.toHaveBeenCalled();
|
|
1002
|
+
});
|
|
1003
|
+
it('does not resume_last from a different member even when native context exists in the same workspace', async () => {
|
|
1004
|
+
const { workspace, teamRun, members } = await createTeamRunFixture({
|
|
1005
|
+
memberCapabilities: [readOnlyCapabilities, readOnlyCapabilities],
|
|
1006
|
+
sessionPolicies: ['resume_last', 'resume_last'],
|
|
1007
|
+
});
|
|
1008
|
+
const otherMemberRequest = await createWorkRequest({
|
|
1009
|
+
teamRunId: teamRun.id,
|
|
1010
|
+
targetMemberId: members[1].id,
|
|
1011
|
+
status: 'STARTED',
|
|
1012
|
+
instruction: 'Other member previous work',
|
|
1013
|
+
});
|
|
1014
|
+
const otherMemberSession = await prisma.session.create({
|
|
1015
|
+
data: {
|
|
1016
|
+
workspaceId: workspace.id,
|
|
1017
|
+
agentType: AgentType.CODEX,
|
|
1018
|
+
providerId: members[1].providerId,
|
|
1019
|
+
prompt: 'other member prompt',
|
|
1020
|
+
status: 'COMPLETED',
|
|
1021
|
+
logSnapshot: JSON.stringify({ sessionId: 'agent-native-session-2', entries: [] }),
|
|
1022
|
+
},
|
|
1023
|
+
});
|
|
1024
|
+
await prisma.agentInvocation.create({
|
|
1025
|
+
data: {
|
|
1026
|
+
teamRunId: teamRun.id,
|
|
1027
|
+
workRequestId: otherMemberRequest.id,
|
|
1028
|
+
memberId: members[1].id,
|
|
1029
|
+
workspaceId: workspace.id,
|
|
1030
|
+
sessionId: otherMemberSession.id,
|
|
1031
|
+
status: 'COMPLETED',
|
|
1032
|
+
},
|
|
1033
|
+
});
|
|
1034
|
+
const nextRequest = await createWorkRequest({
|
|
1035
|
+
teamRunId: teamRun.id,
|
|
1036
|
+
targetMemberId: members[0].id,
|
|
1037
|
+
instruction: 'Member 1 fresh work',
|
|
1038
|
+
});
|
|
1039
|
+
const sessionManager = createSessionManagerMock();
|
|
1040
|
+
service = new TeamSchedulerService(lockService, {
|
|
1041
|
+
workspaceService: createWorkspaceServiceMock(),
|
|
1042
|
+
sessionManager,
|
|
1043
|
+
getProviderById: createProviderLookup(),
|
|
1044
|
+
});
|
|
1045
|
+
const invocations = await service.startNextSessions(teamRun.id);
|
|
1046
|
+
expect(invocations).toHaveLength(1);
|
|
1047
|
+
expect(invocations[0]).toMatchObject({
|
|
1048
|
+
workRequestId: nextRequest.id,
|
|
1049
|
+
memberId: members[0].id,
|
|
1050
|
+
status: 'RUNNING',
|
|
1051
|
+
});
|
|
1052
|
+
expect(sessionManager.start).toHaveBeenCalledWith(invocations[0].sessionId);
|
|
1053
|
+
expect(sessionManager.startFollowUp).not.toHaveBeenCalled();
|
|
1054
|
+
});
|
|
1055
|
+
it('resume_last only resumes native context from the same member in the selected workspace', async () => {
|
|
1056
|
+
const { task, workspace: selectedWorkspace, teamRun, members } = await createTeamRunFixture({
|
|
1057
|
+
sessionPolicies: ['resume_last'],
|
|
1058
|
+
});
|
|
1059
|
+
const otherWorkspace = await prisma.workspace.create({
|
|
1060
|
+
data: {
|
|
1061
|
+
taskId: task.id,
|
|
1062
|
+
parentWorkspaceId: selectedWorkspace.id,
|
|
1063
|
+
branchName: 'other-workspace',
|
|
1064
|
+
worktreePath: path.join(testDir, 'other-workspace'),
|
|
1065
|
+
status: 'ACTIVE',
|
|
1066
|
+
},
|
|
1067
|
+
});
|
|
1068
|
+
const previousRequest = await createWorkRequest({
|
|
1069
|
+
teamRunId: teamRun.id,
|
|
1070
|
+
targetMemberId: members[0].id,
|
|
1071
|
+
status: 'STARTED',
|
|
1072
|
+
instruction: 'Previous work in another workspace',
|
|
1073
|
+
});
|
|
1074
|
+
const otherWorkspaceSession = await prisma.session.create({
|
|
1075
|
+
data: {
|
|
1076
|
+
workspaceId: otherWorkspace.id,
|
|
1077
|
+
agentType: AgentType.CODEX,
|
|
1078
|
+
providerId: members[0].providerId,
|
|
1079
|
+
prompt: 'previous prompt in other workspace',
|
|
1080
|
+
status: 'COMPLETED',
|
|
1081
|
+
logSnapshot: JSON.stringify({ sessionId: 'agent-native-session-other-workspace', entries: [] }),
|
|
1082
|
+
createdAt: new Date(Date.UTC(2026, 0, 1, 0, 0, 0)),
|
|
1083
|
+
updatedAt: new Date(Date.UTC(2026, 0, 1, 0, 0, 0)),
|
|
1084
|
+
},
|
|
1085
|
+
});
|
|
1086
|
+
await prisma.agentInvocation.create({
|
|
1087
|
+
data: {
|
|
1088
|
+
teamRunId: teamRun.id,
|
|
1089
|
+
workRequestId: previousRequest.id,
|
|
1090
|
+
memberId: members[0].id,
|
|
1091
|
+
workspaceId: otherWorkspace.id,
|
|
1092
|
+
sessionId: otherWorkspaceSession.id,
|
|
1093
|
+
status: 'COMPLETED',
|
|
1094
|
+
createdAt: new Date(Date.UTC(2026, 0, 1, 0, 0, 0)),
|
|
1095
|
+
updatedAt: new Date(Date.UTC(2026, 0, 1, 0, 0, 0)),
|
|
1096
|
+
},
|
|
1097
|
+
});
|
|
1098
|
+
const nextRequest = await createWorkRequest({
|
|
1099
|
+
teamRunId: teamRun.id,
|
|
1100
|
+
targetMemberId: members[0].id,
|
|
1101
|
+
instruction: 'Continue in selected workspace',
|
|
1102
|
+
});
|
|
1103
|
+
const sessionManager = createSessionManagerMock();
|
|
1104
|
+
service = new TeamSchedulerService(lockService, {
|
|
1105
|
+
workspaceService: createWorkspaceServiceMock(),
|
|
1106
|
+
sessionManager,
|
|
1107
|
+
getProviderById: createProviderLookup(),
|
|
1108
|
+
});
|
|
1109
|
+
const firstRun = await service.startNextSessions(teamRun.id);
|
|
1110
|
+
expect(firstRun).toHaveLength(1);
|
|
1111
|
+
expect(firstRun[0]).toMatchObject({
|
|
1112
|
+
workRequestId: nextRequest.id,
|
|
1113
|
+
workspaceId: selectedWorkspace.id,
|
|
1114
|
+
status: 'RUNNING',
|
|
1115
|
+
});
|
|
1116
|
+
expect(sessionManager.start).toHaveBeenCalledWith(firstRun[0].sessionId);
|
|
1117
|
+
expect(sessionManager.startFollowUp).not.toHaveBeenCalled();
|
|
1118
|
+
await prisma.session.update({
|
|
1119
|
+
where: { id: firstRun[0].sessionId },
|
|
1120
|
+
data: {
|
|
1121
|
+
status: 'COMPLETED',
|
|
1122
|
+
logSnapshot: JSON.stringify({ sessionId: 'agent-native-session-selected-workspace', entries: [] }),
|
|
1123
|
+
},
|
|
1124
|
+
});
|
|
1125
|
+
await prisma.agentInvocation.update({
|
|
1126
|
+
where: { id: firstRun[0].id },
|
|
1127
|
+
data: { status: 'COMPLETED' },
|
|
1128
|
+
});
|
|
1129
|
+
const followUpRequest = await createWorkRequest({
|
|
1130
|
+
teamRunId: teamRun.id,
|
|
1131
|
+
targetMemberId: members[0].id,
|
|
1132
|
+
instruction: 'Continue again in selected workspace',
|
|
1133
|
+
});
|
|
1134
|
+
sessionManager.start.mockClear();
|
|
1135
|
+
sessionManager.startFollowUp.mockClear();
|
|
1136
|
+
const secondRun = await service.startNextSessions(teamRun.id);
|
|
1137
|
+
expect(secondRun).toHaveLength(1);
|
|
1138
|
+
expect(secondRun[0]).toMatchObject({
|
|
1139
|
+
workRequestId: followUpRequest.id,
|
|
1140
|
+
workspaceId: selectedWorkspace.id,
|
|
1141
|
+
status: 'RUNNING',
|
|
1142
|
+
});
|
|
1143
|
+
expect(sessionManager.startFollowUp).toHaveBeenCalledWith(secondRun[0].sessionId, firstRun[0].sessionId);
|
|
1144
|
+
expect(sessionManager.start).not.toHaveBeenCalled();
|
|
1145
|
+
});
|
|
769
1146
|
it('falls back to a normal session start for resume_last members without previous native context', async () => {
|
|
770
1147
|
const { teamRun, members } = await createTeamRunFixture({
|
|
771
1148
|
sessionPolicies: ['resume_last'],
|
|
@@ -998,6 +1375,73 @@ describe('TeamSchedulerService', () => {
|
|
|
998
1375
|
status: 'QUEUED',
|
|
999
1376
|
});
|
|
1000
1377
|
});
|
|
1378
|
+
it('starts none-policy write and command members on the shared workspace without workspace locks', async () => {
|
|
1379
|
+
const { workspace, teamRun, members } = await createTeamRunFixture({
|
|
1380
|
+
memberCapabilities: [
|
|
1381
|
+
{ ...writeCapabilities, runCommands: true },
|
|
1382
|
+
writeCapabilities,
|
|
1383
|
+
],
|
|
1384
|
+
workspacePolicies: ['none', 'shared'],
|
|
1385
|
+
});
|
|
1386
|
+
const noneRequest = await createWorkRequest({
|
|
1387
|
+
teamRunId: teamRun.id,
|
|
1388
|
+
targetMemberId: members[0].id,
|
|
1389
|
+
});
|
|
1390
|
+
const sharedRequest = await createWorkRequest({
|
|
1391
|
+
teamRunId: teamRun.id,
|
|
1392
|
+
targetMemberId: members[1].id,
|
|
1393
|
+
});
|
|
1394
|
+
const sessionManager = createSessionManagerMock();
|
|
1395
|
+
service = new TeamSchedulerService(lockService, {
|
|
1396
|
+
workspaceService: createWorkspaceServiceMock(),
|
|
1397
|
+
sessionManager,
|
|
1398
|
+
getProviderById: createProviderLookup(),
|
|
1399
|
+
});
|
|
1400
|
+
const invocations = await service.startNextSessions(teamRun.id);
|
|
1401
|
+
expect(invocations).toHaveLength(2);
|
|
1402
|
+
expect(invocations.map((invocation) => invocation.workspaceId)).toEqual([workspace.id, workspace.id]);
|
|
1403
|
+
expect(new Set(invocations.map((invocation) => invocation.workRequestId))).toEqual(new Set([
|
|
1404
|
+
noneRequest.id,
|
|
1405
|
+
sharedRequest.id,
|
|
1406
|
+
]));
|
|
1407
|
+
expect(lockService.listLocks()).toEqual([
|
|
1408
|
+
{ key: `workspace:task:${teamRun.taskId}:write`, ownerId: invocations[1].id },
|
|
1409
|
+
]);
|
|
1410
|
+
});
|
|
1411
|
+
it('keeps shared command locks on the stable task key after creating a real workspace', async () => {
|
|
1412
|
+
const { task, teamRun, members } = await createTeamRunFixture({
|
|
1413
|
+
memberCapabilities: [commandCapabilities, commandCapabilities],
|
|
1414
|
+
withWorkspace: false,
|
|
1415
|
+
});
|
|
1416
|
+
const first = await createWorkRequest({
|
|
1417
|
+
teamRunId: teamRun.id,
|
|
1418
|
+
targetMemberId: members[0].id,
|
|
1419
|
+
});
|
|
1420
|
+
const second = await createWorkRequest({
|
|
1421
|
+
teamRunId: teamRun.id,
|
|
1422
|
+
targetMemberId: members[1].id,
|
|
1423
|
+
});
|
|
1424
|
+
service = new TeamSchedulerService(lockService, {
|
|
1425
|
+
workspaceService: createWorkspaceServiceMock(),
|
|
1426
|
+
sessionManager: createSessionManagerMock(),
|
|
1427
|
+
getProviderById: createProviderLookup(),
|
|
1428
|
+
});
|
|
1429
|
+
const invocations = await service.startNextSessions(teamRun.id);
|
|
1430
|
+
expect(invocations).toHaveLength(1);
|
|
1431
|
+
expect(invocations[0]).toMatchObject({
|
|
1432
|
+
workRequestId: first.id,
|
|
1433
|
+
workspaceId: expect.any(String),
|
|
1434
|
+
status: 'RUNNING',
|
|
1435
|
+
});
|
|
1436
|
+
expect(lockService.listLocks()).toEqual([
|
|
1437
|
+
{ key: `workspace:task:${task.id}:command`, ownerId: invocations[0].id },
|
|
1438
|
+
]);
|
|
1439
|
+
await expect(prisma.workspace.count({ where: { taskId: task.id, status: 'ACTIVE' } })).resolves.toBe(1);
|
|
1440
|
+
await expect(prisma.workRequest.findUnique({ where: { id: second.id } })).resolves.toMatchObject({
|
|
1441
|
+
status: 'QUEUED',
|
|
1442
|
+
});
|
|
1443
|
+
await expect(service.startNextSessions(teamRun.id)).resolves.toEqual([]);
|
|
1444
|
+
});
|
|
1001
1445
|
it('keeps shared writer locks on the stable task key after creating a real workspace', async () => {
|
|
1002
1446
|
const { task, teamRun, members } = await createTeamRunFixture({
|
|
1003
1447
|
memberCapabilities: [writeCapabilities, writeCapabilities],
|
|
@@ -1092,7 +1536,7 @@ describe('TeamSchedulerService', () => {
|
|
|
1092
1536
|
expect(new Set(sessions.map((session) => session.workspaceId))).toEqual(new Set([workspaceIds[0]]));
|
|
1093
1537
|
expect(lockService.listLocks()).toEqual([]);
|
|
1094
1538
|
});
|
|
1095
|
-
it('
|
|
1539
|
+
it('marks a request failed and leaves no session or lock when a provider is missing', async () => {
|
|
1096
1540
|
const { teamRun, members } = await createTeamRunFixture({
|
|
1097
1541
|
memberCapabilities: [writeCapabilities],
|
|
1098
1542
|
withWorkspace: false,
|
|
@@ -1108,18 +1552,104 @@ describe('TeamSchedulerService', () => {
|
|
|
1108
1552
|
sessionManager,
|
|
1109
1553
|
getProviderById: vi.fn(() => null),
|
|
1110
1554
|
});
|
|
1111
|
-
await expect(service.startNextSessions(teamRun.id)).
|
|
1112
|
-
code: 'PROVIDER_NOT_FOUND',
|
|
1113
|
-
message: `Provider not found: ${members[0].providerId}`,
|
|
1114
|
-
});
|
|
1555
|
+
await expect(service.startNextSessions(teamRun.id)).resolves.toEqual([]);
|
|
1115
1556
|
expect(workspaceService.create).not.toHaveBeenCalled();
|
|
1116
1557
|
expect(sessionManager.create).not.toHaveBeenCalled();
|
|
1117
1558
|
expect(lockService.listLocks()).toEqual([]);
|
|
1118
1559
|
await expect(prisma.session.count()).resolves.toBe(0);
|
|
1119
|
-
await expect(prisma.agentInvocation.
|
|
1560
|
+
await expect(prisma.agentInvocation.findFirst({ where: { workRequestId: request.id } })).resolves.toMatchObject({
|
|
1561
|
+
memberId: members[0].id,
|
|
1562
|
+
sessionId: null,
|
|
1563
|
+
status: 'FAILED',
|
|
1564
|
+
});
|
|
1120
1565
|
await expect(prisma.workRequest.findUnique({ where: { id: request.id } })).resolves.toMatchObject({
|
|
1121
|
-
status: '
|
|
1566
|
+
status: 'STARTED',
|
|
1567
|
+
});
|
|
1568
|
+
await expect(service.startNextSessions(teamRun.id)).resolves.toEqual([]);
|
|
1569
|
+
});
|
|
1570
|
+
it('advances an idle TeamRun to review when provider missing failures consume all queued work', async () => {
|
|
1571
|
+
const { task, teamRun, members } = await createTeamRunFixture({
|
|
1572
|
+
memberCapabilities: [writeCapabilities],
|
|
1573
|
+
withWorkspace: false,
|
|
1574
|
+
taskStatus: TaskStatus.IN_PROGRESS,
|
|
1575
|
+
});
|
|
1576
|
+
const request = await createWorkRequest({
|
|
1577
|
+
teamRunId: teamRun.id,
|
|
1578
|
+
targetMemberId: members[0].id,
|
|
1579
|
+
});
|
|
1580
|
+
service = new TeamSchedulerService(lockService, {
|
|
1581
|
+
workspaceService: createWorkspaceServiceMock(),
|
|
1582
|
+
sessionManager: createSessionManagerMock(),
|
|
1583
|
+
getProviderById: vi.fn(() => null),
|
|
1584
|
+
});
|
|
1585
|
+
await expect(service.startNextSessions(teamRun.id)).resolves.toEqual([]);
|
|
1586
|
+
await expect(prisma.workRequest.findUnique({ where: { id: request.id } })).resolves.toMatchObject({
|
|
1587
|
+
status: 'STARTED',
|
|
1588
|
+
});
|
|
1589
|
+
await expect(prisma.agentInvocation.findFirst({ where: { workRequestId: request.id } })).resolves.toMatchObject({
|
|
1590
|
+
status: 'FAILED',
|
|
1591
|
+
sessionId: null,
|
|
1592
|
+
});
|
|
1593
|
+
await expect(prisma.task.findUnique({ where: { id: task.id } })).resolves.toMatchObject({
|
|
1594
|
+
status: TaskStatus.IN_REVIEW,
|
|
1595
|
+
});
|
|
1596
|
+
await expect(prisma.teamRun.findUnique({ where: { id: teamRun.id } })).resolves.toMatchObject({
|
|
1597
|
+
reviewReason: 'TEAM_QUIESCENT',
|
|
1598
|
+
});
|
|
1599
|
+
expect(lockService.listLocks()).toEqual([]);
|
|
1600
|
+
});
|
|
1601
|
+
it('continues starting later queued work when an earlier request has a missing provider', async () => {
|
|
1602
|
+
const { teamRun, members } = await createTeamRunFixture({
|
|
1603
|
+
memberCapabilities: [writeCapabilities, readOnlyCapabilities],
|
|
1604
|
+
withWorkspace: false,
|
|
1605
|
+
});
|
|
1606
|
+
const missingProviderRequest = await createWorkRequest({
|
|
1607
|
+
teamRunId: teamRun.id,
|
|
1608
|
+
targetMemberId: members[0].id,
|
|
1609
|
+
});
|
|
1610
|
+
const validProviderRequest = await createWorkRequest({
|
|
1611
|
+
teamRunId: teamRun.id,
|
|
1612
|
+
targetMemberId: members[1].id,
|
|
1613
|
+
});
|
|
1614
|
+
const workspaceService = createWorkspaceServiceMock();
|
|
1615
|
+
const sessionManager = createSessionManagerMock();
|
|
1616
|
+
service = new TeamSchedulerService(lockService, {
|
|
1617
|
+
workspaceService,
|
|
1618
|
+
sessionManager,
|
|
1619
|
+
getProviderById: vi.fn((providerId) => (providerId === members[0].providerId
|
|
1620
|
+
? null
|
|
1621
|
+
: {
|
|
1622
|
+
id: providerId,
|
|
1623
|
+
name: providerId,
|
|
1624
|
+
agentType: AgentType.CODEX,
|
|
1625
|
+
env: {},
|
|
1626
|
+
config: {},
|
|
1627
|
+
isDefault: false,
|
|
1628
|
+
})),
|
|
1122
1629
|
});
|
|
1630
|
+
const invocations = await service.startNextSessions(teamRun.id);
|
|
1631
|
+
expect(invocations).toHaveLength(1);
|
|
1632
|
+
expect(invocations[0]).toMatchObject({
|
|
1633
|
+
workRequestId: validProviderRequest.id,
|
|
1634
|
+
memberId: members[1].id,
|
|
1635
|
+
status: 'RUNNING',
|
|
1636
|
+
sessionId: expect.any(String),
|
|
1637
|
+
});
|
|
1638
|
+
await expect(prisma.workRequest.findUnique({ where: { id: missingProviderRequest.id } })).resolves.toMatchObject({
|
|
1639
|
+
status: 'STARTED',
|
|
1640
|
+
});
|
|
1641
|
+
await expect(prisma.agentInvocation.findFirst({
|
|
1642
|
+
where: { workRequestId: missingProviderRequest.id },
|
|
1643
|
+
})).resolves.toMatchObject({
|
|
1644
|
+
memberId: members[0].id,
|
|
1645
|
+
sessionId: null,
|
|
1646
|
+
status: 'FAILED',
|
|
1647
|
+
});
|
|
1648
|
+
await expect(prisma.workRequest.findUnique({ where: { id: validProviderRequest.id } })).resolves.toMatchObject({
|
|
1649
|
+
status: 'STARTED',
|
|
1650
|
+
});
|
|
1651
|
+
expect(sessionManager.create).toHaveBeenCalledTimes(1);
|
|
1652
|
+
expect(lockService.listLocks()).toEqual([]);
|
|
1123
1653
|
});
|
|
1124
1654
|
it('marks invocation and session failed and releases locks when session start fails', async () => {
|
|
1125
1655
|
const { teamRun, members } = await createTeamRunFixture({
|