Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 6 additions & 14 deletions src/renderer/src/components/sidebar/folder-workspace-host-id.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
import { toSshExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host'
import type { ExecutionHostId } from '../../../../shared/execution-host'
import type { FolderWorkspace } from '../../../../shared/folder-workspace-types'
import type { ProjectGroup } from '../../../../shared/project-group-types'
import { getFolderWorkspaceExecutionHostIdForRows } from './worktree-list/listing/host-filtering'

/**
* Which host section a folder workspace's row belongs to.
*
* Single source of truth: header counting, host bucketing and reveal all resolve
* the host through here. Two resolvers that drift is how folder workspaces went
* missing from the sidebar in the first place (#15362). Deliberately distinct
* from the host *filter* resolver, which also consults executionHostId — this
* one must match where the row actually renders.
*/
/** Host headers, filtering and reveal must agree on the folder's execution owner. */
export function getFolderWorkspaceHostId(
folderWorkspace: Pick<FolderWorkspace, 'connectionId'>,
projectGroup: Pick<ProjectGroup, 'connectionId'>,
folderWorkspace: Pick<FolderWorkspace, 'connectionId' | 'executionHostId'>,
projectGroup: Pick<ProjectGroup, 'connectionId' | 'executionHostId'>,
defaultHostId: ExecutionHostId
): ExecutionHostId {
const connectionId = folderWorkspace.connectionId ?? projectGroup.connectionId
return connectionId ? toSshExecutionHostId(connectionId) : defaultHostId
return getFolderWorkspaceExecutionHostIdForRows({ folderWorkspace, projectGroup, defaultHostId })
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,88 +19,106 @@ const localGroup: ProjectGroup = {
createdAt: 1,
updatedAt: 1
}
const remoteGroup = { ...localGroup, id: 'remote-group', connectionId: 'builder' }
const local = makeFolderWorkspace({ id: 'local-folder', projectGroupId: localGroup.id })
const remote = makeFolderWorkspace({
id: 'remote-folder',
projectGroupId: remoteGroup.id,
connectionId: 'builder'
})

for (const [groupBy, key] of [
['none', 'all'],
['workspace-status', 'workspace-status:in-progress'],
['pr-status', 'pr:in-progress']
] as const) {
const rows = buildRows(
groupBy,
[],
new Map(),
null,
new Set(),
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
[localGroup, remoteGroup],
undefined,
undefined,
undefined,
undefined,
undefined,
[local, remote]
)
const sectioned = addHostSectionRows({
rows,
hostOptions: [
{ id: 'local', kind: 'local', label: 'Local', detail: '', health: 'local' },
{ id: 'ssh:builder', kind: 'ssh', label: 'Remote', detail: '', health: 'available' }
],
workspaceHostScope: 'all',
visibleWorkspaceHostIds: ['local', 'ssh:builder'],
defaultHostId: 'local'
for (const hostId of ['ssh:builder', 'runtime:managed-builder'] as const) {
const remoteGroup = {
...localGroup,
id: 'remote-group',
connectionId: 'builder',
executionHostId: hostId
}
const remote = makeFolderWorkspace({
id: 'remote-folder',
projectGroupId: remoteGroup.id,
connectionId: 'builder',
executionHostId: hostId
})

it(`${groupBy}: collapses folder workspaces only on the owning host`, () => {
const collapsed = scopeHostSectionCollapse({ rows: sectioned, collapsedGroups: new Set([key]) })
expect(
collapsed
.filter((row) => row.type === 'folder-workspace')
.map((row) => row.folderWorkspace.id)
).toEqual([remote.id])
const remoteCollapsed = scopeHostSectionCollapse({
rows: sectioned,
collapsedGroups: new Set([`${key}:host:ssh:builder`])
})
expect(
remoteCollapsed
.filter((row) => row.type === 'folder-workspace')
.map((row) => row.folderWorkspace.id)
).toEqual([local.id])
})

it(`${groupBy}: reveals a remote folder using the rendered host lane key`, () => {
const keys = getFolderWorkspaceRevealGroupKeys(
folderWorkspaceKey(remote.id),
[local, remote],
for (const [groupBy, key] of [
['none', 'all'],
['workspace-status', 'workspace-status:in-progress'],
['pr-status', 'pr:in-progress']
] as const) {
const rows = buildRows(
groupBy,
[],
new Map(),
null,
new Set(),
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
[localGroup, remoteGroup],
{
groupBy,
defaultHostId: 'local',
hostScopedGroups: true
}
undefined,
undefined,
undefined,
undefined,
undefined,
[local, remote]
)
expect(keys).toContain(`${key}:host:ssh:builder`)
expect(keys).toContain('host:ssh:builder')
expect(keys).not.toContain(key)
expect(
getFolderWorkspaceRevealGroupKeys(folderWorkspaceKey(remote.id), [remote], [remoteGroup], {
groupBy,
defaultHostId: 'local'
const sectioned = addHostSectionRows({
rows,
hostOptions: [
{ id: 'local', kind: 'local', label: 'Local', detail: '', health: 'local' },
{
id: hostId,
kind: hostId.startsWith('runtime:') ? 'runtime' : 'ssh',
label: 'Remote',
detail: '',
health: 'available'
}
],
workspaceHostScope: 'all',
visibleWorkspaceHostIds: ['local', hostId],
defaultHostId: 'local'
})

it(`${hostId} ${groupBy}: collapses folder workspaces only on the owning host`, () => {
const collapsed = scopeHostSectionCollapse({
rows: sectioned,
collapsedGroups: new Set([key])
})
).toContain(key)
})
expect(
collapsed
.filter((row) => row.type === 'folder-workspace')
.map((row) => row.folderWorkspace.id)
).toEqual([remote.id])
const remoteCollapsed = scopeHostSectionCollapse({
rows: sectioned,
collapsedGroups: new Set([`${key}:host:${hostId}`])
})
expect(
remoteCollapsed
.filter((row) => row.type === 'folder-workspace')
.map((row) => row.folderWorkspace.id)
).toEqual([local.id])
})

it(`${hostId} ${groupBy}: reveals a remote folder using the rendered host lane key`, () => {
const keys = getFolderWorkspaceRevealGroupKeys(
folderWorkspaceKey(remote.id),
[local, remote],
[localGroup, remoteGroup],
{
groupBy,
defaultHostId: 'local',
hostScopedGroups: true
}
)
expect(keys).toContain(`${key}:host:${hostId}`)
expect(keys).toContain(`host:${hostId}`)
expect(keys).not.toContain(key)
expect(
getFolderWorkspaceRevealGroupKeys(folderWorkspaceKey(remote.id), [remote], [remoteGroup], {
groupBy,
defaultHostId: 'local'
})
).toContain(key)
})
}
}
28 changes: 26 additions & 2 deletions tests/e2e/ssh-orcad-auto-convert.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,9 @@ test('a relay host converts to managed orcad on connect and keeps its source', a
})
})

test('a relay-era profile converts its SSH host on the first connect after upgrading', async (// oxlint-disable-next-line no-empty-pattern -- Playwright's second fixture arg is testInfo; the first must be an object destructure to opt out of the default fixture set.
{}, testInfo) => {
test('a relay-era profile converts its SSH host on the first connect after upgrading', async ({
testRepoPath
}, testInfo) => {
test.setTimeout(20 * 60_000)
const host = startHost(testInfo)
const session = createRestartSession(testInfo, {
Expand All @@ -226,6 +227,7 @@ test('a relay-era profile converts its SSH host on the first connect after upgra
const first = await session.launch()
app = first.app
await waitForSessionReady(first.page)
await first.page.evaluate((repoPath) => window.api.repos.add({ path: repoPath }), testRepoPath)
await session.close(app)
app = null
const seeded = seedRelayEraProfile(session.userDataDir, host.input, {
Expand All @@ -244,6 +246,28 @@ test('a relay-era profile converts its SSH host on the first connect after upgra
app = upgraded.app
await waitForSessionReady(upgraded.page)
await convertAndRetain(upgraded.page, session.userDataDir, seeded)
const environment = (
await upgraded.page.evaluate(() => window.api.runtimeEnvironments.list())
).find((entry) => entry.orcadDeployment?.sshTargetId === seeded.targetId)
if (!environment) {
throw new Error('Converted host is missing from the environment catalog')
}
const hostId = `runtime:${environment.id}` as const
await upgraded.page.evaluate((hostId) => {
const state = window.__store!.getState()
state.setGroupBy('none')
window.__store!.setState({ visibleWorkspaceHostIds: ['local', hostId] })
}, hostId)
const folder = upgraded.page.getByText('orcad upgrade folder', { exact: true })
await expect(folder).toBeVisible()
await upgraded.page.screenshot({ path: testInfo.outputPath('managed-folder-host-section.png') })
const sectionHeader = folder.locator('xpath=preceding::*[@data-host-header-drag-id][1]')
await expect(sectionHeader).toHaveAttribute('data-host-header-drag-id', hostId)
const managedHeader = upgraded.page.locator(`[data-host-header-drag-id="${hostId}"]`)
await managedHeader.click()
await expect(folder).toBeHidden()
await managedHeader.click()
await expect(folder).toBeVisible()
} finally {
if (app) {
await session.close(app)
Expand Down
Loading