test(e2e): one managed-host setup path and skip gate for the ssh-orcad specs (#27465)

Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
OrcaWin
2026-10-11 05:35:50 -07:00
committed by GitHub
co-authored by m4air
parent 10d5108671
commit c9a9ad33b8
18 changed files with 359 additions and 637 deletions
+3 -3
View File
@@ -144,7 +144,7 @@ export const PR_E2E_SOURCE_ROUTES = [
id: 'ssh.orcad-browser-drop-owner',
specs: ['tests/e2e/ssh-orcad-browser-drop-owner.spec.ts'],
matches: (file) =>
/^tests\/e2e\/helpers\/(?:orcad-convert-(?:flow|host)|orcad-upgrade-profile|docker-ssh-host-target|browser-split-(?:guest-probes|page-server))\.ts$/.test(
/^tests\/e2e\/helpers\/(?:orcad-managed-workspace|orcad-convert-(?:flow|host)|orcad-upgrade-profile|docker-ssh-host-target|browser-split-(?:guest-probes|page-server))\.ts$/.test(
file
) ||
(isProductSource(file) &&
@@ -156,7 +156,7 @@ export const PR_E2E_SOURCE_ROUTES = [
id: 'ssh.orcad-open-in-owner',
specs: ['tests/e2e/ssh-orcad-open-in-owner.spec.ts'],
matches: (file) =>
/^tests\/e2e\/helpers\/(?:orcad-convert-(?:flow|host)|orcad-upgrade-profile|docker-ssh-host-target)\.ts$/.test(
/^tests\/e2e\/helpers\/(?:orcad-managed-workspace|orcad-convert-(?:flow|host)|orcad-upgrade-profile|docker-ssh-host-target)\.ts$/.test(
file
) ||
(isProductSource(file) &&
@@ -168,7 +168,7 @@ export const PR_E2E_SOURCE_ROUTES = [
id: 'ssh.orcad-terminal-root-owner',
specs: ['tests/e2e/ssh-orcad-terminal-root-owner.spec.ts'],
matches: (file) =>
/^tests\/e2e\/helpers\/(?:terminal-workspace-root-link|orcad-convert-(?:flow|host)|orcad-upgrade-profile|docker-ssh-host-target)\.ts$/.test(
/^tests\/e2e\/helpers\/(?:terminal-workspace-root-link|orcad-managed-workspace|orcad-convert-(?:flow|host)|orcad-upgrade-profile|docker-ssh-host-target)\.ts$/.test(
file
) ||
(isProductSource(file) &&
+59 -8
View File
@@ -4,19 +4,27 @@
* managed-host journey specs share, driven through the tab bar and the keyboard.
*/
import type { ElectronApplication, Page, TestInfo } from '@stablyai/playwright-test'
import { expect } from './orca-app'
import { expect, test } from './orca-app'
import { createRestartSession } from './orca-restart'
import { startOrcadConvertHost, type OrcadConvertHost } from './orcad-convert-host'
import { seedRelayEraProfile, type RelayEraProfile } from './orcad-upgrade-profile'
import { convertAndRetain } from './orcad-convert-flow'
import { readPairedPaneContent } from './paired-host-terminal'
import { waitForSessionReady } from './store'
import { waitForSessionReady, waitForStartupWorktreeRefresh } from './store'
import { dismissTransientAnnouncement } from './ssh-config-host-picker'
import { focusActiveTerminalInput } from './terminal'
import { toRuntimeExecutionHostId } from '../../../src/shared/execution-host'
export const ORCAD_TEMPLATE = process.env.ORCA_E2E_ORCAD_CONVERT_TEMPLATE
/** Skips the calling spec file unless the Docker SSH host and the orcad template are available. */
export function skipUnlessManagedDockerHost(): void {
test.skip(
!ORCAD_TEMPLATE || process.env.ORCA_E2E_SSH_DOCKER !== '1',
'Needs Docker and server template'
)
}
export type ManagedHostFixture = {
host: OrcadConvertHost
session: ReturnType<typeof createRestartSession>
@@ -53,32 +61,75 @@ export type ManagedWorkspace = {
environmentId: string
}
/** Leaves the managed repository active; each launch is recorded on `fixture` for teardown. */
export async function launchManagedWorkspace(
fixture: ManagedHostFixture,
export type ManagedProfileOptions = {
/** Overrides the host's repository, e.g. a same-path desktop collision. */
repoPath?: string
/** Overrides the host's folder workspace, e.g. to keep /tmp outside every workspace. */
options: { folderPath?: string } = {}
folderPath?: string
/** Runs on the profile-creating launch, before the SSH profile is seeded. */
beforeSeed?: (page: Page) => Promise<void>
/** Runs as soon as the converting launch starts, before the session is ready. */
onLaunch?: (page: Page, seeded: RelayEraProfile) => void | Promise<void>
}
/** Seeds a relay-era profile and converts it; each launch is recorded on `fixture` for teardown. */
export async function launchConvertedProfile(
fixture: ManagedHostFixture,
options: ManagedProfileOptions = {}
): Promise<ManagedWorkspace> {
const { host, session } = fixture
// The first launch only creates the profile the saved SSH host is written into.
const first = await session.launch()
fixture.app = first.app
await waitForSessionReady(first.page)
await options.beforeSeed?.(first.page)
await session.close(first.app)
fixture.app = null
const seeded = seedRelayEraProfile(session.userDataDir, host.input, {
repoPath: host.remoteRepoPath,
repoPath: options.repoPath ?? host.remoteRepoPath,
folderPath: options.folderPath ?? host.remoteFolderPath
})
const { app, page } = await session.launch()
fixture.app = app
await options.onLaunch?.(page, seeded)
await waitForSessionReady(page)
await convertAndRetain(page, session.userDataDir, seeded)
const environmentId = await managedEnvironmentId(page, seeded.targetId)
await activateManagedWorkspace(page, seeded.worktreeId, environmentId)
return { app, page, seeded, environmentId }
}
/** Like `launchConvertedProfile`, and leaves the managed repository active. */
export async function launchManagedWorkspace(
fixture: ManagedHostFixture,
options: ManagedProfileOptions = {}
): Promise<ManagedWorkspace> {
const workspace = await launchConvertedProfile(fixture, options)
await activateManagedWorkspace(
workspace.page,
workspace.seeded.worktreeId,
workspace.environmentId
)
return workspace
}
/** Quits the live launch and relaunches the same profile, waiting for startup to settle. */
export async function relaunchManagedApp(
fixture: ManagedHostFixture,
onLaunch?: (page: Page) => void | Promise<void>
): Promise<Page> {
if (!fixture.app) {
throw new Error('No live launch to close')
}
await fixture.session.close(fixture.app)
fixture.app = null
const { app, page } = await fixture.session.launch()
fixture.app = app
await onLaunch?.(page)
await waitForSessionReady(page)
await waitForStartupWorktreeRefresh(page)
return page
}
export async function managedEnvironmentId(page: Page, targetId: string): Promise<string> {
const environment = (await page.evaluate(() => window.api.runtimeEnvironments.list())).find(
(entry) => entry.orcadDeployment?.sshTargetId === targetId
@@ -4,10 +4,10 @@ import { waitForSessionReady } from './helpers/store'
import { startOrcadConvertHost } from './helpers/orcad-convert-host'
import { reconnect, serverCall } from './helpers/orcad-convert-flow'
import { runtimeAdvertisesBrowserClientHosting } from '../../src/shared/browser-client-hosting-eligibility'
import { ORCAD_TEMPLATE, skipUnlessManagedDockerHost } from './helpers/orcad-managed-workspace'
const TEMPLATE = process.env.ORCA_E2E_ORCAD_CONVERT_TEMPLATE
test.skip(!TEMPLATE || process.env.ORCA_E2E_SSH_DOCKER !== '1', 'Needs Docker and server template')
test.use({ orcaAppExtraEnv: { ORCA_ORCAD_TEMPLATE_PATH: TEMPLATE } })
skipUnlessManagedDockerHost()
test.use({ orcaAppExtraEnv: { ORCA_ORCAD_TEMPLATE_PATH: ORCAD_TEMPLATE } })
test('a backendless managed host does not promise client-hosted browser creation', async ({
orcaPage: page
+26 -52
View File
@@ -1,15 +1,16 @@
import { writeFileSync } from 'node:fs'
import path from 'node:path'
import type { ElectronApplication, Page } from '@stablyai/playwright-test'
import type { Page } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import { createRestartSession } from './helpers/orca-restart'
import { startOrcadConvertHost } from './helpers/orcad-convert-host'
import { seedRelayEraProfile } from './helpers/orcad-upgrade-profile'
import { convertAndRetain, serverCall } from './helpers/orcad-convert-flow'
import { waitForSessionReady } from './helpers/store'
import { serverCall } from './helpers/orcad-convert-flow'
import { dismissTransientAnnouncement } from './helpers/ssh-config-host-picker'
import { openFileExplorer } from './helpers/file-explorer'
import { startBrowserSplitPageServer } from './helpers/browser-split-page-server'
import {
launchConvertedProfile,
skipUnlessManagedDockerHost,
withManagedHost
} from './helpers/orcad-managed-workspace'
import {
navigateGuest,
waitForGuestUrl,
@@ -19,8 +20,7 @@ import { toRuntimeExecutionHostId } from '../../src/shared/execution-host'
import { getWorktreeHostIdentity } from '../../src/shared/worktree/host-qualified-identity'
import { quotePosixShell } from '../../src/shared/wsl-login-shell-command'
const TEMPLATE = process.env.ORCA_E2E_ORCAD_CONVERT_TEMPLATE
test.skip(!TEMPLATE || process.env.ORCA_E2E_SSH_DOCKER !== '1', 'Needs owned Docker host')
skipUnlessManagedDockerHost()
test.skip(process.platform === 'win32', 'Owned POSIX host collision fixture')
async function dropExplorerFile(page: Page, name: string, tabId: string) {
@@ -78,11 +78,9 @@ test('managed Files drag cannot load a desktop same-path HTML file', async ({
const fileName = 'DROP_OWNER.html'
const filePath = path.join(testRepoPath, fileName)
writeFileSync(filePath, '<html><body>DESKTOP_BROWSER_DROP_OWNER</body></html>')
const host = startOrcadConvertHost('docker', testInfo)
const server = await startBrowserSplitPageServer()
const session = createRestartSession(testInfo, { ORCA_ORCAD_TEMPLATE_PATH: TEMPLATE! })
let app: ElectronApplication | null = null
try {
await withManagedHost(testInfo, async (fixture) => {
const { host } = fixture
if (!host.exec) {
throw new Error('Missing owned host control')
}
@@ -92,33 +90,19 @@ test('managed Files drag cannot load a desktop same-path HTML file', async ({
host.exec(
`printf '%s' REMOTE_DROP_READY > ${quotePosixShell(path.join(testRepoPath, 'REMOTE_DROP_READY.txt'))}`
)
const first = await session.launch()
app = first.app
await waitForSessionReady(first.page)
const nativeId = await first.page.evaluate(async (repoPath) => {
const result = await window.api.repos.add({ path: repoPath })
if ('error' in result) {
throw new Error(result.error)
}
return `${result.repo.id}::${repoPath}`
}, testRepoPath)
await session.close(app)
app = null
const seeded = seedRelayEraProfile(session.userDataDir, host.input, {
let nativeId = ''
const { page, seeded, environmentId } = await launchConvertedProfile(fixture, {
repoPath: testRepoPath,
folderPath: host.remoteFolderPath
beforeSeed: async (page) => {
nativeId = await page.evaluate(async (repoPath) => {
const result = await window.api.repos.add({ path: repoPath })
if ('error' in result) {
throw new Error(result.error)
}
return `${result.repo.id}::${repoPath}`
}, testRepoPath)
}
})
const launched = await session.launch()
app = launched.app
const page = launched.page
await waitForSessionReady(page)
await convertAndRetain(page, session.userDataDir, seeded)
const environment = (await page.evaluate(() => window.api.runtimeEnvironments.list())).find(
(entry) => entry.orcadDeployment?.sshTargetId === seeded.targetId
)
if (!environment) {
throw new Error('Missing actual managed environment')
}
await dismissTransientAnnouncement(page)
const nativeIdentity = getWorktreeHostIdentity({ id: nativeId, hostId: 'local' })
await page.locator(`[data-worktree-host-identity="${nativeIdentity}"]:visible`).click()
@@ -156,7 +140,7 @@ test('managed Files drag cannot load a desktop same-path HTML file', async ({
await waitForGuestIdle(page, floatingId)
const remoteIdentity = getWorktreeHostIdentity({
id: seeded.worktreeId,
hostId: toRuntimeExecutionHostId(environment.id)
hostId: toRuntimeExecutionHostId(environmentId)
})
await page.locator(`[data-worktree-host-identity="${remoteIdentity}"]:visible`).click()
await openFileExplorer(page)
@@ -166,10 +150,10 @@ test('managed Files drag cannot load a desktop same-path HTML file', async ({
).toBeVisible()
expect(
await page.evaluate(() => window.__store?.getState().activeWorkspaceExecutionHostId)
).toBe(toRuntimeExecutionHostId(environment.id))
).toBe(toRuntimeExecutionHostId(environmentId))
console.log(
'[independent-managed-source]',
await serverCall(page, environment.id, 'files.read', {
await serverCall(page, environmentId, 'files.read', {
worktree: `id:${seeded.worktreeId}`,
relativePath: fileName
})
@@ -179,7 +163,7 @@ test('managed Files drag cannot load a desktop same-path HTML file', async ({
expect(
JSON.parse(remotePayload['application/x-orca-workspace-file-source'] ?? '{}')
).toMatchObject({
executionHostId: toRuntimeExecutionHostId(environment.id),
executionHostId: toRuntimeExecutionHostId(environmentId),
workspaceId: seeded.worktreeId
})
await waitForGuestIdle(page, floatingId)
@@ -190,15 +174,5 @@ test('managed Files drag cannot load a desktop same-path HTML file', async ({
page.getByText('Open in Orca Browser is only available for local files.', { exact: true })
).toBeVisible()
expect((await guestText(page, floatingId)).url).toBe(server.pageUrl('float', 2))
} finally {
try {
if (app) {
await session.close(app)
}
await session.dispose()
await server.close()
} finally {
host.cleanup()
}
}
}).finally(() => server.close())
})
+3 -3
View File
@@ -24,9 +24,9 @@ import { deriveBrowserRoutePartition } from '../../src/main/browser/browser-rout
import { sshExecutionHostStorageIdentity } from '../../src/main/browser/browser-execution-host-storage-identity'
import { reconnect } from './helpers/orcad-convert-flow'
import { navigateGuest } from './helpers/browser-split-guest-probes'
import { ORCAD_TEMPLATE, skipUnlessManagedDockerHost } from './helpers/orcad-managed-workspace'
const TEMPLATE = process.env.ORCA_E2E_ORCAD_CONVERT_TEMPLATE
test.skip(!TEMPLATE || process.env.ORCA_E2E_SSH_DOCKER !== '1', 'Needs Docker and server template')
skipUnlessManagedDockerHost()
/** The partition a relay-era build kept this host's browser storage in (local-ssh-browser-partitions). */
function relayEraPartition(targetId: string): string {
@@ -147,7 +147,7 @@ test('retained and new browser tabs keep SSH routing and login cookies after man
value: SSH_REMOTE_ONLY_COOKIE_VALUE
}
)
cpSync(TEMPLATE!, template, { recursive: true })
cpSync(ORCAD_TEMPLATE!, template, { recursive: true })
const server = await reconnect(page, remote.targetId)
console.log('[browser-conversion-connect]', server)
expect(JSON.parse(server)).toMatchObject({ kind: 'managed' })
@@ -16,9 +16,9 @@ import {
} from './helpers/ssh-remote-only-browser-fixture'
import { createRetentionFixtureDirectory } from './helpers/host-created-terminal-retention-oracle'
import { reconnect, serverCall } from './helpers/orcad-convert-flow'
import { ORCAD_TEMPLATE, skipUnlessManagedDockerHost } from './helpers/orcad-managed-workspace'
const TEMPLATE = process.env.ORCA_E2E_ORCAD_CONVERT_TEMPLATE
test.skip(!TEMPLATE || process.env.ORCA_E2E_SSH_DOCKER !== '1', 'Needs Docker and server template')
skipUnlessManagedDockerHost()
test('an unavailable browser on a responding managed host does not report a server outage', async (// oxlint-disable-next-line no-empty-pattern -- Owns app launch.
{}, testInfo) => {
@@ -58,7 +58,7 @@ test('an unavailable browser on a responding managed host does not report a serv
}
})
await waitForSessionReady(page)
cpSync(TEMPLATE!, template, { recursive: true })
cpSync(ORCAD_TEMPLATE!, template, { recursive: true })
const server = await reconnect(page, remote.targetId)
console.log('[browser-conversion-connect]', server)
expect(JSON.parse(server)).toMatchObject({ kind: 'managed' })
+52 -94
View File
@@ -1,87 +1,63 @@
import type { ElectronApplication } from '@stablyai/playwright-test'
import type { Page } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import { createRestartSession } from './helpers/orca-restart'
import { startOrcadConvertHost } from './helpers/orcad-convert-host'
import { seedRelayEraProfile } from './helpers/orcad-upgrade-profile'
import { convertAndRetain, serverCall } from './helpers/orcad-convert-flow'
import { waitForSessionReady, waitForStartupWorktreeRefresh } from './helpers/store'
import { dismissTransientAnnouncement } from './helpers/ssh-config-host-picker'
import { toRuntimeExecutionHostId } from '../../src/shared/execution-host'
import { serverCall } from './helpers/orcad-convert-flow'
import {
activateManagedWorkspace,
launchManagedWorkspace,
relaunchManagedApp,
skipUnlessManagedDockerHost,
withManagedHost
} from './helpers/orcad-managed-workspace'
const TEMPLATE = process.env.ORCA_E2E_ORCAD_CONVERT_TEMPLATE
test.skip(!TEMPLATE || process.env.ORCA_E2E_SSH_DOCKER !== '1', 'Needs Docker and server template')
skipUnlessManagedDockerHost()
async function reportEditorRecords(page: Page, worktreeId: string): Promise<void> {
page.on('console', (message) => {
if (message.text().startsWith('[editor-records]')) {
console.log(message.text())
}
})
await page.evaluate((worktreeId) => {
const store = window.__store
if (!store) {
throw new Error('Missing renderer store')
}
const report = (state: ReturnType<typeof store.getState>) => {
console.log(
'[editor-records]',
JSON.stringify({
files: state.openFiles.filter((file) => file.worktreeId === worktreeId),
tabs: state.unifiedTabsByWorktree[worktreeId],
activeHost: state.activeWorkspaceExecutionHostId,
trace: new Error('Editor file mutation').stack
})
)
}
report(store.getState())
store.subscribe((state, previous) => {
if (state.openFiles !== previous.openFiles) {
report(state)
}
})
}, worktreeId)
}
test('converted editor has one file record per document', async (// oxlint-disable-next-line no-empty-pattern -- This exercise owns its app launch.
{}, testInfo) => {
test.setTimeout(4 * 60_000)
const host = startOrcadConvertHost('docker', testInfo)
const session = createRestartSession(testInfo, { ORCA_ORCAD_TEMPLATE_PATH: TEMPLATE! })
let app: ElectronApplication | null = null
try {
const first = await session.launch()
app = first.app
await waitForSessionReady(first.page)
await session.close(app)
app = null
await withManagedHost(testInfo, async (fixture) => {
const { host } = fixture
const marker = `REMOTE_MARKDOWN_${Date.now()}`
host.exec!(
`printf '# ${marker}\n\nHost-owned Markdown.\n' > '${host.remoteRepoPath}/README.md'`
)
const seeded = seedRelayEraProfile(session.userDataDir, host.input, {
repoPath: host.remoteRepoPath,
folderPath: host.remoteFolderPath
const { page, seeded, environmentId } = await launchManagedWorkspace(fixture, {
onLaunch: (page, seeded) => reportEditorRecords(page, seeded.worktreeId)
})
const launched = await session.launch()
app = launched.app
const page = launched.page
page.on('console', (message) => {
if (message.text().startsWith('[editor-records]')) {
console.log(message.text())
}
})
await page.evaluate((worktreeId) => {
const store = window.__store
if (!store) {
throw new Error('Missing renderer store')
}
const report = (state: ReturnType<typeof store.getState>) => {
console.log(
'[editor-records]',
JSON.stringify({
files: state.openFiles.filter((file) => file.worktreeId === worktreeId),
tabs: state.unifiedTabsByWorktree[worktreeId],
activeHost: state.activeWorkspaceExecutionHostId,
trace: new Error('Editor file mutation').stack
})
)
}
report(store.getState())
store.subscribe((state, previous) => {
if (state.openFiles !== previous.openFiles) {
report(state)
}
})
}, seeded.worktreeId)
await waitForSessionReady(page)
await convertAndRetain(page, session.userDataDir, seeded)
const environment = (await page.evaluate(() => window.api.runtimeEnvironments.list())).find(
(e) => e.orcadDeployment?.sshTargetId === seeded.targetId
)
if (!environment) {
throw new Error('Missing managed environment')
}
await page.evaluate(
({ id, hostId }) => window.__store?.getState().setActiveWorktree(id, hostId),
{
id: seeded.worktreeId,
hostId: toRuntimeExecutionHostId(environment.id)
}
)
await dismissTransientAnnouncement(page)
await expect(page.locator('.rich-markdown-editor')).toContainText(marker, { timeout: 30_000 })
console.log(
'[host-tabs]',
await serverCall(page, environment.id, 'session.tabs.list', {
await serverCall(page, environmentId, 'session.tabs.list', {
worktree: `id:${seeded.worktreeId}`
})
)
@@ -104,24 +80,12 @@ test('converted editor has one file record per document', async (// oxlint-disab
.poll(() => host.exec!(`cat '${seeded.sessionFilePath}'`), { timeout: 30_000 })
.toContain(savedMarker)
console.log('[saved-on-host]', host.exec!(`cat '${seeded.sessionFilePath}'`))
await session.close(app)
app = null
const restarted = await session.launch()
app = restarted.app
await waitForSessionReady(restarted.page)
await waitForStartupWorktreeRefresh(restarted.page)
await restarted.page.evaluate(
({ id, hostId }) => window.__store?.getState().setActiveWorktree(id, hostId),
{
id: seeded.worktreeId,
hostId: toRuntimeExecutionHostId(environment.id)
}
)
await dismissTransientAnnouncement(restarted.page)
await expect(restarted.page.locator('.rich-markdown-editor')).toContainText(savedMarker, {
const restarted = await relaunchManagedApp(fixture)
await activateManagedWorkspace(restarted, seeded.worktreeId, environmentId)
await expect(restarted.locator('.rich-markdown-editor')).toContainText(savedMarker, {
timeout: 30_000
})
const restoredFiles = await restarted.page.evaluate(
const restoredFiles = await restarted.evaluate(
(worktreeId) =>
window.__store?.getState().openFiles.filter((file) => file.worktreeId === worktreeId),
seeded.worktreeId
@@ -130,12 +94,6 @@ test('converted editor has one file record per document', async (// oxlint-disab
expect(restoredFiles?.filter((file) => file.filePath === seeded.sessionFilePath)).toHaveLength(
1
)
await restarted.page.screenshot({ path: testInfo.outputPath('editor-restored.png') })
} finally {
if (app) {
await session.close(app)
}
await session.dispose()
host.cleanup()
}
await restarted.screenshot({ path: testInfo.outputPath('editor-restored.png') })
})
})
@@ -1,15 +1,15 @@
import type { ElectronApplication, Page } from '@stablyai/playwright-test'
import type { Page } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import { createRestartSession } from './helpers/orca-restart'
import { startOrcadConvertHost } from './helpers/orcad-convert-host'
import { seedRelayEraProfile } from './helpers/orcad-upgrade-profile'
import { convertAndRetain, serverCall } from './helpers/orcad-convert-flow'
import { waitForSessionReady, waitForStartupWorktreeRefresh } from './helpers/store'
import { dismissTransientAnnouncement } from './helpers/ssh-config-host-picker'
import { toRuntimeExecutionHostId } from '../../src/shared/execution-host'
import { serverCall } from './helpers/orcad-convert-flow'
import {
activateManagedWorkspace,
launchConvertedProfile,
relaunchManagedApp,
skipUnlessManagedDockerHost,
withManagedHost
} from './helpers/orcad-managed-workspace'
const TEMPLATE = process.env.ORCA_E2E_ORCAD_CONVERT_TEMPLATE
test.skip(!TEMPLATE || process.env.ORCA_E2E_SSH_DOCKER !== '1', 'Needs Docker and server template')
skipUnlessManagedDockerHost()
function captureRendererMessages(page: Page): void {
page.on('console', (message) => {
@@ -27,57 +27,23 @@ for (const recovery of ['conversion', 'server restart'] as const) {
test(`remote editors receive host file changes after ${recovery}`, async (// oxlint-disable-next-line no-empty-pattern -- This exercise owns its app launch.
{}, testInfo) => {
test.setTimeout(4 * 60_000)
const host = startOrcadConvertHost('docker', testInfo)
const session = createRestartSession(testInfo, { ORCA_ORCAD_TEMPLATE_PATH: TEMPLATE! })
let app: ElectronApplication | null = null
try {
await withManagedHost(testInfo, async (fixture) => {
const { host } = fixture
if (!host.exec) {
throw new Error('Docker controls are unavailable')
}
const execute = host.exec
const first = await session.launch()
app = first.app
await waitForSessionReady(first.page)
await session.close(app)
app = null
const marker = `REMOTE_MARKDOWN_${Date.now()}`
execute(
`printf '# ${marker}\n\nHost-owned Markdown.\n\n[[LATE]]\n' > '${host.remoteRepoPath}/README.md'`
)
const seeded = seedRelayEraProfile(session.userDataDir, host.input, {
repoPath: host.remoteRepoPath,
folderPath: host.remoteFolderPath
})
const launched = await session.launch()
app = launched.app
let page = launched.page
captureRendererMessages(page)
await waitForSessionReady(page)
await convertAndRetain(page, session.userDataDir, seeded)
const environment = (await page.evaluate(() => window.api.runtimeEnvironments.list())).find(
(e) => e.orcadDeployment?.sshTargetId === seeded.targetId
)
if (!environment) {
throw new Error('Missing managed environment')
}
if (recovery === 'server restart') {
await session.close(app)
app = null
const restarted = await session.launch()
app = restarted.app
page = restarted.page
captureRendererMessages(page)
await waitForSessionReady(page)
await waitForStartupWorktreeRefresh(page)
}
await page.evaluate(
({ id, hostId }) => window.__store?.getState().setActiveWorktree(id, hostId),
{
id: seeded.worktreeId,
hostId: toRuntimeExecutionHostId(environment.id)
}
)
await dismissTransientAnnouncement(page)
const converted = await launchConvertedProfile(fixture, { onLaunch: captureRendererMessages })
const { seeded, environmentId } = converted
const page =
recovery === 'server restart'
? await relaunchManagedApp(fixture, captureRendererMessages)
: converted.page
await activateManagedWorkspace(page, seeded.worktreeId, environmentId)
await expect(page.locator('.rich-markdown-editor')).toContainText(marker, { timeout: 30_000 })
await page.evaluate(() => {
window.addEventListener('orca:worktree-file-change', (event) => {
@@ -122,7 +88,7 @@ for (const recovery of ['conversion', 'server restart'] as const) {
})
return response.ok ? JSON.stringify(response.result) : ''
},
{ id: environment.id, worktreeId: seeded.worktreeId }
{ id: environmentId, worktreeId: seeded.worktreeId }
),
{ timeout: 180_000 }
)
@@ -132,7 +98,7 @@ for (const recovery of ['conversion', 'server restart'] as const) {
execute('cat /root/.orca-remote/orcad-*/.orcad-pid').trim()
)
}
const content = await serverCall(page, environment.id, 'files.read', {
const content = await serverCall(page, environmentId, 'files.read', {
worktree: `id:${seeded.worktreeId}`,
relativePath: 'README.md'
})
@@ -151,7 +117,7 @@ for (const recovery of ['conversion', 'server restart'] as const) {
timeout: 15_000
})
execute(`printf 'watcher recovered\\n' > '${host.remoteRepoPath}/WATCH_RECOVERED.txt'`)
const created = await serverCall(page, environment.id, 'files.read', {
const created = await serverCall(page, environmentId, 'files.read', {
worktree: `id:${seeded.worktreeId}`,
relativePath: 'WATCH_RECOVERED.txt'
})
@@ -159,12 +125,6 @@ for (const recovery of ['conversion', 'server restart'] as const) {
expect(created).toContain('watcher recovered')
await resumed
}
} finally {
if (app) {
await session.close(app)
}
await session.dispose()
host.cleanup()
}
})
})
}
@@ -9,9 +9,9 @@ import { waitForSessionReady, waitForStartupWorktreeRefresh } from './helpers/st
import { dismissTransientAnnouncement } from './helpers/ssh-config-host-picker'
import { toRuntimeExecutionHostId } from '../../src/shared/execution-host'
import { folderWorkspaceKey } from '../../src/shared/workspace-scope'
import { ORCAD_TEMPLATE, skipUnlessManagedDockerHost } from './helpers/orcad-managed-workspace'
const TEMPLATE = process.env.ORCA_E2E_ORCAD_CONVERT_TEMPLATE
test.skip(!TEMPLATE || process.env.ORCA_E2E_SSH_DOCKER !== '1', 'Needs Docker and server template')
skipUnlessManagedDockerHost()
for (const selectedHost of ['A', 'B'] as const) {
test(`same-ID folder explorer follows selected host ${selectedHost}`, async (// oxlint-disable-next-line no-empty-pattern -- This exercise owns its app launch.
@@ -19,8 +19,10 @@ for (const selectedHost of ['A', 'B'] as const) {
test.setTimeout(5 * 60_000)
const hostA = startOrcadConvertHost('docker', testInfo)
const hostB = startOrcadConvertHost('docker', testInfo)
const session = createRestartSession(testInfo, { ORCA_ORCAD_TEMPLATE_PATH: TEMPLATE! })
const otherSession = createRestartSession(testInfo, { ORCA_ORCAD_TEMPLATE_PATH: TEMPLATE! })
const session = createRestartSession(testInfo, { ORCA_ORCAD_TEMPLATE_PATH: ORCAD_TEMPLATE! })
const otherSession = createRestartSession(testInfo, {
ORCA_ORCAD_TEMPLATE_PATH: ORCAD_TEMPLATE!
})
let app: ElectronApplication | null = null
let otherApp: ElectronApplication | null = null
try {
@@ -1,16 +1,16 @@
import type { ElectronApplication, Page } from '@stablyai/playwright-test'
import type { Page } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import { createRestartSession } from './helpers/orca-restart'
import { startOrcadConvertHost } from './helpers/orcad-convert-host'
import { seedRelayEraProfile } from './helpers/orcad-upgrade-profile'
import { convertAndRetain, serverCall } from './helpers/orcad-convert-flow'
import { waitForSessionReady, waitForStartupWorktreeRefresh } from './helpers/store'
import { dismissTransientAnnouncement } from './helpers/ssh-config-host-picker'
import { toRuntimeExecutionHostId } from '../../src/shared/execution-host'
import { serverCall } from './helpers/orcad-convert-flow'
import {
activateManagedWorkspace,
launchConvertedProfile,
relaunchManagedApp,
skipUnlessManagedDockerHost,
withManagedHost
} from './helpers/orcad-managed-workspace'
import { folderWorkspaceKey } from '../../src/shared/workspace-scope'
const TEMPLATE = process.env.ORCA_E2E_ORCAD_CONVERT_TEMPLATE
test.skip(!TEMPLATE || process.env.ORCA_E2E_SSH_DOCKER !== '1', 'Needs Docker and server template')
skipUnlessManagedDockerHost()
function captureRendererMessages(page: Page): void {
page.on('console', (message) => {
@@ -28,44 +28,19 @@ for (const workspace of ['worktree', 'folder'] as const) {
test(`remote ${workspace} explorer receives host-created files after server restart`, async (// oxlint-disable-next-line no-empty-pattern -- This exercise owns its app launch.
{}, testInfo) => {
test.setTimeout(5 * 60_000)
const host = startOrcadConvertHost('docker', testInfo)
const session = createRestartSession(testInfo, { ORCA_ORCAD_TEMPLATE_PATH: TEMPLATE! })
let app: ElectronApplication | null = null
try {
await withManagedHost(testInfo, async (fixture) => {
const { host } = fixture
if (!host.exec) {
throw new Error('Docker controls are unavailable')
}
const execute = host.exec
const first = await session.launch()
app = first.app
await waitForSessionReady(first.page)
await session.close(app)
app = null
const folderPath = '/tmp/orca-explorer-folder'
execute(`mkdir -p '${folderPath}'`)
const seeded = seedRelayEraProfile(session.userDataDir, host.input, {
repoPath: host.remoteRepoPath,
folderPath
const { seeded, environmentId } = await launchConvertedProfile(fixture, {
folderPath,
onLaunch: captureRendererMessages
})
const launched = await session.launch()
app = launched.app
captureRendererMessages(launched.page)
await waitForSessionReady(launched.page)
await convertAndRetain(launched.page, session.userDataDir, seeded)
const environment = (
await launched.page.evaluate(() => window.api.runtimeEnvironments.list())
).find((entry) => entry.orcadDeployment?.sshTargetId === seeded.targetId)
if (!environment) {
throw new Error('Missing managed environment')
}
await session.close(app)
app = null
const restarted = await session.launch()
app = restarted.app
const page = restarted.page
captureRendererMessages(page)
await waitForSessionReady(page)
await waitForStartupWorktreeRefresh(page)
const page = await relaunchManagedApp(fixture, captureRendererMessages)
const folderId = await page.evaluate(
(path) =>
window.__store?.getState().folderWorkspaces.find((folder) => folder.folderPath === path)
@@ -77,11 +52,7 @@ for (const workspace of ['worktree', 'folder'] as const) {
}
const worktreeId = workspace === 'folder' ? folderWorkspaceKey(folderId) : seeded.worktreeId
const rootPath = workspace === 'folder' ? folderPath : host.remoteRepoPath
await page.evaluate(
({ id, hostId }) => window.__store?.getState().setActiveWorktree(id, hostId),
{ id: worktreeId, hostId: toRuntimeExecutionHostId(environment.id) }
)
await dismissTransientAnnouncement(page)
await activateManagedWorkspace(page, worktreeId, environmentId)
await page.evaluate(() => {
window.addEventListener('orca:worktree-file-change', (event) => {
if (event instanceof CustomEvent) {
@@ -121,7 +92,7 @@ for (const workspace of ['worktree', 'folder'] as const) {
})
return response.ok ? JSON.stringify(response.result) : ''
},
{ id: environment.id, worktreeId }
{ id: environmentId, worktreeId }
),
{ timeout: 180_000 }
)
@@ -137,7 +108,7 @@ for (const workspace of ['worktree', 'folder'] as const) {
timeout: 30_000
})
execute(`printf 'watcher recovered\n' > '${rootPath}/WATCH_RECOVERED.txt'`)
const created = await serverCall(page, environment.id, 'files.read', {
const created = await serverCall(page, environmentId, 'files.read', {
worktree: `id:${worktreeId}`,
relativePath: 'WATCH_RECOVERED.txt'
})
@@ -153,12 +124,6 @@ for (const workspace of ['worktree', 'folder'] as const) {
timeout: 10_000
})
await page.screenshot({ path: testInfo.outputPath('remote-explorer-refreshed.png') })
} finally {
if (app) {
await session.close(app)
}
await session.dispose()
host.cleanup()
}
})
})
}
+28 -60
View File
@@ -1,69 +1,43 @@
import type { ElectronApplication } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import { createRestartSession } from './helpers/orca-restart'
import { startOrcadConvertHost } from './helpers/orcad-convert-host'
import { seedRelayEraProfile } from './helpers/orcad-upgrade-profile'
import { convertAndRetain, serverCall } from './helpers/orcad-convert-flow'
import { waitForSessionReady } from './helpers/store'
import { dismissTransientAnnouncement } from './helpers/ssh-config-host-picker'
import { toRuntimeExecutionHostId } from '../../src/shared/execution-host'
import { serverCall } from './helpers/orcad-convert-flow'
import {
launchManagedWorkspace,
skipUnlessManagedDockerHost,
withManagedHost
} from './helpers/orcad-managed-workspace'
const TEMPLATE = process.env.ORCA_E2E_ORCAD_CONVERT_TEMPLATE
test.skip(!TEMPLATE || process.env.ORCA_E2E_SSH_DOCKER !== '1', 'Needs Docker and server template')
skipUnlessManagedDockerHost()
test('retained Markdown lists documents on its managed owner after conversion', async (// oxlint-disable-next-line no-empty-pattern -- This exercise owns its app launch.
{}, testInfo) => {
test.setTimeout(4 * 60_000)
const host = startOrcadConvertHost('docker', testInfo)
const session = createRestartSession(testInfo, { ORCA_ORCAD_TEMPLATE_PATH: TEMPLATE! })
let app: ElectronApplication | null = null
try {
const first = await session.launch()
app = first.app
await waitForSessionReady(first.page)
await session.close(app)
app = null
await withManagedHost(testInfo, async (fixture) => {
const { host } = fixture
const marker = `REMOTE_MARKDOWN_${Date.now()}`
host.exec!(
`printf '# ${marker}\n\nHost-owned Markdown.\n\n[[NOTES]]\n' > '${host.remoteRepoPath}/README.md'`
)
host.exec!(`printf '# SIBLING_${marker}\n' > '${host.remoteRepoPath}/NOTES.md'`)
const seeded = seedRelayEraProfile(session.userDataDir, host.input, {
repoPath: host.remoteRepoPath,
folderPath: host.remoteFolderPath
})
const launched = await session.launch()
app = launched.app
const page = launched.page
const errors: string[] = []
let failureScreenshot: Promise<void> | undefined
page.on('console', (message) => {
if (message.type() === 'error') {
errors.push(message.text())
console.log('[renderer-error]', message.text())
if (message.text().includes('Failed to list markdown documents') && !failureScreenshot) {
failureScreenshot = (async () => {
await page.screenshot({ path: testInfo.outputPath('markdown-list-failure.png') })
})()
}
const { page, seeded, environmentId } = await launchManagedWorkspace(fixture, {
onLaunch: (page) => {
page.on('console', (message) => {
if (message.type() === 'error') {
errors.push(message.text())
console.log('[renderer-error]', message.text())
if (
message.text().includes('Failed to list markdown documents') &&
!failureScreenshot
) {
failureScreenshot = (async () => {
await page.screenshot({ path: testInfo.outputPath('markdown-list-failure.png') })
})()
}
}
})
}
})
await waitForSessionReady(page)
await convertAndRetain(page, session.userDataDir, seeded)
const environment = (await page.evaluate(() => window.api.runtimeEnvironments.list())).find(
(e) => e.orcadDeployment?.sshTargetId === seeded.targetId
)
if (!environment) {
throw new Error('Missing managed environment')
}
await page.evaluate(
({ id, hostId }) => window.__store?.getState().setActiveWorktree(id, hostId),
{
id: seeded.worktreeId,
hostId: toRuntimeExecutionHostId(environment.id)
}
)
await dismissTransientAnnouncement(page)
await expect(page.locator('.rich-markdown-editor')).toContainText(marker, { timeout: 30_000 })
console.log(
'[markdown-conversion]',
@@ -76,7 +50,7 @@ test('retained Markdown lists documents on its managed owner after conversion',
await failureScreenshot
expect(errors.filter((e) => e.includes('Failed to list markdown documents'))).toEqual([])
expect(
await serverCall(page, environment.id, 'files.listMarkdownDocuments', {
await serverCall(page, environmentId, 'files.listMarkdownDocuments', {
worktree: `id:${seeded.worktreeId}`
})
).toContain('NOTES.md')
@@ -96,7 +70,7 @@ test('retained Markdown lists documents on its managed owner after conversion',
{
filePath: seeded.sessionFilePath,
worktreeId: seeded.worktreeId,
environmentId: environment.id
environmentId
}
)
const previewLink = page
@@ -110,11 +84,5 @@ test('retained Markdown lists documents on its managed owner after conversion',
).toBeVisible()
expect(errors.filter((e) => e.includes('Failed to list markdown documents'))).toEqual([])
await page.screenshot({ path: testInfo.outputPath('markdown-converted-ready.png') })
} finally {
if (app) {
await session.close(app)
}
await session.dispose()
host.cleanup()
}
})
})
@@ -1,63 +1,34 @@
import type { ElectronApplication } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import { createRestartSession } from './helpers/orca-restart'
import { startOrcadConvertHost } from './helpers/orcad-convert-host'
import { seedRelayEraProfile } from './helpers/orcad-upgrade-profile'
import { convertAndRetain, serverCall } from './helpers/orcad-convert-flow'
import { waitForSessionReady } from './helpers/store'
import { dismissTransientAnnouncement } from './helpers/ssh-config-host-picker'
import { toRuntimeExecutionHostId } from '../../src/shared/execution-host'
import { serverCall } from './helpers/orcad-convert-flow'
import {
launchManagedWorkspace,
skipUnlessManagedDockerHost,
withManagedHost
} from './helpers/orcad-managed-workspace'
const TEMPLATE = process.env.ORCA_E2E_ORCAD_CONVERT_TEMPLATE
test.skip(!TEMPLATE || process.env.ORCA_E2E_SSH_DOCKER !== '1', 'Needs Docker and server template')
skipUnlessManagedDockerHost()
test('retained rich Markdown links refresh after remote document metadata arrives', async (// oxlint-disable-next-line no-empty-pattern -- This exercise owns its app launch.
{}, testInfo) => {
test.setTimeout(4 * 60_000)
const host = startOrcadConvertHost('docker', testInfo)
const session = createRestartSession(testInfo, { ORCA_ORCAD_TEMPLATE_PATH: TEMPLATE! })
let app: ElectronApplication | null = null
try {
const first = await session.launch()
app = first.app
await waitForSessionReady(first.page)
await session.close(app)
app = null
await withManagedHost(testInfo, async (fixture) => {
const { host } = fixture
const marker = `REMOTE_MARKDOWN_${Date.now()}`
host.exec!(
`printf '# ${marker}\n\nHost-owned Markdown.\n\n[[NOTES]]\n' > '${host.remoteRepoPath}/README.md'`
)
host.exec!(`printf '# SIBLING_${marker}\n' > '${host.remoteRepoPath}/NOTES.md'`)
const seeded = seedRelayEraProfile(session.userDataDir, host.input, {
repoPath: host.remoteRepoPath,
folderPath: host.remoteFolderPath
})
const launched = await session.launch()
app = launched.app
const page = launched.page
page.on('console', (message) => {
if (message.type() === 'error') {
console.log('[renderer-error]', message.text())
const { page, seeded, environmentId } = await launchManagedWorkspace(fixture, {
onLaunch: (page) => {
page.on('console', (message) => {
if (message.type() === 'error') {
console.log('[renderer-error]', message.text())
}
})
}
})
await waitForSessionReady(page)
await convertAndRetain(page, session.userDataDir, seeded)
const environment = (await page.evaluate(() => window.api.runtimeEnvironments.list())).find(
(e) => e.orcadDeployment?.sshTargetId === seeded.targetId
)
if (!environment) {
throw new Error('Missing managed environment')
}
await page.evaluate(
({ id, hostId }) => window.__store?.getState().setActiveWorktree(id, hostId),
{
id: seeded.worktreeId,
hostId: toRuntimeExecutionHostId(environment.id)
}
)
await dismissTransientAnnouncement(page)
await expect(page.locator('.rich-markdown-editor')).toContainText(marker, { timeout: 30_000 })
const documents = await serverCall(page, environment.id, 'files.listMarkdownDocuments', {
const documents = await serverCall(page, environmentId, 'files.listMarkdownDocuments', {
worktree: `id:${seeded.worktreeId}`
})
console.log('[host-documents]', documents)
@@ -76,13 +47,7 @@ test('retained rich Markdown links refresh after remote document metadata arrive
`${host.remoteRepoPath}/NOTES.md`
)
console.log('[rich-link-opened-file]', JSON.stringify(opened))
expect(opened?.runtimeEnvironmentId).toBe(environment.id)
expect(opened?.runtimeEnvironmentId).toBe(environmentId)
await page.screenshot({ path: testInfo.outputPath('markdown-converted-ready.png') })
} finally {
if (app) {
await session.close(app)
}
await session.dispose()
host.cleanup()
}
})
})
@@ -1,80 +1,44 @@
import type { ElectronApplication } from '@stablyai/playwright-test'
import type { Page } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import { createRestartSession } from './helpers/orca-restart'
import { startOrcadConvertHost } from './helpers/orcad-convert-host'
import { seedRelayEraProfile } from './helpers/orcad-upgrade-profile'
import { convertAndRetain, serverCall } from './helpers/orcad-convert-flow'
import { waitForSessionReady, waitForStartupWorktreeRefresh } from './helpers/store'
import { dismissTransientAnnouncement } from './helpers/ssh-config-host-picker'
import { toRuntimeExecutionHostId } from '../../src/shared/execution-host'
import { serverCall } from './helpers/orcad-convert-flow'
import {
activateManagedWorkspace,
launchConvertedProfile,
relaunchManagedApp,
skipUnlessManagedDockerHost,
withManagedHost
} from './helpers/orcad-managed-workspace'
const TEMPLATE = process.env.ORCA_E2E_ORCAD_CONVERT_TEMPLATE
test.skip(!TEMPLATE || process.env.ORCA_E2E_SSH_DOCKER !== '1', 'Needs Docker and server template')
skipUnlessManagedDockerHost()
function captureRendererMessages(page: Page, label: string): void {
page.on('console', (message) => {
if (
message.type() === 'error' ||
message.type() === 'warning' ||
message.text().startsWith('[file-change-event]')
) {
console.log(label, message.text())
}
})
}
test('open remote Markdown previews discover host-created documents', async (// oxlint-disable-next-line no-empty-pattern -- This exercise owns its app launch.
{}, testInfo) => {
test.setTimeout(4 * 60_000)
const host = startOrcadConvertHost('docker', testInfo)
const session = createRestartSession(testInfo, { ORCA_ORCAD_TEMPLATE_PATH: TEMPLATE! })
let app: ElectronApplication | null = null
try {
const first = await session.launch()
app = first.app
await waitForSessionReady(first.page)
await session.close(app)
app = null
await withManagedHost(testInfo, async (fixture) => {
const { host } = fixture
const marker = `REMOTE_MARKDOWN_${Date.now()}`
host.exec!(
`printf '# ${marker}\n\nHost-owned Markdown.\n\n[[LATE]]\n' > '${host.remoteRepoPath}/README.md'`
)
const seeded = seedRelayEraProfile(session.userDataDir, host.input, {
repoPath: host.remoteRepoPath,
folderPath: host.remoteFolderPath
const { seeded, environmentId } = await launchConvertedProfile(fixture, {
onLaunch: (page) => captureRendererMessages(page, '[renderer-error]')
})
const launched = await session.launch()
app = launched.app
let page = launched.page
page.on('console', (message) => {
if (
message.type() === 'error' ||
message.type() === 'warning' ||
message.text().startsWith('[file-change-event]')
) {
console.log('[renderer-error]', message.text())
}
})
await waitForSessionReady(page)
await convertAndRetain(page, session.userDataDir, seeded)
const environment = (await page.evaluate(() => window.api.runtimeEnvironments.list())).find(
(e) => e.orcadDeployment?.sshTargetId === seeded.targetId
const page = await relaunchManagedApp(fixture, (page) =>
captureRendererMessages(page, '[renderer-event]')
)
if (!environment) {
throw new Error('Missing managed environment')
}
await session.close(app)
app = null
const restarted = await session.launch()
app = restarted.app
page = restarted.page
page.on('console', (message) => {
if (
message.type() === 'error' ||
message.type() === 'warning' ||
message.text().startsWith('[file-change-event]')
) {
console.log('[renderer-event]', message.text())
}
})
await waitForSessionReady(page)
await waitForStartupWorktreeRefresh(page)
await page.evaluate(
({ id, hostId }) => window.__store?.getState().setActiveWorktree(id, hostId),
{
id: seeded.worktreeId,
hostId: toRuntimeExecutionHostId(environment.id)
}
)
await dismissTransientAnnouncement(page)
await activateManagedWorkspace(page, seeded.worktreeId, environmentId)
await expect(page.locator('.rich-markdown-editor')).toContainText(marker, { timeout: 30_000 })
await page.evaluate(
({ filePath, worktreeId, environmentId }) => {
@@ -89,7 +53,7 @@ test('open remote Markdown previews discover host-created documents', async (//
{
filePath: seeded.sessionFilePath,
worktreeId: seeded.worktreeId,
environmentId: environment.id
environmentId
}
)
const link = page.locator('.markdown-preview').getByRole('link', { name: 'LATE', exact: true })
@@ -103,7 +67,7 @@ test('open remote Markdown previews discover host-created documents', async (//
})
})
host.exec!(`printf '# LATE_${marker}\n' > '${host.remoteRepoPath}/LATE.md'`)
const documents = await serverCall(page, environment.id, 'files.listMarkdownDocuments', {
const documents = await serverCall(page, environmentId, 'files.listMarkdownDocuments', {
worktree: `id:${seeded.worktreeId}`
})
console.log('[host-created-documents]', documents)
@@ -115,7 +79,7 @@ test('open remote Markdown previews discover host-created documents', async (//
console.log('[resolved-preview-link]', await link.getAttribute('class'))
await page.screenshot({ path: testInfo.outputPath('live-document-created.png') })
host.exec!(`rm '${host.remoteRepoPath}/LATE.md'`)
const removed = await serverCall(page, environment.id, 'files.listMarkdownDocuments', {
const removed = await serverCall(page, environmentId, 'files.listMarkdownDocuments', {
worktree: `id:${seeded.worktreeId}`
})
console.log('[host-removed-documents]', removed)
@@ -123,11 +87,5 @@ test('open remote Markdown previews discover host-created documents', async (//
await expect(page.getByText('LATE.md', { exact: true })).not.toBeVisible({ timeout: 30_000 })
await expect(link).toHaveClass(/markdown-doc-link-broken/, { timeout: 10_000 })
await page.screenshot({ path: testInfo.outputPath('live-document-removed.png') })
} finally {
if (app) {
await session.close(app)
}
await session.dispose()
host.cleanup()
}
})
})
+31 -61
View File
@@ -9,24 +9,20 @@ import {
} from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import type { ElectronApplication } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import { createRestartSession } from './helpers/orca-restart'
import { startOrcadConvertHost } from './helpers/orcad-convert-host'
import { seedRelayEraProfile } from './helpers/orcad-upgrade-profile'
import { convertAndRetain, serverCall } from './helpers/orcad-convert-flow'
import { waitForSessionReady } from './helpers/store'
import { serverCall } from './helpers/orcad-convert-flow'
import {
launchConvertedProfile,
skipUnlessManagedDockerHost,
withManagedHost
} from './helpers/orcad-managed-workspace'
import { dismissTransientAnnouncement } from './helpers/ssh-config-host-picker'
import { toRuntimeExecutionHostId } from '../../src/shared/execution-host'
import { folderWorkspaceKey } from '../../src/shared/workspace-scope'
import { quotePosixShell } from '../../src/shared/wsl-login-shell-command'
const TEMPLATE = process.env.ORCA_E2E_ORCAD_CONVERT_TEMPLATE
test.skip(
!TEMPLATE || process.env.ORCA_E2E_SSH_DOCKER !== '1' || process.platform === 'win32',
'Needs a POSIX recorder, Docker, and a server template'
)
skipUnlessManagedDockerHost()
test.skip(process.platform === 'win32', 'Needs a POSIX recorder')
for (const surface of [
'source-control',
@@ -56,10 +52,8 @@ for (const surface of [
)
chmodSync(recorder, 0o755)
const receipt = (): string => (existsSync(receiptPath) ? readFileSync(receiptPath, 'utf8') : '')
const host = startOrcadConvertHost('docker', testInfo)
const session = createRestartSession(testInfo, { ORCA_ORCAD_TEMPLATE_PATH: TEMPLATE! })
let app: ElectronApplication | null = null
try {
await withManagedHost(testInfo, async (fixture) => {
const { host } = fixture
if (!host.exec) {
throw new Error('Missing owned Docker controls')
}
@@ -67,40 +61,26 @@ for (const surface of [
execute(
`mkdir -p ${quotePosixShell(owned)} && git clone --quiet ${quotePosixShell(host.remoteRepoPath)} ${quotePosixShell(repoPath)} && printf '%s\\n' ${quotePosixShell(`# ${remoteMarker}`)} > ${quotePosixShell(filePath)} && mkdir -p ${quotePosixShell(folderPath)} && printf '%s\\n' ${quotePosixShell(`# ${remoteMarker}`)} > ${quotePosixShell(path.join(folderPath, 'README.md'))}`
)
const first = await session.launch()
app = first.app
await waitForSessionReady(first.page)
expect(
await first.page.evaluate(
async ({ filePath, recorder }) =>
window.api.shell.openInExternalEditor({
path: filePath,
command: recorder,
ownerHostId: 'local'
}),
{ filePath, recorder }
)
).toEqual({ ok: true })
await expect.poll(receipt).toContain(localMarker)
console.log('[local-recorder-control]', receipt())
rmSync(receiptPath)
await session.close(app)
app = null
const seeded = seedRelayEraProfile(session.userDataDir, host.input, {
const { page, seeded, environmentId } = await launchConvertedProfile(fixture, {
repoPath,
folderPath
folderPath,
beforeSeed: async (page) => {
expect(
await page.evaluate(
async ({ filePath, recorder }) =>
window.api.shell.openInExternalEditor({
path: filePath,
command: recorder,
ownerHostId: 'local'
}),
{ filePath, recorder }
)
).toEqual({ ok: true })
await expect.poll(receipt).toContain(localMarker)
console.log('[local-recorder-control]', receipt())
rmSync(receiptPath)
}
})
const launched = await session.launch()
app = launched.app
const page = launched.page
await waitForSessionReady(page)
await convertAndRetain(page, session.userDataDir, seeded)
const environment = (await page.evaluate(() => window.api.runtimeEnvironments.list())).find(
(entry) => entry.orcadDeployment?.sshTargetId === seeded.targetId
)
if (!environment) {
throw new Error('Missing managed environment')
}
const readConvertedFolderId = () =>
page.evaluate(
(folderPath) =>
@@ -119,7 +99,7 @@ for (const surface of [
const workspaceId = convertedFolderId
? folderWorkspaceKey(convertedFolderId)
: seeded.worktreeId
const content = await serverCall(page, environment.id, 'files.read', {
const content = await serverCall(page, environmentId, 'files.read', {
worktree: `id:${workspaceId}`,
relativePath: 'README.md'
})
@@ -138,7 +118,7 @@ for (const surface of [
state.setRightSidebarTab('explorer')
state.setRightSidebarOpen(true)
},
{ id: workspaceId, hostId: toRuntimeExecutionHostId(environment.id), recorder }
{ id: workspaceId, hostId: toRuntimeExecutionHostId(environmentId), recorder }
)
expect(
await page.evaluate(() => window.__store?.getState().settings?.activeRuntimeEnvironmentId)
@@ -189,16 +169,6 @@ for (const surface of [
await page.screenshot({
path: testInfo.outputPath(`${surface}-editor-owner-corrected.png`)
})
} finally {
try {
if (app) {
await session.close(app)
}
await session.dispose()
} finally {
host.cleanup()
rmSync(owned, { recursive: true, force: true })
}
}
}).finally(() => rmSync(owned, { recursive: true, force: true }))
})
}
+9 -29
View File
@@ -7,11 +7,11 @@
* closed while the host is disconnected stays closed.
* 3. Faults: a pane outlives a dropped SSH link and a frozen host, and answers input after both.
*/
import type { ElectronApplication, Page } from '@stablyai/playwright-test'
import type { Page } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import { reconnect } from './helpers/orcad-convert-flow'
import { readPairedPaneContent } from './helpers/paired-host-terminal'
import { waitForSessionReady, waitForStartupWorktreeRefresh } from './helpers/store'
import { waitForSessionReady } from './helpers/store'
import {
activateManagedWorkspace,
echoMarker,
@@ -19,27 +19,18 @@ import {
expectTerminalAnswers,
launchManagedWorkspace,
openTerminalTab,
ORCAD_TEMPLATE,
relaunchManagedApp,
selectTerminalTab,
skipUnlessManagedDockerHost,
terminalTabIds,
typeInTerminal,
waitForPane,
withManagedHost
} from './helpers/orcad-managed-workspace'
test.skip(
!ORCAD_TEMPLATE || process.env.ORCA_E2E_SSH_DOCKER !== '1',
'Needs Docker and server template'
)
skipUnlessManagedDockerHost()
test.skip(process.platform === 'win32', 'The Docker SSH host uses POSIX tooling')
function relaunchable(app: ElectronApplication | null): ElectronApplication {
if (!app) {
throw new Error('No live launch to close')
}
return app
}
function sshStatus(page: Page, targetId: string): Promise<string | null> {
return page.evaluate(
(id) => window.__store?.getState().sshConnectionStates.get(id)?.status ?? null,
@@ -142,13 +133,7 @@ test('managed tabs restore exactly once, and a tab closed while disconnected sta
expect(expected).toEqual(expect.arrayContaining(opened))
for (let cycle = 0; cycle < 2; cycle += 1) {
await fixture.session.close(relaunchable(fixture.app))
fixture.app = null
const relaunched = await fixture.session.launch()
fixture.app = relaunched.app
page = relaunched.page
await waitForSessionReady(page)
await waitForStartupWorktreeRefresh(page)
page = await relaunchManagedApp(fixture)
await activateManagedWorkspace(page, seeded.worktreeId, environmentId)
expect(await settledTabIds(page, seeded.worktreeId), `relaunch ${cycle + 1}`).toEqual(
expected
@@ -175,14 +160,9 @@ test('managed tabs restore exactly once, and a tab closed while disconnected sta
await reconnect(page, seeded.targetId)
const remaining = expected.filter((id) => id !== closed)
expect(await settledTabIds(page, seeded.worktreeId)).toEqual(remaining)
await fixture.session.close(relaunchable(fixture.app))
fixture.app = null
const final = await fixture.session.launch()
fixture.app = final.app
await waitForSessionReady(final.page)
await waitForStartupWorktreeRefresh(final.page)
await activateManagedWorkspace(final.page, seeded.worktreeId, environmentId)
expect(await settledTabIds(final.page, seeded.worktreeId)).toEqual(remaining)
const final = await relaunchManagedApp(fixture)
await activateManagedWorkspace(final, seeded.worktreeId, environmentId)
expect(await settledTabIds(final, seeded.worktreeId)).toEqual(remaining)
})
})
+37 -60
View File
@@ -1,28 +1,27 @@
import { execFileSync } from 'node:child_process'
import { mkdirSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import type { ElectronApplication } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import { createRestartSession } from './helpers/orca-restart'
import { startOrcadConvertHost } from './helpers/orcad-convert-host'
import { seedRelayEraProfile } from './helpers/orcad-upgrade-profile'
import { convertAndRetain, serverCall } from './helpers/orcad-convert-flow'
import { waitForSessionReady } from './helpers/store'
import { serverCall } from './helpers/orcad-convert-flow'
import {
launchConvertedProfile,
skipUnlessManagedDockerHost,
withManagedHost
} from './helpers/orcad-managed-workspace'
import { dismissTransientAnnouncement } from './helpers/ssh-config-host-picker'
import { openFileExplorer } from './helpers/file-explorer'
import { openTerminalWorkspaceRootLink } from './helpers/terminal-workspace-root-link'
import { toRuntimeExecutionHostId } from '../../src/shared/execution-host'
import { getWorktreeHostIdentity } from '../../src/shared/worktree/host-qualified-identity'
import { quotePosixShell } from '../../src/shared/wsl-login-shell-command'
import {
focusActiveTerminalInput,
getTerminalContent,
waitForActivePanePtyId
} from './helpers/terminal'
import { quotePosixShell } from '../../src/shared/wsl-login-shell-command'
const TEMPLATE = process.env.ORCA_E2E_ORCAD_CONVERT_TEMPLATE
test.skip(!TEMPLATE || process.env.ORCA_E2E_SSH_DOCKER !== '1', 'Needs owned Docker host')
skipUnlessManagedDockerHost()
test.skip(process.platform === 'win32', 'Owned POSIX host collision fixture')
test('a managed terminal path link cannot switch to a desktop workspace at the same path', async ({
@@ -36,10 +35,8 @@ test('a managed terminal path link cannot switch to a desktop workspace at the s
mkdirSync(intermediate)
execFileSync('git', ['init', '-q', intermediate])
writeFileSync(path.join(intermediate, 'INTERMEDIATE_OWNER.txt'), 'INTERMEDIATE\n')
const host = startOrcadConvertHost('docker', testInfo)
const session = createRestartSession(testInfo, { ORCA_ORCAD_TEMPLATE_PATH: TEMPLATE! })
let app: ElectronApplication | null = null
try {
await withManagedHost(testInfo, async (fixture) => {
const { host } = fixture
if (!host.exec) {
throw new Error('Missing owned host control')
}
@@ -47,38 +44,27 @@ test('a managed terminal path link cannot switch to a desktop workspace at the s
execute(
`mkdir -p ${quotePosixShell(path.dirname(testRepoPath))} && git clone --quiet ${quotePosixShell(host.remoteRepoPath)} ${quotePosixShell(testRepoPath)} && printf 'REMOTE_COMMAND_FILE_OWNER\n' > ${quotePosixShell(filePath)}`
)
const first = await session.launch()
app = first.app
await waitForSessionReady(first.page)
const ids = await first.page.evaluate(
async (paths) => {
const resultIds: string[] = []
for (const repoPath of paths) {
const result = await window.api.repos.add({ path: repoPath })
if ('error' in result) {
throw new Error(result.error)
}
resultIds.push(`${result.repo.id}::${repoPath}`)
}
return resultIds
},
[testRepoPath, intermediate]
)
await session.close(app)
app = null
const seeded = seedRelayEraProfile(session.userDataDir, host.input, {
let ids: string[] = []
const { page, seeded, environmentId } = await launchConvertedProfile(fixture, {
repoPath: testRepoPath,
folderPath: host.remoteFolderPath
beforeSeed: async (page) => {
ids = await page.evaluate(
async (paths) => {
const resultIds: string[] = []
for (const repoPath of paths) {
const result = await window.api.repos.add({ path: repoPath })
if ('error' in result) {
throw new Error(result.error)
}
resultIds.push(`${result.repo.id}::${repoPath}`)
}
return resultIds
},
[testRepoPath, intermediate]
)
}
})
const launched = await session.launch()
app = launched.app
const page = launched.page
await waitForSessionReady(page)
await convertAndRetain(page, session.userDataDir, seeded)
const environment = (await page.evaluate(() => window.api.runtimeEnvironments.list())).find(
(entry) => entry.orcadDeployment?.sshTargetId === seeded.targetId
)
if (!environment || !ids[0] || !ids[1]) {
if (!ids[0] || !ids[1]) {
throw new Error('Missing owned workspace')
}
const markerDirectory = path.join(testRepoPath, 'OWNER_SUBDIRECTORY')
@@ -134,7 +120,7 @@ test('a managed terminal path link cannot switch to a desktop workspace at the s
).toBeVisible()
const remoteIdentity = getWorktreeHostIdentity({
id: seeded.worktreeId,
hostId: toRuntimeExecutionHostId(environment.id)
hostId: toRuntimeExecutionHostId(environmentId)
})
await page.locator(`[data-worktree-host-identity="${remoteIdentity}"]:visible`).click()
await openFileExplorer(page)
@@ -154,7 +140,7 @@ test('a managed terminal path link cannot switch to a desktop workspace at the s
await expect(remoteFile).toBeVisible()
console.log(
'[actual-remote-source]',
await serverCall(page, environment.id, 'files.read', {
await serverCall(page, environmentId, 'files.read', {
worktree: `id:${seeded.worktreeId}`,
relativePath: fileName
})
@@ -205,13 +191,13 @@ test('a managed terminal path link cannot switch to a desktop workspace at the s
})
console.log('[actual-selection-after-managed-link]', route)
await page.screenshot({ path: testInfo.outputPath('managed-terminal-root-link-selection.png') })
expect(route.activeWorkspaceExecutionHostId).toBe(toRuntimeExecutionHostId(environment.id))
expect(route.activeWorkspaceExecutionHostId).toBe(toRuntimeExecutionHostId(environmentId))
expect(route.activeWorktreeId).toBe(seeded.worktreeId)
await expect(foreignSwitch).toHaveCount(0)
await popover.getByRole('button', { name: /^Open file/ }).click()
console.log(
'[managed-directory-click-host-read]',
await serverCall(page, environment.id, 'files.read', {
await serverCall(page, environmentId, 'files.read', {
worktree: `id:${seeded.worktreeId}`,
relativePath: 'intermediate-command-repo/LINK_DIRECTORY_OWNER.txt'
})
@@ -224,13 +210,13 @@ test('a managed terminal path link cannot switch to a desktop workspace at the s
} finally {
await page.keyboard.up(primaryModifier)
}
await serverCall(page, environment.id, 'files.read', {
await serverCall(page, environmentId, 'files.read', {
worktree: `id:${seeded.worktreeId}`,
relativePath: 'intermediate-command-repo/LINK_DIRECTORY_OWNER.txt'
})
await expect
.poll(() => page.evaluate(() => window.__store?.getState().activeWorkspaceExecutionHostId))
.toBe(toRuntimeExecutionHostId(environment.id))
.toBe(toRuntimeExecutionHostId(environmentId))
await expect
.poll(() => page.evaluate(() => window.__store?.getState().activeWorktreeId))
.toBe(seeded.worktreeId)
@@ -247,7 +233,7 @@ test('a managed terminal path link cannot switch to a desktop workspace at the s
await ownRoot.getByRole('button', { name: /^Switch workspace/ }).click()
await expect
.poll(() => page.evaluate(() => window.__store?.getState().activeWorkspaceExecutionHostId))
.toBe(toRuntimeExecutionHostId(environment.id))
.toBe(toRuntimeExecutionHostId(environmentId))
await expect
.poll(() => page.evaluate(() => window.__store?.getState().activeWorktreeId))
.toBe(seeded.worktreeId)
@@ -258,14 +244,5 @@ test('a managed terminal path link cannot switch to a desktop workspace at the s
host: window.__store?.getState().activeWorkspaceExecutionHostId
}))
)
} finally {
try {
if (app) {
await session.close(app)
}
await session.dispose()
} finally {
host.cleanup()
}
}
})
})
@@ -13,14 +13,11 @@ import {
type DockerSshHostTarget
} from './helpers/docker-ssh-host-target'
import { ensureTerminalVisible, waitForSessionReady } from './helpers/store'
import { ORCAD_TEMPLATE } from './helpers/orcad-managed-workspace'
import { ORCAD_TEMPLATE, skipUnlessManagedDockerHost } from './helpers/orcad-managed-workspace'
import { quotePosixShell } from '../../src/shared/wsl-login-shell-command'
/** A recipe VM's SSH root, served by a managed orcad the first connect deploys. */
test.skip(
!ORCAD_TEMPLATE || process.env.ORCA_E2E_SSH_DOCKER !== '1',
'Needs Docker and server template'
)
skipUnlessManagedDockerHost()
test.skip(process.platform === 'win32', 'The recipe scripts are POSIX shell')
test.use({
seedTestRepo: false,
+2 -5
View File
@@ -13,16 +13,13 @@ import {
expectTerminalAnswers,
launchManagedWorkspace,
openTerminalTab,
ORCAD_TEMPLATE,
skipUnlessManagedDockerHost,
typeInTerminal,
withManagedHost
} from './helpers/orcad-managed-workspace'
import { quotePosixShell } from '../../src/shared/wsl-login-shell-command'
test.skip(
!ORCAD_TEMPLATE || process.env.ORCA_E2E_SSH_DOCKER !== '1',
'Needs Docker and server template'
)
skipUnlessManagedDockerHost()
test.skip(process.platform === 'win32', 'The Docker SSH host uses POSIX tooling')
test('Quick Open finds a file in a monorepo-sized managed workspace', async (// oxlint-disable-next-line no-empty-pattern -- Owns its app launch through a restart session.