github-issue-tower-defence-management 1.60.1 → 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.
Files changed (123) hide show
  1. package/.github/workflows/publish.yml +13 -0
  2. package/.github/workflows/test.yml +0 -4
  3. package/CHANGELOG.md +14 -0
  4. package/README.md +53 -10
  5. package/bin/adapter/entry-points/cli/index.js +11 -11
  6. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  7. package/bin/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.js +3 -22
  8. package/bin/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.js.map +1 -1
  9. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +8 -22
  10. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  11. package/bin/adapter/entry-points/handlers/rotationOrderFileWriter.js +56 -0
  12. package/bin/adapter/entry-points/handlers/rotationOrderFileWriter.js.map +1 -0
  13. package/bin/adapter/entry-points/handlers/situationFileWriter.js +5 -0
  14. package/bin/adapter/entry-points/handlers/situationFileWriter.js.map +1 -1
  15. package/bin/adapter/proxy/TokenListLoader.js +21 -6
  16. package/bin/adapter/proxy/TokenListLoader.js.map +1 -1
  17. package/bin/adapter/proxy/proxyEntry.js +1 -0
  18. package/bin/adapter/proxy/proxyEntry.js.map +1 -1
  19. package/bin/adapter/repositories/BaseGitHubRepository.js +1 -113
  20. package/bin/adapter/repositories/BaseGitHubRepository.js.map +1 -1
  21. package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js +5 -3
  22. package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js.map +1 -1
  23. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +8 -7
  24. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  25. package/bin/domain/usecases/CreateNewStoryByLabelUseCase.js +19 -9
  26. package/bin/domain/usecases/CreateNewStoryByLabelUseCase.js.map +1 -1
  27. package/bin/domain/usecases/HandleScheduledEventUseCase.js +15 -3
  28. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  29. package/bin/domain/usecases/IssueRejectionEvaluator.js +8 -1
  30. package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
  31. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +5 -1
  32. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  33. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +1 -1
  34. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
  35. package/bin/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.js +32 -1
  36. package/bin/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.js.map +1 -1
  37. package/bin/domain/usecases/StartPreparationUseCase.js +91 -12
  38. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  39. package/package.json +1 -4
  40. package/src/adapter/entry-points/cli/index.test.ts +16 -16
  41. package/src/adapter/entry-points/cli/index.ts +8 -11
  42. package/src/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.test.ts +2 -55
  43. package/src/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.ts +1 -11
  44. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +6 -56
  45. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +7 -11
  46. package/src/adapter/entry-points/handlers/rotationOrderFileWriter.test.ts +177 -0
  47. package/src/adapter/entry-points/handlers/rotationOrderFileWriter.ts +20 -0
  48. package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +36 -0
  49. package/src/adapter/entry-points/handlers/situationFileWriter.ts +8 -0
  50. package/src/adapter/proxy/TokenListLoader.test.ts +50 -1
  51. package/src/adapter/proxy/TokenListLoader.ts +25 -5
  52. package/src/adapter/proxy/proxyEntry.test.ts +270 -1
  53. package/src/adapter/proxy/proxyEntry.ts +2 -1
  54. package/src/adapter/repositories/BaseGitHubRepository.test.ts +1 -186
  55. package/src/adapter/repositories/BaseGitHubRepository.ts +1 -139
  56. package/src/adapter/repositories/GraphqlProjectRepository.errorHandling.test.ts +0 -1
  57. package/src/adapter/repositories/GraphqlProjectRepository.fetchProjectId.test.ts +4 -1
  58. package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.test.ts +60 -19
  59. package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.ts +6 -4
  60. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +23 -13
  61. package/src/adapter/repositories/issue/ApiV3IssueRepository.test.ts +0 -1
  62. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +0 -8
  63. package/src/adapter/repositories/issue/RestIssueRepository.test.ts +0 -1
  64. package/src/domain/entities/ClaudeTokenUsage.ts +1 -0
  65. package/src/domain/usecases/CreateNewStoryByLabelUseCase.test.ts +196 -11
  66. package/src/domain/usecases/CreateNewStoryByLabelUseCase.ts +32 -15
  67. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +4 -0
  68. package/src/domain/usecases/HandleScheduledEventUseCase.ts +21 -5
  69. package/src/domain/usecases/IssueRejectionEvaluator.test.ts +153 -0
  70. package/src/domain/usecases/IssueRejectionEvaluator.ts +8 -0
  71. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +175 -31
  72. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +7 -1
  73. package/src/domain/usecases/RevertNotReadyAwaitingQualityCheckUseCase.test.ts +32 -0
  74. package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +39 -5
  75. package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +1 -1
  76. package/src/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.test.ts +139 -20
  77. package/src/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.ts +62 -2
  78. package/src/domain/usecases/StartPreparationUseCase.test.ts +404 -21
  79. package/src/domain/usecases/StartPreparationUseCase.ts +152 -16
  80. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +16 -0
  81. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  82. package/types/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.d.ts.map +1 -1
  83. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  84. package/types/adapter/entry-points/handlers/rotationOrderFileWriter.d.ts +3 -0
  85. package/types/adapter/entry-points/handlers/rotationOrderFileWriter.d.ts.map +1 -0
  86. package/types/adapter/entry-points/handlers/situationFileWriter.d.ts +1 -0
  87. package/types/adapter/entry-points/handlers/situationFileWriter.d.ts.map +1 -1
  88. package/types/adapter/proxy/TokenListLoader.d.ts +5 -0
  89. package/types/adapter/proxy/TokenListLoader.d.ts.map +1 -1
  90. package/types/adapter/proxy/proxyEntry.d.ts +2 -1
  91. package/types/adapter/proxy/proxyEntry.d.ts.map +1 -1
  92. package/types/adapter/repositories/BaseGitHubRepository.d.ts +1 -23
  93. package/types/adapter/repositories/BaseGitHubRepository.d.ts.map +1 -1
  94. package/types/adapter/repositories/ProxyClaudeTokenUsageRepository.d.ts.map +1 -1
  95. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +14 -5
  96. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  97. package/types/domain/entities/ClaudeTokenUsage.d.ts +1 -0
  98. package/types/domain/entities/ClaudeTokenUsage.d.ts.map +1 -1
  99. package/types/domain/usecases/CreateNewStoryByLabelUseCase.d.ts +4 -2
  100. package/types/domain/usecases/CreateNewStoryByLabelUseCase.d.ts.map +1 -1
  101. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +5 -2
  102. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  103. package/types/domain/usecases/IssueRejectionEvaluator.d.ts +1 -1
  104. package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
  105. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  106. package/types/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.d.ts +5 -2
  107. package/types/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.d.ts.map +1 -1
  108. package/types/domain/usecases/StartPreparationUseCase.d.ts +15 -1
  109. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
  110. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +14 -0
  111. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  112. package/bin/adapter/repositories/issue/CheerioIssueRepository.js +0 -136
  113. package/bin/adapter/repositories/issue/CheerioIssueRepository.js.map +0 -1
  114. package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js +0 -1606
  115. package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js.map +0 -1
  116. package/src/adapter/repositories/issue/CheerioIssueRepository.test.ts +0 -6552
  117. package/src/adapter/repositories/issue/CheerioIssueRepository.ts +0 -142
  118. package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.test.ts +0 -118
  119. package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.ts +0 -584
  120. package/types/adapter/repositories/issue/CheerioIssueRepository.d.ts +0 -40
  121. package/types/adapter/repositories/issue/CheerioIssueRepository.d.ts.map +0 -1
  122. package/types/adapter/repositories/issue/InternalGraphqlIssueRepository.d.ts +0 -220
  123. package/types/adapter/repositories/issue/InternalGraphqlIssueRepository.d.ts.map +0 -1
