Merge master into subagent usage branch
# Conflicts: # .agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.i18n.yaml # .agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md # .agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.zh.md # apps/web/tests/snapshots/subagent-conversation/tree.expected.md # apps/web/tests/subagent-conversation.e2e.ts # packages/client/ui-subagent/src/client/SubagentCatalogAction.tsx
This commit is contained in:
@@ -15,11 +15,12 @@ import {
|
||||
launchWebScaffold, watchConsole,
|
||||
webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, saveFailureShot } from './support.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const BASE_FIXTURE = fileURLToPath(new URL('./snapshots/live-interactions/session.jsonl', import.meta.url))
|
||||
const AVAILABLE_CHILD_EXPECTED = fileURLToPath(new URL('./snapshots/subagent-conversation/ui.expected.md', import.meta.url))
|
||||
const TREE_EXPECTED = fileURLToPath(new URL('./snapshots/subagent-conversation/tree.expected.md', import.meta.url))
|
||||
const STALE_CATALOG_EXPECTED = fileURLToPath(new URL('./snapshots/subagent-conversation/stale-catalog.expected.md', import.meta.url))
|
||||
const SIDEBAR_EXPECTED = fileURLToPath(new URL('./snapshots/subagent-conversation/sidebar.expected.md', import.meta.url))
|
||||
const UNAVAILABLE_GRANDCHILD_EXPECTED = fileURLToPath(new URL('./snapshots/subagent-conversation/nested.expected.md', import.meta.url))
|
||||
const FORK_EXPECTED = fileURLToPath(new URL('./snapshots/subagent-conversation/fork.expected.md', import.meta.url))
|
||||
@@ -77,7 +78,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
|
||||
paceMs: 25,
|
||||
})
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
page.on('request', (request) => {
|
||||
const path = new URL(request.url()).pathname
|
||||
if (path.startsWith('/api/')) apiCalls.push(path)
|
||||
@@ -219,13 +220,13 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
|
||||
const warningStart = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
const catalogButton = page.getByRole('button', { name: /个子代理/ })
|
||||
const catalogButton = page.getByRole('button', { name: /subagents/ })
|
||||
await catalogButton.waitFor({ timeout: 15_000 })
|
||||
await catalogButton.click()
|
||||
const catalogTree = page.getByRole('tree', { name: '子代理会话' })
|
||||
const catalogTree = page.getByRole('tree', { name: 'Subagent sessions' })
|
||||
await catalogTree.getByRole('treeitem').nth(1).waitFor({ timeout: 15_000 })
|
||||
await catalogTree.press('Escape')
|
||||
await page.getByRole('button', { name: '3 个子代理' }).waitFor({ timeout: 15_000 })
|
||||
await page.getByRole('button', { name: '3 subagents' }).waitFor({ timeout: 15_000 })
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
}, 120_000)
|
||||
|
||||
@@ -241,13 +242,66 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
|
||||
if (failures.length > 1) throw new AggregateError(failures, 'subagent Web teardown failed')
|
||||
})
|
||||
|
||||
it('keeps known descendants reachable across a stale empty catalog response', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-subagent-stale-catalog'))
|
||||
const pattern = '**/api/subagent.list'
|
||||
let firstClaimed = false
|
||||
let emptyDelivered = false
|
||||
let trailingRequested = false
|
||||
let releaseCatalog = (): void => {}
|
||||
const catalogHeld = new Promise<void>((resolve) => { releaseCatalog = resolve })
|
||||
await page.route(pattern, async (route) => {
|
||||
if (firstClaimed) {
|
||||
const response = await route.fetch()
|
||||
trailingRequested = true
|
||||
await catalogHeld
|
||||
await route.fulfill({ response })
|
||||
return
|
||||
}
|
||||
firstClaimed = true
|
||||
const response = await route.fetch()
|
||||
const body = await response.json() as {
|
||||
result: { ok: true; value: { entries: unknown[] } } | { ok: false }
|
||||
}
|
||||
if (body.result.ok) body.result.value.entries = []
|
||||
await route.fulfill({ response, json: body })
|
||||
emptyDelivered = true
|
||||
})
|
||||
|
||||
const warningStart = tripwire.warnings.length
|
||||
try {
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await expect.poll(() => emptyDelivered, { timeout: 15_000 }).toBe(true)
|
||||
await page.getByRole('button', { name: '3 subagents' }).waitFor({ timeout: 15_000 })
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
|
||||
await page.getByRole('button', { name: '3 subagents' }).click()
|
||||
await expect.poll(() => trailingRequested, { timeout: 15_000 }).toBe(true)
|
||||
const tree = page.getByRole('tree', { name: 'Subagent sessions' })
|
||||
await tree.getByRole('treeitem', { name: 'Loading subagents' }).first().waitFor()
|
||||
expect(await tree.getByRole('treeitem', { name: 'Loading subagents' }).count()).toBe(2)
|
||||
await compareOrRefreshGolden(
|
||||
STALE_CATALOG_EXPECTED,
|
||||
await captureStableAria(page, '[role="tree"][aria-label="Subagent sessions"]', scaffold.workspaceCwd),
|
||||
MODE,
|
||||
)
|
||||
releaseCatalog()
|
||||
await tree.getByRole('treeitem', { name: new RegExp(LABEL) }).waitFor({ timeout: 15_000 })
|
||||
await tree.press('Escape')
|
||||
} finally {
|
||||
releaseCatalog()
|
||||
await page.unroute(pattern)
|
||||
}
|
||||
})
|
||||
|
||||
it('expands a persisted grandchild progressively without activating either level', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-subagent-tree'))
|
||||
await page.getByRole('button', { name: '3 个子代理' }).click()
|
||||
await page.getByRole('button', { name: '3 subagents' }).click()
|
||||
expect(await page.getByRole('button', {
|
||||
name: `展开 ${ONE_SHOT_LABEL} 的下级子代理`,
|
||||
name: `Expand ${ONE_SHOT_LABEL} descendants`,
|
||||
}).count()).toBe(0)
|
||||
await page.getByRole('button', { name: `展开 ${LABEL} 的下级子代理` }).click()
|
||||
await page.getByRole('button', { name: `Expand ${LABEL} descendants` }).click()
|
||||
const childRow = page.getByRole('treeitem', { name: new RegExp(LABEL) })
|
||||
const childLabel = await childRow.getAttribute('aria-label')
|
||||
await page.waitForTimeout(1_100)
|
||||
@@ -257,16 +311,16 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
|
||||
expect(scaffold.ctx.agents.get(grandchildId)).toBeUndefined()
|
||||
const snapshot = await captureStableAria(
|
||||
page,
|
||||
'[role="tree"][aria-label="子代理会话"]',
|
||||
'[role="tree"][aria-label="Subagent sessions"]',
|
||||
scaffold.workspaceCwd,
|
||||
)
|
||||
await compareOrRefreshGolden(TREE_EXPECTED, snapshot, MODE)
|
||||
await page.getByRole('tree', { name: '子代理会话' }).press('Escape')
|
||||
await page.getByRole('tree', { name: 'Subagent sessions' }).press('Escape')
|
||||
})
|
||||
|
||||
it('opens the completed child from persistence without activating it', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-subagent-open'))
|
||||
await page.getByRole('button', { name: '3 个子代理' }).click()
|
||||
await page.getByRole('button', { name: '3 subagents' }).click()
|
||||
await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click()
|
||||
await expect.poll(
|
||||
() => page.getByText(INITIAL_PROMPT, { exact: true }).count(),
|
||||
@@ -308,18 +362,18 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
|
||||
).toBe('running')
|
||||
const hierarchy = page.getByRole('navigation', { name: 'Session hierarchy' })
|
||||
await hierarchy.getByRole('button').first().click()
|
||||
const runningTrigger = page.getByRole('button', { name: '3 个子代理,正在运行' })
|
||||
const runningTrigger = page.getByRole('button', { name: '3 subagents running' })
|
||||
await runningTrigger.waitFor({ timeout: 10_000 })
|
||||
expect(await runningTrigger.locator('[data-state="ongoing"]').count()).toBe(1)
|
||||
await runningTrigger.click()
|
||||
await page.getByRole('treeitem', {
|
||||
name: new RegExp(`${LABEL}.*正在运行`),
|
||||
name: new RegExp(`${LABEL}.*running`),
|
||||
}).waitFor({ timeout: 10_000 })
|
||||
await ended
|
||||
await page.getByRole('treeitem', {
|
||||
name: new RegExp(`${LABEL}.*当前未运行`),
|
||||
name: new RegExp(`${LABEL}.*not running`),
|
||||
}).waitFor({ timeout: 10_000 })
|
||||
expect(await page.getByRole('button', { name: '3 个子代理' })
|
||||
expect(await page.getByRole('button', { name: '3 subagents' })
|
||||
.locator('[data-state="ongoing"]').count()).toBe(0)
|
||||
await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click()
|
||||
await expect.poll(() => page.getByText(FOLLOWUP, { exact: true }).count(), { timeout: 10_000 }).toBe(1)
|
||||
@@ -337,9 +391,9 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
|
||||
|
||||
it('opens an unavailable persisted grandchild after recording the available child', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-subagent-grandchild'))
|
||||
await page.getByRole('button', { name: '1 个子代理' }).click()
|
||||
await page.getByRole('button', { name: '1 subagent' }).click()
|
||||
await page.getByRole('treeitem', { name: new RegExp(NESTED_LABEL) }).click()
|
||||
await page.getByText('父会话当前不在线,重新打开父会话后即可继续发送消息。').waitFor()
|
||||
await page.getByText('The parent session is offline; reopen it to continue sending messages.').waitFor()
|
||||
const hierarchy = page.getByRole('navigation', { name: 'Session hierarchy' })
|
||||
const crumbs = await hierarchy.getByRole('button').allTextContents()
|
||||
expect(crumbs.slice(-2)).toEqual([LABEL, NESTED_LABEL])
|
||||
@@ -358,9 +412,9 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
|
||||
.getByRole('treeitem')
|
||||
.last()
|
||||
await parentSession.click()
|
||||
await page.getByRole('button', { name: '3 个子代理' }).click()
|
||||
await page.getByRole('button', { name: '3 subagents' }).click()
|
||||
await page.getByRole('treeitem', { name: new RegExp(ONE_SHOT_LABEL) }).click()
|
||||
await page.getByText('一次性任务不支持后续消息,可在这里查看完整执行记录。').waitFor()
|
||||
await page.getByText('One-shot tasks do not accept follow-ups; review the full execution record here.').waitFor()
|
||||
expect(scaffold.ctx.agents.get(oneShotId)).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -369,7 +423,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
|
||||
await page.getByRole('tree', { name: 'Sessions' })
|
||||
.getByRole('treeitem', { name: /Ask a research subagent to/ })
|
||||
.click()
|
||||
await page.getByRole('button', { name: '3 个子代理' }).click()
|
||||
await page.getByRole('button', { name: '3 subagents' }).click()
|
||||
await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click()
|
||||
await page.getByRole('textbox', { name: 'Message the agent' }).waitFor()
|
||||
const forkResponse = page.waitForResponse(response =>
|
||||
@@ -395,7 +449,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-subagent-post-fork-followup'))
|
||||
const sessions = page.getByRole('tree', { name: 'Sessions' })
|
||||
await sessions.getByRole('treeitem', { name: /Ask a research subagent to/ }).click()
|
||||
await page.getByRole('button', { name: '3 个子代理' }).click()
|
||||
await page.getByRole('button', { name: '3 subagents' }).click()
|
||||
await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click()
|
||||
await page.locator('textarea:enabled').first().waitFor()
|
||||
expect(scaffold.ctx.agents.get(childId)).toBeUndefined()
|
||||
@@ -412,7 +466,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
|
||||
await expect.poll(() => scaffold.ctx.agents.get(forkId)).not.toBeUndefined()
|
||||
|
||||
await sessions.getByRole('treeitem', { name: /Ask a research subagent to/ }).click()
|
||||
await page.getByRole('button', { name: '3 个子代理' }).click()
|
||||
await page.getByRole('button', { name: '3 subagents' }).click()
|
||||
await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click()
|
||||
const input = page.locator('textarea:enabled').first()
|
||||
await input.waitFor()
|
||||
|
||||
Reference in New Issue
Block a user