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
672 lines
27 KiB
JavaScript
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');
|
|
});
|
|
});
|