github-issue-tower-defence-management 1.132.1 → 1.134.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +7 -7
  3. package/bin/adapter/entry-points/cli/index.js +4 -4
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.js +2 -1
  6. package/bin/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.js.map +1 -1
  7. package/bin/adapter/entry-points/handlers/OauthTokenSelectHandler.js +2 -1
  8. package/bin/adapter/entry-points/handlers/OauthTokenSelectHandler.js.map +1 -1
  9. package/bin/adapter/proxy/TokenListLoader.js +4 -5
  10. package/bin/adapter/proxy/TokenListLoader.js.map +1 -1
  11. package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -0
  12. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  13. package/bin/domain/usecases/LiveSessionOauthTokenSelectUseCase.js +5 -4
  14. package/bin/domain/usecases/LiveSessionOauthTokenSelectUseCase.js.map +1 -1
  15. package/bin/domain/usecases/OauthTokenSelectUseCase.js +31 -4
  16. package/bin/domain/usecases/OauthTokenSelectUseCase.js.map +1 -1
  17. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +6 -0
  18. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
  19. package/package.json +1 -1
  20. package/src/adapter/entry-points/cli/index.ts +4 -4
  21. package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.ts +28 -24
  22. package/src/adapter/entry-points/handlers/OauthTokenSelectHandler.ts +28 -24
  23. package/src/adapter/proxy/TokenListLoader.test.ts +52 -0
  24. package/src/adapter/proxy/TokenListLoader.ts +25 -5
  25. package/src/domain/usecases/HandleScheduledEventUseCase.ts +1 -0
  26. package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.test.ts +74 -0
  27. package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.ts +13 -2
  28. package/src/domain/usecases/OauthTokenSelectUseCase.test.ts +162 -0
  29. package/src/domain/usecases/OauthTokenSelectUseCase.ts +47 -2
  30. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +157 -1
  31. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +7 -0
  32. package/types/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.d.ts.map +1 -1
  33. package/types/adapter/entry-points/handlers/OauthTokenSelectHandler.d.ts.map +1 -1
  34. package/types/adapter/proxy/TokenListLoader.d.ts +1 -0
  35. package/types/adapter/proxy/TokenListLoader.d.ts.map +1 -1
  36. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  37. package/types/domain/usecases/LiveSessionOauthTokenSelectUseCase.d.ts +2 -2
  38. package/types/domain/usecases/LiveSessionOauthTokenSelectUseCase.d.ts.map +1 -1
  39. package/types/domain/usecases/OauthTokenSelectUseCase.d.ts +6 -1
  40. package/types/domain/usecases/OauthTokenSelectUseCase.d.ts.map +1 -1
  41. package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts +1 -0
  42. package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"RevertNotReadyReviewQueueIssueUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"RevertNotReadyReviewQueueIssueUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "1.132.1",
3
+ "version": "1.134.0",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -836,11 +836,11 @@ addServeWebOptions(program.command('serveConsole'))
836
836
  program
837
837
  .command('selectOauthToken')
838
838
  .description(
839
- 'Print exactly one Claude Code OAuth token chosen by a rate-limit-aware filter. The token string is written to stdout (pipeable); the per-candidate decision trace is written to stderr. Exits non-zero when no token passes the filter.',
839
+ 'Print exactly one Claude Code OAuth token chosen by a rate-limit-aware filter. When tokens carry differing per-token selectionWeight values, the choice among rate-limit-eligible tokens is weighted-random by that weight (default 1), so a lower-weight token is chosen proportionally less often; uniform weights preserve the deterministic soonest-7d-reset choice. The token string is written to stdout (pipeable); the per-candidate decision trace is written to stderr. Exits non-zero when no token passes the filter.',
840
840
  )
841
841
  .option(
842
842
  '--tokenListJsonPath <path>',
843
- 'Path to the JSON array of { name, token } records. Falls back to the CLAUDE_CODE_OAUTH_TOKEN_LIST_JSON_PATH environment variable.',
843
+ 'Path to the JSON array of { name, token, selectionWeight? } records. selectionWeight is an optional positive number (default 1) that biases how often this token is chosen among rate-limit-eligible candidates; a smaller weight is chosen proportionally less often and never bypasses eligibility filtering or starves a sole eligible token. Falls back to the CLAUDE_CODE_OAUTH_TOKEN_LIST_JSON_PATH environment variable.',
844
844
  )
