mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
fix(idle): hold idle while the workspace trust dialog is on screen
The dialog's own prompt glyph armed idle confirmation and the quiet dialog passed it, so a fresh case reported idle about 4 s in. An agent that waited for idle and then prompted typed into the dialog, and its Enter confirmed No, exit and killed the pane.
This commit is contained in:
1 parent
a8a8bb71a3
commit
2a5f5b30be
7 files changed
+154
-16
No files matched your search
@@ -16,6 +16,17 @@
|
||||
*/
|
||||
import { describe, expect, it, vi, afterEach } from 'vitest';
|
||||
import { Session } from '../src/session.js';
|
||||
|
||||
// The restart case below starts a real direct-PTY session; the real `claude` must never
|
||||
// run from a test, so pty.spawn runs a harmless stand-in instead.
|
||||
vi.mock('node-pty', async (importOriginal) => {
|
||||
const real = await importOriginal<typeof import('node-pty')>();
|
||||
return {
|
||||
...real,
|
||||
spawn: (_file: string, _args: string[] | string, options: import('node-pty').IPtyForkOptions) =>
|
||||
real.spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], options),
|
||||
};
|
||||
});
|
||||
import {
|
||||
isTrustDialogScreen,
|
||||
compactScreenText,
|
||||
@@ -249,3 +260,104 @@ describe('Session trust-dialog auto-accept', () => {
|
||||
expect(writes).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Session idle detection while the trust dialog is up', () => {
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
/** A just-launched pane (busy, inside the startup window) rendering `screen` (null: capture failed). */
|
||||
function launchingSession(screen: () => string | null) {
|
||||
const mux = { isAvailable: () => true, capturePaneText: () => screen() };
|
||||
const session = new Session({
|
||||
workingDir: '/tmp',
|
||||
mode: 'claude',
|
||||
mux,
|
||||
muxSession: { muxName: 'codeman-test', sessionId: 'test', createdAt: Date.now() },
|
||||
} as unknown as ConstructorParameters<typeof Session>[0]);
|
||||
const internals = session as unknown as {
|
||||
_detectInteractiveActivity(data: string): void;
|
||||
_markActivity(): void;
|
||||
_interactiveStartedAt: number;
|
||||
_status: string;
|
||||
};
|
||||
internals._interactiveStartedAt = Date.now();
|
||||
internals._status = 'busy'; // what startInteractive() leaves a new pane at
|
||||
let idles = 0;
|
||||
session.on('idle', () => idles++);
|
||||
/** One repaint carrying a `❯`, which is what arms the idle confirmation. */
|
||||
const paint = (chunk: string) => {
|
||||
internals._markActivity();
|
||||
internals._detectInteractiveActivity(chunk);
|
||||
};
|
||||
return { session, paint, idles: () => idles };
|
||||
}
|
||||
|
||||
it('does not call the dialog idle, although its own ❯ arms the check', () => {
|
||||
vi.useFakeTimers();
|
||||
const { session, paint, idles } = launchingSession(() => RENDERED_DIALOG_2_1_252);
|
||||
paint(RENDERED_DIALOG_2_1_252);
|
||||
vi.advanceTimersByTime(30_000);
|
||||
expect(session.status).toBe('busy');
|
||||
expect(idles()).toBe(0);
|
||||
});
|
||||
|
||||
it('goes idle once the dialog has been answered and the composer is up', () => {
|
||||
vi.useFakeTimers();
|
||||
let screen = RENDERED_DIALOG_2_1_252;
|
||||
const { session, paint, idles } = launchingSession(() => screen);
|
||||
paint(RENDERED_DIALOG_2_1_252);
|
||||
vi.advanceTimersByTime(10_000);
|
||||
expect(session.status).toBe('busy');
|
||||
|
||||
// No new output needed: the check keeps re-reading the screen on its own.
|
||||
screen = RENDERED_MAIN_UI.replace('✻ Actualizing… (13m 23s · ↓ 47.5k tokens)', '');
|
||||
vi.advanceTimersByTime(10_000);
|
||||
expect(session.status).toBe('idle');
|
||||
expect(idles()).toBe(1);
|
||||
});
|
||||
|
||||
it('keeps holding when a capture fails after the dialog was seen', () => {
|
||||
vi.useFakeTimers();
|
||||
// A failed capture says nothing about whether the dialog went away.
|
||||
let screen: string | null = RENDERED_DIALOG_2_1_252;
|
||||
const { session, paint, idles } = launchingSession(() => screen);
|
||||
paint(RENDERED_DIALOG_2_1_252);
|
||||
vi.advanceTimersByTime(5000);
|
||||
screen = null;
|
||||
vi.advanceTimersByTime(20_000);
|
||||
expect(session.status).toBe('busy');
|
||||
expect(idles()).toBe(0);
|
||||
});
|
||||
|
||||
it("does not carry the last pane's dialog verdict into a restarted pane", async () => {
|
||||
// The session saw the dialog on its last pane, then restarted where no capture can
|
||||
// replace that verdict (here the direct-PTY fallback, which has no pane to read; a
|
||||
// failed capture keeps it the same way). The new pane must still be able to go idle.
|
||||
const session = new Session({ workingDir: '/tmp', mode: 'claude', useMux: false });
|
||||
(session as unknown as { _lastPaneProbeTrustDialog: boolean })._lastPaneProbeTrustDialog = true;
|
||||
try {
|
||||
await session.startInteractive();
|
||||
vi.useFakeTimers();
|
||||
const internals = session as unknown as {
|
||||
_detectInteractiveActivity(data: string): void;
|
||||
_markActivity(): void;
|
||||
};
|
||||
internals._markActivity();
|
||||
internals._detectInteractiveActivity('❯ ');
|
||||
vi.advanceTimersByTime(30_000);
|
||||
expect(session.status).toBe('idle');
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
await session.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it('lets a dialog-looking screen go idle outside the startup window', () => {
|
||||
vi.useFakeTimers();
|
||||
// A live agent can print the dialog's words hours in, and must not be held busy.
|
||||
const { session, paint } = launchingSession(() => RENDERED_DIALOG_2_1_252);
|
||||
vi.advanceTimersByTime(10 * 60_000);
|
||||
paint(RENDERED_DIALOG_2_1_252);
|
||||
vi.advanceTimersByTime(30_000);
|
||||
expect(session.status).toBe('idle');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user