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()
const streamingInspectionMarkup = renderToStaticMarkup()
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, /Listing project, reading h/)
const completedInspectionMarkup = renderToStaticMarkup()
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(
}
renderUnit={(part, index) => part.type === 'tool'
?
: {part.type === 'text' ? part.text : part.type}}
/>
)
assert.match(markup, //)
assert.match(markup, /