mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 19:17:28 +00:00
* [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>
507 lines
20 KiB
TypeScript
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([])
|
|
}
|
|
})
|
|
}
|
|
}
|