feat(cli): fix UserSimulator deadlock on static screens by tracking composite state

This commit is contained in:
Mahima Shanware
2026-04-28 21:16:35 +00:00
parent e72092c602
commit e46bd05594
2 changed files with 50 additions and 7 deletions
@@ -189,4 +189,43 @@ describe('UserSimulator', () => {
expect(mockMessageBus.unsubscribe).toHaveBeenCalled();
vi.useRealTimers();
});
it('should re-evaluate if internal tool state changes even if screen content is static', async () => {
const simulator = new UserSimulator(
mockConfig,
mockGetScreen,
mockStdinBuffer,
);
mockGetScreen.mockReturnValue('Responding...');
vi.useFakeTimers();
simulator.start();
// Trigger first tick
await vi.advanceTimersByTimeAsync(2000);
expect(mockContentGenerator.generateContent).toHaveBeenCalledTimes(1);
// Trigger second tick with same screen - should skip
await vi.advanceTimersByTimeAsync(2000);
expect(mockContentGenerator.generateContent).toHaveBeenCalledTimes(1);
// Simulate tool call update
const handler = mockMessageBus.subscribe.mock.calls[0][1];
handler({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [
{
status: CoreToolCallStatus.AwaitingApproval,
request: { callId: '123', name: 'test_tool' },
},
],
});
// Trigger third tick with same screen but new tool state - should NOT skip
await vi.advanceTimersByTimeAsync(2000);
expect(mockContentGenerator.generateContent).toHaveBeenCalledTimes(2);
simulator.stop();
vi.useRealTimers();
});
});
+11 -7
View File
@@ -28,7 +28,7 @@ interface SimulatorResponse {
export class UserSimulator {
private isRunning = false;
private timer: NodeJS.Timeout | null = null;
private lastScreenContent = '';
private lastStateKey = '';
private isProcessing = false;
private interactionsFile: string | null = null;
@@ -131,7 +131,16 @@ export class UserSimulator {
.replace(/\(\s*\)/g, '')
.trim();
if (normalizedScreen === this.lastScreenContent) return;
// Create a composite key representing the full state (Vision + Internal State)
const pendingIds = this.pendingToolCalls
.map((tc) => tc.request.callId)
.join(',');
const currentStateKey = `${normalizedScreen}::${pendingIds}`;
if (currentStateKey === this.lastStateKey) {
return;
}
this.lastStateKey = currentStateKey;
debugLogger.log(
`[SIMULATOR] Screen Content Seen:\n---\n${strippedScreen}\n---`,
@@ -304,7 +313,6 @@ ${strippedScreen}
`[LOG] [SIMULATOR] Action History updated with: "<WAIT>"\n\n`,
);
}
this.lastScreenContent = normalizedScreen;
return;
}
@@ -364,8 +372,6 @@ ${strippedScreen}
// Wait a bit to ensure Ink has processed the full input
await new Promise((resolve) => setTimeout(resolve, 100));
this.lastScreenContent = normalizedScreen;
} else {
debugLogger.log('[SIMULATOR] Skipping (empty response)');
@@ -376,8 +382,6 @@ ${strippedScreen}
`[LOG] [SIMULATOR] Action History updated with: "<EMPTY>"\n\n`,
);
}
this.lastScreenContent = normalizedScreen;
}
} catch (e: unknown) {
debugLogger.error('UserSimulator tick failed', e);