Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 81 additions & 59 deletions apps/desktop/src/app/session/hooks/use-session-actions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ describe('desktop branch creation idempotency', () => {
const createReady = deferred<{ session_id: string; stored_session_id: string }>()

const requestGateway = vi.fn(async (method: string, params?: Record<string, unknown>) => {
if (method === 'session.create') {
if (method === 'session.branch_stored') {
return createReady.promise as never
}

Expand Down Expand Up @@ -226,26 +226,26 @@ describe('desktop branch creation idempotency', () => {
})

await waitFor(() =>
expect(requestGateway.mock.calls.filter(([method]) => method === 'session.create')).toHaveLength(1)
expect(requestGateway.mock.calls.filter(([method]) => method === 'session.branch_stored')).toHaveLength(1)
)

await act(async () => {
createReady.resolve({ session_id: 'runtime-branch', stored_session_id: 'stored-branch' })
await expect(Promise.all([first, second])).resolves.toEqual([true, true])
})

expect(requestGateway.mock.calls.filter(([method]) => method === 'session.create')).toHaveLength(1)
expect(requestGateway.mock.calls.filter(([method]) => method === 'session.branch_stored')).toHaveLength(1)
expect(requestGateway).toHaveBeenCalledWith(
'session.create',
'session.branch_stored',
expect.objectContaining({
messages: [
{ content: 'question', role: 'user' },
{ content: 'answer', role: 'assistant' }
],
copy_parent_history: true,
omit_messages: true,
parent_session_id: 'parent',
source: 'desktop'
})
)
expect(requestGateway.mock.calls[0]?.[1]).not.toHaveProperty('messages')
expect(getAllSessionMessages).not.toHaveBeenCalled()
expect($sessions.get().filter(session => session.id === 'stored-branch')).toHaveLength(1)
})

Expand All @@ -260,7 +260,7 @@ describe('desktop branch creation idempotency', () => {
const otherCreate = deferred<{ session_id: string; stored_session_id: string }>()

routedCreate.mockImplementation((async (connectionId: string, _profile: string, method: string) => {
if (method !== 'session.create') {
if (method !== 'session.branch_stored') {
return {} as never
}

Expand Down Expand Up @@ -294,7 +294,7 @@ describe('desktop branch creation idempotency', () => {
await act(async () => {
second = actions!.branchStoredSession('parent')
await waitFor(() =>
expect(routedCreate.mock.calls.filter(([, , method]) => method === 'session.create')).toHaveLength(2)
expect(routedCreate.mock.calls.filter(([, , method]) => method === 'session.branch_stored')).toHaveLength(2)
)
})

Expand All @@ -304,7 +304,7 @@ describe('desktop branch creation idempotency', () => {
await expect(Promise.all([first, second])).resolves.toEqual([true, true])
})

const creates = routedCreate.mock.calls.filter(([, , method]) => method === 'session.create')
const creates = routedCreate.mock.calls.filter(([, , method]) => method === 'session.branch_stored')

expect(creates.map(([connectionId]) => connectionId)).toEqual(['pandora', 'other-box'])
// Two distinct children, not one child claimed twice.
Expand Down Expand Up @@ -649,7 +649,7 @@ async function createWith(
let createParams: Record<string, unknown> | undefined

const requestGateway = vi.fn(async (method: string, params?: Record<string, unknown>) => {
if (method === 'session.create') {
if (method === 'session.create' || method === 'session.branch_stored') {
createParams = params

return { session_id: RUNTIME_SESSION_ID, stored_session_id: null } as never
Expand Down Expand Up @@ -1851,7 +1851,7 @@ describe('branchStoredSession desktop source tagging', () => {

it('opens the branch as the primary session in the main workspace (#93444)', async () => {
const requestGateway = vi.fn(async (method: string) => {
if (method === 'session.create') {
if (method === 'session.create' || method === 'session.branch_stored') {
return { session_id: 'branch-runtime', stored_session_id: 'branch-stored' } as never
}

Expand Down Expand Up @@ -1904,7 +1904,7 @@ describe('branchStoredSession desktop source tagging', () => {

it('keeps the current view when branching a different session from the sidebar (does not reintroduce #69750)', async () => {
const requestGateway = vi.fn(async (method: string) => {
if (method === 'session.create') {
if (method === 'session.create' || method === 'session.branch_stored') {
return { session_id: 'branch-runtime', stored_session_id: 'branch-stored' } as never
}

Expand Down Expand Up @@ -1957,7 +1957,7 @@ describe('branchStoredSession desktop source tagging', () => {
let createParams: Record<string, unknown> | undefined

const requestGateway = vi.fn(async (method: string, params?: Record<string, unknown>) => {
if (method === 'session.create') {
if (method === 'session.create' || method === 'session.branch_stored') {
createParams = params

return { session_id: 'branch-runtime', stored_session_id: 'branch-stored' } as never
Expand Down Expand Up @@ -1998,7 +1998,7 @@ describe('branchStoredSession desktop source tagging', () => {
_profile: string,
method: string
) => {
if (method === 'session.create') {
if (method === 'session.create' || method === 'session.branch_stored') {
return { session_id: 'branch-runtime', stored_session_id: 'branch-stored' } as never
}

Expand All @@ -2020,28 +2020,25 @@ describe('branchStoredSession desktop source tagging', () => {
await expect(branchStoredSession!('stored-parent')).resolves.toBe(true)

// The create must ride the parent's own (connection, profile) socket...
expect(requestGatewayForAgent).toHaveBeenCalledWith(
expect(requestGatewayForAgent).toHaveBeenLastCalledWith(
'pandora',
'default',
'session.create',
'session.branch_stored',
expect.objectContaining({ parent_session_id: 'stored-parent', source: 'desktop' })
)
// ...and never the ambient socket, which may serve a different machine.
expect(ambientRequest).not.toHaveBeenCalledWith('session.create', expect.anything())
})

// The parent transcript read must land on the owning backend too: reading it
// from the ambient socket returns nothing for a foreign-owned parent, which
// aborts the branch as "nothing to branch" before any create is attempted.
it('reads a connection-tagged parent transcript from its owning connection', async () => {
it('does not materialize a connection-tagged parent transcript in the renderer', async () => {
const ambientRequest = vi.fn(async () => ({}) as never)

vi.mocked(requestGatewayForAgent).mockImplementation((async (
_connectionId: null | string,
_profile: string,
method: string
) => {
if (method === 'session.create') {
if (method === 'session.create' || method === 'session.branch_stored') {
return { session_id: 'branch-runtime', stored_session_id: 'branch-stored' } as never
}

Expand All @@ -2051,25 +2048,26 @@ describe('branchStoredSession desktop source tagging', () => {
setSessions([
storedSession({ connection_id: 'pandora', id: 'stored-parent', message_count: 1, profile: 'default' })
])
vi.mocked(getAllSessionMessages).mockResolvedValue({
messages: [{ content: 'branch me', role: 'user', timestamp: 1 }],
session_id: 'stored-parent'
} as never)

let branchStoredSession: ((storedSessionId: string) => Promise<boolean>) | null = null
render(<BranchHarness onReady={branch => (branchStoredSession = branch)} requestGateway={ambientRequest} />)
await waitFor(() => expect(branchStoredSession).not.toBeNull())

await expect(branchStoredSession!('stored-parent')).resolves.toBe(true)

expect(getAllSessionMessages).toHaveBeenCalledWith('stored-parent', {
connectionId: 'pandora',
profile: 'default'
})
expect(requestGatewayForAgent).toHaveBeenLastCalledWith(
'pandora',
'default',
'session.branch_stored',
expect.objectContaining({
copy_parent_history: true,
omit_messages: true,
parent_session_id: 'stored-parent',
source: 'desktop'
})
)
expect(getAllSessionMessages).not.toHaveBeenCalled()
})

// The create landing on the right backend is only half the job: the sidebar
// row must be TAGGED with that owner too. An untagged optimistic row inherits
// The row must be TAGGED with that owner too. An untagged optimistic row inherits
// the ambient profile, so every later owner lookup (resume/hydrate/prompt)
// routes to the wrong backend and the chat pane spins forever on a session
// that backend never had.
Expand All @@ -2081,7 +2079,7 @@ describe('branchStoredSession desktop source tagging', () => {
_profile: string,
method: string
) => {
if (method === 'session.create') {
if (method === 'session.create' || method === 'session.branch_stored') {
return { session_id: 'branch-runtime', stored_session_id: 'branch-stored' } as never
}

Expand Down Expand Up @@ -2114,7 +2112,7 @@ describe('branchStoredSession desktop source tagging', () => {
let createParams: Record<string, unknown> | undefined

const ambientRequest = vi.fn(async (method: string, params?: Record<string, unknown>) => {
if (method === 'session.create') {
if (method === 'session.create' || method === 'session.branch_stored') {
createParams = params

return { session_id: 'branch-runtime', stored_session_id: 'branch-stored' } as never
Expand Down Expand Up @@ -2190,19 +2188,19 @@ describe('branchStoredSession desktop source tagging', () => {
expect(branchParams).toEqual({ session_id: 'live-parent', count: 2 })
})

it('hydrates the complete persisted display transcript before branching a compacted live chat', async () => {
it('branches a compacted live chat without hydrating its transcript in the renderer', async () => {
let branchParams: Record<string, unknown> | undefined

const requestGateway = vi.fn(async (method: string, params?: Record<string, unknown>) => {
if (method === 'session.branch') {
if (method === 'session.branch_whole') {
branchParams = params

return {
session_id: 'branch-runtime',
stored_session_id: 'branch-stored',
title: 'Branch',
message_count: 4,
messages: [],
messages_omitted: true,
info: {}
} as never
}
Expand All @@ -2216,16 +2214,6 @@ describe('branchStoredSession desktop source tagging', () => {
{ id: 'tail-user', role: 'user', parts: [{ type: 'text', text: 'second question' }] },
{ id: 'tail-assistant', role: 'assistant', parts: [{ type: 'text', text: 'second answer' }] }
])
vi.mocked(getAllSessionMessages).mockResolvedValue({
messages: [
{ content: 'first question', role: 'user', timestamp: 1 },
{ content: 'first answer', role: 'assistant', timestamp: 2 },
{ content: 'second question', role: 'user', timestamp: 3 },
{ content: 'second answer', role: 'assistant', timestamp: 4 }
],
session_id: 'stored-parent'
} as never)

let branchCurrentSession: ((messageId?: string) => Promise<boolean>) | null = null
render(
<BranchHarness
Expand All @@ -2240,8 +2228,8 @@ describe('branchStoredSession desktop source tagging', () => {

await expect(branchCurrentSession!()).resolves.toBe(true)

expect(getAllSessionMessages).toHaveBeenCalledWith('stored-parent', undefined)
expect(branchParams).toEqual({ session_id: 'live-parent' })
expect(getAllSessionMessages).not.toHaveBeenCalled()
expect(branchParams).toEqual({ session_id: 'live-parent', omit_messages: true })
})

it('aborts if the active runtime changes while the branch transcript is hydrating', async () => {
Expand Down Expand Up @@ -2275,16 +2263,17 @@ describe('branchStoredSession desktop source tagging', () => {
)
await waitFor(() => expect(branchCurrentSession).not.toBeNull())

await expect(branchCurrentSession!()).resolves.toBe(false)
await expect(branchCurrentSession!('q1')).resolves.toBe(false)
expect(requestGateway).not.toHaveBeenCalledWith('session.branch', expect.anything())
})

// #67603: right-clicking a session outside the paginated sidebar window is a
// cache miss. Resolve its owning profile (cache → active → cross-profile) and
// swap to it before reading the transcript / creating the branch, so the fork
// is not created on whichever profile happens to be live.
// swap to it before asking the backend to copy the transcript, so the fork is
// not created on whichever profile happens to be live.
it('resolves and swaps to the parent profile when the branched session is not cached', async () => {
setSessions([])
vi.mocked(getAllSessionMessages).mockClear()
vi.mocked(getSession).mockResolvedValue(storedSession({ id: 'stored-parent', message_count: 1, profile: 'work' }))
vi.mocked(getAllSessionMessages).mockResolvedValue({
messages: [{ content: 'branch me', role: 'user', timestamp: 1 }],
Expand All @@ -2294,7 +2283,7 @@ describe('branchStoredSession desktop source tagging', () => {
let createParams: Record<string, unknown> | undefined

const requestGateway = vi.fn(async (method: string, params?: Record<string, unknown>) => {
if (method === 'session.create') {
if (method === 'session.create' || method === 'session.branch_stored') {
createParams = params

return { session_id: 'branch-runtime', stored_session_id: 'branch-stored' } as never
Expand All @@ -2312,7 +2301,7 @@ describe('branchStoredSession desktop source tagging', () => {
await expect(branchStoredSession!('stored-parent')).resolves.toBe(true)

expect(ensureGatewayProfile).toHaveBeenCalledWith('work')
expect(getAllSessionMessages).toHaveBeenCalledWith('stored-parent', 'work')
expect(getAllSessionMessages).not.toHaveBeenCalled()
// The create itself must carry the owning profile: in app-global remote
// mode the soft gateway swap alone is not enough — an omitted profile
// lands the branch on the launch (default) profile's state.db.
Expand All @@ -2331,7 +2320,7 @@ describe('branchStoredSession desktop source tagging', () => {
let createParams: Record<string, unknown> | undefined

const requestGateway = vi.fn(async (method: string, params?: Record<string, unknown>) => {
if (method === 'session.create') {
if (method === 'session.create' || method === 'session.branch_stored') {
createParams = params

return { session_id: 'branch-runtime', stored_session_id: 'branch-stored' } as never
Expand Down Expand Up @@ -2360,7 +2349,7 @@ describe('branchStoredSession desktop source tagging', () => {
let createParams: Record<string, unknown> | undefined

const requestGateway = vi.fn(async (method: string, params?: Record<string, unknown>) => {
if (method === 'session.create') {
if (method === 'session.create' || method === 'session.branch_stored') {
createParams = params

return { session_id: 'branch-runtime', stored_session_id: 'branch-stored' } as never
Expand All @@ -2378,6 +2367,39 @@ describe('branchStoredSession desktop source tagging', () => {
expect(createParams).toBeDefined()
expect(createParams).not.toHaveProperty('profile')
})

it('falls back without creating an empty branch against an older backend', async () => {
const requestGateway = vi.fn(async (method: string, params?: Record<string, unknown>) => {
if (method === 'session.branch_stored') {
throw new Error('unknown method: session.branch_stored')
}

if (method === 'session.create') {
expect(params?.messages).toEqual([{ content: 'persisted context', role: 'user' }])

return { session_id: 'branch-runtime', stored_session_id: 'branch-stored' } as never
}

return {} as never
})

vi.mocked(getAllSessionMessages).mockResolvedValue({
messages: [{ content: 'persisted context', role: 'user', timestamp: 1 }],
session_id: 'stored-parent'
} as never)

let branchStoredSession: ((storedSessionId: string) => Promise<boolean>) | null = null
render(<BranchHarness onReady={branch => (branchStoredSession = branch)} requestGateway={requestGateway} />)
await waitFor(() => expect(branchStoredSession).not.toBeNull())

await expect(branchStoredSession!('stored-parent')).resolves.toBe(true)
expect(requestGateway).toHaveBeenCalledWith(
'session.create',
expect.objectContaining({
parent_session_id: 'stored-parent'
})
)
})
})

// ── Main/tile dedup (the "same session open in main AND its own tab" bug) ─────
Expand Down
Loading