github-issue-tower-defence-management 2.3.0 → 2.5.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/CHANGELOG.md +20 -0
- package/bin/adapter/entry-points/cli/fleetConfig.js +19 -1
- package/bin/adapter/entry-points/cli/fleetConfig.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +17 -4
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/subscriptionDisabledTokenHandler.js +27 -0
- package/bin/adapter/entry-points/handlers/subscriptionDisabledTokenHandler.js.map +1 -0
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -11
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/SubscriptionDisabledIssueUseCase.js +42 -0
- package/bin/domain/usecases/SubscriptionDisabledIssueUseCase.js.map +1 -0
- package/bin/domain/usecases/TriagerApprovalDispatchUseCase.js +9 -4
- package/bin/domain/usecases/TriagerApprovalDispatchUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/fleetConfig.test.ts +86 -0
- package/src/adapter/entry-points/cli/fleetConfig.ts +38 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +24 -8
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +27 -5
- package/src/adapter/entry-points/handlers/subscriptionDisabledTokenHandler.ts +41 -0
- package/src/adapter/repositories/githubGraphqlClient.test.ts +2 -2
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +0 -56
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +0 -13
- package/src/domain/usecases/SubscriptionDisabledIssueUseCase.test.ts +197 -0
- package/src/domain/usecases/SubscriptionDisabledIssueUseCase.ts +74 -0
- package/src/domain/usecases/TriagerApprovalDispatchUseCase.test.ts +83 -0
- package/src/domain/usecases/TriagerApprovalDispatchUseCase.ts +12 -4
- package/types/adapter/entry-points/cli/fleetConfig.d.ts +7 -0
- package/types/adapter/entry-points/cli/fleetConfig.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/subscriptionDisabledTokenHandler.d.ts +9 -0
- package/types/adapter/entry-points/handlers/subscriptionDisabledTokenHandler.d.ts.map +1 -0
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -4
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/SubscriptionDisabledIssueUseCase.d.ts +16 -0
- package/types/domain/usecases/SubscriptionDisabledIssueUseCase.d.ts.map +1 -0
- package/types/domain/usecases/TriagerApprovalDispatchUseCase.d.ts.map +1 -1
- package/src/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.test.ts +0 -1638
- package/src/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.ts +0 -192
|
@@ -4,9 +4,11 @@ import * as path from 'path';
|
|
|
4
4
|
import { DEFAULT_LIVE_SESSION_OAUTH_TOKEN_SELECTION_SETTINGS } from '../../../domain/usecases/LiveSessionOauthTokenSelectUseCase';
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_PREPARATION_WORKER_SETTINGS,
|
|
7
|
+
DEFAULT_START_PREPARATION_FLEET_SETTINGS,
|
|
7
8
|
FLEET_CONFIG_FILE_PATH_ENVIRONMENT_VARIABLE,
|
|
8
9
|
loadLiveSessionOauthTokenSelectionSettings,
|
|
9
10
|
loadPreparationWorkerSettings,
|
|
11
|
+
loadStartPreparationFleetSettings,
|
|
10
12
|
resolveFleetConfigFilePath,
|
|
11
13
|
} from './fleetConfig';
|
|
12
14
|
|
|
@@ -303,3 +305,87 @@ describe('loadPreparationWorkerSettings', () => {
|
|
|
303
305
|
);
|
|
304
306
|
});
|
|
305
307
|
});
|
|
308
|
+
|
|
309
|
+
describe('loadStartPreparationFleetSettings', () => {
|
|
310
|
+
let tempDir: string;
|
|
311
|
+
|
|
312
|
+
const writeFleetConfig = (content: string): string => {
|
|
313
|
+
const fleetConfigFilePath = path.join(tempDir, 'fleet.config.yaml');
|
|
314
|
+
fs.writeFileSync(fleetConfigFilePath, content);
|
|
315
|
+
return fleetConfigFilePath;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
beforeEach(() => {
|
|
319
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fleet-config-'));
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
afterEach(() => {
|
|
323
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('returns the built-in settings when no fleet config path is given', () => {
|
|
327
|
+
expect(loadStartPreparationFleetSettings(null)).toEqual(
|
|
328
|
+
DEFAULT_START_PREPARATION_FLEET_SETTINGS,
|
|
329
|
+
);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('reads maximumPreparingIssuesCount from the startPreparation section', () => {
|
|
333
|
+
const fleetConfigFilePath = writeFleetConfig(
|
|
334
|
+
['startPreparation:', ' maximumPreparingIssuesCount: 100'].join('\n'),
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
expect(loadStartPreparationFleetSettings(fleetConfigFilePath)).toEqual({
|
|
338
|
+
maximumPreparingIssuesCount: 100,
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('returns the built-in settings when the fleet config has no startPreparation section', () => {
|
|
343
|
+
const fleetConfigFilePath = writeFleetConfig('inTmuxLauncherCommand: cl\n');
|
|
344
|
+
|
|
345
|
+
expect(loadStartPreparationFleetSettings(fleetConfigFilePath)).toEqual(
|
|
346
|
+
DEFAULT_START_PREPARATION_FLEET_SETTINGS,
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('returns the built-in settings for an empty fleet config file', () => {
|
|
351
|
+
const fleetConfigFilePath = writeFleetConfig('');
|
|
352
|
+
|
|
353
|
+
expect(loadStartPreparationFleetSettings(fleetConfigFilePath)).toEqual(
|
|
354
|
+
DEFAULT_START_PREPARATION_FLEET_SETTINGS,
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('throws when the fleet config file does not exist', () => {
|
|
359
|
+
expect(() =>
|
|
360
|
+
loadStartPreparationFleetSettings(path.join(tempDir, 'missing.yaml')),
|
|
361
|
+
).toThrow();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('throws when maximumPreparingIssuesCount is not a positive integer', () => {
|
|
365
|
+
const fleetConfigFilePath = writeFleetConfig(
|
|
366
|
+
['startPreparation:', ' maximumPreparingIssuesCount: 0'].join('\n'),
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
expect(() =>
|
|
370
|
+
loadStartPreparationFleetSettings(fleetConfigFilePath),
|
|
371
|
+
).toThrow('maximumPreparingIssuesCount');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('throws when maximumPreparingIssuesCount is written as a string', () => {
|
|
375
|
+
const fleetConfigFilePath = writeFleetConfig(
|
|
376
|
+
['startPreparation:', " maximumPreparingIssuesCount: '100'"].join('\n'),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
expect(() =>
|
|
380
|
+
loadStartPreparationFleetSettings(fleetConfigFilePath),
|
|
381
|
+
).toThrow('must be a number');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('throws when the startPreparation section is not a mapping', () => {
|
|
385
|
+
const fleetConfigFilePath = writeFleetConfig('startPreparation: 10\n');
|
|
386
|
+
|
|
387
|
+
expect(() =>
|
|
388
|
+
loadStartPreparationFleetSettings(fleetConfigFilePath),
|
|
389
|
+
).toThrow('must be a mapping');
|
|
390
|
+
});
|
|
391
|
+
});
|
|
@@ -11,6 +11,9 @@ export const FLEET_CONFIG_FILE_PATH_ENVIRONMENT_VARIABLE = 'TDPM_FLEET_CONFIG';
|
|
|
11
11
|
export const LIVE_SESSION_OAUTH_TOKEN_SELECTION_SECTION_KEY =
|
|
12
12
|
'liveSessionOauthTokenSelection';
|
|
13
13
|
export const PREPARATION_WORKER_SECTION_KEY = 'preparationWorker';
|
|
14
|
+
export const START_PREPARATION_SECTION_KEY = 'startPreparation';
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_FLEET_MAXIMUM_PREPARING_ISSUES_COUNT = 80;
|
|
14
17
|
|
|
15
18
|
export type PreparationWorkerSettings = {
|
|
16
19
|
normalConcurrentLimit: number;
|
|
@@ -20,6 +23,15 @@ export const DEFAULT_PREPARATION_WORKER_SETTINGS: PreparationWorkerSettings = {
|
|
|
20
23
|
normalConcurrentLimit: NORMAL_CONCURRENT_LIMIT,
|
|
21
24
|
};
|
|
22
25
|
|
|
26
|
+
export type StartPreparationFleetSettings = {
|
|
27
|
+
maximumPreparingIssuesCount: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const DEFAULT_START_PREPARATION_FLEET_SETTINGS: StartPreparationFleetSettings =
|
|
31
|
+
{
|
|
32
|
+
maximumPreparingIssuesCount: DEFAULT_FLEET_MAXIMUM_PREPARING_ISSUES_COUNT,
|
|
33
|
+
};
|
|
34
|
+
|
|
23
35
|
export const resolveFleetConfigFilePath = (
|
|
24
36
|
cliValue: string | null,
|
|
25
37
|
): string | null => {
|
|
@@ -168,3 +180,29 @@ export const loadPreparationWorkerSettings = (
|
|
|
168
180
|
),
|
|
169
181
|
};
|
|
170
182
|
};
|
|
183
|
+
|
|
184
|
+
export const loadStartPreparationFleetSettings = (
|
|
185
|
+
fleetConfigFilePath: string | null,
|
|
186
|
+
): StartPreparationFleetSettings => {
|
|
187
|
+
if (fleetConfigFilePath === null) {
|
|
188
|
+
return DEFAULT_START_PREPARATION_FLEET_SETTINGS;
|
|
189
|
+
}
|
|
190
|
+
const section = readFleetConfigSection(
|
|
191
|
+
fleetConfigFilePath,
|
|
192
|
+
START_PREPARATION_SECTION_KEY,
|
|
193
|
+
);
|
|
194
|
+
if (section === null) {
|
|
195
|
+
return DEFAULT_START_PREPARATION_FLEET_SETTINGS;
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
maximumPreparingIssuesCount: readBoundedNumber(
|
|
199
|
+
section,
|
|
200
|
+
START_PREPARATION_SECTION_KEY,
|
|
201
|
+
'maximumPreparingIssuesCount',
|
|
202
|
+
fleetConfigFilePath,
|
|
203
|
+
DEFAULT_START_PREPARATION_FLEET_SETTINGS.maximumPreparingIssuesCount,
|
|
204
|
+
(value) => Number.isInteger(value) && value >= 1,
|
|
205
|
+
'integer of at least 1',
|
|
206
|
+
),
|
|
207
|
+
};
|
|
208
|
+
};
|
|
@@ -83,14 +83,6 @@ jest.mock(
|
|
|
83
83
|
jest.mock('../../../domain/usecases/CreateEstimationIssueUseCase', () => ({
|
|
84
84
|
CreateEstimationIssueUseCase: jest.fn().mockImplementation(() => ({})),
|
|
85
85
|
}));
|
|
86
|
-
jest.mock(
|
|
87
|
-
'../../../domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase',
|
|
88
|
-
() => ({
|
|
89
|
-
ConvertCheckboxToIssueInStoryIssueUseCase: jest
|
|
90
|
-
.fn()
|
|
91
|
-
.mockImplementation(() => ({})),
|
|
92
|
-
}),
|
|
93
|
-
);
|
|
94
86
|
jest.mock('../../../domain/usecases/ChangeStatusByStoryColorUseCase', () => ({
|
|
95
87
|
ChangeStatusByStoryColorUseCase: jest.fn().mockImplementation(() => ({})),
|
|
96
88
|
}));
|
|
@@ -419,6 +411,30 @@ allowedIssueAuthors: 'user1, user2, user3'
|
|
|
419
411
|
});
|
|
420
412
|
});
|
|
421
413
|
|
|
414
|
+
it('applies the fleet default maximumPreparingIssuesCount when neither README nor YAML provides it', async () => {
|
|
415
|
+
const configWithPreparationNoMax = {
|
|
416
|
+
...validConfig,
|
|
417
|
+
startPreparation: {
|
|
418
|
+
defaultAgentName: 'yaml-agent',
|
|
419
|
+
configFilePath: '/path/to/config.yml',
|
|
420
|
+
utilizationPercentageThreshold: 90,
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
mockFetchReturningReadme(null);
|
|
424
|
+
jest
|
|
425
|
+
.mocked(fs.readFileSync)
|
|
426
|
+
.mockReturnValue(YAML.stringify(configWithPreparationNoMax));
|
|
427
|
+
|
|
428
|
+
const handler = new HandleScheduledEventUseCaseHandler();
|
|
429
|
+
await handler.handle('config.yml', false);
|
|
430
|
+
|
|
431
|
+
expect(capturedRunInputs[0][0]).toMatchObject({
|
|
432
|
+
startPreparation: {
|
|
433
|
+
maximumPreparingIssuesCount: 80,
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
422
438
|
it('should use README token from manager credentials to fetch README', async () => {
|
|
423
439
|
mockFetchReturningReadme(null);
|
|
424
440
|
jest.mocked(fs.readFileSync).mockReturnValue(YAML.stringify(validConfig));
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { writeDashboardRow } from './dashboardRowWriter';
|
|
11
11
|
import { writeMachineStatus } from './machineStatusWriter';
|
|
12
12
|
import { writeTokenStatus } from './tokenStatusWriter';
|
|
13
|
+
import { handleSubscriptionDisabledTokens } from './subscriptionDisabledTokenHandler';
|
|
13
14
|
import { writeInTmuxByHumanData } from './inTmuxByHumanDataWriter';
|
|
14
15
|
import { cleanClosedIssueOwnerCallFiles } from './ownerCallFileCleaner';
|
|
15
16
|
import { reconcileInTmuxByHumanSessions } from './inTmuxByHumanSessionReconciler';
|
|
@@ -32,6 +33,10 @@ import {
|
|
|
32
33
|
fetchProjectReadme,
|
|
33
34
|
parseProjectReadmeConfig,
|
|
34
35
|
} from '../cli/projectConfig';
|
|
36
|
+
import {
|
|
37
|
+
loadStartPreparationFleetSettings,
|
|
38
|
+
resolveFleetConfigFilePath,
|
|
39
|
+
} from '../cli/fleetConfig';
|
|
35
40
|
import { SystemDateRepository } from '../../repositories/SystemDateRepository';
|
|
36
41
|
import { LocalStorageRepository } from '../../repositories/LocalStorageRepository';
|
|
37
42
|
import { GoogleSpreadsheetRepository } from '../../repositories/GoogleSpreadsheetRepository';
|
|
@@ -55,7 +60,6 @@ import { ClearDependedIssueURLUseCase } from '../../../domain/usecases/ClearDepe
|
|
|
55
60
|
import { SetDependedIssueUrlForOpenTaskPRsUseCase } from '../../../domain/usecases/SetDependedIssueUrlForOpenTaskPRsUseCase';
|
|
56
61
|
import { StaleTaskPullRequestCloseUseCase } from '../../../domain/usecases/StaleTaskPullRequestCloseUseCase';
|
|
57
62
|
import { CreateEstimationIssueUseCase } from '../../../domain/usecases/CreateEstimationIssueUseCase';
|
|
58
|
-
import { ConvertCheckboxToIssueInStoryIssueUseCase } from '../../../domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase';
|
|
59
63
|
import { ChangeStatusByStoryColorUseCase } from '../../../domain/usecases/ChangeStatusByStoryColorUseCase';
|
|
60
64
|
import { SetNoStoryIssueToStoryUseCase } from '../../../domain/usecases/SetNoStoryIssueToStoryUseCase';
|
|
61
65
|
import { CreateNewStoryByLabelUseCase } from '../../../domain/usecases/CreateNewStoryByLabelUseCase';
|
|
@@ -272,6 +276,10 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
272
276
|
? parseProjectReadmeConfig(readme, input.projectUrl)
|
|
273
277
|
: {};
|
|
274
278
|
|
|
279
|
+
const startPreparationFleetSettings = loadStartPreparationFleetSettings(
|
|
280
|
+
resolveFleetConfigFilePath(null),
|
|
281
|
+
);
|
|
282
|
+
|
|
275
283
|
const normalizeAllowedIssueAuthors = (
|
|
276
284
|
value: string | string[] | null | undefined,
|
|
277
285
|
): string[] | null => {
|
|
@@ -323,7 +331,8 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
323
331
|
input.startPreparation.defaultLlmAgentName,
|
|
324
332
|
maximumPreparingIssuesCount:
|
|
325
333
|
readmeConfig.maximumPreparingIssuesCount ??
|
|
326
|
-
input.startPreparation.maximumPreparingIssuesCount
|
|
334
|
+
input.startPreparation.maximumPreparingIssuesCount ??
|
|
335
|
+
startPreparationFleetSettings.maximumPreparingIssuesCount,
|
|
327
336
|
utilizationPercentageThreshold:
|
|
328
337
|
readmeConfig.utilizationPercentageThreshold ??
|
|
329
338
|
input.startPreparation.utilizationPercentageThreshold,
|
|
@@ -454,8 +463,6 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
454
463
|
issueRepository,
|
|
455
464
|
systemDateRepository,
|
|
456
465
|
);
|
|
457
|
-
const convertCheckboxToIssueInStoryIssueUseCase =
|
|
458
|
-
new ConvertCheckboxToIssueInStoryIssueUseCase(issueRepository);
|
|
459
466
|
const changeStatusByStoryColorUseCase = new ChangeStatusByStoryColorUseCase(
|
|
460
467
|
systemDateRepository,
|
|
461
468
|
issueRepository,
|
|
@@ -551,7 +558,6 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
551
558
|
setDependedIssueUrlForOpenTaskPRsUseCase,
|
|
552
559
|
staleTaskPullRequestCloseUseCase,
|
|
553
560
|
createEstimationIssueUseCase,
|
|
554
|
-
convertCheckboxToIssueInStoryIssueUseCase,
|
|
555
561
|
changeStatusByStoryColorUseCase,
|
|
556
562
|
setNoStoryIssueToStoryUseCase,
|
|
557
563
|
createNewStoryByLabel,
|
|
@@ -685,6 +691,22 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
685
691
|
);
|
|
686
692
|
}
|
|
687
693
|
|
|
694
|
+
try {
|
|
695
|
+
await handleSubscriptionDisabledTokens({
|
|
696
|
+
tokenListJsonPath:
|
|
697
|
+
mergedInput.claudeCodeOauthTokenListJsonPath ?? null,
|
|
698
|
+
org: input.org,
|
|
699
|
+
repo: input.workingReport.repo,
|
|
700
|
+
issueRepository,
|
|
701
|
+
});
|
|
702
|
+
} catch (error) {
|
|
703
|
+
console.error(
|
|
704
|
+
`Failed to handle subscription disabled tokens: ${
|
|
705
|
+
error instanceof Error ? error.message : String(error)
|
|
706
|
+
}`,
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
688
710
|
const inTmuxNow = new Date();
|
|
689
711
|
|
|
690
712
|
const inTmuxGetuid = process.getuid?.bind(process);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { IssueRepository } from '../../../domain/usecases/adapter-interfaces/IssueRepository';
|
|
2
|
+
import { SubscriptionDisabledIssueUseCase } from '../../../domain/usecases/SubscriptionDisabledIssueUseCase';
|
|
3
|
+
import { loadTokenEntries } from '../../proxy/TokenListLoader';
|
|
4
|
+
import { readRateLimit } from '../../proxy/RateLimitCache';
|
|
5
|
+
|
|
6
|
+
export type SubscriptionDisabledTokenHandlerParams = {
|
|
7
|
+
tokenListJsonPath: string | null;
|
|
8
|
+
org: string;
|
|
9
|
+
repo: string;
|
|
10
|
+
issueRepository: Pick<
|
|
11
|
+
IssueRepository,
|
|
12
|
+
'searchIssue' | 'createNewIssue' | 'createCommentByUrl'
|
|
13
|
+
>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const handleSubscriptionDisabledTokens = async (
|
|
17
|
+
params: SubscriptionDisabledTokenHandlerParams,
|
|
18
|
+
): Promise<void> => {
|
|
19
|
+
const { tokenListJsonPath, org, repo, issueRepository } = params;
|
|
20
|
+
|
|
21
|
+
if (tokenListJsonPath === null) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const entries = loadTokenEntries(tokenListJsonPath);
|
|
26
|
+
if (entries === null) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const tokenEntries = entries.map((entry) => ({
|
|
31
|
+
name: entry.name,
|
|
32
|
+
subscriptionDisabled:
|
|
33
|
+
readRateLimit(entry.token)?.subscriptionDisabled ?? false,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
await new SubscriptionDisabledIssueUseCase(issueRepository).run({
|
|
37
|
+
tokenEntries,
|
|
38
|
+
org,
|
|
39
|
+
repo,
|
|
40
|
+
});
|
|
41
|
+
};
|
|
@@ -126,12 +126,12 @@ describe('githubGraphqlClient', () => {
|
|
|
126
126
|
' at postGithubGraphqlJson (/app/bin/adapter/repositories/githubGraphqlClient.js:120:22)',
|
|
127
127
|
' at GraphqlProjectItemRepository.fetchProjectItemByUrl (/app/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js:1042:11)',
|
|
128
128
|
' at ApiV3CheerioRestIssueRepository.getIssueByUrl (/app/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js:813:11)',
|
|
129
|
-
' at async
|
|
129
|
+
' at async ChangeStatusByStoryColorUseCase.run (/app/bin/domain/usecases/ChangeStatusByStoryColorUseCase.js:63:31)',
|
|
130
130
|
' at async HandleScheduledEventUseCase.runSlowSweepUseCases (/app/bin/domain/usecases/HandleScheduledEventUseCase.js:552:5)',
|
|
131
131
|
].join('\n');
|
|
132
132
|
|
|
133
133
|
expect(extractGraphqlCallSite(stack)).toBe(
|
|
134
|
-
'GraphqlProjectItemRepository<-ApiV3CheerioRestIssueRepository<-
|
|
134
|
+
'GraphqlProjectItemRepository<-ApiV3CheerioRestIssueRepository<-ChangeStatusByStoryColorUseCase',
|
|
135
135
|
);
|
|
136
136
|
});
|
|
137
137
|
|
|
@@ -9,7 +9,6 @@ import { ClearDependedIssueURLUseCase } from './ClearDependedIssueURLUseCase';
|
|
|
9
9
|
import { SetDependedIssueUrlForOpenTaskPRsUseCase } from './SetDependedIssueUrlForOpenTaskPRsUseCase';
|
|
10
10
|
import { StaleTaskPullRequestCloseUseCase } from './StaleTaskPullRequestCloseUseCase';
|
|
11
11
|
import { CreateEstimationIssueUseCase } from './CreateEstimationIssueUseCase';
|
|
12
|
-
import { ConvertCheckboxToIssueInStoryIssueUseCase } from './ConvertCheckboxToIssueInStoryIssueUseCase';
|
|
13
12
|
import { DateRepository } from './adapter-interfaces/DateRepository';
|
|
14
13
|
import { SpreadsheetRepository } from './adapter-interfaces/SpreadsheetRepository';
|
|
15
14
|
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
@@ -112,8 +111,6 @@ describe('HandleScheduledEventUseCase', () => {
|
|
|
112
111
|
mock<StaleTaskPullRequestCloseUseCase>();
|
|
113
112
|
const mockCreateEstimationIssueUseCase =
|
|
114
113
|
mock<CreateEstimationIssueUseCase>();
|
|
115
|
-
const mockConvertCheckboxToIssueInStoryIssueUseCase =
|
|
116
|
-
mock<ConvertCheckboxToIssueInStoryIssueUseCase>();
|
|
117
114
|
const mockChangeStatusByStoryColorUseCase =
|
|
118
115
|
mock<ChangeStatusByStoryColorUseCase>();
|
|
119
116
|
const mockSetNoStoryIssueToStoryUseCase =
|
|
@@ -158,7 +155,6 @@ describe('HandleScheduledEventUseCase', () => {
|
|
|
158
155
|
mockSetDependedIssueUrlForOpenTaskPRsUseCase,
|
|
159
156
|
mockStaleTaskPullRequestCloseUseCase,
|
|
160
157
|
mockCreateEstimationIssueUseCase,
|
|
161
|
-
mockConvertCheckboxToIssueInStoryIssueUseCase,
|
|
162
158
|
mockChangeStatusByStoryColorUseCase,
|
|
163
159
|
mockSetNoStoryIssueToStoryUseCase,
|
|
164
160
|
mockCreateNewStoryByLabelUseCase,
|
|
@@ -353,57 +349,6 @@ describe('HandleScheduledEventUseCase', () => {
|
|
|
353
349
|
expect(mockProjectRepository.findProjectIdByUrl).toHaveBeenCalled();
|
|
354
350
|
});
|
|
355
351
|
|
|
356
|
-
it('should pass createTaskFromStoryBodyCheckboxEnabled false to ConvertCheckboxToIssueInStoryIssueUseCase when the field is absent', async () => {
|
|
357
|
-
const input = {
|
|
358
|
-
projectName: 'test-project',
|
|
359
|
-
org: 'test-org',
|
|
360
|
-
projectUrl: 'https://github.com/test-org/test-project',
|
|
361
|
-
manager: 'test-manager',
|
|
362
|
-
workingReport: {
|
|
363
|
-
repo: 'test-repo',
|
|
364
|
-
members: ['member1'],
|
|
365
|
-
spreadsheetUrl: 'https://docs.google.com/spreadsheets/test',
|
|
366
|
-
},
|
|
367
|
-
urlOfStoryView: 'https://github.com/test-org/test-project/issues',
|
|
368
|
-
disabled: false,
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
await useCase.run(input);
|
|
372
|
-
expect(
|
|
373
|
-
mockConvertCheckboxToIssueInStoryIssueUseCase.run,
|
|
374
|
-
).toHaveBeenCalledWith(
|
|
375
|
-
expect.objectContaining({
|
|
376
|
-
createTaskFromStoryBodyCheckboxEnabled: false,
|
|
377
|
-
}),
|
|
378
|
-
);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it('should pass createTaskFromStoryBodyCheckboxEnabled true to ConvertCheckboxToIssueInStoryIssueUseCase when the field is true', async () => {
|
|
382
|
-
const input = {
|
|
383
|
-
projectName: 'test-project',
|
|
384
|
-
org: 'test-org',
|
|
385
|
-
projectUrl: 'https://github.com/test-org/test-project',
|
|
386
|
-
manager: 'test-manager',
|
|
387
|
-
workingReport: {
|
|
388
|
-
repo: 'test-repo',
|
|
389
|
-
members: ['member1'],
|
|
390
|
-
spreadsheetUrl: 'https://docs.google.com/spreadsheets/test',
|
|
391
|
-
},
|
|
392
|
-
urlOfStoryView: 'https://github.com/test-org/test-project/issues',
|
|
393
|
-
disabled: false,
|
|
394
|
-
createTaskFromStoryBodyCheckboxEnabled: true,
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
await useCase.run(input);
|
|
398
|
-
expect(
|
|
399
|
-
mockConvertCheckboxToIssueInStoryIssueUseCase.run,
|
|
400
|
-
).toHaveBeenCalledWith(
|
|
401
|
-
expect.objectContaining({
|
|
402
|
-
createTaskFromStoryBodyCheckboxEnabled: true,
|
|
403
|
-
}),
|
|
404
|
-
);
|
|
405
|
-
});
|
|
406
|
-
|
|
407
352
|
it('should pass storyProgressCommentEnabled true to AnalyzeProblemByIssueUseCase when the field is absent', async () => {
|
|
408
353
|
const input = {
|
|
409
354
|
projectName: 'test-project',
|
|
@@ -1872,7 +1817,6 @@ describe('HandleScheduledEventUseCase', () => {
|
|
|
1872
1817
|
mock<SetDependedIssueUrlForOpenTaskPRsUseCase>(),
|
|
1873
1818
|
mock<StaleTaskPullRequestCloseUseCase>(),
|
|
1874
1819
|
mock<CreateEstimationIssueUseCase>(),
|
|
1875
|
-
mock<ConvertCheckboxToIssueInStoryIssueUseCase>(),
|
|
1876
1820
|
mock<ChangeStatusByStoryColorUseCase>(),
|
|
1877
1821
|
mock<SetNoStoryIssueToStoryUseCase>(),
|
|
1878
1822
|
mock<CreateNewStoryByLabelUseCase>(),
|
|
@@ -15,7 +15,6 @@ import { ClearDependedIssueURLUseCase } from './ClearDependedIssueURLUseCase';
|
|
|
15
15
|
import { SetDependedIssueUrlForOpenTaskPRsUseCase } from './SetDependedIssueUrlForOpenTaskPRsUseCase';
|
|
16
16
|
import { StaleTaskPullRequestCloseUseCase } from './StaleTaskPullRequestCloseUseCase';
|
|
17
17
|
import { CreateEstimationIssueUseCase } from './CreateEstimationIssueUseCase';
|
|
18
|
-
import { ConvertCheckboxToIssueInStoryIssueUseCase } from './ConvertCheckboxToIssueInStoryIssueUseCase';
|
|
19
18
|
import { ChangeStatusByStoryColorUseCase } from './ChangeStatusByStoryColorUseCase';
|
|
20
19
|
import { SetNoStoryIssueToStoryUseCase } from './SetNoStoryIssueToStoryUseCase';
|
|
21
20
|
import { CreateNewStoryByLabelUseCase } from './CreateNewStoryByLabelUseCase';
|
|
@@ -130,7 +129,6 @@ export class HandleScheduledEventUseCase {
|
|
|
130
129
|
readonly setDependedIssueUrlForOpenTaskPRsUseCase: SetDependedIssueUrlForOpenTaskPRsUseCase,
|
|
131
130
|
readonly staleTaskPullRequestCloseUseCase: StaleTaskPullRequestCloseUseCase,
|
|
132
131
|
readonly createEstimationIssueUseCase: CreateEstimationIssueUseCase,
|
|
133
|
-
readonly convertCheckboxToIssueInStoryIssueUseCase: ConvertCheckboxToIssueInStoryIssueUseCase,
|
|
134
132
|
readonly changeStatusByStoryColorUseCase: ChangeStatusByStoryColorUseCase,
|
|
135
133
|
readonly setNoStoryIssueToStoryUseCase: SetNoStoryIssueToStoryUseCase,
|
|
136
134
|
readonly createNewStoryByLabelUseCase: CreateNewStoryByLabelUseCase,
|
|
@@ -191,7 +189,6 @@ export class HandleScheduledEventUseCase {
|
|
|
191
189
|
} | null;
|
|
192
190
|
thresholdForAutoReject?: number;
|
|
193
191
|
thresholdForDispatchLoop?: number;
|
|
194
|
-
createTaskFromStoryBodyCheckboxEnabled?: boolean;
|
|
195
192
|
queryToAddProjectEnabled?: boolean;
|
|
196
193
|
queryToAddProject?: string | null;
|
|
197
194
|
storyProgressCommentEnabled?: boolean;
|
|
@@ -615,16 +612,6 @@ ${JSON.stringify(e)}
|
|
|
615
612
|
urlOfStoryView: input.urlOfStoryView,
|
|
616
613
|
storyObjectMap: storyObjectMap,
|
|
617
614
|
});
|
|
618
|
-
await this.convertCheckboxToIssueInStoryIssueUseCase.run({
|
|
619
|
-
project,
|
|
620
|
-
issues,
|
|
621
|
-
cacheUsed,
|
|
622
|
-
urlOfStoryView: input.urlOfStoryView,
|
|
623
|
-
storyObjectMap: storyObjectMap,
|
|
624
|
-
manager: input.manager,
|
|
625
|
-
createTaskFromStoryBodyCheckboxEnabled:
|
|
626
|
-
input.createTaskFromStoryBodyCheckboxEnabled ?? false,
|
|
627
|
-
});
|
|
628
615
|
await this.changeStatusByStoryColorUseCase.run({
|
|
629
616
|
project,
|
|
630
617
|
cacheUsed,
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { mock } from 'jest-mock-extended';
|
|
2
|
+
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
3
|
+
import { SubscriptionDisabledIssueUseCase } from './SubscriptionDisabledIssueUseCase';
|
|
4
|
+
|
|
5
|
+
const ORG = 'test-org';
|
|
6
|
+
const REPO = 'test-repo';
|
|
7
|
+
|
|
8
|
+
const buildEntry = (name: string, subscriptionDisabled: boolean) => ({
|
|
9
|
+
name,
|
|
10
|
+
subscriptionDisabled,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('SubscriptionDisabledIssueUseCase', () => {
|
|
14
|
+
describe('when no tokens are disabled', () => {
|
|
15
|
+
it('does not create any issue or comment', async () => {
|
|
16
|
+
const mockIssueRepository = mock<IssueRepository>();
|
|
17
|
+
|
|
18
|
+
const useCase = new SubscriptionDisabledIssueUseCase(mockIssueRepository);
|
|
19
|
+
|
|
20
|
+
await useCase.run({
|
|
21
|
+
tokenEntries: [buildEntry('dev1', false), buildEntry('dev2', false)],
|
|
22
|
+
org: ORG,
|
|
23
|
+
repo: REPO,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(mockIssueRepository.searchIssue.mock.calls).toHaveLength(0);
|
|
27
|
+
expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(0);
|
|
28
|
+
expect(mockIssueRepository.createCommentByUrl.mock.calls).toHaveLength(0);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('when a token is disabled and no open issue exists', () => {
|
|
33
|
+
it('creates a new issue naming the token by display name only', async () => {
|
|
34
|
+
const mockIssueRepository = mock<IssueRepository>();
|
|
35
|
+
mockIssueRepository.searchIssue.mockResolvedValue([]);
|
|
36
|
+
mockIssueRepository.createNewIssue.mockResolvedValue(42);
|
|
37
|
+
|
|
38
|
+
const useCase = new SubscriptionDisabledIssueUseCase(mockIssueRepository);
|
|
39
|
+
|
|
40
|
+
await useCase.run({
|
|
41
|
+
tokenEntries: [buildEntry('dev3', true)],
|
|
42
|
+
org: ORG,
|
|
43
|
+
repo: REPO,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(mockIssueRepository.searchIssue.mock.calls).toHaveLength(1);
|
|
47
|
+
expect(mockIssueRepository.searchIssue.mock.calls[0][0]).toEqual({
|
|
48
|
+
owner: ORG,
|
|
49
|
+
repositoryName: REPO,
|
|
50
|
+
type: 'issue',
|
|
51
|
+
state: 'open',
|
|
52
|
+
title: 'Restore Claude subscription access for dev3',
|
|
53
|
+
});
|
|
54
|
+
expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(1);
|
|
55
|
+
expect(mockIssueRepository.createNewIssue.mock.calls[0][0]).toBe(ORG);
|
|
56
|
+
expect(mockIssueRepository.createNewIssue.mock.calls[0][1]).toBe(REPO);
|
|
57
|
+
expect(mockIssueRepository.createNewIssue.mock.calls[0][2]).toBe(
|
|
58
|
+
'Restore Claude subscription access for dev3',
|
|
59
|
+
);
|
|
60
|
+
expect(mockIssueRepository.createNewIssue.mock.calls[0][3]).toContain(
|
|
61
|
+
'dev3',
|
|
62
|
+
);
|
|
63
|
+
expect(mockIssueRepository.createCommentByUrl.mock.calls).toHaveLength(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('does not include the token value in the issue body', async () => {
|
|
67
|
+
const mockIssueRepository = mock<IssueRepository>();
|
|
68
|
+
mockIssueRepository.searchIssue.mockResolvedValue([]);
|
|
69
|
+
mockIssueRepository.createNewIssue.mockResolvedValue(1);
|
|
70
|
+
|
|
71
|
+
const useCase = new SubscriptionDisabledIssueUseCase(mockIssueRepository);
|
|
72
|
+
|
|
73
|
+
const SECRET_TOKEN = 'sk-ant-secret-abc123';
|
|
74
|
+
await useCase.run({
|
|
75
|
+
tokenEntries: [{ name: 'dev3', subscriptionDisabled: true }],
|
|
76
|
+
org: ORG,
|
|
77
|
+
repo: REPO,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(mockIssueRepository.createNewIssue.mock.calls[0][3]).not.toContain(
|
|
81
|
+
SECRET_TOKEN,
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('when a token is disabled and an open issue already exists', () => {
|
|
87
|
+
it('comments on the existing issue instead of creating a new one', async () => {
|
|
88
|
+
const mockIssueRepository = mock<IssueRepository>();
|
|
89
|
+
const existingIssueUrl =
|
|
90
|
+
'https://github.com/test-org/test-repo/issues/99';
|
|
91
|
+
mockIssueRepository.searchIssue.mockResolvedValue([
|
|
92
|
+
{
|
|
93
|
+
url: existingIssueUrl,
|
|
94
|
+
title: 'Restore Claude subscription access for dev3',
|
|
95
|
+
number: '99',
|
|
96
|
+
},
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
const useCase = new SubscriptionDisabledIssueUseCase(mockIssueRepository);
|
|
100
|
+
|
|
101
|
+
await useCase.run({
|
|
102
|
+
tokenEntries: [buildEntry('dev3', true)],
|
|
103
|
+
org: ORG,
|
|
104
|
+
repo: REPO,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(0);
|
|
108
|
+
expect(mockIssueRepository.createCommentByUrl.mock.calls).toHaveLength(1);
|
|
109
|
+
expect(mockIssueRepository.createCommentByUrl.mock.calls[0][0]).toBe(
|
|
110
|
+
existingIssueUrl,
|
|
111
|
+
);
|
|
112
|
+
expect(mockIssueRepository.createCommentByUrl.mock.calls[0][1]).toContain(
|
|
113
|
+
'dev3',
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('when multiple tokens have different states', () => {
|
|
119
|
+
it('only handles disabled tokens', async () => {
|
|
120
|
+
const mockIssueRepository = mock<IssueRepository>();
|
|
121
|
+
mockIssueRepository.searchIssue.mockResolvedValue([]);
|
|
122
|
+
mockIssueRepository.createNewIssue.mockResolvedValue(1);
|
|
123
|
+
|
|
124
|
+
const useCase = new SubscriptionDisabledIssueUseCase(mockIssueRepository);
|
|
125
|
+
|
|
126
|
+
await useCase.run({
|
|
127
|
+
tokenEntries: [
|
|
128
|
+
buildEntry('dev1', false),
|
|
129
|
+
buildEntry('dev2', true),
|
|
130
|
+
buildEntry('dev3', false),
|
|
131
|
+
buildEntry('dev4', true),
|
|
132
|
+
],
|
|
133
|
+
org: ORG,
|
|
134
|
+
repo: REPO,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(mockIssueRepository.searchIssue.mock.calls).toHaveLength(2);
|
|
138
|
+
expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(2);
|
|
139
|
+
expect(mockIssueRepository.searchIssue.mock.calls[0][0]).toEqual(
|
|
140
|
+
expect.objectContaining({
|
|
141
|
+
title: 'Restore Claude subscription access for dev2',
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
expect(mockIssueRepository.searchIssue.mock.calls[1][0]).toEqual(
|
|
145
|
+
expect.objectContaining({
|
|
146
|
+
title: 'Restore Claude subscription access for dev4',
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('when the token list is empty', () => {
|
|
153
|
+
it('does nothing', async () => {
|
|
154
|
+
const mockIssueRepository = mock<IssueRepository>();
|
|
155
|
+
|
|
156
|
+
const useCase = new SubscriptionDisabledIssueUseCase(mockIssueRepository);
|
|
157
|
+
|
|
158
|
+
await useCase.run({
|
|
159
|
+
tokenEntries: [],
|
|
160
|
+
org: ORG,
|
|
161
|
+
repo: REPO,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(mockIssueRepository.searchIssue.mock.calls).toHaveLength(0);
|
|
165
|
+
expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(0);
|
|
166
|
+
expect(mockIssueRepository.createCommentByUrl.mock.calls).toHaveLength(0);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('when searchIssue returns issues but none with exact title match', () => {
|
|
171
|
+
it('creates a new issue when title does not exactly match', async () => {
|
|
172
|
+
const mockIssueRepository = mock<IssueRepository>();
|
|
173
|
+
mockIssueRepository.searchIssue.mockResolvedValue([
|
|
174
|
+
{
|
|
175
|
+
url: 'https://github.com/test-org/test-repo/issues/5',
|
|
176
|
+
title: 'Restore Claude subscription access for dev3-old',
|
|
177
|
+
number: '5',
|
|
178
|
+
},
|
|
179
|
+
]);
|
|
180
|
+
mockIssueRepository.createNewIssue.mockResolvedValue(6);
|
|
181
|
+
|
|
182
|
+
const useCase = new SubscriptionDisabledIssueUseCase(mockIssueRepository);
|
|
183
|
+
|
|
184
|
+
await useCase.run({
|
|
185
|
+
tokenEntries: [buildEntry('dev3', true)],
|
|
186
|
+
org: ORG,
|
|
187
|
+
repo: REPO,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(1);
|
|
191
|
+
expect(mockIssueRepository.createNewIssue.mock.calls[0][2]).toBe(
|
|
192
|
+
'Restore Claude subscription access for dev3',
|
|
193
|
+
);
|
|
194
|
+
expect(mockIssueRepository.createCommentByUrl.mock.calls).toHaveLength(0);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|