test(sidebar): prove paired folder host grouping

This commit is contained in:
Jinwoo-H
2026-08-20 20:08:13 -04:00
parent 4c50f662e4
commit 62c78167e3
4 changed files with 262 additions and 1 deletions
@@ -1,4 +1,7 @@
import type { HostSectionRow } from '../../host-section-rows'
import { getFolderWorkspaceHostId } from '../../folder-workspace-host-id'
import { composeWorktreeHostIdentity } from '../../../../../../shared/worktree/host-qualified-identity'
import { folderWorkspaceKey } from '../../../../../../shared/workspace-scope'
type WorktreeItemRow = Extract<HostSectionRow, { type: 'item' }>
export type RenderRow =
@@ -25,7 +28,11 @@ export function getRenderRowKey(row: RenderRow): string {
return `pending:${row.creationId}`
}
if (row.type === 'folder-workspace') {
return `folder-workspace:${row.folderWorkspace.id}`
const hostId = getFolderWorkspaceHostId(row.folderWorkspace, row.projectGroup, 'local')
return `folder-workspace:${composeWorktreeHostIdentity(
hostId,
folderWorkspaceKey(row.folderWorkspace.id)
)}`
}
return `wt:${row.rowKey}`
}
@@ -1,6 +1,8 @@
import { describe, expect, it } from 'vitest'
import type { VirtualItem } from '@tanstack/react-virtual'
import type { ExecutionHostId } from '../../../../../../shared/execution-host'
import type { FolderWorkspace } from '../../../../../../shared/folder-workspace-types'
import type { ProjectGroup } from '../../../../../../shared/project-group-types'
import {
HOST_STICKY_PINNED_HEIGHT,
buildLineageRowRekeyMap,
@@ -41,6 +43,21 @@ function itemStub(id: string): RenderRow {
return { type: 'item', key: id } as unknown as RenderRow
}
function folderRow(executionHostId: ExecutionHostId): RenderRow {
return {
type: 'folder-workspace',
key: 'folder-workspace:same-id',
folderWorkspace: {
id: 'same-id',
executionHostId,
projectGroupId: 'group-id'
} as FolderWorkspace,
projectGroup: { id: 'group-id', executionHostId } as ProjectGroup,
depth: 0,
groupDepth: 0
}
}
function virtualItem(index: number, start: number): VirtualItem {
return { index, start } as VirtualItem
}
@@ -74,6 +91,13 @@ describe('getRenderRowKey', () => {
'hdr:workspace-status:in-progress'
)
})
it('keeps identical folder workspace ids distinct across hosts', () => {
expect(getRenderRowKey(folderRow('local'))).toBe('folder-workspace:local|folder:same-id')
expect(getRenderRowKey(folderRow('runtime:owner'))).toBe(
'folder-workspace:runtime:owner|folder:same-id'
)
})
})
describe('getActiveStickyIndexesForScroll', () => {
@@ -0,0 +1,97 @@
import type { Page, TestInfo } from '@stablyai/playwright-test'
import { expect } from './helpers/orca-app'
export async function expectFolderWorkspaceSidebarGrouping(
page: Page,
testInfo: TestInfo,
args: { folderPath: string; hostId: `runtime:${string}` }
): Promise<void> {
const target = await page.evaluate(({ folderPath, hostId }) => {
const store = window.__store
if (!store) {
throw new Error('Renderer store unavailable')
}
const workspace = store
.getState()
.folderWorkspaces.find(
(candidate) => candidate.folderPath === folderPath && candidate.executionHostId === hostId
)
if (!workspace) {
throw new Error('Runtime folder unavailable for sidebar grouping')
}
store.getState().setVisibleWorkspaceHostIds(['local', hostId])
return { id: workspace.id, name: workspace.name, executionHostId: workspace.executionHostId }
}, args)
expect(target.executionHostId).toBe(args.hostId)
const cases = [
{ id: 'repo', label: 'Project' },
{ id: 'workspace-status', label: 'Status' },
{ id: 'pr-status', label: 'PR' },
{ id: 'none', label: 'None' }
] as const
const identity = `${args.hostId}|folder:${target.id}`
const placements: { groupBy: string; hostId: string; identity: string }[] = []
for (const groupBy of cases) {
await page.evaluate((groupById) => window.__store?.getState().setGroupBy(groupById), groupBy.id)
await expect
.poll(() => page.evaluate(() => window.__store?.getState().groupBy ?? null))
.toBe(groupBy.id)
await expect
.poll(() =>
page.evaluate(
({ folderPath, hostId }) =>
window.__store
?.getState()
.folderWorkspaces.some(
(workspace) =>
workspace.folderPath === folderPath && workspace.executionHostId === hostId
) ?? false,
args
)
)
.toBe(true)
await page.evaluate(
({ folderWorkspaceId, hostId }) => {
window.__store?.getState().revealWorktreeInSidebar(`folder:${folderWorkspaceId}`, {
behavior: 'auto',
executionHostId: hostId
})
},
{ folderWorkspaceId: target.id, hostId: args.hostId }
)
const folderRow = page.locator(`[data-worktree-host-identity="${identity}"]`)
await expect(folderRow).toBeVisible()
await expect(folderRow).toContainText(target.name)
const placement = await page.evaluate((targetIdentity) => {
const rows = [
...document.querySelectorAll<HTMLElement>('[data-worktree-sidebar] [data-index]')
].sort(
(left, right) =>
Number(left.dataset.index ?? Number.MAX_SAFE_INTEGER) -
Number(right.dataset.index ?? Number.MAX_SAFE_INTEGER)
)
let hostId: string | null = null
for (const row of rows) {
const hostHeader = row.querySelector<HTMLElement>('[data-host-header-drag-id]')
if (hostHeader) {
hostId = hostHeader.dataset.hostHeaderDragId ?? null
}
if (row.dataset.worktreeHostIdentity === targetIdentity) {
return { hostId, identity: row.dataset.worktreeHostIdentity ?? '' }
}
}
return null
}, identity)
expect(placement).toEqual({ hostId: args.hostId, identity })
placements.push({ groupBy: groupBy.id, ...placement! })
await page.screenshot({
path: testInfo.outputPath(`sta-4964-${groupBy.id}.png`),
fullPage: true
})
}
console.info(`[sta-4964-sidebar] ${JSON.stringify(placements)}`)
}
+133
View File
@@ -0,0 +1,133 @@
import { mkdirSync } from 'node:fs'
import type { FolderWorkspace } from '../../src/shared/folder-workspace-types'
import type { ProjectGroup } from '../../src/shared/project-group-types'
import { expect, test } from './helpers/orca-app'
import {
createRuntimeDesktopPairingOffer,
launchPairedElectronClient
} from './helpers/paired-electron-client'
import { waitForSessionReady } from './helpers/store'
import { expectFolderWorkspaceSidebarGrouping } from './sta-4964-folder-sidebar-oracle'
test('keeps a runtime-owned folder under its paired host in every grouping mode @headful', async ({
orcaPage
}, testInfo) => {
test.setTimeout(120_000)
await waitForSessionReady(orcaPage)
const offer = await createRuntimeDesktopPairingOffer(orcaPage)
const client = await launchPairedElectronClient(offer, testInfo, 'STA-4964 runtime')
const runtimeHostId = `runtime:${client.environmentId}` as const
const runtimeFolderPath = testInfo.outputPath('runtime-folder')
const localFolderPath = testInfo.outputPath('local-collision-folder')
mkdirSync(runtimeFolderPath, { recursive: true })
mkdirSync(localFolderPath, { recursive: true })
try {
await client.app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0]?.show())
await expect.poll(() => client.page.evaluate(() => window.__store != null)).toBe(true)
const seeded = await client.page.evaluate(
async (args) => {
const store = window.__store
if (!store) {
throw new Error('Renderer store unavailable')
}
await store.getState().setActiveRuntimeEnvironmentPreference(null)
const groupResponse = await window.api.runtimeEnvironments.call({
selector: args.environmentId,
method: 'projectGroup.create',
params: {
name: 'Runtime-owned group',
parentPath: args.runtimeFolderPath,
createdFrom: 'manual'
},
timeoutMs: 15_000
})
if (!groupResponse.ok) {
throw new Error(groupResponse.error.message)
}
const createdGroup = (groupResponse.result as { group: ProjectGroup }).group
const folderResponse = await window.api.runtimeEnvironments.call({
selector: args.environmentId,
method: 'folderWorkspace.create',
params: {
folderPath: args.runtimeFolderPath,
name: 'Runtime-owned folder',
projectGroupId: createdGroup.id
},
timeoutMs: 15_000
})
if (!folderResponse.ok) {
throw new Error(folderResponse.error.message)
}
const createdFolder = (folderResponse.result as { folderWorkspace: FolderWorkspace })
.folderWorkspace
await Promise.all([
store.getState().fetchProjectGroups({ runtimeEnvironmentId: args.environmentId }),
store.getState().fetchFolderWorkspaces({ runtimeEnvironmentId: args.environmentId })
])
const state = store.getState()
const runtimeFolder = state.folderWorkspaces.find(
(workspace) =>
workspace.id === createdFolder.id && workspace.executionHostId === args.runtimeHostId
)
const runtimeGroup = state.projectGroups.find(
(group) => group.id === createdGroup.id && group.executionHostId === args.runtimeHostId
)
if (!runtimeFolder || !runtimeGroup) {
throw new Error('Runtime-owned folder catalog unavailable')
}
const localGroupId = `${runtimeGroup.id}-local-collision`
store.setState({
folderWorkspaces: [
{
...runtimeFolder,
name: 'Local same-ID collision',
folderPath: args.localFolderPath,
projectGroupId: localGroupId,
executionHostId: 'local'
},
...state.folderWorkspaces
],
projectGroups: [
{
...runtimeGroup,
id: localGroupId,
name: 'Local same-ID collision',
parentPath: args.localFolderPath,
executionHostId: 'local'
},
...state.projectGroups
],
workspaceHostScope: 'all',
activeWorkspaceExecutionHostId: 'local'
})
await Promise.all([
store.getState().fetchProjectGroups({ runtimeEnvironmentId: args.environmentId }),
store.getState().fetchFolderWorkspaces({ runtimeEnvironmentId: args.environmentId })
])
return store
.getState()
.folderWorkspaces.filter((workspace) => workspace.id === createdFolder.id)
.map((workspace) => ({
id: workspace.id,
executionHostId: workspace.executionHostId
}))
},
{
environmentId: client.environmentId,
localFolderPath,
runtimeFolderPath,
runtimeHostId
}
)
expect(new Set(seeded.map((workspace) => workspace.id)).size).toBe(1)
expect(seeded.map((workspace) => workspace.executionHostId)).toEqual(['local', runtimeHostId])
await expectFolderWorkspaceSidebarGrouping(client.page, testInfo, {
folderPath: runtimeFolderPath,
hostId: runtimeHostId
})
} finally {
await client.dispose()
}
})