Files
ycode/scripts/completed-fold-regression.tsx

326 lines
18 KiB
TypeScript

import assert from 'node:assert/strict'
import { renderToStaticMarkup } from 'react-dom/server'
import { CompletedFold } from '../src/renderer/src/workbuddy/conversation/CompletedFold'
import { computeSourceFoldSummary } from '../src/renderer/src/workbuddy/conversation/recovered-fold-summary/index.mjs'
import { sourceConversationLabel, type SourceConversationLabelKey } from '../src/renderer/src/workbuddy/conversation/source-labels'
import MessageBubble from '../src/renderer/src/components/MessageBubble'
import { formatStreamingTurnDuration } from '../src/renderer/src/workbuddy/conversation/StreamingTurnDuration'
import {
buildCompletedFoldModel,
classifyCompletedFoldPart,
formatCompletedFoldDuration,
sourceCompletedFoldLabelsZh,
type CompletedFoldPart
} from '../src/renderer/src/workbuddy/conversation/completed-fold'
const orderedParts: CompletedFoldPart[] = [
{ type: 'reasoning', id: 'reasoning-prefix', text: 'thinking' },
{ type: 'tool', toolId: 'tool-prefix' },
{ type: 'text', text: 'This is the longest body text anchor.' },
{ type: 'reasoning', id: 'reasoning-process', text: 'more thinking' },
{ type: 'tool', toolId: 'tool-process' },
{ type: 'text', text: 'tail' },
{ type: 'tool', toolId: 'tool-trailing' },
{ type: 'plan', plan: { id: 'plan-1', title: 'Plan', steps: [] } }
]
const model = buildCompletedFoldModel(orderedParts, { turnFinished: true, turnDurationMs: 65_000 })
assert.ok(model)
assert.deepEqual(model.anchors.map(unit => unit.index), [2, 5])
assert.deepEqual(model.completedUnits.map(unit => unit.index), [0, 1, 6])
assert.deepEqual(model.processGroups.map(group => group.units.map(unit => unit.index)), [[3, 4]])
assert.deepEqual(model.summaryGroups.map(group => ({
indices: group.units.map(unit => unit.index),
shouldFold: group.shouldFold,
adjacentText: group.adjacentText,
toolCallCount: group.toolCallCount
})), [
{ indices: [0, 1], shouldFold: true, adjacentText: 'This is the longest body text anchor.', toolCallCount: 1 },
{ indices: [3, 4], shouldFold: true, adjacentText: 'tail', toolCallCount: 1 }
])
assert.equal(model.title, '已完成 1m5s')
assert.deepEqual(model.segments.map(segment => segment.kind), ['completed', 'unit', 'process', 'unit', 'unit'])
const onlyText = [{ type: 'text', text: 'plain answer' }] satisfies CompletedFoldPart[]
const emptyDurationFold = buildCompletedFoldModel(onlyText, { turnFinished: true, turnDurationMs: 1 })
assert.ok(emptyDurationFold)
assert.equal(emptyDurationFold.completedUnits.length, 0)
assert.equal(emptyDurationFold.title, '已完成 <1s')
assert.equal(buildCompletedFoldModel(onlyText, { turnFinished: true }), null)
assert.equal(buildCompletedFoldModel(onlyText, { turnFinished: false, turnDurationMs: 5000 }), null)
assert.equal(buildCompletedFoldModel(onlyText, { turnFinished: true, turnDurationMs: 5000, suppressCompletedFold: true }), null)
const cancelled = buildCompletedFoldModel(onlyText, { turnFinished: true, turnDurationMs: 2500, turnCancelled: true })
assert.equal(cancelled?.title, '已取消 2s')
assert.equal(cancelled?.status, 'cancelled')
assert.equal(buildCompletedFoldModel(onlyText, { turnFinished: true, turnCancelled: true }), null)
assert.equal(formatCompletedFoldDuration(Number.NaN), '0s')
assert.equal(formatCompletedFoldDuration(60_000), '1m')
assert.equal(formatCompletedFoldDuration(3_661_000), '1h1m')
assert.equal(formatStreamingTurnDuration(0), '1s')
assert.equal(formatStreamingTurnDuration(999), '1s')
assert.equal(formatStreamingTurnDuration(60_000), '1m')
assert.equal(formatStreamingTurnDuration(3_661_000), '1h1m')
const sourceSummary = computeSourceFoldSummary([{ type: 'tool-call', tool: {
name: 'Read', status: 'executed', args: { file_path: 'C:/work/readme.md' }
} }], {
t: (key, params) => sourceConversationLabel(key as SourceConversationLabelKey, params, 'zh-CN')
})
assert.deepEqual(sourceSummary, { text: '读取 readme.md', status: 'success', isFallback: false })
const safeVisibleClassification = {
isBodyText: false, keepVisible: true, isCompletedAnchor: false, foldable: false,
isWidget: false, isShowWidget: false, preservePosition: false, hasRootBlock: true
}
assert.deepEqual(classifyCompletedFoldPart({ type: 'plan', plan: { id: 'p', title: 'Plan', steps: [] } }, 0), safeVisibleClassification)
assert.deepEqual(classifyCompletedFoldPart({ type: 'task', task: { id: 't', title: 'Task', status: 'pending' } }, 0), safeVisibleClassification)
const futurePart = { type: 'future-widget', label: 'keep outside fold' } as CompletedFoldPart
assert.equal(classifyCompletedFoldPart(futurePart, 0).keepVisible, true)
assert.deepEqual(classifyCompletedFoldPart({ type: 'tool', toolId: 'visible-tool' }, 0, () => ({ role: 'show-widget' })), {
isBodyText: false, keepVisible: true, isCompletedAnchor: false, foldable: false,
isWidget: true, isShowWidget: true, preservePosition: false, role: 'show-widget', hasRootBlock: true
})
assert.deepEqual(classifyCompletedFoldPart({ type: 'tool', toolId: 'inline-question' }, 0, () => ({ role: 'inline-question' })), {
isBodyText: false, keepVisible: false, isCompletedAnchor: false, foldable: false,
isWidget: false, isShowWidget: false, preservePosition: false, role: 'inline-question', hasRootBlock: true
})
const activePrefixSummary = buildCompletedFoldModel([
{ type: 'tool', toolId: 'prefix-tool' },
{ type: 'reasoning', id: 'prefix-thinking', text: 'thinking' },
{ type: 'text', text: 'answer' },
{ type: 'tool', toolId: 'trailing-tool' },
{ type: 'reasoning', id: 'trailing-thinking', text: 'thinking' }
], { turnFinished: false })
assert.ok(activePrefixSummary)
assert.deepEqual(activePrefixSummary.summaryGroups.map(group => [group.startIndex, group.endIndex, group.shouldFold]), [
[0, 1, true], [3, 4, false]
])
assert.equal(activePrefixSummary.summaryGroups[0].isRunning, false)
assert.equal(activePrefixSummary.segments.filter(segment => segment.kind === 'unit' && segment.unit.kind === 'summary').length, 1)
const initialInspectionMarkup = renderToStaticMarkup(<MessageBubble message={{
id: 'streaming-inspection', role: 'assistant', status: 'streaming', createdAt: 1,
parts: [
{ type: 'reasoning', id: 'inspection-reasoning', text: '**Listing project, reading handoff**' },
{ type: 'tool', toolId: 'inspect-root' },
{ type: 'tool', toolId: 'read-handoff' }
],
tools: [
{ id: 'inspect-root', name: 'list_dir', status: 'ok', args: { path: '.' } },
{ id: 'read-handoff', name: 'read_file', status: 'ok', args: { path: 'docs/接手状态-2026-10-15.md' } }
]
}} />)
const streamingInspectionMarkup = renderToStaticMarkup(<MessageBubble message={{
id: 'streaming-inspection', role: 'assistant', status: 'streaming', createdAt: 1, turnStartedAt: Date.now(),
parts: [
{ type: 'reasoning', id: 'inspection-reasoning', text: '**Listing project, reading handoff**' },
{ type: 'tool', toolId: 'inspect-root' },
{ type: 'tool', toolId: 'read-handoff' },
{ type: 'reasoning', id: 'inspection-next-step', text: '**Inspecting the source tree**' },
{ type: 'tool', toolId: 'inspect-src' }
],
tools: [
{ id: 'inspect-root', name: 'list_dir', status: 'ok', args: { path: '.' } },
{ id: 'read-handoff', name: 'read_file', status: 'ok', args: { path: 'docs/接手状态-2026-10-15.md' } },
{ id: 'inspect-src', name: 'list_dir', status: 'stream_executing', args: { path: 'src' } }
]
}} />)
assert.equal((initialInspectionMarkup.match(/cr-streaming-activity/g) ?? []).length, 0, 'unfinished tail is not a summary')
assert.equal((streamingInspectionMarkup.match(/cr-streaming-activity/g) ?? []).length, 0)
assert.match(streamingInspectionMarkup, /已处理 1s/)
assert.match(streamingInspectionMarkup, /data-tool-name="list_dir"[^]*data-tool-name="read_file"/)
assert.match(streamingInspectionMarkup, /<strong>Listing project, reading h/)
const completedInspectionMarkup = renderToStaticMarkup(<MessageBubble message={{
id: 'completed-inspection', role: 'assistant', status: 'done', createdAt: 1, content: 'Done.',
parts: [
{ type: 'reasoning', id: 'completed-inspection-reasoning', text: '**Listing project, reading handoff**' },
{ type: 'tool', toolId: 'completed-inspect-root' },
{ type: 'tool', toolId: 'completed-read-handoff' },
{ type: 'text', text: 'Done.' }
],
tools: [
{ id: 'completed-inspect-root', name: 'list_dir', status: 'ok', args: { path: '.' } },
{ id: 'completed-read-handoff', name: 'read_file', status: 'ok', args: { path: 'docs/接手状态-2026-10-15.md' } }
]
}} />)
assert.doesNotMatch(completedInspectionMarkup, /cr-collapse--summary/)
assert.match(completedInspectionMarkup, /data-tool-name="list_dir"/)
assert.match(completedInspectionMarkup, /data-tool-name="read_file"/)
const oneFoldablePart = buildCompletedFoldModel([
{ type: 'text', text: 'answer' },
{ type: 'reasoning', id: 'single-thinking', text: 'thinking' }
], { turnFinished: true })
assert.equal(oneFoldablePart?.summaryGroups.length, 0)
const inlineQuestionParts: CompletedFoldPart[] = [
{ type: 'tool', toolId: 'before-question' },
{ type: 'tool', toolId: 'question' },
{ type: 'tool', toolId: 'after-question' },
{ type: 'text', text: 'answer' }
]
const inlineQuestionBreak = buildCompletedFoldModel(inlineQuestionParts, {
turnFinished: true,
classifyToolCall: part => part.toolId === 'question' ? { role: 'inline-question' } : undefined
})
assert.equal(inlineQuestionBreak?.summaryGroups.length, 0)
const inlineQuestionWithoutBlock = buildCompletedFoldModel(inlineQuestionParts, {
turnFinished: true,
classifyToolCall: part => part.toolId === 'question' ? { role: 'inline-question', hasRootBlock: false } : undefined
})
assert.deepEqual(inlineQuestionWithoutBlock?.summaryGroups.map(group => group.units.map(unit => unit.index)), [[0, 2]])
const widgetReorderParts: CompletedFoldPart[] = [
{ type: 'tool', toolId: 'movable-before-body' },
{ type: 'text', text: 'This is the longest body anchor.' },
{ type: 'tool', toolId: 'preserved-before-guidance' },
{ type: 'tool', toolId: 'linked-show-widget' },
{ type: 'queue-guidance', id: 'queue-boundary' },
{ type: 'tool', toolId: 'movable-after-guidance' },
{ type: 'tool', toolId: 'process-work' },
{ type: 'text', text: 'tail' },
{ type: 'tool', toolId: 'trailing-work' }
]
const widgetReorder = buildCompletedFoldModel(widgetReorderParts, {
turnFinished: true,
classifyToolCall: part => {
if (part.toolId === 'movable-before-body' || part.toolId === 'movable-after-guidance') return { role: 'widget' }
if (part.toolId === 'preserved-before-guidance') return { role: 'widget', preservePosition: true }
if (part.toolId === 'linked-show-widget') return { role: 'show-widget' }
return undefined
}
})
assert.ok(widgetReorder)
const renderedIndices = widgetReorder.segments.flatMap(segment => {
if (segment.kind === 'completed') return [`completed:${segment.units.flatMap(unit => unit.kind === 'part' ? [unit.unit.index] : unit.group.units.map(member => member.index)).join(',')}`]
if (segment.kind === 'process') return [`process:${segment.group.units.map(unit => unit.index).join(',')}`]
return [segment.unit.kind === 'part' ? String(segment.unit.unit.index) : `summary:${segment.unit.group.startIndex}-${segment.unit.group.endIndex}`]
})
assert.deepEqual(renderedIndices, ['completed:8', '1', '0', '2', '3', '4', 'process:6', '7', '5'])
assert.deepEqual(widgetReorder.anchors.map(unit => unit.index), [1, 4, 7])
const presentationClassification = widgetReorder.segments.flatMap(segment => {
if (segment.kind === 'unit' && segment.unit.kind === 'part') return [segment.unit]
if (segment.kind === 'completed') return segment.units.filter(unit => unit.kind === 'part')
if (segment.kind === 'process') return segment.group.children.filter(unit => unit.kind === 'part')
return []
})
const linkedBody = presentationClassification.find(unit => unit.unit.index === 1)?.unit.classification?.presentationGroupId
const linkedWidget = presentationClassification.find(unit => unit.unit.index === 3)?.unit.classification?.presentationGroupId
assert.ok(linkedBody)
assert.equal(linkedBody, linkedWidget)
const markup = renderToStaticMarkup(
<CompletedFold
parts={orderedParts}
turnFinished
turnDurationMs={65_000}
foldId="message-1"
renderGroup={(group, children) => <section className="source-summary-group" data-summary-start={group.startIndex}>{children}</section>}
renderUnit={(part, index) => part.type === 'tool'
? <section className="cr-collapse cr-collapse--summary" data-unit={part.toolId}>{part.toolId}</section>
: <span data-unit={`${part.type}-${index}`}>{part.type === 'text' ? part.text : part.type}</span>}
/>
)
assert.match(markup, /<section class="cr-collapse cr-collapse--completed cr-collapse--collapsed">/)
assert.match(markup, /<header class="cr-collapse__header" role="button" tabindex="0" aria-expanded="false"/)
assert.match(markup, /已完成 1m5s/)
assert.match(markup, /cr-collapse__content-inner cr-collapse__content-inner--completed/)
assert.match(markup, /aria-hidden="true" inert=""/)
assert.equal(markup.split('class="source-summary-group"').length - 1, 2)
assert.match(markup, /data-summary-start="0"/)
assert.match(markup, /data-summary-start="3"/)
const completedStart = markup.indexOf('cr-collapse--completed')
const prefixSummary = markup.indexOf('cr-collapse--summary" data-unit="tool-prefix')
const trailingSummary = markup.indexOf('cr-collapse--summary" data-unit="tool-trailing')
const firstAnchor = markup.indexOf('This is the longest body text anchor.')
const processFold = markup.indexOf('cr-collapse--process')
const processSummary = markup.indexOf('cr-collapse--summary" data-unit="tool-process')
const lastAnchor = markup.indexOf('tail')
const visiblePlan = markup.indexOf('data-unit="plan-7"')
assert.ok(completedStart >= 0 && completedStart < prefixSummary && prefixSummary < trailingSummary && trailingSummary < firstAnchor)
assert.ok(firstAnchor < processFold && processFold < processSummary && processSummary < lastAnchor)
assert.ok(lastAnchor < visiblePlan)
assert.match(markup, /tool-prefix/)
assert.match(markup, /tool-trailing/)
assert.match(markup, /cr-collapse__content-inner cr-collapse__content-inner--completed[^]*cr-collapse--summary/)
const expandedOnCompletionMarkup = renderToStaticMarkup(
<CompletedFold
parts={orderedParts}
turnFinished
turnDurationMs={65_000}
expandFinishedEvents
foldId="message-expanded-live-process"
renderUnit={(part, index) => <span data-unit={part.type === 'tool' ? part.toolId : `${part.type}-${index}`} />}
/>
)
assert.match(expandedOnCompletionMarkup, /aria-expanded="true"/,
'a live process section the user opened stays expanded when its turn completes')
const noFoldMarkup = renderToStaticMarkup(
<CompletedFold parts={onlyText} turnFinished renderUnit={part => <span>{part.type === 'text' ? part.text : part.type}</span>} />
)
assert.doesNotMatch(noFoldMarkup, /cr-collapse/)
assert.match(noFoldMarkup, /plain answer/)
const processWithoutDuration = buildCompletedFoldModel([
{ type: 'text', text: 'longest body anchor' },
{ type: 'tool', toolId: 'between-anchors' },
{ type: 'text', text: 'last' }
], { turnFinished: true })
assert.ok(processWithoutDuration)
assert.equal(processWithoutDuration.segments.some(segment => segment.kind === 'completed'), false)
assert.equal(processWithoutDuration.processGroups.length, 1)
assert.equal(sourceCompletedFoldLabelsZh.cancelled, '已取消')
const widgetBoundaryMarkup = renderToStaticMarkup(<MessageBubble message={{
id: 'widget-boundary', role: 'assistant', content: 'Answer', status: 'done', createdAt: 1,
parts: [
{ type: 'tool', toolId: 'before' }, { type: 'tool', toolId: 'widget' },
{ type: 'tool', toolId: 'after' }, { type: 'text', text: 'Answer' }
],
tools: [
{ id: 'before', name: 'before_widget', status: 'ok' },
{ id: 'widget', name: 'show_widget', status: 'ok' },
{ id: 'after', name: 'after_widget', status: 'ok' }
]
}} />)
for (const name of ['before_widget', 'show_widget', 'after_widget']) {
assert.equal(widgetBoundaryMarkup.split(`data-tool-name="${name}"`).length - 1, 1)
}
assert.doesNotMatch(widgetBoundaryMarkup, /3 个工具调用/)
const streamingToolIntentMarkup = renderToStaticMarkup(<MessageBubble message={{
id: 'streaming-tool-intent', role: 'assistant', status: 'streaming', createdAt: 1,
parts: [{ type: 'tool', toolId: 'list-src' }],
tools: [{ id: 'list-src', name: 'list_dir', status: 'running', intent: 'Inspecting source layout', args: { path: 'src' }, argsRaw: '{"path":"src"}' }]
}} />)
assert.doesNotMatch(streamingToolIntentMarkup, /cr-streaming-activity/, 'single live tool stays independent')
assert.match(streamingToolIntentMarkup, /data-tool-name="list_dir"/)
for (const status of ['cancelled', 'error'] as const) {
const terminalMarkup = renderToStaticMarkup(<MessageBubble message={{
id: `terminal-${status}`, role: 'assistant', status, createdAt: 1, turnDurationMs: 65_000,
error: status === 'error' ? '429 rate limit' : undefined, content: 'Answer',
parts: [{ type: 'tool', toolId: 'finished-tool' }, { type: 'text', text: 'Answer' }],
tools: [{ id: 'finished-tool', name: 'Read', status: 'ok', args: {}, argsRaw: '{}' }]
}} />)
assert.match(terminalMarkup, /cr-collapse--completed cr-collapse--collapsed/)
assert.match(terminalMarkup, status === 'cancelled' ? /已取消 1m5s/ : /已完成 1m5s/)
if (status === 'cancelled') {
assert.match(terminalMarkup, /用户已取消/)
assert.doesNotMatch(terminalMarkup, /msg__status--error/)
} else assert.match(terminalMarkup, /429 rate limit/)
}
const historicalQuestion = renderToStaticMarkup(<MessageBubble message={{
id: 'historical-question', role: 'assistant', status: 'done', createdAt: 1, content: '',
parts: [{ type: 'tool', toolId: 'question' }], tools: [{ id: 'question', name: 'AskUserQuestion',
status: 'ok', args: { questions: [{ question: 'Choose?' }] }, argsRaw: '{}',
result: { content: ' · Choose? → Desktop' }, metadata: { importedHistory: true } }]
}} />)
assert.match(historicalQuestion, /ask-user-question--completed/)
assert.match(historicalQuestion, /向用户提问/)
assert.match(historicalQuestion, /aria-expanded="false"/)
assert.doesNotMatch(historicalQuestion, /data-tool-name="AskUserQuestion"/)
console.log('Completed-fold model and SSR regression checks passed')