Files
wehub-resource-sync 9b395f5cc3
Build Chrome Extension / build (push) Waiting to run
Trigger Website Rebuild (Docs Updated) / dispatch (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

672 lines
27 KiB
JavaScript

import { describe, expect, it, vi } from 'vitest';
import { __test__, waitForGeminiResponse, waitForGeminiSubmission } from './utils.js';
function snapshot(overrides = {}) {
return {
turns: [],
transcriptLines: [],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: true,
...overrides,
};
}
function createPageMock() {
return {
goto: vi.fn().mockResolvedValue(undefined),
evaluate: vi.fn(),
getCookies: vi.fn().mockResolvedValue([]),
snapshot: vi.fn().mockResolvedValue(undefined),
click: vi.fn().mockResolvedValue(undefined),
typeText: vi.fn().mockResolvedValue(undefined),
pressKey: vi.fn().mockResolvedValue(undefined),
scrollTo: vi.fn().mockResolvedValue(undefined),
getFormState: vi.fn().mockResolvedValue({}),
wait: vi.fn().mockResolvedValue(undefined),
tabs: vi.fn().mockResolvedValue([]),
selectTab: vi.fn().mockResolvedValue(undefined),
networkRequests: vi.fn().mockResolvedValue([]),
consoleMessages: vi.fn().mockResolvedValue([]),
scroll: vi.fn().mockResolvedValue(undefined),
autoScroll: vi.fn().mockResolvedValue(undefined),
installInterceptor: vi.fn().mockResolvedValue(undefined),
getInterceptedRequests: vi.fn().mockResolvedValue([]),
waitForCapture: vi.fn().mockResolvedValue(undefined),
screenshot: vi.fn().mockResolvedValue(''),
nativeType: vi.fn().mockResolvedValue(undefined),
nativeKeyPress: vi.fn().mockResolvedValue(undefined),
};
}
describe('Gemini snapshot diff helpers', () => {
it('reports appended trusted turns when the current snapshot extends the baseline', () => {
const before = snapshot({
turns: [{ Role: 'Assistant', Text: '旧回答' }],
});
const current = snapshot({
turns: [
{ Role: 'Assistant', Text: '旧回答' },
{ Role: 'User', Text: '请只回复:OK' },
],
});
expect(__test__.diffTrustedStructuredTurns(before, current)).toEqual({
appendedTurns: [{ Role: 'User', Text: '请只回复:OK' }],
hasTrustedAppend: true,
hasNewUserTurn: true,
hasNewAssistantTurn: false,
});
});
it('treats restored structured turns as untrusted when the pre-send snapshot had no trustworthy turns', () => {
const before = snapshot({
turns: [],
transcriptLines: ['旧问题', '旧回答'],
structuredTurnsTrusted: false,
});
const current = snapshot({
turns: [
{ Role: 'User', Text: '旧问题' },
{ Role: 'Assistant', Text: '旧回答' },
],
transcriptLines: ['旧问题', '旧回答'],
structuredTurnsTrusted: true,
});
expect(__test__.diffTrustedStructuredTurns(before, current)).toEqual({
appendedTurns: [],
hasTrustedAppend: false,
hasNewUserTurn: false,
hasNewAssistantTurn: false,
});
});
it('keeps transcript delta lines raw for later conservative fallback checks', () => {
const before = snapshot({
transcriptLines: ['baseline'],
});
const current = snapshot({
transcriptLines: ['baseline', '关于“请只回复:OK”,这里是解释。'],
structuredTurnsTrusted: false,
});
expect(__test__.diffTranscriptLines(before, current)).toEqual([
'关于“请只回复:OK”,这里是解释。',
]);
});
});
describe('Gemini submission state', () => {
it('confirms submission from a trusted appended user turn', async () => {
const page = createPageMock();
const evaluate = vi.mocked(page.evaluate);
evaluate
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [
{ Role: 'Assistant', Text: '旧回答' },
{ Role: 'User', Text: '请只回复:OK' },
],
transcriptLines: ['baseline', '请只回复:OK'],
composerHasText: false,
isGenerating: true,
structuredTurnsTrusted: true,
});
const result = await waitForGeminiSubmission(page, snapshot({
turns: [{ Role: 'Assistant', Text: '旧回答' }],
transcriptLines: ['baseline'],
composerHasText: true,
structuredTurnsTrusted: true,
}), 4);
expect(result).toEqual({
snapshot: {
turns: [
{ Role: 'Assistant', Text: '旧回答' },
{ Role: 'User', Text: '请只回复:OK' },
],
transcriptLines: ['baseline', '请只回复:OK'],
composerHasText: false,
isGenerating: true,
structuredTurnsTrusted: true,
},
preSendAssistantCount: 1,
userAnchorTurn: { Role: 'User', Text: '请只回复:OK' },
reason: 'user_turn',
});
});
it('confirms submission from composer cleared plus generating even when transcript has not changed yet', async () => {
const page = createPageMock();
const evaluate = vi.mocked(page.evaluate);
evaluate
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [],
transcriptLines: ['baseline'],
composerHasText: false,
isGenerating: true,
structuredTurnsTrusted: false,
});
const result = await waitForGeminiSubmission(page, snapshot({
transcriptLines: ['baseline'],
composerHasText: true,
structuredTurnsTrusted: false,
}), 2);
expect(result).toEqual({
snapshot: {
turns: [],
transcriptLines: ['baseline'],
composerHasText: false,
isGenerating: true,
structuredTurnsTrusted: false,
},
preSendAssistantCount: 0,
userAnchorTurn: null,
reason: 'composer_generating',
});
});
it('confirms submission from generating state even when the pre-send baseline composer was empty', async () => {
const page = createPageMock();
const evaluate = vi.mocked(page.evaluate);
evaluate
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [
{ Role: 'User', Text: '你说\n\n请只回复:DBG2' },
{ Role: 'User', Text: '请只回复:DBG2' },
],
transcriptLines: ['baseline', '请只回复:DBG2'],
composerHasText: false,
isGenerating: true,
structuredTurnsTrusted: true,
});
const result = await waitForGeminiSubmission(page, snapshot({
turns: [{ Role: 'Assistant', Text: '需要我为你做些什么?' }],
transcriptLines: ['baseline'],
composerHasText: false,
structuredTurnsTrusted: true,
}), 2);
expect(result).toEqual({
snapshot: {
turns: [
{ Role: 'User', Text: '你说\n\n请只回复:DBG2' },
{ Role: 'User', Text: '请只回复:DBG2' },
],
transcriptLines: ['baseline', '请只回复:DBG2'],
composerHasText: false,
isGenerating: true,
structuredTurnsTrusted: true,
},
preSendAssistantCount: 1,
userAnchorTurn: { Role: 'User', Text: '请只回复:DBG2' },
reason: 'composer_generating',
});
});
it('confirms submission from composer cleared plus transcript growth when generation state is unavailable', async () => {
const page = createPageMock();
const evaluate = vi.mocked(page.evaluate);
// This transcript delta may be only a prompt echo. It is allowed to confirm
// submission only because the composer has already cleared, and it must never
// be reused later as reply ownership evidence.
evaluate
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [],
transcriptLines: ['baseline', '请只回复:OK'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: false,
});
const result = await waitForGeminiSubmission(page, snapshot({
transcriptLines: ['baseline'],
composerHasText: true,
structuredTurnsTrusted: false,
}), 2);
expect(result).toEqual({
snapshot: {
turns: [],
transcriptLines: ['baseline', '请只回复:OK'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: false,
},
preSendAssistantCount: 0,
userAnchorTurn: null,
reason: 'composer_transcript',
});
});
it('does not confirm submission when old structured turns only reappear after an untrusted pre-send snapshot', async () => {
const page = createPageMock();
const evaluate = vi.mocked(page.evaluate);
evaluate
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [
{ Role: 'User', Text: '旧问题' },
{ Role: 'Assistant', Text: '旧回答' },
],
transcriptLines: ['旧问题', '旧回答'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: true,
})
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [
{ Role: 'User', Text: '旧问题' },
{ Role: 'Assistant', Text: '旧回答' },
],
transcriptLines: ['旧问题', '旧回答'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: true,
});
const result = await waitForGeminiSubmission(page, snapshot({
turns: [],
transcriptLines: ['旧问题', '旧回答'],
composerHasText: true,
structuredTurnsTrusted: false,
}), 2);
expect(result).toBeNull();
});
it('does not confirm submission from transcript growth alone when the composer never clears', async () => {
const page = createPageMock();
const evaluate = vi.mocked(page.evaluate);
evaluate
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [],
transcriptLines: ['baseline', '请只回复:OK'],
composerHasText: true,
isGenerating: false,
structuredTurnsTrusted: false,
})
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [],
transcriptLines: ['baseline', '请只回复:OK'],
composerHasText: true,
isGenerating: false,
structuredTurnsTrusted: false,
});
const result = await waitForGeminiSubmission(page, snapshot({
transcriptLines: ['baseline'],
composerHasText: true,
structuredTurnsTrusted: false,
}), 2);
expect(result).toBeNull();
});
it('does not confirm transcript-only submission while url remains /app root', async () => {
const page = createPageMock();
const evaluate = vi.mocked(page.evaluate);
evaluate
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
url: 'https://gemini.google.com/app',
turns: [],
transcriptLines: ['baseline', 'prompt'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: false,
})
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
url: 'https://gemini.google.com/app',
turns: [],
transcriptLines: ['baseline', 'prompt'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: false,
});
const result = await waitForGeminiSubmission(page, snapshot({
url: 'https://gemini.google.com/app',
transcriptLines: ['baseline'],
composerHasText: true,
structuredTurnsTrusted: false,
}), 2);
expect(result).toBeNull();
});
it('keeps polling past ten seconds when the overall timeout budget still allows submission confirmation', async () => {
const page = createPageMock();
const evaluate = vi.mocked(page.evaluate);
for (let index = 0; index < 10; index += 1) {
evaluate
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [],
transcriptLines: ['baseline'],
composerHasText: true,
isGenerating: false,
structuredTurnsTrusted: false,
});
}
evaluate
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [],
transcriptLines: ['baseline', '请只回复:OK'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: false,
});
const result = await waitForGeminiSubmission(page, snapshot({
transcriptLines: ['baseline'],
composerHasText: true,
structuredTurnsTrusted: false,
}), 12);
expect(result?.reason).toBe('composer_transcript');
});
});
describe('Gemini reply state', () => {
it('does not reuse an older identical reply when the submission baseline has no structured user anchor', async () => {
const page = createPageMock();
const evaluate = vi.mocked(page.evaluate);
evaluate
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [{ Role: 'Assistant', Text: 'OK' }],
transcriptLines: ['baseline', 'OK'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: true,
})
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [
{ Role: 'Assistant', Text: 'OK' },
{ Role: 'Assistant', Text: 'OK' },
],
transcriptLines: ['baseline', 'OK'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: true,
})
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [
{ Role: 'Assistant', Text: 'OK' },
{ Role: 'Assistant', Text: 'OK' },
],
transcriptLines: ['baseline', 'OK'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: true,
});
const result = await waitForGeminiResponse(page, {
snapshot: snapshot({
turns: [{ Role: 'Assistant', Text: 'OK' }],
transcriptLines: ['baseline', 'OK'],
composerHasText: false,
isGenerating: true,
structuredTurnsTrusted: true,
}),
preSendAssistantCount: 1,
userAnchorTurn: null,
reason: 'composer_generating',
}, '请只回复:OK', 6);
expect(result).toBe('OK');
});
it('does not treat prepended older history as the current round reply when reply ownership has no user anchor', async () => {
const page = createPageMock();
const evaluate = vi.mocked(page.evaluate);
evaluate
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [
{ Role: 'Assistant', Text: '更早的问题' },
{ Role: 'Assistant', Text: '旧回答' },
],
transcriptLines: ['baseline'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: true,
})
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [
{ Role: 'Assistant', Text: '更早的问题' },
{ Role: 'Assistant', Text: '旧回答' },
],
transcriptLines: ['baseline'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: true,
})
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [
{ Role: 'Assistant', Text: '更早的问题' },
{ Role: 'Assistant', Text: '旧回答' },
],
transcriptLines: ['baseline'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: true,
});
const result = await waitForGeminiResponse(page, {
snapshot: snapshot({
turns: [{ Role: 'Assistant', Text: '旧回答' }],
transcriptLines: ['baseline'],
composerHasText: false,
isGenerating: true,
structuredTurnsTrusted: true,
}),
preSendAssistantCount: 1,
userAnchorTurn: null,
reason: 'composer_generating',
}, '请只回复:OK', 6);
expect(result).toBe('');
});
it('accepts a reply when the submission snapshot contains only the current round user turns and later appends a new assistant', async () => {
const page = createPageMock();
const evaluate = vi.mocked(page.evaluate);
evaluate
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [
{ Role: 'User', Text: '你说\n\n请只回复:DBGREG' },
{ Role: 'User', Text: '请只回复:DBGREG' },
{ Role: 'Assistant', Text: 'DBGREG' },
],
transcriptLines: ['baseline'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: true,
})
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [
{ Role: 'User', Text: '你说\n\n请只回复:DBGREG' },
{ Role: 'User', Text: '请只回复:DBGREG' },
{ Role: 'Assistant', Text: 'DBGREG' },
],
transcriptLines: ['baseline'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: true,
});
const result = await waitForGeminiResponse(page, {
snapshot: snapshot({
turns: [
{ Role: 'User', Text: '你说\n\n请只回复:DBGREG' },
{ Role: 'User', Text: '请只回复:DBGREG' },
],
transcriptLines: ['baseline'],
composerHasText: false,
isGenerating: true,
structuredTurnsTrusted: true,
}),
preSendAssistantCount: 1,
userAnchorTurn: { Role: 'User', Text: '请只回复:DBGREG' },
reason: 'composer_generating',
}, '请只回复:DBGREG', 6);
expect(result).toBe('DBGREG');
});
it('does not trust an assistant-only submission snapshot without a stable post-submission owner', async () => {
const page = createPageMock();
const evaluate = vi.mocked(page.evaluate);
evaluate
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [{ Role: 'Assistant', Text: '完整回答' }],
transcriptLines: ['baseline'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: true,
})
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [{ Role: 'Assistant', Text: '完整回答' }],
transcriptLines: ['baseline'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: true,
});
const result = await waitForGeminiResponse(page, {
snapshot: snapshot({
turns: [{ Role: 'Assistant', Text: '半截回答' }],
transcriptLines: ['baseline'],
composerHasText: false,
isGenerating: true,
structuredTurnsTrusted: true,
}),
preSendAssistantCount: 0,
userAnchorTurn: null,
reason: 'composer_generating',
}, '请解释', 4);
expect(result).toBe('');
});
it('accepts an assistant reply that appears after a structured user anchor only after it stabilizes and generation stops', async () => {
const page = createPageMock();
const evaluate = vi.mocked(page.evaluate);
evaluate
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [
{ Role: 'Assistant', Text: '旧回答' },
{ Role: 'User', Text: '请解释' },
{ Role: 'Assistant', Text: '半截回答' },
],
transcriptLines: ['baseline'],
composerHasText: false,
isGenerating: true,
structuredTurnsTrusted: true,
})
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [
{ Role: 'Assistant', Text: '旧回答' },
{ Role: 'User', Text: '请解释' },
{ Role: 'Assistant', Text: '完整回答' },
],
transcriptLines: ['baseline'],
composerHasText: false,
isGenerating: true,
structuredTurnsTrusted: true,
})
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [
{ Role: 'Assistant', Text: '旧回答' },
{ Role: 'User', Text: '请解释' },
{ Role: 'Assistant', Text: '完整回答' },
],
transcriptLines: ['baseline'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: true,
});
const result = await waitForGeminiResponse(page, {
snapshot: snapshot({
turns: [
{ Role: 'Assistant', Text: '旧回答' },
{ Role: 'User', Text: '请解释' },
],
transcriptLines: ['baseline'],
composerHasText: false,
isGenerating: true,
structuredTurnsTrusted: true,
}),
preSendAssistantCount: 1,
userAnchorTurn: { Role: 'User', Text: '请解释' },
reason: 'user_turn',
}, '请解释', 6);
expect(result).toBe('完整回答');
});
it('uses transcript fallback only after two identical post-submission deltas and after generation stops', async () => {
const page = createPageMock();
const evaluate = vi.mocked(page.evaluate);
evaluate
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [{ Role: 'User', Text: '请只回复:OK' }],
transcriptLines: ['baseline', 'OK'],
composerHasText: false,
isGenerating: true,
structuredTurnsTrusted: true,
})
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [{ Role: 'User', Text: '请只回复:OK' }],
transcriptLines: ['baseline', 'OK'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: true,
})
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [{ Role: 'User', Text: '请只回复:OK' }],
transcriptLines: ['baseline', 'OK'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: true,
});
const result = await waitForGeminiResponse(page, {
snapshot: snapshot({
turns: [{ Role: 'User', Text: '请只回复:OK' }],
transcriptLines: ['baseline'],
composerHasText: false,
isGenerating: true,
structuredTurnsTrusted: true,
}),
preSendAssistantCount: 0,
userAnchorTurn: { Role: 'User', Text: '请只回复:OK' },
reason: 'user_turn',
}, '请只回复:OK', 6);
expect(result).toBe('OK');
});
it('ignores transcript lines that appeared before submission confirmation and only accepts post-submission transcript deltas', async () => {
const page = createPageMock();
const evaluate = vi.mocked(page.evaluate);
evaluate
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [{ Role: 'User', Text: '请只回复:OK' }],
transcriptLines: ['baseline', '早到的提示词回声', 'OK'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: true,
})
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [{ Role: 'User', Text: '请只回复:OK' }],
transcriptLines: ['baseline', '早到的提示词回声', 'OK'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: true,
})
.mockResolvedValueOnce('https://gemini.google.com/app')
.mockResolvedValueOnce({
turns: [{ Role: 'User', Text: '请只回复:OK' }],
transcriptLines: ['baseline', '早到的提示词回声', 'OK'],
composerHasText: false,
isGenerating: false,
structuredTurnsTrusted: true,
});
const result = await waitForGeminiResponse(page, {
snapshot: snapshot({
turns: [{ Role: 'User', Text: '请只回复:OK' }],
transcriptLines: ['baseline', '早到的提示词回声'],
composerHasText: false,
isGenerating: true,
structuredTurnsTrusted: true,
}),
preSendAssistantCount: 0,
userAnchorTurn: { Role: 'User', Text: '请只回复:OK' },
reason: 'composer_transcript',
}, '请只回复:OK', 6);
expect(result).toBe('OK');
});
});