@@ -475,7 +475,7 @@ codexHomeCandidates:
475
475
 
476
476
  describe('startDaemon', () => {
477
477
  it('should read parameters from config file', async () => {
478
- const mockRun = jest.fn().mockResolvedValue(undefined);
478
+ const mockRun = jest.fn().mockResolvedValue({ rotationOrder: null });
479
479
  const MockedStartPreparationUseCase = jest.mocked(
480
480
  StartPreparationUseCase,
481
481
  );
@@ -511,7 +511,7 @@ codexHomeCandidates:
511
511
  });
512
512
 
513
513
  it('should allow CLI args to override config file values', async () => {
514
- const mockRun = jest.fn().mockResolvedValue(undefined);
514
+ const mockRun = jest.fn().mockResolvedValue({ rotationOrder: null });
515
515
  const MockedStartPreparationUseCase = jest.mocked(
516
516
  StartPreparationUseCase,
517
517
  );
@@ -558,7 +558,7 @@ codexHomeCandidates:
558
558
  };
559
559
  writeConfig(configWithLlm);
560
560
 
561
- const mockRun = jest.fn().mockResolvedValue(undefined);
561
+ const mockRun = jest.fn().mockResolvedValue({ rotationOrder: null });
562
562
  const MockedStartPreparationUseCase = jest.mocked(
563
563
  StartPreparationUseCase,
564
564
  );
@@ -593,7 +593,7 @@ codexHomeCandidates:
593
593
  };
594
594
  writeConfig(configWithCount);
595
595
 
596
- const mockRun = jest.fn().mockResolvedValue(undefined);
596
+ const mockRun = jest.fn().mockResolvedValue({ rotationOrder: null });
597
597
  const MockedStartPreparationUseCase = jest.mocked(
598
598
  StartPreparationUseCase,
599
599
  );
@@ -621,7 +621,7 @@ codexHomeCandidates:
621
621
  });
