mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
* fix(cli): check the runtime source before sending A terminal Orca launches now carries which Orca launched it (ORCA_RUNTIME_SOURCE_ID = the profile's installation id, ORCA_RUNTIME_SOURCE_INCARNATION = the launching runtime id). Before a request is sent, the CLI compares that with the runtime the profile's runtime record names. Same runtime: sent as before. Different runtime: a read-only status.get asks which profile it serves; the same profile (a restart or update) is accepted, anything else is refused before the request reaches it. The vars cross into WSL through WSLENV. The SSH bridge stamps the bridging runtime itself rather than trusting the remote env. The existing orchestration compatibility stamp is untouched. * fix(orcad): drop an inherited runtime source at startup * fix(cli): an explicit switch to another profile is not fenced The stamp also carries the launching runtime's userData path. A CLI that dials a different profile than the one that launched its terminal, and not the platform default an unpinned terminal falls back to, was pointed there on purpose (orca-dev from a packaged Orca's terminal), so the fence does not apply. * refactor(cli): reuse the shared userData path comparison --------- Co-authored-by: m4air <m4air@Mac.localdomain>
979 lines
38 KiB
TypeScript
979 lines
38 KiB
TypeScript
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { homedir, tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
vi.mock('electron', () => {
|
|
const paths = new Map<string, string>([['appData', '/tmp/app-data']])
|
|
return {
|
|
app: {
|
|
getPath: vi.fn((name: string) => paths.get(name) ?? ''),
|
|
setPath: vi.fn((name: string, value: string) => {
|
|
paths.set(name, value)
|
|
}),
|
|
quit: vi.fn(),
|
|
exit: vi.fn(),
|
|
isPackaged: false,
|
|
disableHardwareAcceleration: vi.fn(),
|
|
commandLine: {
|
|
appendSwitch: vi.fn(),
|
|
getSwitchValue: vi.fn(() => '')
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
describe('patchPackagedProcessPath', () => {
|
|
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
|
|
const originalHome = process.env.HOME
|
|
const originalPath = process.env.PATH
|
|
|
|
function setPlatform(platform: NodeJS.Platform): void {
|
|
Object.defineProperty(process, 'platform', {
|
|
configurable: true,
|
|
value: platform
|
|
})
|
|
}
|
|
|
|
afterEach(() => {
|
|
if (originalPlatform) {
|
|
Object.defineProperty(process, 'platform', originalPlatform)
|
|
}
|
|
if (originalHome === undefined) {
|
|
delete process.env.HOME
|
|
} else {
|
|
process.env.HOME = originalHome
|
|
}
|
|
if (originalPath === undefined) {
|
|
delete process.env.PATH
|
|
} else {
|
|
process.env.PATH = originalPath
|
|
}
|
|
})
|
|
|
|
it('prepends agent-CLI install dirs (~/.opencode/bin, ~/.vite-plus/bin) for packaged darwin runs', async () => {
|
|
const { app } = await import('electron')
|
|
const { patchPackagedProcessPath } = await import('./configure-process')
|
|
|
|
setPlatform('darwin')
|
|
Object.defineProperty(app, 'isPackaged', { configurable: true, value: true })
|
|
process.env.HOME = '/Users/tester'
|
|
process.env.PATH = '/usr/bin:/bin'
|
|
|
|
patchPackagedProcessPath()
|
|
|
|
const segments = (process.env.PATH ?? '').split(':')
|
|
// Why: issue #829 — ~/.opencode/bin and ~/.vite-plus/bin are the documented
|
|
// fallback install locations for the opencode and Pi CLI install scripts.
|
|
// Without them on PATH, GUI-launched Orca reports both as "Not installed"
|
|
// even when `which` resolves them in the user's shell.
|
|
expect(segments).toContain(join('/Users/tester', '.opencode/bin'))
|
|
expect(segments).toContain(join('/Users/tester', '.vite-plus/bin'))
|
|
expect(segments).toContain(join('/Users/tester', 'bin'))
|
|
})
|
|
|
|
it('omits Linux-only snap/Linuxbrew dirs but keeps Nix on packaged darwin runs', async () => {
|
|
const { app } = await import('electron')
|
|
const { patchPackagedProcessPath } = await import('./configure-process')
|
|
|
|
setPlatform('darwin')
|
|
Object.defineProperty(app, 'isPackaged', { configurable: true, value: true })
|
|
process.env.HOME = '/Users/tester'
|
|
process.env.PATH = '/usr/bin:/bin'
|
|
|
|
patchPackagedProcessPath()
|
|
|
|
const segments = (process.env.PATH ?? '').split(':')
|
|
// Why: neither has a macOS installer, so both are phantom PATH entries.
|
|
expect(segments).not.toContain('/snap/bin')
|
|
expect(segments).not.toContain('/home/linuxbrew/.linuxbrew/bin')
|
|
// Why: Nix does ship a macOS default profile, so it stays seeded.
|
|
expect(segments).toContain('/nix/var/nix/profiles/default/bin')
|
|
expect(segments).toContain('/opt/homebrew/bin')
|
|
expect(segments).toContain('/usr/local/bin')
|
|
})
|
|
|
|
it('keeps snap, Linuxbrew, and Nix dirs for packaged linux runs', async () => {
|
|
const { app } = await import('electron')
|
|
const { patchPackagedProcessPath } = await import('./configure-process')
|
|
|
|
setPlatform('linux')
|
|
Object.defineProperty(app, 'isPackaged', { configurable: true, value: true })
|
|
process.env.HOME = '/home/tester'
|
|
process.env.PATH = '/usr/bin:/bin'
|
|
|
|
patchPackagedProcessPath()
|
|
|
|
const segments = (process.env.PATH ?? '').split(':')
|
|
expect(segments).toContain('/snap/bin')
|
|
expect(segments).toContain('/home/linuxbrew/.linuxbrew/bin')
|
|
expect(segments).toContain('/nix/var/nix/profiles/default/bin')
|
|
expect(segments.indexOf('/usr/local/sbin')).toBeLessThan(segments.indexOf('/snap/bin'))
|
|
expect(segments.indexOf('/home/linuxbrew/.linuxbrew/bin')).toBeLessThan(
|
|
segments.indexOf('/nix/var/nix/profiles/default/bin')
|
|
)
|
|
})
|
|
|
|
it('omits snap/Linuxbrew but keeps Nix on non-Linux POSIX platforms', async () => {
|
|
const { app } = await import('electron')
|
|
const { patchPackagedProcessPath } = await import('./configure-process')
|
|
|
|
setPlatform('freebsd')
|
|
Object.defineProperty(app, 'isPackaged', { configurable: true, value: true })
|
|
process.env.HOME = '/home/tester'
|
|
process.env.PATH = '/usr/bin:/bin'
|
|
|
|
patchPackagedProcessPath()
|
|
|
|
const segments = (process.env.PATH ?? '').split(':')
|
|
expect(segments).not.toContain('/snap/bin')
|
|
expect(segments).not.toContain('/home/linuxbrew/.linuxbrew/bin')
|
|
expect(segments).toContain('/nix/var/nix/profiles/default/bin')
|
|
expect(segments).toContain('/usr/local/bin')
|
|
})
|
|
|
|
// Why derived, not a second literal: system-cli-install-dirs.ts documents its
|
|
// order as matching this seed's system block, and hardcoding the order in the
|
|
// fallback's own test lets a reorder here break that parity while both stay green.
|
|
it('seeds the system block in the order the install-dir fallback expects', async () => {
|
|
const { app } = await import('electron')
|
|
const { patchPackagedProcessPath } = await import('./configure-process')
|
|
const { getSystemCliInstallDirectories } = await import('../../shared/system-cli-install-dirs')
|
|
|
|
setPlatform('linux')
|
|
Object.defineProperty(app, 'isPackaged', { configurable: true, value: true })
|
|
process.env.HOME = '/home/tester'
|
|
process.env.PATH = '/usr/bin:/bin'
|
|
|
|
patchPackagedProcessPath()
|
|
|
|
const segments = (process.env.PATH ?? '').split(':')
|
|
const offsets = getSystemCliInstallDirectories('linux', '/home/tester').map((directory) =>
|
|
segments.indexOf(directory)
|
|
)
|
|
expect(offsets.every((offset) => offset >= 0)).toBe(true)
|
|
expect([...offsets].sort((a, b) => a - b)).toEqual(offsets)
|
|
})
|
|
|
|
// Why this ordering is load-bearing (#18234): a seed exists so a GUI-launched
|
|
// Electron can *find* a tool, not to re-rank tools the user already has.
|
|
// `~/.local/bin` is user-writable and can hold a wrapper for any system tool.
|
|
// The reporter's `~/.local/bin/gh` wrapped `mise x gh -- gh`; seeded ahead of
|
|
// /usr/bin it ran instead of the real gh, and the wrapper's inner bare `gh`
|
|
// resolved back to itself. Measured in a container: with the login shell's
|
|
// ordering that chain exits in 22ms, with the seeded ordering it never
|
|
// terminates and creates ~1,300 processes/second.
|
|
it('never lets a seeded user dir overtake a system dir already on PATH', async () => {
|
|
const { app } = await import('electron')
|
|
const { patchPackagedProcessPath } = await import('./configure-process')
|
|
|
|
setPlatform('linux')
|
|
Object.defineProperty(app, 'isPackaged', { configurable: true, value: true })
|
|
process.env.HOME = '/home/tester'
|
|
process.env.PATH = '/usr/local/bin:/usr/bin:/bin'
|
|
|
|
patchPackagedProcessPath()
|
|
|
|
const segments = (process.env.PATH ?? '').split(':')
|
|
const localBin = segments.indexOf(join('/home/tester', '.local/bin'))
|
|
// Still reachable — that is what the seeding is for (#829).
|
|
expect(localBin).toBeGreaterThan(-1)
|
|
for (const systemDir of ['/usr/bin', '/bin', '/usr/local/bin']) {
|
|
expect(segments.indexOf(systemDir)).toBeLessThan(localBin)
|
|
}
|
|
expect(segments.indexOf(join('/home/tester', 'bin'))).toBeGreaterThan(
|
|
segments.indexOf('/usr/bin')
|
|
)
|
|
})
|
|
|
|
it('keeps version-manager shims ahead of the inherited PATH', async () => {
|
|
const { app } = await import('electron')
|
|
const { patchPackagedProcessPath } = await import('./configure-process')
|
|
const { getVersionManagerBinPaths } = await import('../../shared/node-cli-command-resolution')
|
|
|
|
setPlatform('linux')
|
|
Object.defineProperty(app, 'isPackaged', { configurable: true, value: true })
|
|
process.env.HOME = '/home/tester'
|
|
process.env.PATH = '/usr/bin:/bin'
|
|
|
|
patchPackagedProcessPath()
|
|
|
|
const segments = (process.env.PATH ?? '').split(':')
|
|
const genericUserBinDirs = [join('/home/tester', 'bin'), join('/home/tester', '.local/bin')]
|
|
const seeded = getVersionManagerBinPaths({ platform: 'linux', homePath: '/home/tester' })
|
|
const shimDirs = seeded.filter((dir) => !genericUserBinDirs.includes(dir))
|
|
expect(shimDirs).not.toHaveLength(0)
|
|
// Why these keep leading: an nvm/mise/asdf user's runtime must beat a
|
|
// system install, which is the reason this seeding is ordered at all.
|
|
for (const dir of shimDirs) {
|
|
expect(segments.indexOf(dir)).toBeLessThan(segments.indexOf('/usr/bin'))
|
|
}
|
|
// Why these do not: the same list carries the generic user bin dirs, which
|
|
// hold whatever was last installed there rather than a managed toolchain.
|
|
for (const dir of genericUserBinDirs) {
|
|
expect(segments.indexOf(dir)).toBeGreaterThan(segments.indexOf('/usr/bin'))
|
|
}
|
|
})
|
|
|
|
it('leaves PATH untouched when the app is not packaged', async () => {
|
|
const { app } = await import('electron')
|
|
const { patchPackagedProcessPath } = await import('./configure-process')
|
|
|
|
setPlatform('darwin')
|
|
Object.defineProperty(app, 'isPackaged', { configurable: true, value: false })
|
|
process.env.HOME = '/Users/tester'
|
|
process.env.PATH = '/usr/bin:/bin'
|
|
|
|
patchPackagedProcessPath()
|
|
|
|
expect(process.env.PATH).toBe('/usr/bin:/bin')
|
|
})
|
|
|
|
it('prepends Windows user-local CLI dirs for packaged Start Menu launches', async () => {
|
|
const { app } = await import('electron')
|
|
const { patchPackagedProcessPath } = await import('./configure-process')
|
|
|
|
setPlatform('win32')
|
|
Object.defineProperty(app, 'isPackaged', { configurable: true, value: true })
|
|
const pathDelimiter = process.platform === 'win32' ? ';' : ':'
|
|
process.env.PATH = `C:\\Windows\\System32${pathDelimiter}C:\\Windows`
|
|
|
|
patchPackagedProcessPath()
|
|
|
|
const segments = (process.env.PATH ?? '').split(pathDelimiter)
|
|
const userLocalBin = join(homedir(), '.local', 'bin')
|
|
expect(segments).toContain(userLocalBin)
|
|
expect(segments.indexOf(userLocalBin)).toBeLessThan(segments.indexOf('C:\\Windows\\System32'))
|
|
})
|
|
})
|
|
|
|
describe('configureDevUserDataPath', () => {
|
|
it('forces Electron home into the disposable E2E profile', async () => {
|
|
const { app } = await import('electron')
|
|
const { configureDevUserDataPath } = await import('./configure-process')
|
|
const originalE2EUserDataDir = process.env.ORCA_E2E_USER_DATA_DIR
|
|
const originalE2EHomeDir = process.env.ORCA_E2E_HOME_DIR
|
|
const originalHome = process.env.HOME
|
|
const originalUserProfile = process.env.USERPROFILE
|
|
const tempRoot = mkdtempSync(join(tmpdir(), 'orca-configure-e2e-home-'))
|
|
const e2eRoot = join(tempRoot, 'user-data')
|
|
const e2eHome = join(tempRoot, 'home')
|
|
process.env.ORCA_E2E_USER_DATA_DIR = e2eRoot
|
|
process.env.ORCA_E2E_HOME_DIR = e2eHome
|
|
process.env.HOME = e2eHome
|
|
process.env.USERPROFILE = e2eHome
|
|
|
|
try {
|
|
configureDevUserDataPath(true)
|
|
} finally {
|
|
rmSync(tempRoot, { recursive: true, force: true })
|
|
if (originalE2EUserDataDir === undefined) {
|
|
delete process.env.ORCA_E2E_USER_DATA_DIR
|
|
} else {
|
|
process.env.ORCA_E2E_USER_DATA_DIR = originalE2EUserDataDir
|
|
}
|
|
if (originalE2EHomeDir === undefined) {
|
|
delete process.env.ORCA_E2E_HOME_DIR
|
|
} else {
|
|
process.env.ORCA_E2E_HOME_DIR = originalE2EHomeDir
|
|
}
|
|
if (originalHome === undefined) {
|
|
delete process.env.HOME
|
|
} else {
|
|
process.env.HOME = originalHome
|
|
}
|
|
if (originalUserProfile === undefined) {
|
|
delete process.env.USERPROFILE
|
|
} else {
|
|
process.env.USERPROFILE = originalUserProfile
|
|
}
|
|
}
|
|
|
|
expect(app.setPath).toHaveBeenCalledWith('home', e2eHome)
|
|
expect(app.setPath).toHaveBeenCalledWith('userData', e2eRoot)
|
|
})
|
|
|
|
it('rejects an E2E launch whose Node home escaped the disposable profile', async () => {
|
|
const { configureDevUserDataPath } = await import('./configure-process')
|
|
const originalE2EUserDataDir = process.env.ORCA_E2E_USER_DATA_DIR
|
|
const originalE2EHomeDir = process.env.ORCA_E2E_HOME_DIR
|
|
const originalHome = process.env.HOME
|
|
const originalUserProfile = process.env.USERPROFILE
|
|
const e2eRoot = mkdtempSync(join(tmpdir(), 'orca-configure-e2e-escape-'))
|
|
process.env.ORCA_E2E_USER_DATA_DIR = e2eRoot
|
|
process.env.ORCA_E2E_HOME_DIR = join(e2eRoot, 'home')
|
|
process.env.HOME = join(e2eRoot, 'escaped-home')
|
|
process.env.USERPROFILE = join(e2eRoot, 'escaped-home')
|
|
|
|
try {
|
|
expect(() => configureDevUserDataPath(true)).toThrow(/disposable home boundary/)
|
|
} finally {
|
|
rmSync(e2eRoot, { recursive: true, force: true })
|
|
restoreEnv('ORCA_E2E_USER_DATA_DIR', originalE2EUserDataDir)
|
|
restoreEnv('ORCA_E2E_HOME_DIR', originalE2EHomeDir)
|
|
restoreEnv('HOME', originalHome)
|
|
restoreEnv('USERPROFILE', originalUserProfile)
|
|
}
|
|
})
|
|
|
|
it('uses an explicit dev userData override when provided', async () => {
|
|
const { app } = await import('electron')
|
|
const { configureDevUserDataPath } = await import('./configure-process')
|
|
const originalOverride = process.env.ORCA_DEV_USER_DATA_PATH
|
|
process.env.ORCA_DEV_USER_DATA_PATH = '/tmp/orca-dev-repro'
|
|
|
|
try {
|
|
configureDevUserDataPath(true)
|
|
} finally {
|
|
if (originalOverride === undefined) {
|
|
delete process.env.ORCA_DEV_USER_DATA_PATH
|
|
} else {
|
|
process.env.ORCA_DEV_USER_DATA_PATH = originalOverride
|
|
}
|
|
}
|
|
|
|
expect(app.setPath).toHaveBeenCalledWith('userData', '/tmp/orca-dev-repro')
|
|
})
|
|
|
|
it('moves dev runs onto an orca-dev userData path', async () => {
|
|
const { app } = await import('electron')
|
|
const { configureDevUserDataPath } = await import('./configure-process')
|
|
|
|
delete process.env.ORCA_DEV_USER_DATA_PATH
|
|
configureDevUserDataPath(true)
|
|
|
|
// Why: production code uses path.join(app.getPath('appData'), 'orca-dev')
|
|
// which produces platform-specific separators.
|
|
expect(app.setPath).toHaveBeenCalledWith('userData', join('/tmp/app-data', 'orca-dev'))
|
|
})
|
|
|
|
it('leaves packaged runs on the default userData path', async () => {
|
|
const { app } = await import('electron')
|
|
const { configureDevUserDataPath } = await import('./configure-process')
|
|
|
|
vi.mocked(app.setPath).mockClear()
|
|
configureDevUserDataPath(false)
|
|
|
|
expect(app.setPath).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
function restoreEnv(key: string, value: string | undefined): void {
|
|
if (value === undefined) {
|
|
delete process.env[key]
|
|
} else {
|
|
process.env[key] = value
|
|
}
|
|
}
|
|
|
|
describe('configureOrcaUserDataPathEnv', () => {
|
|
it('overwrites stale inherited ORCA_USER_DATA_PATH with Electron userData', async () => {
|
|
const { app } = await import('electron')
|
|
const { configureOrcaUserDataPathEnv } = await import('./configure-process')
|
|
const originalUserDataPath = process.env.ORCA_USER_DATA_PATH
|
|
process.env.ORCA_USER_DATA_PATH = '/tmp/stale-orca-user-data'
|
|
app.setPath('userData', '/tmp/current-orca-user-data')
|
|
let configuredUserDataPath: string | undefined
|
|
|
|
try {
|
|
configureOrcaUserDataPathEnv()
|
|
configuredUserDataPath = process.env.ORCA_USER_DATA_PATH
|
|
} finally {
|
|
if (originalUserDataPath === undefined) {
|
|
delete process.env.ORCA_USER_DATA_PATH
|
|
} else {
|
|
process.env.ORCA_USER_DATA_PATH = originalUserDataPath
|
|
}
|
|
}
|
|
|
|
expect(configuredUserDataPath).toBe('/tmp/current-orca-user-data')
|
|
})
|
|
|
|
it('drops a runtime source inherited from the Orca terminal that started this Orca', async () => {
|
|
const { app } = await import('electron')
|
|
const { configureOrcaUserDataPathEnv } = await import('./configure-process')
|
|
const saved = { ...process.env }
|
|
process.env.ORCA_RUNTIME_SOURCE_ID = 'parent-source'
|
|
process.env.ORCA_RUNTIME_SOURCE_INCARNATION = 'parent-runtime'
|
|
app.setPath('userData', '/tmp/current-orca-user-data')
|
|
let inherited: (string | undefined)[] = []
|
|
|
|
try {
|
|
configureOrcaUserDataPathEnv()
|
|
inherited = [process.env.ORCA_RUNTIME_SOURCE_ID, process.env.ORCA_RUNTIME_SOURCE_INCARNATION]
|
|
} finally {
|
|
process.env = saved
|
|
}
|
|
|
|
expect(inherited).toEqual([undefined, undefined])
|
|
})
|
|
})
|
|
|
|
describe('shouldInstallManagedHooks', () => {
|
|
it('keeps managed hook auto-install enabled for default dev runs', async () => {
|
|
const { shouldInstallManagedHooks } = await import('./configure-process')
|
|
|
|
expect(shouldInstallManagedHooks(true)).toBe(true)
|
|
})
|
|
|
|
it('allows managed hook auto-install for packaged runs', async () => {
|
|
const { shouldInstallManagedHooks } = await import('./configure-process')
|
|
|
|
expect(shouldInstallManagedHooks(false)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('configureElectronNetworkCompatibility', () => {
|
|
const tempDirs: string[] = []
|
|
const originalEnvValue = process.env.ORCA_DISABLE_HTTP2
|
|
|
|
function createUserDataDir(settings: Record<string, unknown>): string {
|
|
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-http1-compat-'))
|
|
tempDirs.push(userDataPath)
|
|
writeFileSync(join(userDataPath, 'orca-data.json'), JSON.stringify({ settings }), 'utf-8')
|
|
return userDataPath
|
|
}
|
|
|
|
function createProfileState(
|
|
userDataPath: string,
|
|
profileId: string,
|
|
settings: Record<string, unknown>
|
|
): string {
|
|
const profileDirectory = join(userDataPath, 'profiles', profileId)
|
|
mkdirSync(profileDirectory, { recursive: true })
|
|
writeFileSync(
|
|
join(userDataPath, 'orca-profile-index.json'),
|
|
JSON.stringify({ activeProfileId: profileId, profiles: [{ id: profileId }] }),
|
|
'utf-8'
|
|
)
|
|
writeFileSync(join(profileDirectory, 'orca-data.json'), JSON.stringify({ settings }), 'utf-8')
|
|
return profileDirectory
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const dir of tempDirs.splice(0)) {
|
|
rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
if (originalEnvValue === undefined) {
|
|
delete process.env.ORCA_DISABLE_HTTP2
|
|
} else {
|
|
process.env.ORCA_DISABLE_HTTP2 = originalEnvValue
|
|
}
|
|
})
|
|
|
|
it('enables HTTP/1.1 compatibility when the persisted setting is on', async () => {
|
|
const { shouldDisableHttp2ForElectronNetworking } = await import('./configure-process')
|
|
const userDataPath = createUserDataDir({ electronHttp1CompatibilityMode: true })
|
|
|
|
expect(shouldDisableHttp2ForElectronNetworking({ env: {}, userDataPath })).toBe(true)
|
|
})
|
|
|
|
it('leaves HTTP/2 enabled by default', async () => {
|
|
const { shouldDisableHttp2ForElectronNetworking } = await import('./configure-process')
|
|
const userDataPath = createUserDataDir({})
|
|
|
|
expect(shouldDisableHttp2ForElectronNetworking({ env: {}, userDataPath })).toBe(false)
|
|
})
|
|
|
|
it('lets the environment override force compatibility on', async () => {
|
|
const { shouldDisableHttp2ForElectronNetworking } = await import('./configure-process')
|
|
|
|
expect(
|
|
shouldDisableHttp2ForElectronNetworking({
|
|
env: { ORCA_DISABLE_HTTP2: 'true' },
|
|
userDataPath: createUserDataDir({ electronHttp1CompatibilityMode: false })
|
|
})
|
|
).toBe(true)
|
|
})
|
|
|
|
it('lets the environment override force compatibility off', async () => {
|
|
const { shouldDisableHttp2ForElectronNetworking } = await import('./configure-process')
|
|
|
|
expect(
|
|
shouldDisableHttp2ForElectronNetworking({
|
|
env: { ORCA_DISABLE_HTTP2: '0' },
|
|
userDataPath: createUserDataDir({ electronHttp1CompatibilityMode: true })
|
|
})
|
|
).toBe(false)
|
|
})
|
|
|
|
it('answers from the marker without reading the settings file', async () => {
|
|
const { shouldDisableHttp2ForElectronNetworking } = await import('./configure-process')
|
|
const { writeHttp1CompatibilityMarker } = await import('./http1-compatibility-marker')
|
|
const userDataPath = createUserDataDir({ electronHttp1CompatibilityMode: false })
|
|
writeHttp1CompatibilityMarker(userDataPath, true)
|
|
rmSync(join(userDataPath, 'orca-data.json'), { force: true })
|
|
|
|
expect(shouldDisableHttp2ForElectronNetworking({ env: {}, userDataPath })).toBe(true)
|
|
})
|
|
|
|
it('falls back to the settings file when no marker has been written yet', async () => {
|
|
const { shouldDisableHttp2ForElectronNetworking } = await import('./configure-process')
|
|
const userDataPath = createUserDataDir({ electronHttp1CompatibilityMode: true })
|
|
|
|
expect(existsSync(join(userDataPath, 'http1-compatibility.json'))).toBe(false)
|
|
expect(shouldDisableHttp2ForElectronNetworking({ env: {}, userDataPath })).toBe(true)
|
|
})
|
|
|
|
it('falls back to the settings file when the marker is corrupt', async () => {
|
|
const { shouldDisableHttp2ForElectronNetworking } = await import('./configure-process')
|
|
const userDataPath = createUserDataDir({ electronHttp1CompatibilityMode: true })
|
|
writeFileSync(join(userDataPath, 'http1-compatibility.json'), '{ not json', 'utf-8')
|
|
|
|
expect(shouldDisableHttp2ForElectronNetworking({ env: {}, userDataPath })).toBe(true)
|
|
})
|
|
|
|
it('lets the environment override the marker', async () => {
|
|
const { shouldDisableHttp2ForElectronNetworking } = await import('./configure-process')
|
|
const { writeHttp1CompatibilityMarker } = await import('./http1-compatibility-marker')
|
|
const userDataPath = createUserDataDir({})
|
|
writeHttp1CompatibilityMarker(userDataPath, true)
|
|
|
|
expect(
|
|
shouldDisableHttp2ForElectronNetworking({ env: { ORCA_DISABLE_HTTP2: '0' }, userDataPath })
|
|
).toBe(false)
|
|
})
|
|
|
|
it('scopes a profile marker to the active profile before trusting it', async () => {
|
|
const { shouldDisableHttp2ForElectronNetworking } = await import('./configure-process')
|
|
const { writeHttp1CompatibilityMarker } = await import('./http1-compatibility-marker')
|
|
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-http1-profile-'))
|
|
tempDirs.push(userDataPath)
|
|
createProfileState(userDataPath, 'profile-b', { electronHttp1CompatibilityMode: false })
|
|
writeHttp1CompatibilityMarker(userDataPath, true, 'profile-a')
|
|
|
|
expect(shouldDisableHttp2ForElectronNetworking({ env: {}, userDataPath })).toBe(false)
|
|
})
|
|
|
|
it('uses a matching profile marker even when the legacy JSON is stale', async () => {
|
|
const { shouldDisableHttp2ForElectronNetworking } = await import('./configure-process')
|
|
const { writeHttp1CompatibilityMarker } = await import('./http1-compatibility-marker')
|
|
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-http1-profile-'))
|
|
tempDirs.push(userDataPath)
|
|
createProfileState(userDataPath, 'profile-b', { electronHttp1CompatibilityMode: false })
|
|
writeHttp1CompatibilityMarker(userDataPath, true, 'profile-b')
|
|
|
|
expect(shouldDisableHttp2ForElectronNetworking({ env: {}, userDataPath })).toBe(true)
|
|
})
|
|
|
|
it('fails closed when a profile database exists without a trusted marker', async () => {
|
|
const { shouldDisableHttp2ForElectronNetworking } = await import('./configure-process')
|
|
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-http1-profile-'))
|
|
tempDirs.push(userDataPath)
|
|
const profileDirectory = createProfileState(userDataPath, 'profile-b', {
|
|
electronHttp1CompatibilityMode: true
|
|
})
|
|
writeFileSync(join(profileDirectory, 'profile-state.db'), 'sqlite-present', 'utf-8')
|
|
|
|
expect(shouldDisableHttp2ForElectronNetworking({ env: {}, userDataPath })).toBe(false)
|
|
})
|
|
|
|
it('appends Electron disable-http2 before sessions are created', async () => {
|
|
const { app } = await import('electron')
|
|
const { configureElectronNetworkCompatibility } = await import('./configure-process')
|
|
const userDataPath = createUserDataDir({ electronHttp1CompatibilityMode: true })
|
|
|
|
vi.mocked(app.commandLine.appendSwitch).mockClear()
|
|
configureElectronNetworkCompatibility({ env: {}, userDataPath })
|
|
|
|
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith('disable-http2')
|
|
})
|
|
})
|
|
|
|
const EXPECTED_DISABLED_FEATURES =
|
|
'FedCm,DirectSockets,DirectSocketsInSharedWorkers,DirectSocketsInServiceWorkers'
|
|
|
|
describe('disableUnsupportedChromiumFeatures', () => {
|
|
it('matches the shared list the real-Electron egress probes launch with', async () => {
|
|
const { DISABLED_CHROMIUM_FEATURES } = await import('./disabled-chromium-features')
|
|
|
|
expect(DISABLED_CHROMIUM_FEATURES.join(',')).toBe(EXPECTED_DISABLED_FEATURES)
|
|
})
|
|
|
|
it('disables FedCM before Chromium sessions are created', async () => {
|
|
const { app } = await import('electron')
|
|
const { disableUnsupportedChromiumFeatures } = await import('./configure-process')
|
|
|
|
vi.mocked(app.commandLine.appendSwitch).mockClear()
|
|
disableUnsupportedChromiumFeatures()
|
|
|
|
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith(
|
|
'disable-features',
|
|
EXPECTED_DISABLED_FEATURES
|
|
)
|
|
})
|
|
|
|
it('disables every Direct Sockets surface so a hostile page cannot kill its renderer', async () => {
|
|
const { app } = await import('electron')
|
|
const { disableUnsupportedChromiumFeatures } = await import('./configure-process')
|
|
|
|
vi.mocked(app.commandLine.appendSwitch).mockClear()
|
|
disableUnsupportedChromiumFeatures()
|
|
|
|
const disabled = vi
|
|
.mocked(app.commandLine.appendSwitch)
|
|
.mock.calls.find(([name]) => name === 'disable-features')?.[1]
|
|
?.split(',')
|
|
|
|
expect(disabled).toEqual(
|
|
expect.arrayContaining([
|
|
'DirectSockets',
|
|
'DirectSocketsInSharedWorkers',
|
|
'DirectSocketsInServiceWorkers'
|
|
])
|
|
)
|
|
})
|
|
|
|
it('preserves existing disabled Chromium features', async () => {
|
|
const { app } = await import('electron')
|
|
const { disableUnsupportedChromiumFeatures } = await import('./configure-process')
|
|
|
|
vi.mocked(app.commandLine.getSwitchValue).mockReturnValueOnce('ExistingFeature')
|
|
vi.mocked(app.commandLine.appendSwitch).mockClear()
|
|
disableUnsupportedChromiumFeatures()
|
|
|
|
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith(
|
|
'disable-features',
|
|
`${EXPECTED_DISABLED_FEATURES},ExistingFeature`
|
|
)
|
|
})
|
|
|
|
it('does not duplicate features when disable-features already includes them', async () => {
|
|
const { app } = await import('electron')
|
|
const { disableUnsupportedChromiumFeatures } = await import('./configure-process')
|
|
|
|
vi.mocked(app.commandLine.getSwitchValue).mockReturnValueOnce(
|
|
`${EXPECTED_DISABLED_FEATURES},ExistingFeature`
|
|
)
|
|
vi.mocked(app.commandLine.appendSwitch).mockClear()
|
|
disableUnsupportedChromiumFeatures()
|
|
|
|
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith(
|
|
'disable-features',
|
|
`${EXPECTED_DISABLED_FEATURES},ExistingFeature`
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('enableMainProcessGpuFeatures', () => {
|
|
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
|
|
const originalE2EUserDataDir = process.env.ORCA_E2E_USER_DATA_DIR
|
|
|
|
function setPlatform(platform: NodeJS.Platform): void {
|
|
Object.defineProperty(process, 'platform', {
|
|
configurable: true,
|
|
value: platform
|
|
})
|
|
}
|
|
|
|
afterEach(() => {
|
|
if (originalPlatform) {
|
|
Object.defineProperty(process, 'platform', originalPlatform)
|
|
}
|
|
if (originalE2EUserDataDir === undefined) {
|
|
delete process.env.ORCA_E2E_USER_DATA_DIR
|
|
} else {
|
|
process.env.ORCA_E2E_USER_DATA_DIR = originalE2EUserDataDir
|
|
}
|
|
})
|
|
|
|
it('appends VS Code-style GPU channel flags without unsafe WebGPU/Vulkan opt-ins', async () => {
|
|
const { app } = await import('electron')
|
|
const { enableMainProcessGpuFeatures } = await import('./configure-process')
|
|
|
|
delete process.env.ORCA_E2E_USER_DATA_DIR
|
|
vi.mocked(app.commandLine.appendSwitch).mockClear()
|
|
enableMainProcessGpuFeatures()
|
|
|
|
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith(
|
|
'enable-features',
|
|
'EarlyEstablishGpuChannel,EstablishGpuChannelAsync'
|
|
)
|
|
expect(app.commandLine.appendSwitch).not.toHaveBeenCalledWith('enable-unsafe-webgpu')
|
|
})
|
|
|
|
it('raises the WebGL context budget above the 16-context Blink default', async () => {
|
|
const { app } = await import('electron')
|
|
const { enableMainProcessGpuFeatures } = await import('./configure-process')
|
|
|
|
delete process.env.ORCA_E2E_USER_DATA_DIR
|
|
vi.mocked(app.commandLine.appendSwitch).mockClear()
|
|
enableMainProcessGpuFeatures()
|
|
|
|
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith('max-active-webgl-contexts', '128')
|
|
})
|
|
|
|
it('disables Skia Graphite only on macOS without disabling hardware acceleration', async () => {
|
|
const { app } = await import('electron')
|
|
const { enableMainProcessGpuFeatures } = await import('./configure-process')
|
|
|
|
delete process.env.ORCA_E2E_USER_DATA_DIR
|
|
vi.mocked(app.disableHardwareAcceleration).mockClear()
|
|
|
|
for (const platform of ['darwin', 'linux', 'win32'] as const) {
|
|
setPlatform(platform)
|
|
vi.mocked(app.commandLine.appendSwitch).mockClear()
|
|
|
|
enableMainProcessGpuFeatures()
|
|
|
|
if (platform === 'darwin') {
|
|
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith('disable-skia-graphite')
|
|
} else {
|
|
expect(app.commandLine.appendSwitch).not.toHaveBeenCalledWith('disable-skia-graphite')
|
|
}
|
|
}
|
|
|
|
expect(app.disableHardwareAcceleration).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('disables the GPU sandbox on Linux Wayland without disabling acceleration', async () => {
|
|
const { app } = await import('electron')
|
|
const { enableMainProcessGpuFeatures } = await import('./configure-process')
|
|
const originalWaylandDisplay = process.env.WAYLAND_DISPLAY
|
|
|
|
try {
|
|
setPlatform('linux')
|
|
delete process.env.ORCA_E2E_USER_DATA_DIR
|
|
process.env.WAYLAND_DISPLAY = 'wayland-1'
|
|
vi.mocked(app.disableHardwareAcceleration).mockClear()
|
|
vi.mocked(app.commandLine.appendSwitch).mockClear()
|
|
|
|
enableMainProcessGpuFeatures()
|
|
} finally {
|
|
if (originalWaylandDisplay === undefined) {
|
|
delete process.env.WAYLAND_DISPLAY
|
|
} else {
|
|
process.env.WAYLAND_DISPLAY = originalWaylandDisplay
|
|
}
|
|
}
|
|
|
|
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith('disable-gpu-sandbox')
|
|
expect(app.disableHardwareAcceleration).not.toHaveBeenCalled()
|
|
expect(app.commandLine.appendSwitch).not.toHaveBeenCalledWith(
|
|
'enable-features',
|
|
expect.stringContaining('EarlyEstablishGpuChannel')
|
|
)
|
|
expect(app.commandLine.appendSwitch).not.toHaveBeenCalledWith(
|
|
'enable-features',
|
|
expect.stringContaining('EstablishGpuChannelAsync')
|
|
)
|
|
})
|
|
|
|
it('uses Electron Ozone hints to recognize forced Linux Wayland launches', async () => {
|
|
const { app } = await import('electron')
|
|
const { enableMainProcessGpuFeatures } = await import('./configure-process')
|
|
|
|
setPlatform('linux')
|
|
delete process.env.ORCA_E2E_USER_DATA_DIR
|
|
vi.mocked(app.commandLine.appendSwitch).mockClear()
|
|
vi.mocked(app.commandLine.getSwitchValue).mockImplementation((switchName: string) =>
|
|
switchName === 'ozone-platform' ? 'wayland' : ''
|
|
)
|
|
|
|
enableMainProcessGpuFeatures()
|
|
|
|
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith('disable-gpu-sandbox')
|
|
expect(app.commandLine.appendSwitch).not.toHaveBeenCalledWith(
|
|
'enable-features',
|
|
expect.stringContaining('EarlyEstablishGpuChannel')
|
|
)
|
|
})
|
|
|
|
it('honors explicit Linux X11 Ozone overrides even when Wayland env vars are present', async () => {
|
|
const { app } = await import('electron')
|
|
const { enableMainProcessGpuFeatures } = await import('./configure-process')
|
|
const originalWaylandDisplay = process.env.WAYLAND_DISPLAY
|
|
const originalSessionType = process.env.XDG_SESSION_TYPE
|
|
|
|
try {
|
|
setPlatform('linux')
|
|
delete process.env.ORCA_E2E_USER_DATA_DIR
|
|
process.env.WAYLAND_DISPLAY = 'wayland-1'
|
|
process.env.XDG_SESSION_TYPE = 'wayland'
|
|
vi.mocked(app.commandLine.appendSwitch).mockClear()
|
|
vi.mocked(app.commandLine.getSwitchValue).mockImplementation((switchName: string) =>
|
|
switchName === 'ozone-platform' ? 'x11' : ''
|
|
)
|
|
|
|
enableMainProcessGpuFeatures()
|
|
} finally {
|
|
if (originalWaylandDisplay === undefined) {
|
|
delete process.env.WAYLAND_DISPLAY
|
|
} else {
|
|
process.env.WAYLAND_DISPLAY = originalWaylandDisplay
|
|
}
|
|
if (originalSessionType === undefined) {
|
|
delete process.env.XDG_SESSION_TYPE
|
|
} else {
|
|
process.env.XDG_SESSION_TYPE = originalSessionType
|
|
}
|
|
}
|
|
|
|
expect(app.commandLine.appendSwitch).not.toHaveBeenCalledWith('disable-gpu-sandbox')
|
|
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith(
|
|
'enable-features',
|
|
'EarlyEstablishGpuChannel,EstablishGpuChannelAsync'
|
|
)
|
|
})
|
|
|
|
it('does not disable the GPU sandbox outside Linux Wayland', async () => {
|
|
const { app } = await import('electron')
|
|
const { enableMainProcessGpuFeatures } = await import('./configure-process')
|
|
const originalWaylandDisplay = process.env.WAYLAND_DISPLAY
|
|
const originalSessionType = process.env.XDG_SESSION_TYPE
|
|
const originalOzoneHint = process.env.ELECTRON_OZONE_PLATFORM_HINT
|
|
|
|
try {
|
|
delete process.env.ORCA_E2E_USER_DATA_DIR
|
|
delete process.env.WAYLAND_DISPLAY
|
|
delete process.env.XDG_SESSION_TYPE
|
|
delete process.env.ELECTRON_OZONE_PLATFORM_HINT
|
|
|
|
for (const platform of ['linux', 'darwin', 'win32'] as const) {
|
|
setPlatform(platform)
|
|
vi.mocked(app.commandLine.appendSwitch).mockClear()
|
|
vi.mocked(app.commandLine.getSwitchValue).mockImplementation((switchName: string) =>
|
|
switchName === 'enable-features' ? '' : ''
|
|
)
|
|
|
|
enableMainProcessGpuFeatures()
|
|
|
|
expect(app.commandLine.appendSwitch).not.toHaveBeenCalledWith('disable-gpu-sandbox')
|
|
}
|
|
} finally {
|
|
if (originalWaylandDisplay === undefined) {
|
|
delete process.env.WAYLAND_DISPLAY
|
|
} else {
|
|
process.env.WAYLAND_DISPLAY = originalWaylandDisplay
|
|
}
|
|
if (originalSessionType === undefined) {
|
|
delete process.env.XDG_SESSION_TYPE
|
|
} else {
|
|
process.env.XDG_SESSION_TYPE = originalSessionType
|
|
}
|
|
if (originalOzoneHint === undefined) {
|
|
delete process.env.ELECTRON_OZONE_PLATFORM_HINT
|
|
} else {
|
|
process.env.ELECTRON_OZONE_PLATFORM_HINT = originalOzoneHint
|
|
}
|
|
}
|
|
})
|
|
|
|
it('disables the GPU process for Linux E2E runs', async () => {
|
|
const { app } = await import('electron')
|
|
const { enableMainProcessGpuFeatures } = await import('./configure-process')
|
|
|
|
setPlatform('linux')
|
|
process.env.ORCA_E2E_USER_DATA_DIR = '/tmp/orca-e2e'
|
|
vi.mocked(app.disableHardwareAcceleration).mockClear()
|
|
vi.mocked(app.commandLine.appendSwitch).mockClear()
|
|
|
|
enableMainProcessGpuFeatures()
|
|
|
|
expect(app.disableHardwareAcceleration).toHaveBeenCalledTimes(1)
|
|
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith('disable-gpu')
|
|
expect(app.commandLine.appendSwitch).not.toHaveBeenCalledWith(
|
|
'enable-features',
|
|
expect.any(String)
|
|
)
|
|
expect(app.commandLine.appendSwitch).not.toHaveBeenCalledWith(
|
|
'max-active-webgl-contexts',
|
|
expect.any(String)
|
|
)
|
|
})
|
|
|
|
it('preserves existing enable-features switches', async () => {
|
|
const { app } = await import('electron')
|
|
const { enableMainProcessGpuFeatures } = await import('./configure-process')
|
|
|
|
delete process.env.ORCA_E2E_USER_DATA_DIR
|
|
vi.mocked(app.commandLine.appendSwitch).mockClear()
|
|
vi.mocked(app.commandLine.getSwitchValue).mockReturnValue('ExistingFeature')
|
|
enableMainProcessGpuFeatures()
|
|
|
|
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith(
|
|
'enable-features',
|
|
'EarlyEstablishGpuChannel,EstablishGpuChannelAsync,ExistingFeature'
|
|
)
|
|
})
|
|
|
|
it('preserves existing enable-features switches on Linux Wayland without eager GPU channel flags', async () => {
|
|
const { app } = await import('electron')
|
|
const { enableMainProcessGpuFeatures } = await import('./configure-process')
|
|
const originalWaylandDisplay = process.env.WAYLAND_DISPLAY
|
|
|
|
try {
|
|
setPlatform('linux')
|
|
delete process.env.ORCA_E2E_USER_DATA_DIR
|
|
process.env.WAYLAND_DISPLAY = 'wayland-1'
|
|
vi.mocked(app.commandLine.appendSwitch).mockClear()
|
|
vi.mocked(app.commandLine.getSwitchValue).mockImplementation((switchName: string) =>
|
|
switchName === 'enable-features' ? 'ExistingFeature' : ''
|
|
)
|
|
|
|
enableMainProcessGpuFeatures()
|
|
} finally {
|
|
if (originalWaylandDisplay === undefined) {
|
|
delete process.env.WAYLAND_DISPLAY
|
|
} else {
|
|
process.env.WAYLAND_DISPLAY = originalWaylandDisplay
|
|
}
|
|
}
|
|
|
|
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith('disable-gpu-sandbox')
|
|
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith('enable-features', 'ExistingFeature')
|
|
})
|
|
})
|
|
|
|
describe('safe graphics mode startup switches', () => {
|
|
const originalE2EUserDataDir = process.env.ORCA_E2E_USER_DATA_DIR
|
|
|
|
afterEach(() => {
|
|
if (originalE2EUserDataDir === undefined) {
|
|
delete process.env.ORCA_E2E_USER_DATA_DIR
|
|
} else {
|
|
process.env.ORCA_E2E_USER_DATA_DIR = originalE2EUserDataDir
|
|
}
|
|
})
|
|
|
|
function disabledFeaturesFrom(appendSwitch: ReturnType<typeof vi.fn>): string[] {
|
|
return appendSwitch.mock.calls
|
|
.filter(([name]) => name === 'disable-features')
|
|
.flatMap(([, value]) => String(value ?? '').split(','))
|
|
.filter(Boolean)
|
|
}
|
|
|
|
it('opts hidden pages out of intensive wake-up throttling', async () => {
|
|
const { app } = await import('electron')
|
|
const { optOutOfHiddenPageWakeUpThrottling } = await import('./configure-process')
|
|
|
|
vi.mocked(app.commandLine.appendSwitch).mockClear()
|
|
optOutOfHiddenPageWakeUpThrottling()
|
|
|
|
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith(
|
|
'disable-features',
|
|
'IntensiveWakeUpThrottling'
|
|
)
|
|
})
|
|
|
|
// Why: Chromium enables IntensiveWakeUpThrottling on every desktop platform, so the opt-out
|
|
// must never become reachable only through the GPU-feature path again.
|
|
it('does not couple the throttling opt-out to GPU feature setup', async () => {
|
|
const { app } = await import('electron')
|
|
const { enableMainProcessGpuFeatures } = await import('./configure-process')
|
|
|
|
delete process.env.ORCA_E2E_USER_DATA_DIR
|
|
vi.mocked(app.commandLine.appendSwitch).mockClear()
|
|
enableMainProcessGpuFeatures()
|
|
|
|
expect(disabledFeaturesFrom(vi.mocked(app.commandLine.appendSwitch))).not.toContain(
|
|
'IntensiveWakeUpThrottling'
|
|
)
|
|
})
|
|
})
|