github-issue-tower-defence-management 1.60.2 → 1.63.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/.github/workflows/publish.yml +13 -0
- package/.github/workflows/test.yml +0 -4
- package/CHANGELOG.md +7 -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 +91 -12
- 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 +404 -21
- package/src/domain/usecases/StartPreparationUseCase.ts +152 -16
- 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 +15 -1
- 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
|
@@ -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;
|
|
@@ -15,21 +15,41 @@ const expandHome = (filePath: string): string => {
|
|
|
15
15
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
16
16
|
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
17
17
|
|
|
18
|
-
export
|
|
18
|
+
export type TokenEntry = {
|
|
19
|
+
name: string;
|
|
20
|
+
token: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const loadTokenEntries = (jsonPath: string): TokenEntry[] | null => {
|
|
19
24
|
const resolved = expandHome(jsonPath);
|
|
20
25
|
if (!fs.existsSync(resolved)) return null;
|
|
21
26
|
try {
|
|
22
27
|
const raw = fs.readFileSync(resolved, 'utf8');
|
|
23
28
|
const parsed: unknown = JSON.parse(raw);
|
|
24
29
|
if (!Array.isArray(parsed)) return null;
|
|
25
|
-
const
|
|
30
|
+
const entries: TokenEntry[] = [];
|
|
26
31
|
for (const entry of parsed) {
|
|
27
|
-
if (
|
|
28
|
-
|
|
32
|
+
if (
|
|
33
|
+
isRecord(entry) &&
|
|
34
|
+
typeof entry.token === 'string' &&
|
|
35
|
+
typeof entry.name === 'string'
|
|
36
|
+
) {
|
|
37
|
+
entries.push({ name: entry.name, token: entry.token });
|
|
38
|
+
} else if (isRecord(entry) && typeof entry.token === 'string') {
|
|
39
|
+
entries.push({
|
|
40
|
+
name: `token-${entries.length + 1}`,
|
|
41
|
+
token: entry.token,
|
|
42
|
+
});
|
|
29
43
|
}
|
|
30
44
|
}
|
|
31
|
-
return
|
|
45
|
+
return entries.length > 0 ? entries : null;
|
|
32
46
|
} catch {
|
|
33
47
|
return null;
|
|
34
48
|
}
|
|
35
49
|
};
|
|
50
|
+
|
|
51
|
+
export const loadTokens = (jsonPath: string): string[] | null => {
|
|
52
|
+
const entries = loadTokenEntries(jsonPath);
|
|
53
|
+
if (entries === null) return null;
|
|
54
|
+
return entries.map((e) => e.token);
|
|
55
|
+
};
|