Files
orca/src/main/runtime/structured-agent-session-windowless-layout-host.test.ts
T
Jinwoo Hong d3b17ca9f4 fix(runtime): save a windowless host's chat tabs in its layout (#28354)
* fix(runtime): save a windowless host's chat tabs in its layout

Claude-Session: p2-headless

* fix(runtime): remove a chat's saved tab on the index's hidden notice, which every close reaches

Claude-Session: p2-headless

* fix(runtime): a chat whose record is gone loses its saved tab at startup

Claude-Session: p2-headless

* test: run the windowless chat-layout host test in the Node runtime project

Claude-Session: p2-headless

* fix(runtime): remove a chat's saved tab from whichever partition still holds it

Claude-Session: p2-headless

* refactor(runtime): list saved chat tabs across partitions in the layout module

Claude-Session: p2-headless

* refactor(runtime): read saved chat tabs straight from the store's partitions

Claude-Session: p2-headless

* fix(chat-tabs): save a windowless chat tab under its own tab id

Review pass: the saved tab reuses the id status and attention address, the close
checks for a window before scanning partitions, and the host tests seed readable
records with unreadable, other-host and no-index cases.

Claude-Session: p2-headless

* test(chat-tabs): a window opening a windowless-saved chat reuses its tab

Claude-Session: p2-headless

* test(chat-tabs): check the saved tab with the lookup a window reuses

The window's existing-tab lookup moves to the shared projection so the
windowless-layout test exercises it without importing the renderer.

Claude-Session: p2-headless

* test(chat-tabs): assert the reused tab's identity directly

Drops the shared lookup added for the test; two functions already carry that name.

Claude-Session: p2-headless

* fix(chat-tabs): leave an unstamped chat tab in an SSH partition to that host

Claude-Session: p2-headless

* fix(chat-tabs): count an unstamped chat tab as this host's only in the local partition

A desktop mirrors a remote server's chats into that server's runtime partition
without a host stamp; the startup sweep must not delete them.

Claude-Session: p2-headless

* refactor(chat-tabs): one chat tab writer, one startup match, local partition only

Chats run only on local hosts, so their saved tabs live in the local partition:
the cross-partition listing and its writer go. Open and close share one layout
load, the startup fill and sweep become one function beside the index seed, and
the tab filter is the existing savedSessionId.

Claude-Session: p2-headless

* fix(chat-tabs): restore and sweep saved Claude chat tabs like any structured chat

