mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 00:03:15 +00:00
* fix(terminal): mount one surface per workspace id in the workbench (STA-4846) * test(terminal): pin the workbench projection against under-selecting Losing a surface unmounts live terminals, which is worse than the duplicate mount STA-4846 fixes, so cover every catalog shape that reaches the workbench: local-only rows that name no host, an unqualified row colliding with a host-qualified one, two SSH hosts on one id, folder rows across three hosts, folder ids alongside git worktree ids, and a whole-catalog assertion that the emitted id set equals the distinct input id set. Also pin the `useAllWorktrees` -> `useWorktreeMap` swap: both read the same WeakMap-cached snapshot, so the zustand compare is unchanged. Harden the folder tie-break to require the row to name its own host. `getCatalogOwnerHostId` defaults an unstamped row to `local`, which would let a row that never named a host win the `local` tie and mount another host's path; it now keeps first-wins instead of guessing. * fix(terminal): surface the unresolvable folder-surface collision When two hosts publish the same folder-workspace id and the active workspace's host cannot be resolved, the projection drops one row's folderPath first-wins. That path is the PTY cwd for any tab without a startupCwd, so the drop was silent. Warn on it, and pin the two tie-break branches the unit tests missed: a colliding row that is not the active workspace, and the same collision with the rows in swapped order (a host reconnect re-appends its rows, flipping which row is first mid-session). * test(e2e): ride out Playwright's spurious main-process evaluate rejection `e2e / changed e2e specs` failed on `pr11346-selected-runtime-add.spec.ts` with "Execution context was destroyed, most likely because of a navigation" from the paired client's first `app.evaluate` — the isolated-HOME assert that runs one millisecond after `electron.launch()` resolves, which is before the app is `ready`. Nothing navigates there: Playwright raises that message for any main-process CDP failure that is neither a JS error nor a closed session, and `ElectronApplication.evaluate` is unreliable on Electron 27+ (microsoft/playwright#33737). Reproduced locally, and a plain re-run of the same commit went green. Extract the retry `installTerminalPtyWriteSpy` already carried for this exact message into `retryTransientMainEvaluate`, and use it for the launch-time home read in all three launchers. The read is idempotent and a real boundary escape still throws on the first successful read. Also forward the paired client's process logs before the assert instead of after: this failure reached CI with none of the client's own output, because forwarding had not started yet. * test(e2e): wait on the owning group before asserting a Cmd-J browser tab is active `changed e2e specs` then failed at the remote browser-page step: the store poll had already seen `activeBrowserTabId` land on the mirrored workspace, but `[data-tab-id=...][data-active="true"]` never appeared. `data-active` on a `BrowserTab` is the strip's active tab, which comes from the owning group's `activeTabId` — not from `activeBrowserTabId` — so the DOM assert was racing an activation the poll never waited for. The simulator rows in the same spec already poll the group; the two browser-page rows did not. Poll the same triple for them, so a genuinely stuck group fails with the ids it ended on instead of a bare "element(s) not found".
313 lines
11 KiB
TypeScript
313 lines
11 KiB
TypeScript
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { randomUUID } from 'node:crypto'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
import {
|
|
_electron as electron,
|
|
type ElectronApplication,
|
|
type Page,
|
|
type TestInfo
|
|
} from '@stablyai/playwright-test'
|
|
|
|
import { getE2ECompletedOnboardingProfile } from './e2e-completed-onboarding-profile'
|
|
import { getOrcaElectronLaunchArgs } from './electron-launch-args'
|
|
import { cleanupE2EDaemons, closeElectronAppForE2E } from './electron-process-shutdown'
|
|
import {
|
|
assertElectronResolvedIsolatedHome,
|
|
createElectronHomeIsolation
|
|
} from './electron-home-isolation'
|
|
import { retryTransientMainEvaluate } from './electron-main-evaluate-retry'
|
|
import { forwardElectronProcessLogs } from './orca-app'
|
|
import {
|
|
replaceRuntimePairingInPlace,
|
|
type SameIdPairingReplacement
|
|
} from './nested-runtime-same-id-pairing'
|
|
import { createPairedWebClientUrl, type PairedWebClientOptions } from './paired-web-client-url'
|
|
import { selectPairedRuntimeEnvironment } from './paired-client-runtime-environment'
|
|
|
|
export type { SameIdPairingReplacement } from './nested-runtime-same-id-pairing'
|
|
|
|
export type PairedElectronClient = {
|
|
app: ElectronApplication
|
|
page: Page
|
|
environmentId: string
|
|
captureDirectSshAttempts: () => Promise<void>
|
|
dispose: () => Promise<void>
|
|
getDirectSshAttemptTargetIds: () => Promise<string[]>
|
|
installDirectSshAttemptProbe: () => Promise<void>
|
|
replacePairingInPlace: (offer: RuntimeDesktopPairingOffer) => Promise<SameIdPairingReplacement>
|
|
/** Profile directory backing this client. Pass it to `reuseUserDataDir` to relaunch the same device. */
|
|
userDataDir: string
|
|
}
|
|
|
|
export type RuntimeDesktopPairingOffer = {
|
|
pairingUrl: string
|
|
webClientUrl?: string
|
|
}
|
|
|
|
export type PairedWebClient = {
|
|
page: Page
|
|
dispose: () => Promise<void>
|
|
}
|
|
|
|
const DIRECT_SSH_PROBE_CANARY_TARGET_ID = '__orca_e2e_direct_ssh_probe_canary__'
|
|
|
|
function readDirectSshAttemptTargetIds(probePath: string): string[] {
|
|
try {
|
|
return readFileSync(probePath, 'utf8')
|
|
.split(/\r?\n/)
|
|
.filter(Boolean)
|
|
.map((line) => JSON.parse(line) as string)
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
async function removeProfile(userDataDir: string): Promise<void> {
|
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
try {
|
|
rmSync(userDataDir, { recursive: true, force: true })
|
|
return
|
|
} catch (error) {
|
|
if (attempt === 4) {
|
|
throw error
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 250 * (attempt + 1)))
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function createRuntimeDesktopPairingOffer(
|
|
hubPage: Page
|
|
): Promise<RuntimeDesktopPairingOffer> {
|
|
return hubPage.evaluate(async () => {
|
|
const offer = await window.api.mobile.getRuntimePairingUrl({
|
|
address: '127.0.0.1',
|
|
rotate: true
|
|
})
|
|
if (!offer.available || !offer.pairingUrl) {
|
|
throw new Error('HUB runtime did not provide a desktop pairing URL')
|
|
}
|
|
return {
|
|
pairingUrl: offer.pairingUrl,
|
|
...(offer.webClientUrl ? { webClientUrl: offer.webClientUrl } : {})
|
|
}
|
|
})
|
|
}
|
|
|
|
export async function launchPairedWebClient(
|
|
hubApp: ElectronApplication,
|
|
offer: RuntimeDesktopPairingOffer,
|
|
options: PairedWebClientOptions = {}
|
|
): Promise<PairedWebClient> {
|
|
if (!offer.webClientUrl) {
|
|
throw new Error('HUB runtime did not provide a paired web client URL')
|
|
}
|
|
const clientUrl = createPairedWebClientUrl(offer.webClientUrl, options)
|
|
let page: Page | undefined
|
|
const pagePromise = hubApp.waitForEvent('window').then((candidate) => (page = candidate))
|
|
try {
|
|
await hubApp.evaluate(
|
|
async ({ BrowserWindow }, { partition, url }) => {
|
|
const clientWindow = new BrowserWindow({
|
|
height: 1200,
|
|
show: false,
|
|
width: 1440,
|
|
webPreferences: {
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
partition,
|
|
sandbox: true
|
|
}
|
|
})
|
|
await clientWindow.loadURL(url).catch((error) => {
|
|
clientWindow.destroy()
|
|
throw error
|
|
})
|
|
},
|
|
{
|
|
partition: `e2e-nested-runtime-web-${randomUUID()}`,
|
|
url: clientUrl
|
|
}
|
|
)
|
|
page = await pagePromise
|
|
if (options.waitForWorkspace !== false) {
|
|
await page.locator('[data-worktree-sidebar]').waitFor({ state: 'visible', timeout: 30_000 })
|
|
}
|
|
return { page, dispose: () => page?.close() ?? Promise.resolve() }
|
|
} catch (error) {
|
|
void pagePromise.catch(() => undefined)
|
|
await page?.close().catch(() => undefined)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
export async function launchPairedElectronClient(
|
|
offer: RuntimeDesktopPairingOffer,
|
|
testInfo: TestInfo,
|
|
name: string,
|
|
// reuseUserDataDir relaunches on an existing profile, so its stored pairing credential — and
|
|
// therefore its pairedDeviceId — survives the restart, as it does for a real force-quit reopen.
|
|
options: { extraEnv?: Record<string, string>; reuseUserDataDir?: string } = {}
|
|
): Promise<PairedElectronClient> {
|
|
const reusedProfile = options.reuseUserDataDir !== undefined
|
|
const userDataDir =
|
|
options.reuseUserDataDir ?? mkdtempSync(path.join(os.tmpdir(), 'orca-e2e-paired-desktop-'))
|
|
const directSshProbePath = path.join(userDataDir, 'forbidden-local-ssh-connects.jsonl')
|
|
if (!reusedProfile) {
|
|
writeFileSync(
|
|
path.join(userDataDir, 'orca-data.json'),
|
|
`${JSON.stringify(getE2ECompletedOnboardingProfile(), null, 2)}\n`
|
|
)
|
|
}
|
|
const { ELECTRON_RUN_AS_NODE: _unused, ...cleanEnv } = process.env
|
|
void _unused
|
|
const homeIsolation = createElectronHomeIsolation({
|
|
inheritedEnv: cleanEnv,
|
|
launchEnv: {},
|
|
extraEnv: options.extraEnv ?? {},
|
|
userDataDir
|
|
})
|
|
const mainPath = path.join(process.cwd(), 'out', 'main', 'index.js')
|
|
const app = await electron.launch({
|
|
args: getOrcaElectronLaunchArgs(mainPath, false),
|
|
env: {
|
|
...homeIsolation.env,
|
|
NODE_ENV: 'development',
|
|
ORCA_E2E_HEADLESS: '1',
|
|
ORCA_E2E_FORBID_LOCAL_SSH_CONNECT_PROBE: directSshProbePath
|
|
}
|
|
})
|
|
|
|
// Why before the home assert: forwarding starts here, so a client that fails during startup
|
|
// otherwise reaches CI as a bare Playwright error with none of its own output attached.
|
|
forwardElectronProcessLogs(app, testInfo)
|
|
try {
|
|
assertElectronResolvedIsolatedHome(
|
|
await retryTransientMainEvaluate(() =>
|
|
app.evaluate(({ app: electronApp }) => electronApp.getPath('home'))
|
|
),
|
|
homeIsolation
|
|
)
|
|
const page = await app.firstWindow({ timeout: 120_000 })
|
|
await page.waitForLoadState('domcontentloaded')
|
|
await page.waitForFunction(() => Boolean(window.__store), null, { timeout: 30_000 })
|
|
await page.waitForFunction(
|
|
() => window.__store?.getState().workspaceSessionReady === true,
|
|
null,
|
|
{ timeout: 30_000 }
|
|
)
|
|
const canaryBlocked = await page.evaluate(async (targetId) => {
|
|
try {
|
|
await window.api.ssh.connect({ targetId })
|
|
return false
|
|
} catch (error) {
|
|
return String(error).includes('e2e_forbidden_local_ssh_connect')
|
|
}
|
|
}, DIRECT_SSH_PROBE_CANARY_TARGET_ID)
|
|
if (
|
|
!canaryBlocked ||
|
|
!readDirectSshAttemptTargetIds(directSshProbePath).includes(DIRECT_SSH_PROBE_CANARY_TARGET_ID)
|
|
) {
|
|
throw new Error('Paired-client direct SSH probe did not intercept its canary attempt')
|
|
}
|
|
|
|
const environmentId = await selectPairedRuntimeEnvironment(page, {
|
|
name,
|
|
pairingUrl: offer.pairingUrl,
|
|
reusedProfile
|
|
})
|
|
const captureDirectSshAttempts = async (): Promise<void> => {}
|
|
const replacePairingInPlace = async (
|
|
replacementOffer: RuntimeDesktopPairingOffer
|
|
): Promise<SameIdPairingReplacement> =>
|
|
replaceRuntimePairingInPlace({
|
|
environmentId,
|
|
page,
|
|
pairingUrl: replacementOffer.pairingUrl,
|
|
userDataDir
|
|
})
|
|
|
|
return {
|
|
app,
|
|
page,
|
|
environmentId,
|
|
captureDirectSshAttempts,
|
|
dispose: async () => {
|
|
await closeElectronAppForE2E(app)
|
|
await cleanupE2EDaemons(userDataDir)
|
|
await removeProfile(userDataDir)
|
|
},
|
|
getDirectSshAttemptTargetIds: async () => {
|
|
return readDirectSshAttemptTargetIds(directSshProbePath).filter(
|
|
(targetId) => targetId !== DIRECT_SSH_PROBE_CANARY_TARGET_ID
|
|
)
|
|
},
|
|
installDirectSshAttemptProbe: async () => {},
|
|
replacePairingInPlace,
|
|
userDataDir
|
|
}
|
|
} catch (error) {
|
|
await closeElectronAppForE2E(app)
|
|
await cleanupE2EDaemons(userDataDir)
|
|
await removeProfile(userDataDir)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
export async function rePairPairedElectronClient(
|
|
client: PairedElectronClient,
|
|
offer: RuntimeDesktopPairingOffer,
|
|
name: string
|
|
): Promise<void> {
|
|
await client.captureDirectSshAttempts()
|
|
const environmentId = await client.page.evaluate(
|
|
async ({ currentEnvironmentId, name, pairingUrl }) => {
|
|
const store = window.__store
|
|
if (!store) {
|
|
throw new Error('Paired desktop store is unavailable')
|
|
}
|
|
await window.api.runtimeEnvironments.remove({ selector: currentEnvironmentId })
|
|
const result = await window.api.runtimeEnvironments.addFromPairingCode({
|
|
name,
|
|
pairingCode: pairingUrl
|
|
})
|
|
store.getState().setRuntimeEnvironments(await window.api.runtimeEnvironments.list())
|
|
if (!(await store.getState().refreshRuntimeEnvironmentStatus(result.environment.id))) {
|
|
throw new Error('Re-paired desktop could not reach the HUB runtime')
|
|
}
|
|
if (!(await store.getState().setActiveRuntimeEnvironmentPreference(result.environment.id))) {
|
|
throw new Error('Re-paired desktop could not select the HUB runtime')
|
|
}
|
|
return result.environment.id
|
|
},
|
|
{
|
|
currentEnvironmentId: client.environmentId,
|
|
name,
|
|
pairingUrl: offer.pairingUrl
|
|
}
|
|
)
|
|
client.environmentId = environmentId
|
|
// Why: removing and re-adding the same HUB changes the environment identity; remount so no pane keeps the retired transport wrapper.
|
|
await client.page.reload()
|
|
await client.page.waitForFunction(
|
|
() => window.__store?.getState().workspaceSessionReady === true,
|
|
null,
|
|
{ timeout: 30_000 }
|
|
)
|
|
await client.installDirectSshAttemptProbe()
|
|
const reachable = await client.page.evaluate(async (nextEnvironmentId) => {
|
|
const store = window.__store
|
|
if (!store) {
|
|
throw new Error('Re-paired desktop store is unavailable after reload')
|
|
}
|
|
if (!(await store.getState().refreshRuntimeEnvironmentStatus(nextEnvironmentId))) {
|
|
return false
|
|
}
|
|
return store.getState().setActiveRuntimeEnvironmentPreference(nextEnvironmentId)
|
|
}, environmentId)
|
|
if (!reachable) {
|
|
throw new Error('Re-paired desktop could not reach the HUB after reload')
|
|
}
|
|
}
|