Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 13 additions & 46 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -557,51 +557,13 @@ jobs:
retention-days: 7
if-no-files-found: ignore

# A profile-less Windows session has no roaming AppData; Electron 43 used to crash natively there.
windows-missing-appdata-startup:
name: orca serve starts without AppData on Windows
if: inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/windows-missing-appdata-startup.spec.ts')
runs-on: windows-2022
timeout-minutes: 30
env:
NODE_OPTIONS: --max-old-space-size=4096
ORCA_BACKGROUND_LAUNCH: '1'
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
with:
native-runtime: electron
- name: Build the e2e app and CLI
shell: bash
run: |
pnpm exec electron-vite build --mode e2e
pnpm run build:cli
- name: Start serve with no AppData folder
env:
SKIP_BUILD: '1'
ORCA_E2E_FORWARD_APP_LOGS: '1'
ORCA_STARTUP_DIAGNOSTICS: '1'
ELECTRON_ENABLE_LOGGING: '1'
ELECTRON_ENABLE_STACK_DUMPING: '1'
run: pnpm exec playwright test --config tests/playwright.config.ts tests/e2e/windows-missing-appdata-startup.spec.ts --project=electron-headless --workers=1
- uses: actions/upload-artifact@v7
if: failure()
with:
name: windows-missing-appdata-startup-traces
path: test-results/
retention-days: 7
if-no-files-found: ignore

# #24979 on a real host: a relay-era Docker host converts to managed orcad on connect, and a managed
# orcad idles out and restarts on the next connect. Needs the
# orcad template for the fixture's target (Debian, linux-x64-glibc), which only this job builds.
orcad-auto-convert-docker:
name: ssh host auto-converts to managed orcad (Docker)
needs: [build, prepare-native-cache]
if: inputs.test_files == '' || inputs.ssh_source_changed == 'true' || contains(inputs.test_files, 'tests/e2e/ssh-orcad-auto-convert.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-idle-exit.spec.ts')
if: inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/ssh-orcad-auto-convert.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-idle-exit.spec.ts')
runs-on: ubuntu-latest
timeout-minutes: 45
env:
Expand Down Expand Up @@ -647,10 +609,11 @@ jobs:
if-no-files-found: ignore

