fix(web): synchronize subagent navigation state

This commit is contained in:
imccyu
2026-08-02 01:38:28 +08:00
committed by Tianyi Cui
parent b94d2f9c1d
commit 23680e838b
25 changed files with 462 additions and 106 deletions

View File

@@ -84,6 +84,54 @@ function summary(id: SessionId, updatedAt: number): SessionSummary {
}
describe('SubagentCatalogAction', () => {
it('aggregates live descendant activity onto the closed trigger', () => {
const summaries: Record<SessionId, SessionSummary> = {
[CHILD]: {
...summary(CHILD, Date.now()),
parentId: PARENT,
origin: 'subagent',
},
[GRANDCHILD]: {
...summary(GRANDCHILD, Date.now()),
parentId: CHILD,
origin: 'subagent',
running: true,
},
['child-2' as SessionId]: {
...summary('child-2' as SessionId, Date.now()),
parentId: PARENT,
origin: 'subagent',
},
}
const view = render(<SubagentCatalogAction {...props(catalog(), {}, summaries)} />)
const trigger = screen.getByRole('button', { name: '3 个子代理,正在运行' })
expect(trigger.querySelector('[data-state="ongoing"]')).not.toBeNull()
view.rerender(<SubagentCatalogAction {...props(catalog(), {}, {
...summaries,
[GRANDCHILD]: { ...summaries[GRANDCHILD]!, running: false },
})} />)
expect(screen.getByRole('button', { name: '3 个子代理' })
.querySelector('[data-state="ongoing"]')).toBeNull()
})
it('does not aggregate subagents reached through an ordinary fork', () => {
const fork = 'fork' as SessionId
const forkChild = 'fork-child' as SessionId
render(<SubagentCatalogAction {...props(catalog(), {}, {
[CHILD]: { ...summary(CHILD, 1), parentId: PARENT, origin: 'subagent' },
['child-2' as SessionId]: {
...summary('child-2' as SessionId, 1), parentId: PARENT, origin: 'subagent',
},
[fork]: { ...summary(fork, 1), parentId: PARENT },
[forkChild]: { ...summary(forkChild, 1), parentId: fork, origin: 'subagent', running: true },
})} />)
const trigger = screen.getByRole('button', { name: '2 个子代理' })
expect(trigger.querySelector('[data-state="ongoing"]')).toBeNull()
})
it('renders healthy counts, stable rows, diagnostics, and catalog-addressed navigation', () => {
const input = props(catalog())
render(<SubagentCatalogAction {...input} />)
@@ -237,29 +285,54 @@ describe('SubagentCatalogAction', () => {
expect(input.setCatalogOpen).toHaveBeenCalledWith(CHILD, false)
})
it('shows initial descendant loading status without drawing its branch', () => {
const loading = props(catalog(), {
[CHILD]: catalog({ entries: [], state: 'loading' }),
})
const view = render(<SubagentCatalogAction {...loading} />)
it('shows known descendant rows while their catalog loads', () => {
const secondGrandchild = 'grandchild-2' as SessionId
const summaries = {
[GRANDCHILD]: {
...summary(GRANDCHILD, 1), parentId: CHILD, origin: 'subagent' as const,
},
[secondGrandchild]: {
...summary(secondGrandchild, 1), parentId: CHILD, origin: 'subagent' as const,
running: true,
},
}
const deferred = props(catalog(), {}, summaries)
const view = render(<SubagentCatalogAction {...deferred} />)
fireEvent.click(screen.getByRole('button', { name: /2 个子代理/ }))
fireEvent.click(screen.getByRole('button', { name: '展开 worker 的下级子代理' }))
expect(loading.setCatalogOpen).toHaveBeenCalledWith(CHILD, true)
expect(deferred.setCatalogOpen).toHaveBeenCalledWith(CHILD, true)
expect(screen.getByRole('group').getAttribute('aria-busy')).toBe('true')
expect(screen.getByText('正在加载子代理')).toBeTruthy()
const loadingRows = screen.getAllByRole('treeitem', { name: '正在加载子代理' })
expect(loadingRows).toHaveLength(2)
expect(loadingRows.every(row => row.getAttribute('aria-level') === '2')).toBe(true)
expect(loadingRows[1]?.querySelector('[data-state="ongoing"]')).not.toBeNull()
const loading = props(catalog(), {
[CHILD]: catalog({ entries: [], state: 'loading' }),
}, summaries)
view.rerender(<SubagentCatalogAction {...loading} />)
expect(screen.getAllByRole('treeitem', { name: '正在加载子代理' })).toHaveLength(2)
const ready = props(catalog(), {
[CHILD]: catalog({
entries: [{
kind: 'child', id: GRANDCHILD, mode: 'continuable',
label: 'indexer', activity: 'inactive', hasChildren: false,
}],
entries: [
{
kind: 'child', id: GRANDCHILD, mode: 'continuable',
label: 'indexer', activity: 'inactive', hasChildren: false,
},
{
kind: 'child', id: secondGrandchild, mode: 'one-shot',
label: 'critic', activity: 'running', hasChildren: false,
},
],
}),
})
}, summaries)
view.rerender(<SubagentCatalogAction {...ready} />)
expect(screen.getByRole('group').getAttribute('aria-busy')).toBeNull()
expect(screen.getByRole('treeitem', { name: /indexer/ })).toBeTruthy()
expect(screen.getByRole('treeitem', { name: /critic/ })).toBeTruthy()
expect(screen.queryByRole('treeitem', { name: '正在加载子代理' })).toBeNull()
})
it('uses ArrowRight and ArrowLeft for branch disclosure', async () => {