mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
fix: bind structured TUI before rollout proof
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { StructuredAgentSessionHandoffTransport } from '../native-chat/agent-session-wire/structured-agent-session-handoff-types'
|
||||
import { OrcaRuntimeService } from './orca-runtime'
|
||||
|
||||
const readStructuredTuiProcessIdentity = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('./structured-tui-process-identity', () => ({ readStructuredTuiProcessIdentity }))
|
||||
|
||||
const WORKTREE_ID = 'repo-1::/tmp/structured-handoff'
|
||||
|
||||
function notifier(revealTerminalSession: ReturnType<typeof vi.fn>) {
|
||||
return {
|
||||
worktreesChanged: vi.fn(),
|
||||
reposChanged: vi.fn(),
|
||||
activateWorktree: vi.fn(),
|
||||
createTerminal: vi.fn(),
|
||||
revealTerminalSession,
|
||||
splitTerminal: vi.fn(),
|
||||
renameTerminal: vi.fn(),
|
||||
focusTerminal: vi.fn(),
|
||||
closeTerminal: vi.fn(),
|
||||
sleepWorktree: vi.fn(),
|
||||
terminalFitOverrideChanged: vi.fn(),
|
||||
terminalDriverChanged: vi.fn()
|
||||
}
|
||||
}
|
||||
|
||||
describe('structured TUI launch tab binding', () => {
|
||||
it('proves the published launch tab before returning its revealed renderer binding', async () => {
|
||||
const revealTerminalSession = vi.fn(
|
||||
(_worktreeId: string, _options: { tabId?: string; leafId?: string; ptyId?: string }) =>
|
||||
Promise.resolve({ tabId: 'tab-renderer' })
|
||||
)
|
||||
const runtime = new OrcaRuntimeService({
|
||||
getSettings: () => ({
|
||||
disabledTuiAgents: [],
|
||||
agentCmdOverrides: {},
|
||||
agentDefaultArgs: {},
|
||||
agentDefaultEnv: {}
|
||||
})
|
||||
} as never)
|
||||
runtime.setNotifier(notifier(revealTerminalSession) as never)
|
||||
runtime.setPtyController({
|
||||
spawn: vi.fn().mockResolvedValue({ id: 'pty-structured', pid: 4242 }),
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null
|
||||
})
|
||||
|
||||
const internal = runtime as unknown as {
|
||||
createStructuredAgentSessionHandoffTransport(): StructuredAgentSessionHandoffTransport
|
||||
resolveTerminalWorkspaceLaunchScope(): Promise<{
|
||||
id: string
|
||||
path: string
|
||||
connectionId: null
|
||||
repo: null
|
||||
folderWorkspace: null
|
||||
}>
|
||||
markLocalWorkspaceTrustedForAgent(): void
|
||||
waitForTerminal(): Promise<unknown>
|
||||
waitForStructuredTuiProof(): Promise<{ transcriptPath?: string }>
|
||||
}
|
||||
internal.resolveTerminalWorkspaceLaunchScope = vi.fn(async () => ({
|
||||
id: WORKTREE_ID,
|
||||
path: '/tmp/structured-handoff',
|
||||
connectionId: null,
|
||||
repo: null,
|
||||
folderWorkspace: null
|
||||
}))
|
||||
internal.markLocalWorkspaceTrustedForAgent = vi.fn()
|
||||
const waitForTerminal = vi.fn(async () => ({}))
|
||||
internal.waitForTerminal = waitForTerminal
|
||||
const waitForStructuredTuiProof = vi.fn(async () => {
|
||||
const snapshot = await runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`)
|
||||
expect(snapshot.tabs).toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: 'terminal',
|
||||
parentTabId: expect.any(String),
|
||||
leafId: expect.any(String),
|
||||
ptyId: 'pty-structured',
|
||||
terminal: expect.any(String)
|
||||
})
|
||||
)
|
||||
expect(revealTerminalSession).not.toHaveBeenCalled()
|
||||
return { transcriptPath: '/tmp/rollout.jsonl' }
|
||||
})
|
||||
internal.waitForStructuredTuiProof = waitForStructuredTuiProof
|
||||
readStructuredTuiProcessIdentity.mockResolvedValue({
|
||||
hostId: 'local',
|
||||
pid: 4243,
|
||||
processStartTimeMs: 10,
|
||||
spawnToken: 'spawn-token'
|
||||
})
|
||||
|
||||
const transport = internal.createStructuredAgentSessionHandoffTransport()
|
||||
const owner = await transport.launchTui({
|
||||
record: {
|
||||
sessionId: 'session-1',
|
||||
location: { workspaceId: WORKTREE_ID, executionHostId: 'local' },
|
||||
accountHome: { variable: 'CODEX_HOME', path: '/tmp/codex-home' },
|
||||
providerHandleChain: [
|
||||
{ handle: { provider: 'codex', threadId: 'thread-1' }, observedAt: 1 }
|
||||
]
|
||||
} as never,
|
||||
fence: 3,
|
||||
spawnToken: 'spawn-token'
|
||||
})
|
||||
|
||||
const reveal = revealTerminalSession.mock.calls[0]?.[1] as {
|
||||
tabId: string
|
||||
leafId: string
|
||||
}
|
||||
expect(owner.terminal).toMatchObject({
|
||||
tabId: 'tab-renderer',
|
||||
paneKey: `${reveal.tabId}:${reveal.leafId}`
|
||||
})
|
||||
expect(waitForTerminal).toHaveBeenCalledWith(
|
||||
owner.terminal.handle,
|
||||
expect.objectContaining({ condition: 'tui-idle' })
|
||||
)
|
||||
expect(waitForStructuredTuiProof).toHaveBeenCalledOnce()
|
||||
expect(waitForStructuredTuiProof.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
revealTerminalSession.mock.invocationCallOrder[0]!
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -9123,6 +9123,10 @@ export class OrcaRuntimeService {
|
||||
if (!terminal.processId || !terminal.paneKey || !terminal.tabId) {
|
||||
throw new Error('The resumed terminal did not publish a process identity.')
|
||||
}
|
||||
await this.waitForTerminal(terminal.handle, {
|
||||
condition: 'tui-idle',
|
||||
timeoutMs: 30_000
|
||||
})
|
||||
const proof = await this.waitForStructuredTuiProof({
|
||||
handle: terminal.handle,
|
||||
paneKey: terminal.paneKey,
|
||||
@@ -9131,11 +9135,11 @@ export class OrcaRuntimeService {
|
||||
codexHome: record.accountHome.path,
|
||||
sessionId: record.sessionId
|
||||
})
|
||||
await this.focusTerminal(terminal.handle)
|
||||
const revealed = await this.focusTerminal(terminal.handle)
|
||||
return {
|
||||
terminal: {
|
||||
handle: terminal.handle,
|
||||
tabId: terminal.tabId,
|
||||
tabId: revealed.tabId,
|
||||
paneKey: terminal.paneKey
|
||||
},
|
||||
process: await readStructuredTuiProcessIdentity({
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-types'
|
||||
import type { Tab } from '../../../shared/types'
|
||||
import { applyWebSessionTabsSnapshot, type WebSessionTabsSyncState } from './web-session-tabs-sync'
|
||||
|
||||
const WORKTREE_ID = 'repo-1::/worktree'
|
||||
const GROUP_ID = 'group-1'
|
||||
|
||||
function structuredTab(sessionId: string, sortOrder: number): Tab {
|
||||
return {
|
||||
id: `structured-agent-session-${sessionId}`,
|
||||
entityId: sessionId,
|
||||
groupId: GROUP_ID,
|
||||
worktreeId: WORKTREE_ID,
|
||||
contentType: 'agent-session',
|
||||
agentSessionAgent: 'codex',
|
||||
label: 'Codex Chat',
|
||||
customLabel: null,
|
||||
color: null,
|
||||
sortOrder,
|
||||
createdAt: sortOrder + 1
|
||||
}
|
||||
}
|
||||
|
||||
describe('web session structured tab focus', () => {
|
||||
it('keeps the exact active structured tab across a host snapshot', () => {
|
||||
const first = structuredTab('session-1', 0)
|
||||
const second = structuredTab('session-2', 1)
|
||||
const state = {
|
||||
activeBrowserTabId: null,
|
||||
activeBrowserTabIdByWorktree: {},
|
||||
activeFileId: null,
|
||||
activeFileIdByWorktree: {},
|
||||
activeGroupIdByWorktree: { [WORKTREE_ID]: GROUP_ID },
|
||||
activeTabId: null,
|
||||
activeTabIdByWorktree: {},
|
||||
activeTabType: 'terminal',
|
||||
activeTabTypeByWorktree: { [WORKTREE_ID]: 'terminal' },
|
||||
activeWorktreeId: WORKTREE_ID,
|
||||
agentStatusByPaneKey: {},
|
||||
agentStatusEpoch: 0,
|
||||
browserCertificateFailuresByPageId: {},
|
||||
browserPagesByWorkspace: {},
|
||||
browserTabsByWorktree: {},
|
||||
groupsByWorktree: {
|
||||
[WORKTREE_ID]: [
|
||||
{
|
||||
id: GROUP_ID,
|
||||
worktreeId: WORKTREE_ID,
|
||||
activeTabId: second.id,
|
||||
tabOrder: [first.id, second.id]
|
||||
}
|
||||
]
|
||||
},
|
||||
layoutByWorktree: {},
|
||||
openFiles: [],
|
||||
ptyIdsByTabId: {},
|
||||
remoteBrowserPageHandlesByPageId: {},
|
||||
tabBarOrderByWorktree: { [WORKTREE_ID]: [first.id, second.id] },
|
||||
tabsByWorktree: {},
|
||||
terminalLayoutsByTabId: {},
|
||||
unifiedTabsByWorktree: { [WORKTREE_ID]: [first, second] },
|
||||
unreadTerminalTabs: {},
|
||||
sortEpoch: 0
|
||||
} as WebSessionTabsSyncState
|
||||
const snapshot: RuntimeMobileSessionTabsResult = {
|
||||
worktree: WORKTREE_ID,
|
||||
publicationEpoch: 'epoch-1',
|
||||
snapshotVersion: 2,
|
||||
activeGroupId: GROUP_ID,
|
||||
activeTabId: 'agent-session:session-1',
|
||||
activeTabType: 'agent-session',
|
||||
tabGroups: [
|
||||
{
|
||||
id: GROUP_ID,
|
||||
activeTabId: 'agent-session:session-1',
|
||||
tabOrder: ['agent-session:session-1', 'agent-session:session-2']
|
||||
}
|
||||
],
|
||||
tabs: [
|
||||
{
|
||||
type: 'agent-session',
|
||||
id: 'agent-session:session-1',
|
||||
title: 'Codex Chat',
|
||||
sessionId: 'session-1',
|
||||
agent: 'codex',
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
type: 'agent-session',
|
||||
id: 'agent-session:session-2',
|
||||
title: 'Codex Chat',
|
||||
sessionId: 'session-2',
|
||||
agent: 'codex',
|
||||
isActive: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const patch = applyWebSessionTabsSnapshot(state, snapshot, 'environment-1', 10)
|
||||
|
||||
expect(patch.groupsByWorktree?.[WORKTREE_ID]?.[0]?.activeTabId).toBe(second.id)
|
||||
})
|
||||
})
|
||||
@@ -2004,6 +2004,18 @@ function findCurrentVisibleUnifiedTabId(args: {
|
||||
if (!nextUnifiedTabs) {
|
||||
return null
|
||||
}
|
||||
const activeGroupId = state.activeGroupIdByWorktree[worktreeId]
|
||||
const activeGroupTabId = state.groupsByWorktree[worktreeId]?.find(
|
||||
(group) => group.id === activeGroupId
|
||||
)?.activeTabId
|
||||
if (
|
||||
activeGroupTabId &&
|
||||
nextUnifiedTabs.some(
|
||||
(tab) => tab.id === activeGroupTabId && tab.contentType === 'agent-session'
|
||||
)
|
||||
) {
|
||||
return activeGroupTabId
|
||||
}
|
||||
const currentVisibleType =
|
||||
state.activeTabTypeByWorktree[worktreeId] ??
|
||||
(state.activeWorktreeId === worktreeId ? state.activeTabType : null)
|
||||
|
||||
Reference in New Issue
Block a user