mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
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:
@@ -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) &&
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 }))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user