github-issue-tower-defence-management 1.67.3 → 1.67.5

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 (29) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +1 -1
  3. package/bin/adapter/entry-points/cli/index.js +2 -2
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/cli/projectConfig.js +2 -24
  6. package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
  7. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +1 -16
  8. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  9. package/bin/adapter/proxy/RateLimitCache.js +9 -6
  10. package/bin/adapter/proxy/RateLimitCache.js.map +1 -1
  11. package/bin/domain/usecases/HandleScheduledEventUseCase.js +33 -10
  12. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  13. package/package.json +1 -1
  14. package/src/adapter/entry-points/cli/index.test.ts +0 -117
  15. package/src/adapter/entry-points/cli/index.ts +2 -2
  16. package/src/adapter/entry-points/cli/projectConfig.ts +1 -31
  17. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +0 -94
  18. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +1 -47
  19. package/src/adapter/proxy/RateLimitCache.test.ts +123 -0
  20. package/src/adapter/proxy/RateLimitCache.ts +9 -6
  21. package/src/adapter/repositories/issue/RestIssueRepository.test.ts +77 -195
  22. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +65 -0
  23. package/src/domain/usecases/HandleScheduledEventUseCase.ts +120 -24
  24. package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -2
  25. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  26. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  27. package/types/adapter/proxy/RateLimitCache.d.ts.map +1 -1
  28. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +3 -2
  29. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
@@ -109,27 +109,7 @@ export const loadConfigFile = (configFilePath: string): ConfigFile => {
109
109
  }
110
110
  };
111
111
 
112
- export const knownProjectReadmeConfigKeys: readonly string[] = [
113
- 'defaultAgentName',
114
- 'defaultLlmModelName',
115
- 'defaultLlmAgentName',
116
- 'maximumPreparingIssuesCount',
117
- 'allowIssueCacheMinutes',
118
- 'utilizationPercentageThreshold',
119
- 'allowedIssueAuthors',
120
- 'thresholdForAutoReject',
121
- 'workflowBlockerResolvedWebhookUrl',
122
- 'preparationProcessCheckCommand',
123
- 'codexHomeCandidates',
124
- 'claudeCodeOauthTokenListJsonPath',
125
- 'awLogDirectoryPath',
126
- 'awLogStaleThresholdMinutes',
127
- ];
128
-
129
- export const parseProjectReadmeConfig = (
130
- readme: string,
131
- projectUrl?: string,
132
- ): ConfigFile => {
112
+ export const parseProjectReadmeConfig = (readme: string): ConfigFile => {
133
113
  const detailsRegex =
134
114
  /<details>\s*<summary>config<\/summary>([\s\S]*?)<\/details>/i;
135
115
  const match = detailsRegex.exec(readme);
@@ -145,16 +125,6 @@ export const parseProjectReadmeConfig = (
145
125
  if (!isRecord(parsed)) {
146
126
  return {};
147
127
  }
148
- const knownKeySet = new Set<string>(knownProjectReadmeConfigKeys);
149
- const unknownKeys = Object.keys(parsed).filter(
150
- (key) => !knownKeySet.has(key),
151
- );
152
- const projectUrlSuffix = projectUrl ? ` (project: ${projectUrl})` : '';
153
- for (const unknownKey of unknownKeys) {
154
- console.warn(
155
- `Unknown key "${unknownKey}" in project README config section${projectUrlSuffix}`,
156
- );
157
- }
158
128
  return {
159
129
  defaultAgentName: getStringValue(parsed, 'defaultAgentName'),
160
130
  defaultLlmModelName: getStringValue(parsed, 'defaultLlmModelName'),
@@ -457,98 +457,4 @@ claudeCodeOauthTokenListJsonPath: /readme/tokens.json
457
457
  );
458
458
  });
459
459
  });
