mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
fix(session-tabs): rebuild an emptied headless session snapshot instead of hiding saved terminals (#29283)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
+116
@@ -0,0 +1,116 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { OrcaRuntimeService } from '../orca-runtime-test-mocks.spec'
|
||||
import {
|
||||
HEADLESS_LEAF_ID,
|
||||
TEST_WORKTREE_ID,
|
||||
makeHeadlessTerminalLayout,
|
||||
makeRuntimeStoreWithWorkspaceSession,
|
||||
makeWorkspaceSessionWithHeadlessTerminal
|
||||
} from '../orca-runtime-test-fixtures.spec'
|
||||
import { getRuntimeBrowserPageRegistry } from '../runtime-browser-page-registry'
|
||||
|
||||
// #25900: a windowless host listed no tabs for a worktree whose saved session still had live
|
||||
// agent terminals, so a client waiting to reattach heard "nothing here" and stayed on Reconnecting.
|
||||
|
||||
const AGENT_PTY_ID = 'daemon-agent-pty'
|
||||
const PLACEMENT = {
|
||||
kind: 'client' as const,
|
||||
browserHostClientId: 'host-a',
|
||||
browserHostGeneration: 3,
|
||||
pageHostGeneration: 9
|
||||
}
|
||||
|
||||
function makeHost() {
|
||||
const { runtimeStore, getSession, setSession } = makeRuntimeStoreWithWorkspaceSession(
|
||||
makeWorkspaceSessionWithHeadlessTerminal({
|
||||
activeTabIdByWorktree: {},
|
||||
tabsByWorktree: { [TEST_WORKTREE_ID]: [] },
|
||||
terminalLayoutsByTabId: {}
|
||||
})
|
||||
)
|
||||
const runtime = new OrcaRuntimeService(runtimeStore)
|
||||
runtime.setPtyController({
|
||||
spawn: vi.fn().mockResolvedValue({ id: 'unexpected-pty' }),
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null,
|
||||
listProcesses: async () => []
|
||||
})
|
||||
const pages = getRuntimeBrowserPageRegistry(runtime)
|
||||
return {
|
||||
runtime,
|
||||
openPage: (browserPageId: string) =>
|
||||
pages.publishClientPage({
|
||||
browserPageId,
|
||||
workspaceId: TEST_WORKTREE_ID,
|
||||
browserProfileId: 'default',
|
||||
executionHostKey: 'native:runtime-a:7',
|
||||
placement: PLACEMENT,
|
||||
url: 'https://client.test/',
|
||||
title: 'Client page',
|
||||
loading: false,
|
||||
active: true
|
||||
}),
|
||||
closePage: (browserPageId: string) => pages.retirePage(browserPageId, PLACEMENT),
|
||||
saveAgentTerminal: () => {
|
||||
const session = getSession()
|
||||
setSession({
|
||||
...session,
|
||||
activeTabIdByWorktree: { [TEST_WORKTREE_ID]: 'agent-tab' },
|
||||
tabsByWorktree: {
|
||||
[TEST_WORKTREE_ID]: [
|
||||
{
|
||||
id: 'agent-tab',
|
||||
ptyId: AGENT_PTY_ID,
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
title: 'codex',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
terminalLayoutsByTabId: {
|
||||
'agent-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: AGENT_PTY_ID })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function listedTabTypes(runtime: OrcaRuntimeService): Promise<string[]> {
|
||||
const snapshots = await runtime.listAllMobileSessionTabs()
|
||||
return (snapshots.find((snapshot) => snapshot.worktree === TEST_WORKTREE_ID)?.tabs ?? [])
|
||||
.map((tab) => tab.type)
|
||||
.sort()
|
||||
}
|
||||
|
||||
describe('OrcaRuntimeService empty headless session snapshot', () => {
|
||||
it('rebuilds the terminals when the last browser closes instead of listing nothing', async () => {
|
||||
const host = makeHost()
|
||||
host.openPage('page-1')
|
||||
expect(await listedTabTypes(host.runtime)).toEqual(['browser'])
|
||||
|
||||
host.saveAgentTerminal()
|
||||
host.closePage('page-1')
|
||||
|
||||
expect(await listedTabTypes(host.runtime)).toEqual(['terminal'])
|
||||
})
|
||||
|
||||
it('keeps the saved terminals listed when a browser opens over an emptied snapshot', async () => {
|
||||
const host = makeHost()
|
||||
host.openPage('page-1')
|
||||
await listedTabTypes(host.runtime)
|
||||
host.closePage('page-1')
|
||||
// The emptied snapshot this host built stays stored while the session gains its terminal.
|
||||
expect(await listedTabTypes(host.runtime)).toEqual([])
|
||||
|
||||
host.saveAgentTerminal()
|
||||
host.openPage('page-2')
|
||||
|
||||
expect(await listedTabTypes(host.runtime)).toEqual(['browser', 'terminal'])
|
||||
const listed = await host.runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)
|
||||
expect(listed.tabs.map((tab) => tab.type).sort()).toEqual(['browser', 'terminal'])
|
||||
})
|
||||
})
|
||||
@@ -84,6 +84,7 @@ await import('./orca-runtime-tests/mobile-session-tabs-part-12.spec')
|
||||
await import('./orca-runtime-tests/mobile-session-tabs-part-13.spec')
|
||||
await import('./orca-runtime-tests/mobile-session-tabs-part-14.spec')
|
||||
await import('./orca-runtime-tests/mobile-session-tabs-cold-serve-hydrate.spec')
|
||||
await import('./orca-runtime-tests/mobile-session-tabs-empty-headless-snapshot.spec')
|
||||
await import('./orca-runtime-tests/exit-retirement-activation.spec')
|
||||
await import('./orca-runtime-tests/mobile-creation-and-orchestration.spec')
|
||||
await import('./orca-runtime-tests/mobile-creation-and-orchestration-part-02.spec')
|
||||
|
||||
@@ -94,6 +94,20 @@ export abstract class OrcaRuntimeWithHydrateHeadlessMobileSessionTabsFromWorkspa
|
||||
|
||||
protected abstract getAvailableAuthoritativeWindow(): BrowserWindow | null
|
||||
|
||||
/** A stored snapshot, or undefined when it is one this host built and left empty. */
|
||||
private readRebuildableMobileSessionSnapshot(
|
||||
worktreeId: string
|
||||
): RuntimeMobileSessionTabsSnapshot | undefined {
|
||||
const stored = this.mobileSessionTabsByWorktree.get(worktreeId)
|
||||
// Why: only the persisted session feeds a headless-built snapshot, so an empty one is never an
|
||||
// answer — as a base it made the next pass seed browser rows alone and hide every terminal.
|
||||
return stored &&
|
||||
stored.tabs.length === 0 &&
|
||||
this.isHeadlessBuiltMobileSessionPublicationBase(stored.publicationEpoch)
|
||||
? undefined
|
||||
: stored
|
||||
}
|
||||
|
||||
hydrateHeadlessMobileSessionTabsFromWorkspaceSession(
|
||||
worktreeId?: string,
|
||||
options: {
|
||||
@@ -169,10 +183,10 @@ export abstract class OrcaRuntimeWithHydrateHeadlessMobileSessionTabsFromWorkspa
|
||||
continue
|
||||
}
|
||||
}
|
||||
const existing = this.mobileSessionTabsByWorktree.get(entryWorktreeId)
|
||||
const stored = this.readRebuildableMobileSessionSnapshot(entryWorktreeId)
|
||||
if (
|
||||
existing &&
|
||||
existing.tabs.length > 0 &&
|
||||
stored &&
|
||||
stored.tabs.length > 0 &&
|
||||
options.force !== true &&
|
||||
options.onlyRuntimeOwnedTerminals !== true
|
||||
) {
|
||||
@@ -180,10 +194,15 @@ export abstract class OrcaRuntimeWithHydrateHeadlessMobileSessionTabsFromWorkspa
|
||||
// offscreen browser tabs are live and may have been created/closed since.
|
||||
// Reconcile just the browser tabs against the live bridge instead of
|
||||
// leaving a stale snapshot that omits a freshly-opened browser tab.
|
||||
this.reconcileHeadlessMobileSessionBrowserTabs(entryWorktreeId, existing)
|
||||
this.reconcileHeadlessMobileSessionBrowserTabs(entryWorktreeId, stored)
|
||||
reconciledWorktreeIds.add(entryWorktreeId)
|
||||
continue
|
||||
// Why: closing the last browser row empties a browser-only snapshot; rebuild it now
|
||||
// rather than serve that empty answer once.
|
||||
if (this.readRebuildableMobileSessionSnapshot(entryWorktreeId)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
const existing = this.readRebuildableMobileSessionSnapshot(entryWorktreeId)
|
||||
// Why: windowless, nothing publishes the other tabs, and a filtered seed would make the full pass skip this worktree.
|
||||
const runtimeOwnedOnly =
|
||||
options.onlyRuntimeOwnedTerminals === true &&
|
||||
|
||||
Reference in New Issue
Block a user