test(web): cover row fork in the real app

This commit is contained in:
imccyu
2026-07-30 13:27:02 +08:00
parent 36f1489398
commit 7d27c92851
4 changed files with 48 additions and 6 deletions

View File

@@ -87,13 +87,41 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
})
it.skipIf(MODE === 'record')('forks the session through the settled user-message action', async () => {
it.skipIf(MODE === 'record')('forks through the settled-message and session-row actions', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-message-fork'))
await page.getByRole('button', { name: '在新对话中分支' }).first().click()
await expect.poll(
() => scaffold.ctx.agents.list().find(agent => agent.session.header.parentSession === SessionId(SEED_ID)),
{ timeout: 15_000 },
).toBeDefined()
await expect.poll(
() => page.locator('[role="treeitem"]').count(),
{ timeout: 10_000 },
).toBe(3)
await expect.poll(
() => page.locator('[role="treeitem"][aria-selected="true"]').count(),
{ timeout: 10_000 },
).toBe(1)
// The row action owns a distinct ui-workspace injection from the message
// action above, so exercise both through the loaded app before capture.
const sourceRow = page.locator('[role="treeitem"][aria-expanded="true"]').last()
const rowBox = await sourceRow.boundingBox()
if (rowBox === null) throw new Error('fork source row has no layout box')
const actionButton = sourceRow.locator('button[aria-label^="Session actions for "]')
await sourceRow.hover({ position: { x: rowBox.width - 16, y: rowBox.height / 2 } })
await expect.poll(() => actionButton.isVisible(), { timeout: 2_000 }).toBe(true)
const buttonBox = await actionButton.boundingBox()
if (buttonBox === null) throw new Error('fork source row action has no layout box')
await page.mouse.click(buttonBox.x + buttonBox.width / 2, buttonBox.y + buttonBox.height / 2)
await page.getByRole('menuitem', { name: 'Fork session' }).click()
await expect.poll(
() => scaffold.ctx.agents.list().filter(agent => agent.session.header.parentSession !== undefined).length,
{ timeout: 15_000 },
).toBe(2)
await expect.poll(
() => page.locator('[role="treeitem"]').count(),
{ timeout: 10_000 },
).toBe(4)
await expect.poll(
() => page.locator('[role="treeitem"][aria-selected="true"]').count(),
{ timeout: 10_000 },

View File

@@ -1,9 +1,10 @@
- tree "Sessions":
- treeitem "Ungrouped 2 sessions" [expanded]:
- treeitem "Ungrouped 3 sessions" [expanded]:
- img
- text: Ungrouped 2 sessions
- text: Ungrouped 3 sessions
- treeitem "Collapse Use the read tool twice 1min" [expanded]:
- button "Collapse":
- img
- text: Use the read tool twice 1min
- treeitem "Use the read tool twice now" [selected]
- treeitem "Use the read tool twice now"

View File

@@ -31,7 +31,7 @@
- img
- button "在新对话中分支":
- img
- text: {{clock}} cache hit 98% · 15,962 tokens · 1 turns · 2 steps
- text: {{clock}}
- textbox "Message the agent"
- button "Add attachment":
- img
@@ -41,3 +41,4 @@
- text: deepseek-v4-flash
- img
- button "Send message" [disabled]
- text: 1 turns · 2 steps Tool call {{duration}} Cache hit 98% Input 15.8K tok · Output 135 tok

View File

@@ -82,7 +82,9 @@ describe('sessions.fork', () => {
expect(response.result.ok).toBe(true)
if (!response.result.ok) return
const child = ctx.sessions.get(response.result.value.sessionId)
expect(child?.events.length).toBe(3)
expect(child?.events.map(event => event.type)).toEqual([
'turn/start', 'user/message', 'turn/end', 'session/end-seed',
])
expect(child?.header.parentSession).toBe(source.id)
expect(child?.header.cwd).toBe('/proj')
await ctx.fiber.dispose()
@@ -92,13 +94,23 @@ describe('sessions.fork', () => {
const ctx = await composed()
const source = liveAgent(ctx, 'session-tail', 2, true)
const proxy = api(ctx)
const expectedTypes = [
'turn/start', 'user/message', 'turn/end',
'turn/start', 'user/message', 'turn/end',
'session/end-seed',
]
const omitted = await proxy.sessions.fork(request({ sessionId: source.id }))
expect(omitted.result.ok).toBe(true)
if (omitted.result.ok) {
expect(ctx.sessions.get(omitted.result.value.sessionId)?.events.length).toBe(6)
expect(ctx.sessions.get(omitted.result.value.sessionId)?.events.map(event => event.type))
.toEqual(expectedTypes)
}
const pastEnd = await proxy.sessions.fork(request({ sessionId: source.id, atSeq: 999 }))
expect(pastEnd.result.ok).toBe(true)
if (pastEnd.result.ok) {
expect(ctx.sessions.get(pastEnd.result.value.sessionId)?.events.map(event => event.type))
.toEqual(expectedTypes)
}
await ctx.fiber.dispose()
})