diff --git a/apps/web/tests/chat-long-interactions.e2e.ts b/apps/web/tests/chat-long-interactions.e2e.ts index dec6749ab9..25cbcf590d 100644 --- a/apps/web/tests/chat-long-interactions.e2e.ts +++ b/apps/web/tests/chat-long-interactions.e2e.ts @@ -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) diff --git a/apps/web/tests/fork-mid-turn.e2e.ts b/apps/web/tests/fork-mid-turn.e2e.ts index 2ffa330007..15c8d67512 100644 --- a/apps/web/tests/fork-mid-turn.e2e.ts +++ b/apps/web/tests/fork-mid-turn.e2e.ts @@ -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') diff --git a/apps/web/tests/message-actions.e2e.ts b/apps/web/tests/message-actions.e2e.ts index 2f96305714..405e70b57f 100644 --- a/apps/web/tests/message-actions.e2e.ts +++ b/apps/web/tests/message-actions.e2e.ts @@ -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) } diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index 4ce50f5075..6a0c089a9a 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -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('') diff --git a/apps/web/tests/shortcuts-workspace.e2e.ts b/apps/web/tests/shortcuts-workspace.e2e.ts index db74374ae7..410fa3ce84 100644 --- a/apps/web/tests/shortcuts-workspace.e2e.ts +++ b/apps/web/tests/shortcuts-workspace.e2e.ts @@ -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')