fix: robust foreground detection for macOS and multiple bug fixes
- Use `ps -o stat=` to check process foreground state directly, fixing macOS where tmux pane_current_command reports "zsh" instead of the child process. Falls back to pane_current_command safely. - Fix print mode retry off-by-one (maxRetries=5 only gave 4 retries) - Fix sendKeys failure leaving state unupdated (tight error loop) - Validate customPatterns config entries (non-string/invalid regex crashed monitor) - Parse "resets in: N hours" format (was detected but not time-parsed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,17 @@ describe('loadConfig', () => {
|
||||
assert.equal(config.pollIntervalSeconds, 5);
|
||||
} finally { await unlink(f); }
|
||||
});
|
||||
it('filters invalid customPatterns entries', async () => {
|
||||
const { writeFile, unlink } = await import('node:fs/promises');
|
||||
const { tmpdir } = await import('node:os');
|
||||
const { join } = await import('node:path');
|
||||
const f = join(tmpdir(), `car-test-${Date.now()}.json`);
|
||||
await writeFile(f, JSON.stringify({ customPatterns: ["valid", 42, null, "[invalid"] }));
|
||||
try {
|
||||
const config = await loadConfig(f);
|
||||
assert.deepEqual(config.customPatterns, ["valid"]);
|
||||
} finally { await unlink(f); }
|
||||
});
|
||||
it('rejects negative numbers and falls back to defaults', async () => {
|
||||
const { writeFile, unlink } = await import('node:fs/promises');
|
||||
const { tmpdir } = await import('node:os');
|
||||
|
||||
+23
-7
@@ -3,12 +3,13 @@ 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') {
|
||||
function mockTmux(paneContent = '', paneCommand = 'node', claudeForeground = true) {
|
||||
const t = {
|
||||
_sent: [],
|
||||
capturePane: async () => paneContent,
|
||||
getPaneCommand: async () => paneCommand,
|
||||
sendKeys: async (_p, text) => { t._sent.push(text); },
|
||||
isClaudeForeground: async () => claudeForeground,
|
||||
};
|
||||
return t;
|
||||
}
|
||||
@@ -49,24 +50,39 @@ describe('processOneTick', () => {
|
||||
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');
|
||||
it('retries when Claude process is in foreground (fixes macOS zsh issue)', async () => {
|
||||
const t = mockTmux('5-hour limit reached - resets 3pm (UTC)', 'zsh', true);
|
||||
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);
|
||||
});
|
||||
it('falls back to pane_current_command when process state is false', async () => {
|
||||
const t = mockTmux('5-hour limit reached - resets 3pm (UTC)', 'vim', false);
|
||||
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');
|
||||
it('falls back to pane_current_command when process state is null', async () => {
|
||||
const t = mockTmux('5-hour limit reached - resets 3pm (UTC)', 'vim', null);
|
||||
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 in fallback path', async () => {
|
||||
const t = mockTmux('5-hour limit reached - resets 3pm (UTC)', 'my-claude-wrapper', null);
|
||||
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');
|
||||
it('matches npx in fallback path', async () => {
|
||||
const t = mockTmux('5-hour limit reached - resets 3pm (UTC)', 'npx', null);
|
||||
const s = createMonitorState();
|
||||
s.waitUntil = Date.now() - 1000; s.status = 'waiting';
|
||||
assert.equal(await processOneTick(s, t, '%0', DEFAULT_CONFIG, () => true), 'retried');
|
||||
|
||||
@@ -46,6 +46,16 @@ describe('parseResetTime', () => {
|
||||
assert.ok(r.relative);
|
||||
assert.equal(r.waitMs, 30 * 60_000);
|
||||
});
|
||||
it('parses "resets in: 3 hours" as relative time', () => {
|
||||
const r = parseResetTime('usage limit · resets in: 3 hours');
|
||||
assert.ok(r.relative);
|
||||
assert.equal(r.waitMs, 3 * 3_600_000);
|
||||
});
|
||||
it('parses "resets in 2 hours" as relative time', () => {
|
||||
const r = parseResetTime('resets in 2 hours');
|
||||
assert.ok(r.relative);
|
||||
assert.equal(r.waitMs, 2 * 3_600_000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateWaitMs', () => {
|
||||
|
||||
Reference in New Issue
Block a user