mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 16:03:26 +00:00
fix project group owner authorization refresh
This commit is contained in:
@@ -298,6 +298,37 @@ describe('filesystem-auth path containment', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps connection-free local folders authorized beside legacy SSH folders', () => {
|
||||
const localPath = resolve('/explicit-local-mixed-folder/local')
|
||||
const sshPath = resolve('/explicit-local-mixed-folder/ssh')
|
||||
const projectGroup = makeProjectGroup({
|
||||
parentPath: localPath,
|
||||
connectionId: undefined,
|
||||
executionHostId: 'local'
|
||||
})
|
||||
const store = makeStore([], {
|
||||
projectGroups: [projectGroup],
|
||||
folderWorkspaces: [
|
||||
makeFolderWorkspace({
|
||||
id: 'local-folder',
|
||||
folderPath: localPath,
|
||||
projectGroupId: projectGroup.id,
|
||||
connectionId: undefined,
|
||||
executionHostId: undefined
|
||||
}),
|
||||
makeFolderWorkspace({
|
||||
id: 'ssh-folder',
|
||||
folderPath: sshPath,
|
||||
projectGroupId: projectGroup.id,
|
||||
connectionId: 'ssh-1'
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
expect(isPathAllowed(localPath, store)).toBe(true)
|
||||
expect(isPathAllowed(sshPath, store)).toBe(false)
|
||||
})
|
||||
|
||||
it('does not authorize SSH-only folder-backed project group roots as local paths', async () => {
|
||||
const tempRoot = await mkdtemp(join(tmpdir(), 'orca-auth-remote-project-group-'))
|
||||
try {
|
||||
|
||||
@@ -171,16 +171,18 @@ function buildFolderAuthIndex(
|
||||
}
|
||||
}
|
||||
for (const workspace of folderWorkspaces) {
|
||||
if (
|
||||
workspace.connectionId === undefined &&
|
||||
!normalizeExecutionHostId(workspace.executionHostId)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
const group = resolveFolderWorkspaceProjectGroupWithLegacySsh(projectGroupIndex, workspace)
|
||||
if (!group) {
|
||||
continue
|
||||
}
|
||||
if (
|
||||
workspace.connectionId === undefined &&
|
||||
!normalizeExecutionHostId(workspace.executionHostId) &&
|
||||
group.connectionId === undefined &&
|
||||
!normalizeExecutionHostId(group.executionHostId)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
const ownerHostId = resolveFolderWorkspaceCatalogOwnerHostIdFromIndex(
|
||||
workspace,
|
||||
projectGroupIndex
|
||||
|
||||
@@ -322,6 +322,7 @@ import {
|
||||
import {
|
||||
filterFolderWorkspacesForVisibleHosts,
|
||||
filterProjectGroupsForVisibleHosts,
|
||||
buildFolderPathStatusRepoMembershipKey,
|
||||
getFolderPathStatusRouteOptionsForRows,
|
||||
getVisibleSidebarHostIdSet
|
||||
} from './worktree-list-host-filtering'
|
||||
@@ -731,6 +732,7 @@ type VirtualizedWorktreeViewportProps = {
|
||||
workspaceLineageByChildKey: Record<string, WorkspaceLineage>
|
||||
// Full canonical repo-id order; must include hidden repos or a reorder silently drops them.
|
||||
allRepoIds: string[]
|
||||
folderPathStatusRepoMembershipKey: string
|
||||
onReorderHostSections: (orderedHostIds: ExecutionHostId[]) => void
|
||||
onHostDragActiveChange: (active: boolean) => void
|
||||
prCache: AppState['prCache'] | null
|
||||
@@ -1464,6 +1466,7 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
||||
worktreeLineageById,
|
||||
workspaceLineageByChildKey,
|
||||
allRepoIds,
|
||||
folderPathStatusRepoMembershipKey,
|
||||
onReorderHostSections,
|
||||
onHostDragActiveChange,
|
||||
prCache,
|
||||
@@ -2023,16 +2026,6 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
||||
activeRuntimeEnvironmentId: s.settings?.activeRuntimeEnvironmentId ?? null
|
||||
}))
|
||||
)
|
||||
const folderPathStatusRepoMembershipKey = useMemo(
|
||||
() =>
|
||||
allRepoIds
|
||||
.map((repoId) => {
|
||||
const repo = repoMap.get(repoId)
|
||||
return `${repoId}:${repo?.path ?? ''}:${repo?.projectGroupId ?? ''}:${repo?.connectionId ?? ''}`
|
||||
})
|
||||
.join('\0'),
|
||||
[allRepoIds, repoMap]
|
||||
)
|
||||
const folderPathStatusSshConnectionKey = useMemo(
|
||||
() =>
|
||||
[...sshConnectionStates.entries()]
|
||||
@@ -5936,6 +5929,10 @@ const WorktreeList = React.memo(function WorktreeList({
|
||||
})
|
||||
}, [filterRepoIds, groupBy, visibleReposForRows, visibleWorktrees, worktreesByRepo])
|
||||
const allRepoIds = useMemo(() => repos.map((r) => r.id), [repos])
|
||||
const folderPathStatusRepoMembershipKey = useMemo(
|
||||
() => buildFolderPathStatusRepoMembershipKey(repos),
|
||||
[repos]
|
||||
)
|
||||
|
||||
// Why: subscribe on a flat key array (useShallow) so progress ticks don't rebuild the whole row model.
|
||||
// Split on first space — creationId is a UUID (no space) so a space-containing repoId stays intact.
|
||||
@@ -7132,6 +7129,7 @@ const WorktreeList = React.memo(function WorktreeList({
|
||||
worktreeLineageById={worktreeLineageById}
|
||||
workspaceLineageByChildKey={workspaceLineageByChildKey}
|
||||
allRepoIds={allRepoIds}
|
||||
folderPathStatusRepoMembershipKey={folderPathStatusRepoMembershipKey}
|
||||
onReorderHostSections={handleReorderHostSections}
|
||||
onHostDragActiveChange={setHostDragActive}
|
||||
prCache={prCache}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { FolderWorkspace, ProjectGroup, Repo, Worktree } from '../../../../
|
||||
import { addHostSectionRows } from './host-section-rows'
|
||||
import { buildRows, type Row } from './worktree-list-groups'
|
||||
import {
|
||||
buildFolderPathStatusRepoMembershipKey,
|
||||
filterFolderWorkspacesForVisibleHosts,
|
||||
filterProjectGroupsForVisibleHosts,
|
||||
getFolderPathStatusRouteOptionsForRows,
|
||||
@@ -140,6 +141,35 @@ function folderWorkspace(overrides: Partial<FolderWorkspace> = {}): FolderWorksp
|
||||
}
|
||||
|
||||
describe('WorktreeList host filtering ownership', () => {
|
||||
it('tracks each same-id repo owner in the folder path-status refresh key', () => {
|
||||
type MembershipRepo = Parameters<typeof buildFolderPathStatusRepoMembershipKey>[0][number]
|
||||
const local: MembershipRepo = {
|
||||
id: 'same:repo',
|
||||
path: '/local:repo',
|
||||
projectGroupId: 'group:local',
|
||||
connectionId: null,
|
||||
executionHostId: 'local'
|
||||
}
|
||||
const runtime: MembershipRepo = {
|
||||
id: 'same:repo',
|
||||
path: '/runtime:repo',
|
||||
projectGroupId: 'group:runtime',
|
||||
connectionId: null,
|
||||
executionHostId: 'runtime:env:1'
|
||||
}
|
||||
const before = buildFolderPathStatusRepoMembershipKey([local, runtime])
|
||||
const after = buildFolderPathStatusRepoMembershipKey([
|
||||
{ ...local, projectGroupId: 'group:moved' },
|
||||
runtime
|
||||
])
|
||||
|
||||
expect(after).not.toBe(before)
|
||||
expect(JSON.parse(before)).toEqual([
|
||||
['local', 'same:repo', '/local:repo', 'group:local', ''],
|
||||
['runtime:env:1', 'same:repo', '/runtime:repo', 'group:runtime', '']
|
||||
])
|
||||
})
|
||||
|
||||
it('uses runtime execution host stamps before SSH/default fallbacks for project groups', () => {
|
||||
expect(
|
||||
getProjectGroupExecutionHostIdForRows(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
ALL_EXECUTION_HOSTS_SCOPE,
|
||||
getRepoExecutionHostId,
|
||||
normalizeExecutionHostId,
|
||||
parseExecutionHostId,
|
||||
toSshExecutionHostId,
|
||||
@@ -7,7 +8,7 @@ import {
|
||||
type ExecutionHostScope
|
||||
} from '../../../../shared/execution-host'
|
||||
import type { FolderWorkspacePathStatusRequest } from '../../../../shared/folder-workspace-path-status'
|
||||
import type { FolderWorkspace, ProjectGroup } from '../../../../shared/types'
|
||||
import type { FolderWorkspace, ProjectGroup, Repo } from '../../../../shared/types'
|
||||
import {
|
||||
buildProjectGroupSidebarIndex,
|
||||
findProjectGroupForFolderWorkspace,
|
||||
@@ -19,6 +20,23 @@ import {
|
||||
|
||||
export { getFolderWorkspaceExecutionHostIdForRows, getProjectGroupExecutionHostIdForRows }
|
||||
|
||||
export function buildFolderPathStatusRepoMembershipKey(
|
||||
repos: readonly Pick<
|
||||
Repo,
|
||||
'id' | 'path' | 'projectGroupId' | 'connectionId' | 'executionHostId'
|
||||
>[]
|
||||
): string {
|
||||
return JSON.stringify(
|
||||
repos.map((repo) => [
|
||||
getRepoExecutionHostId(repo),
|
||||
repo.id,
|
||||
repo.path,
|
||||
repo.projectGroupId ?? '',
|
||||
repo.connectionId ?? ''
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
/** null means "no host filter" — every host is visible. */
|
||||
export function getVisibleSidebarHostIdSet(
|
||||
visibleWorkspaceHostIds: readonly ExecutionHostId[] | null | undefined,
|
||||
|
||||
Reference in New Issue
Block a user