| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133 |
- // Web e2e scenario: the session-header background-task list over the real
- // host. No model call is involved — a genuine `run_in_background` bash call
- // registers with `ctx.tasks`, and the assertion chain is the whole delivery
- // path: registry change feed → api-proxy `session/tasks` frame → the client's
- // `tasksBySession` mirror → the header action.
- import { readFile } from 'node:fs/promises'
- import { fileURLToPath } from 'node:url'
- import { join } from 'node:path'
- import type { Browser, Page } from 'playwright'
- import { chromium } from 'playwright'
- import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
- import type { Agent } from '@deepseek-ai/dsh-agent'
- import { CallId } from '@deepseek-ai/dsh-llm'
- import { SessionId } from '@deepseek-ai/dsh-session'
- import { TaskId } from '@deepseek-ai/dsh-tasks'
- import {
- assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
- launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
- } from './scaffold.ts'
- import { newEnglishPage, saveFailureShot } from './support.ts'
- const FIXTURE = fileURLToPath(new URL('./snapshots/fresh-round-trip/session.jsonl', import.meta.url))
- const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/background-task-list', import.meta.url))
- const RUNNING_EXPECTED = join(SNAPSHOT_DIR, 'running.expected.md')
- const SETTLED_EXPECTED = join(SNAPSHOT_DIR, 'settled.expected.md')
- const MODE = webSnapshotMode()
- const SEED_ID = 'background-task-list-web-e2e'
- // Long enough that the running assertions never race the process exiting on
- // their own; the test kills it explicitly to reach the settled state.
- const COMMAND = 'sleep 45'
- /**
- * Wait for the Host to publish the live Agent that opening a session resumes.
- * @param scaffold - the booted web scaffold.
- * @param sessionId - the opened session's identity.
- * @returns the registered Agent instance.
- */
- async function liveAgent(scaffold: WebScaffold, sessionId: SessionId): Promise<Agent> {
- const deadline = Date.now() + 30_000
- for (;;) {
- const found = scaffold.ctx.agents.get(sessionId)
- if (found !== undefined) return found
- if (Date.now() > deadline) throw new Error(`opening session "${sessionId}" published no live Agent`)
- await new Promise(resolve => setTimeout(resolve, 100))
- }
- }
- describe.skipIf(MODE === 'record')('web e2e: background task list', () => {
- let scaffold: WebScaffold
- let browser: Browser
- let page: Page
- let tripwire: ReturnType<typeof watchConsole>
- let agent: Agent
- let taskId: TaskId
- beforeAll(async () => {
- scaffold = await launchWebScaffold({})
- await seedSession(scaffold, await readFile(FIXTURE, 'utf8'), SEED_ID)
- 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 })
- const groupRow = page.locator('[role="treeitem"]').first()
- await groupRow.waitFor({ timeout: 15_000 })
- await groupRow.click()
- const sessionRow = page.locator('[role="treeitem"]').nth(1)
- await sessionRow.waitFor({ timeout: 10_000 })
- await sessionRow.click()
- // Opening the session drives the Host's ordinary Agent resolution; the
- // task owner must be that exact live instance, never a second one.
- // `expect.poll` is test-scoped, so this hook polls by hand.
- agent = await liveAgent(scaffold, SessionId(SEED_ID))
- }, 120_000)
- afterAll(async () => {
- await browser?.close()
- await scaffold?.close()
- })
- it('shows a running background task in the session header without a refresh', async () => {
- onTestFailed(() => saveFailureShot(page, 'web-e2e-background-task-running'))
- // Point assertion, not a poll: `expect.poll` retries until a predicate
- // holds, so polling for zero passes at t=0 and proves nothing. The
- // "renders nothing without a task" branch is owned by the component suite.
- const trigger = page.getByRole('button', { name: '1 background task running' })
- expect(await trigger.count()).toBe(0)
- const started = await scaffold.ctx.tools.execute({
- signal: new AbortController().signal,
- callId: CallId('background-task-list-e2e'),
- name: 'bash',
- arguments: { command: COMMAND, description: 'Hold a background slot open', run_in_background: true },
- agent,
- })
- const reported = started.content.map(block => block.type === 'text' ? block.text : '').join('')
- const matched = /\bbash-\d+\b/.exec(reported)
- if (matched === null) throw new Error(`background bash reported no task id: ${reported}`)
- taskId = TaskId(matched[0])
- await trigger.waitFor({ timeout: 15_000 })
- await trigger.click()
- const row = page.getByRole('list', { name: 'Background tasks' }).getByRole('listitem').first()
- await row.waitFor({ timeout: 10_000 })
- await expect.poll(() => row.textContent()).toContain(COMMAND)
- const snapshot = await captureStableAria(page, '[class*="menu"]', scaffold.workspaceCwd)
- await compareOrRefreshGolden(RUNNING_EXPECTED, snapshot, MODE)
- expect(tripwire.pageErrors).toEqual([])
- expect(tripwire.warnings).toEqual([])
- }, 60_000)
- it('flips the open list to the cancelled outcome when the registry settles it', async () => {
- onTestFailed(() => saveFailureShot(page, 'web-e2e-background-task-settled'))
- expect(scaffold.ctx.tasks.kill(taskId, agent, 'web e2e cancellation')).toBe('requested')
- // The trigger drops its live count once the task leaves running/stopping,
- // which is also the proof that settlement reached the browser unprompted.
- const idle = page.getByRole('button', { name: '1 background task' })
- await idle.waitFor({ timeout: 20_000 })
- const snapshot = await captureStableAria(page, '[class*="menu"]', scaffold.workspaceCwd)
- await compareOrRefreshGolden(SETTLED_EXPECTED, snapshot, MODE)
- expect(tripwire.pageErrors).toEqual([])
- expect(tripwire.warnings).toEqual([])
- }, 60_000)
- it('keeps its snapshot inventory closed', async () => {
- await assertFixtureInventory(SNAPSHOT_DIR, ['running.expected.md', 'settled.expected.md'])
- })
- })
|