Files
orca/tests/e2e/workspace-layout-oracle.spec.ts
T
OrcaWinandOrcaWin 7d6603aa17 [comment audit] Complete repository-wide comment cleanup (#33249)
* [comment audit] Align legacy failure-read counts with cached display

* [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci]

* [comment audit] Condense shared generation, path and preview comments [skip ci]

* [comment audit] Condense host, file and Git contract rationale [skip ci]

* [comment audit] Tighten Git lifecycle and provider contract comments [skip ci]

* [comment audit] Clarify shared host authority and launch isolation [skip ci]

* [comment audit] simplify local access mobile and native chat contracts [skip ci]

* [comment audit] simplify native chat presentation and state contracts [skip ci]

* [comment audit] simplify native runtime and provider contracts [skip ci]

* [comment audit] simplify migration and orchestration contracts [skip ci]

* [comment audit] simplify plugin trust and process capture rationale [skip ci]

* [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci]

* [comment audit] clarify remote recovery and launch wire rationale [skip ci]

* [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci]

* [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci]

* [comment audit] clarify sidebar, skill and SSH invariants [skip ci]

* [comment audit] condense structured chat delivery and timing rationale [skip ci]

* [comment audit] condense terminal replay and telemetry invariants [skip ci]

* [comment audit] clarify launch permission and terminal delivery rationale [skip ci]

* [comment audit] clarify workspace ownership and layout contracts [skip ci]

* [comment audit] wait for fitted and painted SSH terminal links

* [comment audit] clarify layout persistence and session ownership [skip ci]

* [comment audit] clarify host routing and workspace lifecycle contracts [skip ci]

* [comment audit] clarify startup persistence and app lifetime comments [skip ci]

* [comment audit] clarify recovery and activity boundaries [skip ci]

* [comment audit] condense activity and artifact invariants [skip ci]

* [comment audit] condense automation authority contracts [skip ci]

* [comment audit] simplify automation scheduling and history contracts [skip ci]

* [comment audit] clarify browser chrome and annotation lifecycles [skip ci]

* [comment audit] clarify browser routing and retained guest ownership [skip ci]

* [comment audit] clarify browser recovery and document ownership comments [skip ci]

* [comment audit] restore hosted runners required by native IME fixtures

* [comment audit] shorten preview, palette and dashboard invariants [skip ci]

* [comment audit] clarify terminal preview, dashboard and retention comments [skip ci]

* [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci]

* [comment audit] shorten rich-text and combined-diff ownership comments [skip ci]

* [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci]

* [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci]

* [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci]

* [comment audit] clarify rich Markdown source and selection safeguards [skip ci]

* [comment audit] clarify editor loading and emulator stream lifetimes [skip ci]

* [comment audit] trim emulator and feature tour narration [skip ci]

* [comment audit] clarify floating workspace and GitHub cache ownership [skip ci]

* [comment audit] trim provider controls and Linear filter explanations [skip ci]

* [comment audit] clarify pairing and chat resume lifetimes [skip ci]

* [comment audit] clarify chat delivery and transcript lifetimes [skip ci]

* [comment audit] clarify session options and host-owned status [skip ci]

* [comment audit] clarify draft durability and live-status precedence [skip ci]

* [comment audit] Trim chat send and model ownership explanations [skip ci]

* [comment audit] Shorten transcript ownership and send recovery comments [skip ci]

* [comment audit] Simplify native chat composer and transcript hook comments [skip ci]

* [comment audit] Shorten native chat scroll and structured session explanations [skip ci]

* [comment audit] Trim workspace picker and onboarding commentary [skip ci]

* [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci]

* [comment audit] Trim sidebar and session history explanations [skip ci]

* [comment audit] Condense session resume and review checks comments [skip ci]

* [comment audit] Shorten checks state and file explorer refresh comments [skip ci]

* [comment audit] Trim plugin bridge and source-control action rationale [skip ci]

* [comment audit] Trim source-control listing and review rationale [skip ci]

* [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci]

* [comment audit] Condense Settings account and appearance rationale [skip ci]

* [comment audit] Trim pairing and repository Settings explanations [skip ci]

* [comment audit] Condense Settings search SSH and terminal safeguards [skip ci]

* [comment audit] Shorten Settings voice and provider ownership comments [skip ci]

* [comment audit] Condense Settings draft persistence and host-state rationale [skip ci]

* [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci]

* [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci]

* [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci]

* [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci]

* [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci]

* [comment audit] Prune focus-read baseline after comment cleanup

* [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci]

* [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci]

* [comment audit] Clarify viewport geometry and skill update safeguards [skip ci]

* [comment audit] Tighten skill inventory, usage and host status comments [skip ci]

* [comment audit] Clarify usage ownership and resource kill safeguards [skip ci]

* [comment audit] Tighten tab selection, rename and search contracts [skip ci]

* [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci]

* [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci]

* [comment audit] Tighten provider loading and terminal recovery comments [skip ci]

* [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci]

* [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci]

* [comment audit] Tighten PTY startup and replay explanations [skip ci]

* [comment audit] Clarify PTY identity and delivery ownership [skip ci]

* [comment audit] Shorten remote reconnect and input replay rationale [skip ci]

* [comment audit] Condense terminal parking clipboard and file link comments [skip ci]

* [comment audit] Clarify IME ownership and trim trace history [skip ci]

* [comment audit] Shorten pane recovery and parked watcher explanations [skip ci]

* [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci]

* [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci]

* [comment audit] Trim renderer lifecycle and menu rationale [skip ci]

* [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci]

* [comment audit] Clarify renderer hook evidence and source ownership [skip ci]

* [comment audit] Tighten IPC ownership and reconnect explanations [skip ci]

* [comment audit] Trim renderer focus, paste and attention comments [skip ci]

* [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci]

* [comment audit] Trim agent launch, resume and ownership commentary [skip ci]

* [comment audit] Shorten account, browser and editor comments [skip ci]

* [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci]

* [comment audit] Condense launch recovery and editor tokenization comments [skip ci]

* [comment audit] Condense workspace selection and terminal pane rationale [skip ci]

* [comment audit] Condense terminal fitting and rendering recovery comments [skip ci]

* [comment audit] Condense terminal input and host routing explanations [skip ci]

* [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci]

* [comment audit] Condense structured launch and terminal link explanations [skip ci]

* [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci]

* [comment audit] Condense session persistence and workspace activation rationale [skip ci]

* [comment audit] Condense workspace routing and browser ownership rationale [skip ci]

* [comment audit] Condense mirror recovery and layout authority rationale [skip ci]

* [comment audit] Condense runtime transport and file safety rationale [skip ci]

* [comment audit] Condense runtime graph and structured session rationale [skip ci]

* [comment audit] Condense browser creation and remote tab recovery rationale [skip ci]

* [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci]

* [comment audit] Condense frame, startup and provider cache rationale [skip ci]

* [comment audit] Condense host twin and agent lifecycle rationale [skip ci]

* [comment audit] Condense browser authority and editor cleanup rationale [skip ci]

* [comment audit] Condense editor buffer identity and review cache rationale [skip ci]

* [comment audit] Condense provider lifetime and repository ownership rationale [skip ci]

* [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci]

* [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci]

* [comment audit] Condense cleanup ownership and listing admission rationale [skip ci]

* [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci]

* [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci]

* [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci]

* [comment audit] Condense cloud rollout and assignment safety rationale [skip ci]

* [comment audit] Condense relay session and database recovery rationale [skip ci]

* [comment audit] Condense relay placement and protocol compatibility rationale [skip ci]

* [comment audit] Condense build and packaging constraints [skip ci]

* [comment audit] Condense CI gates and validation isolation rationale [skip ci]

* [comment audit] Condense catalog identity and browser validation rationale [skip ci]

* [comment audit] Condense mobile browser and frame-budget rationale [skip ci]

* [comment audit] Shorten mobile route and browser test explanations [skip ci]

* [comment audit] Condense editor and session seam test rationale [skip ci]

* [comment audit] Trim session and terminal closure history [skip ci]

* [comment audit] Shorten terminal browser and census explanations [skip ci]

* [comment audit] Preserve root-only E2E diagnostic baseline

* [comment audit] Condense mobile seam and HTML preview test rationale [skip ci]

* [comment audit] Condense preview instrumentation and render harness rationale [skip ci]

* [comment audit] Condense bundle contracts and native-build constraints [skip ci]

* [comment audit] Shorten native packaging and CI routing rationale [skip ci]

* [comment audit] Condense release tooling and benchmark constraints [skip ci]

* [comment audit] Condense packaging and platform gate rationale [skip ci]

* [comment audit] Shorten mobile route and history rationale [skip ci]

* [comment audit] Condense mobile browser and component rationale [skip ci]

* [comment audit] Shorten drawer and workspace-form rationale [skip ci]

* [comment audit] Trim mobile review and editor-document explanations [skip ci]

* [comment audit] Condense editor lifecycle and Markdown explanations [skip ci]

* [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci]

* [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci]

* [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci]

* [comment audit] Condense bridge stream, native request and audio rationale [skip ci]

* [comment audit] Reduce bridge schema and page-client commentary [skip ci]

* [comment audit] Tighten mobile page bridge contracts [skip ci]

* [comment audit] Clarify mobile cache and session constraints [skip ci]

* [comment audit] Condense mobile route and lifecycle rationale [skip ci]

* [comment audit] Tighten mobile shell hook timing rationale [skip ci]

* [comment audit] Clarify route handoff and notification compatibility [skip ci]

* [comment audit] Clarify push registration and dictation cleanup constraints [skip ci]

* [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci]

* [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci]

* [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci]

* [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci]

* [comment audit] Tighten stream replay and queued session ownership rationale [skip ci]

* [comment audit] Condense mobile composer and reply-schema constraints [skip ci]

* [comment audit] Clarify mobile transcript recovery and session ownership [skip ci]

* [comment audit] Clarify mobile queue and source-control compatibility [skip ci]

* [comment audit] Condense storage and task launch constraints [skip ci]

* [comment audit] Clarify task schemas and terminal document ownership [skip ci]

* [comment audit] Tighten terminal input and viewport rationale [skip ci]

* [comment audit] Shorten terminal and RPC recorder contracts [skip ci]

* [comment audit] Condense recorder engine and connection contracts [skip ci]

* [comment audit] Shorten host credential and relay recovery comments [skip ci]

* [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci]

* [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci]

* [comment audit] Condense catalog and relay entry contracts [skip ci]

* [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci]

* [comment audit] Condense WSL relay and E2E regression rationale [skip ci]

* [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci]

* [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci]

* [comment audit] Shorten layout oracle and regression explanations [skip ci]

* [comment audit] Condense paired recovery and ownership regression contracts [skip ci]

* [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci]

* [comment audit] Condense terminal rendering and IME regression rationale [skip ci]

* [comment audit] Shorten terminal recovery and benchmark contracts [skip ci]

* [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci]

* [comment audit] Condense workflow routing and release rationale [skip ci]

* [comment audit] Condense mobile security and installer contracts [skip ci]

* [comment audit] Shorten native rationale and stylesheet workarounds [skip ci]

* [comment audit] Shorten Windows visibility probe rationale [skip ci]

* [comment audit] Condense embedded release and signing rationale [skip ci]

* [comment audit] Retain current main test behavior in final reconciliation [skip ci]

* [comment audit] Complete final comment review across current source inventory

* [comment audit] Handle consolidated audit reports before final CI

* [comment audit] Preserve credential storage census detail [skip ci]

* [comment audit] Scale broad changed E2E selections with verified timing shards

* [comment audit] Align E2E assertions with browser close and settled session metadata

* [comment audit] Match existing E2Es to current resource and windowing contracts

* [comment audit] Observe the shared server outage notice in reconnect coverage

* [comment audit] Make existing renderer probes measure settled tab and PTY work

* [comment audit] Align palette E2E selector with task search

* [comment audit] Refresh remaining palette search selectors

* [comment audit] Keep selector correction within selected audit specs

* [comment audit] Await emoji fixture settings inside renderer evaluation

* [comment audit] Cancel fixture fit retries between terminal tests

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-25 04:05:46 -07:00

507 lines
20 KiB
TypeScript

// Drive user/CLI/paired-client layout journeys; compare structural rules, window DOM, client/CLI views, expected layout and restart preservation after every step.
// Fail findings absent from workspace-layout-oracle-known-on-main.ts. ORCA_LAYOUT_ORACLE_RECORD=1 records without failing; ORCA_LAYOUT_ORACLE_REPEAT=<n> repeats.
import { mkdtempSync, rmSync } from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import type { Page } from '@stablyai/playwright-test'
import { test, expect } from './helpers/orca-app'
import {
moveTerminalPaneByLeafId,
readTerminalPaneDomLeafOrder,
splitActiveTerminalPane,
waitForActiveTerminalManager
} from './helpers/terminal'
import { bootstrapFirstLaunch, seededRepoPathOrSkip } from './helpers/terminal-restart-persistence'
import { createTerminalTabFromMenu, SORTABLE_TAB } from './helpers/terminal-tab-menu'
import { ensureTerminalVisible, waitForSessionReady } from './helpers/store'
import {
addFolderWorkspace,
closeActivePaneFromKeyboard,
dragPaneOutToTabStrip,
waitForBoundPanes
} from './helpers/terminal-layout-journeys'
import {
addHostCreatedSplitWorktree,
openHostSplitWorktree,
sleepAgentPane,
wakeByClick
} from './helpers/host-split-and-sleep-journeys'
import { runOracleScenario, type OracleRun } from './helpers/workspace-layout-oracle-session'
import { RuntimeClient } from '../../src/cli/runtime/client'
import { unexpectedFindings } from './workspace-layout-oracle-known-on-main'
import type { RuntimeTerminalListResult } from '../../src/shared/runtime-types'
import type { RuntimeMobileSessionTabsResult } from '../../src/shared/runtime-session-contracts'
type Scenario = {
id: string
/** Defaults to the seeded repo's first worktree with one terminal. */
setup?: (run: OracleRun) => Promise<{ worktreeId: string; cleanup?: () => void }>
journey: (run: OracleRun, worktreeId: string) => Promise<void>
/** False where a pane runs an agent, not a shell, so an echo proves nothing. */
endMarkers?: false
}
async function listTerminals(run: OracleRun, worktreeId: string) {
const listed = await run.client.call<RuntimeTerminalListResult>('terminal.list', {
worktree: `id:${worktreeId}`
})
return listed.result.terminals
}
async function firstHandle(run: OracleRun, worktreeId: string): Promise<string> {
let handle: string | undefined
await expect
.poll(async () => {
handle = (await listTerminals(run, worktreeId).catch(() => []))[0]?.handle
return handle
})
.toBeDefined()
return handle!
}
/** The relaunched window shows the worktree again, however many panes its tab restores. */
async function reopenActiveWorktree(page: Page): Promise<void> {
await waitForSessionReady(page)
await ensureTerminalVisible(page)
await waitForActiveTerminalManager(page, 30_000)
}
/** A client paired to the window's runtime over its WebSocket offer, with its own state directory. */
// Both clients use the window's one runtime offer, as the headless corpus's two clients share one.
async function pairWindowClient(run: OracleRun, stateDir: string): Promise<RuntimeClient> {
const offer = await run.page.evaluate(() =>
window.api.mobile.getRuntimePairingUrl({ address: '127.0.0.1' })
)
if (!offer.available || !offer.pairingUrl) {
throw new Error('The window runtime did not offer a pairing')
}
return new RuntimeClient(stateDir, 30_000, offer.pairingUrl, null)
}
/** Each paired client's tab order must equal the CLI's; a difference is a `client` finding. */
async function compareClientTabOrders(
run: OracleRun,
step: string,
worktreeId: string,
clients: Record<string, RuntimeClient>
): Promise<void> {
const order = async (client: RuntimeClient) =>
JSON.stringify(
(
await client.call<RuntimeMobileSessionTabsResult>('session.tabs.list', {
worktree: `id:${worktreeId}`
})
).result.tabGroups?.map((group) => group.tabOrder) ?? []
)
const expected = await order(run.client)
for (const [name, client] of Object.entries(clients)) {
const seen = await order(client)
if (seen !== expected) {
run.oracle.findings.push({
check: 'client',
step,
details: [`${name} tab order ${seen} vs CLI ${expected}`]
})
}
}
}
async function splitTwice(page: Page): Promise<void> {
await splitActiveTerminalPane(page, 'vertical')
await waitForBoundPanes(page, 2)
await splitActiveTerminalPane(page, 'horizontal')
await waitForBoundPanes(page, 3)
}
async function newTabFromMenu(page: Page): Promise<void> {
// The tab menu can miss a click while the previous tab's menu is still closing.
await createTerminalTabFromMenu(page).catch(async () => {
await page.keyboard.press('Escape')
await createTerminalTabFromMenu(page)
})
await waitForActiveTerminalManager(page)
await waitForBoundPanes(page, 1)
}
const SCENARIOS: Scenario[] = [
{
id: 'create-tab',
journey: async (run, worktreeId) => {
await newTabFromMenu(run.page)
await run.oracle.step('new tab', { worktreeId, panesPerTab: [1, 1] })
}
},
{
id: 'split-right-down',
journey: async (run, worktreeId) => {
await splitTwice(run.page)
await run.oracle.step('split twice', { worktreeId, panesPerTab: [3] })
}
},
{
id: 'close-pane',
journey: async (run, worktreeId) => {
await splitTwice(run.page)
await run.oracle.step('split twice', { worktreeId, panesPerTab: [3] })
await closeActivePaneFromKeyboard(run.page, 3)
await run.oracle.step('close pane', { worktreeId, panesPerTab: [2], removed: 1 })
}
},
{
id: 'close-tab',
journey: async (run, worktreeId) => {
await newTabFromMenu(run.page)
await run.oracle.step('new tab', { worktreeId, panesPerTab: [1, 1] })
const firstTab = run.page.locator(SORTABLE_TAB).first()
await firstTab.getByRole('button', { name: /^Close tab /i }).click()
await expect(run.page.locator(SORTABLE_TAB)).toHaveCount(1)
await run.oracle.step('close first tab', { worktreeId, panesPerTab: [1], removed: 1 })
}
},
{
id: 'reorder-panes',
journey: async (run, worktreeId) => {
await splitActiveTerminalPane(run.page, 'vertical')
await waitForBoundPanes(run.page, 2)
const [firstLeaf, secondLeaf] = await readTerminalPaneDomLeafOrder(run.page)
await moveTerminalPaneByLeafId(run.page, secondLeaf!, firstLeaf!, 'left')
await expect
.poll(() => readTerminalPaneDomLeafOrder(run.page))
.toEqual([secondLeaf, firstLeaf])
await run.oracle.step('swap panes', { worktreeId, panesPerTab: [2] })
}
},
{
// A paired client reorders the tab bar through the runtime command.
id: 'client-reorder-tabs',
journey: async (run, worktreeId) => {
await newTabFromMenu(run.page)
await newTabFromMenu(run.page)
await run.oracle.step('three tabs', { worktreeId, panesPerTab: [1, 1, 1] })
const tabs = await run.client.call<RuntimeMobileSessionTabsResult>('session.tabs.list', {
worktree: `id:${worktreeId}`
})
const group = tabs.result.tabGroups?.[0]
expect(group?.tabOrder.length).toBe(3)
const reversed = group!.tabOrder.toReversed()
await run.client.call('session.tabs.move', {
worktree: `id:${worktreeId}`,
tabId: reversed[0],
targetGroupId: group!.id,
kind: 'reorder',
tabOrder: reversed
})
await run.oracle.step('client reverses tab order', { worktreeId, panesPerTab: [1, 1, 1] })
}
},
{
// STA-9259: the dragged-out pane must live in exactly one tab, across two restarts.
id: 'drag-out-to-tab',
journey: async (run, worktreeId) => {
await splitActiveTerminalPane(run.page, 'vertical')
await waitForBoundPanes(run.page, 2)
const [, secondLeaf] = await readTerminalPaneDomLeafOrder(run.page)
await dragPaneOutToTabStrip(run.page, worktreeId, secondLeaf!)
await expect(run.page.locator(SORTABLE_TAB)).toHaveCount(2)
await waitForBoundPanes(run.page, 1)
await run.oracle.step('drag pane out', { worktreeId, panesPerTab: [1, 1] })
await run.relaunch({ worktreeId, paneCount: 1, panesPerTab: [1, 1] })
await run.relaunch({ worktreeId, paneCount: 1, panesPerTab: [1, 1] })
}
},
{
id: 'cli-split',
journey: async (run, worktreeId) => {
const handle = await firstHandle(run, worktreeId)
await run.client.call('terminal.split', { terminal: handle, direction: 'horizontal' })
await waitForBoundPanes(run.page, 2)
await run.oracle.step('CLI split', { worktreeId, panesPerTab: [2] })
}
},
{
// #19582 and #10747 with a window: a CLI-created tab, renamed, then closed.
id: 'cli-create-rename-close',
journey: async (run, worktreeId) => {
await run.client.call('terminal.create', { worktree: `id:${worktreeId}` })
await expect.poll(async () => (await listTerminals(run, worktreeId)).length).toBe(2)
await run.oracle.step('CLI create', { worktreeId, panesPerTab: [1, 1] })
const created = (await listTerminals(run, worktreeId))[1]!
await run.client.call('terminal.rename', {
terminal: created.handle,
title: 'oracle-renamed'
})
await run.oracle.step('CLI rename', { worktreeId, panesPerTab: [1, 1] })
await run.client.call('terminal.close', { terminal: created.handle })
await run.oracle.step('CLI close', {
worktreeId,
panesPerTab: [1],
removed: [created.leafId]
})
}
},
{
// The paired-client tab commands: create a terminal tab, split it into a new group, close it.
id: 'client-create-split-close',
journey: async (run, worktreeId) => {
const worktree = `id:${worktreeId}`
const created = await run.client.call<{ tab: { parentTabId: string } }>(
'session.tabs.createTerminal',
{ worktree }
)
const tabId = created.result.tab.parentTabId
await run.oracle.step('client creates tab', { worktreeId, panesPerTab: [1, 1] })
const tabs = await run.client.call<RuntimeMobileSessionTabsResult>('session.tabs.list', {
worktree
})
await run.client.call('session.tabs.move', {
worktree,
tabId,
targetGroupId: tabs.result.tabGroups![0]!.id,
kind: 'split',
splitDirection: 'right'
})
await run.oracle.step('client moves tab to a new group', { worktreeId, panesPerTab: [1, 1] })
await run.client.call('session.tabs.close', { worktree, tabId, reason: 'user' })
await run.oracle.step('client closes tab', { worktreeId, panesPerTab: [1], removed: 1 })
}
},
{
// #9450 with a window attached: paired client A creates, paired client B closes.
id: 'two-clients-create-close',
journey: async (run, worktreeId) => {
const stateDir = mkdtempSync(path.join(os.tmpdir(), 'layout-oracle-clients-'))
try {
const clients = {
'client A': await pairWindowClient(run, path.join(stateDir, 'a')),
'client B': await pairWindowClient(run, path.join(stateDir, 'b'))
}
const worktree = `id:${worktreeId}`
const create = async () =>
(
await clients['client A'].call<{ tab: { parentTabId: string } }>(
'session.tabs.createTerminal',
{ worktree }
)
).result.tab.parentTabId
const first = await create()
await create()
await run.oracle.step('A creates two tabs', { worktreeId, panesPerTab: [1, 1, 1] })
await compareClientTabOrders(run, 'A creates two tabs', worktreeId, clients)
await clients['client B'].call('session.tabs.close', {
worktree,
tabId: first,
reason: 'user'
})
await run.oracle.step('B closes one', { worktreeId, panesPerTab: [1, 1], removed: 1 })
await compareClientTabOrders(run, 'B closes one', worktreeId, clients)
} finally {
rmSync(stateDir, { recursive: true, force: true })
}
}
},
{
// The CLI and a phone move the host window ('host' navigation); the layout must not change.
id: 'host-navigation',
// It ends on an editor tab, so its markers are checked while a terminal is shown.
endMarkers: false,
journey: async (run, worktreeId) => {
await newTabFromMenu(run.page)
await run.oracle.step('new tab', { worktreeId, panesPerTab: [1, 1] })
const first = (await listTerminals(run, worktreeId))[0]!
await run.client.call('terminal.focus', { terminal: first.handle, navigation: 'host' })
await expect(
run.page.locator(`${SORTABLE_TAB}[data-tab-id="${first.tabId}"][data-active="true"]`)
).toHaveCount(1)
await run.oracle.step('CLI focuses the first tab', { worktreeId, panesPerTab: [1, 1] })
await run.oracle.checkMarkers('first tab shown', worktreeId)
await run.client.call('files.open', {
worktree: `id:${worktreeId}`,
relativePath: 'README.md',
navigation: 'host'
})
await expect(
run.page.locator('[data-tab-group-strip-id]').getByText('README.md').first()
).toBeVisible()
await run.oracle.step('CLI opens a file', { worktreeId, panesPerTab: [1, 1] })
}
},
{
// A reloaded window republishes its graph, which is what the phone's tab list is built from.
id: 'window-reload',
journey: async (run, worktreeId) => {
await splitActiveTerminalPane(run.page, 'vertical')
await waitForBoundPanes(run.page, 2)
await run.oracle.step('split', { worktreeId, panesPerTab: [2] })
await run.reloadWindow()
await waitForActiveTerminalManager(run.page)
await waitForBoundPanes(run.page, 2).catch(() => {})
await run.oracle.step(
'after window reload',
{ worktreeId, panesPerTab: [2] },
{ allowRemount: true }
)
}
},
{
// Quit straight after a gesture: only the quit-time save can carry it.
id: 'quit-right-after-change',
journey: async (run, worktreeId) => {
await splitActiveTerminalPane(run.page, 'vertical')
await run.relaunch({
worktreeId,
paneCount: 2,
panesPerTab: [2],
quitWithoutSettling: true,
reopen: reopenActiveWorktree
})
}
},
{
// An older phone build sends a whole pane tree; a stale one must not drop a live pane.
id: 'old-client-pane-tree',
journey: async (run, worktreeId) => {
const handle = await firstHandle(run, worktreeId)
await run.client.call('terminal.split', { terminal: handle, direction: 'vertical' })
await waitForBoundPanes(run.page, 2)
await run.oracle.step('split', { worktreeId, panesPerTab: [2] })
const [first] = await listTerminals(run, worktreeId)
await run.client
.call('session.tabs.updatePaneLayout', {
worktree: `id:${worktreeId}`,
tabId: first!.tabId,
root: { type: 'leaf', leafId: first!.leafId }
})
.catch(() => {})
await run.oracle.step('stale pane tree from an old client', { worktreeId, panesPerTab: [2] })
}
},
{
id: 'restart-restore',
journey: async (run, worktreeId) => {
await splitActiveTerminalPane(run.page, 'vertical')
await waitForBoundPanes(run.page, 2)
await newTabFromMenu(run.page)
await run.oracle.step('split and new tab', { worktreeId, panesPerTab: [2, 1] })
await run.relaunch({ worktreeId, paneCount: 1, panesPerTab: [2, 1] })
await run.oracle.checkMarkers('after relaunch', worktreeId)
}
},
{
id: 'cold-daemon-restart',
journey: async (run, worktreeId) => {
await splitActiveTerminalPane(run.page, 'vertical')
await waitForBoundPanes(run.page, 2)
await newTabFromMenu(run.page)
await run.oracle.step('split and new tab', { worktreeId, panesPerTab: [2, 1] })
await run.relaunch({ worktreeId, paneCount: 1, coldDaemon: true, panesPerTab: [2, 1] })
await run.oracle.checkMarkers('after cold relaunch', worktreeId)
}
},
{
// The in-app "restart terminal daemon" action, with the window open.
id: 'live-daemon-restart',
journey: async (run, worktreeId) => {
await splitActiveTerminalPane(run.page, 'vertical')
await waitForBoundPanes(run.page, 2)
const before = await run.oracle.step('split', { worktreeId, panesPerTab: [2] })
run.oracle.rememberForRestart(before)
await run.page.evaluate(() => window.api.pty.management.restart())
await waitForBoundPanes(run.page, 2).catch(() => {})
const after = await run.oracle.step(
'daemon restarted',
{ worktreeId, panesPerTab: [2], terminalsRestart: true },
{ allowRemount: true }
)
run.oracle.compareRestart('daemon restart', after, { maskPtyIds: true })
await run.oracle.checkMarkers('after daemon restart', worktreeId)
// What the user gets back after the next relaunch is the layout that survived.
await run.relaunch({ worktreeId, paneCount: 2, panesPerTab: [2] })
}
},
{
id: 'folder-workspace',
setup: async (run) => addFolderWorkspace(run.page),
journey: async (run, worktreeId) => {
await splitActiveTerminalPane(run.page, 'vertical')
await waitForBoundPanes(run.page, 2)
await newTabFromMenu(run.page)
await run.oracle.step('split and new tab', { worktreeId, panesPerTab: [2, 1] })
}
},
{
// A host creates and manually splits a terminal, then the user returns to its saved layout.
id: 'host-split-reactivation',
// Its own worktree's panes are checked in the journey; the seeded one is no longer shown.
endMarkers: false,
setup: async (run) => addHostCreatedSplitWorktree(run.page, run.userDataDir),
journey: async (run) => {
const setupWorktreeId = await openHostSplitWorktree(run.page, run.userDataDir)
run.worktreeIds.push(setupWorktreeId)
await run.oracle.step('open setup worktree', {
worktreeId: setupWorktreeId,
panesPerTab: [2]
})
await run.oracle.checkMarkers('setup worktree', setupWorktreeId)
}
},
{
id: 'sleep-quit-resume',
endMarkers: false,
journey: async (run, worktreeId) => {
await sleepAgentPane(run.page, worktreeId)
await run.oracle.step('sleep agent pane', { worktreeId, panesPerTab: [2] })
await run.relaunch({
worktreeId,
paneCount: 2,
panesPerTab: [2],
terminalsRestart: true,
reopen: wakeByClick
})
}
}
]
const REPEAT = Math.max(1, Number(process.env.ORCA_LAYOUT_ORACLE_REPEAT ?? 1))
const RECORD_ONLY = process.env.ORCA_LAYOUT_ORACLE_RECORD === '1'
for (const scenario of SCENARIOS) {
for (let attempt = 1; attempt <= REPEAT; attempt += 1) {
const suffix = REPEAT > 1 ? ` #${attempt}` : ''
// oxlint-disable-next-line no-empty-pattern -- Each scenario owns its launches.
test(`layout oracle: ${scenario.id}${suffix}`, async ({}, testInfo) => {
const repoPath = seededRepoPathOrSkip()
const findings = await runOracleScenario(testInfo, scenario.id, async (run) => {
let cleanup: (() => void) | undefined
try {
let worktreeId: string
if (scenario.setup) {
const setup = await scenario.setup(run)
worktreeId = setup.worktreeId
cleanup = setup.cleanup
} else {
worktreeId = (await bootstrapFirstLaunch(run.page, repoPath)).worktreeId
}
run.worktreeIds.push(worktreeId)
await waitForBoundPanes(run.page, 1)
await run.oracle.step('start', { worktreeId, panesPerTab: [1] })
await scenario.journey(run, worktreeId)
if (scenario.endMarkers !== false) {
await run.oracle.checkMarkers('end', worktreeId)
}
} finally {
cleanup?.()
}
})
for (const finding of findings) {
console.log(
`[layout-oracle] ${scenario.id}: ${finding.check} @ ${finding.step}\n ${finding.details.join('\n ')}`
)
}
if (!RECORD_ONLY) {
expect(unexpectedFindings(scenario.id, findings)).toEqual([])
}
})
}
}