feat(workflow): auto-follow run and phase disclosures
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-workflow-run/README.md
|
||||
README.md: 66539e0c16ac4102f9e1fe881106e6881b36a7d5
|
||||
README.zh.md: a803857af24802e8a4645c4d5aca56c04424c85e
|
||||
README.md: 489715c51759b1efd2da68d3bd3e0f7788ce7ecd
|
||||
README.zh.md: 326a7ae4e4b8eaad43ca7ad0d22145452af6a734
|
||||
|
||||
@@ -12,7 +12,7 @@ Phase groups come only from members that actually started. Exact phase strings s
|
||||
|
||||
## Presentation and navigation
|
||||
|
||||
The run and each phase have independent disclosure state. The run uses a 32-pixel `--dsw-alias-bg-module-platform` row with persistent right/down chevrons and an inline state dot plus status text, without a badge. Phases use 32-pixel disclosure rows with title and member count in the flexible main area and a fixed precise aggregate-status tail, without another dot. Members use a 16-pixel dot slot, a truncating name area, and a fixed 64-pixel status column. A running run initially expands; a terminal run loaded from history initially collapses. Local choices survive data updates while the keyed node remains mounted and reset only on a full remount.
|
||||
The run and each phase derive disclosure control from their current lifecycle facts. The run stays expanded while its own status is running, failed, cancelled, or interrupted, or while any phase contains such a member; each affected phase also stays expanded. Forced-open headers are static expanded rows without button, keyboard, or `aria-expanded` promises. A phase folds once when every member completes, and the run folds once when it and every phase complete. Each clean layer then exposes an ordinary disclosure control whose local choice survives clean rerenders; new activity takes control again, and a remount derives the initial state from current data. The run uses a 32-pixel `--dsw-alias-bg-module-platform` row with persistent right/down chevrons and an inline state dot plus status text, without a badge. Phases use 32-pixel disclosure rows with title and member count in the flexible main area and a fixed precise aggregate-status tail, without another dot. Members use a 16-pixel dot slot, a truncating name area, and a fixed 64-pixel status column.
|
||||
|
||||
A member opens a child Session only while every current fact agrees: the member is running, the child id is in the ordinary Session list, the row has `origin: 'subagent'`, its `parentId` is the current Session, and the list row is still running. Underlined member text is the only visible navigation affordance; keyboard focus draws a two-pixel business-primary ring around the name area, while status copy remains `Running`. The component calls only the injected ordinary `sessions.open(id)` action; remote, addressed-only, wrong-parent, or terminal rows remain non-interactive.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
## 展示与导航
|
||||
|
||||
运行和每个阶段分别拥有本地 disclosure 状态。运行使用 32 像素 `--dsw-alias-bg-module-platform` 背景行,常驻向右/向下 chevron,并以内联状态点加状态文字表达结局,不使用胶囊。阶段使用 32 像素 disclosure 行,在可伸缩主区显示标题与成员数,在固定尾部精确显示聚合状态且不重复状态点。成员使用 16 像素状态点槽、可省略名称区和固定 64 像素状态列。运行中记录首次挂载时展开,从历史加载的终态记录首次挂载时折叠。只要 keyed 节点仍挂载,本地选择就在数据更新时保持;只有完整 remount 才重新初始化。
|
||||
运行和每个阶段都从当前生命周期事实派生 disclosure 控制。运行自身处于运行中、失败、已取消或已中断,或者任一阶段包含这些状态的成员时,运行保持展开;受影响的阶段也保持展开。强制展开的标题行只是静态展开行,不承诺按钮、键盘操作或 `aria-expanded`。阶段在全部成员完成时折叠一次;运行在自身和全部阶段都完成时折叠一次。每个干净层级随后恢复普通 disclosure 控件,其本地选择在干净状态的 rerender 中保持;新活动会重新取得控制,remount 则从当前数据派生初始状态。运行使用 32 像素 `--dsw-alias-bg-module-platform` 背景行,常驻向右/向下 chevron,并以内联状态点加状态文字表达结局,不使用胶囊。阶段使用 32 像素 disclosure 行,在可伸缩主区显示标题与成员数,在固定尾部精确显示聚合状态且不重复状态点。成员使用 16 像素状态点槽、可省略名称区和固定 64 像素状态列。
|
||||
|
||||
只有所有实时事实同时成立时,成员才可打开子 Session:成员仍在运行、子 id 位于普通 Session 列表、列表行为 `origin: 'subagent'`、`parentId` 等于当前 Session,且列表行仍标记运行。带下划线的成员文字是唯一可见导航提示;键盘聚焦时,名称区显示 2 像素 business-primary 焦点环,右侧状态仍只显示“运行中”。组件只调用注入的普通 `sessions.open(id)`;远程、仅地址化、父级不符或终态的行都不可交互。
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, type ReactNode } from 'react'
|
||||
import {
|
||||
DisclosureRow, IconChevronRightOutline14, StateDot, type StateDotState,
|
||||
DisclosureRow, IconChevronRightOutline14, StateDot,
|
||||
type DisclosureRowProps, type StateDotState,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { shallowEqual, type SessionId, type SessionListState } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
@@ -62,6 +63,34 @@ function memberCount(count: number, t: WorkflowRunPanelProps['t']): string {
|
||||
return t(count === 1 ? 'run.members.one' : 'run.members.other', { count })
|
||||
}
|
||||
|
||||
function phaseRequiresExpansion(phase: WorkflowRunPhaseData): boolean {
|
||||
return phase.members.some(member => member.status !== 'completed')
|
||||
}
|
||||
|
||||
type StatusDisclosureProps = Omit<DisclosureRowProps, 'open' | 'expandable' | 'onToggle'>
|
||||
|
||||
/* v8 ignore next -- DisclosureRow requires the callback but cannot invoke it when expandable is false. */
|
||||
const forcedOpenToggle = (): void => {}
|
||||
|
||||
function ManualDisclosure(props: StatusDisclosureProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<DisclosureRow
|
||||
{...props}
|
||||
open={open}
|
||||
expandable
|
||||
onToggle={() => { setOpen(value => !value) }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusDisclosure({ requiresExpansion, ...props }: StatusDisclosureProps & {
|
||||
readonly requiresExpansion: boolean
|
||||
}) {
|
||||
if (!requiresExpansion) return <ManualDisclosure {...props} />
|
||||
return <DisclosureRow {...props} open expandable={false} onToggle={forcedOpenToggle} />
|
||||
}
|
||||
|
||||
function phaseStatusSummary(members: readonly WorkflowRunMemberData[], t: WorkflowRunPanelProps['t']): string {
|
||||
const counts = new Map<WorkflowRunStatus, number>()
|
||||
for (const member of members) counts.set(member.status, (counts.get(member.status) ?? 0) + 1)
|
||||
@@ -97,21 +126,19 @@ function navigableMembers(
|
||||
return result
|
||||
}
|
||||
|
||||
function RunHeader({ count, name, onToggle, open, status, t }: {
|
||||
function RunHeader({ children, count, name, requiresExpansion, status, t }: {
|
||||
readonly children: ReactNode
|
||||
readonly count: number
|
||||
readonly name: string
|
||||
readonly onToggle: () => void
|
||||
readonly open: boolean
|
||||
readonly requiresExpansion: boolean
|
||||
readonly status: WorkflowRunStatus
|
||||
readonly t: WorkflowRunPanelProps['t']
|
||||
}) {
|
||||
return (
|
||||
<DisclosureRow
|
||||
<StatusDisclosure
|
||||
icon={<IconChevronRightOutline14 />}
|
||||
title={t('run.title', { name })}
|
||||
open={open}
|
||||
expandable
|
||||
onToggle={onToggle}
|
||||
requiresExpansion={requiresExpansion}
|
||||
expandOnRowClick
|
||||
previewChevron={false}
|
||||
keepContentWhenOpen
|
||||
@@ -128,7 +155,9 @@ function RunHeader({ count, name, onToggle, open, status, t }: {
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
>
|
||||
{children}
|
||||
</StatusDisclosure>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -168,15 +197,11 @@ function PhaseSection({ phase, navigable, openSession, t }: {
|
||||
readonly openSession: WorkflowRunInjected['openSession']
|
||||
readonly t: WorkflowRunPanelProps['t']
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const toggle = (): void => { setOpen(value => !value) }
|
||||
return (
|
||||
<DisclosureRow
|
||||
<StatusDisclosure
|
||||
icon={<IconChevronRightOutline14 />}
|
||||
title={readablePhase(phase.phase, t)}
|
||||
open={open}
|
||||
expandable
|
||||
onToggle={toggle}
|
||||
requiresExpansion={phaseRequiresExpansion(phase)}
|
||||
expandOnRowClick
|
||||
previewChevron={false}
|
||||
keepContentWhenOpen
|
||||
@@ -203,14 +228,15 @@ function PhaseSection({ phase, navigable, openSession, t }: {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</DisclosureRow>
|
||||
</StatusDisclosure>
|
||||
)
|
||||
}
|
||||
|
||||
/** Render one durable workflow run with independent run and phase disclosure. */
|
||||
/** Render one durable workflow run with status-driven run and phase disclosure. */
|
||||
export function WorkflowRunPanel({ node, sessionId, useSessions, openSession, t }: WorkflowRunPanelProps) {
|
||||
const [open, setOpen] = useState(() => node.data.status === 'running')
|
||||
const memberCount = node.data.phases.reduce((count, phase) => count + phase.members.length, 0)
|
||||
const totalMembers = node.data.phases.reduce((count, phase) => count + phase.members.length, 0)
|
||||
const requiresExpansion = node.data.status !== 'completed'
|
||||
|| node.data.phases.some(phaseRequiresExpansion)
|
||||
const navigable = useSessions(
|
||||
sessions => navigableMembers(sessions, node.data.phases, sessionId),
|
||||
shallowEqual,
|
||||
@@ -218,14 +244,12 @@ export function WorkflowRunPanel({ node, sessionId, useSessions, openSession, t
|
||||
return (
|
||||
<section className={css.root} data-workflow-run data-run-status={node.data.status}>
|
||||
<RunHeader
|
||||
count={memberCount}
|
||||
count={totalMembers}
|
||||
name={node.data.name}
|
||||
open={open}
|
||||
requiresExpansion={requiresExpansion}
|
||||
status={node.data.status}
|
||||
t={t}
|
||||
onToggle={() => { setOpen(value => !value) }}
|
||||
/>
|
||||
{open && (
|
||||
>
|
||||
<div className={css.phaseList}>
|
||||
{node.data.phases.length === 0
|
||||
? <span className={css.empty}>{t('run.empty')}</span>
|
||||
@@ -239,7 +263,7 @@ export function WorkflowRunPanel({ node, sessionId, useSessions, openSession, t
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</RunHeader>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -301,90 +301,148 @@ function panelProps(data: WorkflowRunChatData, sessions = listState(), openSessi
|
||||
}
|
||||
|
||||
describe('WorkflowRunPanel', () => {
|
||||
it('defaults running runs open, terminal history closed, and keeps the current choice across data updates', () => {
|
||||
it('forces running run and phase content open without false disclosure controls', () => {
|
||||
const view = render(<WorkflowRunPanel {...panelProps({
|
||||
name: 'audit', status: 'running', phases: [phase({ key: 'research', phase: 'Research' })],
|
||||
})} />)
|
||||
expect(screen.getByText('worker')).toBeTruthy()
|
||||
expect(screen.queryByRole('button', { name: /^audit/ })).toBeNull()
|
||||
expect(screen.queryByRole('button', { name: /Research/ })).toBeNull()
|
||||
const rows = [...view.container.querySelectorAll('[data-disclosure-row]')]
|
||||
expect(rows).toHaveLength(2)
|
||||
for (const row of rows) {
|
||||
expect(row.getAttribute('role')).toBeNull()
|
||||
expect(row.getAttribute('tabindex')).toBeNull()
|
||||
expect(row.getAttribute('aria-expanded')).toBeNull()
|
||||
expect(row.getAttribute('data-expandable')).toBeNull()
|
||||
}
|
||||
})
|
||||
|
||||
it('folds each clean transition once and preserves review choices until activity returns', () => {
|
||||
const running: WorkflowRunChatData = {
|
||||
name: 'audit', status: 'running', phases: [phase()],
|
||||
}
|
||||
const view = render(<WorkflowRunPanel {...panelProps(running)} />)
|
||||
expect(screen.getByText('未分阶段')).toBeTruthy()
|
||||
fireEvent.click(screen.getByRole('button', { name: /^audit/ }))
|
||||
expect(screen.queryByText('未分阶段')).toBeNull()
|
||||
const phaseCompleted: WorkflowRunChatData = {
|
||||
...running,
|
||||
phases: [phase({
|
||||
members: [{
|
||||
seq: 1, label: 'done', childId: 'child-1' as SessionId, status: 'completed',
|
||||
}],
|
||||
})],
|
||||
}
|
||||
view.rerender(<WorkflowRunPanel {...panelProps(phaseCompleted)} />)
|
||||
const phaseHeader = screen.getByRole('button', { name: /未分阶段/ })
|
||||
expect(phaseHeader.getAttribute('aria-expanded')).toBe('false')
|
||||
expect(screen.queryByText('done')).toBeNull()
|
||||
fireEvent.click(phaseHeader)
|
||||
expect(screen.getByText('done')).toBeTruthy()
|
||||
|
||||
const terminal: WorkflowRunChatData = { ...running, status: 'completed' }
|
||||
view.rerender(<WorkflowRunPanel {...panelProps(terminal)} />)
|
||||
const completed: WorkflowRunChatData = { ...phaseCompleted, status: 'completed' }
|
||||
view.rerender(<WorkflowRunPanel {...panelProps(completed)} />)
|
||||
const runHeader = screen.getByRole('button', { name: /^audit/ })
|
||||
expect(runHeader.getAttribute('aria-expanded')).toBe('false')
|
||||
expect(screen.queryByText('未分阶段')).toBeNull()
|
||||
fireEvent.keyDown(runHeader, { key: 'ArrowDown' })
|
||||
expect(runHeader.getAttribute('aria-expanded')).toBe('false')
|
||||
fireEvent.keyDown(runHeader, { key: 'Enter' })
|
||||
expect(runHeader.getAttribute('aria-expanded')).toBe('true')
|
||||
const completedPhase = screen.getByRole('button', { name: /未分阶段/ })
|
||||
fireEvent.keyDown(completedPhase, { key: 'Enter' })
|
||||
expect(screen.getByText('done')).toBeTruthy()
|
||||
fireEvent.keyDown(runHeader, { key: ' ' })
|
||||
expect(runHeader.getAttribute('aria-expanded')).toBe('false')
|
||||
fireEvent.keyDown(runHeader, { key: ' ' })
|
||||
expect(runHeader.getAttribute('aria-expanded')).toBe('true')
|
||||
fireEvent.click(screen.getByRole('button', { name: /未分阶段/ }))
|
||||
expect(screen.getByText('done')).toBeTruthy()
|
||||
|
||||
cleanup()
|
||||
render(<WorkflowRunPanel {...panelProps(terminal)} />)
|
||||
const cleanUpdate: WorkflowRunChatData = {
|
||||
...completed,
|
||||
phases: [phase({
|
||||
members: [{
|
||||
seq: 1, label: 'reviewed', childId: 'child-1' as SessionId, status: 'completed',
|
||||
}],
|
||||
})],
|
||||
}
|
||||
view.rerender(<WorkflowRunPanel {...panelProps(cleanUpdate)} />)
|
||||
expect(screen.getByText('reviewed')).toBeTruthy()
|
||||
|
||||
view.rerender(<WorkflowRunPanel {...panelProps(running)} />)
|
||||
expect(screen.queryByRole('button', { name: /^audit/ })).toBeNull()
|
||||
expect(screen.queryByRole('button', { name: /未分阶段/ })).toBeNull()
|
||||
expect(screen.getByText('worker')).toBeTruthy()
|
||||
view.rerender(<WorkflowRunPanel {...panelProps(completed)} />)
|
||||
expect(screen.getByRole('button', { name: /^audit/ }).getAttribute('aria-expanded')).toBe('false')
|
||||
expect(screen.queryByText('未分阶段')).toBeNull()
|
||||
})
|
||||
|
||||
it('supports root keyboard disclosure and renders a zero-member running state', () => {
|
||||
render(<WorkflowRunPanel {...panelProps({
|
||||
name: 'keyboard', status: 'running',
|
||||
phases: [phase({ key: 'research', phase: 'Research' })],
|
||||
})} />)
|
||||
const header = screen.getByRole('button', { name: /^keyboard/ })
|
||||
expect(header.getAttribute('aria-expanded')).toBe('true')
|
||||
fireEvent.keyDown(header, { key: 'ArrowDown' })
|
||||
expect(header.getAttribute('aria-expanded')).toBe('true')
|
||||
fireEvent.keyDown(header, { key: 'Enter' })
|
||||
it('derives the zero-member running and completed states from the current run status', () => {
|
||||
const running: WorkflowRunChatData = { name: 'empty', status: 'running', phases: [] }
|
||||
const view = render(<WorkflowRunPanel {...panelProps(running)} />)
|
||||
expect(screen.queryByRole('button', { name: /^empty/ })).toBeNull()
|
||||
expect(screen.getByText('没有启动成员')).toBeTruthy()
|
||||
view.rerender(<WorkflowRunPanel {...panelProps({ ...running, status: 'completed' })} />)
|
||||
const header = screen.getByRole('button', { name: /^empty/ })
|
||||
expect(header.getAttribute('aria-expanded')).toBe('false')
|
||||
fireEvent.keyDown(header, { key: ' ' })
|
||||
expect(header.getAttribute('aria-expanded')).toBe('true')
|
||||
expect(screen.getByText('Research')).toBeTruthy()
|
||||
expect(screen.getByText('运行中 1')).toBeTruthy()
|
||||
const phaseHeader = screen.getByRole('button', { name: /Research/ })
|
||||
fireEvent.keyDown(phaseHeader, { key: 'ArrowDown' })
|
||||
expect(phaseHeader.getAttribute('aria-expanded')).toBe('false')
|
||||
fireEvent.keyDown(phaseHeader, { key: 'Enter' })
|
||||
expect(phaseHeader.getAttribute('aria-expanded')).toBe('true')
|
||||
fireEvent.keyDown(phaseHeader, { key: ' ' })
|
||||
expect(phaseHeader.getAttribute('aria-expanded')).toBe('false')
|
||||
|
||||
cleanup()
|
||||
render(<WorkflowRunPanel {...panelProps({
|
||||
name: 'empty', status: 'running', phases: [],
|
||||
})} />)
|
||||
expect(screen.queryByText('没有启动成员')).toBeNull()
|
||||
fireEvent.click(header)
|
||||
expect(screen.getByText('没有启动成员')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('keeps phase disclosure independent and preserves empty versus absent names', () => {
|
||||
it.each(['failed', 'cancelled', 'interrupted'] as const)(
|
||||
'bubbles a %s member to the run and keeps a matching run outcome open',
|
||||
(status) => {
|
||||
const memberView = render(<WorkflowRunPanel {...panelProps({
|
||||
name: 'member-outcome', status: 'completed',
|
||||
phases: [phase({
|
||||
members: [{ seq: 1, label: status, childId: CHILD_ID, status }],
|
||||
})],
|
||||
})} />)
|
||||
expect(screen.queryByRole('button', { name: /^member-outcome/ })).toBeNull()
|
||||
expect(screen.queryByRole('button', { name: /未分阶段/ })).toBeNull()
|
||||
expect(screen.getByText(status)).toBeTruthy()
|
||||
memberView.unmount()
|
||||
|
||||
render(<WorkflowRunPanel {...panelProps({
|
||||
name: 'run-outcome', status,
|
||||
phases: [phase({
|
||||
members: [{ seq: 1, label: 'done', childId: CHILD_ID, status: 'completed' }],
|
||||
})],
|
||||
})} />)
|
||||
expect(screen.queryByRole('button', { name: /^run-outcome/ })).toBeNull()
|
||||
expect(screen.getByRole('button', { name: /未分阶段/ }).getAttribute('aria-expanded')).toBe('false')
|
||||
expect(screen.queryByText('done')).toBeNull()
|
||||
},
|
||||
)
|
||||
|
||||
it('keeps clean sibling phases independent and preserves empty versus absent names', () => {
|
||||
render(<WorkflowRunPanel {...panelProps({
|
||||
name: 'audit', status: 'running',
|
||||
name: 'audit', status: 'completed',
|
||||
phases: [
|
||||
phase({ key: 'value:0:', phase: '', members: [{
|
||||
seq: 1, label: '', childId: 'child-1' as SessionId, status: 'running',
|
||||
seq: 1, label: '', childId: 'child-1' as SessionId, status: 'completed',
|
||||
}] }),
|
||||
phase({ key: 'missing', phase: null, members: [{
|
||||
seq: 2, label: 'second', childId: 'child-2' as SessionId, status: 'running',
|
||||
}] }),
|
||||
],
|
||||
})} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: /空阶段名/ }))
|
||||
expect(screen.getByText('空成员名')).toBeTruthy()
|
||||
expect(screen.queryByText('second')).toBeNull()
|
||||
fireEvent.click(screen.getByRole('button', { name: /未分阶段/ }))
|
||||
expect(screen.queryByRole('button', { name: /^audit/ })).toBeNull()
|
||||
const cleanPhase = screen.getByRole('button', { name: /空阶段名/ })
|
||||
expect(cleanPhase.getAttribute('aria-expanded')).toBe('false')
|
||||
expect(screen.queryByRole('button', { name: /未分阶段/ })).toBeNull()
|
||||
expect(screen.queryByText('空成员名')).toBeNull()
|
||||
expect(screen.getByText('second')).toBeTruthy()
|
||||
fireEvent.click(screen.getByRole('button', { name: /空阶段名/ }))
|
||||
fireEvent.click(cleanPhase)
|
||||
expect(screen.getByText('空成员名')).toBeTruthy()
|
||||
expect(screen.getByText('second')).toBeTruthy()
|
||||
fireEvent.click(cleanPhase)
|
||||
expect(screen.queryByText('空成员名')).toBeNull()
|
||||
expect(screen.getByText('second')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('covers the Figma completed, failed/cancelled, and interrupted state boards', () => {
|
||||
const completed: WorkflowRunChatData = {
|
||||
name: 'repo-audit', status: 'completed',
|
||||
phases: [phase({
|
||||
members: [{ seq: 1, label: 'done', childId: 'child-1' as SessionId, status: 'completed' }],
|
||||
})],
|
||||
}
|
||||
const completedView = render(<WorkflowRunPanel {...panelProps(completed)} />)
|
||||
const completedHeader = screen.getByRole('button', { name: /^repo-audit/ })
|
||||
expect(completedHeader.getAttribute('aria-expanded')).toBe('false')
|
||||
fireEvent.click(completedHeader)
|
||||
expect(completedHeader.getAttribute('aria-expanded')).toBe('true')
|
||||
completedView.unmount()
|
||||
|
||||
it('renders mixed and interrupted aggregate status while attention stays visible', () => {
|
||||
const mixed: WorkflowRunChatData = {
|
||||
name: 'repo-audit', status: 'failed',
|
||||
phases: [phase({
|
||||
@@ -395,8 +453,6 @@ describe('WorkflowRunPanel', () => {
|
||||
})],
|
||||
}
|
||||
const mixedView = render(<WorkflowRunPanel {...panelProps(mixed)} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: /^repo-audit/ }))
|
||||
fireEvent.click(screen.getByRole('button', { name: /未分阶段/ }))
|
||||
expect(screen.getByText('失败 1 · 已取消 1')).toBeTruthy()
|
||||
expect([...mixedView.container.querySelectorAll('[data-member-status]')]
|
||||
.map(row => row.getAttribute('data-member-status'))).toEqual(['failed', 'cancelled'])
|
||||
@@ -404,28 +460,18 @@ describe('WorkflowRunPanel', () => {
|
||||
expect(mixedView.container.querySelectorAll('[data-state="warning"]')).toHaveLength(1)
|
||||
mixedView.unmount()
|
||||
|
||||
const interrupted: WorkflowRunChatData = {
|
||||
const interruptedView = render(<WorkflowRunPanel {...panelProps({
|
||||
name: 'repo-audit', status: 'interrupted',
|
||||
phases: [
|
||||
phase({
|
||||
members: [
|
||||
{ seq: 1, label: 'done', childId: 'child-1' as SessionId, status: 'completed' },
|
||||
{ seq: 2, label: 'interrupted', childId: 'child-2' as SessionId, status: 'interrupted' },
|
||||
],
|
||||
}),
|
||||
phase({
|
||||
key: 'interrupted-only', phase: 'Interrupted only',
|
||||
members: [{
|
||||
seq: 3, label: 'interrupted', childId: 'child-3' as SessionId, status: 'interrupted',
|
||||
}],
|
||||
}),
|
||||
],
|
||||
}
|
||||
const interruptedView = render(<WorkflowRunPanel {...panelProps(interrupted)} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: /^repo-audit/ }))
|
||||
phases: [phase({
|
||||
members: [
|
||||
{ seq: 1, label: 'done', childId: 'child-1' as SessionId, status: 'completed' },
|
||||
{ seq: 2, label: 'interrupted', childId: 'child-2' as SessionId, status: 'interrupted' },
|
||||
],
|
||||
})],
|
||||
})} />)
|
||||
expect(screen.getByText('已完成 1 · 已中断 1')).toBeTruthy()
|
||||
expect(interruptedView.container.querySelector('[data-run-status="interrupted"]')).toBeTruthy()
|
||||
expect(interruptedView.container.querySelectorAll('[data-state="warning"]')).toHaveLength(1)
|
||||
expect(interruptedView.container.querySelectorAll('[data-state="warning"]')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('opens only a running ordinary-list subagent proven to have this parent', () => {
|
||||
@@ -434,7 +480,6 @@ describe('WorkflowRunPanel', () => {
|
||||
}
|
||||
const openSession = vi.fn()
|
||||
render(<WorkflowRunPanel {...panelProps(data, listState(), openSession)} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: /未分阶段/ }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开 worker' }))
|
||||
expect(openSession).toHaveBeenCalledWith('child-1')
|
||||
})
|
||||
@@ -464,7 +509,6 @@ describe('WorkflowRunPanel', () => {
|
||||
})],
|
||||
}
|
||||
render(<WorkflowRunPanel {...panelProps(data, sessions)} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: /未分阶段/ }))
|
||||
expect(screen.queryByRole('button', { name: '打开 worker' })).toBeNull()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user