Why: Process output folded on every text delta, successful tool results disappeared after refresh, and generated files stayed plain text instead of a card that opens a closable preview. Evidence: npx tsc -b --noEmit (clean); npx vitest run (51 passed). Impact: Live work stays expanded until the run ends, then folds to one worked-time line. File cards open a per-conversation preview pane. docx renders page by page, with mammoth if that fails. Refs: feature/chat-timeline-preview Co-authored-by: Cursor <cursoragent@cursor.com>
79 lines
3.3 KiB
TypeScript
79 lines
3.3 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { splitTimeline, humanSize, artifactIcon, artifactIdsIn, markMissingArtifacts, workedLabel } from '../src/messageparts'
|
|
import type { Part } from '../src/contracts'
|
|
|
|
function p(partial: Partial<Part> & { type: Part['type'] }): Part {
|
|
return partial as Part
|
|
}
|
|
|
|
describe('splitTimeline', () => {
|
|
it('keeps everything up to the last process part in the timeline', () => {
|
|
const parts: Part[] = [
|
|
p({ type: 'text', text: '先找资料', process: true }),
|
|
p({ type: 'tool_call', process: true, toolCall: { id: 'c1', name: 'search' } }),
|
|
p({ type: 'tool_result', process: true, toolResult: { callId: 'c1', status: 'ok' } }),
|
|
p({ type: 'text', text: '文档已生成,请查收' }),
|
|
p({ type: 'artifact_ref', artifact: { artifactId: 'a1', name: '会议纪要.docx' } }),
|
|
]
|
|
const split = splitTimeline(parts)
|
|
expect(split.timeline.map(x => x.type)).toEqual(['text', 'tool_call', 'tool_result'])
|
|
expect(split.final.map(x => x.type)).toEqual(['text', 'artifact_ref'])
|
|
})
|
|
|
|
it('keeps a card sandwiched between process parts inside the timeline', () => {
|
|
const parts: Part[] = [
|
|
p({ type: 'thinking', process: true, text: '想' }),
|
|
p({ type: 'card', card: { kind: 'result', data: [] } }),
|
|
p({ type: 'tool_call', process: true, toolCall: { id: 'c1', name: 'a' } }),
|
|
p({ type: 'text', text: '最终' }),
|
|
]
|
|
const split = splitTimeline(parts)
|
|
expect(split.timeline.map(x => x.type)).toEqual(['thinking', 'card', 'tool_call'])
|
|
expect(split.final.map(x => x.text)).toEqual(['最终'])
|
|
})
|
|
|
|
it('puts a message with no process parts entirely in the final section', () => {
|
|
const parts = [p({ type: 'text', text: 'hi' })]
|
|
expect(splitTimeline(parts).timeline).toEqual([])
|
|
expect(splitTimeline(parts).final).toEqual(parts)
|
|
expect(splitTimeline([])).toEqual({ timeline: [], final: [] })
|
|
})
|
|
})
|
|
|
|
describe('artifact ids in reply text', () => {
|
|
it('extracts unique ids in order', () => {
|
|
expect(artifactIdsIn('见 art_0123456789ab 和 `art_abcdefabcdef`,再提 art_0123456789ab'))
|
|
.toEqual(['art_0123456789ab', 'art_abcdefabcdef'])
|
|
})
|
|
it('marks only ids the project does not have', () => {
|
|
const text = '文件 art_0123456789ab 与 art_abcdefabcdef'
|
|
const out = markMissingArtifacts(text, id => id === 'art_0123456789ab')
|
|
expect(out).toContain('art_0123456789ab')
|
|
expect(out).not.toContain('art_abcdefabcdef')
|
|
expect(out).toContain('未找到该文件')
|
|
})
|
|
})
|
|
|
|
describe('workedLabel', () => {
|
|
it('formats the run span and degrades without timestamps', () => {
|
|
expect(workedLabel('2026-10-01T00:00:00.000Z', '2026-10-01T00:01:05.000Z')).toBe('已工作 1 分 5 秒')
|
|
expect(workedLabel(undefined, undefined)).toBe('已工作')
|
|
})
|
|
})
|
|
|
|
describe('humanSize / artifactIcon', () => {
|
|
it('formats sizes compactly', () => {
|
|
expect(humanSize(undefined)).toBe('')
|
|
expect(humanSize(0)).toBe('')
|
|
expect(humanSize(512)).toBe('512 B')
|
|
expect(humanSize(2048)).toBe('2 KB')
|
|
expect(humanSize(1536 * 1024)).toBe('1.5 MB')
|
|
})
|
|
it('picks icons by extension then kind', () => {
|
|
expect(artifactIcon('会议纪要.docx')).toBe('file')
|
|
expect(artifactIcon('截图.png')).toBe('image')
|
|
expect(artifactIcon('blob', 'image')).toBe('image')
|
|
expect(artifactIcon('打包.zip')).toBe('package')
|
|
})
|
|
})
|