Files
orca/mobile/scripts/mock-server-session-tabs-fixture.ts
hanjoonchoeandBrennan Benson f4e46383df feat(mobile): add session.tabs.list handler to mock server (#9293)
* feat(mobile): add session.tabs.list handler to mock server

The mock WebSocket server had no handler for session.tabs.list, so the
session screen of a paired dev client hung on 'Loading tabs' forever —
the terminal pane, live input, and command input could never be
exercised against the mock. Respond with a single ready terminal tab
wired to the existing term-1 fixture so the whole session surface works
offline.

* fix(mobile): complete the session.tabs.list mock contract

The new mock response omitted four non-optional fields of
RuntimeMobileSessionTabsResult: publicationEpoch and activeGroupId on the
result, and parentTabId and leafId on the terminal tab. Nothing caught it —
the object literal had no type annotation, and MobileSessionTabsStreamHealth
is generic over both result and tab. A shape-incomplete mock yields
untrustworthy repros for exactly the bugs it gets used for (session tabs,
split panes, pane-to-tab attribution).

Fill the fields with host-realistic values: a per-process publisher epoch, a
layout UUID leaf id, and the `${parentTabId}::${leafId}` surface id
mobileTerminalSurfaceId actually emits. Pin the shape with an explicit return
type so a future required field fails typecheck instead of silently drifting.

Move the fixture into its own module: inlining it pushed
mock-server-rpc-handlers.ts to 317 lines against a 300-line max-lines cap,
which broke `pnpm lint` on the parent commit. It registers through the file's
existing delegation chain, after the native-chat scenario so MOCK_NATIVE_CHAT=1
keeps ownership of the method.

Co-authored-by: Hanjoon Choe <hanjoonchoe@gmail.com>

* test(mobile): pin session tabs mock fidelity

Normalize the selector-backed worktree ID like the real runtime and cover the complete terminal surface response so future contract drift fails the mobile suite.

* fix(mobile): share terminal.list worktree resolution with session tabs

Main added `terminalListWorktreeId`, which the rebased session-tabs fixture
duplicated with a different no-selector fallback — `terminal.list` resolved to
the active fake worktree while `session.tabs.list` returned a literal 'mock',
so a session repro saw two different worktree ids for one screen.

* test(mobile): cover the bare session-tabs worktree selector

Answers the review note that only the `id:`-prefixed path was exercised.

* fix(mobile): make the mock publication epoch unique per process

Date.now() can repeat across a sub-millisecond restart, so the epoch did not
actually guarantee the fresh-publisher identity its comment claimed.

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-29 18:01:38 -07:00

70 lines
2.6 KiB
TypeScript

import { randomUUID } from 'node:crypto'
import type { RuntimeMobileSessionTabsResult } from '../../src/shared/runtime-types'
import type { RpcRequest, RpcResponse } from './mock-server-rpc-handlers'
// Why: the client's snapshot-acceptance gate keys on the publisher epoch, so it
// must stay stable for the process and change on restart like a real publisher —
// hence a uuid, not a clock read two restarts could land on.
// The `mobile-local:` prefix is reserved for phone-local writes — never use it.
const PUBLICATION_EPOCH = `mock-server:${randomUUID()}`
const GROUP_ID = 'group-1'
const PARENT_TAB_ID = 'tab-1'
// The host only ever publishes terminal-layout UUIDs here; pane-key parsing
// rejects any other shape, so a placeholder would mask pane-attribution bugs.
const LEAF_ID = 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
// The host publishes terminal surfaces as `${parentTabId}::${leafId}`.
const SURFACE_TAB_ID = `${PARENT_TAB_ID}::${LEAF_ID}`
/** One ready terminal tab bound to the `term-1` fixture. Mirrors the full
* `session.tabs.list` contract so mock-server repros of tab, split-pane, and
* pane-attribution bugs aren't shape-incomplete. */
function createMockSessionTabs(worktreeId: string): RuntimeMobileSessionTabsResult {
return {
worktree: worktreeId,
publicationEpoch: PUBLICATION_EPOCH,
snapshotVersion: 1,
activeGroupId: GROUP_ID,
activeTabId: SURFACE_TAB_ID,
activeTabType: 'terminal',
// Groups track top-level tabs, so they carry parentTabId, not surface ids.
tabGroups: [
{
id: GROUP_ID,
activeTabId: PARENT_TAB_ID,
tabOrder: [PARENT_TAB_ID],
recentTabIds: [PARENT_TAB_ID]
}
],
tabs: [
{
type: 'terminal',
id: SURFACE_TAB_ID,
title: 'zsh',
parentTabId: PARENT_TAB_ID,
leafId: LEAF_ID,
status: 'ready',
terminal: 'term-1',
isActive: true
}
]
}
}
/** Default session-tabs backend: without it the session screen hangs on
* 'Loading tabs'. Returns false for methods it does not own. */
export function handleMockSessionTabsRequest(
request: RpcRequest,
respond: (response: RpcResponse) => void,
success: (id: string, result: unknown, streaming?: boolean) => RpcResponse,
// Shared with `terminal.list` so both surfaces agree on which worktree an
// absent or `id:`-prefixed selector means.
resolveWorktreeId: (selector: unknown) => string | undefined
): boolean {
if (request.method !== 'session.tabs.list') {
return false
}
const worktreeId = resolveWorktreeId(request.params?.worktree) ?? 'mock'
respond(success(request.id, createMockSessionTabs(worktreeId)))
return true
}