460
-
461
- describe('effective config logging', () => {
462
- let consoleLogSpy: jest.SpyInstance;
463
-
464
- beforeEach(() => {
465
- consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
466
- });
467
-
468
- afterEach(() => {
469
- consoleLogSpy.mockRestore();
470
- });
471
-
472
- it('should log the effective values with configFile source when only the YAML config sets them', async () => {
473
- const configWithPreparation = {
474
- ...validConfig,
475
- startPreparation: {
476
- defaultAgentName: 'yaml-agent',
477
- defaultLlmModelName: 'yaml-model',
478
- configFilePath: './config.yml',
479
- maximumPreparingIssuesCount: 10,
480
- },
481
- };
482
- mockFetchReturningReadme(null);
483
- jest
484
- .mocked(fs.readFileSync)
485
- .mockReturnValue(YAML.stringify(configWithPreparation));
486
-
487
- const handler = new HandleScheduledEventUseCaseHandler();
488
- await handler.handle('config.yml', false);
489
-
490
- expect(consoleLogSpy).toHaveBeenCalledWith(
491
- 'Effective maximumPreparingIssuesCount: 10 (source: configFile)',
492
- );
493
- expect(consoleLogSpy).toHaveBeenCalledWith(
494
- 'Effective defaultLlmModelName: yaml-model (source: configFile)',
495
- );
496
- expect(consoleLogSpy).toHaveBeenCalledWith(
497
- 'Effective defaultAgentName: yaml-agent (source: configFile)',
498
- );
499
- });
500
-
501
- it('should log the effective values with readmeOverride source when the README config overrides them', async () => {
502
- const readmeContent = `<details>
503
- <summary>config</summary>
504
- maximumPreparingIssuesCount: 3
505
- defaultLlmModelName: readme-model
506
- defaultAgentName: readme-agent
507
- </details>`;
508
- mockFetchReturningReadme(readmeContent);
509
- const configWithPreparation = {
510
- ...validConfig,
511
- startPreparation: {
512
- defaultAgentName: 'yaml-agent',
513
- defaultLlmModelName: 'yaml-model',
514
- configFilePath: './config.yml',
515
- maximumPreparingIssuesCount: 10,
516
- },
517
- };
518
- jest
519
- .mocked(fs.readFileSync)
520
- .mockReturnValue(YAML.stringify(configWithPreparation));
521
-
522
- const handler = new HandleScheduledEventUseCaseHandler();
523
- await handler.handle('config.yml', false);
524
-
525
- expect(consoleLogSpy).toHaveBeenCalledWith(
526
- 'Effective maximumPreparingIssuesCount: 3 (source: readmeOverride)',
527
- );
528
- expect(consoleLogSpy).toHaveBeenCalledWith(
529
- 'Effective defaultLlmModelName: readme-model (source: readmeOverride)',
530
- );
531
- expect(consoleLogSpy).toHaveBeenCalledWith(
532
- 'Effective defaultAgentName: readme-agent (source: readmeOverride)',
533
- );
534
- });
535
-
536
- it('should log null with unset (default) source when neither README nor config provides the value', async () => {
537
- mockFetchReturningReadme(null);
538
- jest.mocked(fs.readFileSync).mockReturnValue(YAML.stringify(validConfig));
539
-
540
- const handler = new HandleScheduledEventUseCaseHandler();
541
- await handler.handle('config.yml', false);
542
-
543
- expect(consoleLogSpy).toHaveBeenCalledWith(
544
- 'Effective maximumPreparingIssuesCount: null (source: unset (default))',
545
- );
546
- expect(consoleLogSpy).toHaveBeenCalledWith(
547
- 'Effective defaultLlmModelName: null (source: unset (default))',
548
- );
549
- expect(consoleLogSpy).toHaveBeenCalledWith(
550
- 'Effective defaultAgentName: null (source: unset (default))',
551
- );
552
- });
553
- });
554
460
  });
