github-issue-tower-defence-management 1.83.0 → 1.84.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/CHANGELOG.md +14 -0
- package/README.md +67 -4
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +19 -2
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/consoleListsWriter.js +43 -0
- package/bin/adapter/entry-points/handlers/consoleListsWriter.js.map +1 -0
- package/bin/adapter/proxy/RateLimitCache.js +27 -2
- package/bin/adapter/proxy/RateLimitCache.js.map +1 -1
- package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js +5 -1
- package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +6 -6
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js +101 -0
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +18 -0
- package/src/adapter/entry-points/handlers/consoleListsWriter.test.ts +167 -0
- package/src/adapter/entry-points/handlers/consoleListsWriter.ts +60 -0
- package/src/adapter/proxy/RateLimitCache.test.ts +95 -0
- package/src/adapter/proxy/RateLimitCache.ts +32 -1
- package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.test.ts +43 -0
- package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.ts +6 -1
- package/src/domain/entities/ClaudeTokenUsage.ts +1 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +343 -0
- package/src/domain/usecases/StartPreparationUseCase.ts +6 -6
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +372 -0
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.ts +206 -0
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/consoleListsWriter.d.ts +13 -0
- package/types/adapter/entry-points/handlers/consoleListsWriter.d.ts.map +1 -0
- package/types/adapter/proxy/RateLimitCache.d.ts +1 -0
- package/types/adapter/proxy/RateLimitCache.d.ts.map +1 -1
- package/types/adapter/repositories/ProxyClaudeTokenUsageRepository.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/console/GenerateConsoleListsUseCase.d.ts +63 -0
- package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts.map +1 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GenerateConsoleListsUseCase = void 0;
|
|
4
|
+
const UNKNOWN_STORY_SORT_INDEX = 999999;
|
|
5
|
+
class GenerateConsoleListsUseCase {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.run = (input) => {
|
|
8
|
+
const { project, issues, pjcode, assigneeLogin, generatedAt } = input;
|
|
9
|
+
const storyOptions = project.story ? project.story.stories : [];
|
|
10
|
+
const storyOrder = storyOptions.map((option) => option.name);
|
|
11
|
+
const statusOptions = project.status.statuses;
|
|
12
|
+
const actionableIssues = issues.filter((issue) => this.isActionable(issue, assigneeLogin));
|
|
13
|
+
const buildStatusTab = (selector, excludedStatusNames) => ({
|
|
14
|
+
pjcode,
|
|
15
|
+
generatedAt,
|
|
16
|
+
statusOptions: this.buildFieldOptions(statusOptions, excludedStatusNames),
|
|
17
|
+
storyOrder,
|
|
18
|
+
storyColors: this.buildStoryColorsObject(storyOptions),
|
|
19
|
+
items: this.sortByStoryOrder(actionableIssues
|
|
20
|
+
.filter(selector)
|
|
21
|
+
.map((issue) => this.projectItem(issue)), storyOrder),
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
prs: buildStatusTab((issue) => issue.status !== null &&
|
|
25
|
+
issue.status.toLowerCase() === 'awaiting quality check', ['awaiting quality check', 'done']),
|
|
26
|
+
unread: buildStatusTab((issue) => issue.status !== null && issue.status.toLowerCase() === 'unread', ['unread', 'done']),
|
|
27
|
+
'failed-preparation': buildStatusTab((issue) => issue.status === 'Failed Preparation', [
|
|
28
|
+
'failed preparation',
|
|
29
|
+
'done',
|
|
30
|
+
'preparation',
|
|
31
|
+
'icebox',
|
|
32
|
+
'unread',
|
|
33
|
+
'in tmux by human',
|
|
34
|
+
]),
|
|
35
|
+
triage: {
|
|
36
|
+
pjcode,
|
|
37
|
+
generatedAt,
|
|
38
|
+
storyOptions: this.buildFieldOptions(storyOptions, []),
|
|
39
|
+
storyOrder,
|
|
40
|
+
storyColors: this.buildStoryColorsString(storyOptions),
|
|
41
|
+
items: this.sortByStoryOrder(actionableIssues
|
|
42
|
+
.filter((issue) => issue.story !== null &&
|
|
43
|
+
issue.story.toLowerCase().includes('no story'))
|
|
44
|
+
.map((issue) => this.projectItem(issue)), storyOrder),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
this.isActionable = (issue, assigneeLogin) => issue.isClosed === false &&
|
|
49
|
+
issue.assignees.includes(assigneeLogin) &&
|
|
50
|
+
issue.dependedIssueUrls.length === 0 &&
|
|
51
|
+
issue.nextActionDate === null &&
|
|
52
|
+
issue.nextActionHour === null;
|
|
53
|
+
this.projectItem = (issue) => ({
|
|
54
|
+
number: issue.number,
|
|
55
|
+
title: issue.title,
|
|
56
|
+
url: issue.url,
|
|
57
|
+
repo: issue.nameWithOwner,
|
|
58
|
+
nameWithOwner: issue.nameWithOwner,
|
|
59
|
+
projectItemId: issue.itemId,
|
|
60
|
+
itemId: issue.itemId,
|
|
61
|
+
isPr: issue.isPr,
|
|
62
|
+
story: issue.story ?? '',
|
|
63
|
+
labels: issue.labels,
|
|
64
|
+
createdAt: issue.createdAt.toISOString(),
|
|
65
|
+
});
|
|
66
|
+
this.buildFieldOptions = (options, excludedLowerCaseNames) => options
|
|
67
|
+
.filter((option) => !excludedLowerCaseNames.includes(option.name.toLowerCase()))
|
|
68
|
+
.map((option) => ({
|
|
69
|
+
id: option.id,
|
|
70
|
+
name: option.name,
|
|
71
|
+
color: option.color,
|
|
72
|
+
}));
|
|
73
|
+
this.buildStoryColorsObject = (options) => {
|
|
74
|
+
const result = {};
|
|
75
|
+
for (const option of options) {
|
|
76
|
+
result[option.name] = { color: option.color };
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
};
|
|
80
|
+
this.buildStoryColorsString = (options) => {
|
|
81
|
+
const result = {};
|
|
82
|
+
for (const option of options) {
|
|
83
|
+
result[option.name] = option.color;
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
};
|
|
87
|
+
this.sortByStoryOrder = (items, storyOrder) => {
|
|
88
|
+
const indexByStory = new Map(storyOrder.map((name, index) => [name, index]));
|
|
89
|
+
return items
|
|
90
|
+
.map((item, position) => ({
|
|
91
|
+
item,
|
|
92
|
+
position,
|
|
93
|
+
sortKey: indexByStory.get(item.story) ?? UNKNOWN_STORY_SORT_INDEX,
|
|
94
|
+
}))
|
|
95
|
+
.sort((a, b) => a.sortKey - b.sortKey || a.position - b.position)
|
|
96
|
+
.map((entry) => entry.item);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
exports.GenerateConsoleListsUseCase = GenerateConsoleListsUseCase;
|
|
101
|
+
//# sourceMappingURL=GenerateConsoleListsUseCase.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GenerateConsoleListsUseCase.js","sourceRoot":"","sources":["../../../../src/domain/usecases/console/GenerateConsoleListsUseCase.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -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 { writeConsoleLists } from './consoleListsWriter';
|
|
5
6
|
import { writeRotationOrderFile } from './rotationOrderFileWriter';
|
|
6
7
|
import {
|
|
7
8
|
fetchProjectReadme,
|
|
@@ -66,6 +67,7 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
66
67
|
const input: unknown = YAML.parse(configFileContent);
|
|
67
68
|
type inputType = Parameters<HandleScheduledEventUseCase['run']>[0] & {
|
|
68
69
|
claudeCodeOauthTokenListJsonPath?: string;
|
|
70
|
+
consoleDataOutputDir?: string;
|
|
69
71
|
credentials: {
|
|
70
72
|
manager: {
|
|
71
73
|
github: {
|
|
@@ -365,6 +367,22 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
365
367
|
mergedInput.startPreparation?.preparationProcessCheckCommand ?? null,
|
|
366
368
|
localCommandRunner: nodeLocalCommandRunner,
|
|
367
369
|
});
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
writeConsoleLists({
|
|
373
|
+
consoleDataOutputDir: mergedInput.consoleDataOutputDir ?? null,
|
|
374
|
+
pjcode: input.projectName,
|
|
375
|
+
assigneeLogin: input.manager,
|
|
376
|
+
project: result.project,
|
|
377
|
+
issues: result.issues,
|
|
378
|
+
});
|
|
379
|
+
} catch (error) {
|
|
380
|
+
console.error(
|
|
381
|
+
`Failed to write console lists: ${
|
|
382
|
+
error instanceof Error ? error.message : String(error)
|
|
383
|
+
}`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
368
386
|
}
|
|
369
387
|
return result;
|
|
370
388
|
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { mock } from 'jest-mock-extended';
|
|
5
|
+
import { Issue } from '../../../domain/entities/Issue';
|
|
6
|
+
import { FieldOption, Project } from '../../../domain/entities/Project';
|
|
7
|
+
import {
|
|
8
|
+
formatConsoleGeneratedAt,
|
|
9
|
+
writeConsoleLists,
|
|
10
|
+
} from './consoleListsWriter';
|
|
11
|
+
|
|
12
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
13
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
14
|
+
|
|
15
|
+
const isUnknownArray = (value: unknown): value is unknown[] =>
|
|
16
|
+
Array.isArray(value);
|
|
17
|
+
|
|
18
|
+
const ASSIGNEE = 'owner-login';
|
|
19
|
+
|
|
20
|
+
const option = (
|
|
21
|
+
id: string,
|
|
22
|
+
name: string,
|
|
23
|
+
color: FieldOption['color'],
|
|
24
|
+
): FieldOption => ({ id, name, color, description: '' });
|
|
25
|
+
|
|
26
|
+
const project: Project = {
|
|
27
|
+
...mock<Project>(),
|
|
28
|
+
status: {
|
|
29
|
+
name: 'Status',
|
|
30
|
+
fieldId: 'status-field',
|
|
31
|
+
statuses: [
|
|
32
|
+
option('st-unread', 'Unread', 'ORANGE'),
|
|
33
|
+
option('st-aw', 'Awaiting Workspace', 'BLUE'),
|
|
34
|
+
option('st-aqc', 'Awaiting Quality Check', 'GREEN'),
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
story: {
|
|
38
|
+
name: 'story',
|
|
39
|
+
fieldId: 'story-field',
|
|
40
|
+
databaseId: 2,
|
|
41
|
+
stories: [option('s1', 'Story Alpha', 'BLUE')],
|
|
42
|
+
workflowManagementStory: { id: 'wm', name: 'workflow management' },
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const makeIssue = (overrides: Partial<Issue>): Issue => ({
|
|
47
|
+
...mock<Issue>(),
|
|
48
|
+
number: 1,
|
|
49
|
+
title: 'Issue 1',
|
|
50
|
+
nameWithOwner: 'demo/repo',
|
|
51
|
+
url: 'https://github.com/demo/repo/issues/1',
|
|
52
|
+
status: null,
|
|
53
|
+
story: null,
|
|
54
|
+
nextActionDate: null,
|
|
55
|
+
nextActionHour: null,
|
|
56
|
+
dependedIssueUrls: [],
|
|
57
|
+
assignees: [ASSIGNEE],
|
|
58
|
+
labels: [],
|
|
59
|
+
body: 'should never be written',
|
|
60
|
+
itemId: 'item-1',
|
|
61
|
+
isPr: false,
|
|
62
|
+
isClosed: false,
|
|
63
|
+
createdAt: new Date('2026-06-13T08:18:45.000Z'),
|
|
64
|
+
...overrides,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('writeConsoleLists', () => {
|
|
68
|
+
let outDir: string;
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-out-'));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
fs.rmSync(outDir, { recursive: true, force: true });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const tabFile = (tab: string): string =>
|
|
79
|
+
path.join(outDir, 'demo', tab, 'list.json');
|
|
80
|
+
|
|
81
|
+
it('writes all four tab list.json files', () => {
|
|
82
|
+
writeConsoleLists({
|
|
83
|
+
consoleDataOutputDir: outDir,
|
|
84
|
+
pjcode: 'demo',
|
|
85
|
+
assigneeLogin: ASSIGNEE,
|
|
86
|
+
project,
|
|
87
|
+
issues: [makeIssue({ status: 'Unread' })],
|
|
88
|
+
generatedAt: '2026-06-14T07:22:33Z',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
for (const tab of ['prs', 'triage', 'unread', 'failed-preparation']) {
|
|
92
|
+
expect(fs.existsSync(tabFile(tab))).toBe(true);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('writes items with no body field', () => {
|
|
97
|
+
writeConsoleLists({
|
|
98
|
+
consoleDataOutputDir: outDir,
|
|
99
|
+
pjcode: 'demo',
|
|
100
|
+
assigneeLogin: ASSIGNEE,
|
|
101
|
+
project,
|
|
102
|
+
issues: [makeIssue({ status: 'Unread' })],
|
|
103
|
+
generatedAt: '2026-06-14T07:22:33Z',
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const raw: unknown = JSON.parse(fs.readFileSync(tabFile('unread'), 'utf8'));
|
|
107
|
+
expect(isRecord(raw)).toBe(true);
|
|
108
|
+
const items: unknown = isRecord(raw) ? raw.items : undefined;
|
|
109
|
+
expect(isUnknownArray(items)).toBe(true);
|
|
110
|
+
const firstItem: unknown = isUnknownArray(items) ? items[0] : undefined;
|
|
111
|
+
expect(isRecord(firstItem)).toBe(true);
|
|
112
|
+
expect(firstItem).not.toHaveProperty('body');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('does not leave a temp file behind after writing', () => {
|
|
116
|
+
writeConsoleLists({
|
|
117
|
+
consoleDataOutputDir: outDir,
|
|
118
|
+
pjcode: 'demo',
|
|
119
|
+
assigneeLogin: ASSIGNEE,
|
|
120
|
+
project,
|
|
121
|
+
issues: [],
|
|
122
|
+
generatedAt: '2026-06-14T07:22:33Z',
|
|
123
|
+
});
|
|
124
|
+
expect(fs.existsSync(`${tabFile('prs')}.tmp`)).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('is a no-op when consoleDataOutputDir is unset', () => {
|
|
128
|
+
writeConsoleLists({
|
|
129
|
+
consoleDataOutputDir: undefined,
|
|
130
|
+
pjcode: 'demo',
|
|
131
|
+
assigneeLogin: ASSIGNEE,
|
|
132
|
+
project,
|
|
133
|
+
issues: [makeIssue({ status: 'Unread' })],
|
|
134
|
+
});
|
|
135
|
+
expect(fs.readdirSync(outDir)).toHaveLength(0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('is a no-op when pjcode is unset', () => {
|
|
139
|
+
writeConsoleLists({
|
|
140
|
+
consoleDataOutputDir: outDir,
|
|
141
|
+
pjcode: '',
|
|
142
|
+
assigneeLogin: ASSIGNEE,
|
|
143
|
+
project,
|
|
144
|
+
issues: [makeIssue({ status: 'Unread' })],
|
|
145
|
+
});
|
|
146
|
+
expect(fs.readdirSync(outDir)).toHaveLength(0);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('is a no-op when assigneeLogin is unset', () => {
|
|
150
|
+
writeConsoleLists({
|
|
151
|
+
consoleDataOutputDir: outDir,
|
|
152
|
+
pjcode: 'demo',
|
|
153
|
+
assigneeLogin: null,
|
|
154
|
+
project,
|
|
155
|
+
issues: [makeIssue({ status: 'Unread' })],
|
|
156
|
+
});
|
|
157
|
+
expect(fs.readdirSync(outDir)).toHaveLength(0);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('formatConsoleGeneratedAt', () => {
|
|
162
|
+
it('strips milliseconds and keeps the trailing Z', () => {
|
|
163
|
+
expect(formatConsoleGeneratedAt(new Date('2026-06-14T07:22:33.456Z'))).toBe(
|
|
164
|
+
'2026-06-14T07:22:33Z',
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import type { Issue } from '../../../domain/entities/Issue';
|
|
4
|
+
import type { Project } from '../../../domain/entities/Project';
|
|
5
|
+
import {
|
|
6
|
+
ConsoleLists,
|
|
7
|
+
ConsoleTabName,
|
|
8
|
+
GenerateConsoleListsUseCase,
|
|
9
|
+
} from '../../../domain/usecases/console/GenerateConsoleListsUseCase';
|
|
10
|
+
|
|
11
|
+
export type ConsoleListsWriterParams = {
|
|
12
|
+
consoleDataOutputDir: string | null | undefined;
|
|
13
|
+
pjcode: string | null | undefined;
|
|
14
|
+
assigneeLogin: string | null | undefined;
|
|
15
|
+
project: Project;
|
|
16
|
+
issues: Issue[];
|
|
17
|
+
generatedAt?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const CONSOLE_TAB_NAMES: ConsoleTabName[] = [
|
|
21
|
+
'prs',
|
|
22
|
+
'triage',
|
|
23
|
+
'unread',
|
|
24
|
+
'failed-preparation',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export const formatConsoleGeneratedAt = (date: Date): string =>
|
|
28
|
+
date.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
29
|
+
|
|
30
|
+
const writeJsonAtomic = (filePath: string, data: unknown): void => {
|
|
31
|
+
const dir = path.dirname(filePath);
|
|
32
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
33
|
+
const tmpPath = `${filePath}.tmp`;
|
|
34
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data));
|
|
35
|
+
fs.renameSync(tmpPath, filePath);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const writeConsoleLists = (params: ConsoleListsWriterParams): void => {
|
|
39
|
+
const { consoleDataOutputDir, pjcode, assigneeLogin } = params;
|
|
40
|
+
if (!consoleDataOutputDir || !pjcode || !assigneeLogin) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const generatedAt =
|
|
45
|
+
params.generatedAt ?? formatConsoleGeneratedAt(new Date());
|
|
46
|
+
const lists: ConsoleLists = new GenerateConsoleListsUseCase().run({
|
|
47
|
+
project: params.project,
|
|
48
|
+
issues: params.issues,
|
|
49
|
+
pjcode,
|
|
50
|
+
assigneeLogin,
|
|
51
|
+
generatedAt,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
for (const tab of CONSOLE_TAB_NAMES) {
|
|
55
|
+
writeJsonAtomic(
|
|
56
|
+
path.join(consoleDataOutputDir, pjcode, tab, 'list.json'),
|
|
57
|
+
lists[tab],
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
HEADERLESS_429_DEFAULT_COOLDOWN_SECONDS,
|
|
9
9
|
HEADERLESS_429_MAX_COOLDOWN_SECONDS,
|
|
10
10
|
parseModelRateLimitsFromBody,
|
|
11
|
+
parseModelRateLimitsFromHeaders,
|
|
11
12
|
readRateLimit,
|
|
12
13
|
writeModelRateLimit,
|
|
13
14
|
writeRateLimit,
|
|
@@ -647,4 +648,98 @@ describe('RateLimitCache', () => {
|
|
|
647
648
|
expect(readRateLimit(token)).toBeNull();
|
|
648
649
|
});
|
|
649
650
|
});
|
|
651
|
+
|
|
652
|
+
describe('parseModelRateLimitsFromHeaders', () => {
|
|
653
|
+
it('should extract a rejected seven_day_sonnet limit from the per-model unified headers', () => {
|
|
654
|
+
expect(
|
|
655
|
+
parseModelRateLimitsFromHeaders({
|
|
656
|
+
'anthropic-ratelimit-unified-7d_sonnet-status': 'rejected',
|
|
657
|
+
'anthropic-ratelimit-unified-7d_sonnet-reset': '1779642000',
|
|
658
|
+
}),
|
|
659
|
+
).toEqual({
|
|
660
|
+
seven_day_sonnet: { rejected: true, resetsAt: 1779642000 },
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('should extract both seven_day_sonnet and seven_day_opus limits with their statuses', () => {
|
|
665
|
+
expect(
|
|
666
|
+
parseModelRateLimitsFromHeaders({
|
|
667
|
+
'anthropic-ratelimit-unified-7d_sonnet-status': 'rejected',
|
|
668
|
+
'anthropic-ratelimit-unified-7d_sonnet-reset': '1779642000',
|
|
669
|
+
'anthropic-ratelimit-unified-7d_opus-status': 'allowed',
|
|
670
|
+
'anthropic-ratelimit-unified-7d_opus-reset': '1779700000',
|
|
671
|
+
}),
|
|
672
|
+
).toEqual({
|
|
673
|
+
seven_day_sonnet: { rejected: true, resetsAt: 1779642000 },
|
|
674
|
+
seven_day_opus: { rejected: false, resetsAt: 1779700000 },
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it('should return an empty map when no per-model headers are present', () => {
|
|
679
|
+
expect(
|
|
680
|
+
parseModelRateLimitsFromHeaders({
|
|
681
|
+
'anthropic-ratelimit-unified-status': 'allowed',
|
|
682
|
+
'anthropic-ratelimit-unified-7d-status': 'allowed',
|
|
683
|
+
}),
|
|
684
|
+
).toEqual({});
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it('should default resetsAt to 0 when the per-model reset header is missing', () => {
|
|
688
|
+
expect(
|
|
689
|
+
parseModelRateLimitsFromHeaders({
|
|
690
|
+
'anthropic-ratelimit-unified-7d_opus-status': 'rejected',
|
|
691
|
+
}),
|
|
692
|
+
).toEqual({
|
|
693
|
+
seven_day_opus: { rejected: true, resetsAt: 0 },
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
describe('readRateLimit per-model header population', () => {
|
|
699
|
+
it('should populate seven_day_sonnet from the per-model headers when the body carried no rate_limit event', () => {
|
|
700
|
+
const token = 'header-only-sonnet-token';
|
|
701
|
+
writeRateLimit(token, {
|
|
702
|
+
'anthropic-ratelimit-unified-status': 'rejected',
|
|
703
|
+
'anthropic-ratelimit-unified-5h-status': 'allowed',
|
|
704
|
+
'anthropic-ratelimit-unified-5h-reset': '1700000000',
|
|
705
|
+
'anthropic-ratelimit-unified-5h-utilization': '53',
|
|
706
|
+
'anthropic-ratelimit-unified-7d-status': 'allowed_warning',
|
|
707
|
+
'anthropic-ratelimit-unified-7d-reset': '1700100000',
|
|
708
|
+
'anthropic-ratelimit-unified-7d-utilization': '88',
|
|
709
|
+
'anthropic-ratelimit-unified-7d_sonnet-status': 'rejected',
|
|
710
|
+
'anthropic-ratelimit-unified-7d_sonnet-reset': '1779642000',
|
|
711
|
+
'anthropic-ratelimit-unified-7d_opus-status': 'allowed',
|
|
712
|
+
'anthropic-ratelimit-unified-7d_opus-reset': '1779700000',
|
|
713
|
+
});
|
|
714
|
+
const snapshot = readRateLimit(token);
|
|
715
|
+
expect(snapshot?.modelWeeklyLimits).toEqual({
|
|
716
|
+
seven_day_sonnet: { rejected: true, resetsAt: 1779642000 },
|
|
717
|
+
seven_day_opus: { rejected: false, resetsAt: 1779700000 },
|
|
718
|
+
});
|
|
719
|
+
expect(snapshot?.unifiedRejected).toBe(true);
|
|
720
|
+
expect(snapshot?.fiveHourRejected).toBe(false);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('should let a body-derived model limit override the per-model header value for the same claim', () => {
|
|
724
|
+
const token = 'body-overrides-header-token';
|
|
725
|
+
writeRateLimit(token, {
|
|
726
|
+
'anthropic-ratelimit-unified-status': 'allowed',
|
|
727
|
+
'anthropic-ratelimit-unified-5h-status': 'allowed',
|
|
728
|
+
'anthropic-ratelimit-unified-5h-reset': '1700000000',
|
|
729
|
+
'anthropic-ratelimit-unified-5h-utilization': '10',
|
|
730
|
+
'anthropic-ratelimit-unified-7d-status': 'allowed',
|
|
731
|
+
'anthropic-ratelimit-unified-7d-reset': '1700100000',
|
|
732
|
+
'anthropic-ratelimit-unified-7d-utilization': '5',
|
|
733
|
+
'anthropic-ratelimit-unified-7d_sonnet-status': 'allowed',
|
|
734
|
+
'anthropic-ratelimit-unified-7d_sonnet-reset': '1700200000',
|
|
735
|
+
});
|
|
736
|
+
writeModelRateLimit(token, {
|
|
737
|
+
seven_day_sonnet: { rejected: true, resetsAt: 1779642000 },
|
|
738
|
+
});
|
|
739
|
+
const snapshot = readRateLimit(token);
|
|
740
|
+
expect(snapshot?.modelWeeklyLimits).toEqual({
|
|
741
|
+
seven_day_sonnet: { rejected: true, resetsAt: 1779642000 },
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
});
|
|
650
745
|
});
|
|
@@ -194,6 +194,34 @@ export const parseModelRateLimitsFromBody = (
|
|
|
194
194
|
return result;
|
|
195
195
|
};
|
|
196
196
|
|
|
197
|
+
const HEADER_CLAIM_TO_LIMIT_TYPE: Record<string, string> = {
|
|
198
|
+
'7d_sonnet': 'seven_day_sonnet',
|
|
199
|
+
'7d_opus': 'seven_day_opus',
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export const parseModelRateLimitsFromHeaders = (
|
|
203
|
+
headers: Record<string, string>,
|
|
204
|
+
): Record<string, ModelWeeklyLimit> => {
|
|
205
|
+
const result: Record<string, ModelWeeklyLimit> = {};
|
|
206
|
+
for (const [headerClaim, limitType] of Object.entries(
|
|
207
|
+
HEADER_CLAIM_TO_LIMIT_TYPE,
|
|
208
|
+
)) {
|
|
209
|
+
const status = headers[`anthropic-ratelimit-unified-${headerClaim}-status`];
|
|
210
|
+
const resetRaw =
|
|
211
|
+
headers[`anthropic-ratelimit-unified-${headerClaim}-reset`];
|
|
212
|
+
if (status === undefined) continue;
|
|
213
|
+
const resetsAt =
|
|
214
|
+
resetRaw !== undefined && Number.isFinite(Number(resetRaw))
|
|
215
|
+
? Number(resetRaw)
|
|
216
|
+
: 0;
|
|
217
|
+
result[limitType] = {
|
|
218
|
+
rejected: status === 'rejected',
|
|
219
|
+
resetsAt,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
};
|
|
224
|
+
|
|
197
225
|
export const readRateLimit = (token: string): RateLimitSnapshot | null => {
|
|
198
226
|
const filePath = cachePathForToken(token);
|
|
199
227
|
if (!fs.existsSync(filePath)) return null;
|
|
@@ -240,7 +268,10 @@ export const readRateLimit = (token: string): RateLimitSnapshot | null => {
|
|
|
240
268
|
unifiedRejected,
|
|
241
269
|
fiveHourRejected,
|
|
242
270
|
sevenDayRejected,
|
|
243
|
-
modelWeeklyLimits:
|
|
271
|
+
modelWeeklyLimits: {
|
|
272
|
+
...parseModelRateLimitsFromHeaders(headers),
|
|
273
|
+
...readModelWeeklyLimits(parsed),
|
|
274
|
+
},
|
|
244
275
|
lastUpdatedEpoch,
|
|
245
276
|
blockedUntilEpoch,
|
|
246
277
|
};
|
|
@@ -109,6 +109,7 @@ describe('ProxyClaudeTokenUsageRepository', () => {
|
|
|
109
109
|
sevenDayUtilization: 0,
|
|
110
110
|
blocked: false,
|
|
111
111
|
rejected: false,
|
|
112
|
+
fiveHourRejected: false,
|
|
112
113
|
blockedUntilEpoch: 0,
|
|
113
114
|
modelWeeklyLimits: {
|
|
114
115
|
seven_day: { rejected: false, resetsAt: futureReset },
|
|
@@ -121,6 +122,7 @@ describe('ProxyClaudeTokenUsageRepository', () => {
|
|
|
121
122
|
sevenDayUtilization: 0,
|
|
122
123
|
blocked: false,
|
|
123
124
|
rejected: false,
|
|
125
|
+
fiveHourRejected: false,
|
|
124
126
|
blockedUntilEpoch: 0,
|
|
125
127
|
modelWeeklyLimits: {},
|
|
126
128
|
},
|
|
@@ -155,6 +157,7 @@ describe('ProxyClaudeTokenUsageRepository', () => {
|
|
|
155
157
|
sevenDayUtilization: 0,
|
|
156
158
|
blocked: true,
|
|
157
159
|
rejected: false,
|
|
160
|
+
fiveHourRejected: false,
|
|
158
161
|
blockedUntilEpoch: 0,
|
|
159
162
|
modelWeeklyLimits: {
|
|
160
163
|
seven_day: { rejected: false, resetsAt: futureReset },
|
|
@@ -191,6 +194,7 @@ describe('ProxyClaudeTokenUsageRepository', () => {
|
|
|
191
194
|
sevenDayUtilization: 0,
|
|
192
195
|
blocked: false,
|
|
193
196
|
rejected: true,
|
|
197
|
+
fiveHourRejected: true,
|
|
194
198
|
blockedUntilEpoch: 0,
|
|
195
199
|
modelWeeklyLimits: {
|
|
196
200
|
seven_day: { rejected: false, resetsAt: futureReset },
|
|
@@ -227,6 +231,7 @@ describe('ProxyClaudeTokenUsageRepository', () => {
|
|
|
227
231
|
sevenDayUtilization: 30,
|
|
228
232
|
blocked: false,
|
|
229
233
|
rejected: false,
|
|
234
|
+
fiveHourRejected: false,
|
|
230
235
|
blockedUntilEpoch: 0,
|
|
231
236
|
modelWeeklyLimits: {
|
|
232
237
|
seven_day: { rejected: false, resetsAt: futureReset },
|
|
@@ -263,6 +268,7 @@ describe('ProxyClaudeTokenUsageRepository', () => {
|
|
|
263
268
|
sevenDayUtilization: 0,
|
|
264
269
|
blocked: false,
|
|
265
270
|
rejected: false,
|
|
271
|
+
fiveHourRejected: false,
|
|
266
272
|
blockedUntilEpoch: 0,
|
|
267
273
|
modelWeeklyLimits: {
|
|
268
274
|
seven_day: { rejected: false, resetsAt: futureReset },
|
|
@@ -299,6 +305,7 @@ describe('ProxyClaudeTokenUsageRepository', () => {
|
|
|
299
305
|
sevenDayUtilization: 0,
|
|
300
306
|
blocked: false,
|
|
301
307
|
rejected: false,
|
|
308
|
+
fiveHourRejected: false,
|
|
302
309
|
blockedUntilEpoch: 0,
|
|
303
310
|
modelWeeklyLimits: {
|
|
304
311
|
seven_day: { rejected: false, resetsAt: futureReset },
|
|
@@ -335,6 +342,7 @@ describe('ProxyClaudeTokenUsageRepository', () => {
|
|
|
335
342
|
sevenDayUtilization: 0,
|
|
336
343
|
blocked: false,
|
|
337
344
|
rejected: false,
|
|
345
|
+
fiveHourRejected: false,
|
|
338
346
|
blockedUntilEpoch: 0,
|
|
339
347
|
modelWeeklyLimits: {},
|
|
340
348
|
},
|
|
@@ -369,6 +377,7 @@ describe('ProxyClaudeTokenUsageRepository', () => {
|
|
|
369
377
|
sevenDayUtilization: 0,
|
|
370
378
|
blocked: false,
|
|
371
379
|
rejected: true,
|
|
380
|
+
fiveHourRejected: true,
|
|
372
381
|
blockedUntilEpoch: 0,
|
|
373
382
|
modelWeeklyLimits: {
|
|
374
383
|
seven_day: { rejected: false, resetsAt: futureReset },
|
|
@@ -405,6 +414,7 @@ describe('ProxyClaudeTokenUsageRepository', () => {
|
|
|
405
414
|
sevenDayUtilization: 100,
|
|
406
415
|
blocked: false,
|
|
407
416
|
rejected: true,
|
|
417
|
+
fiveHourRejected: false,
|
|
408
418
|
blockedUntilEpoch: 0,
|
|
409
419
|
modelWeeklyLimits: {
|
|
410
420
|
seven_day: { rejected: true, resetsAt: futureReset },
|
|
@@ -441,6 +451,7 @@ describe('ProxyClaudeTokenUsageRepository', () => {
|
|
|
441
451
|
sevenDayUtilization: 0,
|
|
442
452
|
blocked: false,
|
|
443
453
|
rejected: false,
|
|
454
|
+
fiveHourRejected: false,
|
|
444
455
|
blockedUntilEpoch: 0,
|
|
445
456
|
modelWeeklyLimits: {
|
|
446
457
|
seven_day: { rejected: false, resetsAt: futureReset },
|
|
@@ -466,6 +477,7 @@ describe('ProxyClaudeTokenUsageRepository', () => {
|
|
|
466
477
|
sevenDayUtilization: 0,
|
|
467
478
|
blocked: false,
|
|
468
479
|
rejected: false,
|
|
480
|
+
fiveHourRejected: false,
|
|
469
481
|
blockedUntilEpoch: 0,
|
|
470
482
|
modelWeeklyLimits: {},
|
|
471
483
|
},
|
|
@@ -502,6 +514,7 @@ describe('ProxyClaudeTokenUsageRepository', () => {
|
|
|
502
514
|
sevenDayUtilization: 10,
|
|
503
515
|
blocked: false,
|
|
504
516
|
rejected: false,
|
|
517
|
+
fiveHourRejected: false,
|
|
505
518
|
blockedUntilEpoch: 0,
|
|
506
519
|
modelWeeklyLimits: {
|
|
507
520
|
seven_day_sonnet: { rejected: true, resetsAt: futureReset },
|
|
@@ -540,6 +553,7 @@ describe('ProxyClaudeTokenUsageRepository', () => {
|
|
|
540
553
|
sevenDayUtilization: 10,
|
|
541
554
|
blocked: false,
|
|
542
555
|
rejected: false,
|
|
556
|
+
fiveHourRejected: false,
|
|
543
557
|
blockedUntilEpoch: 0,
|
|
544
558
|
modelWeeklyLimits: {
|
|
545
559
|
seven_day_sonnet: { rejected: false, resetsAt: pastReset },
|
|
@@ -682,6 +696,35 @@ describe('ProxyClaudeTokenUsageRepository', () => {
|
|
|
682
696
|
});
|
|
683
697
|
});
|
|
684
698
|
|
|
699
|
+
it('should bridge a rejected generic seven_day window even when a per-model weekly limit is already present', async () => {
|
|
700
|
+
const opusResetsAt = futureReset + 2000;
|
|
701
|
+
mockLoadTokenEntries.mockReturnValue([
|
|
702
|
+
{ name: 'alice', token: 'token-a' },
|
|
703
|
+
]);
|
|
704
|
+
mockReadRateLimit.mockReturnValue({
|
|
705
|
+
fiveHourUtilization: 10,
|
|
706
|
+
fiveHourReset: futureReset,
|
|
707
|
+
sevenDayUtilization: 100,
|
|
708
|
+
sevenDayReset: futureReset,
|
|
709
|
+
blocked: false,
|
|
710
|
+
rejected: true,
|
|
711
|
+
unifiedRejected: false,
|
|
712
|
+
fiveHourRejected: false,
|
|
713
|
+
sevenDayRejected: true,
|
|
714
|
+
modelWeeklyLimits: {
|
|
715
|
+
seven_day_opus: { rejected: false, resetsAt: opusResetsAt },
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
|
|
719
|
+
|
|
720
|
+
const result = await repository.getAvailableTokenUsages();
|
|
721
|
+
|
|
722
|
+
expect(result[0].modelWeeklyLimits).toEqual({
|
|
723
|
+
seven_day_opus: { rejected: false, resetsAt: opusResetsAt },
|
|
724
|
+
seven_day: { rejected: true, resetsAt: futureReset },
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
|
|
685
728
|
it('should not bridge sevenDayReset when the 7d reset has already passed', async () => {
|
|
686
729
|
mockLoadTokenEntries.mockReturnValue([
|
|
687
730
|
{ name: 'alice', token: 'token-a' },
|