reply-state.test.js
1 import { describe, expect, it, vi } from 'vitest'; 2 import { __test__, waitForGeminiResponse, waitForGeminiSubmission } from './utils.js'; 3 function snapshot(overrides = {}) { 4 return { 5 turns: [], 6 transcriptLines: [], 7 composerHasText: false, 8 isGenerating: false, 9 structuredTurnsTrusted: true, 10 ...overrides, 11 }; 12 } 13 function createPageMock() { 14 return { 15 goto: vi.fn().mockResolvedValue(undefined), 16 evaluate: vi.fn(), 17 getCookies: vi.fn().mockResolvedValue([]), 18 snapshot: vi.fn().mockResolvedValue(undefined), 19 click: vi.fn().mockResolvedValue(undefined), 20 typeText: vi.fn().mockResolvedValue(undefined), 21 pressKey: vi.fn().mockResolvedValue(undefined), 22 scrollTo: vi.fn().mockResolvedValue(undefined), 23 getFormState: vi.fn().mockResolvedValue({}), 24 wait: vi.fn().mockResolvedValue(undefined), 25 tabs: vi.fn().mockResolvedValue([]), 26 selectTab: vi.fn().mockResolvedValue(undefined), 27 networkRequests: vi.fn().mockResolvedValue([]), 28 consoleMessages: vi.fn().mockResolvedValue([]), 29 scroll: vi.fn().mockResolvedValue(undefined), 30 autoScroll: vi.fn().mockResolvedValue(undefined), 31 installInterceptor: vi.fn().mockResolvedValue(undefined), 32 getInterceptedRequests: vi.fn().mockResolvedValue([]), 33 waitForCapture: vi.fn().mockResolvedValue(undefined), 34 screenshot: vi.fn().mockResolvedValue(''), 35 nativeType: vi.fn().mockResolvedValue(undefined), 36 nativeKeyPress: vi.fn().mockResolvedValue(undefined), 37 }; 38 } 39 describe('Gemini snapshot diff helpers', () => { 40 it('reports appended trusted turns when the current snapshot extends the baseline', () => { 41 const before = snapshot({ 42 turns: [{ Role: 'Assistant', Text: '旧回答' }], 43 }); 44 const current = snapshot({ 45 turns: [ 46 { Role: 'Assistant', Text: '旧回答' }, 47 { Role: 'User', Text: '请只回复:OK' }, 48 ], 49 }); 50 expect(__test__.diffTrustedStructuredTurns(before, current)).toEqual({ 51 appendedTurns: [{ Role: 'User', Text: '请只回复:OK' }], 52 hasTrustedAppend: true, 53 hasNewUserTurn: true, 54 hasNewAssistantTurn: false, 55 }); 56 }); 57 it('treats restored structured turns as untrusted when the pre-send snapshot had no trustworthy turns', () => { 58 const before = snapshot({ 59 turns: [], 60 transcriptLines: ['旧问题', '旧回答'], 61 structuredTurnsTrusted: false, 62 }); 63 const current = snapshot({ 64 turns: [ 65 { Role: 'User', Text: '旧问题' }, 66 { Role: 'Assistant', Text: '旧回答' }, 67 ], 68 transcriptLines: ['旧问题', '旧回答'], 69 structuredTurnsTrusted: true, 70 }); 71 expect(__test__.diffTrustedStructuredTurns(before, current)).toEqual({ 72 appendedTurns: [], 73 hasTrustedAppend: false, 74 hasNewUserTurn: false, 75 hasNewAssistantTurn: false, 76 }); 77 }); 78 it('keeps transcript delta lines raw for later conservative fallback checks', () => { 79 const before = snapshot({ 80 transcriptLines: ['baseline'], 81 }); 82 const current = snapshot({ 83 transcriptLines: ['baseline', '关于“请只回复:OK”,这里是解释。'], 84 structuredTurnsTrusted: false, 85 }); 86 expect(__test__.diffTranscriptLines(before, current)).toEqual([ 87 '关于“请只回复:OK”,这里是解释。', 88 ]); 89 }); 90 }); 91 describe('Gemini submission state', () => { 92 it('confirms submission from a trusted appended user turn', async () => { 93 const page = createPageMock(); 94 const evaluate = vi.mocked(page.evaluate); 95 evaluate 96 .mockResolvedValueOnce('https://gemini.google.com/app') 97 .mockResolvedValueOnce({ 98 turns: [ 99 { Role: 'Assistant', Text: '旧回答' }, 100 { Role: 'User', Text: '请只回复:OK' }, 101 ], 102 transcriptLines: ['baseline', '请只回复:OK'], 103 composerHasText: false, 104 isGenerating: true, 105 structuredTurnsTrusted: true, 106 }); 107 const result = await waitForGeminiSubmission(page, snapshot({ 108 turns: [{ Role: 'Assistant', Text: '旧回答' }], 109 transcriptLines: ['baseline'], 110 composerHasText: true, 111 structuredTurnsTrusted: true, 112 }), 4); 113 expect(result).toEqual({ 114 snapshot: { 115 turns: [ 116 { Role: 'Assistant', Text: '旧回答' }, 117 { Role: 'User', Text: '请只回复:OK' }, 118 ], 119 transcriptLines: ['baseline', '请只回复:OK'], 120 composerHasText: false, 121 isGenerating: true, 122 structuredTurnsTrusted: true, 123 }, 124 preSendAssistantCount: 1, 125 userAnchorTurn: { Role: 'User', Text: '请只回复:OK' }, 126 reason: 'user_turn', 127 }); 128 }); 129 it('confirms submission from composer cleared plus generating even when transcript has not changed yet', async () => { 130 const page = createPageMock(); 131 const evaluate = vi.mocked(page.evaluate); 132 evaluate 133 .mockResolvedValueOnce('https://gemini.google.com/app') 134 .mockResolvedValueOnce({ 135 turns: [], 136 transcriptLines: ['baseline'], 137 composerHasText: false, 138 isGenerating: true, 139 structuredTurnsTrusted: false, 140 }); 141 const result = await waitForGeminiSubmission(page, snapshot({ 142 transcriptLines: ['baseline'], 143 composerHasText: true, 144 structuredTurnsTrusted: false, 145 }), 2); 146 expect(result).toEqual({ 147 snapshot: { 148 turns: [], 149 transcriptLines: ['baseline'], 150 composerHasText: false, 151 isGenerating: true, 152 structuredTurnsTrusted: false, 153 }, 154 preSendAssistantCount: 0, 155 userAnchorTurn: null, 156 reason: 'composer_generating', 157 }); 158 }); 159 it('confirms submission from generating state even when the pre-send baseline composer was empty', async () => { 160 const page = createPageMock(); 161 const evaluate = vi.mocked(page.evaluate); 162 evaluate 163 .mockResolvedValueOnce('https://gemini.google.com/app') 164 .mockResolvedValueOnce({ 165 turns: [ 166 { Role: 'User', Text: '你说\n\n请只回复:DBG2' }, 167 { Role: 'User', Text: '请只回复:DBG2' }, 168 ], 169 transcriptLines: ['baseline', '请只回复:DBG2'], 170 composerHasText: false, 171 isGenerating: true, 172 structuredTurnsTrusted: true, 173 }); 174 const result = await waitForGeminiSubmission(page, snapshot({ 175 turns: [{ Role: 'Assistant', Text: '需要我为你做些什么?' }], 176 transcriptLines: ['baseline'], 177 composerHasText: false, 178 structuredTurnsTrusted: true, 179 }), 2); 180 expect(result).toEqual({ 181 snapshot: { 182 turns: [ 183 { Role: 'User', Text: '你说\n\n请只回复:DBG2' }, 184 { Role: 'User', Text: '请只回复:DBG2' }, 185 ], 186 transcriptLines: ['baseline', '请只回复:DBG2'], 187 composerHasText: false, 188 isGenerating: true, 189 structuredTurnsTrusted: true, 190 }, 191 preSendAssistantCount: 1, 192 userAnchorTurn: { Role: 'User', Text: '请只回复:DBG2' }, 193 reason: 'composer_generating', 194 }); 195 }); 196 it('confirms submission from composer cleared plus transcript growth when generation state is unavailable', async () => { 197 const page = createPageMock(); 198 const evaluate = vi.mocked(page.evaluate); 199 // This transcript delta may be only a prompt echo. It is allowed to confirm 200 // submission only because the composer has already cleared, and it must never 201 // be reused later as reply ownership evidence. 202 evaluate 203 .mockResolvedValueOnce('https://gemini.google.com/app') 204 .mockResolvedValueOnce({ 205 turns: [], 206 transcriptLines: ['baseline', '请只回复:OK'], 207 composerHasText: false, 208 isGenerating: false, 209 structuredTurnsTrusted: false, 210 }); 211 const result = await waitForGeminiSubmission(page, snapshot({ 212 transcriptLines: ['baseline'], 213 composerHasText: true, 214 structuredTurnsTrusted: false, 215 }), 2); 216 expect(result).toEqual({ 217 snapshot: { 218 turns: [], 219 transcriptLines: ['baseline', '请只回复:OK'], 220 composerHasText: false, 221 isGenerating: false, 222 structuredTurnsTrusted: false, 223 }, 224 preSendAssistantCount: 0, 225 userAnchorTurn: null, 226 reason: 'composer_transcript', 227 }); 228 }); 229 it('does not confirm submission when old structured turns only reappear after an untrusted pre-send snapshot', async () => { 230 const page = createPageMock(); 231 const evaluate = vi.mocked(page.evaluate); 232 evaluate 233 .mockResolvedValueOnce('https://gemini.google.com/app') 234 .mockResolvedValueOnce({ 235 turns: [ 236 { Role: 'User', Text: '旧问题' }, 237 { Role: 'Assistant', Text: '旧回答' }, 238 ], 239 transcriptLines: ['旧问题', '旧回答'], 240 composerHasText: false, 241 isGenerating: false, 242 structuredTurnsTrusted: true, 243 }) 244 .mockResolvedValueOnce('https://gemini.google.com/app') 245 .mockResolvedValueOnce({ 246 turns: [ 247 { Role: 'User', Text: '旧问题' }, 248 { Role: 'Assistant', Text: '旧回答' }, 249 ], 250 transcriptLines: ['旧问题', '旧回答'], 251 composerHasText: false, 252 isGenerating: false, 253 structuredTurnsTrusted: true, 254 }); 255 const result = await waitForGeminiSubmission(page, snapshot({ 256 turns: [], 257 transcriptLines: ['旧问题', '旧回答'], 258 composerHasText: true, 259 structuredTurnsTrusted: false, 260 }), 2); 261 expect(result).toBeNull(); 262 }); 263 it('does not confirm submission from transcript growth alone when the composer never clears', async () => { 264 const page = createPageMock(); 265 const evaluate = vi.mocked(page.evaluate); 266 evaluate 267 .mockResolvedValueOnce('https://gemini.google.com/app') 268 .mockResolvedValueOnce({ 269 turns: [], 270 transcriptLines: ['baseline', '请只回复:OK'], 271 composerHasText: true, 272 isGenerating: false, 273 structuredTurnsTrusted: false, 274 }) 275 .mockResolvedValueOnce('https://gemini.google.com/app') 276 .mockResolvedValueOnce({ 277 turns: [], 278 transcriptLines: ['baseline', '请只回复:OK'], 279 composerHasText: true, 280 isGenerating: false, 281 structuredTurnsTrusted: false, 282 }); 283 const result = await waitForGeminiSubmission(page, snapshot({ 284 transcriptLines: ['baseline'], 285 composerHasText: true, 286 structuredTurnsTrusted: false, 287 }), 2); 288 expect(result).toBeNull(); 289 }); 290 it('does not confirm transcript-only submission while url remains /app root', async () => { 291 const page = createPageMock(); 292 const evaluate = vi.mocked(page.evaluate); 293 evaluate 294 .mockResolvedValueOnce('https://gemini.google.com/app') 295 .mockResolvedValueOnce({ 296 url: 'https://gemini.google.com/app', 297 turns: [], 298 transcriptLines: ['baseline', 'prompt'], 299 composerHasText: false, 300 isGenerating: false, 301 structuredTurnsTrusted: false, 302 }) 303 .mockResolvedValueOnce('https://gemini.google.com/app') 304 .mockResolvedValueOnce({ 305 url: 'https://gemini.google.com/app', 306 turns: [], 307 transcriptLines: ['baseline', 'prompt'], 308 composerHasText: false, 309 isGenerating: false, 310 structuredTurnsTrusted: false, 311 }); 312 const result = await waitForGeminiSubmission(page, snapshot({ 313 url: 'https://gemini.google.com/app', 314 transcriptLines: ['baseline'], 315 composerHasText: true, 316 structuredTurnsTrusted: false, 317 }), 2); 318 expect(result).toBeNull(); 319 }); 320 it('keeps polling past ten seconds when the overall timeout budget still allows submission confirmation', async () => { 321 const page = createPageMock(); 322 const evaluate = vi.mocked(page.evaluate); 323 for (let index = 0; index < 10; index += 1) { 324 evaluate 325 .mockResolvedValueOnce('https://gemini.google.com/app') 326 .mockResolvedValueOnce({ 327 turns: [], 328 transcriptLines: ['baseline'], 329 composerHasText: true, 330 isGenerating: false, 331 structuredTurnsTrusted: false, 332 }); 333 } 334 evaluate 335 .mockResolvedValueOnce('https://gemini.google.com/app') 336 .mockResolvedValueOnce({ 337 turns: [], 338 transcriptLines: ['baseline', '请只回复:OK'], 339 composerHasText: false, 340 isGenerating: false, 341 structuredTurnsTrusted: false, 342 }); 343 const result = await waitForGeminiSubmission(page, snapshot({ 344 transcriptLines: ['baseline'], 345 composerHasText: true, 346 structuredTurnsTrusted: false, 347 }), 12); 348 expect(result?.reason).toBe('composer_transcript'); 349 }); 350 }); 351 describe('Gemini reply state', () => { 352 it('does not reuse an older identical reply when the submission baseline has no structured user anchor', async () => { 353 const page = createPageMock(); 354 const evaluate = vi.mocked(page.evaluate); 355 evaluate 356 .mockResolvedValueOnce('https://gemini.google.com/app') 357 .mockResolvedValueOnce({ 358 turns: [{ Role: 'Assistant', Text: 'OK' }], 359 transcriptLines: ['baseline', 'OK'], 360 composerHasText: false, 361 isGenerating: false, 362 structuredTurnsTrusted: true, 363 }) 364 .mockResolvedValueOnce('https://gemini.google.com/app') 365 .mockResolvedValueOnce({ 366 turns: [ 367 { Role: 'Assistant', Text: 'OK' }, 368 { Role: 'Assistant', Text: 'OK' }, 369 ], 370 transcriptLines: ['baseline', 'OK'], 371 composerHasText: false, 372 isGenerating: false, 373 structuredTurnsTrusted: true, 374 }) 375 .mockResolvedValueOnce('https://gemini.google.com/app') 376 .mockResolvedValueOnce({ 377 turns: [ 378 { Role: 'Assistant', Text: 'OK' }, 379 { Role: 'Assistant', Text: 'OK' }, 380 ], 381 transcriptLines: ['baseline', 'OK'], 382 composerHasText: false, 383 isGenerating: false, 384 structuredTurnsTrusted: true, 385 }); 386 const result = await waitForGeminiResponse(page, { 387 snapshot: snapshot({ 388 turns: [{ Role: 'Assistant', Text: 'OK' }], 389 transcriptLines: ['baseline', 'OK'], 390 composerHasText: false, 391 isGenerating: true, 392 structuredTurnsTrusted: true, 393 }), 394 preSendAssistantCount: 1, 395 userAnchorTurn: null, 396 reason: 'composer_generating', 397 }, '请只回复:OK', 6); 398 expect(result).toBe('OK'); 399 }); 400 it('does not treat prepended older history as the current round reply when reply ownership has no user anchor', async () => { 401 const page = createPageMock(); 402 const evaluate = vi.mocked(page.evaluate); 403 evaluate 404 .mockResolvedValueOnce('https://gemini.google.com/app') 405 .mockResolvedValueOnce({ 406 turns: [ 407 { Role: 'Assistant', Text: '更早的问题' }, 408 { Role: 'Assistant', Text: '旧回答' }, 409 ], 410 transcriptLines: ['baseline'], 411 composerHasText: false, 412 isGenerating: false, 413 structuredTurnsTrusted: true, 414 }) 415 .mockResolvedValueOnce('https://gemini.google.com/app') 416 .mockResolvedValueOnce({ 417 turns: [ 418 { Role: 'Assistant', Text: '更早的问题' }, 419 { Role: 'Assistant', Text: '旧回答' }, 420 ], 421 transcriptLines: ['baseline'], 422 composerHasText: false, 423 isGenerating: false, 424 structuredTurnsTrusted: true, 425 }) 426 .mockResolvedValueOnce('https://gemini.google.com/app') 427 .mockResolvedValueOnce({ 428 turns: [ 429 { Role: 'Assistant', Text: '更早的问题' }, 430 { Role: 'Assistant', Text: '旧回答' }, 431 ], 432 transcriptLines: ['baseline'], 433 composerHasText: false, 434 isGenerating: false, 435 structuredTurnsTrusted: true, 436 }); 437 const result = await waitForGeminiResponse(page, { 438 snapshot: snapshot({ 439 turns: [{ Role: 'Assistant', Text: '旧回答' }], 440 transcriptLines: ['baseline'], 441 composerHasText: false, 442 isGenerating: true, 443 structuredTurnsTrusted: true, 444 }), 445 preSendAssistantCount: 1, 446 userAnchorTurn: null, 447 reason: 'composer_generating', 448 }, '请只回复:OK', 6); 449 expect(result).toBe(''); 450 }); 451 it('accepts a reply when the submission snapshot contains only the current round user turns and later appends a new assistant', async () => { 452 const page = createPageMock(); 453 const evaluate = vi.mocked(page.evaluate); 454 evaluate 455 .mockResolvedValueOnce('https://gemini.google.com/app') 456 .mockResolvedValueOnce({ 457 turns: [ 458 { Role: 'User', Text: '你说\n\n请只回复:DBGREG' }, 459 { Role: 'User', Text: '请只回复:DBGREG' }, 460 { Role: 'Assistant', Text: 'DBGREG' }, 461 ], 462 transcriptLines: ['baseline'], 463 composerHasText: false, 464 isGenerating: false, 465 structuredTurnsTrusted: true, 466 }) 467 .mockResolvedValueOnce('https://gemini.google.com/app') 468 .mockResolvedValueOnce({ 469 turns: [ 470 { Role: 'User', Text: '你说\n\n请只回复:DBGREG' }, 471 { Role: 'User', Text: '请只回复:DBGREG' }, 472 { Role: 'Assistant', Text: 'DBGREG' }, 473 ], 474 transcriptLines: ['baseline'], 475 composerHasText: false, 476 isGenerating: false, 477 structuredTurnsTrusted: true, 478 }); 479 const result = await waitForGeminiResponse(page, { 480 snapshot: snapshot({ 481 turns: [ 482 { Role: 'User', Text: '你说\n\n请只回复:DBGREG' }, 483 { Role: 'User', Text: '请只回复:DBGREG' }, 484 ], 485 transcriptLines: ['baseline'], 486 composerHasText: false, 487 isGenerating: true, 488 structuredTurnsTrusted: true, 489 }), 490 preSendAssistantCount: 1, 491 userAnchorTurn: { Role: 'User', Text: '请只回复:DBGREG' }, 492 reason: 'composer_generating', 493 }, '请只回复:DBGREG', 6); 494 expect(result).toBe('DBGREG'); 495 }); 496 it('does not trust an assistant-only submission snapshot without a stable post-submission owner', async () => { 497 const page = createPageMock(); 498 const evaluate = vi.mocked(page.evaluate); 499 evaluate 500 .mockResolvedValueOnce('https://gemini.google.com/app') 501 .mockResolvedValueOnce({ 502 turns: [{ Role: 'Assistant', Text: '完整回答' }], 503 transcriptLines: ['baseline'], 504 composerHasText: false, 505 isGenerating: false, 506 structuredTurnsTrusted: true, 507 }) 508 .mockResolvedValueOnce('https://gemini.google.com/app') 509 .mockResolvedValueOnce({ 510 turns: [{ Role: 'Assistant', Text: '完整回答' }], 511 transcriptLines: ['baseline'], 512 composerHasText: false, 513 isGenerating: false, 514 structuredTurnsTrusted: true, 515 }); 516 const result = await waitForGeminiResponse(page, { 517 snapshot: snapshot({ 518 turns: [{ Role: 'Assistant', Text: '半截回答' }], 519 transcriptLines: ['baseline'], 520 composerHasText: false, 521 isGenerating: true, 522 structuredTurnsTrusted: true, 523 }), 524 preSendAssistantCount: 0, 525 userAnchorTurn: null, 526 reason: 'composer_generating', 527 }, '请解释', 4); 528 expect(result).toBe(''); 529 }); 530 it('accepts an assistant reply that appears after a structured user anchor only after it stabilizes and generation stops', async () => { 531 const page = createPageMock(); 532 const evaluate = vi.mocked(page.evaluate); 533 evaluate 534 .mockResolvedValueOnce('https://gemini.google.com/app') 535 .mockResolvedValueOnce({ 536 turns: [ 537 { Role: 'Assistant', Text: '旧回答' }, 538 { Role: 'User', Text: '请解释' }, 539 { Role: 'Assistant', Text: '半截回答' }, 540 ], 541 transcriptLines: ['baseline'], 542 composerHasText: false, 543 isGenerating: true, 544 structuredTurnsTrusted: true, 545 }) 546 .mockResolvedValueOnce('https://gemini.google.com/app') 547 .mockResolvedValueOnce({ 548 turns: [ 549 { Role: 'Assistant', Text: '旧回答' }, 550 { Role: 'User', Text: '请解释' }, 551 { Role: 'Assistant', Text: '完整回答' }, 552 ], 553 transcriptLines: ['baseline'], 554 composerHasText: false, 555 isGenerating: true, 556 structuredTurnsTrusted: true, 557 }) 558 .mockResolvedValueOnce('https://gemini.google.com/app') 559 .mockResolvedValueOnce({ 560 turns: [ 561 { Role: 'Assistant', Text: '旧回答' }, 562 { Role: 'User', Text: '请解释' }, 563 { Role: 'Assistant', Text: '完整回答' }, 564 ], 565 transcriptLines: ['baseline'], 566 composerHasText: false, 567 isGenerating: false, 568 structuredTurnsTrusted: true, 569 }); 570 const result = await waitForGeminiResponse(page, { 571 snapshot: snapshot({ 572 turns: [ 573 { Role: 'Assistant', Text: '旧回答' }, 574 { Role: 'User', Text: '请解释' }, 575 ], 576 transcriptLines: ['baseline'], 577 composerHasText: false, 578 isGenerating: true, 579 structuredTurnsTrusted: true, 580 }), 581 preSendAssistantCount: 1, 582 userAnchorTurn: { Role: 'User', Text: '请解释' }, 583 reason: 'user_turn', 584 }, '请解释', 6); 585 expect(result).toBe('完整回答'); 586 }); 587 it('uses transcript fallback only after two identical post-submission deltas and after generation stops', async () => { 588 const page = createPageMock(); 589 const evaluate = vi.mocked(page.evaluate); 590 evaluate 591 .mockResolvedValueOnce('https://gemini.google.com/app') 592 .mockResolvedValueOnce({ 593 turns: [{ Role: 'User', Text: '请只回复:OK' }], 594 transcriptLines: ['baseline', 'OK'], 595 composerHasText: false, 596 isGenerating: true, 597 structuredTurnsTrusted: true, 598 }) 599 .mockResolvedValueOnce('https://gemini.google.com/app') 600 .mockResolvedValueOnce({ 601 turns: [{ Role: 'User', Text: '请只回复:OK' }], 602 transcriptLines: ['baseline', 'OK'], 603 composerHasText: false, 604 isGenerating: false, 605 structuredTurnsTrusted: true, 606 }) 607 .mockResolvedValueOnce('https://gemini.google.com/app') 608 .mockResolvedValueOnce({ 609 turns: [{ Role: 'User', Text: '请只回复:OK' }], 610 transcriptLines: ['baseline', 'OK'], 611 composerHasText: false, 612 isGenerating: false, 613 structuredTurnsTrusted: true, 614 }); 615 const result = await waitForGeminiResponse(page, { 616 snapshot: snapshot({ 617 turns: [{ Role: 'User', Text: '请只回复:OK' }], 618 transcriptLines: ['baseline'], 619 composerHasText: false, 620 isGenerating: true, 621 structuredTurnsTrusted: true, 622 }), 623 preSendAssistantCount: 0, 624 userAnchorTurn: { Role: 'User', Text: '请只回复:OK' }, 625 reason: 'user_turn', 626 }, '请只回复:OK', 6); 627 expect(result).toBe('OK'); 628 }); 629 it('ignores transcript lines that appeared before submission confirmation and only accepts post-submission transcript deltas', async () => { 630 const page = createPageMock(); 631 const evaluate = vi.mocked(page.evaluate); 632 evaluate 633 .mockResolvedValueOnce('https://gemini.google.com/app') 634 .mockResolvedValueOnce({ 635 turns: [{ Role: 'User', Text: '请只回复:OK' }], 636 transcriptLines: ['baseline', '早到的提示词回声', 'OK'], 637 composerHasText: false, 638 isGenerating: false, 639 structuredTurnsTrusted: true, 640 }) 641 .mockResolvedValueOnce('https://gemini.google.com/app') 642 .mockResolvedValueOnce({ 643 turns: [{ Role: 'User', Text: '请只回复:OK' }], 644 transcriptLines: ['baseline', '早到的提示词回声', 'OK'], 645 composerHasText: false, 646 isGenerating: false, 647 structuredTurnsTrusted: true, 648 }) 649 .mockResolvedValueOnce('https://gemini.google.com/app') 650 .mockResolvedValueOnce({ 651 turns: [{ Role: 'User', Text: '请只回复:OK' }], 652 transcriptLines: ['baseline', '早到的提示词回声', 'OK'], 653 composerHasText: false, 654 isGenerating: false, 655 structuredTurnsTrusted: true, 656 }); 657 const result = await waitForGeminiResponse(page, { 658 snapshot: snapshot({ 659 turns: [{ Role: 'User', Text: '请只回复:OK' }], 660 transcriptLines: ['baseline', '早到的提示词回声'], 661 composerHasText: false, 662 isGenerating: true, 663 structuredTurnsTrusted: true, 664 }), 665 preSendAssistantCount: 0, 666 userAnchorTurn: { Role: 'User', Text: '请只回复:OK' }, 667 reason: 'composer_transcript', 668 }, '请只回复:OK', 6); 669 expect(result).toBe('OK'); 670 }); 671 });