@@ -94,9 +94,7 @@ export class HandleScheduledEventUseCaseHandler {
94
94
 
95
95
  const managerToken = input.credentials.manager.github.token;
96
96
  const readme = await fetchProjectReadme(input.projectUrl, managerToken);
97
- const readmeConfig = readme
98
- ? parseProjectReadmeConfig(readme, input.projectUrl)
99
- : {};
97
+ const readmeConfig = readme ? parseProjectReadmeConfig(readme) : {};
100
98
 
101
99
  const mergedInput = {
102
100
  ...input,
@@ -139,50 +137,6 @@ export class HandleScheduledEventUseCaseHandler {
139
137
  : input.startPreparation,
140
138
  };
141
139
 
142
- type EffectiveConfigValue = string | number | null | undefined;
143
-
144
- const resolveConfigSource = (
145
- readmeValue: EffectiveConfigValue,
146
- configFileValue: EffectiveConfigValue,
147
- ): 'readmeOverride' | 'configFile' | 'unset (default)' => {
148
- if (readmeValue !== undefined && readmeValue !== null) {
149
- return 'readmeOverride';
150
- }
151
- if (configFileValue !== undefined && configFileValue !== null) {
152
- return 'configFile';
153
- }
154
- return 'unset (default)';
155
- };
156
-
157
- const formatEffectiveConfig = (
158
- value: EffectiveConfigValue,
159
- readmeValue: EffectiveConfigValue,
160
- configFileValue: EffectiveConfigValue,
161
- ): string =>
162
- `${value ?? 'null'} (source: ${resolveConfigSource(readmeValue, configFileValue)})`;
163
-
164
- console.log(
165
- `Effective maximumPreparingIssuesCount: ${formatEffectiveConfig(
166
- mergedInput.startPreparation?.maximumPreparingIssuesCount,
167
- readmeConfig.maximumPreparingIssuesCount,
168
- input.startPreparation?.maximumPreparingIssuesCount,
169
- )}`,
170
- );
171
- console.log(
172
- `Effective defaultLlmModelName: ${formatEffectiveConfig(
173
- mergedInput.startPreparation?.defaultLlmModelName,
174
- readmeConfig.defaultLlmModelName,
175
- input.startPreparation?.defaultLlmModelName,
176
- )}`,
177
- );
178
- console.log(
179
- `Effective defaultAgentName: ${formatEffectiveConfig(
180
- mergedInput.startPreparation?.defaultAgentName,
181
- readmeConfig.defaultAgentName,
182
- input.startPreparation?.defaultAgentName,
183
- )}`,
184
- );
185
-
186
140
  const systemDateRepository = new SystemDateRepository();
187
141
  const localStorageRepository = new LocalStorageRepository();
188
142
  const googleSpreadsheetRepository = new GoogleSpreadsheetRepository(
@@ -280,6 +280,129 @@ describe('RateLimitCache', () => {
280
280
  });
281
281
  });
282
282
 
283
+ describe('writeRateLimit preserves previous values when response has no anthropic-ratelimit-* headers', () => {
284
+ it('should not write any file when no previous cache exists and headers contain no anthropic-ratelimit-*', () => {
285
+ const token = '429-no-headers-no-prior-token';
286
+ writeRateLimit(token, {
287
+ 'content-type': 'application/json',
288
+ });
289
+ expect(fs.existsSync(cachePathForToken(token))).toBe(false);
290
+ expect(readRateLimit(token)).toBeNull();
291
+ });
292
+
293
+ it('should preserve the previous cache when a later response carries no anthropic-ratelimit-* headers (429 without headers)', () => {
294
+ const token = '429-no-headers-with-prior-token';
295
+ writeRateLimit(token, {
296
+ 'anthropic-ratelimit-unified-status': 'allowed',
297
+ 'anthropic-ratelimit-unified-5h-status': 'allowed',
298
+ 'anthropic-ratelimit-unified-5h-reset': '1700000000',
299
+ 'anthropic-ratelimit-unified-5h-utilization': '42',
300
+ 'anthropic-ratelimit-unified-7d-status': 'allowed',
301
+ 'anthropic-ratelimit-unified-7d-reset': '1700100000',
302
+ 'anthropic-ratelimit-unified-7d-utilization': '17',
303
+ });
304
+ const beforeContent = fs.readFileSync(cachePathForToken(token), 'utf8');
305
+ writeRateLimit(token, {
306
+ 'content-type': 'application/json',
307
+ 'anthropic-organization-id': 'org-1',
308
+ });
309
+ const afterContent = fs.readFileSync(cachePathForToken(token), 'utf8');
310
+ expect(afterContent).toBe(beforeContent);
311
+ const snapshot = readRateLimit(token);
312
+ expect(snapshot?.fiveHourUtilization).toBe(42);
313
+ expect(snapshot?.fiveHourReset).toBe(1700000000);
314
+ expect(snapshot?.sevenDayUtilization).toBe(17);
315
+ expect(snapshot?.sevenDayReset).toBe(1700100000);
316
+ expect(snapshot?.unifiedRejected).toBe(false);
317
+ expect(snapshot?.fiveHourRejected).toBe(false);
318
+ expect(snapshot?.sevenDayRejected).toBe(false);
319
+ expect(snapshot?.blocked).toBe(false);
320
+ expect(snapshot?.rejected).toBe(false);
321
+ });
322
+
323
+ it('should overwrite the previous cache when the response carries anthropic-ratelimit-* headers regardless of status code (429 with headers)', () => {
324
+ const token = '429-with-headers-token';
325
+ writeRateLimit(token, {
326
+ 'anthropic-ratelimit-unified-status': 'allowed',
327
+ 'anthropic-ratelimit-unified-5h-status': 'allowed',
328
+ 'anthropic-ratelimit-unified-5h-reset': '1700000000',
329
+ 'anthropic-ratelimit-unified-5h-utilization': '42',
330
+ 'anthropic-ratelimit-unified-7d-status': 'allowed',
331
+ 'anthropic-ratelimit-unified-7d-reset': '1700100000',
332
+ 'anthropic-ratelimit-unified-7d-utilization': '17',
333
+ });
334
+ writeRateLimit(token, {
335
+ 'anthropic-ratelimit-unified-status': 'rejected',
336
+ 'anthropic-ratelimit-unified-5h-status': 'rejected',
337
+ 'anthropic-ratelimit-unified-5h-reset': '1700050000',
338
+ 'anthropic-ratelimit-unified-5h-utilization': '100',
339
+ 'anthropic-ratelimit-unified-7d-status': 'allowed',
340
+ 'anthropic-ratelimit-unified-7d-reset': '1700100000',
341
+ 'anthropic-ratelimit-unified-7d-utilization': '60',
342
+ });
343
+ const snapshot = readRateLimit(token);
344
+ expect(snapshot?.unifiedRejected).toBe(true);
345
+ expect(snapshot?.fiveHourRejected).toBe(true);
346
+ expect(snapshot?.fiveHourUtilization).toBe(100);
347
+ expect(snapshot?.fiveHourReset).toBe(1700050000);
348
+ expect(snapshot?.sevenDayUtilization).toBe(60);
349
+ });
350
+
351
+ it('should overwrite the previous cache when a 200 response carries anthropic-ratelimit-* headers (200 with headers)', () => {
352
+ const token = '200-with-headers-token';
353
+ writeRateLimit(token, {
354
+ 'anthropic-ratelimit-unified-status': 'rejected',
355
+ 'anthropic-ratelimit-unified-5h-status': 'rejected',
356
+ 'anthropic-ratelimit-unified-5h-reset': '1700000000',
357
+ 'anthropic-ratelimit-unified-5h-utilization': '100',
358
+ 'anthropic-ratelimit-unified-7d-status': 'allowed',
359
+ 'anthropic-ratelimit-unified-7d-reset': '1700100000',
360
+ 'anthropic-ratelimit-unified-7d-utilization': '60',
361
+ });
362
+ writeRateLimit(token, {
363
+ 'anthropic-ratelimit-unified-status': 'allowed',
364
+ 'anthropic-ratelimit-unified-5h-status': 'allowed',
365
+ 'anthropic-ratelimit-unified-5h-reset': '1700050000',
366
+ 'anthropic-ratelimit-unified-5h-utilization': '30',
367
+ 'anthropic-ratelimit-unified-7d-status': 'allowed',
368
+ 'anthropic-ratelimit-unified-7d-reset': '1700100000',
369
+ 'anthropic-ratelimit-unified-7d-utilization': '20',
370
+ });
371
+ const snapshot = readRateLimit(token);
372
+ expect(snapshot?.unifiedRejected).toBe(false);
373
+ expect(snapshot?.fiveHourRejected).toBe(false);
374
+ expect(snapshot?.fiveHourUtilization).toBe(30);
375
+ expect(snapshot?.fiveHourReset).toBe(1700050000);
376
+ expect(snapshot?.sevenDayUtilization).toBe(20);
377
+ expect(snapshot?.rejected).toBe(false);
378
+ });
379
+
380
+ it('should not modify any header value based on response status code', () => {
381
+ const token = 'no-status-based-mutation-token';
382
+ const inputHeaders: Record<string, string> = {
383
+ 'anthropic-ratelimit-unified-status': 'allowed',
384
+ 'anthropic-ratelimit-unified-5h-status': 'allowed',
385
+ 'anthropic-ratelimit-unified-5h-reset': '1700000000',
386
+ 'anthropic-ratelimit-unified-5h-utilization': '42',
387
+ 'anthropic-ratelimit-unified-7d-status': 'allowed',
388
+ 'anthropic-ratelimit-unified-7d-reset': '1700100000',
389
+ 'anthropic-ratelimit-unified-7d-utilization': '17',
390
+ };
391
+ writeRateLimit(token, inputHeaders);
392
+ const raw: unknown = JSON.parse(
393
+ fs.readFileSync(cachePathForToken(token), 'utf8'),
394
+ );
395
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
396
+ value !== null && typeof value === 'object' && !Array.isArray(value);
397
+ if (!isRecord(raw) || !isRecord(raw.headers)) {
398
+ throw new Error('expected stored cache to contain headers object');
399
+ }
400
+ for (const [key, expectedValue] of Object.entries(inputHeaders)) {
401
+ expect(raw.headers[key]).toBe(expectedValue);
402
+ }
403
+ });
404
+ });
405
+
283
406
  describe('parseModelRateLimitsFromBody', () => {
284
407
  it('should extract a rejected seven_day_sonnet limit from a rate_limit event body', () => {
285
408
  const body =
@@ -71,17 +71,11 @@ export const writeRateLimit = (
71
71
  token: string,
72
72
  headers: Record<string, string | string[] | undefined>,
73
73
  ): void => {
74
- const dir = cacheDir();
75
- if (!fs.existsSync(dir)) {
76
- fs.mkdirSync(dir, { recursive: true });
77
- }
78
74
  const pick = (key: string): string | undefined => {
79
75
  const value = headers[key];
80
76
  if (Array.isArray(value)) return value[0];
81
77
  return value;
82
78
  };
83
- const filePath = path.join(dir, `${hashToken(token)}.json`);
84
- const existing = readPayload(filePath);
85
79
  const rateLimitHeaders: Record<string, string> = {};
86
80
  for (const key of Object.keys(headers)) {
87
81
  if (key.startsWith('anthropic-ratelimit-')) {
@@ -91,6 +85,15 @@ export const writeRateLimit = (
91
85
  }
92
86
  }
93
87
  }
88
+ if (Object.keys(rateLimitHeaders).length === 0) {
89
+ return;
90
+ }
91
+ const dir = cacheDir();
92
+ if (!fs.existsSync(dir)) {
93
+ fs.mkdirSync(dir, { recursive: true });
94
+ }
95
+ const filePath = path.join(dir, `${hashToken(token)}.json`);
96
+ const existing = readPayload(filePath);
94
97
  const payload = {
95
98
  ts: Date.now() / 1000,
96
99
  headers: rateLimitHeaders,