test(web): assert ordinary fork identity without source parents

This commit is contained in:
Dudu-0223 committed 2026-10-23 14:50:06 +08:00
1 parent b096659245
commit 63d3504b14
5 files changed
+29 -15

No files matched your search

+4 -2
View File
@@ -307,15 +307,17 @@ describe('web e2e: long Chat interaction contract', () => {
await expect.poll(() => page.evaluate(() => navigator.clipboard.readText()), { timeout: 5_000 })
.toBe(expectedUserText)
const existing = new Set(scaffold.ctx.agents.list().map(agent => agent.id))
await turnTailRow.hover()
await turnTailRow.getByRole('button', { name: 'Branch into a new conversation', exact: true }).click()
await expect.poll(
() => scaffold.ctx.agents.list().find(agent => agent.session.header.parentSession === SessionId(SESSION_ID)),
() => scaffold.ctx.agents.list().find(agent => !existing.has(agent.id)),
{ timeout: 15_000 },
).toBeDefined()
const child = scaffold.ctx.agents.list()
.find(agent => agent.session.header.parentSession === SessionId(SESSION_ID))
.find(agent => !existing.has(agent.id))
if (child === undefined) throw new Error('message branch did not create a child session')
expect(child.session.header.parentSession).toBeUndefined()
expect(child.session.inheritedEventCount).toBe(boundary.seq + 1)
expect(child.session.snapshotEvents().some(event => carries(event, branchAssistantMarker))).toBe(true)
expect(child.session.snapshotEvents().some(event => carries(event, FIXTURE.markers.user(BRANCH_TURN + 1)))).toBe(false)
+1 -1
View File
@@ -114,7 +114,7 @@ describe('web e2e: exact-boundary fork seeds branch closers and continues', () =
const child = scaffold.ctx.sessions.get(childId)
if (child === undefined) throw new Error('fork did not publish a live child session')
expect(child.header.parentSession).toBe(SessionId(SEED_ID))
expect(child.header.parentSession).toBeUndefined()
// inheritedEventCount counts only the copied parent prefix; the closers are child work.
expect(child.inheritedEventCount).toBe(forkAt + 1)
expect(child.snapshotEvents()[forkAt]?.type).toBe('tool/call')
+13 -6
View File
@@ -371,13 +371,15 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
it.skipIf(MODE === 'record')('forks through the settled-message and session-row actions', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-message-fork'))
// The second answer is followed by another completed user turn in the source.
const existing = new Set(scaffold.ctx.agents.list().map(agent => agent.id))
await page.getByRole('button', { name: 'Branch into a new conversation' }).nth(1).click()
await expect.poll(
() => scaffold.ctx.agents.list().find(agent => agent.session.header.parentSession === SessionId(SEED_ID)),
() => scaffold.ctx.agents.list().find(agent => !existing.has(agent.id)),
{ timeout: 15_000 },
).toBeDefined()
const child = scaffold.ctx.agents.list().find(agent =>
agent.session.header.parentSession === SessionId(SEED_ID))!
!existing.has(agent.id))!
expect(child.session.header.parentSession).toBeUndefined()
expect(child.inbox.nextTurn).toEqual([])
expect(child.session.snapshotEvents().some(event => event.type === 'agent/inbox/spliced'
&& event.data.inserted.some(message => message.content.some(part =>
@@ -405,11 +407,13 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
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)
const beforeNested = new Set(scaffold.ctx.agents.list().map(agent => agent.id))
await page.getByRole('menuitem', { name: 'Fork session' }).click()
await expect.poll(
() => scaffold.ctx.agents.list().filter(agent => agent.session.header.parentSession !== undefined).length,
() => scaffold.ctx.agents.list().filter(agent => !beforeNested.has(agent.id)).length,
{ timeout: 15_000 },
).toBe(2)
).toBe(1)
expect(scaffold.ctx.agents.list().find(agent => !beforeNested.has(agent.id))!.session.header.parentSession).toBeUndefined()
await expect.poll(
() => page.locator('[role="treeitem"]').count(),
{ timeout: 10_000 },
@@ -445,8 +449,10 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
expect(scaffold.ctx.agents.get(id)).toBeUndefined()
await row.hover()
await row.locator('button[aria-label^="Session actions for "]').click()
const existing = new Set(scaffold.ctx.agents.list().map(agent => agent.id))
await page.getByRole('menuitem', { name: 'Fork session', exact: true }).click()
await expect.poll(() => scaffold.ctx.agents.list().some(agent => agent.session.header.parentSession === id)).toBe(true)
await expect.poll(() => scaffold.ctx.agents.list().some(agent => !existing.has(agent.id))).toBe(true)
expect(scaffold.ctx.agents.list().find(agent => !existing.has(agent.id))!.session.header.parentSession).toBeUndefined()
expect(scaffold.ctx.agents.get(id)).toBeUndefined()
expect(await row.getAttribute('aria-selected')).not.toBe('true')
})
@@ -481,6 +487,7 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
if (path === '/api/session/fork') forkRequests.push(path)
}
page.on('request', trackFork)
const existing = new Set(scaffold.ctx.agents.list().map(agent => agent.id))
try {
await row.hover()
await row.locator('button[aria-label^="Session actions for "]').click()
@@ -501,7 +508,7 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
expect(await readdir(directory)).toContain(`session.v${SESSION_FORMAT_VERSION}.jsonl.zstd`)
expect(await readFile(predecessor)).toEqual(source)
expect(forkRequests).toEqual(['/api/session/fork'])
expect(scaffold.ctx.agents.list().filter(agent => agent.session.header.parentSession === id)).toEqual([])
expect(scaffold.ctx.agents.list().filter(agent => !existing.has(agent.id) && agent.id !== id)).toEqual([])
} finally {
page.off('request', trackFork)
}
+1 -1
View File
@@ -1209,7 +1209,7 @@ async function assertReplaySession(
let expectedPath = fixturePath
const userPrompts = fixtureUserPrompts(expected)
const candidates = sessions.filter((session) => {
if (session.header.parentSession !== undefined) return false
if (session.header.origin === 'subagent' || session.header.isSeeded) return false
const actual = session.snapshotEvents().flatMap((event) => {
if (event.type !== 'user/message' || event.data.source.kind !== 'user') return []
const text = event.data.content.filter(block => block.type === 'text').map(block => block.text).join('')
+10 -5
View File
@@ -93,14 +93,17 @@ describe.skipIf(mode === 'record')('web e2e: workspace shortcuts', () => {
await bind('Fork session')
const original = await readPersistedEvents(scaffold, sourceId)
const lastEnd = original.findLastIndex(event => event.type === 'turn/end')
const existing = new Set(scaffold.ctx.agents.list().map(agent => agent.id))
await page.keyboard.press('Meta+Shift+Comma')
await expect.poll(() => scaffold.ctx.agents.list().find(agent => agent.session.header.parentSession === sourceId)).toBeDefined()
const child = scaffold.ctx.agents.list().find(agent => agent.session.header.parentSession === sourceId)!
await expect.poll(() => scaffold.ctx.agents.list().find(agent => !existing.has(agent.id))).toBeDefined()
const child = scaffold.ctx.agents.list().find(agent => !existing.has(agent.id))!
expect(child.session.header.parentSession).toBeUndefined()
const childId = child.session.header.id
await expect.poll(async () => (await readPersistedEvents(scaffold, childId)).some(event => event.type === 'session/title' && event.data.title === 'T4 source (1)')).toBe(true)
const copied = await readPersistedEvents(scaffold, childId)
expect(copied.slice(0, lastEnd + 1)).toEqual(original.slice(0, lastEnd + 1))
const sourceRow = page.getByRole('treeitem').filter({ has: page.getByText('T4 source', { exact: true }) })
expect(child.session.header.parentSession).toBeUndefined()
const childRow = page.getByRole('treeitem').filter({ has: page.getByText('T4 source (1)', { exact: true }) })
await childRow.waitFor()
expect(await sourceRow.getAttribute('aria-selected')).toBe('true')
@@ -218,6 +221,7 @@ it.skipIf(mode === 'record')('explains a first-turn fork refusal and retries fro
await connectFreshWorkspace(page, scaffold.workspaceCwd)
const source = scaffold.ctx.agents.list().find(agent => !before.has(agent.id))
if (source === undefined) throw new Error('The workspace did not create a Session')
const existing = new Set(scaffold.ctx.agents.list().map(agent => agent.id))
source.session.append('turn/start', { turn: 1 })
const user = source.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'The first turn has not ended.' }], source: { kind: 'user' },
@@ -240,7 +244,7 @@ it.skipIf(mode === 'record')('explains a first-turn fork refusal and retries fro
await compareOrRefreshGolden(join(root, 'fork-unavailable.expected.md'),
await unavailable.ariaSnapshot(), mode)
expect(await selected.textContent()).toContain('First turn')
expect(scaffold.ctx.agents.list().filter(agent => agent.session.header.parentSession === source.id)).toHaveLength(0)
expect(scaffold.ctx.agents.list().filter(agent => !existing.has(agent.id))).toHaveLength(0)
expect(forkWarnings).toEqual([])
source.session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
@@ -252,8 +256,9 @@ it.skipIf(mode === 'record')('explains a first-turn fork refusal and retries fro
await scaffold.ctx.sessions.flush(source.session)
await page.getByText('The second turn has not ended.', { exact: true }).waitFor()
await page.keyboard.press('Meta+Shift+Comma')
await expect.poll(() => scaffold.ctx.agents.list().find(agent => agent.session.header.parentSession === source.id)).toBeDefined()
const child = scaffold.ctx.agents.list().find(agent => agent.session.header.parentSession === source.id)!
await expect.poll(() => scaffold.ctx.agents.list().find(agent => !existing.has(agent.id))).toBeDefined()
const child = scaffold.ctx.agents.list().find(agent => !existing.has(agent.id))!
expect(child.session.header.parentSession).toBeUndefined()
const childRow = page.getByRole('treeitem').filter({ has: page.getByText('First turn (1)', { exact: true }) })
await childRow.waitFor()
expect(await childRow.getAttribute('aria-selected')).toBe('false')