76 lines
6.9 KiB
TypeScript
76 lines
6.9 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
import type { ChatMessage } from '../src/shared/types'
|
|
import { projectQuestionAnswers } from '../src/renderer/src/workbuddy/conversation/question-projection'
|
|
import { buildCompletedFoldModel } from '../src/renderer/src/workbuddy/conversation/completed-fold'
|
|
import { CompletedFold } from '../src/renderer/src/workbuddy/conversation/CompletedFold'
|
|
import { StreamingFooter, resolveRuntimeStatusText, isLoadingTipPaused } from '../src/renderer/src/workbuddy/conversation/StreamingFooter'
|
|
import MessageBubble from '../src/renderer/src/components/MessageBubble'
|
|
|
|
const message: ChatMessage = { id:'trace', role:'assistant', content:'A B', createdAt:1, status:'streaming', parts:[
|
|
{type:'text',text:'A'}, {type:'reasoning',id:'r1',text:'thought one'}, {type:'tool',toolId:'t1'},
|
|
{type:'text',text:'B'}, {type:'reasoning',id:'r2',text:'thought two'}, {type:'tool',toolId:'t2'}
|
|
], tools: ['t1','t2'].map(id=>({id,name:'Read',args:{file_path:id},argsRaw:'{}',status:'ok'})) }
|
|
const model=buildCompletedFoldModel(message.parts!,{turnFinished:false})!
|
|
assert.deepEqual(model.summaryGroups.map(group=>[group.startIndex,group.shouldFold]),[[1,true],[4,false]])
|
|
const live=renderToStaticMarkup(<MessageBubble message={message}/> )
|
|
assert.equal((live.match(/cr-live-event-feed/g)||[]).length,1)
|
|
assert.match(live,/data-activity-tool-id="t1"/)
|
|
assert.match(live.replace(/<[^>]*>/g,''),/thought two/)
|
|
assert.equal((live.match(/data-tool-name="Read"/g)||[]).length,2)
|
|
const expanded=renderToStaticMarkup(<CompletedFold parts={message.parts!} turnFinished turnDurationMs={1000} expandFinishedEvents renderUnit={(p,i)=><i data-index={i}>{p.type}</i>}/> )
|
|
assert.deepEqual([...expanded.matchAll(/data-index="(\d+)"/g)].map(m=>Number(m[1])),[0,1,2,3,4,5])
|
|
const question: ChatMessage={...message,parts:[{type:'tool',toolId:'q'},{type:'text',text:'after'}],tools:[{id:'q',name:'AskUserQuestion',args:{},argsRaw:'{}',status:'ok'}],interactions:[{kind:'question',id:'i',runId:'r',toolCallId:'q',title:'Q',status:'answered',questions:[],answer:{questions:[{id:'q1',question:'Choose',answers:['yes']}]}}]}
|
|
const projection=projectQuestionAnswers(question)
|
|
assert.equal(projection.unpositioned.length,0)
|
|
assert.deepEqual(projection.tools[0].metadata?.questionAnswer,question.interactions![0].kind==='question'&&question.interactions![0].answer)
|
|
assert.equal(question.tools![0].metadata,undefined,'projection must not mutate persisted source')
|
|
assert.equal(projectQuestionAnswers({...question,tools:[],parts:[{type:'text',text:'old'}]}).unpositioned.length,1)
|
|
assert.equal(projectQuestionAnswers({...question,interactions:[...question.interactions!,...question.interactions!]}).tools.length,1)
|
|
assert.equal(resolveRuntimeStatusText('准备项目',{source:'agent',phase:'tool',toolName:'Read'}),'准备项目')
|
|
assert.equal(resolveRuntimeStatusText(null,{source:'interaction',phase:'waiting-answer'}),'等待回答')
|
|
assert.equal(resolveRuntimeStatusText(null,{source:'agent',phase:'finishing'}),'正在收尾')
|
|
assert.equal(renderToStaticMarkup(<StreamingFooter locale="zh" reasoningOnly/>),'')
|
|
assert.match(renderToStaticMarkup(<StreamingFooter locale="zh" reasoningOnly progress={{source:'interaction',phase:'waiting-approval'}}/>),/等待授权/)
|
|
console.log('alignment-conversation: local groups, original expanded order, question projection and truthful status passed')
|
|
|
|
assert.equal(isLoadingTipPaused({hovered:false,focused:true}),true)
|
|
assert.equal(isLoadingTipPaused({hovered:true,focused:false}),true)
|
|
assert.equal(isLoadingTipPaused({hovered:false,focused:false}),false)
|
|
|
|
const expandedGrouped=renderToStaticMarkup(<CompletedFold parts={message.parts!} turnFinished turnDurationMs={1000} expandFinishedEvents
|
|
renderGroup={(group,children)=><section data-local-group={group.startIndex}>{children}</section>}
|
|
renderUnit={(p,i)=><i data-index={i}>{p.type}</i>}/> )
|
|
assert.deepEqual([...expandedGrouped.matchAll(/data-index="(\d+)"/g)].map(m=>Number(m[1])),[0,1,2,3,4,5])
|
|
assert.deepEqual([...expandedGrouped.matchAll(/data-local-group="(\d+)"/g)].map(m=>Number(m[1])),[1,4],
|
|
'opening completion must preserve independently controlled local summaries')
|
|
|
|
// A01: exercise the real native name through MessageBubble, not just its projection.
|
|
const nativeQuestion = {...question, status: 'done' as const, tools: question.tools!.map(tool => ({...tool, output: 'RAW SDK ANSWER'}))}
|
|
for (const name of ['AskUserQuestion', 'askuserquestion', 'ask_user_question', 'AskFollowupQuestion', 'ask-followup-question']) {
|
|
const fixture = {...nativeQuestion, tools: nativeQuestion.tools.map(tool => ({...tool, name}))}
|
|
const before = JSON.stringify(fixture)
|
|
const markup = renderToStaticMarkup(<MessageBubble message={fixture}/>)
|
|
assert.match(markup, /ask-user-question--completed/, `${name} routes to dedicated question card`)
|
|
assert.doesNotMatch(markup, /RAW SDK ANSWER/, `${name} suppresses raw SDK answer`)
|
|
assert.equal(JSON.stringify(fixture), before, 'rendering does not rewrite recorded names or metadata')
|
|
}
|
|
const ordinary = renderToStaticMarkup(<MessageBubble message={{...nativeQuestion, interactions: [], tools: [{...nativeQuestion.tools[0], name: 'CustomQuestionReporter', metadata: undefined}]}}/>)
|
|
assert.doesNotMatch(ordinary, /ask-user-question--completed/)
|
|
assert.match(ordinary, /RAW SDK ANSWER/, 'ordinary tools retain their raw output')
|
|
console.log('A01 native MessageBubble identity aliases, immutable inputs and ordinary raw output passed')
|
|
const importedNative = {...nativeQuestion, interactions: [], tools: nativeQuestion.tools.map(tool => ({...tool,
|
|
args: {questions: [{id: 'q1', question: 'Imported native question'}]}, output: 'q1 -> imported answer', metadata: {importedHistory: true}
|
|
}))}
|
|
const importedMarkup = renderToStaticMarkup(<MessageBubble message={importedNative}/>)
|
|
assert.match(importedMarkup, /ask-user-question--completed/)
|
|
assert.equal(importedNative.tools[0].name, 'AskUserQuestion', 'historical display adaptation does not rewrite stored name')
|
|
const duplicateMarkup = renderToStaticMarkup(<MessageBubble message={{...nativeQuestion, interactions: [...nativeQuestion.interactions!, ...nativeQuestion.interactions!]}}/>)
|
|
assert.equal((duplicateMarkup.match(/ask-user-question--completed/g) || []).length, 1, 'linked answers have one dedicated card')
|
|
const legacyNative = {...nativeQuestion, parts: undefined, tools: nativeQuestion.tools.map(tool => ({...tool, metadata: {questionAnswer: {questions: [{id: 'legacy', question: 'Unknown-position question', answers: ['legacy answer']}]}}})), interactions: []}
|
|
const legacyNativeMarkup = renderToStaticMarkup(<MessageBubble message={legacyNative}/>)
|
|
assert.match(legacyNativeMarkup, /历史问答(原始位置不可用)/)
|
|
assert.equal((legacyNativeMarkup.match(/Unknown-position question/g) || []).length, 1)
|
|
assert.doesNotMatch(legacyNativeMarkup, /RAW SDK ANSWER/)
|
|
console.log('A01 imported native card, linked dedup and unknown-position compatibility passed')
|