Quellcode durchsuchen

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

imccyu vor 2 Monaten
Ursprung
Commit
7d27c92851

+ 29 - 1
apps/web/tests/message-actions.e2e.ts

@@ -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 },

+ 3 - 2
apps/web/tests/snapshots/message-actions/fork.expected.md

@@ -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"

+ 2 - 1
apps/web/tests/snapshots/message-actions/ui.expected.md

@@ -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

+ 14 - 2
packages/host/apiproxy/tests/api-proxy-fork.spec.ts

@@ -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()
   })