test(web): cover row fork in the real app
This commit is contained in:
@@ -87,13 +87,41 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
|
|||||||
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
|
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'))
|
onTestFailed(() => saveFailureShot(page, 'web-e2e-message-fork'))
|
||||||
await page.getByRole('button', { name: '在新对话中分支' }).first().click()
|
await page.getByRole('button', { name: '在新对话中分支' }).first().click()
|
||||||
await expect.poll(
|
await expect.poll(
|
||||||
() => scaffold.ctx.agents.list().find(agent => agent.session.header.parentSession === SessionId(SEED_ID)),
|
() => scaffold.ctx.agents.list().find(agent => agent.session.header.parentSession === SessionId(SEED_ID)),
|
||||||
{ timeout: 15_000 },
|
{ timeout: 15_000 },
|
||||||
).toBeDefined()
|
).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(
|
await expect.poll(
|
||||||
() => page.locator('[role="treeitem"][aria-selected="true"]').count(),
|
() => page.locator('[role="treeitem"][aria-selected="true"]').count(),
|
||||||
{ timeout: 10_000 },
|
{ timeout: 10_000 },
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
- tree "Sessions":
|
- tree "Sessions":
|
||||||
- treeitem "Ungrouped 2 sessions" [expanded]:
|
- treeitem "Ungrouped 3 sessions" [expanded]:
|
||||||
- img
|
- img
|
||||||
- text: Ungrouped 2 sessions
|
- text: Ungrouped 3 sessions
|
||||||
- treeitem "Collapse Use the read tool twice 1min" [expanded]:
|
- treeitem "Collapse Use the read tool twice 1min" [expanded]:
|
||||||
- button "Collapse":
|
- button "Collapse":
|
||||||
- img
|
- img
|
||||||
- text: Use the read tool twice 1min
|
- text: Use the read tool twice 1min
|
||||||
- treeitem "Use the read tool twice now" [selected]
|
- treeitem "Use the read tool twice now" [selected]
|
||||||
|
- treeitem "Use the read tool twice now"
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
- img
|
- img
|
||||||
- button "在新对话中分支":
|
- button "在新对话中分支":
|
||||||
- img
|
- img
|
||||||
- text: {{clock}} cache hit 98% · 15,962 tokens · 1 turns · 2 steps
|
- text: {{clock}}
|
||||||
- textbox "Message the agent"
|
- textbox "Message the agent"
|
||||||
- button "Add attachment":
|
- button "Add attachment":
|
||||||
- img
|
- img
|
||||||
@@ -41,3 +41,4 @@
|
|||||||
- text: deepseek-v4-flash
|
- text: deepseek-v4-flash
|
||||||
- img
|
- img
|
||||||
- button "Send message" [disabled]
|
- button "Send message" [disabled]
|
||||||
|
- text: 1 turns · 2 steps Tool call {{duration}} Cache hit 98% Input 15.8K tok · Output 135 tok
|
||||||
|
|||||||
@@ -82,7 +82,9 @@ describe('sessions.fork', () => {
|
|||||||
expect(response.result.ok).toBe(true)
|
expect(response.result.ok).toBe(true)
|
||||||
if (!response.result.ok) return
|
if (!response.result.ok) return
|
||||||
const child = ctx.sessions.get(response.result.value.sessionId)
|
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.parentSession).toBe(source.id)
|
||||||
expect(child?.header.cwd).toBe('/proj')
|
expect(child?.header.cwd).toBe('/proj')
|
||||||
await ctx.fiber.dispose()
|
await ctx.fiber.dispose()
|
||||||
@@ -92,13 +94,23 @@ describe('sessions.fork', () => {
|
|||||||
const ctx = await composed()
|
const ctx = await composed()
|
||||||
const source = liveAgent(ctx, 'session-tail', 2, true)
|
const source = liveAgent(ctx, 'session-tail', 2, true)
|
||||||
const proxy = api(ctx)
|
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 }))
|
const omitted = await proxy.sessions.fork(request({ sessionId: source.id }))
|
||||||
expect(omitted.result.ok).toBe(true)
|
expect(omitted.result.ok).toBe(true)
|
||||||
if (omitted.result.ok) {
|
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 }))
|
const pastEnd = await proxy.sessions.fork(request({ sessionId: source.id, atSeq: 999 }))
|
||||||
expect(pastEnd.result.ok).toBe(true)
|
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()
|
await ctx.fiber.dispose()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user