Skip to content

Commit 16bf392

Browse files
committed
fix(gemini): stabilize ask reply state handling
1 parent e18e0ed commit 16bf392

File tree

5 files changed

+1614
-64
lines changed

5 files changed

+1614
-64
lines changed

src/clis/gemini/ask.test.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
import type { IPage } from '../../types.js';
3+
4+
const baseline = {
5+
turns: [{ Role: 'Assistant', Text: '旧回答' }],
6+
transcriptLines: ['baseline'],
7+
composerHasText: true,
8+
isGenerating: false,
9+
structuredTurnsTrusted: true,
10+
};
11+
12+
const submission = {
13+
snapshot: {
14+
turns: [
15+
{ Role: 'Assistant', Text: '旧回答' },
16+
{ Role: 'User', Text: '请只回复:OK' },
17+
],
18+
transcriptLines: ['baseline', '请只回复:OK'],
19+
composerHasText: false,
20+
isGenerating: true,
21+
structuredTurnsTrusted: true,
22+
},
23+
preSendAssistantCount: 1,
24+
userAnchorTurn: { Role: 'User', Text: '请只回复:OK' },
25+
reason: 'user_turn' as const,
26+
};
27+
28+
const mocks = vi.hoisted(() => ({
29+
readGeminiSnapshot: vi.fn(),
30+
sendGeminiMessage: vi.fn(),
31+
startNewGeminiChat: vi.fn(),
32+
waitForGeminiSubmission: vi.fn(),
33+
waitForGeminiResponse: vi.fn(),
34+
}));
35+
36+
vi.mock('./utils.js', async () => {
37+
const actual = await vi.importActual<typeof import('./utils.js')>('./utils.js');
38+
return {
39+
...actual,
40+
readGeminiSnapshot: mocks.readGeminiSnapshot,
41+
sendGeminiMessage: mocks.sendGeminiMessage,
42+
startNewGeminiChat: mocks.startNewGeminiChat,
43+
waitForGeminiSubmission: mocks.waitForGeminiSubmission,
44+
waitForGeminiResponse: mocks.waitForGeminiResponse,
45+
};
46+
});
47+
48+
import { askCommand } from './ask.js';
49+
50+
function createPageMock(): IPage {
51+
return {
52+
goto: vi.fn().mockResolvedValue(undefined),
53+
evaluate: vi.fn(),
54+
getCookies: vi.fn().mockResolvedValue([]),
55+
snapshot: vi.fn().mockResolvedValue(undefined),
56+
click: vi.fn().mockResolvedValue(undefined),
57+
typeText: vi.fn().mockResolvedValue(undefined),
58+
pressKey: vi.fn().mockResolvedValue(undefined),
59+
scrollTo: vi.fn().mockResolvedValue(undefined),
60+
getFormState: vi.fn().mockResolvedValue({}),
61+
wait: vi.fn().mockResolvedValue(undefined),
62+
tabs: vi.fn().mockResolvedValue([]),
63+
selectTab: vi.fn().mockResolvedValue(undefined),
64+
networkRequests: vi.fn().mockResolvedValue([]),
65+
consoleMessages: vi.fn().mockResolvedValue([]),
66+
scroll: vi.fn().mockResolvedValue(undefined),
67+
autoScroll: vi.fn().mockResolvedValue(undefined),
68+
installInterceptor: vi.fn().mockResolvedValue(undefined),
69+
getInterceptedRequests: vi.fn().mockResolvedValue([]),
70+
waitForCapture: vi.fn().mockResolvedValue(undefined),
71+
screenshot: vi.fn().mockResolvedValue(''),
72+
nativeType: vi.fn().mockResolvedValue(undefined),
73+
nativeKeyPress: vi.fn().mockResolvedValue(undefined),
74+
} as unknown as IPage;
75+
}
76+
77+
describe('gemini ask orchestration', () => {
78+
beforeEach(() => {
79+
vi.clearAllMocks();
80+
});
81+
82+
it('captures baseline, sends, waits for confirmed submission, then waits with the remaining timeout', async () => {
83+
vi.spyOn(Date, 'now')
84+
.mockReturnValueOnce(0)
85+
.mockReturnValueOnce(2000);
86+
87+
const page = createPageMock();
88+
mocks.readGeminiSnapshot.mockResolvedValueOnce(baseline);
89+
mocks.sendGeminiMessage.mockResolvedValueOnce('button');
90+
mocks.waitForGeminiSubmission.mockResolvedValueOnce(submission);
91+
mocks.waitForGeminiResponse.mockResolvedValueOnce('OK');
92+
93+
const result = await askCommand.func!(page, { prompt: '请只回复:OK', timeout: '20', new: 'false' });
94+
95+
expect(mocks.readGeminiSnapshot).toHaveBeenCalledWith(page);
96+
expect(mocks.waitForGeminiSubmission).toHaveBeenCalledWith(page, baseline, 20);
97+
expect(mocks.waitForGeminiResponse).toHaveBeenCalledWith(page, submission, '请只回复:OK', 18);
98+
expect(result).toEqual([{ response: '💬 OK' }]);
99+
});
100+
101+
it('does not spend extra response wait time after submission has already consumed the full timeout budget', async () => {
102+
vi.spyOn(Date, 'now')
103+
.mockReturnValueOnce(0)
104+
.mockReturnValueOnce(20000);
105+
106+
const page = createPageMock();
107+
mocks.readGeminiSnapshot.mockResolvedValueOnce(baseline);
108+
mocks.sendGeminiMessage.mockResolvedValueOnce('button');
109+
mocks.waitForGeminiSubmission.mockResolvedValueOnce(submission);
110+
mocks.waitForGeminiResponse.mockResolvedValueOnce('');
111+
112+
await askCommand.func!(page, { prompt: '请只回复:OK', timeout: '20', new: 'false' });
113+
114+
expect(mocks.waitForGeminiResponse).toHaveBeenCalledWith(page, submission, '请只回复:OK', 0);
115+
});
116+
});