The Claude skip predates Claude structured chats (#18519, #18560); an id the host
has no record of is already dropped by the readable restorer.

Claude-Session: p2-headless
2026-10-14 04:15:36 -04:00

271 lines
9.6 KiB
TypeScript

// A windowless host's saved chat tab against a real chat host: the index hiding a chat reaches the
// runtime and takes the tab out of the layout, and a chat whose record is gone loses its tab.
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import {
agentSessionLeaseFixture,
agentSessionRecordFixture
} from '../../shared/agent-session-record.test-fixture'
import { structuredAgentSessionTabId } from '../../shared/structured-agent-session-projection'
import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types'
import { closeTestJournalHostDatabases } from '../native-chat/agent-session-journal/journal-host-database-test-support'
import { recordingStructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'
import { seedTestAgentSessionRecordStore } from './agent-session-record-store-test-harness'
import { OrcaRuntimeService } from './orca-runtime'
import { withChatTabChanges } from './agent-session-tab-layout'
import {
ensureStructuredAgentSessionHost,
stopStructuredAgentSessionRuntime
} from './structured-agent-session-runtime'
const WT = 'workspace-1'
// Session ids are 8 to 128 characters; a shorter one is not a readable record.
const CHAT_A = 'chat-a-0001'
const CHAT_B = 'chat-b-0002'
const CHAT_GONE = 'chat-gone-0003'
const CHAT_SSH = 'chat-ssh-0004'
const CHAT_REMOTE = 'chat-remote-0005'
const CHAT_CLAUDE = 'chat-claude-0006'
const EMPTY_SESSION: WorkspaceSessionState = {
activeRepoId: null,
activeWorktreeId: WT,
activeTabId: null,
tabsByWorktree: {},
terminalLayoutsByTabId: {}
}
let root: string
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), 'orca-windowless-chat-layout-'))
})
afterEach(async () => {
await stopStructuredAgentSessionRuntime().catch(() => undefined)
closeTestJournalHostDatabases()
await rm(root, { recursive: true, force: true })
})
function released(sessionId: string) {
return agentSessionRecordFixture(
agentSessionLeaseFixture({
sessionId,
ownerProcess: null,
reservedSpawnToken: null,
claimStatus: 'released'
})
)
}
/** A runtime with no window whose layout saves a tab for each chat. */
/** `partitions` are saved layouts besides the local one, by partition id. */
async function runtimeWithSavedChats(
sessionIds: string[],
partitions = new Map<string, WorkspaceSessionState>()
) {
const state: { session: WorkspaceSessionState } = {
session: {
activeRepoId: null,
activeWorktreeId: WT,
activeTabId: null,
tabsByWorktree: {},
terminalLayoutsByTabId: {}
}
}
const store = {
getRepos: () => [],
getSettings: () => ({}),
getWorkspaceSession: (hostId = 'local') =>
hostId === 'local' ? state.session : (partitions.get(hostId) ?? null),
getWorkspaceSessionHostIds: () => ['local', ...partitions.keys()],
setWorkspaceSession: (next: WorkspaceSessionState, hostId = 'local') => {
if (hostId === 'local') {
state.session = next
} else {
partitions.set(hostId, next)
}
}
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the runtime reads only these store methods on these paths.
const runtime = new OrcaRuntimeService(store as never)
for (const sessionId of sessionIds) {
await runtime.publishStructuredAgentSessionTab({
workspaceId: WT,
sessionId,
agent: 'codex',
activate: false
})
}
return { runtime, state }
}
function installHost(runtime: OrcaRuntimeService) {
return ensureStructuredAgentSessionHost({
logger: recordingStructuredAgentSessionLogger().logger,
stateDirectory: root,
hostId: 'local',
claimKeyId: 'key-1',
resolveWorkspacePath: async () => root,
resolveEnvironment: async () => ({}),
resolveLaunchArgs: () => [],
resolveClaudeAuthPolicy: () => ({ account: 'managed' }),
onSessionTabHidden: (sessionId) => runtime.onStructuredSessionTabHidden(sessionId)
})
}
/** A runtime whose startup restore installs a host rooted at `root`, with no PTY daemon. */
function restoringRuntime(runtime: OrcaRuntimeService): void {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these are the runtime's own protected members; the test roots the host at `root` and stubs the PTY daemon.
const internal = runtime as unknown as {
hasPersistedStructuredAgentSessionStore(): boolean
ensureStructuredAgentSessionHost(): Promise<unknown>
refreshMobileSessionPtyRecords(): Promise<Set<string> | null>
}
internal.hasPersistedStructuredAgentSessionStore = () => true
internal.ensureStructuredAgentSessionHost = () => installHost(runtime)
internal.refreshMobileSessionPtyRecords = async () => new Set<string>()
}
function shown(...sessionIds: string[]) {
return sessionIds.map((sessionId) => ({
tabId: structuredAgentSessionTabId(sessionId),
sessionId
}))
}
function savedChatIds(state: { session: WorkspaceSessionState }): string[] {
return (state.session.unifiedTabs?.[WT] ?? [])
.filter((tab) => tab.contentType === 'agent-session')
.map((tab) => tab.entityId)
}
describe('a windowless chat tab against the chat host', () => {
it('leaves the layout when the host hides the chat', async () => {
await seedTestAgentSessionRecordStore(root, {
records: [released(CHAT_A), released(CHAT_B)],
sessionTabs: shown(CHAT_A, CHAT_B)
})
const { runtime, state } = await runtimeWithSavedChats([CHAT_A, CHAT_B])
expect(savedChatIds(state)).toEqual([CHAT_A, CHAT_B])
const host = await installHost(runtime)
expect(host.deps.store.getRecord(CHAT_A)).not.toBeNull()
await host.setSessionTabVisibility(CHAT_A, false)
expect(savedChatIds(state)).toEqual([CHAT_B])
})
it('leaves the layout at startup when the chat record is gone, and keeps a listed one', async () => {
await seedTestAgentSessionRecordStore(root, {
records: [released(CHAT_B)],
sessionTabs: shown(CHAT_B)
})
const { runtime, state } = await runtimeWithSavedChats([CHAT_GONE, CHAT_B])
expect(savedChatIds(state)).toEqual([CHAT_GONE, CHAT_B])
restoringRuntime(runtime)
await runtime.restoreStructuredAgentSessionTabs()
expect(savedChatIds(state)).toEqual([CHAT_B])
})
it("removes a closed Claude chat's saved tab at startup", async () => {
await seedTestAgentSessionRecordStore(root, {
records: [released(CHAT_B)],
sessionTabs: shown(CHAT_B)
})
const { runtime, state } = await runtimeWithSavedChats([CHAT_B])
state.session = withChatTabChanges('local', state.session, WT, [
{
open: {
sessionId: CHAT_CLAUDE,
agent: 'claude',
tabId: structuredAgentSessionTabId(CHAT_CLAUDE)
}
}
])!
expect(savedChatIds(state)).toEqual([CHAT_B, CHAT_CLAUDE])
restoringRuntime(runtime)
await runtime.restoreStructuredAgentSessionTabs()
expect(savedChatIds(state)).toEqual([CHAT_B])
})
it('keeps the tab of a chat whose record this build cannot read', async () => {
// An id the current record shape rejects stands in for a record a newer build wrote.
await seedTestAgentSessionRecordStore(root, {
records: [released(CHAT_B), released('short')],
sessionTabs: shown(CHAT_B)
})
const { runtime, state } = await runtimeWithSavedChats(['short', CHAT_B])
restoringRuntime(runtime)
await runtime.restoreStructuredAgentSessionTabs()
expect(savedChatIds(state)).toEqual(['short', CHAT_B])
})
it("leaves another execution host's chat tab alone at startup", async () => {
await seedTestAgentSessionRecordStore(root, {
records: [released(CHAT_B)],
sessionTabs: shown(CHAT_B)
})
const { runtime, state } = await runtimeWithSavedChats([CHAT_B])
state.session = withChatTabChanges('ssh:box', state.session, WT, [
{
open: {
sessionId: CHAT_SSH,
agent: 'codex',
tabId: structuredAgentSessionTabId(CHAT_SSH)
}
}
])!
restoringRuntime(runtime)
await runtime.restoreStructuredAgentSessionTabs()
expect(savedChatIds(state)).toEqual([CHAT_B, CHAT_SSH])
})
it("leaves a remote server's mirrored chat tab alone at startup", async () => {
await seedTestAgentSessionRecordStore(root, {
records: [released(CHAT_B)],
sessionTabs: shown(CHAT_B)
})
// A desktop mirrors a remote server's chat into that server's partition with no host stamp.
const mirrored = withChatTabChanges('local', EMPTY_SESSION, WT, [
{
open: {
sessionId: CHAT_REMOTE,
agent: 'codex',
tabId: structuredAgentSessionTabId(CHAT_REMOTE)
}
}
])!
const unifiedTabs = {
[WT]: (mirrored.unifiedTabs?.[WT] ?? []).map(({ executionHostId: _host, ...tab }) => tab)
}
const partitions = new Map([['runtime:env-y', { ...mirrored, unifiedTabs }]])
const { runtime } = await runtimeWithSavedChats([CHAT_B], partitions)
restoringRuntime(runtime)
await runtime.restoreStructuredAgentSessionTabs()
expect(partitions.get('runtime:env-y')?.unifiedTabs?.[WT]?.map((tab) => tab.entityId)).toEqual([
CHAT_REMOTE
])
})
it('prunes nothing at startup when the store has no tab index', async () => {
await seedTestAgentSessionRecordStore(root, { records: [released(CHAT_B)] })
const { runtime, state } = await runtimeWithSavedChats([CHAT_GONE, CHAT_B])
restoringRuntime(runtime)
await runtime.restoreStructuredAgentSessionTabs()
expect(savedChatIds(state)).toEqual([CHAT_GONE, CHAT_B])
})
})