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(
{children}
} renderUnit={(part, index) => part.type === 'tool' ?
{part.toolId}
: {part.type === 'text' ? part.text : part.type}} /> ) assert.match(markup, /
/) assert.match(markup, /