mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-21 09:13:05 -07:00
feat(cli): fix UserSimulator deadlock on static screens by tracking composite state
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user