mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 08:03:28 +00:00
* fix(startup): pin Windows appData and userData before anything resolves them A Windows session without a loaded profile (e.g. orca serve over SSH) can fail the roaming AppData known-folder lookup. Electron 43 then falls through to Chromium's userData provider and crashes natively. Resolve appData first (falling back to APPDATA, then USERPROFILE\AppData\Roaming), and set userData explicitly so Electron's provider never runs. * test(startup): remove the AppData fixture through the retrying helper --------- Co-authored-by: m4air <m4air@Mac.localdomain>
95 lines
3.9 KiB
TypeScript
95 lines
3.9 KiB
TypeScript
import { existsSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
import { _electron as electron, type ElectronApplication } from '@stablyai/playwright-test'
|
|
import { test, expect, forwardElectronProcessLogs } from './helpers/orca-app'
|
|
import { getE2ECompletedOnboardingProfile } from './helpers/e2e-completed-onboarding-profile'
|
|
import { getOrcaElectronLaunchArgs } from './helpers/electron-launch-args'
|
|
import { cleanupE2EDaemons, closeElectronAppForE2E } from './helpers/electron-process-shutdown'
|
|
import {
|
|
assertElectronResolvedIsolatedHome,
|
|
createElectronHomeIsolation
|
|
} from './helpers/electron-home-isolation'
|
|
import { RuntimeClient } from '../../src/cli/runtime/client'
|
|
import { RuntimeClientError } from '../../src/cli/runtime/types'
|
|
|
|
// Regression: a profile-less Windows session (e.g. `orca serve` over SSH) has no roaming AppData,
|
|
// and Electron 43 crashed natively resolving userData before Orca pinned it.
|
|
test('orca serve starts on Windows when the home has no AppData folder', async (// oxlint-disable-next-line no-empty-pattern -- This spec owns its launch and opts out of the default app fixture.
|
|
{}, testInfo) => {
|
|
test.skip(process.platform !== 'win32', 'Windows AppData resolution only')
|
|
|
|
const mainPath = path.join(process.cwd(), 'out', 'main', 'index.js')
|
|
const userDataDir = mkdtempSync(path.join(os.tmpdir(), 'orca-e2e-missing-appdata-'))
|
|
const missingAppData = path.join(userDataDir, 'absent', 'AppData', 'Roaming')
|
|
const { ELECTRON_RUN_AS_NODE: _unused, ...cleanEnv } = process.env
|
|
void _unused
|
|
const isolation = createElectronHomeIsolation({
|
|
inheritedEnv: cleanEnv,
|
|
launchEnv: {
|
|
NODE_ENV: 'development',
|
|
ORCA_E2E_HEADLESS: '1',
|
|
APPDATA: missingAppData
|
|
},
|
|
extraEnv: {},
|
|
userDataDir
|
|
})
|
|
rmSync(path.join(isolation.isolatedHome, 'AppData'), { recursive: true, force: true })
|
|
expect(existsSync(missingAppData)).toBe(false)
|
|
writeFileSync(
|
|
path.join(userDataDir, 'orca-data.json'),
|
|
`${JSON.stringify(getE2ECompletedOnboardingProfile(), null, 2)}\n`
|
|
)
|
|
|
|
let serveApp: ElectronApplication | null = null
|
|
try {
|
|
serveApp = await electron.launch({
|
|
args: [...getOrcaElectronLaunchArgs(mainPath, false), '--serve', '--serve-no-pairing'],
|
|
env: Object.fromEntries(
|
|
Object.entries(isolation.env).filter(
|
|
(entry): entry is [string, string] => entry[1] !== undefined
|
|
)
|
|
)
|
|
})
|
|
forwardElectronProcessLogs(serveApp, testInfo)
|
|
const paths = await serveApp.evaluate(({ app }) => ({
|
|
home: app.getPath('home'),
|
|
appData: app.getPath('appData'),
|
|
userData: app.getPath('userData')
|
|
}))
|
|
assertElectronResolvedIsolatedHome(paths.home, isolation)
|
|
// Why realpath: tmpdir can be an 8.3 short-name alias of the path Electron reports.
|
|
expect(realpathSync.native(paths.userData).toLowerCase()).toBe(
|
|
realpathSync.native(userDataDir).toLowerCase()
|
|
)
|
|
// Records whether Windows reported AppData or Orca fell back to the environment.
|
|
testInfo.annotations.push({
|
|
type: 'appData',
|
|
description: `${paths.appData === missingAppData ? 'env fallback' : 'native'}: ${paths.appData}`
|
|
})
|
|
|
|
const client = new RuntimeClient(userDataDir, 5_000)
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
try {
|
|
return (await client.getCliStatus()).result.app.desktopWindowStatus
|
|
} catch (error) {
|
|
if (error instanceof RuntimeClientError && error.code === 'runtime_unavailable') {
|
|
return 'starting'
|
|
}
|
|
throw error
|
|
}
|
|
},
|
|
{ timeout: 60_000, message: 'orca serve never became ready without AppData' }
|
|
)
|
|
.toBe('openable')
|
|
} finally {
|
|
if (serveApp) {
|
|
await closeElectronAppForE2E(serveApp)
|
|
}
|
|
await cleanupE2EDaemons(userDataDir)
|
|
rmSync(userDataDir, { recursive: true, force: true, maxRetries: 20, retryDelay: 100 })
|
|
}
|
|
})
|