agent-tower 0.5.3-beta.9 → 0.5.4-beta.0
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/mcp/http-client.d.ts +31 -1
- package/dist/mcp/http-client.d.ts.map +1 -1
- package/dist/mcp/http-client.js +14 -2
- package/dist/mcp/http-client.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +62 -8
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools/__tests__/workspaces.test.d.ts +2 -0
- package/dist/mcp/tools/__tests__/workspaces.test.d.ts.map +1 -0
- package/dist/mcp/tools/__tests__/workspaces.test.js +72 -0
- package/dist/mcp/tools/__tests__/workspaces.test.js.map +1 -0
- package/dist/mcp/tools/workspaces.d.ts +9 -1
- package/dist/mcp/tools/workspaces.d.ts.map +1 -1
- package/dist/mcp/tools/workspaces.js +96 -2
- package/dist/mcp/tools/workspaces.js.map +1 -1
- package/dist/mcp/types.d.ts +46 -0
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/mcp/types.js +23 -0
- package/dist/mcp/types.js.map +1 -1
- package/dist/routes/__tests__/team-runs-merge.test.d.ts +2 -0
- package/dist/routes/__tests__/team-runs-merge.test.d.ts.map +1 -0
- package/dist/routes/__tests__/team-runs-merge.test.js +156 -0
- package/dist/routes/__tests__/team-runs-merge.test.js.map +1 -0
- package/dist/routes/__tests__/team-runs-private-messages.test.d.ts +2 -0
- package/dist/routes/__tests__/team-runs-private-messages.test.d.ts.map +1 -0
- package/dist/routes/__tests__/team-runs-private-messages.test.js +82 -0
- package/dist/routes/__tests__/team-runs-private-messages.test.js.map +1 -0
- package/dist/routes/team-runs.d.ts +2 -0
- package/dist/routes/team-runs.d.ts.map +1 -1
- package/dist/routes/team-runs.js +48 -1
- package/dist/routes/team-runs.js.map +1 -1
- package/dist/routes/workspaces.d.ts.map +1 -1
- package/dist/routes/workspaces.js +43 -4
- package/dist/routes/workspaces.js.map +1 -1
- package/dist/services/__tests__/session-manager.team-run.test.js +75 -0
- package/dist/services/__tests__/session-manager.team-run.test.js.map +1 -1
- package/dist/services/__tests__/team-reconciler.service.test.js +127 -8
- package/dist/services/__tests__/team-reconciler.service.test.js.map +1 -1
- package/dist/services/__tests__/team-run.service.test.js +172 -0
- package/dist/services/__tests__/team-run.service.test.js.map +1 -1
- package/dist/services/__tests__/team-scheduler.service.test.js +397 -0
- package/dist/services/__tests__/team-scheduler.service.test.js.map +1 -1
- package/dist/services/__tests__/workspace.service.test.js +959 -3
- package/dist/services/__tests__/workspace.service.test.js.map +1 -1
- package/dist/services/session-manager.d.ts.map +1 -1
- package/dist/services/session-manager.js +16 -0
- package/dist/services/session-manager.js.map +1 -1
- package/dist/services/team-run.service.d.ts +5 -1
- package/dist/services/team-run.service.d.ts.map +1 -1
- package/dist/services/team-run.service.js +63 -2
- package/dist/services/team-run.service.js.map +1 -1
- package/dist/services/team-scheduler.service.d.ts +21 -1
- package/dist/services/team-scheduler.service.d.ts.map +1 -1
- package/dist/services/team-scheduler.service.js +230 -8
- package/dist/services/team-scheduler.service.js.map +1 -1
- package/dist/services/workspace.service.d.ts +56 -1
- package/dist/services/workspace.service.d.ts.map +1 -1
- package/dist/services/workspace.service.js +837 -4
- package/dist/services/workspace.service.js.map +1 -1
- package/node_modules/@agent-tower/shared/dist/types.d.ts +168 -0
- 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 +39 -5
- package/node_modules/@prisma/client/.prisma/client/index-browser.js +34 -0
- package/node_modules/@prisma/client/.prisma/client/index.d.ts +2871 -158
- package/node_modules/@prisma/client/.prisma/client/index.js +39 -5
- package/node_modules/@prisma/client/.prisma/client/package.json +1 -1
- package/node_modules/@prisma/client/.prisma/client/schema.prisma +81 -30
- package/node_modules/@prisma/client/.prisma/client/wasm.js +34 -0
- package/package.json +1 -1
- package/prisma/migrations/20260618010000_add_workspace_verdicts/migration.sql +27 -0
- package/prisma/migrations/20260702000000_add_teamrun_target_commit_fields/migration.sql +38 -0
- package/prisma/schema.prisma +51 -0
|
@@ -41,6 +41,20 @@ const commandCapabilities = {
|
|
|
41
41
|
function stringifyJson(value) {
|
|
42
42
|
return JSON.stringify(value);
|
|
43
43
|
}
|
|
44
|
+
function git(cwd, args) {
|
|
45
|
+
return execFileSync('git', args, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
46
|
+
}
|
|
47
|
+
function initGitRepo(repoPath) {
|
|
48
|
+
fs.mkdirSync(repoPath, { recursive: true });
|
|
49
|
+
git(repoPath, ['init']);
|
|
50
|
+
git(repoPath, ['checkout', '-B', 'main']);
|
|
51
|
+
git(repoPath, ['config', 'user.email', 'test@example.com']);
|
|
52
|
+
git(repoPath, ['config', 'user.name', 'Test User']);
|
|
53
|
+
fs.writeFileSync(path.join(repoPath, 'README.md'), '# test\n');
|
|
54
|
+
git(repoPath, ['add', 'README.md']);
|
|
55
|
+
git(repoPath, ['commit', '-m', 'initial commit']);
|
|
56
|
+
return git(repoPath, ['rev-parse', 'HEAD']).trim();
|
|
57
|
+
}
|
|
44
58
|
async function createTask(title = 'Team scheduler task', status = TaskStatus.TODO) {
|
|
45
59
|
const project = await prisma.project.create({
|
|
46
60
|
data: {
|
|
@@ -103,6 +117,7 @@ async function createWorkRequest(options) {
|
|
|
103
117
|
requesterMemberId: null,
|
|
104
118
|
requesterType: 'user',
|
|
105
119
|
targetMemberId: options.targetMemberId,
|
|
120
|
+
...(options.target ?? {}),
|
|
106
121
|
triggerMessageId: options.triggerMessageId ?? `message-${Math.random().toString(16).slice(2)}`,
|
|
107
122
|
instruction: options.instruction ?? 'Please do the work',
|
|
108
123
|
ifBusy: options.ifBusy ?? 'queue',
|
|
@@ -1001,6 +1016,308 @@ describe('TeamSchedulerService', () => {
|
|
|
1001
1016
|
members[1].id,
|
|
1002
1017
|
]));
|
|
1003
1018
|
});
|
|
1019
|
+
it('marks targeted review/test requests for non-dedicated members as failed', async () => {
|
|
1020
|
+
const { workspace: sourceWorkspace, teamRun, members } = await createTeamRunFixture({
|
|
1021
|
+
memberCapabilities: [commandCapabilities],
|
|
1022
|
+
workspacePolicies: ['shared'],
|
|
1023
|
+
});
|
|
1024
|
+
const request = await createWorkRequest({
|
|
1025
|
+
teamRunId: teamRun.id,
|
|
1026
|
+
targetMemberId: members[0].id,
|
|
1027
|
+
target: {
|
|
1028
|
+
targetKind: 'WORKSPACE_COMMIT',
|
|
1029
|
+
targetPurpose: 'REVIEW',
|
|
1030
|
+
targetSourceWorkspaceId: sourceWorkspace.id,
|
|
1031
|
+
targetHeadSha: 'a'.repeat(40),
|
|
1032
|
+
targetBranchName: sourceWorkspace.branchName,
|
|
1033
|
+
},
|
|
1034
|
+
});
|
|
1035
|
+
const sessionManager = createSessionManagerMock();
|
|
1036
|
+
service = new TeamSchedulerService(lockService, {
|
|
1037
|
+
workspaceService: createWorkspaceServiceMock(),
|
|
1038
|
+
sessionManager,
|
|
1039
|
+
getProviderById: createProviderLookup(),
|
|
1040
|
+
});
|
|
1041
|
+
await expect(service.planNext(teamRun.id)).resolves.toEqual([
|
|
1042
|
+
expect.objectContaining({
|
|
1043
|
+
workRequestId: request.id,
|
|
1044
|
+
canStart: false,
|
|
1045
|
+
blockedReason: 'unsupported_workspace_policy',
|
|
1046
|
+
}),
|
|
1047
|
+
]);
|
|
1048
|
+
const invocations = await service.startNextSessions(teamRun.id);
|
|
1049
|
+
expect(invocations).toEqual([]);
|
|
1050
|
+
expect(sessionManager.create).not.toHaveBeenCalled();
|
|
1051
|
+
const invocation = await prisma.agentInvocation.findFirst({ where: { workRequestId: request.id } });
|
|
1052
|
+
expect(invocation).toMatchObject({
|
|
1053
|
+
status: 'FAILED',
|
|
1054
|
+
targetSyncError: expect.stringContaining('workspacePolicy=dedicated'),
|
|
1055
|
+
targetKind: 'WORKSPACE_COMMIT',
|
|
1056
|
+
targetPurpose: 'REVIEW',
|
|
1057
|
+
});
|
|
1058
|
+
expect(invocation?.targetSyncError).toContain('workspacePolicy=shared');
|
|
1059
|
+
await expect(prisma.roomMessage.findFirst({ where: { senderInvocationId: invocation?.id ?? '' } })).resolves.toMatchObject({
|
|
1060
|
+
senderType: 'system',
|
|
1061
|
+
kind: 'system',
|
|
1062
|
+
content: expect.stringContaining('change this TeamMember instance to workspacePolicy=dedicated'),
|
|
1063
|
+
workRequestIds: JSON.stringify([request.id]),
|
|
1064
|
+
});
|
|
1065
|
+
await expect(prisma.workRequest.findUnique({ where: { id: request.id } })).resolves.toMatchObject({
|
|
1066
|
+
status: 'STARTED',
|
|
1067
|
+
});
|
|
1068
|
+
});
|
|
1069
|
+
it('syncs a targeted dedicated execution workspace to the target commit before starting', async () => {
|
|
1070
|
+
const repoPath = path.join(testDir, 'target-sync-success-repo');
|
|
1071
|
+
const targetSha = initGitRepo(repoPath);
|
|
1072
|
+
const { project, task } = await createTask('Target sync success task');
|
|
1073
|
+
await prisma.project.update({ where: { id: project.id }, data: { repoPath } });
|
|
1074
|
+
const teamRun = await prisma.teamRun.create({ data: { taskId: task.id, mode: 'AUTO' } });
|
|
1075
|
+
const mainWorkspace = await prisma.workspace.create({
|
|
1076
|
+
data: {
|
|
1077
|
+
taskId: task.id,
|
|
1078
|
+
branchName: 'main',
|
|
1079
|
+
baseBranch: 'main',
|
|
1080
|
+
worktreePath: repoPath,
|
|
1081
|
+
workingDir: repoPath,
|
|
1082
|
+
status: 'ACTIVE',
|
|
1083
|
+
},
|
|
1084
|
+
});
|
|
1085
|
+
await prisma.teamRun.update({ where: { id: teamRun.id }, data: { mainWorkspaceId: mainWorkspace.id } });
|
|
1086
|
+
const sourceWorkspace = await prisma.workspace.create({
|
|
1087
|
+
data: {
|
|
1088
|
+
taskId: task.id,
|
|
1089
|
+
parentWorkspaceId: mainWorkspace.id,
|
|
1090
|
+
ownerMemberId: null,
|
|
1091
|
+
branchName: 'source-delivery',
|
|
1092
|
+
baseBranch: 'main',
|
|
1093
|
+
worktreePath: repoPath,
|
|
1094
|
+
workingDir: repoPath,
|
|
1095
|
+
status: 'ACTIVE',
|
|
1096
|
+
},
|
|
1097
|
+
});
|
|
1098
|
+
const member = await prisma.teamMember.create({
|
|
1099
|
+
data: {
|
|
1100
|
+
teamRunId: teamRun.id,
|
|
1101
|
+
presetId: null,
|
|
1102
|
+
name: 'Tester',
|
|
1103
|
+
aliases: stringifyJson(['tester']),
|
|
1104
|
+
providerId: 'provider-1',
|
|
1105
|
+
rolePrompt: 'Test role',
|
|
1106
|
+
capabilities: stringifyJson(commandCapabilities),
|
|
1107
|
+
workspacePolicy: 'dedicated',
|
|
1108
|
+
triggerPolicy: 'MENTION_ONLY',
|
|
1109
|
+
sessionPolicy: 'new_per_request',
|
|
1110
|
+
queueManagementPolicy: 'own_only',
|
|
1111
|
+
avatar: null,
|
|
1112
|
+
},
|
|
1113
|
+
});
|
|
1114
|
+
const request = await createWorkRequest({
|
|
1115
|
+
teamRunId: teamRun.id,
|
|
1116
|
+
targetMemberId: member.id,
|
|
1117
|
+
target: {
|
|
1118
|
+
targetKind: 'WORKSPACE_COMMIT',
|
|
1119
|
+
targetPurpose: 'TEST',
|
|
1120
|
+
targetSourceWorkspaceId: sourceWorkspace.id,
|
|
1121
|
+
targetHeadSha: targetSha,
|
|
1122
|
+
targetBranchName: sourceWorkspace.branchName,
|
|
1123
|
+
targetPlanItemId: 'plan-sync',
|
|
1124
|
+
},
|
|
1125
|
+
});
|
|
1126
|
+
const sessionManager = createSessionManagerMock();
|
|
1127
|
+
service = new TeamSchedulerService(lockService, {
|
|
1128
|
+
sessionManager,
|
|
1129
|
+
getProviderById: createProviderLookup(),
|
|
1130
|
+
});
|
|
1131
|
+
const invocations = await service.startNextSessions(teamRun.id);
|
|
1132
|
+
expect(invocations).toHaveLength(1);
|
|
1133
|
+
const invocation = await prisma.agentInvocation.findUniqueOrThrow({ where: { id: invocations[0].id } });
|
|
1134
|
+
expect(invocation).toMatchObject({
|
|
1135
|
+
workRequestId: request.id,
|
|
1136
|
+
targetKind: 'WORKSPACE_COMMIT',
|
|
1137
|
+
targetPurpose: 'TEST',
|
|
1138
|
+
targetSourceWorkspaceId: sourceWorkspace.id,
|
|
1139
|
+
targetHeadSha: targetSha,
|
|
1140
|
+
targetSyncStatus: 'SYNCED',
|
|
1141
|
+
targetSyncError: null,
|
|
1142
|
+
targetPlanItemId: 'plan-sync',
|
|
1143
|
+
targetPort: expect.any(Number),
|
|
1144
|
+
targetVitePort: expect.any(Number),
|
|
1145
|
+
targetE2EPort: expect.any(Number),
|
|
1146
|
+
});
|
|
1147
|
+
expect(invocation.targetExecutionBranch).toBe(`at/team/${teamRun.id.slice(0, 8)}/target/test-${member.id.slice(0, 8)}-${targetSha.slice(0, 12)}`);
|
|
1148
|
+
const executionWorkspace = await prisma.workspace.findUniqueOrThrow({ where: { id: invocation.workspaceId } });
|
|
1149
|
+
expect(executionWorkspace.branchName).toBe(invocation.targetExecutionBranch);
|
|
1150
|
+
expect(git(executionWorkspace.worktreePath, ['rev-parse', 'HEAD']).trim()).toBe(targetSha);
|
|
1151
|
+
expect(sessionManager.create).toHaveBeenCalledWith(executionWorkspace.id, AgentType.CODEX, buildExpectedSessionPrompt('Test role', [
|
|
1152
|
+
'Target sync success task',
|
|
1153
|
+
[
|
|
1154
|
+
'Target commit handoff:',
|
|
1155
|
+
'- purpose: TEST',
|
|
1156
|
+
`- sourceWorkspaceId: ${sourceWorkspace.id}`,
|
|
1157
|
+
`- targetHeadSha: ${targetSha}`,
|
|
1158
|
+
`- sourceBranch: ${sourceWorkspace.branchName}`,
|
|
1159
|
+
'- planItemId: plan-sync',
|
|
1160
|
+
'- The execution workspace is synced to targetHeadSha before this session starts.',
|
|
1161
|
+
'- Record review/test verdicts against sourceWorkspaceId with reviewed_sha=targetHeadSha.',
|
|
1162
|
+
].join('\n'),
|
|
1163
|
+
'Work request summary:\nPlease do the work',
|
|
1164
|
+
].join('\n\n')), 'DEFAULT', member.providerId);
|
|
1165
|
+
}, 15_000);
|
|
1166
|
+
it('fails targeted sync when the execution workspace is dirty', async () => {
|
|
1167
|
+
const repoPath = path.join(testDir, 'target-sync-dirty-repo');
|
|
1168
|
+
const targetSha = initGitRepo(repoPath);
|
|
1169
|
+
const { project, task } = await createTask('Target sync dirty task');
|
|
1170
|
+
await prisma.project.update({ where: { id: project.id }, data: { repoPath } });
|
|
1171
|
+
const teamRun = await prisma.teamRun.create({ data: { taskId: task.id, mode: 'AUTO' } });
|
|
1172
|
+
const mainWorkspace = await prisma.workspace.create({
|
|
1173
|
+
data: {
|
|
1174
|
+
taskId: task.id,
|
|
1175
|
+
branchName: 'main',
|
|
1176
|
+
baseBranch: 'main',
|
|
1177
|
+
worktreePath: repoPath,
|
|
1178
|
+
workingDir: repoPath,
|
|
1179
|
+
status: 'ACTIVE',
|
|
1180
|
+
},
|
|
1181
|
+
});
|
|
1182
|
+
await prisma.teamRun.update({ where: { id: teamRun.id }, data: { mainWorkspaceId: mainWorkspace.id } });
|
|
1183
|
+
const sourceWorkspace = await prisma.workspace.create({
|
|
1184
|
+
data: {
|
|
1185
|
+
taskId: task.id,
|
|
1186
|
+
parentWorkspaceId: mainWorkspace.id,
|
|
1187
|
+
ownerMemberId: null,
|
|
1188
|
+
branchName: 'source-delivery-dirty',
|
|
1189
|
+
baseBranch: 'main',
|
|
1190
|
+
worktreePath: repoPath,
|
|
1191
|
+
workingDir: repoPath,
|
|
1192
|
+
status: 'ACTIVE',
|
|
1193
|
+
},
|
|
1194
|
+
});
|
|
1195
|
+
const member = await prisma.teamMember.create({
|
|
1196
|
+
data: {
|
|
1197
|
+
teamRunId: teamRun.id,
|
|
1198
|
+
presetId: null,
|
|
1199
|
+
name: 'Reviewer',
|
|
1200
|
+
aliases: stringifyJson(['reviewer']),
|
|
1201
|
+
providerId: 'provider-1',
|
|
1202
|
+
rolePrompt: 'Review role',
|
|
1203
|
+
capabilities: stringifyJson(commandCapabilities),
|
|
1204
|
+
workspacePolicy: 'dedicated',
|
|
1205
|
+
triggerPolicy: 'MENTION_ONLY',
|
|
1206
|
+
sessionPolicy: 'new_per_request',
|
|
1207
|
+
queueManagementPolicy: 'own_only',
|
|
1208
|
+
avatar: null,
|
|
1209
|
+
},
|
|
1210
|
+
});
|
|
1211
|
+
const executionWorkspace = await prisma.workspace.create({
|
|
1212
|
+
data: {
|
|
1213
|
+
taskId: task.id,
|
|
1214
|
+
parentWorkspaceId: mainWorkspace.id,
|
|
1215
|
+
ownerMemberId: member.id,
|
|
1216
|
+
branchName: `at/team/${teamRun.id.slice(0, 8)}/member-${member.id.slice(0, 8)}`,
|
|
1217
|
+
baseBranch: mainWorkspace.branchName,
|
|
1218
|
+
worktreePath: await import('../../git/worktree.manager.js').then(async ({ WorktreeManager }) => {
|
|
1219
|
+
const manager = new WorktreeManager(repoPath);
|
|
1220
|
+
return manager.create(`at/team/${teamRun.id.slice(0, 8)}/member-${member.id.slice(0, 8)}`);
|
|
1221
|
+
}),
|
|
1222
|
+
status: 'ACTIVE',
|
|
1223
|
+
},
|
|
1224
|
+
});
|
|
1225
|
+
fs.writeFileSync(path.join(executionWorkspace.worktreePath, 'dirty.txt'), 'dirty');
|
|
1226
|
+
const request = await createWorkRequest({
|
|
1227
|
+
teamRunId: teamRun.id,
|
|
1228
|
+
targetMemberId: member.id,
|
|
1229
|
+
target: {
|
|
1230
|
+
targetKind: 'WORKSPACE_COMMIT',
|
|
1231
|
+
targetPurpose: 'REVIEW',
|
|
1232
|
+
targetSourceWorkspaceId: sourceWorkspace.id,
|
|
1233
|
+
targetHeadSha: targetSha,
|
|
1234
|
+
targetBranchName: sourceWorkspace.branchName,
|
|
1235
|
+
},
|
|
1236
|
+
});
|
|
1237
|
+
const sessionManager = createSessionManagerMock();
|
|
1238
|
+
service = new TeamSchedulerService(lockService, {
|
|
1239
|
+
sessionManager,
|
|
1240
|
+
getProviderById: createProviderLookup(),
|
|
1241
|
+
});
|
|
1242
|
+
const invocations = await service.startNextSessions(teamRun.id);
|
|
1243
|
+
expect(invocations).toEqual([]);
|
|
1244
|
+
expect(sessionManager.create).not.toHaveBeenCalled();
|
|
1245
|
+
await expect(prisma.agentInvocation.findFirst({ where: { workRequestId: request.id } })).resolves.toMatchObject({
|
|
1246
|
+
workspaceId: executionWorkspace.id,
|
|
1247
|
+
status: 'FAILED',
|
|
1248
|
+
targetSyncStatus: 'FAILED',
|
|
1249
|
+
targetSyncError: expect.stringContaining('Execution workspace has uncommitted changes'),
|
|
1250
|
+
});
|
|
1251
|
+
}, 15_000);
|
|
1252
|
+
it('fails targeted sync when the target commit does not exist', async () => {
|
|
1253
|
+
const repoPath = path.join(testDir, 'target-sync-missing-sha-repo');
|
|
1254
|
+
initGitRepo(repoPath);
|
|
1255
|
+
const { project, task } = await createTask('Target sync missing sha task');
|
|
1256
|
+
await prisma.project.update({ where: { id: project.id }, data: { repoPath } });
|
|
1257
|
+
const teamRun = await prisma.teamRun.create({ data: { taskId: task.id, mode: 'AUTO' } });
|
|
1258
|
+
const mainWorkspace = await prisma.workspace.create({
|
|
1259
|
+
data: {
|
|
1260
|
+
taskId: task.id,
|
|
1261
|
+
branchName: 'main',
|
|
1262
|
+
baseBranch: 'main',
|
|
1263
|
+
worktreePath: repoPath,
|
|
1264
|
+
workingDir: repoPath,
|
|
1265
|
+
status: 'ACTIVE',
|
|
1266
|
+
},
|
|
1267
|
+
});
|
|
1268
|
+
await prisma.teamRun.update({ where: { id: teamRun.id }, data: { mainWorkspaceId: mainWorkspace.id } });
|
|
1269
|
+
const sourceWorkspace = await prisma.workspace.create({
|
|
1270
|
+
data: {
|
|
1271
|
+
taskId: task.id,
|
|
1272
|
+
parentWorkspaceId: mainWorkspace.id,
|
|
1273
|
+
ownerMemberId: null,
|
|
1274
|
+
branchName: 'source-delivery-missing',
|
|
1275
|
+
baseBranch: 'main',
|
|
1276
|
+
worktreePath: repoPath,
|
|
1277
|
+
workingDir: repoPath,
|
|
1278
|
+
status: 'ACTIVE',
|
|
1279
|
+
},
|
|
1280
|
+
});
|
|
1281
|
+
const member = await prisma.teamMember.create({
|
|
1282
|
+
data: {
|
|
1283
|
+
teamRunId: teamRun.id,
|
|
1284
|
+
presetId: null,
|
|
1285
|
+
name: 'Reviewer',
|
|
1286
|
+
aliases: stringifyJson(['reviewer']),
|
|
1287
|
+
providerId: 'provider-1',
|
|
1288
|
+
rolePrompt: 'Review role',
|
|
1289
|
+
capabilities: stringifyJson(commandCapabilities),
|
|
1290
|
+
workspacePolicy: 'dedicated',
|
|
1291
|
+
triggerPolicy: 'MENTION_ONLY',
|
|
1292
|
+
sessionPolicy: 'new_per_request',
|
|
1293
|
+
queueManagementPolicy: 'own_only',
|
|
1294
|
+
avatar: null,
|
|
1295
|
+
},
|
|
1296
|
+
});
|
|
1297
|
+
const request = await createWorkRequest({
|
|
1298
|
+
teamRunId: teamRun.id,
|
|
1299
|
+
targetMemberId: member.id,
|
|
1300
|
+
target: {
|
|
1301
|
+
targetKind: 'WORKSPACE_COMMIT',
|
|
1302
|
+
targetPurpose: 'REVIEW',
|
|
1303
|
+
targetSourceWorkspaceId: sourceWorkspace.id,
|
|
1304
|
+
targetHeadSha: 'f'.repeat(40),
|
|
1305
|
+
targetBranchName: sourceWorkspace.branchName,
|
|
1306
|
+
},
|
|
1307
|
+
});
|
|
1308
|
+
const sessionManager = createSessionManagerMock();
|
|
1309
|
+
service = new TeamSchedulerService(lockService, {
|
|
1310
|
+
sessionManager,
|
|
1311
|
+
getProviderById: createProviderLookup(),
|
|
1312
|
+
});
|
|
1313
|
+
await expect(service.startNextSessions(teamRun.id)).resolves.toEqual([]);
|
|
1314
|
+
expect(sessionManager.create).not.toHaveBeenCalled();
|
|
1315
|
+
await expect(prisma.agentInvocation.findFirst({ where: { workRequestId: request.id } })).resolves.toMatchObject({
|
|
1316
|
+
status: 'FAILED',
|
|
1317
|
+
targetSyncStatus: 'FAILED',
|
|
1318
|
+
targetSyncError: expect.stringContaining('git cat-file -e'),
|
|
1319
|
+
});
|
|
1320
|
+
}, 15_000);
|
|
1004
1321
|
it('records the dedicated child workspace for queued no-session invocations', async () => {
|
|
1005
1322
|
const { workspace: mainWorkspace, teamRun, members } = await createTeamRunFixture({
|
|
1006
1323
|
memberCapabilities: [writeCapabilities],
|
|
@@ -1299,6 +1616,86 @@ describe('TeamSchedulerService', () => {
|
|
|
1299
1616
|
status: 'RUNNING',
|
|
1300
1617
|
});
|
|
1301
1618
|
});
|
|
1619
|
+
it('does not resume targeted requests across different targetHeadSha values', async () => {
|
|
1620
|
+
const { workspace, teamRun, members } = await createTeamRunFixture({
|
|
1621
|
+
sessionPolicies: ['resume_last'],
|
|
1622
|
+
});
|
|
1623
|
+
const previousRequest = await createWorkRequest({
|
|
1624
|
+
teamRunId: teamRun.id,
|
|
1625
|
+
targetMemberId: members[0].id,
|
|
1626
|
+
status: 'STARTED',
|
|
1627
|
+
target: {
|
|
1628
|
+
targetKind: 'WORKSPACE_COMMIT',
|
|
1629
|
+
targetPurpose: 'REVIEW',
|
|
1630
|
+
targetSourceWorkspaceId: workspace.id,
|
|
1631
|
+
targetHeadSha: '1'.repeat(40),
|
|
1632
|
+
targetBranchName: workspace.branchName,
|
|
1633
|
+
},
|
|
1634
|
+
});
|
|
1635
|
+
const previousSession = await prisma.session.create({
|
|
1636
|
+
data: {
|
|
1637
|
+
workspaceId: workspace.id,
|
|
1638
|
+
agentType: AgentType.CODEX,
|
|
1639
|
+
providerId: members[0].providerId,
|
|
1640
|
+
prompt: 'previous prompt',
|
|
1641
|
+
status: 'COMPLETED',
|
|
1642
|
+
logSnapshot: JSON.stringify({ sessionId: 'agent-native-session-previous-target', entries: [] }),
|
|
1643
|
+
},
|
|
1644
|
+
});
|
|
1645
|
+
await prisma.agentInvocation.create({
|
|
1646
|
+
data: {
|
|
1647
|
+
teamRunId: teamRun.id,
|
|
1648
|
+
workRequestId: previousRequest.id,
|
|
1649
|
+
memberId: members[0].id,
|
|
1650
|
+
workspaceId: workspace.id,
|
|
1651
|
+
sessionId: previousSession.id,
|
|
1652
|
+
targetKind: 'WORKSPACE_COMMIT',
|
|
1653
|
+
targetPurpose: 'REVIEW',
|
|
1654
|
+
targetSourceWorkspaceId: workspace.id,
|
|
1655
|
+
targetHeadSha: '1'.repeat(40),
|
|
1656
|
+
targetBranchName: workspace.branchName,
|
|
1657
|
+
targetSyncStatus: 'SYNCED',
|
|
1658
|
+
status: 'COMPLETED',
|
|
1659
|
+
},
|
|
1660
|
+
});
|
|
1661
|
+
const nextRequest = await createWorkRequest({
|
|
1662
|
+
teamRunId: teamRun.id,
|
|
1663
|
+
targetMemberId: members[0].id,
|
|
1664
|
+
instruction: 'Review a different target',
|
|
1665
|
+
target: {
|
|
1666
|
+
targetKind: 'WORKSPACE_COMMIT',
|
|
1667
|
+
targetPurpose: 'REVIEW',
|
|
1668
|
+
targetSourceWorkspaceId: workspace.id,
|
|
1669
|
+
targetHeadSha: '2'.repeat(40),
|
|
1670
|
+
targetBranchName: workspace.branchName,
|
|
1671
|
+
},
|
|
1672
|
+
});
|
|
1673
|
+
const sessionManager = createSessionManagerMock();
|
|
1674
|
+
service = new TeamSchedulerService(lockService, {
|
|
1675
|
+
workspaceService: {
|
|
1676
|
+
create: vi.fn(),
|
|
1677
|
+
getOrCreateDedicatedWorkspace: vi.fn(async () => workspace),
|
|
1678
|
+
prepareTargetedExecutionWorkspace: vi.fn(async () => ({
|
|
1679
|
+
executionBranch: 'target-review-branch',
|
|
1680
|
+
})),
|
|
1681
|
+
},
|
|
1682
|
+
sessionManager,
|
|
1683
|
+
getProviderById: createProviderLookup(),
|
|
1684
|
+
});
|
|
1685
|
+
await prisma.teamMember.update({
|
|
1686
|
+
where: { id: members[0].id },
|
|
1687
|
+
data: { workspacePolicy: 'dedicated' },
|
|
1688
|
+
});
|
|
1689
|
+
const invocations = await service.startNextSessions(teamRun.id);
|
|
1690
|
+
expect(invocations).toHaveLength(1);
|
|
1691
|
+
expect(invocations[0]).toMatchObject({
|
|
1692
|
+
workRequestId: nextRequest.id,
|
|
1693
|
+
targetHeadSha: '2'.repeat(40),
|
|
1694
|
+
targetSyncStatus: 'SYNCED',
|
|
1695
|
+
});
|
|
1696
|
+
expect(sessionManager.start).toHaveBeenCalledWith(invocations[0].sessionId);
|
|
1697
|
+
expect(sessionManager.startFollowUp).not.toHaveBeenCalled();
|
|
1698
|
+
});
|
|
1302
1699
|
it('starts new_per_request members without resuming previous native context', async () => {
|
|
1303
1700
|
const { workspace, teamRun, members } = await createTeamRunFixture({
|
|
1304
1701
|
sessionPolicies: ['new_per_request'],
|