fix(gui): the collapsed plan hint accounts for parallel active items
Lifting the single-in_progress cap makes a list shape reachable that the web surfaces never received. Two sites derived their one-line summary with todos.find(t => t.status === 'in_progress') — the collapsed TodoPanel header and the todo_write row — which was total under the old cap and silently dropped every active item but the first once several could match: a plan with three running tasks collapsed to the name of one. The expanded list was always correct, so neither PR's tests covered it. Both sites now take planSummary in contract/todo-plan-model.ts, the domain-shared face both the skeleton and toolviews domains may import; the duplicated derivation was why one find could be fixed while the other stayed wrong. The hint names the first active item and suffixes +<n> for the rest, so the collapsed line reports how many tasks are running. The web fixture's todo sample now runs two items in_progress, so the assembled web transcript replays a parallel plan: the row reads '1/4 已完成 · 实现 fixture 样本 +1' over the built bundles.
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* Todo display acceptance: the TodoPanel plan strip (empty-hidden, status
|
||||
* rows, collapse with active hint), its TodoDock adapter (selects the plan off
|
||||
* the session snapshot and follows changes), and the todo_write toolview row
|
||||
* (progress summary from args, generic fallback on malformed JSON, error badge,
|
||||
* keyboard activation).
|
||||
* Todo display acceptance: the shared plan model (counts + the one-line active
|
||||
* hint, which carries `+N` once parallel work marks several items in_progress),
|
||||
* the TodoPanel plan strip (empty-hidden, status rows, collapse with active
|
||||
* hint), its TodoDock adapter (selects the plan off the session snapshot and
|
||||
* follows changes), and the todo_write toolview row (progress summary from
|
||||
* args, generic fallback on malformed JSON, error badge, keyboard activation).
|
||||
*/
|
||||
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
@@ -16,6 +17,7 @@ import type { ToolRowProps } from '@deepseek-ai/dsh-client-ui-conversation/clien
|
||||
import { TodoRow, todoToolview } from '../src/client/toolviews/todo-row.tsx'
|
||||
import type { TodoDockProps } from '../src/client/skeleton/TodoPanel.tsx'
|
||||
import { TodoDock, TodoPanel, todoDockEntry } from '../src/client/skeleton/TodoPanel.tsx'
|
||||
import { planSummary } from '../src/client/contract/todo-plan-model.ts'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
@@ -25,6 +27,43 @@ const LIST: TodoItem[] = [
|
||||
{ content: '补测试', status: 'pending' },
|
||||
]
|
||||
|
||||
/** A parallel plan: three tasks running at once (concurrent subagents). */
|
||||
const PARALLEL: TodoItem[] = [
|
||||
{ content: '搭骨架', status: 'completed' },
|
||||
{ content: '写组件', status: 'in_progress' },
|
||||
{ content: '跑后台构建', status: 'in_progress' },
|
||||
{ content: '读源码', status: 'in_progress' },
|
||||
{ content: '补测试', status: 'pending' },
|
||||
]
|
||||
|
||||
describe('planSummary', () => {
|
||||
it('counts done/total and names the single active item verbatim', () => {
|
||||
expect(planSummary(LIST)).toEqual({ done: 1, total: 3, activeHint: '写组件' })
|
||||
})
|
||||
|
||||
it('suffixes the extra active count when several items are in progress', () => {
|
||||
// Parallel work marks several: naming one and hiding the rest would lose them.
|
||||
expect(planSummary(PARALLEL)).toEqual({ done: 1, total: 5, activeHint: '写组件 +2' })
|
||||
})
|
||||
|
||||
it('has no hint when nothing is in progress', () => {
|
||||
expect(planSummary([{ content: '都完了', status: 'completed' }]))
|
||||
.toEqual({ done: 1, total: 1, activeHint: null })
|
||||
})
|
||||
|
||||
it('has no hint when the first active item carries no usable content (model JSON)', () => {
|
||||
// Unvalidated args: a missing, mistyped, or empty content yields no hint,
|
||||
// even with a second active item that would otherwise supply the count.
|
||||
expect(planSummary([{ status: 'in_progress' }, { content: 'x', status: 'in_progress' }]).activeHint).toBeNull()
|
||||
expect(planSummary([{ content: 42, status: 'in_progress' }]).activeHint).toBeNull()
|
||||
expect(planSummary([{ content: '', status: 'in_progress' }]).activeHint).toBeNull()
|
||||
})
|
||||
|
||||
it('is empty-safe', () => {
|
||||
expect(planSummary([])).toEqual({ done: 0, total: 0, activeHint: null })
|
||||
})
|
||||
})
|
||||
|
||||
describe('TodoPanel', () => {
|
||||
it('renders nothing while the list is empty', () => {
|
||||
const { container } = render(<TodoPanel todos={[]} />)
|
||||
@@ -52,6 +91,19 @@ describe('TodoPanel', () => {
|
||||
expect(screen.getAllByRole('listitem')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('shows every parallel active item expanded, and counts the extra ones collapsed', () => {
|
||||
render(<TodoPanel todos={PARALLEL} />)
|
||||
// Expanded: one row per item, all three active ones carrying the ● glyph.
|
||||
const statuses = screen.getAllByRole('listitem').map(li => li.getAttribute('data-status'))
|
||||
expect(statuses.filter(s => s === 'in_progress')).toHaveLength(3)
|
||||
expect(screen.getByText('跑后台构建')).toBeTruthy()
|
||||
expect(screen.getByText('读源码')).toBeTruthy()
|
||||
// Collapsed: the hint reports the other two rather than dropping them.
|
||||
fireEvent.click(screen.getByRole('button', { expanded: true }))
|
||||
expect(screen.queryByRole('list')).toBeNull()
|
||||
expect(screen.getByText('写组件 +2')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('collapsed header omits the hint when nothing is in progress', () => {
|
||||
render(<TodoPanel todos={[{ content: '都完了', status: 'completed' }]} />)
|
||||
fireEvent.click(screen.getByRole('button', { expanded: true }))
|
||||
@@ -110,6 +162,11 @@ describe('TodoRow', () => {
|
||||
expect(screen.getByText('1/3 已完成 · 写组件')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('reports the extra active count when the written list runs several tasks', () => {
|
||||
render(<TodoRow {...rowProps(resultNode(JSON.stringify({ todos: PARALLEL })))} />)
|
||||
expect(screen.getByText('1/5 已完成 · 写组件 +2')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('omits the active clause when no item is in progress and reads running-call args', () => {
|
||||
const args = JSON.stringify({ todos: [{ content: 'x', status: 'completed' }] })
|
||||
render(<TodoRow {...rowProps({ callId: 'c1', name: 'todo_write', argsRaw: args, turn: 1, step: 1, time: 1_000, callView: null })} />)
|
||||
|
||||
Reference in New Issue
Block a user