mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 16:03:20 +00:00
* 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>
70 lines
2.6 KiB
TypeScript
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
|
|
}
|