Files
orca/src/main/runtime/multi-client-navigation-isolation.integration.test.ts
T

755 lines
26 KiB
TypeScript

import { mkdtempSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { parsePairingCode } from '../../shared/pairing'
import type { RuntimeMobileSessionTabsResult } from '../../shared/runtime-types'
import type { PersistedMobileClientTabSelections } from '../../shared/persisted-state-types'
import { OrcaRuntimeService } from './orca-runtime'
import { OrcaRuntimeRpcServer } from './runtime-rpc'
import {
activeTabId,
authenticate,
CLIENT_A2_WORKTREE_ID,
CLIENT_A_WORKTREE_ID,
CLIENT_B_WORKTREE_ID,
createReader,
FOLDER_REPO_ID,
HOST_WORKTREE_ID,
makeStore,
REPO_ID,
resultType,
seedSessionTabs,
send,
SESSION_WORKTREE_ID,
snapshotVersion,
worktreeId,
type PairedSession,
type ResponseReader
} from './paired-client-navigation-test-harness'
vi.mock('../git/worktree', () => {
const worktrees = ['host', 'client-a', 'client-a2', 'client-b', 'session'].map((name) => ({
path: `/tmp/${name}`,
head: name,
branch: name,
isBare: false,
isMainWorktree: false
}))
return {
listWorktrees: vi.fn().mockResolvedValue(worktrees),
listWorktreesStrict: vi.fn().mockResolvedValue(worktrees)
}
})
describe('paired runtime navigation isolation', () => {
const servers: OrcaRuntimeRpcServer[] = []
const sessions: PairedSession[] = []
const readers: ResponseReader[] = []
afterEach(async () => {
for (const reader of readers.splice(0)) {
reader.dispose()
}
for (const session of sessions.splice(0)) {
session.ws.close()
}
await Promise.all(servers.splice(0).map((server) => server.stop()))
})
/** `headless: true` models `orca serve` — no renderer notifier and no attached window. */
async function startHarness(options: { headless?: boolean } = {}) {
const hostSelections = { worktreeId: HOST_WORKTREE_ID, tabId: 'host-tab' }
const activateWorktree = vi.fn((_repoId: string, nextWorktreeId: string) => {
hostSelections.worktreeId = nextWorktreeId
})
const focusTerminal = vi.fn((nextTabId: string) => {
hostSelections.tabId = nextTabId
})
const runtime = new OrcaRuntimeService(makeStore() as never)
if (!options.headless) {
runtime.setNotifier({
worktreesChanged: vi.fn(),
reposChanged: vi.fn(),
activateWorktree,
createTerminal: vi.fn(),
revealTerminalSession: vi.fn(),
splitTerminal: vi.fn(),
renameTerminal: vi.fn(),
focusTerminal,
closeTerminal: vi.fn(),
sleepWorktree: vi.fn(),
terminalFitOverrideChanged: vi.fn(),
terminalDriverChanged: vi.fn()
})
runtime.attachWindow(1)
runtime.markGraphReady(1)
seedSessionTabs(runtime)
}
const server = new OrcaRuntimeRpcServer({
runtime,
userDataPath: mkdtempSync(join(tmpdir(), 'orca-navigation-isolation-')),
enableWebSocket: true,
wsPort: 0
})
servers.push(server)
await server.start()
const offerA = server.createPairingOffer({
address: '127.0.0.1',
name: 'client-a',
scope: 'runtime'
})
if (!offerA.available) {
throw new Error('pairing_unavailable')
}
const clientA = await authenticate(offerA.pairingUrl)
sessions.push(clientA)
const offerB = server.createPairingOffer({
address: '127.0.0.1',
name: 'client-b',
scope: 'runtime'
})
if (!offerB.available) {
throw new Error('pairing_unavailable')
}
const clientB = await authenticate(offerB.pairingUrl)
sessions.push(clientB)
expect(offerA.deviceId).not.toBe(offerB.deviceId)
expect(parsePairingCode(offerA.pairingUrl)?.pairedDeviceId).toBe(offerA.deviceId)
expect(parsePairingCode(offerB.pairingUrl)?.pairedDeviceId).toBe(offerB.deviceId)
expect(parsePairingCode(offerA.pairingUrl)?.deviceToken).not.toBe(
parsePairingCode(offerB.pairingUrl)?.deviceToken
)
const readerA = createReader(clientA)
const readerB = createReader(clientB)
readers.push(readerA, readerB)
return {
runtime,
hostSelections,
activateWorktree,
focusTerminal,
clientA,
clientB,
deviceIdA: offerA.deviceId,
deviceIdB: offerB.deviceId,
pairingUrlA: offerA.pairingUrl,
pairingUrlB: offerB.pairingUrl,
readerA,
readerB
}
}
it('attributes workspace creation to each authenticated device across reconnect', async () => {
const harness = await startHarness()
const createManagedWorktree = vi
.spyOn(harness.runtime, 'createManagedWorktree')
.mockImplementation(async (args) => {
const creatorProvenance = (args as unknown as Record<string, unknown>).creatorProvenance
return {
worktree: {
id: worktreeId(args.name),
creatorProvenance
}
} as never
})
send(harness.clientA, {
id: 'status-a',
method: 'status.get'
})
send(harness.clientB, {
id: 'status-b',
method: 'status.get'
})
const [statusA, statusB] = await Promise.all([
harness.readerA.next('status-a'),
harness.readerB.next('status-b')
])
expect(statusA).toMatchObject({
ok: true,
result: { pairedDeviceId: harness.deviceIdA }
})
expect(statusB).toMatchObject({
ok: true,
result: { pairedDeviceId: harness.deviceIdB }
})
send(harness.clientA, {
id: 'create-a',
method: 'worktree.create',
params: {
repo: REPO_ID,
name: 'created-a',
creatorProvenance: { kind: 'paired-device', deviceId: harness.deviceIdB }
}
})
send(harness.clientB, {
id: 'create-b',
method: 'worktree.create',
params: { repo: REPO_ID, name: 'created-b' }
})
await Promise.all([harness.readerA.next('create-a'), harness.readerB.next('create-b')])
expect(createManagedWorktree).toHaveBeenCalledWith(
expect.objectContaining({
name: 'created-a',
creatorProvenance: { kind: 'paired-device', deviceId: harness.deviceIdA }
})
)
expect(createManagedWorktree).toHaveBeenCalledWith(
expect.objectContaining({
name: 'created-b',
creatorProvenance: { kind: 'paired-device', deviceId: harness.deviceIdB }
})
)
await new Promise<void>((resolve) => {
harness.clientA.ws.once('close', () => resolve())
harness.clientA.ws.close()
})
const reconnectedA = await authenticate(harness.pairingUrlA)
sessions.push(reconnectedA)
const reconnectedReaderA = createReader(reconnectedA)
readers.push(reconnectedReaderA)
send(reconnectedA, { id: 'status-a2', method: 'status.get' })
await expect(reconnectedReaderA.next('status-a2')).resolves.toMatchObject({
ok: true,
result: { pairedDeviceId: harness.deviceIdA }
})
})
it('keeps worktree navigation local to each paired runtime client by default', async () => {
const harness = await startHarness()
const clientSelections = {
a: CLIENT_A_WORKTREE_ID,
b: CLIENT_B_WORKTREE_ID
}
for (const [session, id] of [
[harness.clientA, 'events-a'],
[harness.clientB, 'events-b']
] as const) {
send(session, { id, method: 'runtime.clientEvents.subscribe' })
}
await Promise.all([
harness.readerA.next('events-a', (response) => resultType(response) === 'ready'),
harness.readerB.next('events-b', (response) => resultType(response) === 'ready')
])
clientSelections.a = CLIENT_A2_WORKTREE_ID
send(harness.clientA, {
id: 'activate-a2',
method: 'worktree.activate',
params: {
worktree: `id:${CLIENT_A2_WORKTREE_ID}`,
notifyClients: true
}
})
await expect(harness.readerA.next('activate-a2')).resolves.toMatchObject({ ok: true })
harness.runtime.notifyWorktreesChangedForRemoteClients(REPO_ID)
const [nextA, nextB] = await Promise.all([
harness.readerA.next('events-a'),
harness.readerB.next('events-b')
])
expect([resultType(nextA), resultType(nextB)]).toEqual(['worktreesChanged', 'worktreesChanged'])
expect(clientSelections).toEqual({
a: CLIENT_A2_WORKTREE_ID,
b: CLIENT_B_WORKTREE_ID
})
expect(harness.hostSelections.worktreeId).toBe(HOST_WORKTREE_ID)
expect(harness.activateWorktree).not.toHaveBeenCalled()
})
async function subscribeBothClientEventStreams(harness: {
clientA: PairedSession
clientB: PairedSession
readerA: ResponseReader
readerB: ResponseReader
}): Promise<void> {
for (const [session, id] of [
[harness.clientA, 'events-a'],
[harness.clientB, 'events-b']
] as const) {
send(session, { id, method: 'runtime.clientEvents.subscribe' })
}
await Promise.all([
harness.readerA.next('events-a', (response) => resultType(response) === 'ready'),
harness.readerB.next('events-b', (response) => resultType(response) === 'ready')
])
}
it('keeps a paired client workspace create-with-activate off every other client and the host', async () => {
const harness = await startHarness()
await subscribeBothClientEventStreams(harness)
// Negative control: repeated ordinary navigation by A never reaches B or the host.
for (const [index, target] of [
CLIENT_A2_WORKTREE_ID,
CLIENT_A_WORKTREE_ID,
CLIENT_A2_WORKTREE_ID
].entries()) {
send(harness.clientA, {
id: `nav-${index}`,
method: 'worktree.activate',
params: { worktree: `id:${target}` }
})
await expect(harness.readerA.next(`nav-${index}`)).resolves.toMatchObject({ ok: true })
}
send(harness.clientA, {
id: 'create-with-agent',
method: 'worktree.create',
params: {
repo: FOLDER_REPO_ID,
name: 'teammate-agent-workspace',
activate: true
}
})
await expect(harness.readerA.next('create-with-agent')).resolves.toMatchObject({ ok: true })
// Fence: a shared-catalog event emitted after the create must be the next frame each
// client sees. An activation frame would be delivered ahead of it on the ordered stream.
harness.runtime.notifyReposChangedForRemoteClients()
const observed: Record<'a' | 'b', string[]> = { a: [], b: [] }
for (const [key, reader, id] of [
['a', harness.readerA, 'events-a'],
['b', harness.readerB, 'events-b']
] as const) {
for (;;) {
const type = resultType(await reader.next(id))
observed[key].push(type ?? 'unknown')
if (type === 'reposChanged' || type === 'activateWorktree') {
break
}
}
}
// The observer must never be navigated by the creator...
expect(observed.b).not.toContain('activateWorktree')
expect(observed.b.at(-1)).toBe('reposChanged')
expect(harness.activateWorktree).not.toHaveBeenCalled()
expect(harness.hostSelections.worktreeId).toBe(HOST_WORKTREE_ID)
// ...nor is the creator's own view driven from the host stream (it navigates from its RPC result)...
expect(observed.a).not.toContain('activateWorktree')
expect(observed.a.at(-1)).toBe('reposChanged')
// ...while shared catalog state still reaches both clients.
expect(observed.a).toContain('worktreesChanged')
expect(observed.b).toContain('worktreesChanged')
})
it('does not replay a missed create activation to a reconnecting observer', async () => {
const harness = await startHarness()
await subscribeBothClientEventStreams(harness)
// The observer is offline while the creator works.
harness.readerB.dispose()
await new Promise<void>((resolve) => {
harness.clientB.ws.once('close', () => resolve())
harness.clientB.ws.close()
})
// Why navigation 'all': a caller-scoped create emits no activation at all, so the
// reconnect assertion below would pass vacuously — there must be a real frame to miss.
send(harness.clientA, {
id: 'create-while-b-offline',
method: 'worktree.create',
params: {
repo: FOLDER_REPO_ID,
name: 'offline-observer-workspace',
activate: true,
navigation: 'all'
}
})
await expect(harness.readerA.next('create-while-b-offline')).resolves.toMatchObject({
ok: true
})
// The still-connected client proves the activation really was emitted while B was down.
expect(
resultType(
await harness.readerA.next(
'events-a',
(response) => resultType(response) === 'activateWorktree'
)
)
).toBe('activateWorktree')
const reconnectedB = await authenticate(harness.pairingUrlB)
sessions.push(reconnectedB)
const reconnectedReaderB = createReader(reconnectedB)
readers.push(reconnectedReaderB)
send(reconnectedB, { id: 'events-b2', method: 'runtime.clientEvents.subscribe' })
await reconnectedReaderB.next('events-b2', (response) => resultType(response) === 'ready')
// Why: the ready snapshot must not carry navigation intent forward; only live,
// addressed activation is legitimate, so a reconnect can never inherit it.
harness.runtime.notifyReposChangedForRemoteClients()
const observed: string[] = []
for (;;) {
const type = resultType(await reconnectedReaderB.next('events-b2'))
observed.push(type ?? 'unknown')
if (type === 'reposChanged' || type === 'activateWorktree') {
break
}
}
expect(observed).not.toContain('activateWorktree')
expect(observed.at(-1)).toBe('reposChanged')
})
it('still reveals to every client when a paired caller asks for all-surface navigation', async () => {
// Why: the CLI pairs as a runtime device but has no viewer of its own, so
// `orca worktree create --activate` against a remote runtime sends navigation 'all'.
const harness = await startHarness()
await subscribeBothClientEventStreams(harness)
send(harness.clientA, {
id: 'cli-shaped-create',
method: 'worktree.create',
params: {
repo: FOLDER_REPO_ID,
name: 'cli-shaped-workspace',
activate: true,
navigation: 'all'
}
})
await expect(harness.readerA.next('cli-shaped-create')).resolves.toMatchObject({ ok: true })
const [eventA, eventB] = await Promise.all([
harness.readerA.next('events-a', (response) => resultType(response) === 'activateWorktree'),
harness.readerB.next('events-b', (response) => resultType(response) === 'activateWorktree')
])
expect([resultType(eventA), resultType(eventB)]).toEqual([
'activateWorktree',
'activateWorktree'
])
expect(harness.activateWorktree).toHaveBeenCalled()
})
it('keeps create activation caller-scoped on a headless orca serve host', async () => {
const harness = await startHarness({ headless: true })
await subscribeBothClientEventStreams(harness)
send(harness.clientA, {
id: 'headless-create',
method: 'worktree.create',
params: { repo: FOLDER_REPO_ID, name: 'headless-agent-workspace', activate: true }
})
await expect(harness.readerA.next('headless-create')).resolves.toMatchObject({ ok: true })
harness.runtime.notifyReposChangedForRemoteClients()
const observed: string[] = []
for (;;) {
const type = resultType(await harness.readerB.next('events-b'))
observed.push(type ?? 'unknown')
if (type === 'reposChanged' || type === 'activateWorktree') {
break
}
}
expect(observed).not.toContain('activateWorktree')
expect(observed).toContain('worktreesChanged')
// A headless host with no viewer of its own still reveals an in-process/CLI create.
await harness.runtime.createManagedWorktree({
repoSelector: `id:${FOLDER_REPO_ID}`,
name: 'headless-cli-workspace',
activate: true
})
expect(
resultType(
await harness.readerB.next(
'events-b',
(response) => resultType(response) === 'activateWorktree'
)
)
).toBe('activateWorktree')
})
it('still reveals a host-originated create-with-activate on the host and every client', async () => {
const harness = await startHarness()
await subscribeBothClientEventStreams(harness)
// Why: a headless server's only viewer is a remote client, so an in-process/CLI
// create must keep reaching clients; only paired-client callers are scoped.
await harness.runtime.createManagedWorktree({
repoSelector: `id:${FOLDER_REPO_ID}`,
name: 'cli-created-workspace',
activate: true
})
const [eventA, eventB] = await Promise.all([
harness.readerA.next('events-a', (response) => resultType(response) === 'activateWorktree'),
harness.readerB.next('events-b', (response) => resultType(response) === 'activateWorktree')
])
expect([resultType(eventA), resultType(eventB)]).toEqual([
'activateWorktree',
'activateWorktree'
])
expect(harness.activateWorktree).toHaveBeenCalled()
})
it('projects session-tab activation only to the paired caller across fanout and reconnect', async () => {
const harness = await startHarness()
send(harness.clientA, {
id: 'select-a',
method: 'session.tabs.activate',
params: {
worktree: `id:${SESSION_WORKTREE_ID}`,
tabId: 'client-a-tab',
navigation: 'caller',
notifyClients: false
}
})
send(harness.clientB, {
id: 'select-b',
method: 'session.tabs.activate',
params: {
worktree: `id:${SESSION_WORKTREE_ID}`,
tabId: 'client-b-tab',
navigation: 'caller',
notifyClients: false
}
})
expect(activeTabId(await harness.readerA.next('select-a'))).toBe('client-a-tab')
expect(activeTabId(await harness.readerB.next('select-b'))).toBe('client-b-tab')
for (const [session, id] of [
[harness.clientA, 'tabs-a'],
[harness.clientB, 'tabs-b']
] as const) {
send(session, {
id,
method: 'session.tabs.subscribe',
params: { worktree: `id:${SESSION_WORKTREE_ID}` }
})
}
expect(activeTabId(await harness.readerA.next('tabs-a'))).toBe('client-a-tab')
expect(activeTabId(await harness.readerB.next('tabs-b'))).toBe('client-b-tab')
send(harness.clientA, {
id: 'select-a2',
method: 'session.tabs.activate',
params: {
worktree: `id:${SESSION_WORKTREE_ID}`,
tabId: 'client-a2-tab',
navigation: 'caller',
notifyClients: false
}
})
const selectA2 = await harness.readerA.next('select-a2')
expect(activeTabId(selectA2)).toBe('client-a2-tab')
harness.runtime.notifyMobileSessionTabsChanged(SESSION_WORKTREE_ID)
const [updateA, updateB] = await Promise.all([
harness.readerA.next(
'tabs-a',
(response) =>
resultType(response) === 'updated' &&
snapshotVersion(response) >= snapshotVersion(selectA2)
),
harness.readerB.next('tabs-b', (response) => resultType(response) === 'updated')
])
expect(activeTabId(updateA)).toBe('client-a2-tab')
expect(activeTabId(updateB)).toBe('client-b-tab')
expect(harness.hostSelections.tabId).toBe('host-tab')
expect(harness.focusTerminal).not.toHaveBeenCalled()
expect(
(await harness.runtime.listMobileSessionTabs(`id:${SESSION_WORKTREE_ID}`)).activeTabId
).toBe('host-tab')
harness.readerA.dispose()
harness.clientA.ws.close()
const reconnectedA = await authenticate(harness.pairingUrlA)
sessions.push(reconnectedA)
const reconnectedReaderA = createReader(reconnectedA)
readers.push(reconnectedReaderA)
send(reconnectedA, {
id: 'tabs-a-reconnected',
method: 'session.tabs.list',
params: { worktree: `id:${SESSION_WORKTREE_ID}` }
})
expect(activeTabId(await reconnectedReaderA.next('tabs-a-reconnected'))).toBe('client-a2-tab')
})
it('routes explicit host and paired-client follow intent without changing the default', async () => {
const harness = await startHarness()
for (const [session, id] of [
[harness.clientA, 'events-a'],
[harness.clientB, 'events-b']
] as const) {
send(session, { id, method: 'runtime.clientEvents.subscribe' })
}
await Promise.all([
harness.readerA.next('events-a', (response) => resultType(response) === 'ready'),
harness.readerB.next('events-b', (response) => resultType(response) === 'ready')
])
send(harness.clientA, {
id: 'host-follow',
method: 'worktree.activate',
params: { worktree: `id:${CLIENT_A_WORKTREE_ID}`, navigation: 'host' }
})
await harness.readerA.next('host-follow')
harness.runtime.notifyWorktreesChangedForRemoteClients(REPO_ID)
expect(resultType(await harness.readerA.next('events-a'))).toBe('worktreesChanged')
expect(resultType(await harness.readerB.next('events-b'))).toBe('worktreesChanged')
expect(harness.hostSelections.worktreeId).toBe(CLIENT_A_WORKTREE_ID)
send(harness.clientB, {
id: 'clients-follow',
method: 'worktree.activate',
params: { worktree: `id:${CLIENT_B_WORKTREE_ID}`, navigation: 'clients' }
})
await harness.readerB.next('clients-follow')
const [eventA, eventB] = await Promise.all([
harness.readerA.next('events-a'),
harness.readerB.next('events-b')
])
expect([resultType(eventA), resultType(eventB)]).toEqual([
'activateWorktree',
'activateWorktree'
])
expect(harness.hostSelections.worktreeId).toBe(CLIENT_A_WORKTREE_ID)
for (const [session, id] of [
[harness.clientA, 'tabs-a'],
[harness.clientB, 'tabs-b']
] as const) {
send(session, {
id,
method: 'session.tabs.subscribe',
params: { worktree: `id:${SESSION_WORKTREE_ID}` }
})
}
await Promise.all([harness.readerA.next('tabs-a'), harness.readerB.next('tabs-b')])
send(harness.clientA, {
id: 'tabs-host-follow',
method: 'session.tabs.activate',
params: {
worktree: `id:${SESSION_WORKTREE_ID}`,
tabId: 'client-a-tab',
navigation: 'host'
}
})
expect(activeTabId(await harness.readerA.next('tabs-host-follow'))).toBe('client-a-tab')
expect(
activeTabId(
await harness.readerA.next('tabs-a', (response) => resultType(response) === 'updated')
)
).toBe('client-a-tab')
expect(harness.hostSelections.tabId).toBe('client-a-tab')
send(harness.clientB, {
id: 'tabs-clients-follow',
method: 'session.tabs.activate',
params: {
worktree: `id:${SESSION_WORKTREE_ID}`,
tabId: 'client-b-tab',
navigation: 'clients'
}
})
await harness.readerB.next('tabs-clients-follow')
const [tabsA, tabsB] = await Promise.all([
harness.readerA.next('tabs-a', (response) => resultType(response) === 'updated'),
harness.readerB.next('tabs-b', (response) => resultType(response) === 'updated')
])
expect([activeTabId(tabsA), activeTabId(tabsB)]).toEqual(['client-b-tab', 'client-b-tab'])
expect(
[tabsA, tabsB].map(
(response) =>
(response.result as RuntimeMobileSessionTabsResult | undefined)?.navigationIntent
)
).toEqual(['follow', 'follow'])
expect(harness.hostSelections.tabId).toBe('client-a-tab')
})
it('isolates crossed clients across two runtime servers', async () => {
const serverOne = await startHarness()
const serverTwo = await startHarness()
for (const [session, id] of [
[serverTwo.clientA, 'server-two-events-a'],
[serverTwo.clientB, 'server-two-events-b']
] as const) {
send(session, { id, method: 'runtime.clientEvents.subscribe' })
}
await Promise.all([
serverTwo.readerA.next('server-two-events-a', (response) => resultType(response) === 'ready'),
serverTwo.readerB.next('server-two-events-b', (response) => resultType(response) === 'ready')
])
send(serverOne.clientA, {
id: 'server-one-worktree',
method: 'worktree.activate',
params: { worktree: `id:${CLIENT_A2_WORKTREE_ID}`, notifyClients: true }
})
await serverOne.readerA.next('server-one-worktree')
serverTwo.runtime.notifyWorktreesChangedForRemoteClients(REPO_ID)
expect(resultType(await serverTwo.readerA.next('server-two-events-a'))).toBe('worktreesChanged')
expect(resultType(await serverTwo.readerB.next('server-two-events-b'))).toBe('worktreesChanged')
expect(serverOne.hostSelections.worktreeId).toBe(HOST_WORKTREE_ID)
expect(serverTwo.hostSelections.worktreeId).toBe(HOST_WORKTREE_ID)
for (const [session, reader, requestId, tabId] of [
[serverOne.clientA, serverOne.readerA, 's1-a', 'client-a2-tab'],
[serverOne.clientB, serverOne.readerB, 's1-b', 'client-b-tab'],
[serverTwo.clientA, serverTwo.readerA, 's2-a', 'client-a-tab'],
[serverTwo.clientB, serverTwo.readerB, 's2-b', 'host-tab']
] as const) {
send(session, {
id: requestId,
method: 'session.tabs.activate',
params: {
worktree: `id:${SESSION_WORKTREE_ID}`,
tabId,
navigation: 'caller',
notifyClients: false
}
})
expect(activeTabId(await reader.next(requestId))).toBe(tabId)
}
expect(serverOne.hostSelections.tabId).toBe('host-tab')
expect(serverTwo.hostSelections.tabId).toBe('host-tab')
})
it('restores a device tab selection after a runtime restart', async () => {
const persisted: { state: PersistedMobileClientTabSelections } = { state: {} }
const makeStoreWithSelections = () => ({
...makeStore(),
getMobileClientTabSelections: () => persisted.state,
setMobileClientTabSelections: (next: PersistedMobileClientTabSelections) => {
persisted.state = next
}
})
const first = new OrcaRuntimeService(makeStoreWithSelections() as never)
first.attachWindow(1)
first.markGraphReady(1)
seedSessionTabs(first)
await first.activateMobileSessionTab(`id:${SESSION_WORKTREE_ID}`, 'client-a-tab', undefined, {
notifyClients: false,
clientNavigationId: 'device-a',
navigation: 'caller'
})
expect(persisted.state['device-a']?.[SESSION_WORKTREE_ID]?.activeTabId).toBe('client-a-tab')
const restarted = new OrcaRuntimeService(makeStoreWithSelections() as never)
restarted.attachWindow(1)
restarted.markGraphReady(1)
seedSessionTabs(restarted)
const remembered = await restarted.listMobileSessionTabs(
`id:${SESSION_WORKTREE_ID}`,
'device-a'
)
expect(remembered.activeTabId).toBe('client-a-tab')
expect(remembered.tabs.find((tab) => tab.isActive)?.id).toBe('client-a-tab')
// Why: an unknown device must still start from deterministic topology, not inherit another device's restored state.
const freshDevice = await restarted.listMobileSessionTabs(
`id:${SESSION_WORKTREE_ID}`,
'device-b'
)
expect(freshDevice.activeTabId).toBe('host-tab')
})
})