github-issue-tower-defence-management 1.41.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.
@@ -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: [