9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
182 lines
11 KiB
JavaScript
182 lines
11 KiB
JavaScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
const { mockGetCurrentGeminiUrl, mockReadGeminiSnapshot, mockSelectGeminiTool, mockSendGeminiMessage, mockStartNewGeminiChat, mockWaitForGeminiSubmission, mockWaitForGeminiConfirmButton, mockGetLatestGeminiAssistantResponse, } = vi.hoisted(() => ({
|
|
mockGetCurrentGeminiUrl: vi.fn(),
|
|
mockReadGeminiSnapshot: vi.fn(),
|
|
mockSelectGeminiTool: vi.fn(),
|
|
mockSendGeminiMessage: vi.fn(),
|
|
mockStartNewGeminiChat: vi.fn(),
|
|
mockWaitForGeminiSubmission: vi.fn(),
|
|
mockWaitForGeminiConfirmButton: vi.fn(),
|
|
mockGetLatestGeminiAssistantResponse: vi.fn(),
|
|
}));
|
|
vi.mock('./utils.js', () => ({
|
|
GEMINI_DOMAIN: 'gemini.google.com',
|
|
GEMINI_APP_URL: 'https://gemini.google.com/app',
|
|
GEMINI_DEEP_RESEARCH_DEFAULT_TOOL_LABELS: ['Deep Research', 'Deep research', '\u6df1\u5ea6\u7814\u7a76'],
|
|
GEMINI_DEEP_RESEARCH_DEFAULT_CONFIRM_LABELS: [
|
|
'Start research',
|
|
'Start Research',
|
|
'Start deep research',
|
|
'Start Deep Research',
|
|
'Generate research plan',
|
|
'Generate Research Plan',
|
|
'Generate deep research plan',
|
|
'Generate Deep Research Plan',
|
|
'\u5f00\u59cb\u7814\u7a76',
|
|
'\u5f00\u59cb\u6df1\u5ea6\u7814\u7a76',
|
|
'\u5f00\u59cb\u8c03\u7814',
|
|
'\u751f\u6210\u7814\u7a76\u8ba1\u5212',
|
|
'\u751f\u6210\u8c03\u7814\u8ba1\u5212',
|
|
],
|
|
resolveGeminiLabels: (value, fallback) => {
|
|
const label = String(value ?? '').trim();
|
|
return label ? [label] : fallback;
|
|
},
|
|
getCurrentGeminiUrl: mockGetCurrentGeminiUrl,
|
|
getLatestGeminiAssistantResponse: mockGetLatestGeminiAssistantResponse,
|
|
readGeminiSnapshot: mockReadGeminiSnapshot,
|
|
selectGeminiTool: mockSelectGeminiTool,
|
|
sendGeminiMessage: mockSendGeminiMessage,
|
|
startNewGeminiChat: mockStartNewGeminiChat,
|
|
waitForGeminiSubmission: mockWaitForGeminiSubmission,
|
|
waitForGeminiConfirmButton: mockWaitForGeminiConfirmButton,
|
|
}));
|
|
import { deepResearchCommand } from './deep-research.js';
|
|
describe('gemini/deep-research', () => {
|
|
const page = {};
|
|
const runCommand = (kwargs) => deepResearchCommand.func(page, { timeout: 180, ...kwargs });
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockGetCurrentGeminiUrl.mockResolvedValue('https://gemini.google.com/app/chat');
|
|
mockReadGeminiSnapshot.mockResolvedValue({
|
|
turns: [],
|
|
transcriptLines: [],
|
|
composerHasText: false,
|
|
isGenerating: false,
|
|
structuredTurnsTrusted: true,
|
|
});
|
|
mockSelectGeminiTool.mockResolvedValue('Deep Research');
|
|
mockSendGeminiMessage.mockResolvedValue();
|
|
mockStartNewGeminiChat.mockResolvedValue();
|
|
mockWaitForGeminiSubmission.mockResolvedValue({
|
|
snapshot: { turns: [], transcriptLines: [], composerHasText: false, isGenerating: false, structuredTurnsTrusted: true },
|
|
preSendAssistantCount: 0,
|
|
userAnchorTurn: null,
|
|
reason: 'user_turn',
|
|
});
|
|
mockWaitForGeminiConfirmButton.mockResolvedValue('Start research');
|
|
mockGetLatestGeminiAssistantResponse.mockResolvedValue('');
|
|
});
|
|
it('starts a new chat by default, then sends prompt and confirms deep research', async () => {
|
|
const result = await runCommand({ prompt: 'research this topic' });
|
|
expect(mockStartNewGeminiChat).toHaveBeenCalledTimes(1);
|
|
expect(mockSelectGeminiTool).toHaveBeenCalledTimes(1);
|
|
expect(mockSendGeminiMessage).toHaveBeenCalledWith(page, 'research this topic');
|
|
expect(mockWaitForGeminiSubmission).toHaveBeenCalledTimes(1);
|
|
expect(mockWaitForGeminiConfirmButton).toHaveBeenCalledWith(page, expect.arrayContaining(['Start research', 'Start deep research', 'Generate research plan', '\u751f\u6210\u7814\u7a76\u8ba1\u5212']), 180);
|
|
expect(result).toEqual([{ status: 'started', url: 'https://gemini.google.com/app/chat' }]);
|
|
});
|
|
it('returns tool-not-found when the tool cannot be selected', async () => {
|
|
mockSelectGeminiTool.mockResolvedValue('');
|
|
const result = await runCommand({ prompt: 'research this topic' });
|
|
expect(result).toEqual([{ status: 'tool-not-found', url: 'https://gemini.google.com/app/chat' }]);
|
|
expect(mockSendGeminiMessage).not.toHaveBeenCalled();
|
|
expect(mockWaitForGeminiSubmission).not.toHaveBeenCalled();
|
|
expect(mockWaitForGeminiConfirmButton).not.toHaveBeenCalled();
|
|
});
|
|
it('retries send once when first submission cannot be confirmed', async () => {
|
|
mockWaitForGeminiSubmission.mockResolvedValueOnce(null).mockResolvedValueOnce({
|
|
snapshot: { turns: [], transcriptLines: [], composerHasText: false, isGenerating: false, structuredTurnsTrusted: true },
|
|
preSendAssistantCount: 0,
|
|
userAnchorTurn: null,
|
|
reason: 'user_turn',
|
|
});
|
|
const result = await runCommand({ prompt: 'research this topic' });
|
|
expect(mockSelectGeminiTool).toHaveBeenCalledTimes(2);
|
|
expect(mockReadGeminiSnapshot).toHaveBeenCalledTimes(2);
|
|
expect(mockSendGeminiMessage).toHaveBeenCalledTimes(2);
|
|
expect(mockWaitForGeminiSubmission).toHaveBeenCalledTimes(2);
|
|
expect(result).toEqual([{ status: 'started', url: 'https://gemini.google.com/app/chat' }]);
|
|
});
|
|
it('returns submit-not-found when submission cannot be confirmed after retry', async () => {
|
|
mockWaitForGeminiSubmission.mockResolvedValue(null);
|
|
const result = await runCommand({ prompt: 'research this topic' });
|
|
expect(mockSelectGeminiTool).toHaveBeenCalledTimes(2);
|
|
expect(mockSendGeminiMessage).toHaveBeenCalledTimes(2);
|
|
expect(mockWaitForGeminiConfirmButton).not.toHaveBeenCalled();
|
|
expect(result).toEqual([{ status: 'submit-not-found', url: 'https://gemini.google.com/app/chat' }]);
|
|
});
|
|
it('returns confirm-not-found when no confirm button is found', async () => {
|
|
mockWaitForGeminiConfirmButton.mockResolvedValue('');
|
|
const result = await runCommand({ prompt: 'research this topic' });
|
|
expect(result).toEqual([{ status: 'confirm-not-found', url: 'https://gemini.google.com/app/chat' }]);
|
|
});
|
|
it('returns started when confirm is missing but research appears to be running', async () => {
|
|
mockWaitForGeminiConfirmButton.mockResolvedValue('');
|
|
mockGetCurrentGeminiUrl.mockResolvedValue('https://gemini.google.com/app/abc123');
|
|
mockGetLatestGeminiAssistantResponse.mockResolvedValue('Researching websites now');
|
|
const result = await runCommand({ prompt: 'research this topic' });
|
|
expect(result).toEqual([{ status: 'started', url: 'https://gemini.google.com/app/abc123' }]);
|
|
});
|
|
it('does not treat conversation url alone as started when confirm is missing', async () => {
|
|
mockWaitForGeminiConfirmButton.mockResolvedValue('');
|
|
mockGetCurrentGeminiUrl.mockResolvedValue('https://gemini.google.com/app/abc999');
|
|
mockGetLatestGeminiAssistantResponse.mockResolvedValue('I drafted a plan. Start research');
|
|
const result = await runCommand({ prompt: 'research this topic' });
|
|
expect(result).toEqual([{ status: 'confirm-not-found', url: 'https://gemini.google.com/app/abc999' }]);
|
|
});
|
|
it('retries once when stuck on root app URL and starts successfully on second confirm', async () => {
|
|
mockWaitForGeminiConfirmButton.mockResolvedValueOnce('').mockResolvedValueOnce('Start research');
|
|
mockGetCurrentGeminiUrl.mockResolvedValueOnce('https://gemini.google.com/app').mockResolvedValueOnce('https://gemini.google.com/app/retry123');
|
|
const result = await runCommand({ prompt: 'research this topic', timeout: 20 });
|
|
expect(mockSelectGeminiTool).toHaveBeenCalledTimes(2);
|
|
expect(mockSendGeminiMessage).toHaveBeenCalledTimes(1);
|
|
expect(mockWaitForGeminiConfirmButton).toHaveBeenCalledTimes(2);
|
|
expect(result).toEqual([{ status: 'started', url: 'https://gemini.google.com/app/retry123' }]);
|
|
});
|
|
it('treats root-url confirm as false-positive and retries', async () => {
|
|
mockWaitForGeminiConfirmButton.mockResolvedValueOnce('Start research').mockResolvedValueOnce('Start research');
|
|
mockGetCurrentGeminiUrl.mockResolvedValueOnce('https://gemini.google.com/app').mockResolvedValueOnce('https://gemini.google.com/app/retry456');
|
|
const result = await runCommand({ prompt: 'research this topic', timeout: 20 });
|
|
expect(mockSelectGeminiTool).toHaveBeenCalledTimes(2);
|
|
expect(mockSendGeminiMessage).toHaveBeenCalledTimes(1);
|
|
expect(result).toEqual([{ status: 'started', url: 'https://gemini.google.com/app/retry456' }]);
|
|
});
|
|
it('does not resend prompt during root-url retry to avoid duplicate chats', async () => {
|
|
mockWaitForGeminiConfirmButton.mockResolvedValueOnce('Start research').mockResolvedValueOnce('');
|
|
mockGetCurrentGeminiUrl.mockResolvedValueOnce('https://gemini.google.com/app').mockResolvedValueOnce('https://gemini.google.com/app');
|
|
mockGetLatestGeminiAssistantResponse.mockResolvedValue('');
|
|
const result = await runCommand({ prompt: 'research this topic', timeout: 20 });
|
|
expect(mockSelectGeminiTool).toHaveBeenCalledTimes(2);
|
|
expect(mockSendGeminiMessage).toHaveBeenCalledTimes(1);
|
|
expect(mockWaitForGeminiConfirmButton).toHaveBeenCalledTimes(2);
|
|
expect(result).toEqual([{ status: 'confirm-not-found', url: 'https://gemini.google.com/app' }]);
|
|
});
|
|
it('attempts one more confirm click when still waiting for start research', async () => {
|
|
mockWaitForGeminiConfirmButton
|
|
.mockResolvedValueOnce('')
|
|
.mockResolvedValueOnce('Start research');
|
|
mockGetCurrentGeminiUrl
|
|
.mockResolvedValueOnce('https://gemini.google.com/app/xyz123')
|
|
.mockResolvedValueOnce('https://gemini.google.com/app/xyz123');
|
|
mockGetLatestGeminiAssistantResponse
|
|
.mockResolvedValueOnce('I drafted a plan. Start research')
|
|
.mockResolvedValueOnce('Researching websites now');
|
|
const result = await runCommand({ prompt: 'research this topic', timeout: 20 });
|
|
expect(mockWaitForGeminiConfirmButton).toHaveBeenCalledTimes(2);
|
|
expect(mockWaitForGeminiConfirmButton).toHaveBeenNthCalledWith(2, page, expect.arrayContaining(['Start research', 'Start deep research', '开始研究', '开始深度研究']), 8);
|
|
expect(mockSendGeminiMessage).toHaveBeenCalledTimes(1);
|
|
expect(result).toEqual([{ status: 'started', url: 'https://gemini.google.com/app/xyz123' }]);
|
|
});
|
|
it('uses custom tool/confirm labels when provided', async () => {
|
|
await runCommand({
|
|
prompt: 'research this topic',
|
|
tool: 'Custom Tool',
|
|
confirm: 'Custom Confirm',
|
|
timeout: 42,
|
|
});
|
|
expect(mockSelectGeminiTool).toHaveBeenCalledWith(page, ['Custom Tool']);
|
|
expect(mockWaitForGeminiConfirmButton).toHaveBeenCalledWith(page, ['Custom Confirm'], 42);
|
|
});
|
|
});
|