845
845
  .option(
846
846
  '--cacheDir <path>',
@@ -868,11 +868,11 @@ program
868
868
  program
869
869
  .command('selectLiveSessionOauthToken')
870
870
  .description(
871
- 'Print exactly one Claude Code OAuth token chosen for a new live interactive session. Among rate-limit-eligible tokens it prefers the one with the fewest current live sessions (by distinct CLAUDE_CODE_SESSION_ID found in running Claude Code processes), tiebreaking on the soonest 7d reset. The token string is written to stdout (pipeable); the per-candidate decision trace is written to stderr. Exits non-zero when no token passes the filter.',
871
+ 'Print exactly one Claude Code OAuth token chosen for a new live interactive session. Among rate-limit-eligible tokens it prefers the one with the fewest current live sessions (by distinct CLAUDE_CODE_SESSION_ID found in running Claude Code processes), tiebreaking on the soonest 7d reset. When tokens carry differing per-token selectionWeight values, the choice among eligible tokens is weighted-random by that weight (default 1), so a lower-weight token is chosen proportionally less often; uniform weights preserve the fewest-live-sessions-then-soonest-7d-reset choice. The token string is written to stdout (pipeable); the per-candidate decision trace is written to stderr. Exits non-zero when no token passes the filter.',
872
872
  )
873
873
  .option(
874
874
  '--tokenListJsonPath <path>',
875
- 'Path to the JSON array of { name, token } records. Falls back to the CLAUDE_CODE_OAUTH_TOKEN_LIST_JSON_PATH environment variable.',
875
+ 'Path to the JSON array of { name, token, selectionWeight? } records. selectionWeight is an optional positive number (default 1) that biases how often this token is chosen among rate-limit-eligible candidates; a smaller weight is chosen proportionally less often and never bypasses eligibility filtering or starves a sole eligible token. Falls back to the CLAUDE_CODE_OAUTH_TOKEN_LIST_JSON_PATH environment variable.',
876
876
  )
877
877
  .option(
878
878
  '--cacheDir <path>',
@@ -4,6 +4,7 @@ import {
4
4
  LiveSessionOauthTokenSelectUseCase,
5
5
  } from '../../../domain/usecases/LiveSessionOauthTokenSelectUseCase';
6
6
  import {
7
+ DEFAULT_SELECTION_WEIGHT,
7
8
  FIVE_HOUR_MIN_FREE_RATIO,
8
9
  OauthTokenCandidate,
9
10
  SEVEN_DAY_MIN_FREE_RATIO,
@@ -61,30 +62,33 @@ export class LiveSessionOauthTokenSelectHandler {
61
62
 
62
63
  const cacheDirectory = resolveCacheDirectory(input.cacheDirectory);
63
64
 
64
- const candidates: OauthTokenCandidate[] = entries.map(({ name, token }) => {
65
- const snapshot = readRateLimit(token, cacheDirectory);
66
- const fableLimit = snapshot?.modelWeeklyLimits[FABLE_LIMIT_TYPE];
67
- const fableRejected =
68
- fableLimit !== undefined &&
69
- fableLimit.rejected &&
70
- input.nowEpochSeconds <= fableLimit.resetsAt;
71
- return {
72
- name,
73
- token,
74
- snapshot:
75
- snapshot === null
76
- ? null
77
- : {
78
- fiveHourUtilization: snapshot.fiveHourUtilization,
79
- fiveHourReset: snapshot.fiveHourReset,
80
- sevenDayUtilization: snapshot.sevenDayUtilization,
81
- sevenDayReset: snapshot.sevenDayReset,
82
- },
83
- subscriptionDisabled: snapshot?.subscriptionDisabled ?? false,
84
- unifiedRejected: snapshot?.unifiedRejected ?? false,
85
- fableRejected,
86
- };
87
- });
65
+ const candidates: OauthTokenCandidate[] = entries.map(
66
+ ({ name, token, selectionWeight }) => {
67
+ const snapshot = readRateLimit(token, cacheDirectory);
68
+ const fableLimit = snapshot?.modelWeeklyLimits[FABLE_LIMIT_TYPE];
69
+ const fableRejected =
70
+ fableLimit !== undefined &&
71
+ fableLimit.rejected &&
72
+ input.nowEpochSeconds <= fableLimit.resetsAt;
73
+ return {
74
+ name,
75
+ token,
76
+ snapshot:
77
+ snapshot === null
78
+ ? null
79
+ : {
80
+ fiveHourUtilization: snapshot.fiveHourUtilization,
81
+ fiveHourReset: snapshot.fiveHourReset,
82
+ sevenDayUtilization: snapshot.sevenDayUtilization,
83
+ sevenDayReset: snapshot.sevenDayReset,
84
+ },
85
+ subscriptionDisabled: snapshot?.subscriptionDisabled ?? false,
86
+ unifiedRejected: snapshot?.unifiedRejected ?? false,
87
+ fableRejected,
88
+ selectionWeight: selectionWeight ?? DEFAULT_SELECTION_WEIGHT,
89
+ };
90
+ },
91
+ );
88
92
 
89
93
  const liveSessions = this.liveSessionRepository.listLiveSessions();
90
94
 
@@ -1,4 +1,5 @@
1
1
  import {
2
+ DEFAULT_SELECTION_WEIGHT,
2
3
  OauthTokenCandidate,
3
4
  OauthTokenSelectResult,
4
5
  OauthTokenSelectUseCase,
@@ -83,30 +84,33 @@ export class OauthTokenSelectHandler {
83
84
 
84
85
  const cacheDirectory = resolveCacheDirectory(input.cacheDirectory);
85
86
 
86
- const candidates: OauthTokenCandidate[] = entries.map(({ name, token }) => {
87
- const snapshot = readRateLimit(token, cacheDirectory);
88
- const fableLimit = snapshot?.modelWeeklyLimits[FABLE_LIMIT_TYPE];
89
- const fableRejected =
90
- fableLimit !== undefined &&
91
- fableLimit.rejected &&
92
- input.nowEpochSeconds <= fableLimit.resetsAt;
93
- return {
94
- name,
95
- token,
96
- snapshot:
97
- snapshot === null
98
- ? null
99
- : {
100
- fiveHourUtilization: snapshot.fiveHourUtilization,
101
- fiveHourReset: snapshot.fiveHourReset,
102
- sevenDayUtilization: snapshot.sevenDayUtilization,
103
- sevenDayReset: snapshot.sevenDayReset,
104
- },
105
- subscriptionDisabled: snapshot?.subscriptionDisabled ?? false,
106
- unifiedRejected: snapshot?.unifiedRejected ?? false,
107
- fableRejected,
108
- };
109
- });
87
+ const candidates: OauthTokenCandidate[] = entries.map(
88
+ ({ name, token, selectionWeight }) => {
89
+ const snapshot = readRateLimit(token, cacheDirectory);
90
+ const fableLimit = snapshot?.modelWeeklyLimits[FABLE_LIMIT_TYPE];
91
+ const fableRejected =
92
+ fableLimit !== undefined &&
93
+ fableLimit.rejected &&
94
+ input.nowEpochSeconds <= fableLimit.resetsAt;
95
+ return {
96
+ name,
97
+ token,
98
+ snapshot:
99
+ snapshot === null
100
+ ? null
101
+ : {
102
+ fiveHourUtilization: snapshot.fiveHourUtilization,
103
+ fiveHourReset: snapshot.fiveHourReset,
104
+ sevenDayUtilization: snapshot.sevenDayUtilization,
105
+ sevenDayReset: snapshot.sevenDayReset,
106
+ },
107
+ subscriptionDisabled: snapshot?.subscriptionDisabled ?? false,
108
+ unifiedRejected: snapshot?.unifiedRejected ?? false,
109
+ fableRejected,
110
+ selectionWeight: selectionWeight ?? DEFAULT_SELECTION_WEIGHT,
111
+ };
112
+ },
113
+ );
110
114
 
111
115
  const result = this.useCase.run(candidates, input.nowEpochSeconds);
112
116
 
@@ -50,6 +50,58 @@ describe('loadTokenEntries', () => {
50
50
  fs.writeFileSync(filePath, JSON.stringify([{ name: 'no-token' }]));
51
51
  expect(loadTokenEntries(filePath)).toBeNull();
52
52
  });
53
+
54
+ it('should include selectionWeight when the entry has a finite number', () => {
55
+ const filePath = path.join(tempDir, 'tokens.json');
56
+ fs.writeFileSync(
57
+ filePath,
58
+ JSON.stringify([
59
+ { name: 'alice', token: 'token-a', selectionWeight: 0.8 },
60
+ { name: 'bob', token: 'token-b', selectionWeight: 2 },
61
+ ]),
62
+ );
63
+ expect(loadTokenEntries(filePath)).toEqual([
64
+ { name: 'alice', token: 'token-a', selectionWeight: 0.8 },
65
+ { name: 'bob', token: 'token-b', selectionWeight: 2 },
66
+ ]);
67
+ });
68
+
69
+ it('should omit selectionWeight when it is absent', () => {
70
+ const filePath = path.join(tempDir, 'tokens.json');
71
+ fs.writeFileSync(
72
+ filePath,
73
+ JSON.stringify([{ name: 'alice', token: 'token-a' }]),
74
+ );
75
+ expect(loadTokenEntries(filePath)).toEqual([
76
+ { name: 'alice', token: 'token-a' },
77
+ ]);
78
+ });
79
+
80
+ it('should ignore a non-numeric selectionWeight', () => {
81
+ const filePath = path.join(tempDir, 'tokens.json');
82
+ fs.writeFileSync(
83
+ filePath,
84
+ JSON.stringify([
85
+ { name: 'alice', token: 'token-a', selectionWeight: 'high' },
86
+ { name: 'bob', token: 'token-b', selectionWeight: null },
87
+ ]),
88
+ );
89
+ expect(loadTokenEntries(filePath)).toEqual([
90
+ { name: 'alice', token: 'token-a' },
91
+ { name: 'bob', token: 'token-b' },
92
+ ]);
93
+ });
94
+
95
+ it('should carry selectionWeight on a positionally named entry', () => {
96
+ const filePath = path.join(tempDir, 'tokens.json');
97
+ fs.writeFileSync(
98
+ filePath,
99
+ JSON.stringify([{ token: 'token-a', selectionWeight: 0.5 }]),
100
+ );
101
+ expect(loadTokenEntries(filePath)).toEqual([
102
+ { name: 'token-1', token: 'token-a', selectionWeight: 0.5 },
103
+ ]);
104
+ });
53
105
  });
54
106
 
55
107
  describe('TokenListLoader', () => {
@@ -18,8 +18,19 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
18
18
  export type TokenEntry = {
19
19
  name: string;
20
20
  token: string;
21
+ selectionWeight?: number;
21
22
  };
22
23
 
24
+ const parseSelectionWeight = (value: unknown): number | null =>
25
+ typeof value === 'number' && Number.isFinite(value) ? value : null;
26
+
27
+ const buildTokenEntry = (
28
+ name: string,
29
+ token: string,
30
+ selectionWeight: number | null,
31
+ ): TokenEntry =>
32
+ selectionWeight === null ? { name, token } : { name, token, selectionWeight };
33
+
23
34
  export const loadTokenEntries = (jsonPath: string): TokenEntry[] | null => {
24
35
  const resolved = expandHome(jsonPath);
25
36
  if (!fs.existsSync(resolved)) return null;
@@ -34,12 +45,21 @@ export const loadTokenEntries = (jsonPath: string): TokenEntry[] | null => {
34
45
  typeof entry.token === 'string' &&
35
46
  typeof entry.name === 'string'
36
47
  ) {
37
- entries.push({ name: entry.name, token: entry.token });
48
+ entries.push(
49
+ buildTokenEntry(
50
+ entry.name,
51
+ entry.token,
52
+ parseSelectionWeight(entry.selectionWeight),
53
+ ),
54
+ );
38
55
  } else if (isRecord(entry) && typeof entry.token === 'string') {
39
- entries.push({
40
- name: `token-${entries.length + 1}`,
41
- token: entry.token,
42
- });
56
+ entries.push(
57
+ buildTokenEntry(
58
+ `token-${entries.length + 1}`,
59
+ entry.token,
60
+ parseSelectionWeight(entry.selectionWeight),
61
+ ),
62
+ );
43
63
  }
44
64
  }
45
65
  return entries.length > 0 ? entries : null;
@@ -398,6 +398,7 @@ ${JSON.stringify(e)}
398
398
  });
399
399
  await this.revertNotReadyReviewQueueIssueUseCase.run({
400
400
  projectUrl: input.projectUrl,
401
+ manager: input.manager,
401
402
  labelsAsLlmAgentName,
402
403
  changeTargetPathAliases: input.changeTargetPathAliases,
403
404
  allowedIssueAuthors,
@@ -209,3 +209,77 @@ describe('LiveSessionOauthTokenSelectUseCase', () => {
209
209
  expect(rejected?.exclusionReason).toContain('rejected');
210
210
  });
211
211
  });
212
+
213
+ const sweepingRandom = (count: number): (() => number) => {
214
+ let index = 0;
215
+ return () => {
216
+ const value = (index + 0.5) / count;
217
+ index += 1;
218
+ return value;
219
+ };
220
+ };
221
+
222
+ const throwingRandom = (): number => {
223
+ throw new Error('random source must not be consulted');
224
+ };
225
+
226
+ const withSelectionWeight = (
227
+ base: OauthTokenCandidate,
228
+ selectionWeight: number,
229
+ ): OauthTokenCandidate => ({ ...base, selectionWeight });
230
+
231
+ describe('LiveSessionOauthTokenSelectUseCase selectionWeight', () => {
232
+ const useCase = new LiveSessionOauthTokenSelectUseCase();
233
+
234
+ it('keeps the deterministic fewest-live-sessions selection and never consults random when weights are uniform', () => {
235
+ const result = useCase.run(
236
+ [candidate('busy', snapshot({})), candidate('idle', snapshot({}))],
237
+ [session('busy', 'session-a')],
238
+ NOW,
239
+ throwingRandom,
240
+ );
241
+
242
+ expect(result.selected?.name).toBe('idle');
243
+ });
244
+
245
+ it('selects a sole eligible low-weight token without consulting random (no starvation)', () => {
246
+ const result = useCase.run(
247
+ [
248
+ withSelectionWeight(candidate('lowWeightOnly', snapshot({})), 0.01),
249
+ candidate('blocked', snapshot({ fiveHourUtilization: 0.9 })),
250
+ ],
251
+ [],
252
+ NOW,
253
+ throwingRandom,
254
+ );
255
+
256
+ expect(result.selected?.name).toBe('lowWeightOnly');
257
+ });
258
+
259
+ it('chooses a lower-weight token proportionally less often among equally-idle eligible candidates', () => {
260
+ const count = 1000;
261
+ const random = sweepingRandom(count);
262
+ const selectionCounts = new Map<string, number>();
263
+
264
+ for (let i = 0; i < count; i += 1) {
265
+ const result = useCase.run(
266
+ [
267
+ withSelectionWeight(candidate('heavy', snapshot({})), 1),
268
+ withSelectionWeight(candidate('light', snapshot({})), 0.5),
269
+ ],
270
+ [],
271
+ NOW,
272
+ random,
273
+ );
274
+ const name = result.selected?.name ?? 'none';
275
+ selectionCounts.set(name, (selectionCounts.get(name) ?? 0) + 1);
276
+ }
277
+
278
+ const heavy = selectionCounts.get('heavy') ?? 0;
279
+ const light = selectionCounts.get('light') ?? 0;
280
+ expect(heavy + light).toBe(count);
281
+ expect(light).toBeGreaterThan(0);
282
+ expect(light).toBeLessThan(heavy);
283
+ expect(Math.abs(light / count - 1 / 3)).toBeLessThan(0.02);
284
+ });
285
+ });
@@ -2,6 +2,8 @@ import { ClaudeLiveSession } from './adapter-interfaces/ClaudeLiveSessionReposit
2
2
  import {
3
3
  OauthTokenCandidate,
4
4
  OauthTokenSelectUseCase,
5
+ SelectionRandom,
6
+ selectWeightedCandidate,
5
7
  } from './OauthTokenSelectUseCase';
6
8
 
7
9
  export type LiveSessionOauthTokenCandidateMetrics = {
@@ -28,10 +30,12 @@ export class LiveSessionOauthTokenSelectUseCase {
28
30
  candidates: OauthTokenCandidate[],
29
31
  liveSessions: ClaudeLiveSession[],
30
32
  nowEpochSeconds: number,
33
+ random: SelectionRandom = Math.random,
31
34
  ): LiveSessionOauthTokenSelectResult => {
32
35
  const rateLimitResult = this.rateLimitSelectUseCase.run(
33
36
  candidates,
34
37
  nowEpochSeconds,
38
+ () => 0,
35
39
  );
36
40
  const liveSessionCountByToken = this.liveSessionCountByToken(liveSessions);
37
41
 
@@ -60,13 +64,20 @@ export class LiveSessionOauthTokenSelectUseCase {
60
64
  return { selected: null, metrics };
61
65
  }
62
66
 
63
- const best = eligible.reduce((bestEntry, currentEntry) =>
67
+ const deterministicBest = eligible.reduce((bestEntry, currentEntry) =>
64
68
  this.preferred(currentEntry.metric, bestEntry.metric)
65
69
  ? currentEntry
66
70
  : bestEntry,
67
71
  );
68
72
 
69
- return { selected: best.candidate, metrics };
73
+ const selected = selectWeightedCandidate(
74
+ eligible,
75
+ (entry) => entry.candidate,
76
+ deterministicBest,
77
+ random,
78
+ );
79
+
80
+ return { selected: selected.candidate, metrics };
70
81
  };
71
82
 
72
83
  private preferred = (
@@ -247,3 +247,165 @@ describe('OauthTokenSelectUseCase', () => {
247
247
  expect(alive?.eligible).toBe(true);
248
248
  });
249
249
  });
250
+
251
+ const sweepingRandom = (count: number): (() => number) => {
252
+ let index = 0;
253
+ return () => {
254
+ const value = (index + 0.5) / count;
255
+ index += 1;
256
+ return value;
257
+ };
258
+ };
259
+
260
+ const throwingRandom = (): number => {
261
+ throw new Error('random source must not be consulted');
262
+ };
263
+
264
+ const withSelectionWeight = (
265
+ base: OauthTokenCandidate,
266
+ selectionWeight: number,
267
+ ): OauthTokenCandidate => ({ ...base, selectionWeight });
268
+
269
+ describe('OauthTokenSelectUseCase selectionWeight', () => {
270
+ const useCase = new OauthTokenSelectUseCase();
271
+
272
+ it('keeps the deterministic soonest-7d selection and never consults random when weights are uniform', () => {
273
+ const result = useCase.run(
274
+ [
275
+ candidate(
276
+ 'far',
277
+ snapshot({ sevenDayUtilization: 0.1, sevenDayReset: NOW + 6 * DAY }),
278
+ ),
279
+ candidate(
280
+ 'soon',
281
+ snapshot({ sevenDayUtilization: 0.1, sevenDayReset: NOW + 2 * DAY }),
282
+ ),
283
+ ],
284
+ NOW,
285
+ throwingRandom,
286
+ );
287
+
288
+ expect(result.selected?.name).toBe('soon');
289
+ });
290
+
291
+ it('treats an absent selectionWeight the same as weight 1 (uniform, deterministic)', () => {
292
+ const result = useCase.run(
293
+ [
294
+ withSelectionWeight(
295
+ candidate('far', snapshot({ sevenDayReset: NOW + 6 * DAY })),
296
+ 1,
297
+ ),
298
+ candidate('soon', snapshot({ sevenDayReset: NOW + 2 * DAY })),
299
+ ],
300
+ NOW,
301
+ throwingRandom,
302
+ );
303
+
304
+ expect(result.selected?.name).toBe('soon');
305
+ });
306
+
307
+ it('selects a sole eligible low-weight token without consulting random (no starvation)', () => {
308
+ const result = useCase.run(
309
+ [
310
+ withSelectionWeight(candidate('lowWeightOnly', snapshot({})), 0.01),
311
+ candidate('blocked', snapshot({ fiveHourUtilization: 0.9 })),
312
+ ],
313
+ NOW,
314
+ throwingRandom,
315
+ );
316
+
317
+ expect(result.selected?.name).toBe('lowWeightOnly');
318
+ });
319
+
320
+ it('chooses a lower-weight token proportionally less often among eligible candidates', () => {
321
+ const count = 1000;
322
+ const random = sweepingRandom(count);
323
+ const selectionCounts = new Map<string, number>();
324
+
325
+ for (let i = 0; i < count; i += 1) {
326
+ const result = useCase.run(
327
+ [
328
+ withSelectionWeight(candidate('heavy', snapshot({})), 1),
329
+ withSelectionWeight(candidate('light', snapshot({})), 0.5),
330
+ ],
331
+ NOW,
332
+ random,
333
+ );
334
+ const name = result.selected?.name ?? 'none';
335
+ selectionCounts.set(name, (selectionCounts.get(name) ?? 0) + 1);
336
+ }
337
+
338
+ const heavy = selectionCounts.get('heavy') ?? 0;
339
+ const light = selectionCounts.get('light') ?? 0;
340
+ expect(heavy + light).toBe(count);
341
+ expect(light).toBeGreaterThan(0);
342
+ expect(light).toBeLessThan(heavy);
343
+ expect(Math.abs(light / count - 1 / 3)).toBeLessThan(0.02);
344
+ expect(Math.abs(heavy / count - 2 / 3)).toBeLessThan(0.02);
345
+ });
346
+
347
+ it('chooses a higher-weight token more often among eligible candidates', () => {
348
+ const count = 1000;
349
+ const random = sweepingRandom(count);
350
+ const selectionCounts = new Map<string, number>();
351
+
352
+ for (let i = 0; i < count; i += 1) {
353
+ const result = useCase.run(
354
+ [
355
+ withSelectionWeight(candidate('double', snapshot({})), 2),
356
+ withSelectionWeight(candidate('single', snapshot({})), 1),
357
+ ],
358
+ NOW,
359
+ random,
360
+ );
361
+ const name = result.selected?.name ?? 'none';
362
+ selectionCounts.set(name, (selectionCounts.get(name) ?? 0) + 1);
363
+ }
364
+
365
+ const double = selectionCounts.get('double') ?? 0;
366
+ const single = selectionCounts.get('single') ?? 0;
367
+ expect(double).toBeGreaterThan(single);
368
+ expect(single).toBeGreaterThan(0);
369
+ });
370
+
371
+ it('gives a zero-weight token no chance while other eligible tokens exist', () => {
372
+ const count = 100;
373
+ const random = sweepingRandom(count);
374
+ const selectionCounts = new Map<string, number>();
375
+
376
+ for (let i = 0; i < count; i += 1) {
377
+ const result = useCase.run(
378
+ [
379
+ withSelectionWeight(candidate('zero', snapshot({})), 0),
380
+ withSelectionWeight(candidate('positive', snapshot({})), 1),
381
+ ],
382
+ NOW,
383
+ random,
384
+ );
385
+ const name = result.selected?.name ?? 'none';
386
+ selectionCounts.set(name, (selectionCounts.get(name) ?? 0) + 1);
387
+ }
388
+
389
+ expect(selectionCounts.get('zero') ?? 0).toBe(0);
390
+ expect(selectionCounts.get('positive') ?? 0).toBe(count);
391
+ });
392
+
393
+ it('falls back to the deterministic best when every eligible weight is zero', () => {
394
+ const result = useCase.run(
395
+ [
396
+ withSelectionWeight(
397
+ candidate('far', snapshot({ sevenDayReset: NOW + 6 * DAY })),
398
+ 0,
399
+ ),
400
+ withSelectionWeight(
401
+ candidate('soon', snapshot({ sevenDayReset: NOW + 2 * DAY })),
402
+ 0,
403
+ ),
404
+ ],
405
+ NOW,
406
+ throwingRandom,
407
+ );
408
+
409
+ expect(result.selected?.name).toBe('soon');
410
+ });
411
+ });