github-issue-tower-defence-management 1.41.0 → 1.42.1
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/CHANGELOG.md +16 -0
- package/README.md +6 -0
- package/bin/adapter/entry-points/cli/index.js +23 -0
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +18 -6
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -0
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +10 -2
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/jest.config.js +1 -0
- package/jest.setup.js +1 -0
- package/package.json +2 -2
- package/src/adapter/entry-points/cli/index.test.ts +158 -0
- package/src/adapter/entry-points/cli/index.ts +31 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +2 -0
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +42 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +277 -0
- package/src/domain/usecases/StartPreparationUseCase.ts +16 -2
- package/types/adapter/entry-points/cli/index.d.ts +1 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -0
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts +1 -0
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
|
@@ -138,6 +138,7 @@ describe('CLI', () => {
|
|
|
138
138
|
thresholdForAutoReject: 5,
|
|
139
139
|
workflowBlockerResolvedWebhookUrl: 'https://example.com/webhook',
|
|
140
140
|
projectName: 'test-project',
|
|
141
|
+
codexHomeCandidates: ['.codex-dev1', '.codex-main'],
|
|
141
142
|
};
|
|
142
143
|
writeConfig(config);
|
|
143
144
|
|
|
@@ -146,6 +147,42 @@ describe('CLI', () => {
|
|
|
146
147
|
expect(result).toEqual(config);
|
|
147
148
|
});
|
|
148
149
|
|
|
150
|
+
it('should load codexHomeCandidates string array from config file', () => {
|
|
151
|
+
const config = {
|
|
152
|
+
...defaultConfig,
|
|
153
|
+
codexHomeCandidates: ['.codex-dev1', '.codex-dev2', '.codex-main'],
|
|
154
|
+
};
|
|
155
|
+
writeConfig(config);
|
|
156
|
+
|
|
157
|
+
const result = loadConfigFile(configFilePath);
|
|
158
|
+
|
|
159
|
+
expect(result.codexHomeCandidates).toEqual([
|
|
160
|
+
'.codex-dev1',
|
|
161
|
+
'.codex-dev2',
|
|
162
|
+
'.codex-main',
|
|
163
|
+
]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should return undefined codexHomeCandidates when not in config', () => {
|
|
167
|
+
writeConfig(defaultConfig);
|
|
168
|
+
|
|
169
|
+
const result = loadConfigFile(configFilePath);
|
|
170
|
+
|
|
171
|
+
expect(result.codexHomeCandidates).toBeUndefined();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should return undefined codexHomeCandidates when array contains non-string items', () => {
|
|
175
|
+
const config = {
|
|
176
|
+
...defaultConfig,
|
|
177
|
+
codexHomeCandidates: ['.codex-dev1', 123],
|
|
178
|
+
};
|
|
179
|
+
writeConfig(config);
|
|
180
|
+
|
|
181
|
+
const result = loadConfigFile(configFilePath);
|
|
182
|
+
|
|
183
|
+
expect(result.codexHomeCandidates).toBeUndefined();
|
|
184
|
+
});
|
|
185
|
+
|
|
149
186
|
it('should return empty config for empty YAML', () => {
|
|
150
187
|
fs.writeFileSync(configFilePath, '');
|
|
151
188
|
|
|
@@ -300,6 +337,24 @@ defaultAgentName: 'case-test-agent'
|
|
|
300
337
|
|
|
301
338
|
expect(result.defaultAgentName).toBe('case-test-agent');
|
|
302
339
|
});
|
|
340
|
+
|
|
341
|
+
it('should parse codexHomeCandidates string array from README config', () => {
|
|
342
|
+
const readme = `<details>
|
|
343
|
+
<summary>config</summary>
|
|
344
|
+
codexHomeCandidates:
|
|
345
|
+
- .codex-dev1
|
|
346
|
+
- .codex-dev2
|
|
347
|
+
- .codex-main
|
|
348
|
+
</details>`;
|
|
349
|
+
|
|
350
|
+
const result = parseProjectReadmeConfig(readme);
|
|
351
|
+
|
|
352
|
+
expect(result.codexHomeCandidates).toEqual([
|
|
353
|
+
'.codex-dev1',
|
|
354
|
+
'.codex-dev2',
|
|
355
|
+
'.codex-main',
|
|
356
|
+
]);
|
|
357
|
+
});
|
|
303
358
|
});
|
|
304
359
|
|
|
305
360
|
describe('mergeConfigs', () => {
|
|
@@ -457,6 +512,7 @@ defaultAgentName: 'case-test-agent'
|
|
|
457
512
|
maximumPreparingIssuesCount: null,
|
|
458
513
|
utilizationPercentageThreshold: 90,
|
|
459
514
|
allowedIssueAuthors: null,
|
|
515
|
+
codexHomeCandidates: null,
|
|
460
516
|
});
|
|
461
517
|
});
|
|
462
518
|
|
|
@@ -497,6 +553,7 @@ defaultAgentName: 'case-test-agent'
|
|
|
497
553
|
maximumPreparingIssuesCount: null,
|
|
498
554
|
utilizationPercentageThreshold: 90,
|
|
499
555
|
allowedIssueAuthors: null,
|
|
556
|
+
codexHomeCandidates: null,
|
|
500
557
|
});
|
|
501
558
|
});
|
|
502
559
|
|
|
@@ -905,6 +962,107 @@ defaultAgentName: 'case-test-agent'
|
|
|
905
962
|
}),
|
|
906
963
|
);
|
|
907
964
|
});
|
|
965
|
+
|
|
966
|
+
it('should pass codexHomeCandidates from config file', async () => {
|
|
967
|
+
const configWithCandidates = {
|
|
968
|
+
...defaultConfig,
|
|
969
|
+
codexHomeCandidates: ['.codex-dev1', '.codex-dev2', '.codex-main'],
|
|
970
|
+
};
|
|
971
|
+
writeConfig(configWithCandidates);
|
|
972
|
+
|
|
973
|
+
const mockRun = jest.fn().mockResolvedValue(undefined);
|
|
974
|
+
const MockedStartPreparationUseCase = jest.mocked(
|
|
975
|
+
StartPreparationUseCase,
|
|
976
|
+
);
|
|
977
|
+
|
|
978
|
+
MockedStartPreparationUseCase.mockImplementation(function (
|
|
979
|
+
this: StartPreparationUseCase,
|
|
980
|
+
) {
|
|
981
|
+
this.run = mockRun;
|
|
982
|
+
return this;
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
await program.parseAsync([
|
|
986
|
+
'node',
|
|
987
|
+
'test',
|
|
988
|
+
'startDaemon',
|
|
989
|
+
'--configFilePath',
|
|
990
|
+
configFilePath,
|
|
991
|
+
]);
|
|
992
|
+
|
|
993
|
+
expect(mockRun).toHaveBeenCalledWith(
|
|
994
|
+
expect.objectContaining({
|
|
995
|
+
codexHomeCandidates: ['.codex-dev1', '.codex-dev2', '.codex-main'],
|
|
996
|
+
}),
|
|
997
|
+
);
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
it('should pass codexHomeCandidates as null when not in config', async () => {
|
|
1001
|
+
const mockRun = jest.fn().mockResolvedValue(undefined);
|
|
1002
|
+
const MockedStartPreparationUseCase = jest.mocked(
|
|
1003
|
+
StartPreparationUseCase,
|
|
1004
|
+
);
|
|
1005
|
+
|
|
1006
|
+
MockedStartPreparationUseCase.mockImplementation(function (
|
|
1007
|
+
this: StartPreparationUseCase,
|
|
1008
|
+
) {
|
|
1009
|
+
this.run = mockRun;
|
|
1010
|
+
return this;
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
await program.parseAsync([
|
|
1014
|
+
'node',
|
|
1015
|
+
'test',
|
|
1016
|
+
'startDaemon',
|
|
1017
|
+
'--configFilePath',
|
|
1018
|
+
configFilePath,
|
|
1019
|
+
]);
|
|
1020
|
+
|
|
1021
|
+
expect(mockRun).toHaveBeenCalledWith(
|
|
1022
|
+
expect.objectContaining({
|
|
1023
|
+
codexHomeCandidates: null,
|
|
1024
|
+
}),
|
|
1025
|
+
);
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
it('should pass codexHomeCandidates from README config', async () => {
|
|
1029
|
+
const readmeContent = [
|
|
1030
|
+
'# Project',
|
|
1031
|
+
'<details>',
|
|
1032
|
+
'<summary>config</summary>',
|
|
1033
|
+
'codexHomeCandidates:',
|
|
1034
|
+
' - .codex-readme1',
|
|
1035
|
+
' - .codex-readme2',
|
|
1036
|
+
'</details>',
|
|
1037
|
+
].join('\n');
|
|
1038
|
+
mockFetchReturningReadme(readmeContent);
|
|
1039
|
+
|
|
1040
|
+
const mockRun = jest.fn().mockResolvedValue(undefined);
|
|
1041
|
+
const MockedStartPreparationUseCase = jest.mocked(
|
|
1042
|
+
StartPreparationUseCase,
|
|
1043
|
+
);
|
|
1044
|
+
|
|
1045
|
+
MockedStartPreparationUseCase.mockImplementation(function (
|
|
1046
|
+
this: StartPreparationUseCase,
|
|
1047
|
+
) {
|
|
1048
|
+
this.run = mockRun;
|
|
1049
|
+
return this;
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
await program.parseAsync([
|
|
1053
|
+
'node',
|
|
1054
|
+
'test',
|
|
1055
|
+
'startDaemon',
|
|
1056
|
+
'--configFilePath',
|
|
1057
|
+
configFilePath,
|
|
1058
|
+
]);
|
|
1059
|
+
|
|
1060
|
+
expect(mockRun).toHaveBeenCalledWith(
|
|
1061
|
+
expect.objectContaining({
|
|
1062
|
+
codexHomeCandidates: ['.codex-readme1', '.codex-readme2'],
|
|
1063
|
+
}),
|
|
1064
|
+
);
|
|
1065
|
+
});
|
|
908
1066
|
});
|
|
909
1067
|
|
|
910
1068
|
describe('notifyFinishedIssuePreparation', () => {
|
|
@@ -37,6 +37,7 @@ type ConfigFile = {
|
|
|
37
37
|
workflowBlockerResolvedWebhookUrl?: string;
|
|
38
38
|
projectName?: string;
|
|
39
39
|
preparationProcessCheckCommand?: string;
|
|
40
|
+
codexHomeCandidates?: string[];
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
type StartDaemonOptions = {
|
|
@@ -81,6 +82,24 @@ const getNumberValue = (
|
|
|
81
82
|
return typeof value === 'number' ? value : undefined;
|
|
82
83
|
};
|
|
83
84
|
|
|
85
|
+
const getStringArrayValue = (
|
|
86
|
+
obj: Record<string, unknown>,
|
|
87
|
+
key: string,
|
|
88
|
+
): string[] | undefined => {
|
|
89
|
+
const value = obj[key];
|
|
90
|
+
if (!Array.isArray(value)) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const strings: string[] = [];
|
|
94
|
+
for (const item of value) {
|
|
95
|
+
if (typeof item !== 'string') {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
strings.push(item);
|
|
99
|
+
}
|
|
100
|
+
return strings;
|
|
101
|
+
};
|
|
102
|
+
|
|
84
103
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
85
104
|
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
86
105
|
|
|
@@ -125,6 +144,7 @@ export const loadConfigFile = (configFilePath: string): ConfigFile => {
|
|
|
125
144
|
parsed,
|
|
126
145
|
'preparationProcessCheckCommand',
|
|
127
146
|
),
|
|
147
|
+
codexHomeCandidates: getStringArrayValue(parsed, 'codexHomeCandidates'),
|
|
128
148
|
};
|
|
129
149
|
} catch (error) {
|
|
130
150
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -183,6 +203,7 @@ export const parseProjectReadmeConfig = (readme: string): ConfigFile => {
|
|
|
183
203
|
parsed,
|
|
184
204
|
'preparationProcessCheckCommand',
|
|
185
205
|
),
|
|
206
|
+
codexHomeCandidates: getStringArrayValue(parsed, 'codexHomeCandidates'),
|
|
186
207
|
};
|
|
187
208
|
} catch {
|
|
188
209
|
console.warn('Failed to parse YAML from project README config section');
|
|
@@ -249,6 +270,10 @@ export const mergeConfigs = (
|
|
|
249
270
|
readmeOverrides.preparationProcessCheckCommand ??
|
|
250
271
|
cliOverrides.preparationProcessCheckCommand ??
|
|
251
272
|
configFile.preparationProcessCheckCommand,
|
|
273
|
+
codexHomeCandidates:
|
|
274
|
+
readmeOverrides.codexHomeCandidates ??
|
|
275
|
+
cliOverrides.codexHomeCandidates ??
|
|
276
|
+
configFile.codexHomeCandidates,
|
|
252
277
|
});
|
|
253
278
|
|
|
254
279
|
type GraphqlProjectV2ReadmeResponse = {
|
|
@@ -585,6 +610,11 @@ program
|
|
|
585
610
|
.filter(Boolean)
|
|
586
611
|
: null;
|
|
587
612
|
|
|
613
|
+
const codexHomeCandidates =
|
|
614
|
+
config.codexHomeCandidates && config.codexHomeCandidates.length > 0
|
|
615
|
+
? config.codexHomeCandidates
|
|
616
|
+
: null;
|
|
617
|
+
|
|
588
618
|
await useCase.run({
|
|
589
619
|
projectUrl,
|
|
590
620
|
awaitingWorkspaceStatus,
|
|
@@ -597,6 +627,7 @@ program
|
|
|
597
627
|
utilizationPercentageThreshold:
|
|
598
628
|
config.utilizationPercentageThreshold ?? 90,
|
|
599
629
|
allowedIssueAuthors,
|
|
630
|
+
codexHomeCandidates,
|
|
600
631
|
});
|
|
601
632
|
});
|
|
602
633
|
|
|
@@ -80,6 +80,7 @@ export class HandleScheduledEventUseCase {
|
|
|
80
80
|
utilizationPercentageThreshold?: number;
|
|
81
81
|
allowedIssueAuthors?: string[] | null;
|
|
82
82
|
preparationProcessCheckCommand?: string;
|
|
83
|
+
codexHomeCandidates?: string[] | null;
|
|
83
84
|
} | null;
|
|
84
85
|
notifyFinishedPreparation?: {
|
|
85
86
|
preparationStatus: string;
|
|
@@ -356,6 +357,7 @@ ${JSON.stringify(e)}
|
|
|
356
357
|
utilizationPercentageThreshold:
|
|
357
358
|
input.startPreparation.utilizationPercentageThreshold ?? 90,
|
|
358
359
|
allowedIssueAuthors: input.startPreparation.allowedIssueAuthors ?? null,
|
|
360
|
+
codexHomeCandidates: input.startPreparation.codexHomeCandidates ?? null,
|
|
359
361
|
});
|
|
360
362
|
}
|
|
361
363
|
if (input.notifyFinishedPreparation) {
|
|
@@ -259,6 +259,48 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
259
259
|
).rejects.toThrow('Project not found');
|
|
260
260
|
});
|
|
261
261
|
|
|
262
|
+
it('should throw when getProject returns null after findProjectIdByUrl succeeds', async () => {
|
|
263
|
+
mockProjectRepository.findProjectIdByUrl.mockResolvedValue('project-1');
|
|
264
|
+
mockProjectRepository.getProject.mockResolvedValue(null);
|
|
265
|
+
|
|
266
|
+
await expect(
|
|
267
|
+
useCase.run({
|
|
268
|
+
projectUrl: 'https://github.com/user/repo',
|
|
269
|
+
preparationStatus: 'Preparation',
|
|
270
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
271
|
+
allowIssueCacheMinutes: 0,
|
|
272
|
+
preparationProcessCheckCommand: 'check {URL}',
|
|
273
|
+
}),
|
|
274
|
+
).rejects.toThrow('Project not found. projectId: project-1');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should do nothing when awaitingWorkspaceStatus is not found in project statuses', async () => {
|
|
278
|
+
const preparationIssue = createMockIssue({
|
|
279
|
+
url: 'https://github.com/user/repo/issues/10',
|
|
280
|
+
status: 'Preparation',
|
|
281
|
+
});
|
|
282
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
283
|
+
issues: [preparationIssue],
|
|
284
|
+
cacheUsed: false,
|
|
285
|
+
});
|
|
286
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
287
|
+
stdout: '',
|
|
288
|
+
stderr: '',
|
|
289
|
+
exitCode: 1,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
await useCase.run({
|
|
293
|
+
projectUrl: 'https://github.com/user/repo',
|
|
294
|
+
preparationStatus: 'Preparation',
|
|
295
|
+
awaitingWorkspaceStatus: 'NonExistentStatus',
|
|
296
|
+
allowIssueCacheMinutes: 0,
|
|
297
|
+
preparationProcessCheckCommand: 'check {URL}',
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(0);
|
|
301
|
+
expect(mockIssueRepository.createComment.mock.calls).toHaveLength(0);
|
|
302
|
+
});
|
|
303
|
+
|
|
262
304
|
it('should do nothing when there are no Preparation issues', async () => {
|
|
263
305
|
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
264
306
|
issues: [
|