Files
Agent-System-Frontend/test/toolkind.test.ts
T
weichenandCursor 0ed8ad32f1 Show assistant work as a timeline and preview files in tabs
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>
2026-10-01 06:15:44 +00:00

63 lines
3.2 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { toolKind, toolHeadline, toolGroupLabel, timelineEntries } from '../src/toolkind'
import type { Part } from '../src/contracts'
function p(partial: Partial<Part> & { type: Part['type'] }): Part {
return partial as Part
}
describe('toolKind', () => {
it('classifies known tools and falls back to other', () => {
expect(toolKind('fs_read_file')).toBe('read')
expect(toolKind('web_search')).toBe('search')
expect(toolKind('run_python')).toBe('exec')
expect(toolKind('register_artifact')).toBe('write')
expect(toolKind('submit_card')).toBe('card')
expect(toolKind('delegate_task')).toBe('delegate')
expect(toolKind('update_todos')).toBe('plan')
expect(toolKind('time_now')).toBe('other')
})
it('pulls a path, a query, or the first script line out of arguments', () => {
expect(toolHeadline('fs_read_file', '{"path":"notes/a.md"}')).toBe('notes/a.md')
expect(toolHeadline('web_search', '{"query":"会议冲突"}')).toBe('会议冲突')
expect(toolHeadline('run_python', '{"script":"print(1)\\nprint(2)"}')).toBe('print(1)')
expect(toolGroupLabel('read', 2, false)).toBe('查阅 · 2 个文件')
expect(toolGroupLabel('exec', 1, true)).toBe('运行命令 · 1 条')
})
})
describe('timelineEntries', () => {
it('merges consecutive tools of the same kind and leaves narration in place', () => {
const parts: Part[] = [
p({ type: 'thinking', process: true, text: '先看文件', durationMs: 1500 }),
p({ type: 'tool_call', process: true, toolCall: { id: 'a', name: 'fs_read_file', arguments: '{"path":"a.md"}' } }),
p({ type: 'tool_result', process: true, toolResult: { callId: 'a', status: 'ok', summary: '12 行' } }),
p({ type: 'tool_call', process: true, toolCall: { id: 'b', name: 'fs_read_file', arguments: '{"path":"b.md"}' } }),
p({ type: 'tool_result', process: true, toolResult: { callId: 'b', status: 'ok' } }),
p({ type: 'text', process: true, text: '两份都读完了' }),
p({ type: 'tool_call', process: true, toolCall: { id: 'c', name: 'run_python', arguments: '{"script":"print(1)"}' } }),
]
const rows = timelineEntries(parts, true)
expect(rows.map(r => r.kind)).toEqual(['thinking', 'tools', 'text', 'tools'])
const reads = rows[1]
if (reads.kind !== 'tools') throw new Error('expected tool group')
expect(reads.toolKind).toBe('read')
expect(reads.items.map(it => it.headline)).toEqual(['a.md', 'b.md'])
expect(reads.items[0].summary).toBe('12 行')
const thinking = rows[0]
if (thinking.kind !== 'thinking') throw new Error('expected thinking')
expect(thinking.active).toBe(false)
const exec = rows[3]
if (exec.kind !== 'tools') throw new Error('expected exec group')
expect(exec.items[0].running).toBe(true)
})
it('marks the trailing thinking row active while the run is live', () => {
const rows = timelineEntries([p({ type: 'thinking', process: true, text: '还在想' })], true)
expect(rows[0]).toMatchObject({ kind: 'thinking', active: true })
expect(timelineEntries([p({ type: 'thinking', process: true, text: '想完了' })], false)[0])
.toMatchObject({ kind: 'thinking', active: false })
})
})