622
622
 
623
623
  it('should pass maximumPreparingIssuesCount from CLI overriding config', async () => {
624
- const mockRun = jest.fn().mockResolvedValue(undefined);
624
+ const mockRun = jest.fn().mockResolvedValue({ rotationOrder: null });
625
625
  const MockedStartPreparationUseCase = jest.mocked(
626
626
  StartPreparationUseCase,
627
627
  );
@@ -815,7 +815,7 @@ codexHomeCandidates:
815
815
  });
816
816
  const mockRun = jest.fn().mockImplementation(() => {
817
817
  callOrder.push('useCase.run');
818
- return Promise.resolve(undefined);
818
+ return Promise.resolve({ rotationOrder: null });
819
819
  });
820
820
  const MockedStartPreparationUseCase = jest.mocked(
821
821
  StartPreparationUseCase,
@@ -857,7 +857,7 @@ codexHomeCandidates:
857
857
  ].join('\n');
858
858
  mockFetchReturningReadme(readmeContent);
859
859
 
860
- const mockRun = jest.fn().mockResolvedValue(undefined);
860
+ const mockRun = jest.fn().mockResolvedValue({ rotationOrder: null });
861
861
  const MockedStartPreparationUseCase = jest.mocked(
862
862
  StartPreparationUseCase,
863
863
  );
@@ -891,7 +891,7 @@ codexHomeCandidates:
891
891
  };
892
892
  writeConfig(configWithCandidates);
893
893
 
894
- const mockRun = jest.fn().mockResolvedValue(undefined);
894
+ const mockRun = jest.fn().mockResolvedValue({ rotationOrder: null });
895
895
  const MockedStartPreparationUseCase = jest.mocked(
896
896
  StartPreparationUseCase,
897
897
  );
@@ -919,7 +919,7 @@ codexHomeCandidates:
919
919
  });
920
920
 
