test(web-shell): witness the downstream click, attention word and cascade order

- click the downstream 'Unblocks' reference in the step-details panel and
  assert the named node gains aria-pressed, so a dropped onClick in the
  selectedDependents block turns red
- render a node into attention and assert its sr-only span carries the
  status word beside "Needs attention"; pin the inspector's self-block
  exclusion beside the dedup test
- extend the cascade-order guard to all six status selectors, not just
  blocked, so regrouping any status tone below the attention rule fails

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmtnn5c4cjw
This commit is contained in:
jinjing.zzjandQwen-Coder committed 2026-09-05 09:31:49 +08:00
1 parent ee08bf95e8
commit 18f9fa0721
3 files changed
+133 -8

No files matched your search

@@ -82,16 +82,27 @@ describe('PlanExecutionView stylesheet', () => {
expect(title).toMatch(/color:\s*var\(--foreground\)/);
});
it('declares the attention tone after the blocked/ready tone', () => {
// Both selectors carry the same specificity, so the cascade lets the
// later rule win: attention must stay declared after the status tones,
// or a blocked node that also needs attention would wear the muted
// ring instead of the attention tone its comment promises.
it('declares the attention tone after every status tone', () => {
// All these selectors carry the same specificity, so the cascade lets
// the later rule win. Attention co-occurs with every status (a linked
// agent task can fail under any of them), so the attention rule must
// stay declared after every status tone, or a node that needs attention
// would wear its status ring instead of the attention tone its comment
// promises — whichever status block a later edit regroups.
const attentionAt = planCss.indexOf(".node[data-attention='true']");
const neutralAt = planCss.indexOf(".node[data-status='blocked']");
expect(attentionAt).toBeGreaterThan(-1);
expect(neutralAt).toBeGreaterThan(-1);
expect(attentionAt).toBeGreaterThan(neutralAt);
for (const status of [
'completed',
'running',
'in_progress',
'paused',
'blocked',
'ready',
]) {
const statusAt = planCss.indexOf(`.node[data-status='${status}']`);
expect(statusAt).toBeGreaterThan(-1);
expect(attentionAt).toBeGreaterThan(statusAt);
}
});
it('narrows the DAG lane at two viewport steps', () => {
@@ -594,6 +594,33 @@ describe('PlanExecutionView', () => {
act(() => button?.click());
expect(onOpen).toHaveBeenCalledWith(agentTool('build'));
// The downstream block wires its own onClick; witness it before the
// upstream click below moves the selection off `build`. Dropping that
// handler must turn this red.
act(() =>
details
?.querySelector<HTMLButtonElement>('[data-plan-dependency="verify"]')
?.click(),
);
expect(
container
.querySelector('[data-plan-node-id="verify"]')
?.getAttribute('aria-pressed'),
).toBe('true');
// That click selected `verify` and re-rendered the panel around it, so
// bring the selection back to `build` before exercising the upstream
// reference.
act(() => {
container
.querySelector<HTMLButtonElement>('[data-plan-node-id="build"]')
?.click();
});
expect(
container
.querySelector('[data-plan-node-id="build"]')
?.getAttribute('aria-pressed'),
).toBe('true');
// Last, because it moves the selection: following an upstream reference
// selects the step it names, which is what makes the dependency list the
// graph's navigation rather than a run of text.
@@ -663,6 +690,46 @@ describe('PlanExecutionView', () => {
container.remove();
});
it('announces attention beside the status word, not instead of it', () => {
// A failed descendant puts the node into attention while its own status
// stays running. The sr-only span must keep announcing the status word
// too — attention is additive for assistive tech, never a replacement.
const runningRoot = task('running');
const failedChild = task('failed', {
id: 'agent-child',
toolUseId: 'call-child',
parentAgentId: runningRoot.id,
});
const container = document.createElement('div');
document.body.appendChild(container);
const root = createRoot(container);
act(() => {
root.render(
<I18nProvider language="en">
<PlanExecutionView
todos={todos}
tools={[agentTool('build')]}
tasks={[runningRoot, failedChild]}
/>
</I18nProvider>,
);
});
const buildNode = container
.querySelector('[data-plan-node-id="build"]')
?.closest('article');
expect(buildNode?.textContent).toContain('Running');
expect(buildNode?.textContent).toContain('Needs attention');
expect(
buildNode
?.querySelector(`.${styles.nodeStatusText}`)
?.textContent?.trim(),
).toBe('Running, Needs attention');
act(() => root.unmount());
container.remove();
});
// R7-2: a replayed transcript of an interrupted session carries Agent tool
// calls still in_progress (or paused) with NO live daemon task. The node
// badges render Running/Paused off executionStatus's transcript fallback;
@@ -266,6 +266,53 @@ describe('SessionWorkflowInspector', () => {
container.remove();
});
it('drops a self-reference from the upstream list', () => {
// The projection's dependentsByTodo builder skips self-blocks
// (session-workflow-model.ts), and the DAG draws no edge for them; the
// inspector's upstream filter must apply the same rule, or a todo
// naming itself in blockedBy renders a chip that only re-selects the
// already-selected step.
const container = document.createElement('div');
document.body.appendChild(container);
const root = createRoot(container);
const todos: TodoItem[] = [
{ id: 'prepare', content: 'Prepare inputs', status: 'completed' },
{
id: 'ship',
content: 'Ship result',
status: 'in_progress',
blockedBy: ['ship', 'prepare'],
},
];
act(() => {
root.render(
<I18nProvider language="en">
<SessionWorkflowInspector
todos={todos}
tools={[]}
tasks={[]}
artifacts={[]}
selectedTodoId="ship"
onSelectedTodoIdChange={vi.fn()}
onExpandGraph={vi.fn()}
onOpenSubagent={vi.fn()}
/>
</I18nProvider>,
);
});
expect(
container.querySelector('[data-testid="workflow-dependency-ship"]'),
).toBeNull();
expect(
container.querySelectorAll('[data-testid="workflow-dependency-prepare"]'),
).toHaveLength(1);
act(() => root.unmount());
container.remove();
});
it('keeps every activity row reachable past the preview cap', () => {
const container = document.createElement('div');
document.body.appendChild(container);