github-issue-tower-defence-management 1.40.0 → 1.42.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/.github/workflows/umino-project.yml +5 -4
- package/CHANGELOG.md +20 -0
- package/README.md +27 -9
- package/bin/adapter/entry-points/cli/index.js +68 -10
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +44 -8
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/NodeLocalCommandRunner.js +3 -3
- package/bin/adapter/repositories/NodeLocalCommandRunner.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +412 -177
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +6 -2
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +7 -2
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +115 -72
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +184 -13
- package/src/adapter/entry-points/cli/index.ts +105 -13
- package/src/adapter/repositories/NodeLocalCommandRunner.test.ts +12 -12
- package/src/adapter/repositories/NodeLocalCommandRunner.ts +7 -4
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +3 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +626 -265
- package/src/adapter/repositories/issue/RestIssueRepository.test.ts +3 -0
- package/src/domain/entities/Issue.ts +1 -0
- package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +1 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +13 -3
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +64 -9
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +8 -3
- package/src/domain/usecases/StartPreparationUseCase.test.ts +1978 -295
- package/src/domain/usecases/StartPreparationUseCase.ts +185 -126
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +2 -1
- package/src/domain/usecases/adapter-interfaces/LocalCommandRunner.ts +4 -1
- package/types/adapter/entry-points/cli/index.d.ts +5 -1
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/repositories/NodeLocalCommandRunner.d.ts +1 -1
- package/types/adapter/repositories/NodeLocalCommandRunner.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +5 -3
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/entities/Issue.d.ts +1 -0
- package/types/domain/entities/Issue.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +6 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts +11 -18
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +2 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts +1 -1
- package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts.map +1 -1
|
@@ -1,19 +1,38 @@
|
|
|
1
1
|
import { StartPreparationUseCase } from './StartPreparationUseCase';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
IssueRepository,
|
|
4
|
+
RelatedPullRequest,
|
|
5
|
+
} from './adapter-interfaces/IssueRepository';
|
|
3
6
|
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
4
7
|
import { LocalCommandRunner } from './adapter-interfaces/LocalCommandRunner';
|
|
5
8
|
import { ClaudeRepository } from './adapter-interfaces/ClaudeRepository';
|
|
6
9
|
import { Issue } from '../entities/Issue';
|
|
7
10
|
import { Project } from '../entities/Project';
|
|
11
|
+
import { StoryObjectMap } from '../entities/StoryObjectMap';
|
|
8
12
|
type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
|
|
9
13
|
|
|
14
|
+
const createMockStoryObjectMap = (issues: Issue[]): StoryObjectMap => {
|
|
15
|
+
const map: StoryObjectMap = new Map();
|
|
16
|
+
map.set('Default Story', {
|
|
17
|
+
story: {
|
|
18
|
+
id: 'story-1',
|
|
19
|
+
name: 'Default Story',
|
|
20
|
+
color: 'GRAY',
|
|
21
|
+
description: '',
|
|
22
|
+
},
|
|
23
|
+
storyIssue: null,
|
|
24
|
+
issues: issues,
|
|
25
|
+
});
|
|
26
|
+
return map;
|
|
27
|
+
};
|
|
28
|
+
|
|
10
29
|
const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
|
|
11
30
|
nameWithOwner: 'user/repo',
|
|
12
31
|
number: 1,
|
|
13
32
|
title: 'Test Issue',
|
|
14
33
|
state: 'OPEN',
|
|
15
34
|
status: 'Backlog',
|
|
16
|
-
story:
|
|
35
|
+
story: null,
|
|
17
36
|
nextActionDate: null,
|
|
18
37
|
nextActionHour: null,
|
|
19
38
|
estimationMinutes: null,
|
|
@@ -30,12 +49,13 @@ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
|
|
|
30
49
|
isInProgress: false,
|
|
31
50
|
isClosed: false,
|
|
32
51
|
createdAt: new Date(),
|
|
52
|
+
author: 'testuser',
|
|
33
53
|
...overrides,
|
|
34
54
|
});
|
|
35
55
|
|
|
36
56
|
const createMockProject = (): Project => ({
|
|
37
57
|
id: 'project-1',
|
|
38
|
-
url: 'https://github.com/
|
|
58
|
+
url: 'https://github.com/users/user/projects/1',
|
|
39
59
|
databaseId: 1,
|
|
40
60
|
name: 'Test Project',
|
|
41
61
|
status: {
|
|
@@ -49,23 +69,7 @@ const createMockProject = (): Project => ({
|
|
|
49
69
|
},
|
|
50
70
|
nextActionDate: null,
|
|
51
71
|
nextActionHour: null,
|
|
52
|
-
story:
|
|
53
|
-
name: 'Story',
|
|
54
|
-
fieldId: 'story-field-id',
|
|
55
|
-
databaseId: 1,
|
|
56
|
-
stories: [
|
|
57
|
-
{
|
|
58
|
-
id: 'story-1',
|
|
59
|
-
name: 'Default Story',
|
|
60
|
-
color: 'GRAY',
|
|
61
|
-
description: '',
|
|
62
|
-
},
|
|
63
|
-
],
|
|
64
|
-
workflowManagementStory: {
|
|
65
|
-
id: 'wf-1',
|
|
66
|
-
name: 'Workflow Management',
|
|
67
|
-
},
|
|
68
|
-
},
|
|
72
|
+
story: null,
|
|
69
73
|
remainingEstimationMinutes: null,
|
|
70
74
|
dependedIssueUrlSeparatedByComma: null,
|
|
71
75
|
completionDate50PercentConfidence: null,
|
|
@@ -74,10 +78,17 @@ const createMockProject = (): Project => ({
|
|
|
74
78
|
describe('StartPreparationUseCase', () => {
|
|
75
79
|
let useCase: StartPreparationUseCase;
|
|
76
80
|
let mockProjectRepository: Mocked<
|
|
77
|
-
Pick<ProjectRepository, '
|
|
81
|
+
Pick<ProjectRepository, 'getByUrl' | 'prepareStatus'>
|
|
78
82
|
>;
|
|
79
83
|
let mockIssueRepository: Mocked<
|
|
80
|
-
Pick<
|
|
84
|
+
Pick<
|
|
85
|
+
IssueRepository,
|
|
86
|
+
| 'getAllOpened'
|
|
87
|
+
| 'getStoryObjectMap'
|
|
88
|
+
| 'update'
|
|
89
|
+
| 'findRelatedOpenPRs'
|
|
90
|
+
| 'getOpenPullRequest'
|
|
91
|
+
>
|
|
81
92
|
>;
|
|
82
93
|
let mockClaudeRepository: Mocked<Pick<ClaudeRepository, 'getUsage'>>;
|
|
83
94
|
let mockLocalCommandRunner: Mocked<LocalCommandRunner>;
|
|
@@ -86,14 +97,19 @@ describe('StartPreparationUseCase', () => {
|
|
|
86
97
|
jest.resetAllMocks();
|
|
87
98
|
mockProject = createMockProject();
|
|
88
99
|
mockProjectRepository = {
|
|
89
|
-
|
|
90
|
-
|
|
100
|
+
getByUrl: jest.fn(),
|
|
101
|
+
prepareStatus: jest
|
|
102
|
+
.fn()
|
|
103
|
+
.mockImplementation((_name: string, project: Project) =>
|
|
104
|
+
Promise.resolve(project),
|
|
105
|
+
),
|
|
91
106
|
};
|
|
92
107
|
mockIssueRepository = {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
108
|
+
getAllOpened: jest.fn(),
|
|
109
|
+
getStoryObjectMap: jest.fn().mockResolvedValue(new Map()),
|
|
110
|
+
update: jest.fn(),
|
|
111
|
+
findRelatedOpenPRs: jest.fn().mockResolvedValue([]),
|
|
112
|
+
getOpenPullRequest: jest.fn().mockResolvedValue(null),
|
|
97
113
|
};
|
|
98
114
|
mockClaudeRepository = {
|
|
99
115
|
getUsage: jest.fn().mockResolvedValue([]),
|
|
@@ -108,6 +124,44 @@ describe('StartPreparationUseCase', () => {
|
|
|
108
124
|
mockLocalCommandRunner,
|
|
109
125
|
);
|
|
110
126
|
});
|
|
127
|
+
it('should call prepareStatus for awaitingWorkspaceStatus and preparationStatus with chained project objects', async () => {
|
|
128
|
+
const projectAfterFirstPrepare = createMockProject();
|
|
129
|
+
const projectAfterSecondPrepare = createMockProject();
|
|
130
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
131
|
+
mockProjectRepository.prepareStatus
|
|
132
|
+
.mockResolvedValueOnce(projectAfterFirstPrepare)
|
|
133
|
+
.mockResolvedValueOnce(projectAfterSecondPrepare);
|
|
134
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
135
|
+
createMockStoryObjectMap([]),
|
|
136
|
+
);
|
|
137
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce([]);
|
|
138
|
+
|
|
139
|
+
await useCase.run({
|
|
140
|
+
projectUrl: 'https://github.com/user/repo',
|
|
141
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
142
|
+
preparationStatus: 'Preparation',
|
|
143
|
+
defaultAgentName: 'agent1',
|
|
144
|
+
defaultLlmModelName: null,
|
|
145
|
+
defaultLlmAgentName: null,
|
|
146
|
+
configFilePath: '/path/to/config.yml',
|
|
147
|
+
maximumPreparingIssuesCount: null,
|
|
148
|
+
utilizationPercentageThreshold: 90,
|
|
149
|
+
allowedIssueAuthors: null,
|
|
150
|
+
codexHomeCandidates: null,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(mockProjectRepository.prepareStatus).toHaveBeenCalledTimes(2);
|
|
154
|
+
expect(mockProjectRepository.prepareStatus).toHaveBeenNthCalledWith(
|
|
155
|
+
1,
|
|
156
|
+
'Awaiting Workspace',
|
|
157
|
+
mockProject,
|
|
158
|
+
);
|
|
159
|
+
expect(mockProjectRepository.prepareStatus).toHaveBeenNthCalledWith(
|
|
160
|
+
2,
|
|
161
|
+
'Preparation',
|
|
162
|
+
projectAfterFirstPrepare,
|
|
163
|
+
);
|
|
164
|
+
});
|
|
111
165
|
it('should run aw command for awaiting workspace issues', async () => {
|
|
112
166
|
const awaitingIssues: Issue[] = [
|
|
113
167
|
createMockIssue({
|
|
@@ -117,11 +171,11 @@ describe('StartPreparationUseCase', () => {
|
|
|
117
171
|
status: 'Awaiting Workspace',
|
|
118
172
|
}),
|
|
119
173
|
];
|
|
120
|
-
mockProjectRepository.
|
|
121
|
-
mockIssueRepository.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
174
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
175
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
176
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
177
|
+
);
|
|
178
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
125
179
|
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
126
180
|
stdout: '',
|
|
127
181
|
stderr: '',
|
|
@@ -132,18 +186,391 @@ describe('StartPreparationUseCase', () => {
|
|
|
132
186
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
133
187
|
preparationStatus: 'Preparation',
|
|
134
188
|
defaultAgentName: 'agent1',
|
|
189
|
+
defaultLlmModelName: 'claude-opus',
|
|
190
|
+
defaultLlmAgentName: null,
|
|
191
|
+
configFilePath: '/path/to/config.yml',
|
|
135
192
|
maximumPreparingIssuesCount: null,
|
|
136
|
-
|
|
193
|
+
utilizationPercentageThreshold: 90,
|
|
194
|
+
allowedIssueAuthors: null,
|
|
195
|
+
codexHomeCandidates: null,
|
|
137
196
|
});
|
|
138
|
-
expect(mockIssueRepository.
|
|
139
|
-
expect(mockIssueRepository.
|
|
197
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(1);
|
|
198
|
+
expect(mockIssueRepository.update.mock.calls[0][0]).toMatchObject({
|
|
140
199
|
url: 'url1',
|
|
200
|
+
status: 'Preparation',
|
|
201
|
+
});
|
|
202
|
+
expect(mockIssueRepository.update.mock.calls[0][1]).toBe(mockProject);
|
|
203
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
204
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
205
|
+
'aw',
|
|
206
|
+
[
|
|
207
|
+
'url1',
|
|
208
|
+
'impl',
|
|
209
|
+
'claude-opus',
|
|
210
|
+
'--configFilePath',
|
|
211
|
+
'/path/to/config.yml',
|
|
212
|
+
'--branch',
|
|
213
|
+
'i1',
|
|
214
|
+
],
|
|
215
|
+
]);
|
|
216
|
+
});
|
|
217
|
+
it('should pass --branch to aw command when issue has an existing linked PR', async () => {
|
|
218
|
+
const awaitingIssues: Issue[] = [
|
|
219
|
+
createMockIssue({
|
|
220
|
+
url: 'url1',
|
|
221
|
+
title: 'Issue 1',
|
|
222
|
+
labels: ['category:impl'],
|
|
223
|
+
status: 'Awaiting Workspace',
|
|
224
|
+
}),
|
|
225
|
+
];
|
|
226
|
+
const existingPR: RelatedPullRequest = {
|
|
227
|
+
url: 'https://github.com/user/repo/pull/42',
|
|
228
|
+
branchName: 'i1',
|
|
229
|
+
isConflicted: false,
|
|
230
|
+
isPassedAllCiJob: false,
|
|
231
|
+
isCiStateSuccess: false,
|
|
232
|
+
isResolvedAllReviewComments: false,
|
|
233
|
+
isBranchOutOfDate: false,
|
|
234
|
+
missingRequiredCheckNames: [],
|
|
235
|
+
};
|
|
236
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
237
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
238
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
239
|
+
);
|
|
240
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
241
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([existingPR]);
|
|
242
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
243
|
+
stdout: '',
|
|
244
|
+
stderr: '',
|
|
245
|
+
exitCode: 0,
|
|
246
|
+
});
|
|
247
|
+
await useCase.run({
|
|
248
|
+
projectUrl: 'https://github.com/user/repo',
|
|
249
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
250
|
+
preparationStatus: 'Preparation',
|
|
251
|
+
defaultAgentName: 'agent1',
|
|
252
|
+
defaultLlmModelName: 'claude-opus',
|
|
253
|
+
defaultLlmAgentName: null,
|
|
254
|
+
configFilePath: '/path/to/config.yml',
|
|
255
|
+
maximumPreparingIssuesCount: null,
|
|
256
|
+
utilizationPercentageThreshold: 90,
|
|
257
|
+
allowedIssueAuthors: null,
|
|
258
|
+
codexHomeCandidates: null,
|
|
259
|
+
});
|
|
260
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
261
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
262
|
+
'aw',
|
|
263
|
+
[
|
|
264
|
+
'url1',
|
|
265
|
+
'impl',
|
|
266
|
+
'claude-opus',
|
|
267
|
+
'--configFilePath',
|
|
268
|
+
'/path/to/config.yml',
|
|
269
|
+
'--branch',
|
|
270
|
+
'i1',
|
|
271
|
+
],
|
|
272
|
+
]);
|
|
273
|
+
});
|
|
274
|
+
it('should pass --branch with PR branch name when issue URL is a PR URL', async () => {
|
|
275
|
+
const awaitingIssues: Issue[] = [
|
|
276
|
+
createMockIssue({
|
|
277
|
+
url: 'https://github.com/user/repo/pull/354',
|
|
278
|
+
title: 'PR 354',
|
|
279
|
+
labels: ['category:impl'],
|
|
280
|
+
status: 'Awaiting Workspace',
|
|
281
|
+
}),
|
|
282
|
+
];
|
|
283
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
284
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
285
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
286
|
+
);
|
|
287
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
288
|
+
mockIssueRepository.getOpenPullRequest.mockResolvedValue({
|
|
289
|
+
url: 'https://github.com/user/repo/pull/354',
|
|
290
|
+
branchName: 'dependabot/npm_and_yarn/multi-cc382f683c',
|
|
291
|
+
isConflicted: false,
|
|
292
|
+
isPassedAllCiJob: false,
|
|
293
|
+
isCiStateSuccess: false,
|
|
294
|
+
isResolvedAllReviewComments: false,
|
|
295
|
+
isBranchOutOfDate: false,
|
|
296
|
+
missingRequiredCheckNames: [],
|
|
297
|
+
});
|
|
298
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
299
|
+
stdout: '',
|
|
300
|
+
stderr: '',
|
|
301
|
+
exitCode: 0,
|
|
302
|
+
});
|
|
303
|
+
await useCase.run({
|
|
304
|
+
projectUrl: 'https://github.com/user/repo',
|
|
305
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
306
|
+
preparationStatus: 'Preparation',
|
|
307
|
+
defaultAgentName: 'agent1',
|
|
308
|
+
defaultLlmModelName: 'claude-opus',
|
|
309
|
+
defaultLlmAgentName: null,
|
|
310
|
+
configFilePath: '/path/to/config.yml',
|
|
311
|
+
maximumPreparingIssuesCount: null,
|
|
312
|
+
utilizationPercentageThreshold: 90,
|
|
313
|
+
allowedIssueAuthors: null,
|
|
314
|
+
codexHomeCandidates: null,
|
|
141
315
|
});
|
|
142
|
-
expect(mockIssueRepository.updateStatus.mock.calls[0][2]).toBe('2');
|
|
143
316
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
144
|
-
expect(mockLocalCommandRunner.runCommand.mock.calls[0]
|
|
145
|
-
'aw
|
|
317
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
318
|
+
'aw',
|
|
319
|
+
[
|
|
320
|
+
'https://github.com/user/repo/pull/354',
|
|
321
|
+
'impl',
|
|
322
|
+
'claude-opus',
|
|
323
|
+
'--configFilePath',
|
|
324
|
+
'/path/to/config.yml',
|
|
325
|
+
'--branch',
|
|
326
|
+
'dependabot/npm_and_yarn/multi-cc382f683c',
|
|
327
|
+
],
|
|
328
|
+
]);
|
|
329
|
+
expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
|
|
330
|
+
expect(mockIssueRepository.getOpenPullRequest).toHaveBeenCalledWith(
|
|
331
|
+
'https://github.com/user/repo/pull/354',
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
it('should skip and not call wrapper when PR URL returns null from getOpenPullRequest', async () => {
|
|
335
|
+
const awaitingIssues: Issue[] = [
|
|
336
|
+
createMockIssue({
|
|
337
|
+
url: 'https://github.com/user/repo/pull/999',
|
|
338
|
+
title: 'PR 999',
|
|
339
|
+
labels: ['category:impl'],
|
|
340
|
+
status: 'Awaiting Workspace',
|
|
341
|
+
}),
|
|
342
|
+
];
|
|
343
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
344
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
345
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
346
|
+
);
|
|
347
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
348
|
+
mockIssueRepository.getOpenPullRequest.mockResolvedValue(null);
|
|
349
|
+
const consoleWarnSpy = jest
|
|
350
|
+
.spyOn(console, 'warn')
|
|
351
|
+
.mockImplementation(() => {});
|
|
352
|
+
await useCase.run({
|
|
353
|
+
projectUrl: 'https://github.com/user/repo',
|
|
354
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
355
|
+
preparationStatus: 'Preparation',
|
|
356
|
+
defaultAgentName: 'agent1',
|
|
357
|
+
defaultLlmModelName: 'claude-opus',
|
|
358
|
+
defaultLlmAgentName: null,
|
|
359
|
+
configFilePath: '/path/to/config.yml',
|
|
360
|
+
maximumPreparingIssuesCount: null,
|
|
361
|
+
utilizationPercentageThreshold: 90,
|
|
362
|
+
allowedIssueAuthors: null,
|
|
363
|
+
codexHomeCandidates: null,
|
|
364
|
+
});
|
|
365
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
366
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(0);
|
|
367
|
+
expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
|
|
368
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
369
|
+
'Skipping non-OPEN PR https://github.com/user/repo/pull/999: wrapper requires an open PR.',
|
|
370
|
+
);
|
|
371
|
+
consoleWarnSpy.mockRestore();
|
|
372
|
+
});
|
|
373
|
+
it('should skip and not call wrapper when PR URL has open PR with null branchName', async () => {
|
|
374
|
+
const awaitingIssues: Issue[] = [
|
|
375
|
+
createMockIssue({
|
|
376
|
+
url: 'https://github.com/user/repo/pull/999',
|
|
377
|
+
title: 'PR 999',
|
|
378
|
+
labels: ['category:impl'],
|
|
379
|
+
status: 'Awaiting Workspace',
|
|
380
|
+
}),
|
|
381
|
+
];
|
|
382
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
383
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
384
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
385
|
+
);
|
|
386
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
387
|
+
mockIssueRepository.getOpenPullRequest.mockResolvedValue({
|
|
388
|
+
url: 'https://github.com/user/repo/pull/999',
|
|
389
|
+
branchName: null,
|
|
390
|
+
isConflicted: false,
|
|
391
|
+
isPassedAllCiJob: false,
|
|
392
|
+
isCiStateSuccess: false,
|
|
393
|
+
isResolvedAllReviewComments: false,
|
|
394
|
+
isBranchOutOfDate: false,
|
|
395
|
+
missingRequiredCheckNames: [],
|
|
396
|
+
});
|
|
397
|
+
const consoleWarnSpy = jest
|
|
398
|
+
.spyOn(console, 'warn')
|
|
399
|
+
.mockImplementation(() => {});
|
|
400
|
+
await useCase.run({
|
|
401
|
+
projectUrl: 'https://github.com/user/repo',
|
|
402
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
403
|
+
preparationStatus: 'Preparation',
|
|
404
|
+
defaultAgentName: 'agent1',
|
|
405
|
+
defaultLlmModelName: 'claude-opus',
|
|
406
|
+
defaultLlmAgentName: null,
|
|
407
|
+
configFilePath: '/path/to/config.yml',
|
|
408
|
+
maximumPreparingIssuesCount: null,
|
|
409
|
+
utilizationPercentageThreshold: 90,
|
|
410
|
+
allowedIssueAuthors: null,
|
|
411
|
+
codexHomeCandidates: null,
|
|
412
|
+
});
|
|
413
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
414
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(0);
|
|
415
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
416
|
+
'Skipping PR https://github.com/user/repo/pull/999: head branch is unavailable.',
|
|
417
|
+
);
|
|
418
|
+
consoleWarnSpy.mockRestore();
|
|
419
|
+
});
|
|
420
|
+
it('should skip and not call wrapper when PR has branch name with shell-unsafe characters', async () => {
|
|
421
|
+
const awaitingIssues: Issue[] = [
|
|
422
|
+
createMockIssue({
|
|
423
|
+
url: 'https://github.com/user/repo/pull/999',
|
|
424
|
+
title: 'PR 999',
|
|
425
|
+
labels: ['category:impl'],
|
|
426
|
+
status: 'Awaiting Workspace',
|
|
427
|
+
}),
|
|
428
|
+
];
|
|
429
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
430
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
431
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
432
|
+
);
|
|
433
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
434
|
+
mockIssueRepository.getOpenPullRequest.mockResolvedValue({
|
|
435
|
+
url: 'https://github.com/user/repo/pull/999',
|
|
436
|
+
branchName: 'evil$(rm -rf /)',
|
|
437
|
+
isConflicted: false,
|
|
438
|
+
isPassedAllCiJob: false,
|
|
439
|
+
isCiStateSuccess: false,
|
|
440
|
+
isResolvedAllReviewComments: false,
|
|
441
|
+
isBranchOutOfDate: false,
|
|
442
|
+
missingRequiredCheckNames: [],
|
|
443
|
+
});
|
|
444
|
+
const consoleErrorSpy = jest
|
|
445
|
+
.spyOn(console, 'error')
|
|
446
|
+
.mockImplementation(() => {});
|
|
447
|
+
await useCase.run({
|
|
448
|
+
projectUrl: 'https://github.com/user/repo',
|
|
449
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
450
|
+
preparationStatus: 'Preparation',
|
|
451
|
+
defaultAgentName: 'agent1',
|
|
452
|
+
defaultLlmModelName: 'claude-opus',
|
|
453
|
+
defaultLlmAgentName: null,
|
|
454
|
+
configFilePath: '/path/to/config.yml',
|
|
455
|
+
maximumPreparingIssuesCount: null,
|
|
456
|
+
utilizationPercentageThreshold: 90,
|
|
457
|
+
allowedIssueAuthors: null,
|
|
458
|
+
codexHomeCandidates: null,
|
|
459
|
+
});
|
|
460
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
461
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(0);
|
|
462
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
463
|
+
expect.stringContaining('branch name contains unexpected characters'),
|
|
464
|
+
);
|
|
465
|
+
consoleErrorSpy.mockRestore();
|
|
466
|
+
});
|
|
467
|
+
it('should skip and not call wrapper when issue has multiple related open PRs', async () => {
|
|
468
|
+
const awaitingIssues: Issue[] = [
|
|
469
|
+
createMockIssue({
|
|
470
|
+
url: 'url1',
|
|
471
|
+
title: 'Issue 1',
|
|
472
|
+
labels: ['category:impl'],
|
|
473
|
+
status: 'Awaiting Workspace',
|
|
474
|
+
}),
|
|
475
|
+
];
|
|
476
|
+
const pr1: RelatedPullRequest = {
|
|
477
|
+
url: 'https://github.com/user/repo/pull/42',
|
|
478
|
+
branchName: 'i1',
|
|
479
|
+
isConflicted: false,
|
|
480
|
+
isPassedAllCiJob: false,
|
|
481
|
+
isCiStateSuccess: false,
|
|
482
|
+
isResolvedAllReviewComments: false,
|
|
483
|
+
isBranchOutOfDate: false,
|
|
484
|
+
missingRequiredCheckNames: [],
|
|
485
|
+
};
|
|
486
|
+
const pr2: RelatedPullRequest = {
|
|
487
|
+
url: 'https://github.com/user/repo/pull/43',
|
|
488
|
+
branchName: 'i1-fix',
|
|
489
|
+
isConflicted: false,
|
|
490
|
+
isPassedAllCiJob: false,
|
|
491
|
+
isCiStateSuccess: false,
|
|
492
|
+
isResolvedAllReviewComments: false,
|
|
493
|
+
isBranchOutOfDate: false,
|
|
494
|
+
missingRequiredCheckNames: [],
|
|
495
|
+
};
|
|
496
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
497
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
498
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
499
|
+
);
|
|
500
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
501
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([pr1, pr2]);
|
|
502
|
+
const consoleWarnSpy = jest
|
|
503
|
+
.spyOn(console, 'warn')
|
|
504
|
+
.mockImplementation(() => {});
|
|
505
|
+
await useCase.run({
|
|
506
|
+
projectUrl: 'https://github.com/user/repo',
|
|
507
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
508
|
+
preparationStatus: 'Preparation',
|
|
509
|
+
defaultAgentName: 'agent1',
|
|
510
|
+
defaultLlmModelName: 'claude-opus',
|
|
511
|
+
defaultLlmAgentName: null,
|
|
512
|
+
configFilePath: '/path/to/config.yml',
|
|
513
|
+
maximumPreparingIssuesCount: null,
|
|
514
|
+
utilizationPercentageThreshold: 90,
|
|
515
|
+
allowedIssueAuthors: null,
|
|
516
|
+
codexHomeCandidates: null,
|
|
517
|
+
});
|
|
518
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
519
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(0);
|
|
520
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
521
|
+
'Skipping issue url1: 2 related open PRs found (ambiguous).',
|
|
522
|
+
);
|
|
523
|
+
consoleWarnSpy.mockRestore();
|
|
524
|
+
});
|
|
525
|
+
it('should skip and not call wrapper when issue has one related open PR with null branchName', async () => {
|
|
526
|
+
const awaitingIssues: Issue[] = [
|
|
527
|
+
createMockIssue({
|
|
528
|
+
url: 'url1',
|
|
529
|
+
title: 'Issue 1',
|
|
530
|
+
labels: ['category:impl'],
|
|
531
|
+
status: 'Awaiting Workspace',
|
|
532
|
+
}),
|
|
533
|
+
];
|
|
534
|
+
const prWithNullBranch: RelatedPullRequest = {
|
|
535
|
+
url: 'https://github.com/user/repo/pull/42',
|
|
536
|
+
branchName: null,
|
|
537
|
+
isConflicted: false,
|
|
538
|
+
isPassedAllCiJob: false,
|
|
539
|
+
isCiStateSuccess: false,
|
|
540
|
+
isResolvedAllReviewComments: false,
|
|
541
|
+
isBranchOutOfDate: false,
|
|
542
|
+
missingRequiredCheckNames: [],
|
|
543
|
+
};
|
|
544
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
545
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
546
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
547
|
+
);
|
|
548
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
549
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
550
|
+
prWithNullBranch,
|
|
551
|
+
]);
|
|
552
|
+
const consoleWarnSpy = jest
|
|
553
|
+
.spyOn(console, 'warn')
|
|
554
|
+
.mockImplementation(() => {});
|
|
555
|
+
await useCase.run({
|
|
556
|
+
projectUrl: 'https://github.com/user/repo',
|
|
557
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
558
|
+
preparationStatus: 'Preparation',
|
|
559
|
+
defaultAgentName: 'agent1',
|
|
560
|
+
defaultLlmModelName: 'claude-opus',
|
|
561
|
+
defaultLlmAgentName: null,
|
|
562
|
+
configFilePath: '/path/to/config.yml',
|
|
563
|
+
maximumPreparingIssuesCount: null,
|
|
564
|
+
utilizationPercentageThreshold: 90,
|
|
565
|
+
allowedIssueAuthors: null,
|
|
566
|
+
codexHomeCandidates: null,
|
|
567
|
+
});
|
|
568
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
569
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(0);
|
|
570
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
571
|
+
'Skipping issue url1: related open PR has unavailable head branch.',
|
|
146
572
|
);
|
|
573
|
+
consoleWarnSpy.mockRestore();
|
|
147
574
|
});
|
|
148
575
|
it('should assign workspace to awaiting issues', async () => {
|
|
149
576
|
const awaitingIssues: Issue[] = [
|
|
@@ -160,11 +587,11 @@ describe('StartPreparationUseCase', () => {
|
|
|
160
587
|
status: 'Awaiting Workspace',
|
|
161
588
|
}),
|
|
162
589
|
];
|
|
163
|
-
mockProjectRepository.
|
|
164
|
-
mockIssueRepository.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
590
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
591
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
592
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
593
|
+
);
|
|
594
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
168
595
|
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
169
596
|
stdout: '',
|
|
170
597
|
stderr: '',
|
|
@@ -175,19 +602,30 @@ describe('StartPreparationUseCase', () => {
|
|
|
175
602
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
176
603
|
preparationStatus: 'Preparation',
|
|
177
604
|
defaultAgentName: 'agent1',
|
|
605
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
606
|
+
defaultLlmAgentName: null,
|
|
607
|
+
configFilePath: '/path/to/config.yml',
|
|
178
608
|
maximumPreparingIssuesCount: null,
|
|
179
|
-
|
|
609
|
+
utilizationPercentageThreshold: 90,
|
|
610
|
+
allowedIssueAuthors: null,
|
|
611
|
+
codexHomeCandidates: null,
|
|
180
612
|
});
|
|
181
|
-
|
|
182
|
-
expect(mockIssueRepository.
|
|
613
|
+
// Both awaiting issues should be updated (forward iteration: url1 first, then url2)
|
|
614
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(2);
|
|
615
|
+
expect(mockIssueRepository.update.mock.calls[0][0]).toMatchObject({
|
|
183
616
|
url: 'url1',
|
|
617
|
+
status: 'Preparation',
|
|
184
618
|
});
|
|
185
|
-
expect(mockIssueRepository.
|
|
619
|
+
expect(mockIssueRepository.update.mock.calls[1][0]).toMatchObject({
|
|
186
620
|
url: 'url2',
|
|
621
|
+
status: 'Preparation',
|
|
187
622
|
});
|
|
623
|
+
expect(mockIssueRepository.update.mock.calls[0][1]).toBe(mockProject);
|
|
188
624
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(2);
|
|
189
625
|
});
|
|
190
626
|
it('should stop assigning after maximum preparing issues count is reached', async () => {
|
|
627
|
+
// When we already have 6 preparation issues and max is 6 (default),
|
|
628
|
+
// the loop condition prevents processing any new issues
|
|
191
629
|
const preparationIssues: Issue[] = Array.from({ length: 6 }, (_, i) =>
|
|
192
630
|
createMockIssue({
|
|
193
631
|
url: `url${i + 1}`,
|
|
@@ -204,11 +642,14 @@ describe('StartPreparationUseCase', () => {
|
|
|
204
642
|
status: 'Awaiting Workspace',
|
|
205
643
|
}),
|
|
206
644
|
];
|
|
207
|
-
mockProjectRepository.
|
|
208
|
-
mockIssueRepository.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
645
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
646
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
647
|
+
createMockStoryObjectMap([...preparationIssues, ...awaitingIssues]),
|
|
648
|
+
);
|
|
649
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce([
|
|
650
|
+
...preparationIssues,
|
|
651
|
+
...awaitingIssues,
|
|
652
|
+
]);
|
|
212
653
|
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
213
654
|
stdout: '',
|
|
214
655
|
stderr: '',
|
|
@@ -219,13 +660,19 @@ describe('StartPreparationUseCase', () => {
|
|
|
219
660
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
220
661
|
preparationStatus: 'Preparation',
|
|
221
662
|
defaultAgentName: 'agent1',
|
|
663
|
+
defaultLlmModelName: null,
|
|
664
|
+
defaultLlmAgentName: null,
|
|
665
|
+
configFilePath: '/path/to/config.yml',
|
|
222
666
|
maximumPreparingIssuesCount: null,
|
|
223
|
-
|
|
667
|
+
utilizationPercentageThreshold: 90,
|
|
668
|
+
allowedIssueAuthors: null,
|
|
669
|
+
codexHomeCandidates: null,
|
|
224
670
|
});
|
|
225
|
-
|
|
671
|
+
// Loop doesn't run because we're already at max (6 >= 6)
|
|
672
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(0);
|
|
226
673
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
227
674
|
});
|
|
228
|
-
it('should
|
|
675
|
+
it('should pass configFilePath to aw command', async () => {
|
|
229
676
|
const awaitingIssues: Issue[] = [
|
|
230
677
|
createMockIssue({
|
|
231
678
|
url: 'url1',
|
|
@@ -234,11 +681,11 @@ describe('StartPreparationUseCase', () => {
|
|
|
234
681
|
status: 'Awaiting Workspace',
|
|
235
682
|
}),
|
|
236
683
|
];
|
|
237
|
-
mockProjectRepository.
|
|
238
|
-
mockIssueRepository.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
684
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
685
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
686
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
687
|
+
);
|
|
688
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
242
689
|
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
243
690
|
stdout: '',
|
|
244
691
|
stderr: '',
|
|
@@ -249,16 +696,29 @@ describe('StartPreparationUseCase', () => {
|
|
|
249
696
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
250
697
|
preparationStatus: 'Preparation',
|
|
251
698
|
defaultAgentName: 'agent1',
|
|
252
|
-
|
|
699
|
+
defaultLlmModelName: 'claude-opus',
|
|
700
|
+
defaultLlmAgentName: null,
|
|
701
|
+
configFilePath: '/path/to/config.yml',
|
|
253
702
|
maximumPreparingIssuesCount: null,
|
|
254
|
-
|
|
703
|
+
utilizationPercentageThreshold: 90,
|
|
704
|
+
allowedIssueAuthors: null,
|
|
705
|
+
codexHomeCandidates: null,
|
|
255
706
|
});
|
|
256
707
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
257
|
-
expect(mockLocalCommandRunner.runCommand.mock.calls[0]
|
|
258
|
-
'aw
|
|
259
|
-
|
|
708
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
709
|
+
'aw',
|
|
710
|
+
[
|
|
711
|
+
'url1',
|
|
712
|
+
'impl',
|
|
713
|
+
'claude-opus',
|
|
714
|
+
'--configFilePath',
|
|
715
|
+
'/path/to/config.yml',
|
|
716
|
+
'--branch',
|
|
717
|
+
'i1',
|
|
718
|
+
],
|
|
719
|
+
]);
|
|
260
720
|
});
|
|
261
|
-
it('should
|
|
721
|
+
it('should use configFilePath in aw command', async () => {
|
|
262
722
|
const awaitingIssues: Issue[] = [
|
|
263
723
|
createMockIssue({
|
|
264
724
|
url: 'url1',
|
|
@@ -267,11 +727,11 @@ describe('StartPreparationUseCase', () => {
|
|
|
267
727
|
status: 'Awaiting Workspace',
|
|
268
728
|
}),
|
|
269
729
|
];
|
|
270
|
-
mockProjectRepository.
|
|
271
|
-
mockIssueRepository.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
730
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
731
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
732
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
733
|
+
);
|
|
734
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
275
735
|
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
276
736
|
stdout: '',
|
|
277
737
|
stderr: '',
|
|
@@ -282,28 +742,42 @@ describe('StartPreparationUseCase', () => {
|
|
|
282
742
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
283
743
|
preparationStatus: 'Preparation',
|
|
284
744
|
defaultAgentName: 'agent1',
|
|
745
|
+
defaultLlmModelName: 'claude-opus',
|
|
746
|
+
defaultLlmAgentName: null,
|
|
747
|
+
configFilePath: '/path/to/config.yml',
|
|
285
748
|
maximumPreparingIssuesCount: null,
|
|
286
|
-
|
|
749
|
+
utilizationPercentageThreshold: 90,
|
|
750
|
+
allowedIssueAuthors: null,
|
|
751
|
+
codexHomeCandidates: null,
|
|
287
752
|
});
|
|
288
753
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
289
|
-
expect(mockLocalCommandRunner.runCommand.mock.calls[0]
|
|
290
|
-
'aw
|
|
291
|
-
|
|
754
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
755
|
+
'aw',
|
|
756
|
+
[
|
|
757
|
+
'url1',
|
|
758
|
+
'impl',
|
|
759
|
+
'claude-opus',
|
|
760
|
+
'--configFilePath',
|
|
761
|
+
'/path/to/config.yml',
|
|
762
|
+
'--branch',
|
|
763
|
+
'i1',
|
|
764
|
+
],
|
|
765
|
+
]);
|
|
292
766
|
});
|
|
293
|
-
it('should
|
|
294
|
-
const
|
|
767
|
+
it('should use llm-agent label over category label and defaultLlmAgentName', async () => {
|
|
768
|
+
const awaitingIssues: Issue[] = [
|
|
295
769
|
createMockIssue({
|
|
296
770
|
url: 'url1',
|
|
297
771
|
title: 'Issue 1',
|
|
298
|
-
labels: [],
|
|
299
|
-
status: '
|
|
772
|
+
labels: ['llm-agent:research', 'category:impl'],
|
|
773
|
+
status: 'Awaiting Workspace',
|
|
300
774
|
}),
|
|
301
775
|
];
|
|
302
|
-
mockProjectRepository.
|
|
303
|
-
mockIssueRepository.
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
776
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
777
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
778
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
779
|
+
);
|
|
780
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
307
781
|
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
308
782
|
stdout: '',
|
|
309
783
|
stderr: '',
|
|
@@ -314,26 +788,42 @@ describe('StartPreparationUseCase', () => {
|
|
|
314
788
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
315
789
|
preparationStatus: 'Preparation',
|
|
316
790
|
defaultAgentName: 'agent1',
|
|
791
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
792
|
+
defaultLlmAgentName: 'default-llm-agent',
|
|
793
|
+
configFilePath: '/path/to/config.yml',
|
|
317
794
|
maximumPreparingIssuesCount: null,
|
|
318
|
-
|
|
795
|
+
utilizationPercentageThreshold: 90,
|
|
796
|
+
allowedIssueAuthors: null,
|
|
797
|
+
codexHomeCandidates: null,
|
|
319
798
|
});
|
|
320
|
-
expect(
|
|
321
|
-
expect(mockLocalCommandRunner.runCommand.mock.calls).
|
|
799
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
800
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
801
|
+
'aw',
|
|
802
|
+
[
|
|
803
|
+
'url1',
|
|
804
|
+
'research',
|
|
805
|
+
'claude-sonnet-4-6',
|
|
806
|
+
'--configFilePath',
|
|
807
|
+
'/path/to/config.yml',
|
|
808
|
+
'--branch',
|
|
809
|
+
'i1',
|
|
810
|
+
],
|
|
811
|
+
]);
|
|
322
812
|
});
|
|
323
|
-
it('should use
|
|
324
|
-
const awaitingIssues: Issue[] =
|
|
813
|
+
it('should use category label over defaultLlmAgentName when no llm-agent label', async () => {
|
|
814
|
+
const awaitingIssues: Issue[] = [
|
|
325
815
|
createMockIssue({
|
|
326
|
-
url:
|
|
327
|
-
title:
|
|
328
|
-
labels: [],
|
|
816
|
+
url: 'url1',
|
|
817
|
+
title: 'Issue 1',
|
|
818
|
+
labels: ['category:impl'],
|
|
329
819
|
status: 'Awaiting Workspace',
|
|
330
820
|
}),
|
|
821
|
+
];
|
|
822
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
823
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
824
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
331
825
|
);
|
|
332
|
-
|
|
333
|
-
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
334
|
-
issues: awaitingIssues,
|
|
335
|
-
cacheUsed: false,
|
|
336
|
-
});
|
|
826
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
337
827
|
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
338
828
|
stdout: '',
|
|
339
829
|
stderr: '',
|
|
@@ -344,26 +834,42 @@ describe('StartPreparationUseCase', () => {
|
|
|
344
834
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
345
835
|
preparationStatus: 'Preparation',
|
|
346
836
|
defaultAgentName: 'agent1',
|
|
347
|
-
|
|
348
|
-
|
|
837
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
838
|
+
defaultLlmAgentName: 'default-llm-agent',
|
|
839
|
+
configFilePath: '/path/to/config.yml',
|
|
840
|
+
maximumPreparingIssuesCount: null,
|
|
841
|
+
utilizationPercentageThreshold: 90,
|
|
842
|
+
allowedIssueAuthors: null,
|
|
843
|
+
codexHomeCandidates: null,
|
|
349
844
|
});
|
|
350
|
-
expect(
|
|
351
|
-
expect(mockLocalCommandRunner.runCommand.mock.calls).
|
|
845
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
846
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
847
|
+
'aw',
|
|
848
|
+
[
|
|
849
|
+
'url1',
|
|
850
|
+
'impl',
|
|
851
|
+
'claude-sonnet-4-6',
|
|
852
|
+
'--configFilePath',
|
|
853
|
+
'/path/to/config.yml',
|
|
854
|
+
'--branch',
|
|
855
|
+
'i1',
|
|
856
|
+
],
|
|
857
|
+
]);
|
|
352
858
|
});
|
|
353
|
-
it('should use
|
|
354
|
-
const awaitingIssues: Issue[] =
|
|
859
|
+
it('should use defaultLlmAgentName over defaultAgentName when no label', async () => {
|
|
860
|
+
const awaitingIssues: Issue[] = [
|
|
355
861
|
createMockIssue({
|
|
356
|
-
url:
|
|
357
|
-
title:
|
|
862
|
+
url: 'url1',
|
|
863
|
+
title: 'Issue 1',
|
|
358
864
|
labels: [],
|
|
359
865
|
status: 'Awaiting Workspace',
|
|
360
866
|
}),
|
|
867
|
+
];
|
|
868
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
869
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
870
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
361
871
|
);
|
|
362
|
-
|
|
363
|
-
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
364
|
-
issues: awaitingIssues,
|
|
365
|
-
cacheUsed: false,
|
|
366
|
-
});
|
|
872
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
367
873
|
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
368
874
|
stdout: '',
|
|
369
875
|
stderr: '',
|
|
@@ -374,197 +880,329 @@ describe('StartPreparationUseCase', () => {
|
|
|
374
880
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
375
881
|
preparationStatus: 'Preparation',
|
|
376
882
|
defaultAgentName: 'agent1',
|
|
883
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
884
|
+
defaultLlmAgentName: 'default-llm-agent',
|
|
885
|
+
configFilePath: '/path/to/config.yml',
|
|
377
886
|
maximumPreparingIssuesCount: null,
|
|
378
|
-
|
|
887
|
+
utilizationPercentageThreshold: 90,
|
|
888
|
+
allowedIssueAuthors: null,
|
|
889
|
+
codexHomeCandidates: null,
|
|
379
890
|
});
|
|
380
|
-
expect(
|
|
381
|
-
expect(mockLocalCommandRunner.runCommand.mock.calls).
|
|
891
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
892
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
893
|
+
'aw',
|
|
894
|
+
[
|
|
895
|
+
'url1',
|
|
896
|
+
'default-llm-agent',
|
|
897
|
+
'claude-sonnet-4-6',
|
|
898
|
+
'--configFilePath',
|
|
899
|
+
'/path/to/config.yml',
|
|
900
|
+
'--branch',
|
|
901
|
+
'i1',
|
|
902
|
+
],
|
|
903
|
+
]);
|
|
382
904
|
});
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
labels: [],
|
|
398
|
-
status: 'Awaiting Workspace',
|
|
399
|
-
state: 'OPEN',
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
const projectWithBlocker = {
|
|
403
|
-
...createMockProject(),
|
|
404
|
-
story: {
|
|
405
|
-
name: 'Story',
|
|
406
|
-
fieldId: 'story-field-id',
|
|
407
|
-
databaseId: 1,
|
|
408
|
-
stories: [
|
|
409
|
-
{
|
|
410
|
-
id: 'story-blocker',
|
|
411
|
-
name: 'Workflow blocker',
|
|
412
|
-
color: 'RED' as const,
|
|
413
|
-
description: '',
|
|
414
|
-
},
|
|
415
|
-
{
|
|
416
|
-
id: 'story-1',
|
|
417
|
-
name: 'Default Story',
|
|
418
|
-
color: 'GRAY' as const,
|
|
419
|
-
description: '',
|
|
420
|
-
},
|
|
421
|
-
],
|
|
422
|
-
workflowManagementStory: {
|
|
423
|
-
id: 'wf-1',
|
|
424
|
-
name: 'Workflow Management',
|
|
425
|
-
},
|
|
426
|
-
},
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
mockProjectRepository.getProject.mockResolvedValue(projectWithBlocker);
|
|
430
|
-
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
431
|
-
issues: [blockerIssue, blockedIssue],
|
|
432
|
-
cacheUsed: false,
|
|
433
|
-
});
|
|
905
|
+
it('should use llm-model label over defaultLlmModelName', async () => {
|
|
906
|
+
const awaitingIssues: Issue[] = [
|
|
907
|
+
createMockIssue({
|
|
908
|
+
url: 'url1',
|
|
909
|
+
title: 'Issue 1',
|
|
910
|
+
labels: ['category:impl', 'llm-model:claude-sonnet'],
|
|
911
|
+
status: 'Awaiting Workspace',
|
|
912
|
+
}),
|
|
913
|
+
];
|
|
914
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
915
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
916
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
917
|
+
);
|
|
918
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
434
919
|
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
435
920
|
stdout: '',
|
|
436
921
|
stderr: '',
|
|
437
922
|
exitCode: 0,
|
|
438
923
|
});
|
|
439
|
-
|
|
440
924
|
await useCase.run({
|
|
441
925
|
projectUrl: 'https://github.com/user/repo',
|
|
442
926
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
443
927
|
preparationStatus: 'Preparation',
|
|
444
928
|
defaultAgentName: 'agent1',
|
|
929
|
+
defaultLlmModelName: 'claude-opus',
|
|
930
|
+
defaultLlmAgentName: null,
|
|
931
|
+
configFilePath: '/path/to/config.yml',
|
|
445
932
|
maximumPreparingIssuesCount: null,
|
|
446
|
-
|
|
933
|
+
utilizationPercentageThreshold: 90,
|
|
934
|
+
allowedIssueAuthors: null,
|
|
935
|
+
codexHomeCandidates: null,
|
|
447
936
|
});
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const blockedRunCommandCalls =
|
|
462
|
-
mockLocalCommandRunner.runCommand.mock.calls.filter((call) =>
|
|
463
|
-
call.some(
|
|
464
|
-
(arg) => typeof arg === 'string' && arg.includes(blockedIssue.url),
|
|
465
|
-
),
|
|
466
|
-
);
|
|
467
|
-
expect(blockedRunCommandCalls).toHaveLength(0);
|
|
937
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
938
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
939
|
+
'aw',
|
|
940
|
+
[
|
|
941
|
+
'url1',
|
|
942
|
+
'impl',
|
|
943
|
+
'claude-sonnet',
|
|
944
|
+
'--configFilePath',
|
|
945
|
+
'/path/to/config.yml',
|
|
946
|
+
'--branch',
|
|
947
|
+
'i1',
|
|
948
|
+
],
|
|
949
|
+
]);
|
|
468
950
|
});
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
951
|
+
it('should log error and skip issue when no llm-model label and no defaultLlmModelName', async () => {
|
|
952
|
+
const awaitingIssues: Issue[] = [
|
|
953
|
+
createMockIssue({
|
|
954
|
+
url: 'url1',
|
|
955
|
+
title: 'Issue 1',
|
|
956
|
+
labels: ['category:impl'],
|
|
957
|
+
status: 'Awaiting Workspace',
|
|
958
|
+
}),
|
|
959
|
+
];
|
|
960
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
961
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
962
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
963
|
+
);
|
|
964
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
965
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
966
|
+
stdout: '',
|
|
967
|
+
stderr: '',
|
|
968
|
+
exitCode: 0,
|
|
478
969
|
});
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
workflowManagementStory: {
|
|
495
|
-
id: 'wf-1',
|
|
496
|
-
name: 'Workflow Management',
|
|
497
|
-
},
|
|
498
|
-
},
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
mockProjectRepository.getProject.mockResolvedValue(projectWithBlocker);
|
|
502
|
-
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
503
|
-
issues: [blockerIssue],
|
|
504
|
-
cacheUsed: false,
|
|
970
|
+
const consoleErrorSpy = jest
|
|
971
|
+
.spyOn(console, 'error')
|
|
972
|
+
.mockImplementation(() => {});
|
|
973
|
+
await useCase.run({
|
|
974
|
+
projectUrl: 'https://github.com/user/repo',
|
|
975
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
976
|
+
preparationStatus: 'Preparation',
|
|
977
|
+
defaultAgentName: 'agent1',
|
|
978
|
+
defaultLlmModelName: null,
|
|
979
|
+
defaultLlmAgentName: null,
|
|
980
|
+
configFilePath: '/path/to/config.yml',
|
|
981
|
+
maximumPreparingIssuesCount: null,
|
|
982
|
+
utilizationPercentageThreshold: 90,
|
|
983
|
+
allowedIssueAuthors: null,
|
|
984
|
+
codexHomeCandidates: null,
|
|
505
985
|
});
|
|
986
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
987
|
+
'No LLM model configured for issue url1. Provide --defaultLlmModelName or add an llm-model: label.',
|
|
988
|
+
);
|
|
989
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
990
|
+
consoleErrorSpy.mockRestore();
|
|
991
|
+
});
|
|
992
|
+
it('should continue processing subsequent issues when one issue has no model configured', async () => {
|
|
993
|
+
const awaitingIssues: Issue[] = [
|
|
994
|
+
createMockIssue({
|
|
995
|
+
url: 'url1',
|
|
996
|
+
title: 'Issue 1 (no model)',
|
|
997
|
+
labels: ['category:impl'],
|
|
998
|
+
status: 'Awaiting Workspace',
|
|
999
|
+
}),
|
|
1000
|
+
createMockIssue({
|
|
1001
|
+
url: 'url2',
|
|
1002
|
+
title: 'Issue 2 (with model label)',
|
|
1003
|
+
labels: ['category:impl', 'llm-model:claude-sonnet-4-6'],
|
|
1004
|
+
status: 'Awaiting Workspace',
|
|
1005
|
+
number: 2,
|
|
1006
|
+
itemId: 'item-2',
|
|
1007
|
+
}),
|
|
1008
|
+
];
|
|
1009
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1010
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1011
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
1012
|
+
);
|
|
1013
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
506
1014
|
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
507
1015
|
stdout: '',
|
|
508
1016
|
stderr: '',
|
|
509
1017
|
exitCode: 0,
|
|
510
1018
|
});
|
|
511
|
-
|
|
1019
|
+
const consoleErrorSpy = jest
|
|
1020
|
+
.spyOn(console, 'error')
|
|
1021
|
+
.mockImplementation(() => {});
|
|
512
1022
|
await useCase.run({
|
|
513
1023
|
projectUrl: 'https://github.com/user/repo',
|
|
514
1024
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
515
1025
|
preparationStatus: 'Preparation',
|
|
516
1026
|
defaultAgentName: 'agent1',
|
|
1027
|
+
defaultLlmModelName: null,
|
|
1028
|
+
defaultLlmAgentName: null,
|
|
1029
|
+
configFilePath: '/path/to/config.yml',
|
|
517
1030
|
maximumPreparingIssuesCount: null,
|
|
518
|
-
|
|
1031
|
+
utilizationPercentageThreshold: 90,
|
|
1032
|
+
allowedIssueAuthors: null,
|
|
1033
|
+
codexHomeCandidates: null,
|
|
519
1034
|
});
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
1035
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1036
|
+
'No LLM model configured for issue url1. Provide --defaultLlmModelName or add an llm-model: label.',
|
|
1037
|
+
);
|
|
1038
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
1039
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
1040
|
+
'aw',
|
|
1041
|
+
[
|
|
1042
|
+
'url2',
|
|
1043
|
+
'impl',
|
|
1044
|
+
'claude-sonnet-4-6',
|
|
1045
|
+
'--configFilePath',
|
|
1046
|
+
'/path/to/config.yml',
|
|
1047
|
+
'--branch',
|
|
1048
|
+
'i2',
|
|
1049
|
+
],
|
|
1050
|
+
]);
|
|
1051
|
+
consoleErrorSpy.mockRestore();
|
|
1052
|
+
});
|
|
1053
|
+
it('should handle no awaiting workspace issues gracefully', async () => {
|
|
1054
|
+
// Test that the loop handles an empty awaiting workspace issues array
|
|
1055
|
+
const preparationIssues: Issue[] = [
|
|
1056
|
+
createMockIssue({
|
|
1057
|
+
url: 'url1',
|
|
1058
|
+
title: 'Issue 1',
|
|
1059
|
+
labels: [],
|
|
1060
|
+
status: 'Preparation',
|
|
1061
|
+
}),
|
|
1062
|
+
];
|
|
1063
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1064
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1065
|
+
createMockStoryObjectMap(preparationIssues),
|
|
1066
|
+
);
|
|
1067
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(preparationIssues);
|
|
1068
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
1069
|
+
stdout: '',
|
|
1070
|
+
stderr: '',
|
|
1071
|
+
exitCode: 0,
|
|
1072
|
+
});
|
|
1073
|
+
await useCase.run({
|
|
1074
|
+
projectUrl: 'https://github.com/user/repo',
|
|
1075
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1076
|
+
preparationStatus: 'Preparation',
|
|
1077
|
+
defaultAgentName: 'agent1',
|
|
1078
|
+
defaultLlmModelName: null,
|
|
1079
|
+
defaultLlmAgentName: null,
|
|
1080
|
+
configFilePath: '/path/to/config.yml',
|
|
1081
|
+
maximumPreparingIssuesCount: null,
|
|
1082
|
+
utilizationPercentageThreshold: 90,
|
|
1083
|
+
allowedIssueAuthors: null,
|
|
1084
|
+
codexHomeCandidates: null,
|
|
1085
|
+
});
|
|
1086
|
+
// No issues are in 'Awaiting Workspace' status, so no updates should happen
|
|
1087
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(0);
|
|
1088
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
1089
|
+
});
|
|
1090
|
+
it('should use custom maximumPreparingIssuesCount when provided', async () => {
|
|
1091
|
+
const awaitingIssues: Issue[] = Array.from({ length: 10 }, (_, i) =>
|
|
1092
|
+
createMockIssue({
|
|
1093
|
+
url: `url${i + 1}`,
|
|
1094
|
+
title: `Issue ${i + 1}`,
|
|
1095
|
+
labels: [],
|
|
1096
|
+
status: 'Awaiting Workspace',
|
|
1097
|
+
}),
|
|
1098
|
+
);
|
|
1099
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1100
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1101
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
1102
|
+
);
|
|
1103
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
1104
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
1105
|
+
stdout: '',
|
|
1106
|
+
stderr: '',
|
|
1107
|
+
exitCode: 0,
|
|
1108
|
+
});
|
|
1109
|
+
await useCase.run({
|
|
1110
|
+
projectUrl: 'https://github.com/user/repo',
|
|
1111
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1112
|
+
preparationStatus: 'Preparation',
|
|
1113
|
+
defaultAgentName: 'agent1',
|
|
1114
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
1115
|
+
defaultLlmAgentName: null,
|
|
1116
|
+
configFilePath: '/path/to/config.yml',
|
|
1117
|
+
maximumPreparingIssuesCount: 3,
|
|
1118
|
+
utilizationPercentageThreshold: 90,
|
|
1119
|
+
allowedIssueAuthors: null,
|
|
1120
|
+
codexHomeCandidates: null,
|
|
1121
|
+
});
|
|
1122
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(3);
|
|
1123
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(3);
|
|
1124
|
+
});
|
|
1125
|
+
it('should use default maximumPreparingIssuesCount of 6 when null is provided', async () => {
|
|
1126
|
+
const awaitingIssues: Issue[] = Array.from({ length: 12 }, (_, i) =>
|
|
1127
|
+
createMockIssue({
|
|
1128
|
+
url: `url${i + 1}`,
|
|
1129
|
+
title: `Issue ${i + 1}`,
|
|
1130
|
+
labels: [],
|
|
1131
|
+
status: 'Awaiting Workspace',
|
|
1132
|
+
}),
|
|
524
1133
|
);
|
|
1134
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1135
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1136
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
1137
|
+
);
|
|
1138
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
1139
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
1140
|
+
stdout: '',
|
|
1141
|
+
stderr: '',
|
|
1142
|
+
exitCode: 0,
|
|
1143
|
+
});
|
|
1144
|
+
await useCase.run({
|
|
1145
|
+
projectUrl: 'https://github.com/user/repo',
|
|
1146
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1147
|
+
preparationStatus: 'Preparation',
|
|
1148
|
+
defaultAgentName: 'agent1',
|
|
1149
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
1150
|
+
defaultLlmAgentName: null,
|
|
1151
|
+
configFilePath: '/path/to/config.yml',
|
|
1152
|
+
maximumPreparingIssuesCount: null,
|
|
1153
|
+
utilizationPercentageThreshold: 90,
|
|
1154
|
+
allowedIssueAuthors: null,
|
|
1155
|
+
codexHomeCandidates: null,
|
|
1156
|
+
});
|
|
1157
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(6);
|
|
1158
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(6);
|
|
525
1159
|
});
|
|
526
1160
|
|
|
527
|
-
it('should
|
|
528
|
-
const
|
|
1161
|
+
it('should not skip issues from repositories with workflow blockers', async () => {
|
|
1162
|
+
const blockerIssue = createMockIssue({
|
|
1163
|
+
url: 'https://github.com/user/repo/issues/100',
|
|
1164
|
+
title: 'Blocker Issue',
|
|
1165
|
+
labels: [],
|
|
1166
|
+
status: 'Awaiting Workspace',
|
|
1167
|
+
state: 'OPEN',
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
const issueInBlockedRepo = createMockIssue({
|
|
529
1171
|
url: 'https://github.com/user/repo/issues/101',
|
|
530
|
-
title: '
|
|
1172
|
+
title: 'Issue in blocked repo',
|
|
531
1173
|
labels: [],
|
|
532
1174
|
status: 'Awaiting Workspace',
|
|
533
1175
|
state: 'OPEN',
|
|
534
1176
|
});
|
|
535
1177
|
|
|
536
|
-
const
|
|
537
|
-
|
|
1178
|
+
const workflowBlockerMap: StoryObjectMap = new Map();
|
|
1179
|
+
workflowBlockerMap.set('Workflow blocker', {
|
|
538
1180
|
story: {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
{
|
|
544
|
-
id: 'story-1',
|
|
545
|
-
name: 'Default Story',
|
|
546
|
-
color: 'GRAY' as const,
|
|
547
|
-
description: '',
|
|
548
|
-
},
|
|
549
|
-
{
|
|
550
|
-
id: 'story-blocker',
|
|
551
|
-
name: 'Workflow blocker',
|
|
552
|
-
color: 'RED' as const,
|
|
553
|
-
description: '',
|
|
554
|
-
},
|
|
555
|
-
],
|
|
556
|
-
workflowManagementStory: {
|
|
557
|
-
id: 'wf-1',
|
|
558
|
-
name: 'Workflow Management',
|
|
559
|
-
},
|
|
1181
|
+
id: 'story-blocker',
|
|
1182
|
+
name: 'Workflow blocker',
|
|
1183
|
+
color: 'RED',
|
|
1184
|
+
description: '',
|
|
560
1185
|
},
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
1186
|
+
storyIssue: null,
|
|
1187
|
+
issues: [blockerIssue],
|
|
1188
|
+
});
|
|
1189
|
+
workflowBlockerMap.set('Default Story', {
|
|
1190
|
+
story: {
|
|
1191
|
+
id: 'story-1',
|
|
1192
|
+
name: 'Default Story',
|
|
1193
|
+
color: 'GRAY',
|
|
1194
|
+
description: '',
|
|
1195
|
+
},
|
|
1196
|
+
storyIssue: null,
|
|
1197
|
+
issues: [issueInBlockedRepo],
|
|
567
1198
|
});
|
|
1199
|
+
|
|
1200
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1201
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(workflowBlockerMap);
|
|
1202
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce([
|
|
1203
|
+
blockerIssue,
|
|
1204
|
+
issueInBlockedRepo,
|
|
1205
|
+
]);
|
|
568
1206
|
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
569
1207
|
stdout: '',
|
|
570
1208
|
stderr: '',
|
|
@@ -576,14 +1214,21 @@ describe('StartPreparationUseCase', () => {
|
|
|
576
1214
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
577
1215
|
preparationStatus: 'Preparation',
|
|
578
1216
|
defaultAgentName: 'agent1',
|
|
1217
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
1218
|
+
defaultLlmAgentName: null,
|
|
1219
|
+
configFilePath: '/path/to/config.yml',
|
|
579
1220
|
maximumPreparingIssuesCount: null,
|
|
580
|
-
|
|
1221
|
+
utilizationPercentageThreshold: 90,
|
|
1222
|
+
allowedIssueAuthors: null,
|
|
1223
|
+
codexHomeCandidates: null,
|
|
581
1224
|
});
|
|
582
1225
|
|
|
583
|
-
expect(mockIssueRepository.
|
|
584
|
-
|
|
585
|
-
|
|
1226
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(2);
|
|
1227
|
+
const updatedUrls = mockIssueRepository.update.mock.calls.map(
|
|
1228
|
+
(call) => call[0].url,
|
|
586
1229
|
);
|
|
1230
|
+
expect(updatedUrls).toContain('https://github.com/user/repo/issues/100');
|
|
1231
|
+
expect(updatedUrls).toContain('https://github.com/user/repo/issues/101');
|
|
587
1232
|
});
|
|
588
1233
|
|
|
589
1234
|
it('should skip preparation when Claude usage is over 90%', async () => {
|
|
@@ -599,24 +1244,29 @@ describe('StartPreparationUseCase', () => {
|
|
|
599
1244
|
status: 'Awaiting Workspace',
|
|
600
1245
|
}),
|
|
601
1246
|
];
|
|
602
|
-
mockProjectRepository.
|
|
603
|
-
mockIssueRepository.
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
1247
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1248
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1249
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
1250
|
+
);
|
|
1251
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
607
1252
|
|
|
608
1253
|
await useCase.run({
|
|
609
1254
|
projectUrl: 'https://github.com/user/repo',
|
|
610
1255
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
611
1256
|
preparationStatus: 'Preparation',
|
|
612
1257
|
defaultAgentName: 'agent1',
|
|
1258
|
+
defaultLlmModelName: null,
|
|
1259
|
+
defaultLlmAgentName: null,
|
|
1260
|
+
configFilePath: '/path/to/config.yml',
|
|
613
1261
|
maximumPreparingIssuesCount: null,
|
|
614
|
-
|
|
1262
|
+
utilizationPercentageThreshold: 90,
|
|
1263
|
+
allowedIssueAuthors: null,
|
|
1264
|
+
codexHomeCandidates: null,
|
|
615
1265
|
});
|
|
616
1266
|
|
|
617
|
-
expect(mockIssueRepository.
|
|
1267
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(0);
|
|
618
1268
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
619
|
-
expect(mockProjectRepository.
|
|
1269
|
+
expect(mockProjectRepository.getByUrl).not.toHaveBeenCalled();
|
|
620
1270
|
});
|
|
621
1271
|
|
|
622
1272
|
it('should proceed with preparation when Claude usage is under 90%', async () => {
|
|
@@ -633,11 +1283,11 @@ describe('StartPreparationUseCase', () => {
|
|
|
633
1283
|
status: 'Awaiting Workspace',
|
|
634
1284
|
}),
|
|
635
1285
|
];
|
|
636
|
-
mockProjectRepository.
|
|
637
|
-
mockIssueRepository.
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
1286
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1287
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1288
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
1289
|
+
);
|
|
1290
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
641
1291
|
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
642
1292
|
stdout: '',
|
|
643
1293
|
stderr: '',
|
|
@@ -649,52 +1299,208 @@ describe('StartPreparationUseCase', () => {
|
|
|
649
1299
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
650
1300
|
preparationStatus: 'Preparation',
|
|
651
1301
|
defaultAgentName: 'agent1',
|
|
1302
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
1303
|
+
defaultLlmAgentName: null,
|
|
1304
|
+
configFilePath: '/path/to/config.yml',
|
|
652
1305
|
maximumPreparingIssuesCount: null,
|
|
653
|
-
|
|
1306
|
+
utilizationPercentageThreshold: 90,
|
|
1307
|
+
allowedIssueAuthors: null,
|
|
1308
|
+
codexHomeCandidates: null,
|
|
654
1309
|
});
|
|
655
1310
|
|
|
656
|
-
expect(mockIssueRepository.
|
|
1311
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(1);
|
|
657
1312
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
658
1313
|
});
|
|
659
1314
|
|
|
660
|
-
it('should
|
|
1315
|
+
it('should reduce maximumPreparingIssuesCount gradually when weekly usage window exceeds threshold', async () => {
|
|
661
1316
|
mockClaudeRepository.getUsage.mockResolvedValue([
|
|
662
1317
|
{ hour: 5, utilizationPercentage: 50, resetsAt: new Date() },
|
|
663
1318
|
{ hour: 168, utilizationPercentage: 91, resetsAt: new Date() },
|
|
664
1319
|
]);
|
|
665
1320
|
|
|
1321
|
+
const awaitingIssues: Issue[] = Array.from({ length: 10 }, (_, i) =>
|
|
1322
|
+
createMockIssue({
|
|
1323
|
+
url: `url${i + 1}`,
|
|
1324
|
+
title: `Issue ${i + 1}`,
|
|
1325
|
+
labels: [],
|
|
1326
|
+
status: 'Awaiting Workspace',
|
|
1327
|
+
}),
|
|
1328
|
+
);
|
|
1329
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1330
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1331
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
1332
|
+
);
|
|
1333
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
1334
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
1335
|
+
stdout: '',
|
|
1336
|
+
stderr: '',
|
|
1337
|
+
exitCode: 0,
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
await useCase.run({
|
|
1341
|
+
projectUrl: 'https://github.com/user/repo',
|
|
1342
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1343
|
+
preparationStatus: 'Preparation',
|
|
1344
|
+
defaultAgentName: 'agent1',
|
|
1345
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
1346
|
+
defaultLlmAgentName: null,
|
|
1347
|
+
configFilePath: '/path/to/config.yml',
|
|
1348
|
+
maximumPreparingIssuesCount: null,
|
|
1349
|
+
utilizationPercentageThreshold: 90,
|
|
1350
|
+
allowedIssueAuthors: null,
|
|
1351
|
+
codexHomeCandidates: null,
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
const weeklyUtilization = 91;
|
|
1355
|
+
const threshold = 90;
|
|
1356
|
+
const defaultMax = 6;
|
|
1357
|
+
const normalizedUtilizationBeyondThreshold =
|
|
1358
|
+
(weeklyUtilization - threshold) / (100 - threshold);
|
|
1359
|
+
const expectedMax = Math.floor(
|
|
1360
|
+
defaultMax * Math.pow(1 - normalizedUtilizationBeyondThreshold, 2),
|
|
1361
|
+
);
|
|
1362
|
+
expect(mockProjectRepository.getByUrl).toHaveBeenCalled();
|
|
1363
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(expectedMax);
|
|
1364
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(
|
|
1365
|
+
expectedMax,
|
|
1366
|
+
);
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
it('should skip all preparation when weekly window usage reduces maximumPreparingIssuesCount to 0', async () => {
|
|
1370
|
+
mockClaudeRepository.getUsage.mockResolvedValue([
|
|
1371
|
+
{ hour: 168, utilizationPercentage: 100, resetsAt: new Date() },
|
|
1372
|
+
]);
|
|
1373
|
+
|
|
1374
|
+
const awaitingIssues: Issue[] = [
|
|
1375
|
+
createMockIssue({
|
|
1376
|
+
url: 'url1',
|
|
1377
|
+
labels: [],
|
|
1378
|
+
status: 'Awaiting Workspace',
|
|
1379
|
+
}),
|
|
1380
|
+
];
|
|
1381
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1382
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1383
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
1384
|
+
);
|
|
1385
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
1386
|
+
|
|
666
1387
|
await useCase.run({
|
|
667
1388
|
projectUrl: 'https://github.com/user/repo',
|
|
668
1389
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
669
1390
|
preparationStatus: 'Preparation',
|
|
670
1391
|
defaultAgentName: 'agent1',
|
|
1392
|
+
defaultLlmModelName: null,
|
|
1393
|
+
defaultLlmAgentName: null,
|
|
1394
|
+
configFilePath: '/path/to/config.yml',
|
|
671
1395
|
maximumPreparingIssuesCount: null,
|
|
672
|
-
|
|
1396
|
+
utilizationPercentageThreshold: 90,
|
|
1397
|
+
allowedIssueAuthors: null,
|
|
1398
|
+
codexHomeCandidates: null,
|
|
673
1399
|
});
|
|
674
1400
|
|
|
675
|
-
expect(
|
|
676
|
-
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(0);
|
|
1401
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(0);
|
|
677
1402
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
678
1403
|
});
|
|
679
1404
|
|
|
680
|
-
it('should
|
|
681
|
-
mockClaudeRepository.getUsage.
|
|
682
|
-
|
|
1405
|
+
it('should not apply weekly reduction when threshold is 100', async () => {
|
|
1406
|
+
mockClaudeRepository.getUsage.mockResolvedValue([
|
|
1407
|
+
{ hour: 168, utilizationPercentage: 99, resetsAt: new Date() },
|
|
1408
|
+
]);
|
|
1409
|
+
|
|
1410
|
+
const awaitingIssues: Issue[] = Array.from({ length: 10 }, (_, i) =>
|
|
1411
|
+
createMockIssue({
|
|
1412
|
+
url: `url${i + 1}`,
|
|
1413
|
+
title: `Issue ${i + 1}`,
|
|
1414
|
+
labels: [],
|
|
1415
|
+
status: 'Awaiting Workspace',
|
|
1416
|
+
}),
|
|
683
1417
|
);
|
|
1418
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1419
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1420
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
1421
|
+
);
|
|
1422
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
1423
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
1424
|
+
stdout: '',
|
|
1425
|
+
stderr: '',
|
|
1426
|
+
exitCode: 0,
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
await useCase.run({
|
|
1430
|
+
projectUrl: 'https://github.com/user/repo',
|
|
1431
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1432
|
+
preparationStatus: 'Preparation',
|
|
1433
|
+
defaultAgentName: 'agent1',
|
|
1434
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
1435
|
+
defaultLlmAgentName: null,
|
|
1436
|
+
configFilePath: '/path/to/config.yml',
|
|
1437
|
+
maximumPreparingIssuesCount: null,
|
|
1438
|
+
utilizationPercentageThreshold: 100,
|
|
1439
|
+
allowedIssueAuthors: null,
|
|
1440
|
+
codexHomeCandidates: null,
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(6);
|
|
1444
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(6);
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
it('should still skip immediately when non-weekly window exceeds threshold', async () => {
|
|
1448
|
+
mockClaudeRepository.getUsage.mockResolvedValue([
|
|
1449
|
+
{ hour: 5, utilizationPercentage: 95, resetsAt: new Date() },
|
|
1450
|
+
{ hour: 168, utilizationPercentage: 50, resetsAt: new Date() },
|
|
1451
|
+
]);
|
|
684
1452
|
|
|
685
1453
|
const awaitingIssues: Issue[] = [
|
|
686
1454
|
createMockIssue({
|
|
687
1455
|
url: 'url1',
|
|
688
|
-
|
|
689
|
-
labels: ['category:impl'],
|
|
1456
|
+
labels: [],
|
|
690
1457
|
status: 'Awaiting Workspace',
|
|
691
1458
|
}),
|
|
692
1459
|
];
|
|
693
|
-
mockProjectRepository.
|
|
694
|
-
mockIssueRepository.
|
|
695
|
-
|
|
696
|
-
|
|
1460
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1461
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1462
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
1463
|
+
);
|
|
1464
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
1465
|
+
|
|
1466
|
+
await useCase.run({
|
|
1467
|
+
projectUrl: 'https://github.com/user/repo',
|
|
1468
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1469
|
+
preparationStatus: 'Preparation',
|
|
1470
|
+
defaultAgentName: 'agent1',
|
|
1471
|
+
defaultLlmModelName: null,
|
|
1472
|
+
defaultLlmAgentName: null,
|
|
1473
|
+
configFilePath: '/path/to/config.yml',
|
|
1474
|
+
maximumPreparingIssuesCount: null,
|
|
1475
|
+
utilizationPercentageThreshold: 90,
|
|
1476
|
+
allowedIssueAuthors: null,
|
|
1477
|
+
codexHomeCandidates: null,
|
|
697
1478
|
});
|
|
1479
|
+
|
|
1480
|
+
expect(mockProjectRepository.getByUrl).not.toHaveBeenCalled();
|
|
1481
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(0);
|
|
1482
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
it('should use maximum utilization across multiple weekly window entries for reduction', async () => {
|
|
1486
|
+
mockClaudeRepository.getUsage.mockResolvedValue([
|
|
1487
|
+
{ hour: 168, utilizationPercentage: 91, resetsAt: new Date() },
|
|
1488
|
+
{ hour: 168, utilizationPercentage: 95, resetsAt: new Date() },
|
|
1489
|
+
]);
|
|
1490
|
+
|
|
1491
|
+
const awaitingIssues: Issue[] = Array.from({ length: 10 }, (_, i) =>
|
|
1492
|
+
createMockIssue({
|
|
1493
|
+
url: `url${i + 1}`,
|
|
1494
|
+
title: `Issue ${i + 1}`,
|
|
1495
|
+
labels: [],
|
|
1496
|
+
status: 'Awaiting Workspace',
|
|
1497
|
+
}),
|
|
1498
|
+
);
|
|
1499
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1500
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1501
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
1502
|
+
);
|
|
1503
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
698
1504
|
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
699
1505
|
stdout: '',
|
|
700
1506
|
stderr: '',
|
|
@@ -706,11 +1512,888 @@ describe('StartPreparationUseCase', () => {
|
|
|
706
1512
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
707
1513
|
preparationStatus: 'Preparation',
|
|
708
1514
|
defaultAgentName: 'agent1',
|
|
1515
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
1516
|
+
defaultLlmAgentName: null,
|
|
1517
|
+
configFilePath: '/path/to/config.yml',
|
|
709
1518
|
maximumPreparingIssuesCount: null,
|
|
710
|
-
|
|
1519
|
+
utilizationPercentageThreshold: 90,
|
|
1520
|
+
allowedIssueAuthors: null,
|
|
1521
|
+
codexHomeCandidates: null,
|
|
711
1522
|
});
|
|
712
1523
|
|
|
713
|
-
|
|
714
|
-
|
|
1524
|
+
const normalizedUtilizationBeyondThreshold = (95 - 90) / (100 - 90);
|
|
1525
|
+
const expectedMax = Math.floor(
|
|
1526
|
+
6 * Math.pow(1 - normalizedUtilizationBeyondThreshold, 2),
|
|
1527
|
+
);
|
|
1528
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(expectedMax);
|
|
1529
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(
|
|
1530
|
+
expectedMax,
|
|
1531
|
+
);
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
it('should throw error when Claude usage check fails', async () => {
|
|
1535
|
+
mockClaudeRepository.getUsage.mockRejectedValue(
|
|
1536
|
+
new Error('Claude credentials file not found'),
|
|
1537
|
+
);
|
|
1538
|
+
|
|
1539
|
+
await expect(
|
|
1540
|
+
useCase.run({
|
|
1541
|
+
projectUrl: 'https://github.com/user/repo',
|
|
1542
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1543
|
+
preparationStatus: 'Preparation',
|
|
1544
|
+
defaultAgentName: 'agent1',
|
|
1545
|
+
defaultLlmModelName: null,
|
|
1546
|
+
defaultLlmAgentName: null,
|
|
1547
|
+
configFilePath: '/path/to/config.yml',
|
|
1548
|
+
maximumPreparingIssuesCount: null,
|
|
1549
|
+
utilizationPercentageThreshold: 90,
|
|
1550
|
+
allowedIssueAuthors: null,
|
|
1551
|
+
codexHomeCandidates: null,
|
|
1552
|
+
}),
|
|
1553
|
+
).rejects.toThrow('Claude credentials file not found');
|
|
1554
|
+
|
|
1555
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(0);
|
|
1556
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
it('should skip preparation when Claude usage exceeds custom threshold', async () => {
|
|
1560
|
+
mockClaudeRepository.getUsage.mockResolvedValue([
|
|
1561
|
+
{ hour: 5, utilizationPercentage: 75, resetsAt: new Date() },
|
|
1562
|
+
]);
|
|
1563
|
+
|
|
1564
|
+
const awaitingIssues: Issue[] = [
|
|
1565
|
+
createMockIssue({
|
|
1566
|
+
url: 'url1',
|
|
1567
|
+
title: 'Issue 1',
|
|
1568
|
+
labels: [],
|
|
1569
|
+
status: 'Awaiting Workspace',
|
|
1570
|
+
}),
|
|
1571
|
+
];
|
|
1572
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1573
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1574
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
1575
|
+
);
|
|
1576
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
1577
|
+
|
|
1578
|
+
await useCase.run({
|
|
1579
|
+
projectUrl: 'https://github.com/user/repo',
|
|
1580
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1581
|
+
preparationStatus: 'Preparation',
|
|
1582
|
+
defaultAgentName: 'agent1',
|
|
1583
|
+
defaultLlmModelName: null,
|
|
1584
|
+
defaultLlmAgentName: null,
|
|
1585
|
+
configFilePath: '/path/to/config.yml',
|
|
1586
|
+
maximumPreparingIssuesCount: null,
|
|
1587
|
+
utilizationPercentageThreshold: 70,
|
|
1588
|
+
allowedIssueAuthors: null,
|
|
1589
|
+
codexHomeCandidates: null,
|
|
1590
|
+
});
|
|
1591
|
+
|
|
1592
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(0);
|
|
1593
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
1594
|
+
expect(mockProjectRepository.getByUrl).not.toHaveBeenCalled();
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
it('should proceed with preparation when Claude usage is under custom threshold', async () => {
|
|
1598
|
+
mockClaudeRepository.getUsage.mockResolvedValue([
|
|
1599
|
+
{ hour: 5, utilizationPercentage: 75, resetsAt: new Date() },
|
|
1600
|
+
]);
|
|
1601
|
+
|
|
1602
|
+
const awaitingIssues: Issue[] = [
|
|
1603
|
+
createMockIssue({
|
|
1604
|
+
url: 'url1',
|
|
1605
|
+
title: 'Issue 1',
|
|
1606
|
+
labels: ['category:impl'],
|
|
1607
|
+
status: 'Awaiting Workspace',
|
|
1608
|
+
}),
|
|
1609
|
+
];
|
|
1610
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1611
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1612
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
1613
|
+
);
|
|
1614
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
1615
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
1616
|
+
stdout: '',
|
|
1617
|
+
stderr: '',
|
|
1618
|
+
exitCode: 0,
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
await useCase.run({
|
|
1622
|
+
projectUrl: 'https://github.com/user/repo',
|
|
1623
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1624
|
+
preparationStatus: 'Preparation',
|
|
1625
|
+
defaultAgentName: 'agent1',
|
|
1626
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
1627
|
+
defaultLlmAgentName: null,
|
|
1628
|
+
configFilePath: '/path/to/config.yml',
|
|
1629
|
+
maximumPreparingIssuesCount: null,
|
|
1630
|
+
utilizationPercentageThreshold: 80,
|
|
1631
|
+
allowedIssueAuthors: null,
|
|
1632
|
+
codexHomeCandidates: null,
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(1);
|
|
1636
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
it('should skip issues that have dependedIssueUrls', async () => {
|
|
1640
|
+
const issueWithDependency = createMockIssue({
|
|
1641
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1642
|
+
title: 'Issue with dependency',
|
|
1643
|
+
labels: [],
|
|
1644
|
+
status: 'Awaiting Workspace',
|
|
1645
|
+
dependedIssueUrls: ['https://github.com/user/repo/issues/2'],
|
|
1646
|
+
});
|
|
1647
|
+
const issueWithoutDependency = createMockIssue({
|
|
1648
|
+
url: 'https://github.com/user/repo/issues/3',
|
|
1649
|
+
title: 'Issue without dependency',
|
|
1650
|
+
labels: [],
|
|
1651
|
+
status: 'Awaiting Workspace',
|
|
1652
|
+
dependedIssueUrls: [],
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1656
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1657
|
+
createMockStoryObjectMap([issueWithDependency, issueWithoutDependency]),
|
|
1658
|
+
);
|
|
1659
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce([
|
|
1660
|
+
issueWithDependency,
|
|
1661
|
+
issueWithoutDependency,
|
|
1662
|
+
]);
|
|
1663
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
1664
|
+
stdout: '',
|
|
1665
|
+
stderr: '',
|
|
1666
|
+
exitCode: 0,
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
await useCase.run({
|
|
1670
|
+
projectUrl: 'https://github.com/user/repo',
|
|
1671
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1672
|
+
preparationStatus: 'Preparation',
|
|
1673
|
+
defaultAgentName: 'agent1',
|
|
1674
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
1675
|
+
defaultLlmAgentName: null,
|
|
1676
|
+
configFilePath: '/path/to/config.yml',
|
|
1677
|
+
maximumPreparingIssuesCount: null,
|
|
1678
|
+
utilizationPercentageThreshold: 90,
|
|
1679
|
+
allowedIssueAuthors: null,
|
|
1680
|
+
codexHomeCandidates: null,
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(1);
|
|
1684
|
+
expect(mockIssueRepository.update.mock.calls[0][0]).toMatchObject({
|
|
1685
|
+
url: 'https://github.com/user/repo/issues/3',
|
|
1686
|
+
status: 'Preparation',
|
|
1687
|
+
});
|
|
1688
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
it('should skip issues where nextActionHour is in the future', async () => {
|
|
1692
|
+
jest.useFakeTimers();
|
|
1693
|
+
try {
|
|
1694
|
+
jest.setSystemTime(new Date('2024-01-01T10:00:00'));
|
|
1695
|
+
|
|
1696
|
+
const issueWithFutureNextActionHour = createMockIssue({
|
|
1697
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1698
|
+
title: 'Issue with future next action hour',
|
|
1699
|
+
labels: [],
|
|
1700
|
+
status: 'Awaiting Workspace',
|
|
1701
|
+
nextActionHour: 15,
|
|
1702
|
+
});
|
|
1703
|
+
const issueWithoutNextActionHour = createMockIssue({
|
|
1704
|
+
url: 'https://github.com/user/repo/issues/2',
|
|
1705
|
+
title: 'Issue without next action hour',
|
|
1706
|
+
labels: [],
|
|
1707
|
+
status: 'Awaiting Workspace',
|
|
1708
|
+
nextActionHour: null,
|
|
1709
|
+
});
|
|
1710
|
+
|
|
1711
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1712
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1713
|
+
createMockStoryObjectMap([
|
|
1714
|
+
issueWithFutureNextActionHour,
|
|
1715
|
+
issueWithoutNextActionHour,
|
|
1716
|
+
]),
|
|
1717
|
+
);
|
|
1718
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce([
|
|
1719
|
+
issueWithFutureNextActionHour,
|
|
1720
|
+
issueWithoutNextActionHour,
|
|
1721
|
+
]);
|
|
1722
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
1723
|
+
stdout: '',
|
|
1724
|
+
stderr: '',
|
|
1725
|
+
exitCode: 0,
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
await useCase.run({
|
|
1729
|
+
projectUrl: 'https://github.com/user/repo',
|
|
1730
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1731
|
+
preparationStatus: 'Preparation',
|
|
1732
|
+
defaultAgentName: 'agent1',
|
|
1733
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
1734
|
+
defaultLlmAgentName: null,
|
|
1735
|
+
configFilePath: '/path/to/config.yml',
|
|
1736
|
+
maximumPreparingIssuesCount: null,
|
|
1737
|
+
utilizationPercentageThreshold: 90,
|
|
1738
|
+
allowedIssueAuthors: null,
|
|
1739
|
+
codexHomeCandidates: null,
|
|
1740
|
+
});
|
|
1741
|
+
|
|
1742
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(1);
|
|
1743
|
+
expect(mockIssueRepository.update.mock.calls[0][0]).toMatchObject({
|
|
1744
|
+
url: 'https://github.com/user/repo/issues/2',
|
|
1745
|
+
status: 'Preparation',
|
|
1746
|
+
});
|
|
1747
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
1748
|
+
} finally {
|
|
1749
|
+
jest.useRealTimers();
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
|
|
1753
|
+
it('should skip issues where nextActionDate is tomorrow or more future', async () => {
|
|
1754
|
+
jest.useFakeTimers();
|
|
1755
|
+
try {
|
|
1756
|
+
jest.setSystemTime(new Date('2024-01-15T10:00:00'));
|
|
1757
|
+
|
|
1758
|
+
const issueWithFutureNextActionDate = createMockIssue({
|
|
1759
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1760
|
+
title: 'Issue with future next action date',
|
|
1761
|
+
labels: [],
|
|
1762
|
+
status: 'Awaiting Workspace',
|
|
1763
|
+
nextActionDate: new Date('2024-01-16'),
|
|
1764
|
+
});
|
|
1765
|
+
const issueWithoutNextActionDate = createMockIssue({
|
|
1766
|
+
url: 'https://github.com/user/repo/issues/2',
|
|
1767
|
+
title: 'Issue without next action date',
|
|
1768
|
+
labels: [],
|
|
1769
|
+
status: 'Awaiting Workspace',
|
|
1770
|
+
nextActionDate: null,
|
|
1771
|
+
});
|
|
1772
|
+
|
|
1773
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1774
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1775
|
+
createMockStoryObjectMap([
|
|
1776
|
+
issueWithFutureNextActionDate,
|
|
1777
|
+
issueWithoutNextActionDate,
|
|
1778
|
+
]),
|
|
1779
|
+
);
|
|
1780
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce([
|
|
1781
|
+
issueWithFutureNextActionDate,
|
|
1782
|
+
issueWithoutNextActionDate,
|
|
1783
|
+
]);
|
|
1784
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
1785
|
+
stdout: '',
|
|
1786
|
+
stderr: '',
|
|
1787
|
+
exitCode: 0,
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
await useCase.run({
|
|
1791
|
+
projectUrl: 'https://github.com/user/repo',
|
|
1792
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1793
|
+
preparationStatus: 'Preparation',
|
|
1794
|
+
defaultAgentName: 'agent1',
|
|
1795
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
1796
|
+
defaultLlmAgentName: null,
|
|
1797
|
+
configFilePath: '/path/to/config.yml',
|
|
1798
|
+
maximumPreparingIssuesCount: null,
|
|
1799
|
+
utilizationPercentageThreshold: 90,
|
|
1800
|
+
allowedIssueAuthors: null,
|
|
1801
|
+
codexHomeCandidates: null,
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(1);
|
|
1805
|
+
expect(mockIssueRepository.update.mock.calls[0][0]).toMatchObject({
|
|
1806
|
+
url: 'https://github.com/user/repo/issues/2',
|
|
1807
|
+
status: 'Preparation',
|
|
1808
|
+
});
|
|
1809
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
1810
|
+
} finally {
|
|
1811
|
+
jest.useRealTimers();
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
|
|
1815
|
+
it('should not skip issues where nextActionDate is today', async () => {
|
|
1816
|
+
jest.useFakeTimers();
|
|
1817
|
+
try {
|
|
1818
|
+
jest.setSystemTime(new Date('2024-01-15T10:00:00'));
|
|
1819
|
+
|
|
1820
|
+
const issueWithTodayNextActionDate = createMockIssue({
|
|
1821
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1822
|
+
title: 'Issue with today next action date',
|
|
1823
|
+
labels: [],
|
|
1824
|
+
status: 'Awaiting Workspace',
|
|
1825
|
+
nextActionDate: new Date('2024-01-15'),
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1828
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1829
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1830
|
+
createMockStoryObjectMap([issueWithTodayNextActionDate]),
|
|
1831
|
+
);
|
|
1832
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce([
|
|
1833
|
+
issueWithTodayNextActionDate,
|
|
1834
|
+
]);
|
|
1835
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
1836
|
+
stdout: '',
|
|
1837
|
+
stderr: '',
|
|
1838
|
+
exitCode: 0,
|
|
1839
|
+
});
|
|
1840
|
+
|
|
1841
|
+
await useCase.run({
|
|
1842
|
+
projectUrl: 'https://github.com/user/repo',
|
|
1843
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1844
|
+
preparationStatus: 'Preparation',
|
|
1845
|
+
defaultAgentName: 'agent1',
|
|
1846
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
1847
|
+
defaultLlmAgentName: null,
|
|
1848
|
+
configFilePath: '/path/to/config.yml',
|
|
1849
|
+
maximumPreparingIssuesCount: null,
|
|
1850
|
+
utilizationPercentageThreshold: 90,
|
|
1851
|
+
allowedIssueAuthors: null,
|
|
1852
|
+
codexHomeCandidates: null,
|
|
1853
|
+
});
|
|
1854
|
+
|
|
1855
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(1);
|
|
1856
|
+
expect(mockIssueRepository.update.mock.calls[0][0]).toMatchObject({
|
|
1857
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1858
|
+
status: 'Preparation',
|
|
1859
|
+
});
|
|
1860
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
1861
|
+
} finally {
|
|
1862
|
+
jest.useRealTimers();
|
|
1863
|
+
}
|
|
1864
|
+
});
|
|
1865
|
+
|
|
1866
|
+
it('should not skip issues where nextActionDate is in the past', async () => {
|
|
1867
|
+
jest.useFakeTimers();
|
|
1868
|
+
try {
|
|
1869
|
+
jest.setSystemTime(new Date('2024-01-15T10:00:00'));
|
|
1870
|
+
|
|
1871
|
+
const issueWithPastNextActionDate = createMockIssue({
|
|
1872
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1873
|
+
title: 'Issue with past next action date',
|
|
1874
|
+
labels: [],
|
|
1875
|
+
status: 'Awaiting Workspace',
|
|
1876
|
+
nextActionDate: new Date('2024-01-14'),
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1880
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1881
|
+
createMockStoryObjectMap([issueWithPastNextActionDate]),
|
|
1882
|
+
);
|
|
1883
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce([
|
|
1884
|
+
issueWithPastNextActionDate,
|
|
1885
|
+
]);
|
|
1886
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
1887
|
+
stdout: '',
|
|
1888
|
+
stderr: '',
|
|
1889
|
+
exitCode: 0,
|
|
1890
|
+
});
|
|
1891
|
+
|
|
1892
|
+
await useCase.run({
|
|
1893
|
+
projectUrl: 'https://github.com/user/repo',
|
|
1894
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1895
|
+
preparationStatus: 'Preparation',
|
|
1896
|
+
defaultAgentName: 'agent1',
|
|
1897
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
1898
|
+
defaultLlmAgentName: null,
|
|
1899
|
+
configFilePath: '/path/to/config.yml',
|
|
1900
|
+
maximumPreparingIssuesCount: null,
|
|
1901
|
+
utilizationPercentageThreshold: 90,
|
|
1902
|
+
allowedIssueAuthors: null,
|
|
1903
|
+
codexHomeCandidates: null,
|
|
1904
|
+
});
|
|
1905
|
+
|
|
1906
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(1);
|
|
1907
|
+
expect(mockIssueRepository.update.mock.calls[0][0]).toMatchObject({
|
|
1908
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1909
|
+
status: 'Preparation',
|
|
1910
|
+
});
|
|
1911
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
1912
|
+
} finally {
|
|
1913
|
+
jest.useRealTimers();
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1916
|
+
|
|
1917
|
+
it('should not skip issues where nextActionHour is in the past or current hour', async () => {
|
|
1918
|
+
jest.useFakeTimers();
|
|
1919
|
+
try {
|
|
1920
|
+
jest.setSystemTime(new Date('2024-01-01T15:00:00'));
|
|
1921
|
+
|
|
1922
|
+
const issueWithPastNextActionHour = createMockIssue({
|
|
1923
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1924
|
+
title: 'Issue with past next action hour',
|
|
1925
|
+
labels: [],
|
|
1926
|
+
status: 'Awaiting Workspace',
|
|
1927
|
+
nextActionHour: 10,
|
|
1928
|
+
});
|
|
1929
|
+
|
|
1930
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1931
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1932
|
+
createMockStoryObjectMap([issueWithPastNextActionHour]),
|
|
1933
|
+
);
|
|
1934
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce([
|
|
1935
|
+
issueWithPastNextActionHour,
|
|
1936
|
+
]);
|
|
1937
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
1938
|
+
stdout: '',
|
|
1939
|
+
stderr: '',
|
|
1940
|
+
exitCode: 0,
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1943
|
+
await useCase.run({
|
|
1944
|
+
projectUrl: 'https://github.com/user/repo',
|
|
1945
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1946
|
+
preparationStatus: 'Preparation',
|
|
1947
|
+
defaultAgentName: 'agent1',
|
|
1948
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
1949
|
+
defaultLlmAgentName: null,
|
|
1950
|
+
configFilePath: '/path/to/config.yml',
|
|
1951
|
+
maximumPreparingIssuesCount: null,
|
|
1952
|
+
utilizationPercentageThreshold: 90,
|
|
1953
|
+
allowedIssueAuthors: null,
|
|
1954
|
+
codexHomeCandidates: null,
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(1);
|
|
1958
|
+
expect(mockIssueRepository.update.mock.calls[0][0]).toMatchObject({
|
|
1959
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1960
|
+
status: 'Preparation',
|
|
1961
|
+
});
|
|
1962
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
1963
|
+
} finally {
|
|
1964
|
+
jest.useRealTimers();
|
|
1965
|
+
}
|
|
1966
|
+
});
|
|
1967
|
+
|
|
1968
|
+
it('should skip issues from non-allowed authors', async () => {
|
|
1969
|
+
const issueFromAllowedAuthor = createMockIssue({
|
|
1970
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1971
|
+
title: 'Issue from allowed author',
|
|
1972
|
+
labels: [],
|
|
1973
|
+
status: 'Awaiting Workspace',
|
|
1974
|
+
author: 'user1',
|
|
1975
|
+
});
|
|
1976
|
+
const issueFromNonAllowedAuthor = createMockIssue({
|
|
1977
|
+
url: 'https://github.com/user/repo/issues/2',
|
|
1978
|
+
title: 'Issue from non-allowed author',
|
|
1979
|
+
labels: [],
|
|
1980
|
+
status: 'Awaiting Workspace',
|
|
1981
|
+
author: 'user3',
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1985
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1986
|
+
createMockStoryObjectMap([
|
|
1987
|
+
issueFromAllowedAuthor,
|
|
1988
|
+
issueFromNonAllowedAuthor,
|
|
1989
|
+
]),
|
|
1990
|
+
);
|
|
1991
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce([
|
|
1992
|
+
issueFromAllowedAuthor,
|
|
1993
|
+
issueFromNonAllowedAuthor,
|
|
1994
|
+
]);
|
|
1995
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
1996
|
+
stdout: '',
|
|
1997
|
+
stderr: '',
|
|
1998
|
+
exitCode: 0,
|
|
1999
|
+
});
|
|
2000
|
+
|
|
2001
|
+
await useCase.run({
|
|
2002
|
+
projectUrl: 'https://github.com/user/repo',
|
|
2003
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
2004
|
+
preparationStatus: 'Preparation',
|
|
2005
|
+
defaultAgentName: 'agent1',
|
|
2006
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
2007
|
+
defaultLlmAgentName: null,
|
|
2008
|
+
configFilePath: '/path/to/config.yml',
|
|
2009
|
+
maximumPreparingIssuesCount: null,
|
|
2010
|
+
utilizationPercentageThreshold: 90,
|
|
2011
|
+
allowedIssueAuthors: ['user1', 'user2'],
|
|
2012
|
+
codexHomeCandidates: null,
|
|
2013
|
+
});
|
|
2014
|
+
|
|
2015
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(1);
|
|
2016
|
+
expect(mockIssueRepository.update.mock.calls[0][0]).toMatchObject({
|
|
2017
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
2018
|
+
status: 'Preparation',
|
|
2019
|
+
});
|
|
2020
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
2021
|
+
});
|
|
2022
|
+
|
|
2023
|
+
it('should process all issues when allowedIssueAuthors is null', async () => {
|
|
2024
|
+
const issue1 = createMockIssue({
|
|
2025
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
2026
|
+
title: 'Issue 1',
|
|
2027
|
+
labels: [],
|
|
2028
|
+
status: 'Awaiting Workspace',
|
|
2029
|
+
author: 'user1',
|
|
2030
|
+
});
|
|
2031
|
+
const issue2 = createMockIssue({
|
|
2032
|
+
url: 'https://github.com/user/repo/issues/2',
|
|
2033
|
+
title: 'Issue 2',
|
|
2034
|
+
labels: [],
|
|
2035
|
+
status: 'Awaiting Workspace',
|
|
2036
|
+
author: 'user2',
|
|
2037
|
+
});
|
|
2038
|
+
|
|
2039
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2040
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
2041
|
+
createMockStoryObjectMap([issue1, issue2]),
|
|
2042
|
+
);
|
|
2043
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce([issue1, issue2]);
|
|
2044
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
2045
|
+
stdout: '',
|
|
2046
|
+
stderr: '',
|
|
2047
|
+
exitCode: 0,
|
|
2048
|
+
});
|
|
2049
|
+
|
|
2050
|
+
await useCase.run({
|
|
2051
|
+
projectUrl: 'https://github.com/user/repo',
|
|
2052
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
2053
|
+
preparationStatus: 'Preparation',
|
|
2054
|
+
defaultAgentName: 'agent1',
|
|
2055
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
2056
|
+
defaultLlmAgentName: null,
|
|
2057
|
+
configFilePath: '/path/to/config.yml',
|
|
2058
|
+
maximumPreparingIssuesCount: null,
|
|
2059
|
+
utilizationPercentageThreshold: 90,
|
|
2060
|
+
allowedIssueAuthors: null,
|
|
2061
|
+
codexHomeCandidates: null,
|
|
2062
|
+
});
|
|
2063
|
+
|
|
2064
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(2);
|
|
2065
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(2);
|
|
2066
|
+
});
|
|
2067
|
+
|
|
2068
|
+
it('should not skip issues with empty author (tower defence issues)', async () => {
|
|
2069
|
+
const towerDefenceIssue = createMockIssue({
|
|
2070
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
2071
|
+
title: 'Tower defence issue',
|
|
2072
|
+
labels: [],
|
|
2073
|
+
status: 'Awaiting Workspace',
|
|
2074
|
+
author: '',
|
|
2075
|
+
});
|
|
2076
|
+
const normalIssue = createMockIssue({
|
|
2077
|
+
url: 'https://github.com/user/repo/issues/2',
|
|
2078
|
+
title: 'Normal issue',
|
|
2079
|
+
labels: [],
|
|
2080
|
+
status: 'Awaiting Workspace',
|
|
2081
|
+
author: 'user1',
|
|
2082
|
+
});
|
|
2083
|
+
|
|
2084
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2085
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
2086
|
+
createMockStoryObjectMap([towerDefenceIssue, normalIssue]),
|
|
2087
|
+
);
|
|
2088
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce([
|
|
2089
|
+
towerDefenceIssue,
|
|
2090
|
+
normalIssue,
|
|
2091
|
+
]);
|
|
2092
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
2093
|
+
stdout: '',
|
|
2094
|
+
stderr: '',
|
|
2095
|
+
exitCode: 0,
|
|
2096
|
+
});
|
|
2097
|
+
|
|
2098
|
+
await useCase.run({
|
|
2099
|
+
projectUrl: 'https://github.com/user/repo',
|
|
2100
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
2101
|
+
preparationStatus: 'Preparation',
|
|
2102
|
+
defaultAgentName: 'agent1',
|
|
2103
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
2104
|
+
defaultLlmAgentName: null,
|
|
2105
|
+
configFilePath: '/path/to/config.yml',
|
|
2106
|
+
maximumPreparingIssuesCount: null,
|
|
2107
|
+
utilizationPercentageThreshold: 90,
|
|
2108
|
+
allowedIssueAuthors: ['user1', 'user2'],
|
|
2109
|
+
codexHomeCandidates: null,
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(2);
|
|
2113
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(2);
|
|
2114
|
+
});
|
|
2115
|
+
|
|
2116
|
+
it('should not skip issues without author property when allowedIssueAuthors is set', async () => {
|
|
2117
|
+
const issueWithoutAuthor = createMockIssue({
|
|
2118
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
2119
|
+
title: 'Issue without author property',
|
|
2120
|
+
labels: [],
|
|
2121
|
+
status: 'Awaiting Workspace',
|
|
2122
|
+
author: '',
|
|
2123
|
+
});
|
|
2124
|
+
|
|
2125
|
+
const storyObjectMap: StoryObjectMap = new Map();
|
|
2126
|
+
storyObjectMap.set('Default Story', {
|
|
2127
|
+
story: {
|
|
2128
|
+
id: 'story-1',
|
|
2129
|
+
name: 'Default Story',
|
|
2130
|
+
color: 'GRAY',
|
|
2131
|
+
description: '',
|
|
2132
|
+
},
|
|
2133
|
+
storyIssue: null,
|
|
2134
|
+
issues: [issueWithoutAuthor],
|
|
2135
|
+
});
|
|
2136
|
+
|
|
2137
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2138
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(storyObjectMap);
|
|
2139
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce([
|
|
2140
|
+
issueWithoutAuthor,
|
|
2141
|
+
]);
|
|
2142
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
2143
|
+
stdout: '',
|
|
2144
|
+
stderr: '',
|
|
2145
|
+
exitCode: 0,
|
|
2146
|
+
});
|
|
2147
|
+
|
|
2148
|
+
await useCase.run({
|
|
2149
|
+
projectUrl: 'https://github.com/user/repo',
|
|
2150
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
2151
|
+
preparationStatus: 'Preparation',
|
|
2152
|
+
defaultAgentName: 'agent1',
|
|
2153
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
2154
|
+
defaultLlmAgentName: null,
|
|
2155
|
+
configFilePath: '/path/to/config.yml',
|
|
2156
|
+
maximumPreparingIssuesCount: null,
|
|
2157
|
+
utilizationPercentageThreshold: 90,
|
|
2158
|
+
allowedIssueAuthors: ['user1'],
|
|
2159
|
+
codexHomeCandidates: null,
|
|
2160
|
+
});
|
|
2161
|
+
|
|
2162
|
+
expect(mockIssueRepository.update.mock.calls).toHaveLength(1);
|
|
2163
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
2164
|
+
});
|
|
2165
|
+
|
|
2166
|
+
it('should not pass --codexHome when codexHomeCandidates is null', async () => {
|
|
2167
|
+
const awaitingIssues: Issue[] = [
|
|
2168
|
+
createMockIssue({
|
|
2169
|
+
url: 'url1',
|
|
2170
|
+
title: 'Issue 1',
|
|
2171
|
+
labels: ['category:impl'],
|
|
2172
|
+
status: 'Awaiting Workspace',
|
|
2173
|
+
}),
|
|
2174
|
+
];
|
|
2175
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2176
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
2177
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
2178
|
+
);
|
|
2179
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
2180
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
2181
|
+
stdout: '',
|
|
2182
|
+
stderr: '',
|
|
2183
|
+
exitCode: 0,
|
|
2184
|
+
});
|
|
2185
|
+
|
|
2186
|
+
await useCase.run({
|
|
2187
|
+
projectUrl: 'https://github.com/user/repo',
|
|
2188
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
2189
|
+
preparationStatus: 'Preparation',
|
|
2190
|
+
defaultAgentName: 'agent1',
|
|
2191
|
+
defaultLlmModelName: 'claude-opus',
|
|
2192
|
+
defaultLlmAgentName: null,
|
|
2193
|
+
configFilePath: '/path/to/config.yml',
|
|
2194
|
+
maximumPreparingIssuesCount: null,
|
|
2195
|
+
utilizationPercentageThreshold: 90,
|
|
2196
|
+
allowedIssueAuthors: null,
|
|
2197
|
+
codexHomeCandidates: null,
|
|
2198
|
+
});
|
|
2199
|
+
|
|
2200
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
2201
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
2202
|
+
'aw',
|
|
2203
|
+
[
|
|
2204
|
+
'url1',
|
|
2205
|
+
'impl',
|
|
2206
|
+
'claude-opus',
|
|
2207
|
+
'--configFilePath',
|
|
2208
|
+
'/path/to/config.yml',
|
|
2209
|
+
'--branch',
|
|
2210
|
+
'i1',
|
|
2211
|
+
],
|
|
2212
|
+
]);
|
|
2213
|
+
});
|
|
2214
|
+
|
|
2215
|
+
it('should not pass --codexHome when codexHomeCandidates is empty array', async () => {
|
|
2216
|
+
const awaitingIssues: Issue[] = [
|
|
2217
|
+
createMockIssue({
|
|
2218
|
+
url: 'url1',
|
|
2219
|
+
title: 'Issue 1',
|
|
2220
|
+
labels: ['category:impl'],
|
|
2221
|
+
status: 'Awaiting Workspace',
|
|
2222
|
+
}),
|
|
2223
|
+
];
|
|
2224
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2225
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
2226
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
2227
|
+
);
|
|
2228
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
2229
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
2230
|
+
stdout: '',
|
|
2231
|
+
stderr: '',
|
|
2232
|
+
exitCode: 0,
|
|
2233
|
+
});
|
|
2234
|
+
|
|
2235
|
+
await useCase.run({
|
|
2236
|
+
projectUrl: 'https://github.com/user/repo',
|
|
2237
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
2238
|
+
preparationStatus: 'Preparation',
|
|
2239
|
+
defaultAgentName: 'agent1',
|
|
2240
|
+
defaultLlmModelName: 'claude-opus',
|
|
2241
|
+
defaultLlmAgentName: null,
|
|
2242
|
+
configFilePath: '/path/to/config.yml',
|
|
2243
|
+
maximumPreparingIssuesCount: null,
|
|
2244
|
+
utilizationPercentageThreshold: 90,
|
|
2245
|
+
allowedIssueAuthors: null,
|
|
2246
|
+
codexHomeCandidates: [],
|
|
2247
|
+
});
|
|
2248
|
+
|
|
2249
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
2250
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
2251
|
+
'aw',
|
|
2252
|
+
[
|
|
2253
|
+
'url1',
|
|
2254
|
+
'impl',
|
|
2255
|
+
'claude-opus',
|
|
2256
|
+
'--configFilePath',
|
|
2257
|
+
'/path/to/config.yml',
|
|
2258
|
+
'--branch',
|
|
2259
|
+
'i1',
|
|
2260
|
+
],
|
|
2261
|
+
]);
|
|
2262
|
+
});
|
|
2263
|
+
|
|
2264
|
+
it('should pass --codexHome with the candidate when codexHomeCandidates has one entry', async () => {
|
|
2265
|
+
const awaitingIssues: Issue[] = [
|
|
2266
|
+
createMockIssue({
|
|
2267
|
+
url: 'url1',
|
|
2268
|
+
title: 'Issue 1',
|
|
2269
|
+
labels: ['category:impl'],
|
|
2270
|
+
status: 'Awaiting Workspace',
|
|
2271
|
+
}),
|
|
2272
|
+
];
|
|
2273
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2274
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
2275
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
2276
|
+
);
|
|
2277
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
2278
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
2279
|
+
stdout: '',
|
|
2280
|
+
stderr: '',
|
|
2281
|
+
exitCode: 0,
|
|
2282
|
+
});
|
|
2283
|
+
|
|
2284
|
+
await useCase.run({
|
|
2285
|
+
projectUrl: 'https://github.com/user/repo',
|
|
2286
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
2287
|
+
preparationStatus: 'Preparation',
|
|
2288
|
+
defaultAgentName: 'agent1',
|
|
2289
|
+
defaultLlmModelName: 'claude-opus',
|
|
2290
|
+
defaultLlmAgentName: null,
|
|
2291
|
+
configFilePath: '/path/to/config.yml',
|
|
2292
|
+
maximumPreparingIssuesCount: null,
|
|
2293
|
+
utilizationPercentageThreshold: 90,
|
|
2294
|
+
allowedIssueAuthors: null,
|
|
2295
|
+
codexHomeCandidates: ['.codex-dev1'],
|
|
2296
|
+
});
|
|
2297
|
+
|
|
2298
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
2299
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
2300
|
+
'aw',
|
|
2301
|
+
[
|
|
2302
|
+
'url1',
|
|
2303
|
+
'impl',
|
|
2304
|
+
'claude-opus',
|
|
2305
|
+
'--configFilePath',
|
|
2306
|
+
'/path/to/config.yml',
|
|
2307
|
+
'--branch',
|
|
2308
|
+
'i1',
|
|
2309
|
+
'--codexHome',
|
|
2310
|
+
'.codex-dev1',
|
|
2311
|
+
],
|
|
2312
|
+
]);
|
|
2313
|
+
});
|
|
2314
|
+
|
|
2315
|
+
it('should cycle through codexHomeCandidates across multiple issues', async () => {
|
|
2316
|
+
const awaitingIssues: Issue[] = [
|
|
2317
|
+
createMockIssue({
|
|
2318
|
+
url: 'url1',
|
|
2319
|
+
title: 'Issue 1',
|
|
2320
|
+
labels: ['category:impl'],
|
|
2321
|
+
status: 'Awaiting Workspace',
|
|
2322
|
+
number: 1,
|
|
2323
|
+
itemId: 'item-1',
|
|
2324
|
+
}),
|
|
2325
|
+
createMockIssue({
|
|
2326
|
+
url: 'url2',
|
|
2327
|
+
title: 'Issue 2',
|
|
2328
|
+
labels: ['category:impl'],
|
|
2329
|
+
status: 'Awaiting Workspace',
|
|
2330
|
+
number: 2,
|
|
2331
|
+
itemId: 'item-2',
|
|
2332
|
+
}),
|
|
2333
|
+
createMockIssue({
|
|
2334
|
+
url: 'url3',
|
|
2335
|
+
title: 'Issue 3',
|
|
2336
|
+
labels: ['category:impl'],
|
|
2337
|
+
status: 'Awaiting Workspace',
|
|
2338
|
+
number: 3,
|
|
2339
|
+
itemId: 'item-3',
|
|
2340
|
+
}),
|
|
2341
|
+
];
|
|
2342
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2343
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
2344
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
2345
|
+
);
|
|
2346
|
+
mockIssueRepository.getAllOpened.mockResolvedValueOnce(awaitingIssues);
|
|
2347
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
2348
|
+
stdout: '',
|
|
2349
|
+
stderr: '',
|
|
2350
|
+
exitCode: 0,
|
|
2351
|
+
});
|
|
2352
|
+
|
|
2353
|
+
await useCase.run({
|
|
2354
|
+
projectUrl: 'https://github.com/user/repo',
|
|
2355
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
2356
|
+
preparationStatus: 'Preparation',
|
|
2357
|
+
defaultAgentName: 'agent1',
|
|
2358
|
+
defaultLlmModelName: 'claude-opus',
|
|
2359
|
+
defaultLlmAgentName: null,
|
|
2360
|
+
configFilePath: '/path/to/config.yml',
|
|
2361
|
+
maximumPreparingIssuesCount: null,
|
|
2362
|
+
utilizationPercentageThreshold: 90,
|
|
2363
|
+
allowedIssueAuthors: null,
|
|
2364
|
+
codexHomeCandidates: ['.codex-dev1', '.codex-dev2'],
|
|
2365
|
+
});
|
|
2366
|
+
|
|
2367
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(3);
|
|
2368
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0][1]).toContain(
|
|
2369
|
+
'--codexHome',
|
|
2370
|
+
);
|
|
2371
|
+
expect(
|
|
2372
|
+
mockLocalCommandRunner.runCommand.mock.calls[0][1][
|
|
2373
|
+
mockLocalCommandRunner.runCommand.mock.calls[0][1].indexOf(
|
|
2374
|
+
'--codexHome',
|
|
2375
|
+
) + 1
|
|
2376
|
+
],
|
|
2377
|
+
).toBe('.codex-dev1');
|
|
2378
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[1][1]).toContain(
|
|
2379
|
+
'--codexHome',
|
|
2380
|
+
);
|
|
2381
|
+
expect(
|
|
2382
|
+
mockLocalCommandRunner.runCommand.mock.calls[1][1][
|
|
2383
|
+
mockLocalCommandRunner.runCommand.mock.calls[1][1].indexOf(
|
|
2384
|
+
'--codexHome',
|
|
2385
|
+
) + 1
|
|
2386
|
+
],
|
|
2387
|
+
).toBe('.codex-dev2');
|
|
2388
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[2][1]).toContain(
|
|
2389
|
+
'--codexHome',
|
|
2390
|
+
);
|
|
2391
|
+
expect(
|
|
2392
|
+
mockLocalCommandRunner.runCommand.mock.calls[2][1][
|
|
2393
|
+
mockLocalCommandRunner.runCommand.mock.calls[2][1].indexOf(
|
|
2394
|
+
'--codexHome',
|
|
2395
|
+
) + 1
|
|
2396
|
+
],
|
|
2397
|
+
).toBe('.codex-dev1');
|
|
715
2398
|
});
|
|
716
2399
|
});
|