fix(conversation): align TodoWrite task projection and disclosure lifecycle
This commit is contained in:
1 parent
8d10743fb2
commit
d423d59b08
9 files changed
+335
-66
No files matched your search
@@ -0,0 +1,98 @@
|
||||
/** B05 projection and SSR checks; no SDK, network, or pixel claims. */
|
||||
import assert from 'node:assert/strict'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { ToolCallRenderer, getCapturedToolKind } from '../src/renderer/src/workbuddy/conversation/ToolCallRenderer'
|
||||
import { normalizeTodoWrite, windowTodoTasks } from '../src/renderer/src/workbuddy/conversation/TodoWriteView'
|
||||
import type { ConversationToolCall } from '../src/renderer/src/workbuddy/conversation/types'
|
||||
let checks = 0
|
||||
function check(name: string, run: () => void) { run(); checks++; console.log(`PASS ${name}`) }
|
||||
const tasks = (count: number, active = -1, completed: number[] = []) => Array.from({ length: count }, (_, i) => ({ id: String(i + 1), content: `Task ${i + 1}`, activeForm: `Working ${i + 1}`, status: i === active ? 'in_progress' : completed.includes(i) ? 'completed' : 'pending' }))
|
||||
const tool = (patch: Partial<ConversationToolCall> = {}): ConversationToolCall => ({ id: 'todo-1', name: 'TodoWrite', status: 'executed', isLastContent: true, args: { todos: tasks(7, 4) }, result: 'Todos have been modified successfully...', output: 'Todos have been modified successfully...', ...patch })
|
||||
const render = (value: ConversationToolCall) => renderToStaticMarkup(<ToolCallRenderer tool={value} sessionId="synthetic" />)
|
||||
const ids = (items: ReturnType<typeof tasks>) => windowTodoTasks(items).map(item => String(item.id))
|
||||
const fixtures: [string, ReturnType<typeof tasks>, string[]][] = [
|
||||
['six second active', tasks(6, 1), ['1','2','3','4','5']],
|
||||
['seven fifth active', tasks(7, 4), ['3','4','5','6','7']],
|
||||
['seven first active', tasks(7, 0), ['1','2','3','4','5']],
|
||||
['seven last active', tasks(7, 6), ['3','4','5','6','7']],
|
||||
['seven all pending', tasks(7), ['1','2','3','4','5']],
|
||||
['seven all completed', tasks(7, -1, [0,1,2,3,4,5,6]), ['3','4','5','6','7']],
|
||||
['only fourth completed', tasks(7, -1, [3]), ['2','3','4','5','6']],
|
||||
['multiple active uses first', tasks(7, 1).map((x,i) => i === 5 ? {...x,status:'in_progress'} : x), ['1','2','3','4','5']],
|
||||
['empty', tasks(0), []], ['one', tasks(1), ['1']], ['five', tasks(5), ['1','2','3','4','5']],
|
||||
['long middle', tasks(100, 49), ['48','49','50','51','52']],
|
||||
]
|
||||
for (const [name,input,expected] of fixtures) check(`window ${name}`, () => {
|
||||
const before = structuredClone(input)
|
||||
assert.deepEqual(ids(input), expected); assert.deepEqual(input,before)
|
||||
const html = render(tool({args:{todos:input}}))
|
||||
assert.equal((html.match(/data-task-id=/g) || []).length,expected.length)
|
||||
assert.doesNotMatch(html, /更多|下一页|上一页/)
|
||||
})
|
||||
for (const alias of ['TodoWrite','todo_write','todoWrite','todo-write']) check(`alias ${alias}`, () => {
|
||||
assert.equal(getCapturedToolKind(alias),'todo-write')
|
||||
assert.match(render(tool({name:alias})), /cr-tool-head--todo-write/)
|
||||
})
|
||||
check('ordinary tools do not enter dedicated view', () => {
|
||||
assert.equal(getCapturedToolKind('NotTodoWrite'),undefined)
|
||||
assert.doesNotMatch(render(tool({name:'NotTodoWrite'})), /cr-tool-head--todo-write/)
|
||||
})
|
||||
for (const raw of [false,true,'json'] as const) check(`args and raw input ${raw}`, () => {
|
||||
const input = { merge:true, todos:[{id:0, content:'Done', activeForm:'Doing',status:'completed'},{id:'a',content:'Active',activeForm:'Active now',status:'in_progress'}] }
|
||||
const value = tool({args: raw ? {_rawInput:raw === 'json' ? JSON.stringify(input) : input} : input})
|
||||
const before = structuredClone(value), projection = normalizeTodoWrite(value)
|
||||
assert.equal(projection.merge,true); assert.equal(projection.completedCount,1); assert.equal(projection.totalCount,2)
|
||||
assert.equal(projection.tasks[0].id,0); assert.equal(projection.tasks[1].activeForm,'Active now')
|
||||
render(value); assert.deepEqual(value,before)
|
||||
})
|
||||
check('historical steps retain compatible task aliases', () => {
|
||||
const value = tool({args:{steps:[{title:'Legacy task',status:'doing'}]}})
|
||||
assert.match(render(value),/Legacy task/); assert.match(render(value),/data-task-status="in_progress"/)
|
||||
})
|
||||
check('single dedicated title, modern status rows and output projection', () => {
|
||||
const value = tool({args:{todos:[...tasks(3,1,[0]),{id:'failed',content:'Failed task',status:'failed'}]}})
|
||||
const before = structuredClone(value), html = render(value)
|
||||
assert.equal((html.match(/任务列表/g)||[]).length,1)
|
||||
assert.match(html,/cr-tool-plan-task__text--done/)
|
||||
assert.match(html,/cr-tool-plan-task__text--running/)
|
||||
assert.match(html,/Working 2/); assert.doesNotMatch(html,/>Task 2</)
|
||||
for(const status of ['completed','in_progress','pending','failed']) assert.match(html,new RegExp(`data-task-status="${status}"`))
|
||||
assert.doesNotMatch(html,/cb-plan-task|Todos have been modified successfully|cr-captured-tool__output/)
|
||||
assert.deepEqual(value,before)
|
||||
})
|
||||
check('merge progress title computed from complete list not five-row window', () => {
|
||||
const html = render(tool({args:{merge:true,todos:tasks(7,6,[0,1,2,3,4,5])}}))
|
||||
assert.match(html,/进度 6\/7/); assert.doesNotMatch(html,/任务列表|进度更新/)
|
||||
})
|
||||
check('empty running is header only with receiving cue', () => {
|
||||
const html = render(tool({status:'running',args:{todos:[]}}))
|
||||
assert.match(html,/接收中/); assert.equal((html.match(/任务列表/g)||[]).length,1)
|
||||
assert.doesNotMatch(html,/cr-tool-plan-task__row|cr-tool-exp__content-shell/)
|
||||
})
|
||||
check('empty finished is header only without receiving cue', () => {
|
||||
const html = render(tool({args:{todos:[]}}))
|
||||
assert.match(html,/任务列表/); assert.doesNotMatch(html,/接收中|cr-tool-exp__content-shell/)
|
||||
})
|
||||
check('failure is dedicated error header only', () => {
|
||||
const html = render(tool({status:'failed',result:'error',output:'SDK ERROR RAW'}))
|
||||
assert.match(html,/生成失败/); assert.match(html,/cr-tool-head__icon--error/)
|
||||
assert.doesNotMatch(html,/cr-tool-plan-task__row|cr-tool-exp__content-shell|SDK ERROR RAW/)
|
||||
})
|
||||
check('without host callback rows are not fake buttons', () => {
|
||||
const html = render(tool())
|
||||
assert.doesNotMatch(html,/cr-tool-plan-task__row[^>]*role="button"|cr-tool-plan-task__action/)
|
||||
})
|
||||
check('malformed raw input falls back without modifying original data', () => {
|
||||
const value = tool({args:{_rawInput:'{bad JSON',todos:[null, 7, {}, {content:' '}, {id:0,content:'Safe task',activeForm:' ',status:'in_progress'}]}})
|
||||
const before = structuredClone(value), projection = normalizeTodoWrite(value)
|
||||
assert.equal(projection.tasks.length,1); assert.equal(projection.tasks[0].id,0)
|
||||
assert.equal(projection.tasks[0].activeForm,undefined)
|
||||
const html = render(value); assert.match(html,/Safe task/); assert.deepEqual(value,before)
|
||||
})
|
||||
check('long and escaped content stays plain text with full title', () => {
|
||||
const content = 'Long task ' + '<script>not executable</script> '.repeat(20)
|
||||
const html = render(tool({args:{todos:[{id:'long',content,status:'pending'}]}}))
|
||||
assert.match(html,/title="Long task/); assert.match(html,/<script>/)
|
||||
assert.doesNotMatch(html,/<script>/)
|
||||
})
|
||||
console.log(`todo-write: ${checks} projection and SSR checks passed; real Electron visual acceptance not covered`)
|
||||
Reference in new issue
Block a user