Files
Agent-System-Frontend/test/toolkind.test.ts
weichenandCursor 794c3e1657 Show the final reply apart from the work timeline
Why: Deliverable cards appeared as soon as a file was registered, and the last draft was shown as the answer before the run had finished composing one.
Evidence: vitest run test/live.test.ts test/toolkind.test.ts test/messageparts.test.ts test/design-guard.test.ts
Impact: Cards wait until the run ends. A finalizing phase keeps earlier text in the process block, and check rows render there.
Refs: feature/final-reply-compose
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-02 13:41:36 +00:00

75 lines
3.7 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', 'toolblock'])
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 !== 'toolblock') throw new Error('expected exec block')
expect(exec.item.running).toBe(true)
expect(exec.item.headline).toBe('print(1)')
})
it('renders a check row instead of narration', () => {
const rows = timelineEntries([
p({ type: 'check', process: true, check: { passed: false, reason: '纸张不是 A4' } }),
p({ type: 'check', process: true, check: { passed: true } }),
], false)
expect(rows).toEqual([
{ kind: 'check', passed: false, reason: '纸张不是 A4' },
{ kind: 'check', passed: true, reason: '' },
])
})
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 })
})
})