src/clis/gemini/ask.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { cli, Strategy } from '../../registry.js';
22
import type { IPage } from '../../types.js';
3-
import { GEMINI_DOMAIN, getGeminiTranscriptLines, sendGeminiMessage, startNewGeminiChat, waitForGeminiResponse } from './utils.js';
3+
import { GEMINI_DOMAIN, readGeminiSnapshot, sendGeminiMessage, startNewGeminiChat, waitForGeminiResponse, waitForGeminiSubmission } from './utils.js';
44

55
function normalizeBooleanFlag(value: unknown): boolean {
66
if (typeof value === 'boolean') return value;
@@ -33,9 +33,16 @@ export const askCommand = cli({
3333

3434
if (startFresh) await startNewGeminiChat(page);
3535

36-
const beforeLines = await getGeminiTranscriptLines(page);
36+
const before = await readGeminiSnapshot(page);
3737
await sendGeminiMessage(page, prompt);
38-
const response = await waitForGeminiResponse(page, beforeLines, prompt, timeout);
38+
const submissionStartedAt = Date.now();
39+
const submitted = await waitForGeminiSubmission(page, before, timeout);
40+
if (!submitted) {
41+
return [{ response: `💬 ${NO_RESPONSE_PREFIX} No Gemini response within ${timeout}s.` }];
42+
}
43+
44+
const remainingTimeoutSeconds = Math.max(0, timeout - Math.ceil((Date.now() - submissionStartedAt) / 1000));
45+
const response = await waitForGeminiResponse(page, submitted, prompt, remainingTimeoutSeconds);
3946

4047
if (!response) {
4148
return [{ response: `💬 ${NO_RESPONSE_PREFIX} No Gemini response within ${timeout}s.` }];

0 commit comments

Comments
 (0)