# D7 on Windows: both hosts share <userData>\daemon and so one daemon pipe. Built here rather than
# from the Linux e2e build because the slot, template and addons are Windows-native.
# from the Linux e2e build because the slot, template and addons are Windows-native. Also runs the
# missing-AppData startup check, which needs the same Windows e2e build.
orcad-serve-mode-switch-windows:
name: orca serve Electron/orcad mode switch on Windows (D7)
if: inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/orcad-serve-mode-switch.spec.ts')
name: orca serve on Windows (D7 mode switch, missing AppData)
if: inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/orcad-serve-mode-switch.spec.ts') || contains(inputs.test_files, 'tests/e2e/windows-missing-appdata-startup.spec.ts')
runs-on: windows-2022
timeout-minutes: 45
env:
Expand All @@ -677,16 +640,20 @@ jobs:
pnpm build:orcad
node config/scripts/build-windows-process-tree-relay-addon.mjs --arch=x64
node config/scripts/build-orcad-template.mjs --targets win32-x64
# A profile-less Windows session has no roaming AppData; Electron 43 used to crash natively there.
- name: Start serve with no AppData folder
if: inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/windows-missing-appdata-startup.spec.ts')
env:
SKIP_BUILD: '1'
ORCA_E2E_FORWARD_APP_LOGS: '1'
run: pnpm exec playwright test --config tests/playwright.config.ts tests/e2e/windows-missing-appdata-startup.spec.ts --project=electron-headless --workers=1
- name: Switch serve hosts on one profile
if: ${{ !cancelled() && (inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/orcad-serve-mode-switch.spec.ts')) }}
env:
SKIP_BUILD: '1'
ORCA_E2E_ORCAD_SERVE: '1'
ORCA_E2E_FORWARD_APP_LOGS: '1'
ORCA_E2E_PRESERVE_PROFILE_LOGS_DIR: ${{ github.workspace }}\test-results\profile-logs
# A launch that dies before its window prints this trail into Playwright's call log.
ORCA_STARTUP_DIAGNOSTICS: '1'
ELECTRON_ENABLE_LOGGING: '1'
ELECTRON_ENABLE_STACK_DUMPING: '1'
run: pnpm exec playwright test --config tests/playwright.config.ts tests/e2e/orcad-serve-mode-switch.spec.ts --project=electron-headless --workers=1
- uses: actions/upload-artifact@v7
if: failure()
Expand Down
4 changes: 3 additions & 1 deletion config/scripts/ci-e2e-job-selection.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ export function classifyE2eJobs(input, sshSourceChanged = 'false') {
(spec) =>
dockerSpecs.has(spec) ||
spec === LOCALHOST_SSH_E2E_SPEC ||
spec === ORCAD_SERVE_MODE_SWITCH_E2E_SPEC
spec === ORCAD_SERVE_MODE_SWITCH_E2E_SPEC ||
spec === ORCAD_AUTO_CONVERT_E2E_SPEC ||
spec === ORCAD_IDLE_EXIT_E2E_SPEC
)
}
}
Expand Down
102 changes: 102 additions & 0 deletions config/scripts/orcad-e2e-routing.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { existsSync, readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { expect, it } from 'vitest'
import { parse } from 'yaml'
import {
classifyE2eJobs,
ORCAD_AUTO_CONVERT_E2E_SPEC,
ORCAD_IDLE_EXIT_E2E_SPEC,
ORCAD_SERVE_MODE_SWITCH_E2E_SPEC,
WINDOWS_MISSING_APPDATA_E2E_SPEC
} from './ci-e2e-job-selection.mjs'
import { selectPrE2eSpecs, shouldRunReusablePrE2e } from './pr-e2e-source-routing.mjs'

const root = resolve(import.meta.dirname, '../..')
const jobs = parse(readFileSync(resolve(root, '.github/workflows/e2e.yml'), 'utf8')).jobs

function expectRouted(files, spec) {
for (const file of files) {
expect(existsSync(resolve(root, file)), file).toBe(true)
expect(selectPrE2eSpecs([file]), file).toContain(spec)
expect(shouldRunReusablePrE2e([file]), file).toBe(true)
}
}

it('routes the mode-switch spec from its serve sources and harness', () => {
expectRouted(
[
'src/main/orcad/orcad-lifecycle.ts',
'src/main/daemon/daemon-spawner.ts',
'tests/e2e/helpers/orca-serve-cli-host.ts',
'tests/e2e/helpers/headless-paired-runtime-host.ts'
],
ORCAD_SERVE_MODE_SWITCH_E2E_SPEC
)
})

it('routes the missing-AppData spec from its startup sources and harness', () => {
expectRouted(
['src/main/startup/windows-app-data-path.ts', 'tests/e2e/helpers/orca-serve-cli-host.ts'],
WINDOWS_MISSING_APPDATA_E2E_SPEC
)
})

it('routes the auto-convert spec from its conversion sources and harness', () => {
expectRouted(
[
'src/main/ssh/orcad-runtime-conversion.ts',
'tests/e2e/helpers/orcad-convert-flow.ts',
'tests/e2e/helpers/orcad-convert-host.ts',
'tests/e2e/helpers/orcad-template-variant.ts',
'tests/e2e/helpers/orcad-upgrade-profile.ts'
],
ORCAD_AUTO_CONVERT_E2E_SPEC
)
expect(selectPrE2eSpecs(['src/main/ssh/orcad-runtime-conversion.test.ts'])).not.toContain(
ORCAD_AUTO_CONVERT_E2E_SPEC
)
})

it('routes the idle-exit spec from its idle sources and the shared convert harness', () => {
expectRouted(
[
'src/shared/orcad-idle-exit.ts',
'tests/e2e/helpers/orcad-convert-flow.ts',
'tests/e2e/helpers/orcad-convert-host.ts'
],
ORCAD_IDLE_EXIT_E2E_SPEC
)
})

it('builds the e2e app when only a build-dependent orcad spec is requested', () => {
for (const spec of [
ORCAD_SERVE_MODE_SWITCH_E2E_SPEC,
ORCAD_AUTO_CONVERT_E2E_SPEC,
ORCAD_IDLE_EXIT_E2E_SPEC
]) {
expect(classifyE2eJobs(JSON.stringify([spec])), spec).toEqual({
e2e_run_changed: false,
e2e_needs_build: true
})
}
expect(jobs['orcad-auto-convert-docker'].needs).toEqual(['build', 'prepare-native-cache'])
})

it('runs the auto-convert lane only when routed, not on every SSH source change', () => {
const condition = jobs['orcad-auto-convert-docker'].if
for (const spec of [ORCAD_AUTO_CONVERT_E2E_SPEC, ORCAD_IDLE_EXIT_E2E_SPEC]) {
expect(condition).toContain(`contains(inputs.test_files, '${spec}')`)
}
expect(condition).not.toContain('ssh_source_changed')
})

it('runs both Windows serve specs on one runner, each only when requested', () => {
expect(jobs['windows-missing-appdata-startup']).toBeUndefined()
const job = jobs['orcad-serve-mode-switch-windows']
for (const spec of [ORCAD_SERVE_MODE_SWITCH_E2E_SPEC, WINDOWS_MISSING_APPDATA_E2E_SPEC]) {
expect(job.if, spec).toContain(`contains(inputs.test_files, '${spec}')`)
const step = job.steps.find((candidate) => candidate.run?.includes(spec))
expect(step.if, spec).toContain(`contains(inputs.test_files, '${spec}')`)
expect(step.env.ORCA_STARTUP_DIAGNOSTICS, spec).toBeUndefined()
}
})
32 changes: 20 additions & 12 deletions config/scripts/pr-e2e-source-routing.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,43 @@ export const PR_E2E_SOURCE_ROUTES = [
id: 'serve.orcad-mode-switch',
specs: ['tests/e2e/orcad-serve-mode-switch.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^src\/(?:cli\/runtime\/(?:launch|serve-)|main\/orcad\/(?:main|orcad-entry|orcad-instance-lock|orcad-command-arguments|orcad-lifecycle)\.ts$|main\/startup\/desktop-profile-instance-lock\.ts$|main\/daemon\/daemon-(?:spawner|endpoint-adoption|init)|main\/server\/serve-)/.test(
/^tests\/e2e\/helpers\/(?:orca-serve-cli-host|headless-paired-runtime-host)\.ts$/.test(
file
)
) ||
(isProductSource(file) &&
/^src\/(?:cli\/runtime\/(?:launch|serve-)|main\/orcad\/(?:main|orcad-entry|orcad-instance-lock|orcad-command-arguments|orcad-lifecycle)\.ts$|main\/startup\/desktop-profile-instance-lock\.ts$|main\/daemon\/daemon-(?:spawner|endpoint-adoption|init)|main\/server\/serve-)/.test(
file
))
},
{
id: 'startup.windows-missing-appdata',
specs: ['tests/e2e/windows-missing-appdata-startup.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^src\/main\/startup\/(?:windows-app-data-path|main-process-preflight)\.ts$/.test(file)
file === 'tests/e2e/helpers/orca-serve-cli-host.ts' ||
(isProductSource(file) &&
/^src\/main\/startup\/(?:windows-app-data-path|main-process-preflight)\.ts$/.test(file))
},
{
id: 'ssh.orcad-auto-convert',
specs: ['tests/e2e/ssh-orcad-auto-convert.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^src\/main\/(?:ipc\/ssh-host-server-|ssh\/(?:ssh-host-server-|orcad-runtime-conversion|orcad-migration-|orcad-retained-source|orcad-runtime-deployment))/.test(
/^tests\/e2e\/helpers\/(?:orcad-convert-(?:flow|host)|orcad-template-variant|orcad-upgrade-profile)\.ts$/.test(
file
)
) ||
(isProductSource(file) &&
/^src\/main\/(?:ipc\/ssh-host-server-|ssh\/(?:ssh-host-server-|orcad-runtime-conversion|orcad-migration-|orcad-retained-source|orcad-runtime-deployment))/.test(
file
))
},
{
id: 'ssh.orcad-idle-exit',
specs: ['tests/e2e/ssh-orcad-idle-exit.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^src\/(?:main\/(?:orcad\/orcad-(?:idle-|managed-idle-)|ssh\/orcad-(?:managed-wake|managed-tunnel|recovery-slot|remote-launch))|shared\/orcad-idle-exit)/.test(
file
)
/^tests\/e2e\/helpers\/orcad-convert-(?:flow|host)\.ts$/.test(file) ||
(isProductSource(file) &&
/^src\/(?:main\/(?:orcad\/orcad-(?:idle-|managed-idle-)|ssh\/orcad-(?:managed-wake|managed-tunnel|recovery-slot|remote-launch))|shared\/orcad-idle-exit)/.test(
file
))
},
{
id: 'ssh.localhost-agent-hooks',
Expand Down
1 change: 0 additions & 1 deletion config/scripts/release-cut-token-permissions.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const EXPECTED_MATRIX = {
'.github/workflows/e2e.yml#ssh-browser-network-route': { contents: 'read' },
'.github/workflows/e2e.yml#ssh-localhost': { contents: 'read' },
'.github/workflows/e2e.yml#ssh-docker-watcher-isolation': { contents: 'read' },
'.github/workflows/e2e.yml#windows-missing-appdata-startup': { contents: 'read' },
'.github/workflows/homebrew-bump.yml#bump-cask': { contents: 'read' },
'.github/workflows/node-server-tests.yml#changes': { contents: 'read' },
'.github/workflows/node-server-tests.yml#desktop_template': { contents: 'read' },
Expand Down
3 changes: 2 additions & 1 deletion docs/reference/orcad-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ no template in the install, the pinned Node could not be fetched, or a failed na
app update there, and orcad has no updater. Recipe-JSON serve has no handoff and uses orcad.
- Windows serves on orcad too. Both hosts share `<userData>\daemon`, so the daemon pipe name
(hashed from that path) is the same, and the relocated Electron daemon host changes only the
executable, not the pipe. The `orcad-serve-mode-switch-windows` e2e job gates D7 there.
executable, not the pipe. The `orcad-serve-mode-switch-windows` e2e job checks D7 there, in the
daily run and on PRs routed to it; it does not block merges.

The slot and its pinned Node live under the desktop's `<userData>/orcad-artifacts`.

Expand Down
61 changes: 1 addition & 60 deletions src/main/ipc/orcad-runtime-conversion-handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
handle: vi.fn(),
convert: vi.fn(),
pending: vi.fn(),
preflight: vi.fn(),
manifest: vi.fn(),
terminals: vi.fn(),
inventory: vi.fn()
pending: vi.fn()
}))

vi.mock('electron', () => ({ ipcMain: { handle: mocks.handle } }))
Expand All @@ -17,25 +13,6 @@ vi.mock('../ssh/orcad-runtime-conversion', () => ({
vi.mock('../ssh/orcad-managed-migration-status', () => ({
listPendingManagedOrcadMigrations: mocks.pending
}))
vi.mock('../ssh/orcad-managed-runtime-context', () => ({
requireManagedOrcadInfrastructure: () => ({
targetStore: {
getOrcadMigrationSource: () => ({ getSshTarget: () => ({ id: 'ssh-1', label: 'Builder' }) })
}
})
}))
vi.mock('../ssh/ssh-target-orcad-preflight', () => ({
preflightOrcadMigrationExport: mocks.preflight
}))
vi.mock('../ssh/orcad-migration-manifest-export', () => ({
createOrcadMigrationManifest: mocks.manifest
}))
vi.mock('../ssh/orcad-migration-terminal-gate', () => ({
assessOrcadMigrationTerminals: mocks.terminals
}))
vi.mock('../ssh/orcad-migration-relay-pty-lister', () => ({
orcadMigrationRelayPtyLister: mocks.inventory
}))
vi.mock('../ssh/orcad-runtime-conversion-wiring', () => ({
conversionCollaborators: () => ({ marker: 'live-collaborators' })
}))
Expand All @@ -57,42 +34,6 @@ describe('managed server conversion IPC', () => {
registerOrcadRuntimeConversionHandlers(() => '/profile')
})

it('previews what moves and keeps only blockers that stop the move', async () => {
mocks.preflight.mockReturnValue({
targetLabel: 'Builder',
blockers: [
{ code: 'orcad_migration_direct_ssh_repositories', category: 'drainable-static-state' },
{ code: 'orcad_migration_saved_port_forwards', category: 'client-owned-state' },
{ code: 'orcad_migration_target_owned', category: 'exclusive-ownership' }
]
})
mocks.manifest.mockReturnValue({
payload: {
repositories: [{}, {}],
projectGroups: [{}],
folderWorkspaces: [{}],
dormantState: { automations: [{}], workspaceSession: {} }
}
})
mocks.terminals.mockResolvedValue({ verdict: 'live', ptyIds: ['p'], reason: 'running' })
await expect(
handler('runtimeEnvironments:previewOrcadConversion')(null, { sshTargetId: 'ssh-1' })
).resolves.toEqual({
sshTargetId: 'ssh-1',
targetLabel: 'Builder',
moves: {
repositories: 2,
projectGroups: 1,
folderWorkspaces: 1,
automations: 1,
workspaceSession: true
},
blockers: [{ code: 'orcad_migration_target_owned', category: 'exclusive-ownership' }],
terminals: { verdict: 'live', ptyIds: ['p'], reason: 'running' }
})
expect(mocks.inventory).toHaveBeenCalledWith('ssh-1')
})

it('converts with the live collaborators and lists pending migrations from the profile', async () => {
mocks.convert.mockResolvedValue({ outcome: 'converted' })
await handler('runtimeEnvironments:convertSshHostToManagedOrcad')(null, {
Expand Down
Loading
Loading