mirror of
https://github.com/stablyai/orca.git
synced 2026-11-05 11:37:25 +00:00
* 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
271 lines
9.6 KiB
TypeScript
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])
|
|
})
|
|
})
|