fix(B01): preserve question routing and nested disclosure state

This commit is contained in:
Codex committed 2026-10-21 13:13:16 +09:00
1 parent 21ca0e0fc3
commit 51531dcae8
15 files changed
+341 -55

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
/** A01 real component DOM regression. Offline only; does not verify browser geometry or SDK execution. */
import assert from 'node:assert/strict'
import { createRequire } from 'node:module'
import type { ChatMessage } from '../src/shared/types'
const { JSDOM } = createRequire(process.cwd() + '/package.json')(process.env.YCODE_TEST_DOM_MODULE || 'jsdom')
const dom = new JSDOM('<!doctype html><html><body><main id="root"></main></body></html>', {pretendToBeVisual:true,url:'http://localhost/'})
for (const key of ['window','document','Node','Element','HTMLElement','MouseEvent','KeyboardEvent','Event']) Object.defineProperty(globalThis,key,{value:dom.window[key],configurable:true})
Object.defineProperty(globalThis,'navigator',{value:dom.window.navigator,configurable:true})
Object.assign(globalThis,{IS_REACT_ACT_ENVIRONMENT:true})
const React = require('react') as typeof import('react')
const {act} = React
const {createRoot} = require('react-dom/client') as typeof import('react-dom/client')
const MessageBubble = require('../src/renderer/src/components/MessageBubble').default as typeof import('../src/renderer/src/components/MessageBubble').default
const root = createRoot(document.getElementById('root')!)
const click = (el: Element) => el.dispatchEvent(new MouseEvent('click',{bubbles:true}))
const answer = {questions:[{id:'q1',question:'Choose a unique option',answers:['Unique answer']}]}
const base: ChatMessage = {id:'native-question',role:'assistant',createdAt:1,content:'Final prose',status:'done',parts:[{type:'tool',toolId:'q'},{type:'text',text:'Final prose'}],tools:[{id:'q',name:'AskUserQuestion',args:{questions:[{id:'q1',question:'Choose a unique option'}]},argsRaw:'{}',status:'ok',output:'RAW SDK ANSWER'}],interactions:[{id:'i',runId:'r',kind:'question',toolCallId:'q',title:'Question',questions:[],status:'answered',answer}]}
let checks=0
const check=(condition:unknown,label:string)=>{assert.ok(condition,label);checks++}
async function main(){
for (const name of ['AskUserQuestion','askuserquestion','ask_user_question']) {
const fixture={...base,tools:base.tools!.map(tool=>({...tool,name})),interactions:[...base.interactions!,...base.interactions!]}
const before=JSON.stringify(fixture)
await act(async()=>root.render(<MessageBubble key={name} message={fixture}/>))
check(!document.body.textContent!.includes('RAW SDK ANSWER'),`${name}: no raw answer`)
await act(async()=>click(document.querySelector('.cr-collapse--completed > .cr-collapse__header')!))
check(document.querySelectorAll('.ask-user-question--completed').length===1,`${name}: linked duplicate interactions produce one question card`)
await act(async()=>click(document.querySelector('.ask-user-question__header .cr-tool-head__main')!))
check(document.querySelector('.ask-user-question__header .cr-tool-head__main')?.getAttribute('aria-expanded')==='true',`${name}: answered card actually expanded`)
check(document.querySelectorAll('.question-answer-transcript__question').length===1,`${name}: expanded answer occurs exactly once`)
check(document.body.textContent!.includes('Choose a unique option') && document.body.textContent!.includes('Unique answer'),`${name}: question and answer visible`)
check(JSON.stringify(fixture)===before,`${name}: recorded source unchanged`)
}
// Native SDK identity throughout pending → answered → completed → outer toggle.
const pending: ChatMessage={...base,status:'streaming',parts:[{type:'tool',toolId:'q'}],interactions:[],tools:base.tools!.map(tool=>({...tool,status:'running',output:undefined}))}
await act(async()=>root.render(<MessageBubble key="lifecycle" message={pending}/>))
check(!!document.querySelector('.ask-user-question--pending'),'native pending question uses specialized renderer')
await act(async()=>root.render(<MessageBubble key="lifecycle" message={{...base,status:'streaming'}}/>))
check(!!document.querySelector('.ask-user-question--completed'),'native answered question uses transcript while turn runs')
await act(async()=>root.render(<MessageBubble key="lifecycle" message={base}/>))
const outer=()=>document.querySelector('.cr-collapse--completed > .cr-collapse__header')!
await act(async()=>click(outer()))
const header=()=>document.querySelector('.ask-user-question__header .cr-tool-head__main')!
if(header().getAttribute('aria-expanded')!=='true') await act(async()=>click(header()))
await act(async()=>click(outer()));await act(async()=>click(outer()))
check(header().getAttribute('aria-expanded')==='true','native answered choice survives completed outer collapse/reopen')
check(!document.body.textContent!.includes('RAW SDK ANSWER'),'native lifecycle never leaks raw SDK answer')
const imported={...base,interactions:[],tools:base.tools!.map(tool=>({...tool,output:'q1 -> Unique answer',metadata:{importedHistory:true}}))}
await act(async()=>root.render(<MessageBubble key="imported" message={imported}/>))
await act(async()=>click(document.querySelector('.cr-collapse--completed > .cr-collapse__header')!))
await act(async()=>click(document.querySelector('.ask-user-question__header .cr-tool-head__main')!))
check(document.body.textContent!.includes('Unique answer'),'historical native name and answer-text adaptation preserved')
check(imported.tools[0].name==='AskUserQuestion','historical stored name unchanged')
for (const interactions of [base.interactions, []]) {
const legacy={...base,parts:undefined,interactions,tools:base.tools!.map(tool=>({...tool,metadata:{questionAnswer:answer}}))}
await act(async()=>root.render(<MessageBubble key={`legacy-${interactions?.length}`} message={legacy}/>))
check(document.querySelector('.question-history-unpositioned')?.getAttribute('aria-label')==='历史问答(原始位置不可用)','missing recorded position remains explicitly unknown')
check(document.querySelectorAll('.question-answer-transcript__question').length===1,'unknown-position metadata and interaction do not duplicate')
check(!document.body.textContent!.includes('RAW SDK ANSWER'),'unknown-position raw duplicate suppressed')
}
await act(async()=>root.unmount());dom.window.close()
console.log(`question-identity: ${checks} A01 DOM checks passed (not Electron/pixel/SDK acceptance)`)
}
main().then(() => process.exit(0)).catch(error=>{console.error(error);process.exit(1)})