// Keyless shipped-Web acceptance for the durable workflow Conversation Node. // Reuses the existing recorded workflow parent/child model fixtures; the real // workflow tool, worker, subagent provider, Session log, browser plugin graph, // and navigation all execute during replay. import { readFile } from 'node:fs/promises' import { join } from 'node:path' import { fileURLToPath } from 'node:url' import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session' import { assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' import { connectFreshWorkspace, newEnglishPage, REPO_ROOT, saveFailureShot, } from './support.ts' const MODE = webSnapshotMode() const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/workflow-run', import.meta.url)) const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md') const PARENT_FIXTURE = join(REPO_ROOT, 'examples/acp-agent/tests/snapshots/workflow-run/session.jsonl') const CHILD_FIXTURE = join(REPO_ROOT, 'examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl') const CHILD_PROMPT = 'Reply with exactly the word WF_CHILD_OK and nothing else.' describe.skipIf(MODE === 'record')('web e2e: durable workflow run in Chat', () => { let scaffold: WebScaffold let browser: Browser let page: Page let tripwire: ReturnType let prompt: string const waitForParentSettlement = (): Promise => new Promise((resolve, reject) => { let dispose = (): void => {} dispose = scaffold.ctx.on('session/event', (session: Session, event: SessionEvent) => { if (event.type !== 'turn/end' || session.header.origin === 'subagent') return dispose() void (async () => { await scaffold.ctx.agents.get(session.id)?.whenIdle() await scaffold.ctx.sessions.flush(session) resolve(session.id) })().catch(reject) }) }) beforeAll(async () => { const prompts = fixtureUserPrompts(await readFile(PARENT_FIXTURE, 'utf8')) expect(prompts).toHaveLength(1) prompt = prompts[0]! scaffold = await launchWebScaffold({ replayFixture: PARENT_FIXTURE, replayChildFixtures: [CHILD_FIXTURE], paceMs: 25, }) browser = await chromium.launch() page = await newEnglishPage(browser) tripwire = watchConsole(page) await page.goto(scaffold.baseUrl, { waitUntil: 'load' }) await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) await connectFreshWorkspace(page, scaffold.workspaceCwd) }, 120_000) afterAll(async () => { await browser?.close() await scaffold?.close() }) it('shows the live member, opens its local child, then retains the settled record beside the tool row', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-workflow-run-live')) const settled = waitForParentSettlement() const input = page.locator('textarea').first() await input.fill(prompt) await input.press('Enter') const workflow = page.getByRole('button', { name: /^snapshot-flow/ }) await workflow.waitFor({ timeout: 30_000 }) expect(await workflow.getAttribute('aria-expanded')).toBe('true') const phase = page.getByRole('button', { name: /^Run/ }) await phase.waitFor({ timeout: 15_000 }) await phase.click() const member = page.getByRole('button', { name: /^Open Reply with exactly the word/ }) await member.waitFor({ timeout: 15_000 }) await member.focus() const lightColor = await member.locator('[data-member-label]').evaluate(element => getComputedStyle(element).color) await page.setViewportSize({ width: 560, height: 800 }) await page.evaluate(() => { document.body.setAttribute('data-ds-dark-theme', '') }) const darkNarrow = await page.locator('[data-workflow-run]').evaluate((element) => { const panel = element as HTMLElement panel.style.width = '356px' const label = element.querySelector('[data-member-label]') const labelWrap = element.querySelector('[data-member-label-wrap]') const status = element.querySelector('[data-member-status-text]') const disclosures = element.querySelectorAll('[data-disclosure-row]') const runHeader = disclosures[0] const phaseHeader = disclosures[1] const phaseTitle = phaseHeader?.children.item(1) as HTMLElement | null const phaseStatus = element.querySelector('[data-phase-status-text]') const originalPhaseTitle = phaseTitle?.textContent ?? '' if (phaseTitle !== null) phaseTitle.textContent = 'A phase name long enough to require ellipsis in the narrow layout' const phaseTitleRight = phaseTitle?.getBoundingClientRect().right ?? 0 const phaseStatusLeft = phaseStatus?.getBoundingClientRect().left ?? 0 if (phaseTitle !== null) phaseTitle.textContent = originalPhaseTitle return { clientWidth: element.clientWidth, scrollWidth: element.scrollWidth, color: label === null ? '' : getComputedStyle(label).color, decoration: label === null ? '' : getComputedStyle(label).textDecorationLine, focusWidth: labelWrap === null ? '' : getComputedStyle(labelWrap).outlineWidth, statusWidth: status?.getBoundingClientRect().width ?? 0, statusFontSize: status === null ? '' : getComputedStyle(status).fontSize, runHeight: runHeader?.getBoundingClientRect().height ?? 0, phaseHeight: phaseHeader?.getBoundingClientRect().height ?? 0, phaseTitleRight, phaseStatusLeft, } }) expect(darkNarrow.clientWidth).toBe(356) expect(darkNarrow.scrollWidth).toBeLessThanOrEqual(darkNarrow.clientWidth) expect(darkNarrow.color).not.toBe(lightColor) expect(darkNarrow.decoration).toContain('underline') expect(Number.parseFloat(darkNarrow.focusWidth)).toBeGreaterThanOrEqual(2) expect(darkNarrow.statusWidth).toBe(64) expect(darkNarrow.statusFontSize).toBe('13px') expect(darkNarrow.runHeight).toBe(32) expect(darkNarrow.phaseHeight).toBe(32) expect(darkNarrow.phaseTitleRight).toBeLessThanOrEqual(darkNarrow.phaseStatusLeft) await page.locator('[data-workflow-run]').evaluate((element) => { (element as HTMLElement).style.removeProperty('width') document.body.removeAttribute('data-ds-dark-theme') }) await page.setViewportSize({ width: 1280, height: 800 }) await member.click() await page.getByText(CHILD_PROMPT, { exact: true }).waitFor({ timeout: 15_000 }) const sessions = page.getByRole('tree', { name: 'Sessions' }) await sessions.getByRole('treeitem', { name: /Use the workflow tool exactly/ }).click() await settled expect(await page.locator('[data-chat-flow-kind="tool-call"]').count()).toBeGreaterThanOrEqual(1) expect(await page.locator('[data-chat-flow-kind="workflow-run"]').count()).toBe(1) const terminalWorkflow = page.getByRole('button', { name: /^snapshot-flow/ }) await terminalWorkflow.waitFor() if (await terminalWorkflow.getAttribute('aria-expanded') !== 'true') await terminalWorkflow.click() const terminalPhase = page.getByRole('button', { name: /^Run/ }) await terminalPhase.waitFor() if (await terminalPhase.getAttribute('aria-expanded') !== 'true') await terminalPhase.click() await page.getByText(CHILD_PROMPT, { exact: false }).waitFor() await expect.poll( () => page.getByRole('button', { name: /^Open Reply with exactly the word/ }).count(), { timeout: 10_000 }, ).toBe(0) }, 90_000) it('rebuilds the terminal record from history after reload', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-workflow-run-history')) await page.reload({ waitUntil: 'load' }) await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) const workflow = page.getByRole('button', { name: /^snapshot-flow/ }) await workflow.waitFor({ timeout: 15_000 }) expect(await workflow.getAttribute('aria-expanded')).toBe('false') await workflow.click() const phase = page.getByRole('button', { name: /^Run/ }) await phase.waitFor() await phase.click() await page.getByText(CHILD_PROMPT, { exact: false }).waitFor() expect(await page.getByRole('button', { name: /^Open Reply with exactly the word/ }).count()).toBe(0) const snapshot = await captureStableAria(page, '[data-chat-flow]', scaffold.workspaceCwd) await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE) }, 60_000) it('stays clean and owns only its one golden', async () => { expect(tripwire.pageErrors).toEqual([]) expect(tripwire.warnings).toEqual([]) await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md']) }) })