mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +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>
512 lines
18 KiB
JavaScript
512 lines
18 KiB
JavaScript
// Drive packaged production DOM/ARIA without E2E-only stores. Prove input through command-written filesystem sentinels rather than canvas scraping.
|
||
// Marker canary attributes console flashes; ~500ms heartbeat proves the session remains active.
|
||
|
||
import { _electron as electron } from '@stablyai/playwright-test'
|
||
import { execFileSync } from 'node:child_process'
|
||
import { mkdirSync, realpathSync, writeFileSync } from 'node:fs'
|
||
import path from 'node:path'
|
||
import { seedFreshProfile } from './onboarding-profile.mjs'
|
||
import { isPidAlive } from './daemon-processes.mjs'
|
||
|
||
const NEW_TAB_BUTTON = { role: 'button', name: 'New tab' }
|
||
const NEW_TERMINAL_ITEM = /New Terminal/i
|
||
const NEW_WORKSPACE_BUTTON = { role: 'button', name: 'New workspace' }
|
||
const SORTABLE_TAB = '[data-testid="sortable-tab"]'
|
||
// Target :visible because hidden duplicate panes must not receive focus or measurements.
|
||
const TERMINAL_SURFACE_VISIBLE = '[data-terminal-tab-id]:visible'
|
||
const XTERM_CONTAINER_VISIBLE = '.xterm:visible'
|
||
const XTERM_INPUT = '.xterm-helper-textarea'
|
||
const RESTRICTED_E2E_ENV_KEYS = new Set([
|
||
'HOME',
|
||
'USERPROFILE',
|
||
'CODEX_HOME',
|
||
'ORCA_CODEX_HOME',
|
||
'ORCA_E2E_HOME_DIR',
|
||
'ORCA_E2E_USER_DATA_DIR'
|
||
])
|
||
|
||
// Launch with harness-owned userData for unique daemon scope; optional seedProfile writes only before FIRST launch, preserving later restore evidence.
|
||
export async function launchInstalledApp({
|
||
exePath,
|
||
userDataDir,
|
||
seedProfile = null,
|
||
extraEnv = {}
|
||
}) {
|
||
const {
|
||
ELECTRON_RUN_AS_NODE: _drop,
|
||
CODEX_HOME: _codexHome,
|
||
ORCA_CODEX_HOME: _orcaCodexHome,
|
||
...cleanEnv
|
||
} = process.env
|
||
void _drop
|
||
void _codexHome
|
||
void _orcaCodexHome
|
||
const restrictedExtraEnvKey = Object.keys(extraEnv).find((key) =>
|
||
RESTRICTED_E2E_ENV_KEYS.has(key.toUpperCase())
|
||
)
|
||
if (restrictedExtraEnvKey) {
|
||
throw new Error(`extraEnv.${restrictedExtraEnvKey} cannot override E2E home isolation`)
|
||
}
|
||
mkdirSync(userDataDir, { recursive: true })
|
||
if (seedProfile) {
|
||
seedFreshProfile(userDataDir, seedProfile)
|
||
}
|
||
// Isolate Codex home separately: relocating userData does not change Node home.
|
||
const requestedIsolatedHome = path.join(userDataDir, 'home')
|
||
mkdirSync(requestedIsolatedHome, { recursive: true })
|
||
// Canonicalize Windows 8.3 home aliases so Git worktree paths match Orca’s comparisons.
|
||
const isolatedHome = realpathSync.native(requestedIsolatedHome)
|
||
const app = await electron.launch({
|
||
executablePath: exePath,
|
||
args: [],
|
||
env: {
|
||
...cleanEnv,
|
||
// Packaged main honors ORCA_E2E_USER_DATA_DIR to relocate userData
|
||
// (logs/daemon/terminal-history) under a controlled dir.
|
||
...extraEnv,
|
||
ORCA_E2E_USER_DATA_DIR: userDataDir,
|
||
// Why: the driven app stays off the foreground so a local run doesn't steal focus.
|
||
ORCA_BACKGROUND_LAUNCH: '1',
|
||
HOME: isolatedHome,
|
||
USERPROFILE: isolatedHome,
|
||
ORCA_E2E_HOME_DIR: isolatedHome
|
||
}
|
||
})
|
||
// Kill launch tree if firstWindow fails; Electron may still run after the driver times out.
|
||
let page
|
||
try {
|
||
page = await app.firstWindow({ timeout: 120_000 })
|
||
await page.waitForLoadState('domcontentloaded')
|
||
} catch (err) {
|
||
const pid = await resolveElectronMainPid(app)
|
||
if (pid) {
|
||
try {
|
||
execFileSync('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' })
|
||
} catch {
|
||
/* already gone */
|
||
}
|
||
}
|
||
throw err
|
||
}
|
||
return { app, page }
|
||
}
|
||
|
||
/** Resolve the packaged Electron main, optionally falling back to Playwright's child PID. */
|
||
export async function resolveElectronMainPid(
|
||
app,
|
||
{ allowLauncherFallback = true, timeoutMs = 5_000 } = {}
|
||
) {
|
||
let timeout
|
||
try {
|
||
// app.evaluate resolves authoritative main after packaged launcher re-exec; app.process may point at a dead stub.
|
||
const pid = await Promise.race([
|
||
app.evaluate(() => process.pid),
|
||
new Promise((_, reject) => {
|
||
// Bound main PID resolution because cleanup often has a wedged connection.
|
||
timeout = setTimeout(() => reject(new Error('main PID resolution timed out')), timeoutMs)
|
||
timeout.unref?.()
|
||
})
|
||
])
|
||
if (Number.isInteger(pid) && pid > 0) {
|
||
return pid
|
||
}
|
||
} catch {
|
||
/* the main connection may already be unavailable */
|
||
} finally {
|
||
clearTimeout(timeout)
|
||
}
|
||
// Crash proofs must reject unavailable main identity rather than kill a stub and falsely prove crash.
|
||
if (!allowLauncherFallback) {
|
||
return null
|
||
}
|
||
const fallbackPid = app.process()?.pid
|
||
return Number.isInteger(fallbackPid) && fallbackPid > 0 ? fallbackPid : null
|
||
}
|
||
|
||
// Best-effort screenshot/body/store-exposure diagnostics under dir; production omits stores and driver failures must retain original assertions.
|
||
export async function captureFailureDiagnostics(page, dir, label) {
|
||
const out = {}
|
||
try {
|
||
mkdirSync(dir, { recursive: true })
|
||
} catch {
|
||
return out
|
||
}
|
||
try {
|
||
await page.screenshot({
|
||
path: path.join(dir, `${label}.png`),
|
||
fullPage: false,
|
||
timeout: 10_000
|
||
})
|
||
out.screenshot = `${label}.png`
|
||
} catch {
|
||
/* renderer may be unresponsive */
|
||
}
|
||
try {
|
||
const info = await page.evaluate(() => ({
|
||
hasStore: typeof window.__store,
|
||
title: document.title,
|
||
url: location.href,
|
||
bodyText: (document.body?.innerText ?? '').slice(0, 4000),
|
||
testIds: Array.from(document.querySelectorAll('[data-testid]'))
|
||
.map((el) => el.getAttribute('data-testid'))
|
||
.filter((v, i, a) => v && a.indexOf(v) === i)
|
||
.slice(0, 80),
|
||
buttons: Array.from(document.querySelectorAll('button,[role="button"]'))
|
||
.map((el) => (el.getAttribute('aria-label') || el.textContent || '').trim())
|
||
.filter((v, i, a) => v && a.indexOf(v) === i)
|
||
.slice(0, 60),
|
||
tabs: Array.from(document.querySelectorAll('[data-testid="sortable-tab"]'))
|
||
.map((el) => ({
|
||
id: el.getAttribute('data-tab-id'),
|
||
title: el.getAttribute('data-tab-title'),
|
||
ariaLabel: el.getAttribute('aria-label'),
|
||
selected: el.getAttribute('aria-selected')
|
||
}))
|
||
.slice(0, 40),
|
||
activeElement: document.activeElement
|
||
? {
|
||
tag: document.activeElement.tagName,
|
||
className: document.activeElement.getAttribute('class'),
|
||
ariaLabel: document.activeElement.getAttribute('aria-label')
|
||
}
|
||
: null
|
||
}))
|
||
writeFileSync(path.join(dir, `${label}.json`), JSON.stringify(info, null, 2))
|
||
out.info = info
|
||
} catch {
|
||
/* renderer may be unresponsive */
|
||
}
|
||
return out
|
||
}
|
||
|
||
/** Wait until the visible terminal surface and its xterm container are mounted.
|
||
* An expected tab id prevents post-restore probes from accepting another tab. */
|
||
export async function waitForTerminalReady(page, timeoutMs = 60_000, terminalTabId = null) {
|
||
const selector = terminalTabId
|
||
? `[data-terminal-tab-id="${String(terminalTabId)}"]:visible`
|
||
: TERMINAL_SURFACE_VISIBLE
|
||
const surface = page.locator(selector).first()
|
||
await surface.waitFor({ state: 'visible', timeout: timeoutMs })
|
||
await surface.locator(XTERM_CONTAINER_VISIBLE).first().waitFor({
|
||
state: 'visible',
|
||
timeout: timeoutMs
|
||
})
|
||
}
|
||
|
||
// Create only on first launch when no terminal exists; post-update relaunch must wait for restore without masking failure with a new workspace.
|
||
export async function ensureTerminal(page, { allowCreate = true, timeoutMs = 60_000 } = {}) {
|
||
// Dismiss late feature tips without Escape so restored terminal receives no unsolicited keystroke.
|
||
await dismissKnownOverlays(page)
|
||
const visibleTerminal = page.locator(TERMINAL_SURFACE_VISIBLE).first()
|
||
if (await visibleTerminal.isVisible().catch(() => false)) {
|
||
await waitForTerminalReady(page, timeoutMs)
|
||
return
|
||
}
|
||
if (!allowCreate) {
|
||
// Restored-terminal timeout is restore failure, not an excuse to create.
|
||
await waitForTerminalReady(page, timeoutMs)
|
||
return
|
||
}
|
||
const newTab = page.getByRole(NEW_TAB_BUTTON.role, { name: NEW_TAB_BUTTON.name }).first()
|
||
if (await newTab.isVisible().catch(() => false)) {
|
||
await createTerminalTab(page)
|
||
return
|
||
}
|
||
await createWorkspaceFromSeededRepo(page, timeoutMs)
|
||
await waitForTerminalReady(page, timeoutMs)
|
||
}
|
||
|
||
// Use seeded-project composer’s Blank Terminal path so probes reach a shell rather than an agent/native folder dialog.
|
||
async function createWorkspaceFromSeededRepo(page, timeoutMs) {
|
||
// Share one deadline across creation rather than reset each step’s budget.
|
||
const deadline = Date.now() + timeoutMs
|
||
const newWorkspace = page
|
||
.getByRole(NEW_WORKSPACE_BUTTON.role, { name: NEW_WORKSPACE_BUTTON.name })
|
||
.first()
|
||
if (!(await tryClickWithKnownOverlayRetry(page, newWorkspace, deadline - Date.now()))) {
|
||
// Keep locator diagnostics without spending another full click timeout.
|
||
await newWorkspace.click({ timeout: 1 })
|
||
}
|
||
const composer = page.getByRole('dialog', { name: 'Create worktree' }).last()
|
||
await composer.waitFor({ state: 'visible', timeout: Math.max(1, deadline - Date.now()) })
|
||
// Match Create by prefix for appended shortcut hints; fall back to Ctrl+Enter.
|
||
const created = await tryClickWithKnownOverlayRetry(
|
||
page,
|
||
composer.getByRole('button', { name: /^Create worktree/ }).last(),
|
||
Math.max(0, deadline - Date.now())
|
||
)
|
||
if (!created) {
|
||
await page.keyboard.press('Control+Enter')
|
||
}
|
||
}
|
||
|
||
const OVERLAY_DISMISS_LABELS = ['Got it', 'Dismiss setup scripts', 'Dismiss tip', 'Dismiss update']
|
||
const FEATURE_TIP_DIALOG_TITLES = [
|
||
'Let agents drive Orca with the Orca CLI',
|
||
'Search every agent session'
|
||
]
|
||
|
||
async function dismissKnownOverlays(page) {
|
||
let acted = false
|
||
for (const name of FEATURE_TIP_DIALOG_TITLES) {
|
||
const featureTip = page.getByRole('dialog', { name }).first()
|
||
if (!(await featureTip.isVisible().catch(() => false))) {
|
||
continue
|
||
}
|
||
// Why: a global "Close" role also matches the Windows/Linux title-bar button.
|
||
const dialogClose = featureTip.locator('[data-slot="dialog-close"]').first()
|
||
if (await dialogClose.isVisible().catch(() => false)) {
|
||
const clicked = await dialogClose
|
||
.click({ timeout: 3_000 })
|
||
.then(() => true)
|
||
.catch(() => false)
|
||
acted ||= clicked
|
||
}
|
||
}
|
||
for (const name of OVERLAY_DISMISS_LABELS) {
|
||
const btn = page.getByRole('button', { name }).first()
|
||
if (await btn.isVisible().catch(() => false)) {
|
||
const clicked = await btn
|
||
.click({ timeout: 3_000 })
|
||
.then(() => true)
|
||
.catch(() => false)
|
||
acted ||= clicked
|
||
}
|
||
}
|
||
return acted
|
||
}
|
||
|
||
async function tryClickWithKnownOverlayRetry(page, locator, timeoutMs) {
|
||
const deadline = Date.now() + timeoutMs
|
||
do {
|
||
const remainingMs = deadline - Date.now()
|
||
if (remainingMs <= 0) {
|
||
return false
|
||
}
|
||
try {
|
||
await locator.click({ timeout: Math.min(5_000, remainingMs) })
|
||
return true
|
||
} catch (error) {
|
||
if (page.isClosed()) {
|
||
throw error
|
||
}
|
||
await dismissKnownOverlays(page)
|
||
}
|
||
} while (Date.now() < deadline)
|
||
return false
|
||
}
|
||
|
||
// Dismiss sequential feature tips/setup/update overlays best-effort so they cannot intercept terminal input.
|
||
export async function dismissOverlays(page, rounds = 3) {
|
||
for (let i = 0; i < rounds; i++) {
|
||
const acted = await dismissKnownOverlays(page)
|
||
await page.keyboard.press('Escape').catch(() => {})
|
||
if (!acted) {
|
||
return
|
||
}
|
||
await page.waitForTimeout(400)
|
||
}
|
||
}
|
||
|
||
/** Create a new terminal tab via the New tab menu. Returns the count after. */
|
||
export async function createTerminalTab(page) {
|
||
await dismissOverlays(page, 1)
|
||
const before = await page.locator(SORTABLE_TAB).count()
|
||
await page
|
||
.getByRole(NEW_TAB_BUTTON.role, { name: NEW_TAB_BUTTON.name })
|
||
.first()
|
||
.click({ force: true })
|
||
await page.getByRole('menuitem', { name: NEW_TERMINAL_ITEM }).first().click({ force: true })
|
||
await page.waitForFunction(
|
||
({ selector, prev }) => document.querySelectorAll(selector).length > prev,
|
||
{ selector: SORTABLE_TAB, prev: before },
|
||
{ timeout: 10_000 }
|
||
)
|
||
await waitForTerminalReady(page)
|
||
return page.locator(SORTABLE_TAB).count()
|
||
}
|
||
|
||
/** Cheap session identifiers: the rendered tab ids. */
|
||
export async function listTabIds(page) {
|
||
return page
|
||
.locator(SORTABLE_TAB)
|
||
.evaluateAll((tabs) =>
|
||
tabs.map((t) => t.getAttribute('data-tab-id')).filter((id) => Boolean(id))
|
||
)
|
||
}
|
||
|
||
// Click the live pane then focus its scoped textarea; offscreen textarea focus alone previously dropped input.
|
||
export async function focusActiveTerminal(page, terminalTabId = null) {
|
||
// Clear late feature tips before focusing so keystrokes reach the shell.
|
||
await dismissKnownOverlays(page)
|
||
const selector = terminalTabId
|
||
? `[data-terminal-tab-id="${String(terminalTabId)}"]:visible`
|
||
: TERMINAL_SURFACE_VISIBLE
|
||
const surface = page.locator(selector).first()
|
||
const click = surface.click({ position: { x: 24, y: 24 }, timeout: 15_000 })
|
||
// Fail if the expected restored tab vanishes rather than type into whatever retained focus.
|
||
await (terminalTabId ? click : click.catch(() => {}))
|
||
// Scope textarea to the visible pane, excluding hidden duplicates.
|
||
const input = surface.locator(XTERM_INPUT).last()
|
||
const focus = input.focus()
|
||
await (terminalTabId ? focus : focus.catch(() => {}))
|
||
return input
|
||
}
|
||
|
||
/** Type a line and submit it (Enter → \r submits in the shell). */
|
||
export async function typeLine(page, text, terminalTabId = null) {
|
||
await focusActiveTerminal(page, terminalTabId)
|
||
await page.keyboard.type(text)
|
||
await page.keyboard.press('Enter')
|
||
}
|
||
|
||
/** Send Ctrl+C to the active terminal. */
|
||
export async function sendCtrlC(page, terminalTabId = null) {
|
||
await focusActiveTerminal(page, terminalTabId)
|
||
await page.keyboard.press('Control+C')
|
||
}
|
||
|
||
// Escape backticks, quotes and $ through the outer PowerShell double-quoted argument to nested powershell.exe.
|
||
// Without $ escaping, while($true) becomes while(True), silently breaking loop/heartbeat probes.
|
||
export async function runShellCommand(page, psCommand) {
|
||
const escaped = psCommand.replace(/`/g, '``').replace(/"/g, '`"').replace(/\$/g, '`$')
|
||
await typeLine(page, `powershell.exe -NoProfile -NonInteractive -Command "${escaped}"`)
|
||
}
|
||
|
||
// Start title-canary/PID/500ms heartbeat marker; return command while caller reads its PID file.
|
||
export async function startMarker(page, { canary, pidFile, heartbeatFile }) {
|
||
const script = [
|
||
`$host.UI.RawUI.WindowTitle='${canary}'`,
|
||
`Set-Content -LiteralPath '${pidFile}' -Value $PID`,
|
||
`while($true){ [System.IO.File]::WriteAllText('${heartbeatFile}',(Get-Date).ToString('o')); Start-Sleep -Milliseconds 500 }`
|
||
].join('; ')
|
||
await runShellCommand(page, script)
|
||
return script
|
||
}
|
||
|
||
// Read scrollback best-effort through exposed SerializeAddon, else DOM rows; WebGL without exposure may return empty.
|
||
export async function readTerminalTextBestEffort(page) {
|
||
return page.evaluate(() => {
|
||
const managers = window.__paneManagers
|
||
if (managers && typeof managers.forEach === 'function') {
|
||
let out = ''
|
||
managers.forEach((m) => {
|
||
const pane = m.getActivePane?.() ?? m.getPanes?.()[0]
|
||
const text = pane?.serializeAddon?.serialize?.()
|
||
if (text) {
|
||
out += text
|
||
}
|
||
})
|
||
if (out) {
|
||
return out
|
||
}
|
||
}
|
||
return Array.from(document.querySelectorAll('.xterm-rows'))
|
||
.map((el) => el.textContent ?? '')
|
||
.join('\n')
|
||
})
|
||
}
|
||
|
||
const appShutdownProcesses = {
|
||
isPidAlive,
|
||
killTree(pid) {
|
||
execFileSync('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' })
|
||
}
|
||
}
|
||
|
||
/** Cleanup may kill the tree; survival proofs must disable that fallback. */
|
||
export async function closeApp(
|
||
app,
|
||
timeoutMs = 10_000,
|
||
{ allowForceKill = true, processes = appShutdownProcesses } = {}
|
||
) {
|
||
// A partially-created session (launch failed before assignment) passes undefined.
|
||
if (!app) {
|
||
return
|
||
}
|
||
const mainPid = await resolveElectronMainPid(app, { allowLauncherFallback: allowForceKill })
|
||
const launcher = app.process()
|
||
let closeStatus = 'not-started'
|
||
let closeTimeout
|
||
try {
|
||
if (!allowForceKill && mainPid === null) {
|
||
throw new Error('Cannot verify graceful close without the authoritative Electron PID')
|
||
}
|
||
closeStatus = 'pending'
|
||
const closePromise = app.close().then(
|
||
() => {
|
||
closeStatus = 'resolved'
|
||
},
|
||
(error) => {
|
||
closeStatus = 'rejected'
|
||
throw error
|
||
}
|
||
)
|
||
await Promise.race([
|
||
closePromise,
|
||
new Promise((_, reject) => {
|
||
closeTimeout = setTimeout(() => reject(new Error('close timeout')), timeoutMs)
|
||
closeTimeout.unref?.()
|
||
})
|
||
])
|
||
if (!allowForceKill && processes.isPidAlive(mainPid)) {
|
||
throw new Error('Playwright close resolved while the authoritative Electron PID remains live')
|
||
}
|
||
} catch (error) {
|
||
if (!allowForceKill) {
|
||
const evidence = {
|
||
closeStatus,
|
||
error: error instanceof Error ? error.message : String(error),
|
||
main: readClosePidEvidence(mainPid, processes),
|
||
launcher: {
|
||
...readClosePidEvidence(launcher?.pid, processes),
|
||
exitCode: launcher?.exitCode ?? null,
|
||
signalCode: launcher?.signalCode ?? null,
|
||
stdio:
|
||
launcher?.stdio.map((pipe, fd) => ({
|
||
fd,
|
||
destroyed: pipe?.destroyed ?? null,
|
||
readableEnded: pipe?.readableEnded ?? null
|
||
})) ?? []
|
||
}
|
||
}
|
||
// A surviving daemon can keep the launcher's pipes open after both app processes exit.
|
||
if (
|
||
closeStatus === 'pending' &&
|
||
evidence.main.state === 'exited' &&
|
||
evidence.launcher.state === 'exited'
|
||
) {
|
||
console.log(`[win-update-e2e] close-verified-by-pids: ${JSON.stringify(evidence)}`)
|
||
return
|
||
}
|
||
console.error(`[win-update-e2e] strict-close-failed: ${JSON.stringify(evidence)}`)
|
||
throw error
|
||
}
|
||
if (mainPid) {
|
||
try {
|
||
processes.killTree(mainPid)
|
||
} catch {
|
||
/* already gone */
|
||
}
|
||
}
|
||
} finally {
|
||
// Clear successful-close timers so shared harnesses do not retain timeout closures/handles.
|
||
clearTimeout(closeTimeout)
|
||
}
|
||
}
|
||
|
||
function readClosePidEvidence(pid, processes) {
|
||
if (!Number.isInteger(pid) || pid <= 0) {
|
||
return { pid: pid ?? null, state: 'unverifiable', error: 'PID unavailable' }
|
||
}
|
||
try {
|
||
return { pid, state: processes.isPidAlive(pid) ? 'live' : 'exited' }
|
||
} catch (error) {
|
||
return {
|
||
pid,
|
||
state: 'unverifiable',
|
||
error: error instanceof Error ? error.message : String(error)
|
||
}
|
||
}
|
||
}
|