Files
orca/tests/e2e/ssh-orcad-ssh-failure-reason.spec.ts
T
Neil b67429c03f fix(ssh): lead an SSH connect failure with what to do about it (#33990)
* fix(ssh): say why SSH failed when Connect never reaches the host

When the SSH session itself could not open (key rejected, port refused,
host not answering, host key not trusted), Connect on a host Orca had not
set up yet said "Orca couldn't start its server on this host" and offered
a plain SSH terminal that could not work either. Errors from opening the
SSH session are now marked where they are dialed, and a setup that fails
on one ends the connect with SSH's own reason. A held move keeps its
status. The connect-failure toast also drops Electron's IPC prefix.

* fix(ssh): lead an SSH connect failure with what to do about it

#33928 shows SSH's own reason when Connect never reaches the host. Raw
reasons like "connect ECONNREFUSED 10.0.0.1:22" or "Timed out while
waiting for SSH authentication" still leave users guessing. A failure
from opening the SSH session is now published with a next step for the
kinds users can act on (host key mismatch, rejected sign-in, refused
port, unknown host name, no route, timeout), and SSH's words follow in
parentheses. The connect-failure toast reads the same published text.
2026-10-26 12:16:04 -07:00

91 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// A Connect whose SSH session never opens names SSH's reason, not a server failure with a plain-SSH fallback.
// ORCA_E2E_ORCAD_CONVERT_HOST=docker; ORCA_E2E_ORCAD_CONVERT_TEMPLATE must be built for the Docker host.
import type { ElectronApplication } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import { waitForSessionReady } from './helpers/store'
import { createRestartSession } from './helpers/orca-restart'
import { openSshHostSettings } from './helpers/ssh-config-host-picker'
import {
ORCAD_CONVERT_HOST_ENV,
startOrcadConvertHost,
type OrcadConvertHost
} from './helpers/orcad-convert-host'
const HOST = process.env[ORCAD_CONVERT_HOST_ENV]
const TEMPLATE_SOURCE = process.env.ORCA_E2E_ORCAD_CONVERT_TEMPLATE
test.skip(
HOST !== 'docker' || !TEMPLATE_SOURCE,
`Set ${ORCAD_CONVERT_HOST_ENV}=docker and ORCA_E2E_ORCAD_CONVERT_TEMPLATE`
)
let host: OrcadConvertHost | null = null
test.afterEach(() => {
host?.cleanup()
host = null
})
const CASES = [
{
name: 'a refused SSH port',
reason: /ECONNREFUSED/,
nextStep: 'Nothing accepted the SSH connection at this address',
refused: true
},
{
name: 'a key the host rejects',
reason: /authentication methods failed/i,
nextStep: 'The host didn’t accept any sign-in Orca offered',
refused: false
}
] as const
for (const fault of CASES) {
// oxlint-disable-next-line no-empty-pattern -- Playwright's first fixture arg must be an object pattern; this test owns its app launch.
test(`Connect to ${fault.name} says why SSH failed`, async ({}, testInfo) => {
// Starts a Docker host and an app; a rejected key also waits on its password prompt.
test.setTimeout(6 * 60_000)
host = startOrcadConvertHost('docker', testInfo)
if (!fault.refused) {
host.exec!(': > /root/.ssh/authorized_keys')
}
const input = { ...host.input, ...(fault.refused ? { port: 1 } : {}) }
// No agent, so the only offered key is the one the host rejects.
const session = createRestartSession(testInfo, {
ORCA_ORCAD_TEMPLATE_PATH: TEMPLATE_SOURCE!,
SSH_AUTH_SOCK: ''
})
let app: ElectronApplication | null = null
try {
const launched = await session.launch()
app = launched.app
const page = launched.page
await waitForSessionReady(page)
await page.evaluate((target) => window.api.ssh.addTarget({ target }), input)
const section = await openSshHostSettings(page)
const card = section.locator(`[data-ssh-target-card][data-ssh-target-label="${input.label}"]`)
await card.hover()
await card.getByRole('button', { name: 'Connect', exact: true }).click()
if (!fault.refused) {
// The rejected key falls back to a password prompt; the user dismisses it.
const prompt = page.getByRole('dialog', { name: 'SSH Password' })
await expect(prompt).toBeVisible({ timeout: 60_000 })
await prompt.getByRole('button', { name: 'Cancel' }).click()
}
await expect(card).toContainText(fault.reason, { timeout: 2 * 60_000 })
await page.screenshot({ path: testInfo.outputPath('card.png') })
await expect(card).toContainText(fault.nextStep)
await expect(card).not.toContainText('couldn’t start its server')
await expect(card.getByRole('button', { name: 'Open plain SSH terminal' })).toHaveCount(0)
const toasts = page.locator('[data-sonner-toast]')
await expect(toasts.filter({ hasText: 'Error invoking remote method' })).toHaveCount(0)
await expect(toasts.filter({ hasText: 'couldn’t start its server' })).toHaveCount(0)
} finally {
if (app) {
await session.close(app)
}
await session.dispose()
}
})
}