921
921
  it('should pass codexHomeCandidates as null when not in config', async () => {
922
- const mockRun = jest.fn().mockResolvedValue(undefined);
922
+ const mockRun = jest.fn().mockResolvedValue({ rotationOrder: null });
923
923
  const MockedStartPreparationUseCase = jest.mocked(
924
924
  StartPreparationUseCase,
925
925
  );
@@ -958,7 +958,7 @@ codexHomeCandidates:
958
958
  ].join('\n');
959
959
  mockFetchReturningReadme(readmeContent);
960
960
 
961
- const mockRun = jest.fn().mockResolvedValue(undefined);
961
+ const mockRun = jest.fn().mockResolvedValue({ rotationOrder: null });
962
962
  const MockedStartPreparationUseCase = jest.mocked(
963
963
  StartPreparationUseCase,
964
964
  );
@@ -988,7 +988,7 @@ codexHomeCandidates:
988
988
 
989
989
  describe('notifyFinishedIssuePreparation', () => {
990
990
  it('should read parameters from config file', async () => {
991
- const mockRun = jest.fn().mockResolvedValue(undefined);
991
+ const mockRun = jest.fn().mockResolvedValue({ rotationOrder: null });
992
992
  const MockedNotifyFinishedUseCase = jest.mocked(
993
993
  NotifyFinishedIssuePreparationUseCase,
994
994
  );
@@ -1020,7 +1020,7 @@ codexHomeCandidates:
1020
1020
  });
1021
1021
 
1022
1022
  it('should allow CLI args to override config file values', async () => {
1023
- const mockRun = jest.fn().mockResolvedValue(undefined);
1023
+ const mockRun = jest.fn().mockResolvedValue({ rotationOrder: null });
1024
1024
  const MockedNotifyFinishedUseCase = jest.mocked(
1025
1025
  NotifyFinishedIssuePreparationUseCase,
1026
1026
  );
@@ -1059,7 +1059,7 @@ codexHomeCandidates:
1059
1059
  };
1060
1060
  writeConfig(configWithThreshold);
1061
1061
 
1062
- const mockRun = jest.fn().mockResolvedValue(undefined);
1062
+ const mockRun = jest.fn().mockResolvedValue({ rotationOrder: null });
1063
1063
  const MockedNotifyFinishedUseCase = jest.mocked(
1064
1064
  NotifyFinishedIssuePreparationUseCase,
1065
1065
  );
@@ -1089,7 +1089,7 @@ codexHomeCandidates:
1089
1089
  });
1090
1090
 
1091
1091
  it('should pass custom thresholdForAutoReject from CLI overriding config', async () => {
1092
- const mockRun = jest.fn().mockResolvedValue(undefined);
1092
+ const mockRun = jest.fn().mockResolvedValue({ rotationOrder: null });
1093
1093
  const MockedNotifyFinishedUseCase = jest.mocked(
1094
1094
  NotifyFinishedIssuePreparationUseCase,
1095
1095
  );
@@ -1221,7 +1221,7 @@ codexHomeCandidates:
1221
1221
  };
1222
1222
  writeConfig(configWithWebhook);
1223
1223
 
1224
- const mockRun = jest.fn().mockResolvedValue(undefined);
1224
+ const mockRun = jest.fn().mockResolvedValue({ rotationOrder: null });
1225
1225
  const MockedNotifyFinishedUseCase = jest.mocked(
1226
1226
  NotifyFinishedIssuePreparationUseCase,
1227
1227
  );
@@ -1261,7 +1261,7 @@ codexHomeCandidates:
1261
1261
  ].join('\n');
1262
1262
  mockFetchReturningReadme(readmeContent);
1263
1263
 
1264
- const mockRun = jest.fn().mockResolvedValue(undefined);
1264
+ const mockRun = jest.fn().mockResolvedValue({ rotationOrder: null });
1265
1265
  const MockedNotifyFinishedUseCase = jest.mocked(
1266
1266
  NotifyFinishedIssuePreparationUseCase,
1267
1267
  );
@@ -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 percentage threshold (default: 90)',
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 credentials to repository constructors when provided', async () => {
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 credentials to repository constructors when provided', async () => {
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
+ };