Files
CheapestInference db9f4f81af fix: multi-line TUI detection, stale scrollback, and false positives
The rate limit detection was fundamentally broken because Claude Code
renders rate limit messages across multiple lines in its TUI (Ink/React),
but all regex patterns required keywords on a single line.

Key fixes:
- Split detection into LIMIT_PATTERNS + RESET_PATTERNS with a 6-line
  sliding window, so multi-line TUI renders are detected correctly
- Capture only last 20 lines instead of 200 to avoid stale scrollback
- Add 30s post-retry cooldown to prevent re-detecting old messages
- Fix max-retries infinite loop (stay in waiting, check clear first)
- Add OSC/DCS sequence stripping for complete ANSI handling
- Parse relative times ("try again in 5 minutes") instead of 5h fallback
- Handle invalid timezones gracefully with fallback
- Fix zombie monitor on pane destruction (exit after 10 consecutive errors)
- Replace setInterval with recursive setTimeout to prevent concurrent ticks
- Restore user shell traps instead of clobbering them
- Remove overly broad /\blimit\b/ pattern that caused false positives

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 10:58:16 +01:00

131 lines
5.3 KiB
JavaScript

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { stripAnsi, isRateLimited, findRateLimitMessage } from '../src/patterns.js';
describe('stripAnsi', () => {
it('removes bold codes', () => {
assert.equal(stripAnsi('\x1b[1mlimit\x1b[0m'), 'limit');
});
it('removes color codes', () => {
assert.equal(stripAnsi('\x1b[31mred\x1b[0m'), 'red');
});
it('removes cursor positioning', () => {
assert.equal(stripAnsi('\x1b[2Jhello\x1b[H'), 'hello');
});
it('leaves plain text unchanged', () => {
assert.equal(stripAnsi('plain text'), 'plain text');
});
it('handles mixed content', () => {
assert.equal(
stripAnsi('5-hour \x1b[1mlimit\x1b[0m reached - resets 3pm'),
'5-hour limit reached - resets 3pm'
);
});
});
describe('isRateLimited', () => {
it('detects "5-hour limit reached"', () => {
assert.equal(isRateLimited('5-hour limit reached - resets 3pm'), true);
});
it('detects "usage limit" with reset', () => {
assert.equal(isRateLimited('Claude usage limit reached. Resets at 2pm'), true);
});
it('detects "out of extra usage"', () => {
assert.equal(isRateLimited("You're out of extra usage · resets 3pm"), true);
});
it('detects "try again in 5 hours"', () => {
assert.equal(isRateLimited('Please try again in 5 hours'), true);
});
it('detects "rate limit resets"', () => {
assert.equal(isRateLimited('Rate limit hit. Resets at 4pm'), true);
});
it('returns false for normal output', () => {
assert.equal(isRateLimited('I can help you with that code'), false);
});
it('returns false for empty string', () => {
assert.equal(isRateLimited(''), false);
});
it('detects rate limit with ANSI codes embedded', () => {
assert.equal(isRateLimited('5-hour \x1b[1mlimit\x1b[0m reached - resets 3pm'), true);
});
it('matches custom patterns', () => {
assert.equal(isRateLimited('custom error xyz', [/custom error/i]), true);
});
it('detects "You\'ve hit your limit" (real Claude Code message)', () => {
assert.equal(isRateLimited("You've hit your limit · resets 3pm (Asia/Tbilisi)"), true);
});
it('detects "hit the limit resets"', () => {
assert.equal(isRateLimited('You hit the limit. Resets at 5pm'), true);
});
it('detects "usage limit · resets in: 3 hours"', () => {
assert.equal(isRateLimited('usage limit · resets in: 3 hours'), true);
});
});
describe('stripAnsi (private-mode sequences)', () => {
it('strips cursor hide sequence', () => {
assert.equal(stripAnsi('\x1b[?25lhello\x1b[?25h'), 'hello');
});
it('strips bracketed paste mode', () => {
assert.equal(stripAnsi('\x1b[?2004htext\x1b[?2004l'), 'text');
});
});
describe('findRateLimitMessage', () => {
it('returns the matching line from multiline input', () => {
const text = 'Some output\n5-hour limit reached - resets 3pm (Europe/Dublin)\nMore output';
assert.equal(findRateLimitMessage(text), '5-hour limit reached - resets 3pm (Europe/Dublin)');
});
it('returns null when no match', () => {
assert.equal(findRateLimitMessage('normal output\nmore output'), null);
});
it('returns the resets line from multi-line TUI render', () => {
const text = '⚠ You\'ve hit your limit\n· resets 3pm (UTC)';
assert.equal(findRateLimitMessage(text), '· resets 3pm (UTC)');
});
it('returns Resets line when limit and resets on different lines', () => {
const text = '5-hour limit reached\nResets at 3pm (UTC)';
assert.ok(findRateLimitMessage(text).includes('3pm'));
});
});
describe('isRateLimited (multi-line TUI renders)', () => {
it('detects limit + resets on separate lines', () => {
assert.ok(isRateLimited('⚠ You\'ve hit your limit\n· resets 3pm (UTC)'));
});
it('detects box-drawing TUI format', () => {
const text = '╭──────────╮\n│ ⚠ You\'ve hit your limit │\n│ · resets 3pm │\n╰──────────╯';
assert.ok(isRateLimited(text));
});
it('detects 5-hour limit + Resets on separate lines', () => {
assert.ok(isRateLimited('⚠ 5-hour limit reached\nResets at 3pm (UTC)'));
});
it('detects middle-dot separated multi-line', () => {
assert.ok(isRateLimited('⚠ You\'ve hit your 5-hour limit\n· resets 3pm (Asia/Tbilisi)'));
});
it('rejects limit + resets too far apart (>6 lines)', () => {
assert.equal(isRateLimited('hit your limit\n1\n2\n3\n4\n5\n6\n7\nresets 3pm'), false);
});
it('rejects normal output with no rate limit keywords', () => {
assert.equal(isRateLimited('Working on your request\nHere is the code\nDone'), false);
});
});
describe('stripAnsi (OSC sequences)', () => {
it('strips OSC hyperlinks (\\x1b]8;;url\\x1b\\\\)', () => {
const input = '\x1b]8;;https://example.com\x1b\\click here\x1b]8;;\x1b\\';
assert.equal(stripAnsi(input), 'click here');
});
it('strips OSC window title (\\x1b]0;title\\x07)', () => {
assert.equal(stripAnsi('\x1b]0;My Terminal\x07hello'), 'hello');
});
it('strips OSC + CSI mixed sequences', () => {
const input = '\x1b]8;;url\x1b\\\x1b[33m5-hour limit reached - resets 3pm\x1b[0m\x1b]8;;\x1b\\';
assert.equal(stripAnsi(input), '5-hour limit reached - resets 3pm');
});
it('rate limit detection works through OSC hyperlinks', () => {
const input = '\x1b]8;;link\x1b\\5-hour limit reached\x1b]8;;\x1b\\ - resets 3pm';
assert.ok(isRateLimited(input));
});
});