diff --git a/tests/e2e/onboarding.spec.ts b/tests/e2e/onboarding.spec.ts index 86ed4b9cec4..0fb5ad870c1 100644 --- a/tests/e2e/onboarding.spec.ts +++ b/tests/e2e/onboarding.spec.ts @@ -12,6 +12,7 @@ import { waitForSessionReady } from './helpers/store' import type { Page } from '@stablyai/playwright-test' import type { GlobalSettings, TuiAgent } from '../../src/shared/types' import { ONBOARDING_FINAL_STEP } from '../../src/shared/constants' +import { encodePairingOffer, PAIRING_OFFER_VERSION } from '../../src/shared/pairing' type OnboardingState = { closedAt: number | null @@ -421,43 +422,38 @@ test.describe('Onboarding flow', () => { await expect(orcaPage.getByRole('heading', { name: /Pick your default agent/i })).toBeVisible({ timeout: 15_000 }) - await orcaPage.evaluate(async () => { + // Why: since #10011 `settings:set` strips activeRuntimeEnvironmentId — the + // durable Active Server preference is only writable through its dedicated + // handler, which resolves the id against the main-process environment + // store. So the host has to be registered for real, not faked in the + // renderer. Pairing is offline (no live server needed). + const pairingCode = encodePairingOffer({ + v: PAIRING_OFFER_VERSION, + scope: 'runtime', + endpoint: 'wss://e2e.invalid/ws', + deviceToken: 'e2e-device-token', + publicKeyB64: 'ZTJlLXB1YmxpYy1rZXk' + }) + const environmentId = await orcaPage.evaluate(async (code) => { const store = window.__store if (!store) { throw new Error('window.__store is not available') } + const { environment } = await window.api.runtimeEnvironments.addFromPairingCode({ + name: 'E2E Server', + pairingCode: code + }) // Why: after #5071 the server-path add step gates on the registered // runtime-environment list (store.runtimeEnvironments), not just the - // activeRuntimeEnvironmentId setting. Seed a redacted environment so the - // host option exists and the "on host" add UI renders. - const now = Date.now() - store.getState().setRuntimeEnvironments([ - { - id: 'env-e2e', - name: 'E2E Server', - createdAt: now, - updatedAt: now, - lastUsedAt: null, - runtimeId: null, - source: 'manual', - endpoints: [ - { - id: 'ws-env-e2e', - kind: 'websocket', - label: 'WebSocket', - endpoint: 'wss://e2e.invalid/ws' - } - ], - preferredEndpointId: 'ws-env-e2e' - } - ]) + // activeRuntimeEnvironmentId setting. + store.getState().setRuntimeEnvironments(await window.api.runtimeEnvironments.list()) // Why: a runtime host is only auto-selectable (health 'available') when it // has a live, protocol-compatible status; without one it reads // 'disconnected' and the Add Project dialog falls back to Local Mac. // runtimeProtocolVersion 3 clears MIN_COMPATIBLE_RUNTIME_SERVER_VERSION. - store.getState().setRuntimeEnvironmentStatus('env-e2e', { + store.getState().setRuntimeEnvironmentStatus(environment.id, { status: { - runtimeId: 'env-e2e-runtime', + runtimeId: `${environment.id}-runtime`, rendererGraphEpoch: 0, graphStatus: 'ready', authoritativeWindowId: null, @@ -466,15 +462,23 @@ test.describe('Onboarding flow', () => { runtimeProtocolVersion: 3, minCompatibleRuntimeClientVersion: 1 }, - checkedAt: now + checkedAt: Date.now() }) - await store.getState().updateSettings({ activeRuntimeEnvironmentId: 'env-e2e' }) - }) + // Why: the store's switchRuntimeEnvironment probes reachability, which a + // synthetic host can't satisfy — write the preference directly and push + // the returned settings in rather than refetching (fetchSettings would + // kick off a status hydrate that clobbers the seeded 'available' health). + const settings = await window.api.settings.setActiveRuntimeEnvironmentPreference({ + environmentId: environment.id + }) + store.setState({ settings }) + return environment.id + }, pairingCode) await expect .poll(async () => (await getSettings(orcaPage)).activeRuntimeEnvironmentId, { timeout: 5_000 }) - .toBe('env-e2e') + .toBe(environmentId) await onboardingFooterButton(orcaPage, SKIP_TO_PROJECT_SETUP_BUTTON).click() diff --git a/tests/e2e/terminal-tab-switch-visual-restore.spec.ts b/tests/e2e/terminal-tab-switch-visual-restore.spec.ts index 36258bd3b96..45fc01a600a 100644 --- a/tests/e2e/terminal-tab-switch-visual-restore.spec.ts +++ b/tests/e2e/terminal-tab-switch-visual-restore.spec.ts @@ -289,6 +289,35 @@ async function injectPaneData( } } +function buildAltScreenFrame(marker: string, frame: number): string { + const progress = `${'█'.repeat((frame % 8) + 1)}${'░'.repeat(8 - ((frame % 8) + 1))}` + return [ + '\x1b[?2026h', + '\x1b[?1049h', + '\x1b[2J\x1b[H', + '\x1b[?25l', + `╭────────────────────────────────────────────────────────────────────╮`, + `│ ${marker} frame ${String(frame).padStart(3, '0')} ${progress} │`, + `│ Dimension │ Rating │`, + `╰────────────────────────────────────────────────────────────────────╯`, + '\x1b[?2026l' + ].join('\r\n') +} + +async function writeToPaneTerminal(page: Page, tabId: string, data: string): Promise { + await page.evaluate( + ({ tabId, data }) => { + const manager = window.__paneManagers?.get(tabId) + const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] + if (!pane) { + throw new Error(`No terminal pane for tab ${tabId}`) + } + return new Promise((resolve) => pane.terminal.write(data, resolve)) + }, + { tabId, data } + ) +} + async function setHiddenSnapshotOverride( page: Page, ptyId: string, @@ -611,63 +640,28 @@ test.describe('Terminal tab switch visual restore', () => { const { firstTabId, secondTabId } = await ensureTwoTerminalTabs(orcaPage) await forceWebglOnActiveTab(orcaPage) + await waitForPanePtyIdOnTab(orcaPage, firstTabId) const runId = `${Date.now()}` const finalMarker = `${TAB_SWITCH_MARKER_PREFIX}_${runId}_ALT_24` - await orcaPage.evaluate( - ({ tabId, finalMarker }) => { - const manager = window.__paneManagers?.get(tabId) - const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] - if (!pane) { - throw new Error(`No terminal pane for tab ${tabId}`) - } - const frames = Array.from({ length: 25 }, (_, frame) => { - const progress = `${'█'.repeat((frame % 8) + 1)}${'░'.repeat(8 - ((frame % 8) + 1))}` - return [ - '\x1b[?2026h', - '\x1b[?1049h', - '\x1b[2J\x1b[H', - '\x1b[?25l', - `╭────────────────────────────────────────────────────────────────────╮`, - `│ ${finalMarker} frame ${String(frame).padStart(3, '0')} ${progress} │`, - `│ Dimension │ Rating │`, - `╰────────────────────────────────────────────────────────────────────╯`, - '\x1b[?2026l' - ].join('\r\n') - }).join('') - return new Promise((resolve) => pane.terminal.write(frames, resolve)) - }, - { tabId: firstTabId, finalMarker } + await writeToPaneTerminal( + orcaPage, + firstTabId, + Array.from({ length: 25 }, (_, frame) => buildAltScreenFrame(finalMarker, frame)).join('') ) const corruptionReports: string[] = [] for (let cycle = 0; cycle < 6; cycle += 1) { + const redraw = buildAltScreenFrame(finalMarker, cycle * 4) + // Why: this frame never transits the PTY, so a reveal restore would + // repaint main's model over it. Publish it as the snapshot too, so both + // the live-write and restore paths land the same frame. Identity is + // re-read per cycle because a reattach would re-key the override. + const { ptyId, cols, rows } = await readPaneIdentityOnTab(orcaPage, firstTabId) + await setHiddenSnapshotOverride(orcaPage, ptyId, { data: redraw, cols, rows }) await activateTerminalTab(orcaPage, secondTabId) - await orcaPage.evaluate( - ({ tabId, finalMarker, cycle }) => { - const manager = window.__paneManagers?.get(tabId) - const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] - if (!pane) { - throw new Error(`No terminal pane for tab ${tabId}`) - } - const frame = cycle * 4 - const progress = `${'█'.repeat((frame % 8) + 1)}${'░'.repeat(8 - ((frame % 8) + 1))}` - const redraw = [ - '\x1b[?2026h', - '\x1b[?1049h', - '\x1b[2J\x1b[H', - '\x1b[?25l', - `╭────────────────────────────────────────────────────────────────────╮`, - `│ ${finalMarker} frame ${String(frame).padStart(3, '0')} ${progress} │`, - `│ Dimension │ Rating │`, - `╰────────────────────────────────────────────────────────────────────╯`, - '\x1b[?2026l' - ].join('\r\n') - return new Promise((resolve) => pane.terminal.write(redraw, resolve)) - }, - { tabId: firstTabId, finalMarker, cycle } - ) + await writeToPaneTerminal(orcaPage, firstTabId, redraw) await activateTerminalTab(orcaPage, firstTabId) const geometry = await readTabTerminalGeometry(orcaPage, firstTabId, `${runId}_ALT`)