test(e2e): a paired phone lists and opens a managed SSH host's projects through the desktop (#29549)

Pins #29456 end to end: relay-era SSH projects convert to a managed server, a phone paired with the desktop lists them via mobileRelay.hosts.* under the SSH host's name and opens one through the relay, while the desktop's own list keeps the retained copies hidden.
This commit is contained in:
Neil
2026-10-16 01:38:40 -07:00
committed by GitHub
parent 026efc3a51
commit b89373c757
@@ -0,0 +1,134 @@
/**
* A phone paired with a desktop lists the projects on that desktop's SSH host once the host runs
* a managed Orca server (#29456): the reporter's path of relay-era SSH projects converted on
* upgrade. The desktop's own list hides the converted host's retained rows, so the phone must get
* them from the desktop's server listing (mobileRelay.hosts.*) and open them through the relay.
*
* The oracle is the phone's wire, as in paired-mobile-desktop-mirror.spec.ts: how the phone page
* renders these rows and tags its calls is pinned by host-server-workspaces-wiring.test.ts and
* workspace-route.test.tsx under mobile/src.
*/
import { z } from 'zod'
import { expect, test } from './helpers/orca-app'
import {
launchConvertedProfile,
skipUnlessManagedDockerHost,
withManagedHost
} from './helpers/orcad-managed-workspace'
import { createDesktopMobilePairingOffer } from './helpers/paired-electron-client'
import { pairMobileClient, type PairedMobileSocket } from './helpers/paired-mobile-client'
import { toRuntimeExecutionHostId } from '../../src/shared/execution-host'
import { MOBILE_DESKTOP_RELAY_RUNTIME_CAPABILITY } from '../../src/shared/mobile-desktop-relay-contract'
skipUnlessManagedDockerHost()
const StatusSchema = z.looseObject({ capabilities: z.array(z.string()) })
const WorktreeListSchema = z.looseObject({
worktrees: z.array(z.looseObject({ worktreeId: z.string(), path: z.string() }))
})
const HostsSchema = z.object({
hosts: z.array(
z.looseObject({
hostId: z.string(),
label: z.string(),
health: z.string(),
relay: z.string()
})
)
})
const HostWorktreesSchema = z.object({
worktrees: z
.array(
z.looseObject({
worktreeId: z.string(),
path: z.string(),
hostId: z.string()
})
)
.nullable()
})
const DirectorySchema = z.array(z.looseObject({ name: z.string() }))
let requestId = 0
async function phoneCall<T>(
socket: PairedMobileSocket,
method: string,
params: unknown,
schema: z.ZodType<T>,
executionHost?: string
): Promise<T> {
requestId += 1
const id = `phone-${requestId}`
socket.send(id, method, params, executionHost)
let frame: PairedMobileSocket['frames'][number] | undefined
await expect
.poll(() => (frame = socket.frames.find((entry) => entry.id === id)), {
timeout: 60_000
})
.toBeDefined()
expect(frame!.ok, `${method}: ${JSON.stringify(frame!.error)}`).toBe(true)
return schema.parse(frame!.result)
}
test("a paired phone lists and opens the projects on the desktop's managed SSH host", async (// oxlint-disable-next-line no-empty-pattern -- This exercise owns its app launch.
{}, testInfo) => {
test.setTimeout(12 * 60_000)
await withManagedHost(testInfo, async (fixture) => {
const { page, seeded, environmentId } = await launchConvertedProfile(fixture)
const serverHostId = toRuntimeExecutionHostId(environmentId)
const phone = pairMobileClient(await createDesktopMobilePairingOffer(page))
const socket = await phone.openSocket()
try {
const status = await phoneCall(socket, 'status.get', {}, StatusSchema)
expect(status.capabilities).toContain(MOBILE_DESKTOP_RELAY_RUNTIME_CAPABILITY)
// The desktop's own list: what a page without server rows shows. The relay-era copies stay hidden.
const own = await phoneCall(socket, 'worktree.ps', { limit: 1_000 }, WorktreeListSchema)
expect(own.worktrees.map((row) => row.path)).not.toContain(seeded.repoPath)
// The SSH host listed once, under its SSH name, as the managed server the phone can reach.
const { hosts } = await phoneCall(socket, 'mobileRelay.hosts.list', {}, HostsSchema)
const sshLabel = fixture.host.input.label
expect(hosts).toEqual([
expect.objectContaining({
hostId: serverHostId,
label: sshLabel,
relay: 'ready'
})
])
let listed: z.infer<typeof HostWorktreesSchema>['worktrees'] = null
await expect
.poll(
async () => {
listed = (
await phoneCall(
socket,
'mobileRelay.hosts.worktrees',
{ hostId: serverHostId },
HostWorktreesSchema
)
).worktrees
return listed?.map((row) => row.path) ?? []
},
{ timeout: 60_000 }
)
.toContain(seeded.repoPath)
const project = listed!.find((row) => row.path === seeded.repoPath)!
expect(project.hostId).toBe(serverHostId)
// Opening the project reads it on the server, through the desktop.
const entries = await phoneCall(
socket,
'files.readDir',
{ worktree: `id:${project.worktreeId}`, relativePath: '' },
DirectorySchema,
serverHostId
)
expect(entries.length).toBeGreaterThan(0)
} finally {
socket.close()
phone.dispose()
}
})
})