Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 53 additions & 4 deletions packages/core/src/utils/error-formatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,66 @@ describe('classifyAndFormatError', () => {
describe('rate limit errors', () => {
test('detects lowercase "rate limit"', () => {
const result = classifyAndFormatError(new Error('rate limit exceeded'));
expect(result).toBe('⚠️ AI rate limit reached. Please wait a moment and try again.');
expect(result).toBe('⚠️ AI usage limit reached. Please wait and try again.');
});

test('detects titlecase "Rate limit"', () => {
const result = classifyAndFormatError(new Error('Rate limit: 429 Too Many Requests'));
expect(result).toBe('⚠️ AI rate limit reached. Please wait a moment and try again.');
expect(result).toBe('⚠️ AI usage limit reached. Please wait and try again.');
});

test('matches rate limit anywhere in message', () => {
const result = classifyAndFormatError(new Error('Request failed: rate limit hit'));
expect(result).toBe('⚠️ AI rate limit reached. Please wait a moment and try again.');
expect(result).toBe('⚠️ AI usage limit reached. Please wait and try again.');
});

test('detects "hit your limit" (Claude subscription cap)', () => {
const result = classifyAndFormatError(
new Error("You've hit your limit · resets 4:50pm (UTC)")
);
expect(result).toBe(
'⚠️ AI usage limit reached (resets 4:50pm (UTC)). Please wait and try again.'
);
});

test('detects full enriched Claude usage-cap error with reset time', () => {
const result = classifyAndFormatError(
new Error(
"Claude Code unknown: Claude Code returned an error result: You've hit your limit · resets 4:50pm (UTC)"
)
);
expect(result).toBe(
'⚠️ AI usage limit reached (resets 4:50pm (UTC)). Please wait and try again.'
);
});

test('detects "usage limit" (Claude org-disabled-overage variant)', () => {
const result = classifyAndFormatError(new Error('usage limit exceeded'));
expect(result).toBe('⚠️ AI usage limit reached. Please wait and try again.');
});

test('omits reset clause when no reset time present in hit-your-limit message', () => {
const result = classifyAndFormatError(new Error("You've hit your limit"));
expect(result).toBe('⚠️ AI usage limit reached. Please wait and try again.');
});

test('detects title-case "Hit your limit" (case-insensitive)', () => {
const result = classifyAndFormatError(new Error('Hit your limit'));
expect(result).toBe('⚠️ AI usage limit reached. Please wait and try again.');
});

test('detects title-case "Usage limit" (case-insensitive)', () => {
const result = classifyAndFormatError(new Error('Usage limit exceeded'));
expect(result).toBe('⚠️ AI usage limit reached. Please wait and try again.');
});

test('handles reset text containing abbreviated periods (e.g. p.m.)', () => {
const result = classifyAndFormatError(
new Error("You've hit your limit · resets 4:50 p.m. (UTC)")
);
expect(result).toBe(
'⚠️ AI usage limit reached (resets 4:50 p.m. (UTC)). Please wait and try again.'
);
});
});

Expand Down Expand Up @@ -301,7 +350,7 @@ describe('classifyAndFormatError', () => {
test('rate limit takes precedence over short-message fallback', () => {
// "rate limit" message is also short, but rate-limit branch fires first
const result = classifyAndFormatError(new Error('rate limit'));
expect(result).toBe('⚠️ AI rate limit reached. Please wait a moment and try again.');
expect(result).toBe('⚠️ AI usage limit reached. Please wait and try again.');
});

test('Claude OAuth check takes precedence over general auth check', () => {
Expand Down
19 changes: 16 additions & 3 deletions packages/core/src/utils/error-formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,22 @@
export function classifyAndFormatError(error: Error): string {
const message = error.message || '';

// AI/SDK errors - rate limits
if (message.includes('rate limit') || message.includes('Rate limit')) {
return '⚠️ AI rate limit reached. Please wait a moment and try again.';
// AI/SDK errors - rate limits and usage caps
// Matches: "rate limit" (generic), "hit your limit" (Claude subscription cap),
// "usage limit" (Claude org-disabled-overage variant; this function only receives
// AI-provider errors, so false-positive risk from OS/container messages is negligible)
const lower = message.toLowerCase();
if (
lower.includes('rate limit') ||
lower.includes('hit your limit') ||
lower.includes('usage limit')
) {
// Anchor on · separator when present (Claude format: "... · resets 4:50pm (UTC)");
// fall back to stopping at · or newline so abbreviated periods (e.g. "p.m.") don't truncate.
const reset =
/·\s*(resets[^·\n]*)/i.exec(message)?.[1]?.trim() ??
/resets[^·\n]*/i.exec(message)?.[0]?.trim();
return `⚠️ AI usage limit reached${reset ? ` (${reset})` : ''}. Please wait and try again.`;
}

// Claude-specific auth errors — OAuth token refresh failures
Expand Down