github-issue-tower-defence-management 1.60.2 → 1.64.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/publish.yml +13 -0
- package/.github/workflows/test.yml +0 -4
- package/CHANGELOG.md +14 -0
- package/README.md +53 -10
- package/bin/adapter/entry-points/cli/index.js +11 -11
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.js +3 -22
- package/bin/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +8 -22
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/rotationOrderFileWriter.js +56 -0
- package/bin/adapter/entry-points/handlers/rotationOrderFileWriter.js.map +1 -0
- package/bin/adapter/entry-points/handlers/situationFileWriter.js +5 -0
- package/bin/adapter/entry-points/handlers/situationFileWriter.js.map +1 -1
- package/bin/adapter/proxy/TokenListLoader.js +21 -6
- package/bin/adapter/proxy/TokenListLoader.js.map +1 -1
- package/bin/adapter/proxy/proxyEntry.js +1 -0
- package/bin/adapter/proxy/proxyEntry.js.map +1 -1
- package/bin/adapter/repositories/BaseGitHubRepository.js +1 -113
- package/bin/adapter/repositories/BaseGitHubRepository.js.map +1 -1
- package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js +5 -3
- package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +8 -7
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +14 -3
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/IssueRejectionEvaluator.js +8 -1
- package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +5 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +1 -1
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.js +32 -1
- package/bin/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +148 -28
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/package.json +1 -4
- package/src/adapter/entry-points/cli/index.test.ts +16 -16
- package/src/adapter/entry-points/cli/index.ts +8 -11
- package/src/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.test.ts +2 -55
- package/src/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.ts +1 -11
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +6 -56
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +7 -11
- package/src/adapter/entry-points/handlers/rotationOrderFileWriter.test.ts +177 -0
- package/src/adapter/entry-points/handlers/rotationOrderFileWriter.ts +20 -0
- package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +36 -0
- package/src/adapter/entry-points/handlers/situationFileWriter.ts +8 -0
- package/src/adapter/proxy/TokenListLoader.test.ts +50 -1
- package/src/adapter/proxy/TokenListLoader.ts +25 -5
- package/src/adapter/proxy/proxyEntry.test.ts +270 -1
- package/src/adapter/proxy/proxyEntry.ts +2 -1
- package/src/adapter/repositories/BaseGitHubRepository.test.ts +1 -186
- package/src/adapter/repositories/BaseGitHubRepository.ts +1 -139
- package/src/adapter/repositories/GraphqlProjectRepository.errorHandling.test.ts +0 -1
- package/src/adapter/repositories/GraphqlProjectRepository.fetchProjectId.test.ts +4 -1
- package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.test.ts +60 -19
- package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.ts +6 -4
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +23 -13
- package/src/adapter/repositories/issue/ApiV3IssueRepository.test.ts +0 -1
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +0 -8
- package/src/adapter/repositories/issue/RestIssueRepository.test.ts +0 -1
- package/src/domain/entities/ClaudeTokenUsage.ts +1 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +4 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +20 -5
- package/src/domain/usecases/IssueRejectionEvaluator.test.ts +153 -0
- package/src/domain/usecases/IssueRejectionEvaluator.ts +8 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +175 -31
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +7 -1
- package/src/domain/usecases/RevertNotReadyAwaitingQualityCheckUseCase.test.ts +32 -0
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +39 -5
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +1 -1
- package/src/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.test.ts +139 -20
- package/src/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.ts +62 -2
- package/src/domain/usecases/StartPreparationUseCase.test.ts +614 -23
- package/src/domain/usecases/StartPreparationUseCase.ts +226 -35
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +16 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/rotationOrderFileWriter.d.ts +3 -0
- package/types/adapter/entry-points/handlers/rotationOrderFileWriter.d.ts.map +1 -0
- package/types/adapter/entry-points/handlers/situationFileWriter.d.ts +1 -0
- package/types/adapter/entry-points/handlers/situationFileWriter.d.ts.map +1 -1
- package/types/adapter/proxy/TokenListLoader.d.ts +5 -0
- package/types/adapter/proxy/TokenListLoader.d.ts.map +1 -1
- package/types/adapter/proxy/proxyEntry.d.ts +2 -1
- package/types/adapter/proxy/proxyEntry.d.ts.map +1 -1
- package/types/adapter/repositories/BaseGitHubRepository.d.ts +1 -23
- package/types/adapter/repositories/BaseGitHubRepository.d.ts.map +1 -1
- package/types/adapter/repositories/ProxyClaudeTokenUsageRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +14 -5
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/entities/ClaudeTokenUsage.d.ts +1 -0
- package/types/domain/entities/ClaudeTokenUsage.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +5 -2
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts +1 -1
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.d.ts +5 -2
- package/types/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts +18 -2
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +14 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/bin/adapter/repositories/issue/CheerioIssueRepository.js +0 -136
- package/bin/adapter/repositories/issue/CheerioIssueRepository.js.map +0 -1
- package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js +0 -1606
- package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js.map +0 -1
- package/src/adapter/repositories/issue/CheerioIssueRepository.test.ts +0 -6552
- package/src/adapter/repositories/issue/CheerioIssueRepository.ts +0 -142
- package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.test.ts +0 -118
- package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.ts +0 -584
- package/types/adapter/repositories/issue/CheerioIssueRepository.d.ts +0 -40
- package/types/adapter/repositories/issue/CheerioIssueRepository.d.ts.map +0 -1
- package/types/adapter/repositories/issue/InternalGraphqlIssueRepository.d.ts +0 -220
- package/types/adapter/repositories/issue/InternalGraphqlIssueRepository.d.ts.map +0 -1
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
fetchProjectReadme,
|
|
17
17
|
} from './projectConfig';
|
|
18
18
|
import { StartPreparationUseCase } from '../../../domain/usecases/StartPreparationUseCase';
|
|
19
|
+
import { writeRotationOrderFile } from '../handlers/rotationOrderFileWriter';
|
|
19
20
|
import { ProxyClaudeTokenUsageRepository } from '../../repositories/ProxyClaudeTokenUsageRepository';
|
|
20
21
|
import { NotifyFinishedIssuePreparationUseCase } from '../../../domain/usecases/NotifyFinishedIssuePreparationUseCase';
|
|
21
22
|
import { LocalStorageRepository } from '../../repositories/LocalStorageRepository';
|
|
@@ -54,15 +55,10 @@ type NotifyFinishedOptions = {
|
|
|
54
55
|
|
|
55
56
|
const buildGithubRepositoryParams = (
|
|
56
57
|
localStorageRepository: LocalStorageRepository,
|
|
57
|
-
cachePath: string,
|
|
58
58
|
token: string,
|
|
59
59
|
): ConstructorParameters<typeof BaseGitHubRepository> => [
|
|
60
60
|
localStorageRepository,
|
|
61
|
-
`${cachePath}/github.com.cookies.json`,
|
|
62
61
|
token,
|
|
63
|
-
undefined,
|
|
64
|
-
undefined,
|
|
65
|
-
undefined,
|
|
66
62
|
];
|
|
67
63
|
|
|
68
64
|
interface ScheduleOptions {
|
|
@@ -113,7 +109,7 @@ program
|
|
|
113
109
|
.option('--defaultLlmAgentName <name>', 'Default LLM agent name')
|
|
114
110
|
.option(
|
|
115
111
|
'--maximumPreparingIssuesCount <count>',
|
|
116
|
-
'Maximum number of issues in preparation status (default: 6)',
|
|
112
|
+
'Maximum number of issues in preparation status (default: 6 per available Claude OAuth token, otherwise 6)',
|
|
117
113
|
)
|
|
118
114
|
.option(
|
|
119
115
|
'--allowIssueCacheMinutes <minutes>',
|
|
@@ -121,7 +117,7 @@ program
|
|
|
121
117
|
)
|
|
122
118
|
.option(
|
|
123
119
|
'--utilizationPercentageThreshold <percent>',
|
|
124
|
-
'Claude utilization
|
|
120
|
+
'Legacy Claude utilization threshold setting; token process slots decay from 80% utilization to 0 at 95% (default: 90)',
|
|
125
121
|
)
|
|
126
122
|
.option(
|
|
127
123
|
'--allowedIssueAuthors <authors>',
|
|
@@ -211,7 +207,7 @@ program
|
|
|
211
207
|
const allowIssueCacheMinutes = config.allowIssueCacheMinutes ?? 10;
|
|
212
208
|
|
|
213
209
|
console.log(
|
|
214
|
-
`maximumPreparingIssuesCount: ${maximumPreparingIssuesCount ?? 'null (default: 6)'}`,
|
|
210
|
+
`maximumPreparingIssuesCount: ${maximumPreparingIssuesCount ?? 'null (default: 6 per available Claude OAuth token, otherwise 6)'}`,
|
|
215
211
|
);
|
|
216
212
|
|
|
217
213
|
const projectName = config.projectName ?? 'default';
|
|
@@ -223,7 +219,6 @@ program
|
|
|
223
219
|
);
|
|
224
220
|
const githubRepositoryParams = buildGithubRepositoryParams(
|
|
225
221
|
localStorageRepository,
|
|
226
|
-
cachePath,
|
|
227
222
|
token,
|
|
228
223
|
);
|
|
229
224
|
const projectRepository = new GraphqlProjectRepository(
|
|
@@ -291,7 +286,7 @@ program
|
|
|
291
286
|
? config.codexHomeCandidates
|
|
292
287
|
: null;
|
|
293
288
|
|
|
294
|
-
await useCase.run({
|
|
289
|
+
const preparationResult = await useCase.run({
|
|
295
290
|
projectUrl,
|
|
296
291
|
defaultAgentName,
|
|
297
292
|
defaultLlmModelName: config.defaultLlmModelName ?? null,
|
|
@@ -304,6 +299,9 @@ program
|
|
|
304
299
|
codexHomeCandidates,
|
|
305
300
|
allowIssueCacheMinutes,
|
|
306
301
|
});
|
|
302
|
+
if (preparationResult.rotationOrder !== null) {
|
|
303
|
+
writeRotationOrderFile(preparationResult.rotationOrder);
|
|
304
|
+
}
|
|
307
305
|
});
|
|
308
306
|
|
|
309
307
|
program
|
|
@@ -396,7 +394,6 @@ program
|
|
|
396
394
|
);
|
|
397
395
|
const githubRepositoryParams = buildGithubRepositoryParams(
|
|
398
396
|
localStorageRepository,
|
|
399
|
-
cachePath,
|
|
400
397
|
token,
|
|
401
398
|
);
|
|
402
399
|
const projectRepository = new GraphqlProjectRepository(
|
|
@@ -93,43 +93,17 @@ describe('GetStoryObjectMapUseCaseHandler', () => {
|
|
|
93
93
|
);
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
it('should pass bot
|
|
97
|
-
const configWithCredentials = {
|
|
98
|
-
...validConfig,
|
|
99
|
-
credentials: {
|
|
100
|
-
bot: {
|
|
101
|
-
github: {
|
|
102
|
-
token: 'test-token',
|
|
103
|
-
name: 'bot-user',
|
|
104
|
-
password: 'bot-pass',
|
|
105
|
-
authenticatorKey: 'bot-auth-key',
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
jest
|
|
111
|
-
.mocked(fs.readFileSync)
|
|
112
|
-
.mockReturnValue(YAML.stringify(configWithCredentials));
|
|
113
|
-
|
|
96
|
+
it('should pass bot token to repository constructors', async () => {
|
|
114
97
|
const handler = new GetStoryObjectMapUseCaseHandler();
|
|
115
98
|
await handler.handle('config.yml', false);
|
|
116
99
|
|
|
117
|
-
const expectedCookiePath = `./tmp/cache/${validConfig.projectName}/github.com.cookies.json`;
|
|
118
|
-
|
|
119
100
|
for (const MockedClass of [
|
|
120
101
|
MockedGraphqlProjectRepository,
|
|
121
102
|
MockedApiV3IssueRepository,
|
|
122
103
|
MockedRestIssueRepository,
|
|
123
104
|
MockedGraphqlProjectItemRepository,
|
|
124
105
|
]) {
|
|
125
|
-
expect(MockedClass).toHaveBeenCalledWith(
|
|
126
|
-
expect.anything(),
|
|
127
|
-
expectedCookiePath,
|
|
128
|
-
'test-token',
|
|
129
|
-
'bot-user',
|
|
130
|
-
'bot-pass',
|
|
131
|
-
'bot-auth-key',
|
|
132
|
-
);
|
|
106
|
+
expect(MockedClass).toHaveBeenCalledWith(expect.anything(), 'test-token');
|
|
133
107
|
}
|
|
134
108
|
|
|
135
109
|
expect(MockedApiV3CheerioRestIssueRepository).toHaveBeenCalledWith(
|
|
@@ -138,34 +112,7 @@ describe('GetStoryObjectMapUseCaseHandler', () => {
|
|
|
138
112
|
expect.anything(),
|
|
139
113
|
expect.anything(),
|
|
140
114
|
expect.anything(),
|
|
141
|
-
expectedCookiePath,
|
|
142
115
|
'test-token',
|
|
143
|
-
'bot-user',
|
|
144
|
-
'bot-pass',
|
|
145
|
-
'bot-auth-key',
|
|
146
116
|
);
|
|
147
117
|
});
|
|
148
|
-
|
|
149
|
-
it('should pass undefined credentials when not provided in config', async () => {
|
|
150
|
-
const handler = new GetStoryObjectMapUseCaseHandler();
|
|
151
|
-
await handler.handle('config.yml', false);
|
|
152
|
-
|
|
153
|
-
const expectedCookiePath = `./tmp/cache/${validConfig.projectName}/github.com.cookies.json`;
|
|
154
|
-
|
|
155
|
-
for (const MockedClass of [
|
|
156
|
-
MockedGraphqlProjectRepository,
|
|
157
|
-
MockedApiV3IssueRepository,
|
|
158
|
-
MockedRestIssueRepository,
|
|
159
|
-
MockedGraphqlProjectItemRepository,
|
|
160
|
-
]) {
|
|
161
|
-
expect(MockedClass).toHaveBeenCalledWith(
|
|
162
|
-
expect.anything(),
|
|
163
|
-
expectedCookiePath,
|
|
164
|
-
'test-token',
|
|
165
|
-
undefined,
|
|
166
|
-
undefined,
|
|
167
|
-
undefined,
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
118
|
});
|
|
@@ -33,9 +33,6 @@ export class GetStoryObjectMapUseCaseHandler {
|
|
|
33
33
|
bot: {
|
|
34
34
|
github: {
|
|
35
35
|
token: string;
|
|
36
|
-
name?: string;
|
|
37
|
-
password?: string;
|
|
38
|
-
authenticatorKey?: string;
|
|
39
36
|
};
|
|
40
37
|
};
|
|
41
38
|
};
|
|
@@ -54,14 +51,7 @@ export class GetStoryObjectMapUseCaseHandler {
|
|
|
54
51
|
);
|
|
55
52
|
const githubRepositoryParams: ConstructorParameters<
|
|
56
53
|
typeof BaseGitHubRepository
|
|
57
|
-
> = [
|
|
58
|
-
localStorageRepository,
|
|
59
|
-
`${cachePath}/github.com.cookies.json`,
|
|
60
|
-
input.credentials.bot.github.token,
|
|
61
|
-
input.credentials.bot.github.name,
|
|
62
|
-
input.credentials.bot.github.password,
|
|
63
|
-
input.credentials.bot.github.authenticatorKey,
|
|
64
|
-
];
|
|
54
|
+
> = [localStorageRepository, input.credentials.bot.github.token];
|
|
65
55
|
const projectRepository = {
|
|
66
56
|
...new GraphqlProjectRepository(...githubRepositoryParams),
|
|
67
57
|
};
|
|
@@ -23,6 +23,7 @@ const mockRun = jest.fn().mockImplementation((...args: Parameters<RunFn>) => {
|
|
|
23
23
|
issues: [],
|
|
24
24
|
cacheUsed: false,
|
|
25
25
|
targetDateTimes: [],
|
|
26
|
+
rotationOrder: null,
|
|
26
27
|
});
|
|
27
28
|
});
|
|
28
29
|
|
|
@@ -114,6 +115,9 @@ jest.mock('../../repositories/GitHubIssueCommentRepository', () => ({
|
|
|
114
115
|
jest.mock('./situationFileWriter', () => ({
|
|
115
116
|
writeSituationFile: jest.fn().mockResolvedValue(undefined),
|
|
116
117
|
}));
|
|
118
|
+
jest.mock('./rotationOrderFileWriter', () => ({
|
|
119
|
+
writeRotationOrderFile: jest.fn(),
|
|
120
|
+
}));
|
|
117
121
|
|
|
118
122
|
import { HandleScheduledEventUseCaseHandler } from './HandleScheduledEventUseCaseHandler';
|
|
119
123
|
import { writeSituationFile } from './situationFileWriter';
|
|
@@ -185,44 +189,17 @@ describe('HandleScheduledEventUseCaseHandler', () => {
|
|
|
185
189
|
mockFetchReturningReadme(null);
|
|
186
190
|
});
|
|
187
191
|
|
|
188
|
-
it('should pass bot
|
|
189
|
-
const configWithCredentials = {
|
|
190
|
-
...validConfig,
|
|
191
|
-
credentials: {
|
|
192
|
-
...validConfig.credentials,
|
|
193
|
-
bot: {
|
|
194
|
-
github: {
|
|
195
|
-
token: 'test-token',
|
|
196
|
-
name: 'bot-user',
|
|
197
|
-
password: 'bot-pass',
|
|
198
|
-
authenticatorKey: 'bot-auth-key',
|
|
199
|
-
},
|
|
200
|
-
},
|
|
201
|
-
},
|
|
202
|
-
};
|
|
203
|
-
jest
|
|
204
|
-
.mocked(fs.readFileSync)
|
|
205
|
-
.mockReturnValue(YAML.stringify(configWithCredentials));
|
|
206
|
-
|
|
192
|
+
it('should pass bot token to repository constructors', async () => {
|
|
207
193
|
const handler = new HandleScheduledEventUseCaseHandler();
|
|
208
194
|
await handler.handle('config.yml', false);
|
|
209
195
|
|
|
210
|
-
const expectedCookiePath = `./tmp/cache/${validConfig.projectName}/github.com.cookies.json`;
|
|
211
|
-
|
|
212
196
|
for (const MockedClass of [
|
|
213
197
|
MockedGraphqlProjectRepository,
|
|
214
198
|
MockedApiV3IssueRepository,
|
|
215
199
|
MockedRestIssueRepository,
|
|
216
200
|
MockedGraphqlProjectItemRepository,
|
|
217
201
|
]) {
|
|
218
|
-
expect(MockedClass).toHaveBeenCalledWith(
|
|
219
|
-
expect.anything(),
|
|
220
|
-
expectedCookiePath,
|
|
221
|
-
'test-token',
|
|
222
|
-
'bot-user',
|
|
223
|
-
'bot-pass',
|
|
224
|
-
'bot-auth-key',
|
|
225
|
-
);
|
|
202
|
+
expect(MockedClass).toHaveBeenCalledWith(expect.anything(), 'test-token');
|
|
226
203
|
}
|
|
227
204
|
|
|
228
205
|
expect(MockedApiV3CheerioRestIssueRepository).toHaveBeenCalledWith(
|
|
@@ -231,37 +208,10 @@ describe('HandleScheduledEventUseCaseHandler', () => {
|
|
|
231
208
|
expect.anything(),
|
|
232
209
|
expect.anything(),
|
|
233
210
|
expect.anything(),
|
|
234
|
-
expectedCookiePath,
|
|
235
211
|
'test-token',
|
|
236
|
-
'bot-user',
|
|
237
|
-
'bot-pass',
|
|
238
|
-
'bot-auth-key',
|
|
239
212
|
);
|
|
240
213
|
});
|
|
241
214
|
|
|
242
|
-
it('should pass undefined credentials when not provided in config', async () => {
|
|
243
|
-
const handler = new HandleScheduledEventUseCaseHandler();
|
|
244
|
-
await handler.handle('config.yml', false);
|
|
245
|
-
|
|
246
|
-
const expectedCookiePath = `./tmp/cache/${validConfig.projectName}/github.com.cookies.json`;
|
|
247
|
-
|
|
248
|
-
for (const MockedClass of [
|
|
249
|
-
MockedGraphqlProjectRepository,
|
|
250
|
-
MockedApiV3IssueRepository,
|
|
251
|
-
MockedRestIssueRepository,
|
|
252
|
-
MockedGraphqlProjectItemRepository,
|
|
253
|
-
]) {
|
|
254
|
-
expect(MockedClass).toHaveBeenCalledWith(
|
|
255
|
-
expect.anything(),
|
|
256
|
-
expectedCookiePath,
|
|
257
|
-
'test-token',
|
|
258
|
-
undefined,
|
|
259
|
-
undefined,
|
|
260
|
-
undefined,
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
|
|
265
215
|
it('should write situation file after successful run with resolved config values', async () => {
|
|
266
216
|
const configWithPreparation = {
|
|
267
217
|
...validConfig,
|
|
@@ -2,6 +2,7 @@ import YAML from 'yaml';
|
|
|
2
2
|
import TYPIA from 'typia';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import { writeSituationFile } from './situationFileWriter';
|
|
5
|
+
import { writeRotationOrderFile } from './rotationOrderFileWriter';
|
|
5
6
|
import {
|
|
6
7
|
fetchProjectReadme,
|
|
7
8
|
parseProjectReadmeConfig,
|
|
@@ -44,6 +45,7 @@ import { SetupTowerDefenceProjectUseCase } from '../../../domain/usecases/SetupT
|
|
|
44
45
|
import {
|
|
45
46
|
AWAITING_QUALITY_CHECK_STATUS_NAME,
|
|
46
47
|
AWAITING_WORKSPACE_STATUS_NAME,
|
|
48
|
+
FAILED_PREPARATION_STATUS_NAME,
|
|
47
49
|
PREPARATION_STATUS_NAME,
|
|
48
50
|
} from '../../../domain/entities/WorkflowStatus';
|
|
49
51
|
|
|
@@ -76,9 +78,6 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
76
78
|
bot: {
|
|
77
79
|
github: {
|
|
78
80
|
token: string;
|
|
79
|
-
name?: string;
|
|
80
|
-
password?: string;
|
|
81
|
-
authenticatorKey?: string;
|
|
82
81
|
};
|
|
83
82
|
};
|
|
84
83
|
};
|
|
@@ -151,14 +150,7 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
151
150
|
);
|
|
152
151
|
const githubRepositoryParams: ConstructorParameters<
|
|
153
152
|
typeof BaseGitHubRepository
|
|
154
|
-
> = [
|
|
155
|
-
localStorageRepository,
|
|
156
|
-
`${cachePath}/github.com.cookies.json`,
|
|
157
|
-
input.credentials.bot.github.token,
|
|
158
|
-
input.credentials.bot.github.name,
|
|
159
|
-
input.credentials.bot.github.password,
|
|
160
|
-
input.credentials.bot.github.authenticatorKey,
|
|
161
|
-
];
|
|
153
|
+
> = [localStorageRepository, input.credentials.bot.github.token];
|
|
162
154
|
const projectRepository = new GraphqlProjectRepository(
|
|
163
155
|
...githubRepositoryParams,
|
|
164
156
|
);
|
|
@@ -281,6 +273,9 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
281
273
|
|
|
282
274
|
const result = await handleScheduledEventUseCase.run(mergedInput);
|
|
283
275
|
if (result) {
|
|
276
|
+
if (result.rotationOrder !== null) {
|
|
277
|
+
writeRotationOrderFile(result.rotationOrder);
|
|
278
|
+
}
|
|
284
279
|
await writeSituationFile({
|
|
285
280
|
cachePath,
|
|
286
281
|
projectId: result.project.id,
|
|
@@ -289,6 +284,7 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
289
284
|
awaitingQualityCheckStatus: AWAITING_QUALITY_CHECK_STATUS_NAME,
|
|
290
285
|
preparationStatus: PREPARATION_STATUS_NAME,
|
|
291
286
|
awaitingWorkspaceStatus: AWAITING_WORKSPACE_STATUS_NAME,
|
|
287
|
+
failedPreparationStatus: FAILED_PREPARATION_STATUS_NAME,
|
|
292
288
|
},
|
|
293
289
|
config: {
|
|
294
290
|
maximumPreparingIssuesCount:
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { writeRotationOrderFile } from './rotationOrderFileWriter';
|
|
5
|
+
import type { RotationOrderEntry } from '../../../domain/usecases/StartPreparationUseCase';
|
|
6
|
+
|
|
7
|
+
jest.mock('fs');
|
|
8
|
+
|
|
9
|
+
const TOKEN_A = 'sk-ant-secret-token-a-value';
|
|
10
|
+
const TOKEN_B = 'sk-ant-secret-token-b-value';
|
|
11
|
+
|
|
12
|
+
describe('writeRotationOrderFile', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
jest.mocked(fs.mkdirSync).mockReturnValue(undefined);
|
|
16
|
+
jest.mocked(fs.writeFileSync).mockReturnValue(undefined);
|
|
17
|
+
jest.mocked(fs.renameSync).mockReturnValue(undefined);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('writes rotation order entries sorted selected-first to the stable path under XDG_CACHE_HOME', () => {
|
|
21
|
+
const originalXdg = process.env.XDG_CACHE_HOME;
|
|
22
|
+
process.env.XDG_CACHE_HOME = '/custom/cache';
|
|
23
|
+
|
|
24
|
+
const entries: RotationOrderEntry[] = [
|
|
25
|
+
{
|
|
26
|
+
name: 'personal-1',
|
|
27
|
+
fiveHourUtilization: 0.2,
|
|
28
|
+
blocked: false,
|
|
29
|
+
rejected: false,
|
|
30
|
+
thresholdExcluded: false,
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
writeRotationOrderFile(entries);
|
|
35
|
+
|
|
36
|
+
expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
|
|
37
|
+
'/custom/cache/tdpm/rotation-order.json.tmp',
|
|
38
|
+
expect.any(String),
|
|
39
|
+
);
|
|
40
|
+
expect(jest.mocked(fs.renameSync)).toHaveBeenCalledWith(
|
|
41
|
+
'/custom/cache/tdpm/rotation-order.json.tmp',
|
|
42
|
+
'/custom/cache/tdpm/rotation-order.json',
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
process.env.XDG_CACHE_HOME = originalXdg;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('falls back to ~/.cache/tdpm/rotation-order.json when XDG_CACHE_HOME is unset', () => {
|
|
49
|
+
const originalXdg = process.env.XDG_CACHE_HOME;
|
|
50
|
+
delete process.env.XDG_CACHE_HOME;
|
|
51
|
+
|
|
52
|
+
const home = os.homedir();
|
|
53
|
+
const expectedPath = path.join(
|
|
54
|
+
home,
|
|
55
|
+
'.cache',
|
|
56
|
+
'tdpm',
|
|
57
|
+
'rotation-order.json',
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
writeRotationOrderFile([]);
|
|
61
|
+
|
|
62
|
+
expect(jest.mocked(fs.renameSync)).toHaveBeenCalledWith(
|
|
63
|
+
`${expectedPath}.tmp`,
|
|
64
|
+
expectedPath,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
process.env.XDG_CACHE_HOME = originalXdg;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('includes name, fiveHourUtilization, blocked, rejected, and thresholdExcluded in output', () => {
|
|
71
|
+
process.env.XDG_CACHE_HOME = '/cache';
|
|
72
|
+
|
|
73
|
+
const entries: RotationOrderEntry[] = [
|
|
74
|
+
{
|
|
75
|
+
name: 'personal-1',
|
|
76
|
+
fiveHourUtilization: 0.3,
|
|
77
|
+
blocked: false,
|
|
78
|
+
rejected: false,
|
|
79
|
+
thresholdExcluded: false,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'personal-2',
|
|
83
|
+
fiveHourUtilization: 0.95,
|
|
84
|
+
blocked: false,
|
|
85
|
+
rejected: false,
|
|
86
|
+
thresholdExcluded: true,
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
writeRotationOrderFile(entries);
|
|
91
|
+
|
|
92
|
+
expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
|
|
93
|
+
expect.any(String),
|
|
94
|
+
expect.stringContaining('"name":"personal-1"'),
|
|
95
|
+
);
|
|
96
|
+
expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
|
|
97
|
+
expect.any(String),
|
|
98
|
+
expect.stringContaining('"name":"personal-2"'),
|
|
99
|
+
);
|
|
100
|
+
expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
|
|
101
|
+
expect.any(String),
|
|
102
|
+
expect.stringContaining('"fiveHourUtilization":0.3'),
|
|
103
|
+
);
|
|
104
|
+
expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
|
|
105
|
+
expect.any(String),
|
|
106
|
+
expect.stringContaining('"thresholdExcluded":true'),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
delete process.env.XDG_CACHE_HOME;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('does not write raw token values to the output file', () => {
|
|
113
|
+
process.env.XDG_CACHE_HOME = '/cache';
|
|
114
|
+
|
|
115
|
+
const entries: RotationOrderEntry[] = [
|
|
116
|
+
{
|
|
117
|
+
name: 'personal-1',
|
|
118
|
+
fiveHourUtilization: 0.1,
|
|
119
|
+
blocked: false,
|
|
120
|
+
rejected: false,
|
|
121
|
+
thresholdExcluded: false,
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
writeRotationOrderFile(entries);
|
|
126
|
+
|
|
127
|
+
expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
|
|
128
|
+
expect.any(String),
|
|
129
|
+
expect.not.stringContaining(TOKEN_A),
|
|
130
|
+
);
|
|
131
|
+
expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
|
|
132
|
+
expect.any(String),
|
|
133
|
+
expect.not.stringContaining(TOKEN_B),
|
|
134
|
+
);
|
|
135
|
+
expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
|
|
136
|
+
expect.any(String),
|
|
137
|
+
expect.not.stringContaining('sk-ant-'),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
delete process.env.XDG_CACHE_HOME;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('writes atomically: mkdirSync before writeFileSync before renameSync', () => {
|
|
144
|
+
process.env.XDG_CACHE_HOME = '/cache';
|
|
145
|
+
|
|
146
|
+
const callOrder: string[] = [];
|
|
147
|
+
jest.mocked(fs.mkdirSync).mockImplementation((): undefined => {
|
|
148
|
+
callOrder.push('mkdir');
|
|
149
|
+
return undefined;
|
|
150
|
+
});
|
|
151
|
+
jest.mocked(fs.writeFileSync).mockImplementation((): void => {
|
|
152
|
+
callOrder.push('write');
|
|
153
|
+
});
|
|
154
|
+
jest.mocked(fs.renameSync).mockImplementation((): void => {
|
|
155
|
+
callOrder.push('rename');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
writeRotationOrderFile([]);
|
|
159
|
+
|
|
160
|
+
expect(callOrder).toEqual(['mkdir', 'write', 'rename']);
|
|
161
|
+
|
|
162
|
+
delete process.env.XDG_CACHE_HOME;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('writes an empty array when no rotation entries are provided', () => {
|
|
166
|
+
process.env.XDG_CACHE_HOME = '/cache';
|
|
167
|
+
|
|
168
|
+
writeRotationOrderFile([]);
|
|
169
|
+
|
|
170
|
+
expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
|
|
171
|
+
expect.any(String),
|
|
172
|
+
'[]',
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
delete process.env.XDG_CACHE_HOME;
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import type { RotationOrderEntry } from '../../../domain/usecases/StartPreparationUseCase';
|
|
5
|
+
|
|
6
|
+
const rotationOrderFilePath = (): string => {
|
|
7
|
+
const base = process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), '.cache');
|
|
8
|
+
return path.join(base, 'tdpm', 'rotation-order.json');
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const writeRotationOrderFile = (
|
|
12
|
+
rotationOrder: RotationOrderEntry[],
|
|
13
|
+
): void => {
|
|
14
|
+
const filePath = rotationOrderFilePath();
|
|
15
|
+
const dir = path.dirname(filePath);
|
|
16
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
17
|
+
const tmpPath = `${filePath}.tmp`;
|
|
18
|
+
fs.writeFileSync(tmpPath, JSON.stringify(rotationOrder));
|
|
19
|
+
fs.renameSync(tmpPath, filePath);
|
|
20
|
+
};
|
|
@@ -50,6 +50,7 @@ const baseParams = {
|
|
|
50
50
|
awaitingQualityCheckStatus: 'Awaiting quality check',
|
|
51
51
|
preparationStatus: 'Preparation',
|
|
52
52
|
awaitingWorkspaceStatus: 'Awaiting workspace',
|
|
53
|
+
failedPreparationStatus: 'Failed Preparation',
|
|
53
54
|
},
|
|
54
55
|
config: {
|
|
55
56
|
maximumPreparingIssuesCount: 6,
|
|
@@ -183,6 +184,7 @@ describe('writeSituationFile', () => {
|
|
|
183
184
|
awaitingQualityCheckStatus: null,
|
|
184
185
|
preparationStatus: null,
|
|
185
186
|
awaitingWorkspaceStatus: null,
|
|
187
|
+
failedPreparationStatus: null,
|
|
186
188
|
},
|
|
187
189
|
issues,
|
|
188
190
|
};
|
|
@@ -207,6 +209,40 @@ describe('writeSituationFile', () => {
|
|
|
207
209
|
expect.any(String),
|
|
208
210
|
expect.stringContaining('"awaitingWorkspaceBlockedByDependency":0'),
|
|
209
211
|
);
|
|
212
|
+
expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
|
|
213
|
+
expect.any(String),
|
|
214
|
+
expect.stringContaining('"failedPreparation":0'),
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('counts failedPreparation correctly from fixture issues', async () => {
|
|
219
|
+
const issues = [
|
|
220
|
+
createIssue({ status: 'Failed Preparation' }),
|
|
221
|
+
createIssue({ status: 'Failed Preparation' }),
|
|
222
|
+
createIssue({ status: 'Preparation' }),
|
|
223
|
+
createIssue({ status: 'Awaiting workspace' }),
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
await writeSituationFile({ ...baseParams, issues });
|
|
227
|
+
|
|
228
|
+
expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
|
|
229
|
+
expect.any(String),
|
|
230
|
+
expect.stringContaining('"failedPreparation":2'),
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('sets failedPreparation to 0 when no issues match the failed preparation status', async () => {
|
|
235
|
+
const issues = [
|
|
236
|
+
createIssue({ status: 'Preparation' }),
|
|
237
|
+
createIssue({ status: 'Awaiting workspace' }),
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
await writeSituationFile({ ...baseParams, issues });
|
|
241
|
+
|
|
242
|
+
expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
|
|
243
|
+
expect.any(String),
|
|
244
|
+
expect.stringContaining('"failedPreparation":0'),
|
|
245
|
+
);
|
|
210
246
|
});
|
|
211
247
|
});
|
|
212
248
|
|
|
@@ -10,6 +10,7 @@ export type SituationFileParams = {
|
|
|
10
10
|
awaitingQualityCheckStatus: string | null;
|
|
11
11
|
preparationStatus: string | null;
|
|
12
12
|
awaitingWorkspaceStatus: string | null;
|
|
13
|
+
failedPreparationStatus: string | null;
|
|
13
14
|
};
|
|
14
15
|
config: {
|
|
15
16
|
maximumPreparingIssuesCount: number | null;
|
|
@@ -104,6 +105,12 @@ export const writeSituationFile = async (
|
|
|
104
105
|
? issues.filter((i) => i.status === statusNames.awaitingWorkspaceStatus)
|
|
105
106
|
: [];
|
|
106
107
|
|
|
108
|
+
const failedPreparation =
|
|
109
|
+
statusNames.failedPreparationStatus !== null
|
|
110
|
+
? issues.filter((i) => i.status === statusNames.failedPreparationStatus)
|
|
111
|
+
.length
|
|
112
|
+
: 0;
|
|
113
|
+
|
|
107
114
|
const awaitingWorkspaceImmediatelyActionable = awaitingWorkspaceIssues.filter(
|
|
108
115
|
isImmediatelyActionable,
|
|
109
116
|
).length;
|
|
@@ -142,6 +149,7 @@ export const writeSituationFile = async (
|
|
|
142
149
|
preparation: preparationIssues.length,
|
|
143
150
|
awaitingWorkspaceImmediatelyActionable,
|
|
144
151
|
awaitingWorkspaceBlockedByDependency,
|
|
152
|
+
failedPreparation,
|
|
145
153
|
},
|
|
146
154
|
processes: {
|
|
147
155
|
runningPreparation,
|
|
@@ -1,7 +1,56 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as os from 'os';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
-
import { loadTokens } from './TokenListLoader';
|
|
4
|
+
import { loadTokenEntries, loadTokens } from './TokenListLoader';
|
|
5
|
+
|
|
6
|
+
describe('loadTokenEntries', () => {
|
|
7
|
+
let tempDir: string;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'token-entries-loader-'));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should return entries with name and token', () => {
|
|
18
|
+
const filePath = path.join(tempDir, 'tokens.json');
|
|
19
|
+
fs.writeFileSync(
|
|
20
|
+
filePath,
|
|
21
|
+
JSON.stringify([
|
|
22
|
+
{ name: 'alice', token: 'token-a' },
|
|
23
|
+
{ name: 'bob', token: 'token-b' },
|
|
24
|
+
]),
|
|
25
|
+
);
|
|
26
|
+
expect(loadTokenEntries(filePath)).toEqual([
|
|
27
|
+
{ name: 'alice', token: 'token-a' },
|
|
28
|
+
{ name: 'bob', token: 'token-b' },
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should assign a unique positional name when name is absent from entry', () => {
|
|
33
|
+
const filePath = path.join(tempDir, 'tokens.json');
|
|
34
|
+
fs.writeFileSync(
|
|
35
|
+
filePath,
|
|
36
|
+
JSON.stringify([{ token: 'token-a' }, { token: 'token-b' }]),
|
|
37
|
+
);
|
|
38
|
+
expect(loadTokenEntries(filePath)).toEqual([
|
|
39
|
+
{ name: 'token-1', token: 'token-a' },
|
|
40
|
+
{ name: 'token-2', token: 'token-b' },
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should return null when file does not exist', () => {
|
|
45
|
+
expect(loadTokenEntries(path.join(tempDir, 'missing.json'))).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should return null when every entry is invalid', () => {
|
|
49
|
+
const filePath = path.join(tempDir, 'invalid.json');
|
|
50
|
+
fs.writeFileSync(filePath, JSON.stringify([{ name: 'no-token' }]));
|
|
51
|
+
expect(loadTokenEntries(filePath)).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
5
54
|
|
|
6
55
|
describe('TokenListLoader', () => {
|
|
7
56
|
let tempDir: string;
|