test(e2e): reveal specs create workspaces through main so they pass with the sidebar switch on (#30642)

* test(e2e): create reveal-spec workspaces through main so they pass with the sidebar switch on

The agent-send picker and host-section reveal specs injected fake repos and
workspaces into the window store. With ORCA_SIDEBAR_INPUTS=main the sidebar
lists main's rows, so those workspaces never appeared. The SSH host is now an
unreachable target seeded into the stopped profile, and the paired-server host
is a real headless orca serve host; pins, lineage and notes go through main.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(e2e): type the seeded hosts as execution host ids

Co-Authored-By: Claude <noreply@anthropic.com>

* test(e2e): poll the reveal specs' page setup instead of awaiting it across CDP

An evaluate that awaits a preload promise sometimes fails with "Resulting
promise was garbage collected" in switch-on runs. evaluatePolled runs the work
in the page, polls its outcome, and still fails on a rejected or failed write.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jinwoo Hong
2026-10-19 05:10:40 -04:00
committed by GitHub
co-authored by Claude
parent 7a1b5336a8
commit 0c3807e73b
4 changed files with 579 additions and 404 deletions
+181 -241
View File
@@ -1,255 +1,195 @@
import { expect, test } from './helpers/orca-app'
import {
waitForActiveWorktree,
waitForSessionReady,
waitForStartupWorktreeRefresh
} from './helpers/store'
import { evaluatePolled } from './helpers/page-evaluate-polled'
import { launchWithUnreachableSshWorkspaces } from './helpers/seeded-ssh-workspaces'
for (const hostId of ['local', 'ssh:picker-builder'] as const) {
for (const host of ['local', 'ssh'] as const) {
for (const hiddenBy of ['host', 'parent'] as const) {
test(`agent send picker reveals ${hostId} target hidden by ${hiddenBy} and restores collapse on close`, async ({
orcaPage
test(`agent send picker reveals ${host} target hidden by ${hiddenBy} and restores collapse on close`, async ({
testRepoPath
}, testInfo) => {
await orcaPage.emulateMedia({ reducedMotion: 'reduce' })
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await waitForStartupWorktreeRefresh(orcaPage)
const ids = await orcaPage.evaluate(
async ({ hostId, hiddenBy }) => {
const store = window.__store!
const state = store.getState()
const sourceRepo = state.repos[0]
const source = state.worktreesByRepo[sourceRepo.id][0]
const remoteHostId = hostId === 'local' ? 'ssh:picker-builder' : hostId
const remoteRepo = {
...sourceRepo,
id: 'picker-remote-repo',
displayName: 'Remote project',
executionHostId: remoteHostId,
connectionId: remoteHostId.startsWith('ssh:') ? 'picker-builder' : null
}
const local = {
...source,
hostId: 'local' as const,
isPinned: false,
displayName: 'Local workspace'
}
const remote = {
...source,
id: 'picker-remote-parent',
repoId: remoteRepo.id,
hostId: remoteHostId,
instanceId: 'remote-parent-instance',
isMainWorktree: false,
isPinned: false,
displayName: 'Remote workspace'
}
const parent = hostId === 'local' ? local : remote
const now = Date.now()
const localChild = state.worktreesByRepo[sourceRepo.id].find(
(worktree) => worktree.id !== source.id
)
const child =
hostId === 'local'
? localChild
: {
...parent,
id: 'picker-child',
instanceId: 'picker-child-instance',
isMainWorktree: false
}
if (hiddenBy === 'parent' && !child) {
throw new Error('Seeded child workspace missing')
}
const targetId = hiddenBy === 'parent' ? child!.id : parent.id
const target = {
...parent,
...(hiddenBy === 'parent' ? child : {}),
displayName: hiddenBy === 'parent' ? 'Child workspace' : parent.displayName,
diffComments: [
{
id: 'picker-note',
worktreeId: targetId,
filePath: 'README.md',
source: 'diff' as const,
lineNumber: 1,
body: 'Check sidebar disclosure',
side: 'modified' as const,
createdAt: now
}
]
}
if (hostId === 'local') {
await state.updateWorktreeMeta(
target.id,
{
displayName: target.displayName,
isPinned: false,
diffComments: target.diffComments
},
{ executionHostId: 'local' }
)
if (hiddenBy === 'parent') {
await state.assignWorktreeParent(target.id, { parentWorktreeId: parent.id })
test.setTimeout(3 * 60_000)
const launched = await launchWithUnreachableSshWorkspaces(testInfo, testRepoPath, {
label: 'Remote builder',
workspaces: [
{ name: 'parent', displayName: 'Remote workspace' },
{ name: 'child', displayName: 'Remote child' }
]
})
try {
const { page, ssh, localWorktreeId } = launched
await page.emulateMedia({ reducedMotion: 'reduce' })
const ids = await evaluatePolled(
page,
async ({ host, hiddenBy, ssh, localWorktreeId }) => {
const store = window.__store!
const state = store.getState()
const localChild = Object.values(state.worktreesByRepo)
.flat()
.find((worktree) => worktree.hostId === 'local' && worktree.id !== localWorktreeId)
const hostId = host === 'local' ? 'local' : ssh.hostId
const parent = host === 'local' ? localWorktreeId : ssh.worktreeIds.parent!
const child = host === 'local' ? localChild?.id : ssh.worktreeIds.child
if (hiddenBy === 'parent' && !child) {
throw new Error('Seeded child workspace missing')
}
}
const localWorktrees =
hostId === 'local' ? (hiddenBy === 'parent' ? [local, target] : [target]) : [local]
const remoteWorktrees =
hostId !== 'local' ? (hiddenBy === 'parent' ? [remote, target] : [target]) : [remote]
const collapsedGroups = [
hiddenBy === 'host' ? `host:${hostId}` : `lineage:${hostId}|${parent.id}`
]
const worktreeLineageById =
hiddenBy === 'parent'
? {
[target.id]: {
worktreeId: target.id,
worktreeInstanceId: target.instanceId!,
parentWorktreeId: parent.id,
parentWorktreeInstanceId: parent.instanceId!,
origin: 'orchestration' as const,
capture: {
source: 'orchestration-context' as const,
confidence: 'explicit' as const
},
const target = hiddenBy === 'parent' ? child! : parent
const now = Date.now()
const noted = await state.updateWorktreeMeta(
target,
{
...(hiddenBy === 'parent' ? { displayName: 'Child workspace' } : {}),
isPinned: false,
diffComments: [
{
id: 'picker-note',
worktreeId: target,
filePath: 'README.md',
source: 'diff' as const,
lineNumber: 1,
body: 'Check sidebar disclosure',
side: 'modified' as const,
createdAt: now
}
}
: {}
const tabId = 'picker-agent-tab'
const leafId = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'
const paneKey = `${tabId}:${leafId}`
const ptyId = 'picker-agent-pty'
await window.api.ui.set({ groupBy: 'none', collapsedGroups })
state.setSidebarOpen(true)
state.setShowSleepingWorkspaces(true)
state.setHideDefaultBranchWorkspace(false)
state.setFilterRepoIds([])
state.setRightSidebarTab('source-control')
state.setRightSidebarOpen(true)
store.setState({
groupBy: 'none',
repos: [sourceRepo, remoteRepo],
worktreesByRepo: { [sourceRepo.id]: localWorktrees, [remoteRepo.id]: remoteWorktrees },
worktreeLineageById,
visibleWorkspaceHostIds: ['local', remoteHostId],
workspaceHostScope: 'all',
activeRepoId: target.repoId,
activeWorktreeId: target.id,
activeWorkspaceKey: `worktree:${target.id}`,
activeWorkspaceExecutionHostId: hostId,
pendingRevealWorktree: null,
agentSendPopoverTargetMode: null,
collapsedGroups: new Set(collapsedGroups),
agentActivityDisplayMode: 'compact',
worktreeCardProperties: [
...state.worktreeCardProperties.filter((property) => property !== 'inline-agents'),
'inline-agents'
],
sshTargetLabels: new Map([['picker-builder', 'Remote builder']]),
sshConnectionStates: new Map([
[
'picker-builder',
{
targetId: 'picker-builder',
status: 'connected',
error: null,
reconnectAttempt: 0
}
]
]),
tabsByWorktree: {
...state.tabsByWorktree,
[target.id]: [
...(state.tabsByWorktree[target.id] ?? []),
{
id: tabId,
worktreeId: target.id,
ptyId,
title: 'Claude',
customTitle: null,
color: null,
sortOrder: 1,
createdAt: now
}
]
},
terminalLayoutsByTabId: {
...state.terminalLayoutsByTabId,
[tabId]: {
root: { type: 'leaf', leafId },
activeLeafId: leafId,
expandedLeafId: null,
ptyIdsByLeafId: { [leafId]: ptyId }
}
},
ptyIdsByTabId: { ...state.ptyIdsByTabId, [tabId]: [ptyId] },
agentStatusByPaneKey: {
...state.agentStatusByPaneKey,
[paneKey]: {
state: 'working',
prompt: 'Check sidebar disclosure',
updatedAt: now,
stateStartedAt: now,
agentType: 'claude',
paneKey,
worktreeId: target.id,
stateHistory: []
}
]
},
{ executionHostId: hostId }
)
if (!noted.ok) {
throw new Error(`Writing the note failed: ${noted.error}`)
}
})
return {
target: target.id,
control: hostId === 'local' ? remote.id : local.id,
collapsedGroups
}
},
{ hostId, hiddenBy }
)
const targetRow = orcaPage.locator(
`[data-worktree-sidebar] [role="option"][data-worktree-id=${JSON.stringify(ids.target)}]`
)
const controlRow = orcaPage.locator(
`[data-worktree-sidebar] [role="option"][data-worktree-id=${JSON.stringify(ids.control)}]`
)
await expect(targetRow).toHaveCount(0)
await expect(controlRow).toBeVisible()
const sendButton = orcaPage.getByRole('button', {
name: 'Send notes to an agent',
exact: true
})
await expect(sendButton).toBeEnabled()
await orcaPage.screenshot({
path: testInfo.outputPath('before-picker.png'),
clip: { x: 0, y: 170, width: 280, height: 500 }
})
await sendButton.click()
await expect(orcaPage.getByText('Send notes to', { exact: true })).toBeVisible()
try {
await expect(targetRow).toBeVisible()
await expect(targetRow.locator('[data-agent-send-target="eligible"]')).toBeVisible()
} finally {
await orcaPage.screenshot({
path: testInfo.outputPath('after-picker.png'),
if (hiddenBy === 'parent') {
await state.assignWorktreeParent(target, { parentWorktreeId: parent })
}
const targetRepoId = Object.values(store.getState().worktreesByRepo)
.flat()
.find((worktree) => worktree.id === target)!.repoId
const collapsedGroups = [
hiddenBy === 'host' ? `host:${hostId}` : `lineage:${hostId}|${parent}`
]
const tabId = 'picker-agent-tab'
const leafId = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'
const paneKey = `${tabId}:${leafId}`
const ptyId = 'picker-agent-pty'
await window.api.ui.set({ groupBy: 'none', collapsedGroups })
state.setSidebarOpen(true)
state.setShowSleepingWorkspaces(true)
state.setHideDefaultBranchWorkspace(false)
state.setFilterRepoIds([])
state.setRightSidebarTab('source-control')
state.setRightSidebarOpen(true)
const current = store.getState()
store.setState({
groupBy: 'none',
visibleWorkspaceHostIds: ['local', ssh.hostId],
workspaceHostScope: 'all',
activeRepoId: targetRepoId,
activeWorktreeId: target,
activeWorkspaceKey: `worktree:${target}`,
activeWorkspaceExecutionHostId: hostId,
pendingRevealWorktree: null,
agentSendPopoverTargetMode: null,
collapsedGroups: new Set(collapsedGroups),
agentActivityDisplayMode: 'compact',
worktreeCardProperties: [
...current.worktreeCardProperties.filter(
(property) => property !== 'inline-agents'
),
'inline-agents'
],
tabsByWorktree: {
...current.tabsByWorktree,
[target]: [
...(current.tabsByWorktree[target] ?? []),
{
id: tabId,
worktreeId: target,
ptyId,
title: 'Claude',
customTitle: null,
color: null,
sortOrder: 1,
createdAt: now
}
]
},
terminalLayoutsByTabId: {
...current.terminalLayoutsByTabId,
[tabId]: {
root: { type: 'leaf', leafId },
activeLeafId: leafId,
expandedLeafId: null,
ptyIdsByLeafId: { [leafId]: ptyId }
}
},
ptyIdsByTabId: { ...current.ptyIdsByTabId, [tabId]: [ptyId] },
agentStatusByPaneKey: {
...current.agentStatusByPaneKey,
[paneKey]: {
state: 'working',
prompt: 'Check sidebar disclosure',
updatedAt: now,
stateStartedAt: now,
agentType: 'claude',
paneKey,
worktreeId: target,
stateHistory: []
}
}
})
return {
target,
control: host === 'local' ? ssh.worktreeIds.parent! : localWorktreeId,
collapsedGroups
}
},
{ host, hiddenBy, ssh, localWorktreeId }
)
const targetRow = page.locator(
`[data-worktree-sidebar] [role="option"][data-worktree-id=${JSON.stringify(ids.target)}]`
)
const controlRow = page.locator(
`[data-worktree-sidebar] [role="option"][data-worktree-id=${JSON.stringify(ids.control)}]`
)
await expect(targetRow).toHaveCount(0)
await expect(controlRow).toBeVisible()
const sendButton = page.getByRole('button', {
name: 'Send notes to an agent',
exact: true
})
await expect(sendButton).toBeEnabled()
await page.screenshot({
path: testInfo.outputPath('before-picker.png'),
clip: { x: 0, y: 170, width: 280, height: 500 }
})
await sendButton.click()
await expect(page.getByText('Send notes to', { exact: true })).toBeVisible()
try {
await expect(targetRow).toBeVisible()
await expect(targetRow.locator('[data-agent-send-target="eligible"]')).toBeVisible()
} finally {
await page.screenshot({
path: testInfo.outputPath('after-picker.png'),
clip: { x: 0, y: 170, width: 280, height: 500 }
})
}
await expect(controlRow).toBeVisible()
await expect
.poll(() => page.evaluate(() => window.__store!.getState().pendingRevealWorktree))
.toBeNull()
await page.keyboard.press('Escape')
await expect
.poll(() => page.evaluate(() => window.__store!.getState().agentSendPopoverTargetMode))
.toBeNull()
await expect(targetRow).toHaveCount(0)
expect(await page.evaluate(() => [...window.__store!.getState().collapsedGroups])).toEqual(
ids.collapsedGroups
)
expect(
await page.evaluate(async () => (await window.api.ui.get()).collapsedGroups)
).toEqual(ids.collapsedGroups)
} finally {
await launched.dispose()
}
await expect(controlRow).toBeVisible()
await expect
.poll(() => orcaPage.evaluate(() => window.__store!.getState().pendingRevealWorktree))
.toBeNull()
await orcaPage.keyboard.press('Escape')
await expect
.poll(() => orcaPage.evaluate(() => window.__store!.getState().agentSendPopoverTargetMode))
.toBeNull()
await expect(targetRow).toHaveCount(0)
expect(
await orcaPage.evaluate(() => [...window.__store!.getState().collapsedGroups])
).toEqual(ids.collapsedGroups)
expect(
await orcaPage.evaluate(async () => (await window.api.ui.get()).collapsedGroups)
).toEqual(ids.collapsedGroups)
})
}
}
+39
View File
@@ -0,0 +1,39 @@
import type { Page } from '@stablyai/playwright-test'
import { expect } from './orca-app'
type Outcome<Result> =
| { state: 'pending' }
| { state: 'done'; value: Result }
| { state: 'failed'; error: string }
let nextKey = 0
/** page.evaluate whose outcome is polled: awaiting a preload promise across CDP can be garbage-collected.
* `args` cross as JSON. */
export async function evaluatePolled<Args, Result>(
page: Page,
run: (args: Args) => Promise<Result>,
args: Args
): Promise<Result> {
nextKey += 1
const key = JSON.stringify(`__e2eEvaluatePolled${nextKey}`)
await page.evaluate(`(() => {
const run = ${run.toString()};
globalThis[${key}] = { state: 'pending' };
Promise.resolve()
.then(() => run(${JSON.stringify(args)}))
.then(
(value) => { globalThis[${key}] = { state: 'done', value }; },
(error) => { globalThis[${key}] = { state: 'failed', error: String(error) }; }
);
})()`)
const read = () => page.evaluate<Outcome<Result>>(`globalThis[${key}]`)
await expect
.poll(async () => (await read()).state, { message: 'page work never settled' })
.not.toBe('pending')
const outcome = await read()
if (outcome.state !== 'done') {
throw new Error(outcome.state === 'failed' ? outcome.error : 'page work never settled')
}
return outcome.value
}
+108
View File
@@ -0,0 +1,108 @@
import type { ElectronApplication, Page, TestInfo } from '@stablyai/playwright-test'
import type { Repo } from '../../../src/shared/repo-types'
import { toSshExecutionHostId } from '../../../src/shared/execution-host'
import { expect } from './orca-app'
import { seedRelayEraTarget } from './orcad-upgrade-profile'
import { attachRepoAndOpenTerminal, createRestartSession } from './orca-restart'
import { mutateStoppedProfileState } from './persisted-profile-state'
import { waitForSessionReady, waitForStartupWorktreeRefresh } from './store'
export type SeededSshWorkspaces = {
targetId: string
hostId: `ssh:${string}`
repoId: string
/** Each seeded workspace's id, by its name. */
worktreeIds: Record<string, string>
}
/**
* An SSH host with one project and the workspaces main stores for it, seeded into a stopped profile.
* Nothing listens on its port, so the next launch lists exactly these stored rows.
*/
function seedUnreachableSshWorkspaces(
userDataDir: string,
input: { label: string; workspaces: { name: string; displayName: string }[] }
): SeededSshWorkspaces {
const targetId = seedRelayEraTarget(userDataDir, {
label: input.label,
host: '127.0.0.1',
port: 1,
username: 'nobody'
})
const hostId = toSshExecutionHostId(targetId)
const repoPath = '/srv/seeded-ssh-repo'
const repo: Repo = {
id: `repo-${targetId}`,
path: repoPath,
displayName: 'Remote project',
badgeColor: '#737373',
addedAt: Date.now(),
connectionId: targetId
}
const worktreeIds = Object.fromEntries(
input.workspaces.map(({ name }) => [name, `${repo.id}::${repoPath}/${name}`])
)
mutateStoppedProfileState(userDataDir, (state) => {
state.repos = [...(Array.isArray(state.repos) ? state.repos : []), repo]
state.worktreeMeta = {
...(state.worktreeMeta && typeof state.worktreeMeta === 'object' ? state.worktreeMeta : {}),
...Object.fromEntries(
input.workspaces.map(({ name, displayName }) => [
worktreeIds[name],
{ hostId, instanceId: `${name}-instance`, displayName }
])
)
}
})
return { targetId, hostId, repoId: repo.id, worktreeIds }
}
/**
* A desktop with the local test repo and an unreachable SSH host's seeded workspaces, both held by
* main as a user's would be. `dispose` closes the app and removes the profile.
*/
export async function launchWithUnreachableSshWorkspaces(
testInfo: TestInfo,
localRepoPath: string,
input: Parameters<typeof seedUnreachableSshWorkspaces>[1]
): Promise<{
page: Page
ssh: SeededSshWorkspaces
/** The local test repo's primary workspace. */
localWorktreeId: string
dispose: () => Promise<void>
}> {
const session = createRestartSession(testInfo)
let app: ElectronApplication | undefined
try {
const first = await session.launch()
app = first.app
await waitForSessionReady(first.page)
const localWorktreeId = await attachRepoAndOpenTerminal(first.page, localRepoPath)
await session.close(app)
app = undefined
const ssh = seedUnreachableSshWorkspaces(session.userDataDir, input)
const launched = await session.launch()
app = launched.app
await waitForSessionReady(launched.page)
await waitForStartupWorktreeRefresh(launched.page)
const seededIds = Object.values(ssh.worktreeIds)
await expect
.poll(() =>
launched.page.evaluate((ids) => {
const listed = new Set(
Object.values(window.__store!.getState().worktreesByRepo)
.flat()
.map((worktree) => worktree.id)
)
return ids.every((id) => listed.has(id))
}, seededIds)
)
.toBe(true)
const running = app
return { page: launched.page, ssh, localWorktreeId, dispose: () => session.dispose(running) }
} catch (error) {
await session.dispose(app)
throw error
}
}
+251 -163
View File
@@ -1,9 +1,15 @@
import { execFileSync } from 'node:child_process'
import { mkdirSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import type { Page, TestInfo } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import {
waitForActiveWorktree,
waitForSessionReady,
waitForStartupWorktreeRefresh
} from './helpers/store'
import { launchHeadlessPairedRuntimeHost } from './helpers/headless-paired-runtime-host'
import { launchPairedElectronClient } from './helpers/paired-electron-client'
import { attachRepoAndOpenTerminal } from './helpers/orca-restart'
import { evaluatePolled } from './helpers/page-evaluate-polled'
import { launchWithUnreachableSshWorkspaces } from './helpers/seeded-ssh-workspaces'
import { waitForSessionReady, waitForStartupWorktreeRefresh } from './helpers/store'
import { toRuntimeExecutionHostId, type ExecutionHostId } from '../../src/shared/execution-host'
const sections = [
{ groupBy: 'none', key: 'pinned', label: 'Pinned', pinned: true },
@@ -17,163 +23,245 @@ const sections = [
{ groupBy: 'pr-status', key: 'pr:in-progress', label: 'In progress', pinned: false }
] as const
for (const section of sections) {
test(`reveals ${section.key} workspaces without expanding another host's section`, async ({
orcaPage
}, testInfo) => {
await orcaPage.emulateMedia({ reducedMotion: 'reduce' })
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await waitForStartupWorktreeRefresh(orcaPage)
for (const hostId of ['ssh:reveal-builder', 'runtime:reveal-builder'] as const) {
for (const collapsedHost of [false, true]) {
const scenario = `${hostId.split(':')[0]}-collapsed-${collapsedHost}`
await test.step(scenario, async () => {
const ids = await orcaPage.evaluate(
async ({ hostId, collapsedHost, section }) => {
const store = window.__store!
const state = store.getState()
const sourceRepo = state.repos[0]
const sourceWorktree = state.worktreesByRepo[sourceRepo.id][0]
await state.updateWorktreeMeta(sourceWorktree.id, {
isPinned: section.pinned,
displayName: 'Local workspace'
})
const local = {
...sourceWorktree,
hostId: 'local' as const,
isPinned: section.pinned,
displayName: 'Local workspace'
}
const remoteRepo = {
...sourceRepo,
id: 'reveal-remote-repo',
executionHostId: hostId,
connectionId: hostId.startsWith('ssh:') ? 'reveal-builder' : null,
displayName: 'Remote project'
}
const remote = {
...sourceWorktree,
id: 'reveal-remote-workspace',
repoId: remoteRepo.id,
hostId,
isPinned: section.pinned,
isMainWorktree: false,
displayName: 'Remote workspace'
}
state.setActiveView('terminal')
state.setSidebarOpen(true)
await window.api.ui.set({ groupBy: section.groupBy })
state.setShowSleepingWorkspaces(true)
state.setHideDefaultBranchWorkspace(false)
state.setFilterRepoIds([])
const collapsedGroups = [
section.key,
`${section.key}:host:${hostId}`,
...(collapsedHost ? [`host:${hostId}`] : [])
]
await window.api.ui.set({ collapsedGroups })
store.setState({
groupBy: section.groupBy,
repos: [sourceRepo, remoteRepo],
worktreesByRepo: { [sourceRepo.id]: [local], [remoteRepo.id]: [remote] },
sshTargetLabels: new Map([['reveal-builder', 'Remote builder']]),
sshConnectionStates: new Map([
[
'reveal-builder',
{
targetId: 'reveal-builder',
status: 'connected',
error: null,
reconnectAttempt: 0
}
]
]),
visibleWorkspaceHostIds: ['local', hostId],
workspaceHostScope: 'all',
activeRepoId: remoteRepo.id,
activeWorktreeId: remote.id,
activeWorkspaceKey: `worktree:${remote.id}`,
activeWorkspaceExecutionHostId: hostId,
pendingRevealWorktree: null,
collapsedGroups: new Set(collapsedGroups)
})
return { local: local.id, remote: remote.id }
},
{ hostId, collapsedHost, section }
)
const headers = orcaPage.getByRole('button', { name: new RegExp(`^${section.label}`) })
const localRow = orcaPage.locator(
`[data-worktree-sidebar] [role="option"][data-worktree-id=${JSON.stringify(ids.local)}]`
)
const remoteRow = orcaPage.locator(
`[data-worktree-sidebar] [role="option"][data-worktree-id=${JSON.stringify(ids.remote)}]`
)
if (section.pinned || section.groupBy === 'workspace-status') {
await expect(headers.first()).toHaveAttribute('aria-expanded', 'false')
}
await expect(localRow).toHaveCount(0)
await expect(remoteRow).toHaveCount(0)
await orcaPage.screenshot({
path: testInfo.outputPath(`${scenario}-before-reveal.png`),
clip: { x: 0, y: 170, width: 280, height: 500 }
})
await orcaPage.getByRole('button', { name: 'Reveal active workspace' }).click()
await expect(remoteRow).toBeVisible()
await expect(remoteRow).toHaveAttribute('data-scroll-reveal-highlight', 'true')
await orcaPage.screenshot({
path: testInfo.outputPath(`${scenario}-after-reveal.png`),
clip: { x: 0, y: 170, width: 280, height: 500 }
})
await expect(headers).toHaveCount(2)
if (section.pinned || section.groupBy === 'workspace-status') {
await expect(headers.first()).toHaveAttribute('aria-expanded', 'false')
}
if (section.pinned || section.groupBy === 'workspace-status') {
await expect(headers.last()).toHaveAttribute('aria-expanded', 'true')
}
await expect(localRow).toHaveCount(0)
await headers.last().click()
await expect(remoteRow).toHaveCount(0)
await orcaPage.evaluate((localId) => {
const store = window.__store!
const local = Object.values(store.getState().worktreesByRepo)
.flat()
.find((worktree) => worktree.id === localId)!
store.setState({
activeRepoId: local.repoId,
activeWorktreeId: localId,
activeWorkspaceKey: `worktree:${localId}`,
activeWorkspaceExecutionHostId: 'local',
pendingRevealWorktree: null
})
}, ids.local)
await orcaPage.getByRole('button', { name: 'Reveal active workspace' }).click()
await expect(localRow).toBeVisible()
if (section.pinned || section.groupBy === 'workspace-status') {
await expect(headers.first()).toHaveAttribute('aria-expanded', 'true')
await expect(headers.last()).toHaveAttribute('aria-expanded', 'false')
}
await expect(remoteRow).toHaveCount(0)
// Finish the reveal and its persistence before resetting the next scenario.
await expect
.poll(() =>
orcaPage.evaluate(async () => ({
pendingRevealWorktree: window.__store!.getState().pendingRevealWorktree,
collapsedGroups: [...window.__store!.getState().collapsedGroups].sort(),
persistedCollapsedGroups: (await window.api.ui.get()).collapsedGroups?.sort()
}))
)
.toEqual({
pendingRevealWorktree: null,
collapsedGroups: [`${section.key}:host:${hostId}`],
persistedCollapsedGroups: [`${section.key}:host:${hostId}`]
})
await expect(localRow).toBeVisible()
await expect(remoteRow).toHaveCount(0)
})
/** Pins through main and waits for its answer. */
async function pinWorkspace(
page: Page,
worktreeId: string,
isPinned: boolean,
executionHostId: ExecutionHostId
): Promise<void> {
await evaluatePolled(
page,
async (args) => {
const written = await window.__store!.getState().updateWorktreeMeta(
args.worktreeId,
{ isPinned: args.isPinned },
{
executionHostId: args.executionHostId
}
)
if (!written.ok) {
throw new Error(`Pinning failed: ${written.error}`)
}
}
})
},
{ worktreeId, isPinned, executionHostId }
)
}
/** A desktop whose main holds a local workspace and one on another host, as a user's would. */
type RevealTopology = {
page: Page
local: string
remote: string
remoteHostId: ExecutionHostId
setRemotePinned: (pinned: boolean) => Promise<void>
}
async function expectRevealsWithinHost(
{ page, local, remote, remoteHostId, setRemotePinned }: RevealTopology,
testInfo: TestInfo
): Promise<void> {
for (const section of sections) {
for (const collapsedHost of [false, true]) {
const scenario = `${section.key}-collapsed-${collapsedHost}`
await test.step(scenario, async () => {
await setRemotePinned(section.pinned)
await pinWorkspace(page, local, section.pinned, 'local')
// The window must show both pins before the reveal, or the row opens in the other section.
await expect
.poll(() =>
page.evaluate(
(ids) =>
Object.values(window.__store!.getState().worktreesByRepo)
.flat()
.filter((worktree) => ids.includes(worktree.id))
.map((worktree) => worktree.isPinned),
[local, remote]
)
)
.toEqual([section.pinned, section.pinned])
const collapsedGroups = [
section.key,
`${section.key}:host:${remoteHostId}`,
...(collapsedHost ? [`host:${remoteHostId}`] : [])
]
await evaluatePolled(
page,
async ({ remote, remoteHostId, collapsedGroups, section }) => {
const store = window.__store!
const state = store.getState()
const remoteRepoId = Object.values(state.worktreesByRepo)
.flat()
.find((worktree) => worktree.id === remote)!.repoId
state.setActiveView('terminal')
state.setSidebarOpen(true)
state.setShowSleepingWorkspaces(true)
state.setHideDefaultBranchWorkspace(false)
state.setFilterRepoIds([])
await window.api.ui.set({ groupBy: section.groupBy, collapsedGroups })
store.setState({
groupBy: section.groupBy,
visibleWorkspaceHostIds: ['local', remoteHostId],
workspaceHostScope: 'all',
activeRepoId: remoteRepoId,
activeWorktreeId: remote,
activeWorkspaceKey: `worktree:${remote}`,
activeWorkspaceExecutionHostId: remoteHostId,
pendingRevealWorktree: null,
collapsedGroups: new Set(collapsedGroups)
})
},
{ remote, remoteHostId, collapsedGroups, section }
)
const headers = page.getByRole('button', { name: new RegExp(`^${section.label}`) })
const localRow = page.locator(
`[data-worktree-sidebar] [role="option"][data-worktree-id=${JSON.stringify(local)}]`
)
const remoteRow = page.locator(
`[data-worktree-sidebar] [role="option"][data-worktree-id=${JSON.stringify(remote)}]`
)
if (section.pinned || section.groupBy === 'workspace-status') {
await expect(headers.first()).toHaveAttribute('aria-expanded', 'false')
}
await expect(localRow).toHaveCount(0)
await expect(remoteRow).toHaveCount(0)
await page.screenshot({
path: testInfo.outputPath(`${scenario}-before-reveal.png`),
clip: { x: 0, y: 170, width: 280, height: 500 }
})
await page.getByRole('button', { name: 'Reveal active workspace' }).click()
await expect(remoteRow).toBeVisible()
await expect(remoteRow).toHaveAttribute('data-scroll-reveal-highlight', 'true')
await page.screenshot({
path: testInfo.outputPath(`${scenario}-after-reveal.png`),
clip: { x: 0, y: 170, width: 280, height: 500 }
})
await expect(headers).toHaveCount(2)
if (section.pinned || section.groupBy === 'workspace-status') {
await expect(headers.first()).toHaveAttribute('aria-expanded', 'false')
await expect(headers.last()).toHaveAttribute('aria-expanded', 'true')
}
await expect(localRow).toHaveCount(0)
await headers.last().click()
await expect(remoteRow).toHaveCount(0)
await page.evaluate((localId) => {
const store = window.__store!
const worktree = Object.values(store.getState().worktreesByRepo)
.flat()
.find((entry) => entry.id === localId)!
store.setState({
activeRepoId: worktree.repoId,
activeWorktreeId: localId,
activeWorkspaceKey: `worktree:${localId}`,
activeWorkspaceExecutionHostId: 'local',
pendingRevealWorktree: null
})
}, local)
await page.getByRole('button', { name: 'Reveal active workspace' }).click()
await expect(localRow).toBeVisible()
if (section.pinned || section.groupBy === 'workspace-status') {
await expect(headers.first()).toHaveAttribute('aria-expanded', 'true')
await expect(headers.last()).toHaveAttribute('aria-expanded', 'false')
}
await expect(remoteRow).toHaveCount(0)
// Finish the reveal and its persistence before resetting the next scenario.
await expect
.poll(() =>
page.evaluate(async () => ({
pendingRevealWorktree: window.__store!.getState().pendingRevealWorktree,
collapsedGroups: [...window.__store!.getState().collapsedGroups].sort(),
persistedCollapsedGroups: (await window.api.ui.get()).collapsedGroups?.sort()
}))
)
.toEqual({
pendingRevealWorktree: null,
collapsedGroups: [`${section.key}:host:${remoteHostId}`],
persistedCollapsedGroups: [`${section.key}:host:${remoteHostId}`]
})
await expect(localRow).toBeVisible()
await expect(remoteRow).toHaveCount(0)
})
}
}
}
test("reveals an unreachable SSH host's workspaces without expanding another host's section", async ({
testRepoPath
}, testInfo) => {
test.setTimeout(4 * 60_000)
const launched = await launchWithUnreachableSshWorkspaces(testInfo, testRepoPath, {
label: 'Remote builder',
workspaces: [{ name: 'feature', displayName: 'Remote workspace' }]
})
try {
const { page, ssh, localWorktreeId } = launched
await page.emulateMedia({ reducedMotion: 'reduce' })
const remote = ssh.worktreeIds.feature!
await expectRevealsWithinHost(
{
page,
local: localWorktreeId,
remote,
remoteHostId: ssh.hostId,
setRemotePinned: (isPinned) => pinWorkspace(page, remote, isPinned, ssh.hostId)
},
testInfo
)
} finally {
await launched.dispose()
}
})
test("reveals a paired server's workspaces without expanding another host's section", async ({
testRepoPath
}, testInfo) => {
test.setTimeout(4 * 60_000)
const serverRepo = testInfo.outputPath('server-repo')
mkdirSync(serverRepo, { recursive: true })
writeFileSync(path.join(serverRepo, 'README.md'), 'server repo\n')
const git = (...args: string[]) => execFileSync('git', args, { cwd: serverRepo, stdio: 'pipe' })
git('init', '-b', 'main')
git('add', 'README.md')
git('-c', 'user.name=E2E', '-c', 'user.email=e2e@test.local', 'commit', '-m', 'init')
const host = await launchHeadlessPairedRuntimeHost({ pairingScope: 'runtime' })
try {
const desktop = await launchPairedElectronClient(host.offer, testInfo, 'reveal-host-sections')
try {
const { page } = desktop
await page.emulateMedia({ reducedMotion: 'reduce' })
await waitForSessionReady(page)
const local = await attachRepoAndOpenTerminal(page, testRepoPath)
await waitForStartupWorktreeRefresh(page)
const added = await host.client.call<{ repo: { id: string } }>('repo.add', {
path: serverRepo,
kind: 'git'
})
const created = await host.client.call<{ worktree: { id: string } }>('worktree.create', {
repo: `id:${added.result.repo.id}`,
name: 'feature',
displayName: 'Remote workspace',
setupDecision: 'skip'
})
const remote = created.result.worktree.id
const remoteHostId = toRuntimeExecutionHostId(desktop.environmentId)
await expectRevealsWithinHost(
{
page,
local,
remote,
remoteHostId,
setRemotePinned: async (isPinned) => {
await host.client.call('worktree.set', { worktree: `id:${remote}`, isPinned })
}
},
testInfo
)
} finally {
await desktop.dispose()
}
} finally {
await host.dispose()
}
})