Files
claude-auto-retry/test/monitor.test.js
T
CheapestInference 0652a23274 fix: improve foreground process detection for macOS compatibility
Expand default foreground command list beyond node/claude to include
npx, tsx, bun, deno. Log actual detected process name for debugging.
Add configurable foregroundCommands in ~/.claude-auto-retry.json.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 03:52:16 +02:00

99 lines
4.6 KiB
JavaScript

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { createMonitorState, processOneTick } from '../src/monitor.js';
import { DEFAULT_CONFIG } from '../src/config.js';
function mockTmux(paneContent = '', paneCommand = 'node') {
const t = {
_sent: [],
capturePane: async () => paneContent,
getPaneCommand: async () => paneCommand,
sendKeys: async (_p, text) => { t._sent.push(text); },
};
return t;
}
describe('processOneTick', () => {
it('returns monitoring when no rate limit', async () => {
const t = mockTmux('Normal output');
const s = createMonitorState();
assert.equal(await processOneTick(s, t, '%0', DEFAULT_CONFIG, () => true), 'monitoring');
assert.equal(t._sent.length, 0);
});
it('enters waiting on rate limit', async () => {
const t = mockTmux('5-hour limit reached - resets 3pm (UTC)');
const s = createMonitorState();
assert.equal(await processOneTick(s, t, '%0', DEFAULT_CONFIG, () => true), 'waiting');
assert.ok(s.waitUntil > Date.now());
});
it('exits when PID dead', async () => {
const t = mockTmux('5-hour limit reached - resets 3pm (UTC)');
const s = createMonitorState();
s.waitUntil = Date.now() - 1000; s.status = 'waiting';
assert.equal(await processOneTick(s, t, '%0', DEFAULT_CONFIG, () => false), 'exit');
});
it('sends retry when wait expired and rate limit visible', async () => {
const t = mockTmux('5-hour limit reached - resets 3pm (UTC)');
const s = createMonitorState();
s.waitUntil = Date.now() - 1000; s.status = 'waiting';
assert.equal(await processOneTick(s, t, '%0', DEFAULT_CONFIG, () => true), 'retried');
assert.equal(t._sent.length, 1);
assert.equal(s.attempts, 1);
// Should stay in 'waiting' with a cooldown to let Claude process
assert.equal(s.status, 'waiting');
assert.ok(s.waitUntil > Date.now());
});
it('detects multi-line TUI rate limit', async () => {
const t = mockTmux('⚠ You\'ve hit your limit\n· resets 3pm (UTC)');
const s = createMonitorState();
assert.equal(await processOneTick(s, t, '%0', DEFAULT_CONFIG, () => true), 'waiting');
assert.ok(s.waitUntil > Date.now());
});
it('skips when foreground is not claude', async () => {
const t = mockTmux('5-hour limit reached - resets 3pm (UTC)', 'vim');
const s = createMonitorState();
s.waitUntil = Date.now() - 1000; s.status = 'waiting';
assert.equal(await processOneTick(s, t, '%0', DEFAULT_CONFIG, () => true), 'skipped-not-claude');
assert.equal(t._sent.length, 0);
assert.equal(s._lastForeground, 'vim');
});
it('accepts custom foregroundCommands from config', async () => {
const t = mockTmux('5-hour limit reached - resets 3pm (UTC)', 'my-claude-wrapper');
const s = createMonitorState();
s.waitUntil = Date.now() - 1000; s.status = 'waiting';
const config = { ...DEFAULT_CONFIG, foregroundCommands: ['my-claude-wrapper'] };
assert.equal(await processOneTick(s, t, '%0', config, () => true), 'retried');
assert.equal(t._sent.length, 1);
});
it('matches npx as default foreground command', async () => {
const t = mockTmux('5-hour limit reached - resets 3pm (UTC)', 'npx');
const s = createMonitorState();
s.waitUntil = Date.now() - 1000; s.status = 'waiting';
assert.equal(await processOneTick(s, t, '%0', DEFAULT_CONFIG, () => true), 'retried');
});
it('resets counter when rate limit disappears', async () => {
const t = mockTmux('Claude is working normally');
const s = createMonitorState();
s.waitUntil = Date.now() - 1000; s.status = 'waiting'; s.attempts = 2;
assert.equal(await processOneTick(s, t, '%0', DEFAULT_CONFIG, () => true), 'user-continued');
assert.equal(s.attempts, 0);
});
it('stops retrying after max attempts and stays in waiting', async () => {
const t = mockTmux('5-hour limit reached - resets 3pm (UTC)');
const s = createMonitorState();
s.waitUntil = Date.now() - 1000; s.status = 'waiting'; s.attempts = 5;
assert.equal(await processOneTick(s, t, '%0', DEFAULT_CONFIG, () => true), 'max-retries');
// Should stay in 'waiting' to avoid re-detection loop
assert.equal(s.status, 'waiting');
assert.ok(s.waitUntil > Date.now());
});
it('resets from max-retries when rate limit clears', async () => {
const t = mockTmux('Claude is working normally');
const s = createMonitorState();
s.waitUntil = Date.now() - 1000; s.status = 'waiting'; s.attempts = 10;
// Rate limit cleared → should detect user-continued before max-retries check
assert.equal(await processOneTick(s, t, '%0', DEFAULT_CONFIG, () => true), 'user-continued');
assert.equal(s.attempts, 0);
});
});