Skip to content
Closed
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
8 changes: 5 additions & 3 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,9 @@ jobs:
. != "tests/e2e/ssh-orcad-markdown-conversion.spec.ts" and
. != "tests/e2e/ssh-orcad-markdown-link-refresh.spec.ts" and
. != "tests/e2e/ssh-orcad-markdown-live-documents.spec.ts" and
. != "tests/e2e/ssh-orcad-open-in-owner.spec.ts"
. != "tests/e2e/ssh-orcad-open-in-owner.spec.ts" and
. != "tests/e2e/ssh-orcad-terminal-drop-owner.spec.ts" and
. != "tests/e2e/ssh-orcad-explorer-move-owner.spec.ts"
)' <<<"$TEST_FILES_JSON" > "$RUNNER_TEMP/general-e2e-specs"
fi
mapfile -t TEST_FILES < "$RUNNER_TEMP/general-e2e-specs"
Expand Down Expand Up @@ -592,7 +594,7 @@ jobs:
orcad-auto-convert-docker:
name: ssh host auto-converts to managed orcad (Docker)
needs: [build, prepare-native-cache]
if: inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/ssh-orcad-auto-convert.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-idle-exit.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-browser-capabilities.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-browser-service-status.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-browser-routing.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-editor-ownership.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-markdown-conversion.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-markdown-link-refresh.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-markdown-live-documents.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-open-in-owner.spec.ts')
if: inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/ssh-orcad-auto-convert.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-idle-exit.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-browser-capabilities.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-browser-service-status.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-browser-routing.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-editor-ownership.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-markdown-conversion.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-markdown-link-refresh.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-markdown-live-documents.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-open-in-owner.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-terminal-drop-owner.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-explorer-move-owner.spec.ts')
runs-on: ubuntu-latest
timeout-minutes: 45
env:
Expand Down Expand Up @@ -628,7 +630,7 @@ jobs:
ORCA_RELAY_PATH: ${{ github.workspace }}/out/relay
run: |
export ORCA_E2E_ORCAD_CONVERT_TEMPLATE="$RUNNER_TEMP/orcad-convert-template"
xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh pnpm exec playwright test --config tests/playwright.config.ts tests/e2e/ssh-orcad-auto-convert.spec.ts tests/e2e/ssh-orcad-idle-exit.spec.ts tests/e2e/ssh-orcad-browser-capabilities.spec.ts tests/e2e/ssh-orcad-browser-service-status.spec.ts tests/e2e/ssh-orcad-browser-routing.spec.ts tests/e2e/ssh-orcad-editor-ownership.spec.ts tests/e2e/ssh-orcad-markdown-conversion.spec.ts tests/e2e/ssh-orcad-markdown-link-refresh.spec.ts tests/e2e/ssh-orcad-markdown-live-documents.spec.ts tests/e2e/ssh-orcad-open-in-owner.spec.ts --project=electron-headless --workers=1
xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh pnpm exec playwright test --config tests/playwright.config.ts tests/e2e/ssh-orcad-auto-convert.spec.ts tests/e2e/ssh-orcad-idle-exit.spec.ts tests/e2e/ssh-orcad-browser-capabilities.spec.ts tests/e2e/ssh-orcad-browser-service-status.spec.ts tests/e2e/ssh-orcad-browser-routing.spec.ts tests/e2e/ssh-orcad-editor-ownership.spec.ts tests/e2e/ssh-orcad-markdown-conversion.spec.ts tests/e2e/ssh-orcad-markdown-link-refresh.spec.ts tests/e2e/ssh-orcad-markdown-live-documents.spec.ts tests/e2e/ssh-orcad-open-in-owner.spec.ts tests/e2e/ssh-orcad-terminal-drop-owner.spec.ts tests/e2e/ssh-orcad-explorer-move-owner.spec.ts --project=electron-headless --workers=1
- uses: actions/upload-artifact@v7
if: failure()
with:
Expand Down
10 changes: 8 additions & 2 deletions config/scripts/ci-e2e-job-selection.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export const ORCAD_MARKDOWN_LINK_REFRESH_E2E_SPEC =
'tests/e2e/ssh-orcad-markdown-link-refresh.spec.ts'
export const ORCAD_MARKDOWN_LIVE_DOCUMENTS_E2E_SPEC =
'tests/e2e/ssh-orcad-markdown-live-documents.spec.ts'
export const ORCAD_EXPLORER_MOVE_OWNER_E2E_SPEC = 'tests/e2e/ssh-orcad-explorer-move-owner.spec.ts'
export const ORCAD_TERMINAL_DROP_OWNER_E2E_SPEC = 'tests/e2e/ssh-orcad-terminal-drop-owner.spec.ts'
export const ORCAD_OPEN_IN_OWNER_E2E_SPEC = 'tests/e2e/ssh-orcad-open-in-owner.spec.ts'
export const DEDICATED_E2E_SPECS = [
...DOCKER_SSH_E2E_SPECS,
Expand All @@ -80,7 +82,9 @@ export const DEDICATED_E2E_SPECS = [
ORCAD_MARKDOWN_CONVERSION_E2E_SPEC,
ORCAD_MARKDOWN_LINK_REFRESH_E2E_SPEC,
ORCAD_MARKDOWN_LIVE_DOCUMENTS_E2E_SPEC,
ORCAD_OPEN_IN_OWNER_E2E_SPEC
ORCAD_OPEN_IN_OWNER_E2E_SPEC,
ORCAD_TERMINAL_DROP_OWNER_E2E_SPEC,
ORCAD_EXPLORER_MOVE_OWNER_E2E_SPEC
]
const dedicatedSpecs = new Set(DEDICATED_E2E_SPECS)
const dockerSpecs = new Set(DOCKER_SSH_E2E_SPECS)
Expand Down Expand Up @@ -130,7 +134,9 @@ export function classifyE2eJobs(input, sshSourceChanged = 'false') {
spec === ORCAD_MARKDOWN_CONVERSION_E2E_SPEC ||
spec === ORCAD_MARKDOWN_LINK_REFRESH_E2E_SPEC ||
spec === ORCAD_MARKDOWN_LIVE_DOCUMENTS_E2E_SPEC ||
spec === ORCAD_OPEN_IN_OWNER_E2E_SPEC
spec === ORCAD_OPEN_IN_OWNER_E2E_SPEC ||
spec === ORCAD_TERMINAL_DROP_OWNER_E2E_SPEC ||
spec === ORCAD_EXPLORER_MOVE_OWNER_E2E_SPEC
)
}
}
Expand Down
42 changes: 42 additions & 0 deletions config/scripts/ci-e2e-job-selection.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,48 @@ const workflow = parse(readFileSync('.github/workflows/e2e.yml', 'utf8'))
const prWorkflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8'))
const classify = (specs, ssh = 'false') => classifyE2eJobs(JSON.stringify(specs), ssh)

it('runs Explorer move ownership through the owned Docker fixture', () => {
const spec = 'tests/e2e/ssh-orcad-explorer-move-owner.spec.ts'
expect(classify([spec])).toEqual({ e2e_run_changed: false, e2e_needs_build: true })
expect(selectGeneralE2eSpecs([spec])).toEqual([])
const job = workflow.jobs['orcad-auto-convert-docker']
expect(job.if).toContain(spec)
expect(
job.steps.some((step) => step.run?.includes(spec) && step.env?.ORCA_E2E_SSH_DOCKER === '1')
).toBe(true)
for (const source of [
'src/renderer/src/lib/workspace-file-drag-owner.ts',
'src/renderer/src/components/right-sidebar/useFileExplorerMoveDrop.ts',
'src/renderer/src/components/right-sidebar/useFileExplorerDragDrop.ts',
'src/renderer/src/components/right-sidebar/useFileExplorerRowDrag.ts',
'src/renderer/src/components/right-sidebar/use-file-explorer-tree-pane-state.ts'
]) {
expect(selectPrE2eSpecs([source]), source).toContain(spec)
}
expect(selectPrE2eSpecs(['src/renderer/src/lib/workspace-file-drag-owner.ts'])).toContain(
'tests/e2e/ssh-orcad-terminal-drop-owner.spec.ts'
)
})

it('runs actual terminal drop ownership in the template-building Docker job', () => {
const spec = 'tests/e2e/ssh-orcad-terminal-drop-owner.spec.ts'
expect(classify([spec])).toEqual({ e2e_run_changed: false, e2e_needs_build: true })
const job = workflow.jobs['orcad-auto-convert-docker']
expect(job.if).toContain(spec)
expect(
job.steps.some((step) => step.run?.includes(spec) && step.env?.ORCA_E2E_SSH_DOCKER === '1')
).toBe(true)
for (const source of [
'src/renderer/src/components/terminal-pane/terminal-drop-handler.ts',
'src/renderer/src/components/terminal-pane/terminal-drop-internal-rejection-message.ts',
'src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx',
'src/renderer/src/lib/workspace-file-drag.ts',
'src/renderer/src/components/right-sidebar/FileExplorerRow.tsx'
]) {
expect(selectPrE2eSpecs([source]), source).toContain(spec)
}
})

it('skips the general consumer only when every requested spec has a dedicated owner', () => {
for (const spec of DEDICATED_E2E_SPECS) {
expect(classify([spec]).e2e_run_changed, spec).toBe(false)
Expand Down
24 changes: 24 additions & 0 deletions config/scripts/pr-e2e-source-routing.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,30 @@ export const PR_E2E_SOURCE_ROUTES = [
file
)
},
{
id: 'ssh.orcad-explorer-move-owner',
specs: ['tests/e2e/ssh-orcad-explorer-move-owner.spec.ts'],
matches: (file) =>
/^tests\/e2e\/helpers\/(?:orcad-convert-(?:flow|host)|orcad-upgrade-profile|docker-ssh-relay-target)\.ts$/.test(
file
) ||
(isProductSource(file) &&
/^src\/renderer\/src\/(?:lib\/workspace-file-drag(?:-source|-owner)?\.ts|components\/right-sidebar\/(?:FileExplorer(?:Row|VirtualRows)\.tsx|useFileExplorer(?:MoveDrop|DragDrop|RowDrag)\.ts|use-file-explorer-tree-pane-state\.ts|file-explorer-operation-owner\.ts))$/.test(
file
))
},
{
id: 'ssh.orcad-terminal-drop-owner',
specs: ['tests/e2e/ssh-orcad-terminal-drop-owner.spec.ts'],
matches: (file) =>
/^tests\/e2e\/helpers\/(?:orcad-convert-(?:flow|host)|orcad-upgrade-profile|docker-ssh-relay-target)\.ts$/.test(
file
) ||
(isProductSource(file) &&
/^src\/renderer\/src\/(?:lib\/(?:workspace-file-drag(?:-source|-owner)?|worktree-runtime-owner)\.ts|components\/(?:right-sidebar\/FileExplorer(?:Row|VirtualRows)\.tsx|terminal-pane\/(?:terminal-drop-(?:handler|internal-rejection-message|target|worktree-path)|TerminalPaneSurface)\.tsx?))$/.test(
file
))
},
{
id: 'ssh.orcad-open-in-owner',
specs: ['tests/e2e/ssh-orcad-open-in-owner.spec.ts'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const testState: { executionHostId: ExecutionHostId } = vi.hoisted(() => ({

vi.mock('@/store', () => ({ useAppStore: { getState: () => ({}) } }))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getExecutionHostIdForWorktree: () => testState.executionHostId
getKnownExecutionHostIdForWorktree: () => testState.executionHostId
}))

const { CombinedDiffFileTreeRow } = await import('./combined-diff-file-tree-row')
Expand Down
17 changes: 11 additions & 6 deletions src/renderer/src/components/right-sidebar/FileExplorerRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import {
encodeWorkspaceFilePaths,
WORKSPACE_FILE_PATH_MIME,
WORKSPACE_FILE_PATHS_MIME,
writeWorkspaceFileDragSourceIfResolved
writeWorkspaceFileDragSourceIfResolved,
type WorkspaceFileDragSource
} from '@/lib/workspace-file-drag'
import type { ExecutionHostId } from '../../../../shared/execution-host'
import type { GitFileStatus } from '../../../../shared/git-status-types'
import type { FileExplorerMoveDropHandler } from './useFileExplorerMoveDrop'
import { STATUS_LABELS } from './status-display'
import { RENAME_HOTSPOT_ATTR } from './file-explorer-dir-toggle-timing'
import type { TreeNode } from './file-explorer-types'
Expand Down Expand Up @@ -38,7 +39,9 @@ export type FileExplorerRowProps = {
connectionId?: string | null
sourceWorkspaceId?: string | null
/** Resolved at dragstart so the virtualized list pays nothing per render. */
resolveDragSourceHostId?: (paths: readonly string[]) => ExecutionHostId | null
resolveDragSourceOwner?: (
paths: readonly string[]
) => Omit<WorkspaceFileDragSource, 'workspaceId'> | null
runtimeDownloadContext?: RuntimeFileOperationArgs | null
supportsFolderDownload?: boolean
canOpenInOrcaBrowser: boolean
Expand All @@ -60,7 +63,7 @@ export type FileExplorerRowProps = {
onRequestDelete: () => void
onCollapseFolderSubtree: () => void
onFindInFolder: () => void
onMoveDrop: (sourcePath: string, destDir: string) => void
onMoveDrop: FileExplorerMoveDropHandler
onDragTargetChange: (dir: string | null) => void
onDragSourceChange: (path: string | null) => void
onDragExpandDir: (dirPath: string) => void
Expand All @@ -83,7 +86,7 @@ export function FileExplorerRow({
deleteShortcutLabel,
connectionId,
sourceWorkspaceId,
resolveDragSourceHostId,
resolveDragSourceOwner,
runtimeDownloadContext,
supportsFolderDownload = false,
canOpenInOrcaBrowser,
Expand Down Expand Up @@ -164,10 +167,12 @@ export function FileExplorerRow({
if (paths.length > 1) {
event.dataTransfer.setData(WORKSPACE_FILE_PATHS_MIME, encodeWorkspaceFilePaths(paths))
}
const sourceOwner = resolveDragSourceOwner?.(paths)
writeWorkspaceFileDragSourceIfResolved(
event.dataTransfer,
sourceWorkspaceId,
resolveDragSourceHostId?.(paths)
sourceOwner?.executionHostId,
sourceOwner?.runtimeEnvironmentId
)
event.dataTransfer.effectAllowed = 'copyMove'
onDragSourceChange(node.path)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import type { DirCache, FileExplorerOperationOwner, TreeNode } from './file-expl
import type { FileExplorerRowProjection } from './file-explorer-row-projection'
import type { RuntimeFileOperationArgs } from '@/runtime/runtime-file-client'
import { getFileExplorerOperationExecutionHostId } from './file-explorer-operation-owner'
import type { ExecutionHostId } from '../../../../shared/execution-host'
import type { WorkspaceFileDragSource } from '@/lib/workspace-file-drag'
import type { FileExplorerMoveDropHandler } from './useFileExplorerMoveDrop'

type FileExplorerVirtualRowsProps = {
virtualizer: Virtualizer<HTMLDivElement, Element>
Expand Down Expand Up @@ -52,7 +53,7 @@ type FileExplorerVirtualRowsProps = {
onRequestDelete: (node: TreeNode) => void
onCollapseFolderSubtree: (node: TreeNode) => void
onFindInFolder: (node: TreeNode) => void
onMoveDrop: (sourcePath: string, destDir: string) => void
onMoveDrop: FileExplorerMoveDropHandler
onDragTargetChange: (dir: string | null) => void
onDragSourceChange: (path: string | null) => void
onDragExpandDir: (dirPath: string) => void
Expand Down Expand Up @@ -83,22 +84,27 @@ function getDraggedPathOperationOwner(

/** Null unless every dragged row came from one host: a mixed-owner drag has no
* single source to stamp, so it must fail closed at the drop target. */
function resolveDragSourceExecutionHostId(
function resolveDragSourceOwner(
rowProjection: FileExplorerRowProjection,
dirCache: Record<string, DirCache> | undefined,
paths: readonly string[]
): ExecutionHostId | null {
let sourceExecutionHostId: ExecutionHostId | null = null
): Omit<WorkspaceFileDragSource, 'workspaceId'> | null {
let sourceOwner: Omit<WorkspaceFileDragSource, 'workspaceId'> | null = null
for (const path of paths) {
const executionHostId = getFileExplorerOperationExecutionHostId(
getDraggedPathOperationOwner(rowProjection, dirCache, path)
)
if (!executionHostId || (sourceExecutionHostId && executionHostId !== sourceExecutionHostId)) {
const owner = getDraggedPathOperationOwner(rowProjection, dirCache, path)
const executionHostId = getFileExplorerOperationExecutionHostId(owner)
const runtimeEnvironmentId = owner?.kind === 'runtime' ? owner.environmentId : undefined
if (
!executionHostId ||
(sourceOwner &&
(executionHostId !== sourceOwner.executionHostId ||
runtimeEnvironmentId !== sourceOwner.runtimeEnvironmentId))
) {
return null
}
sourceExecutionHostId = executionHostId
sourceOwner = { executionHostId, ...(runtimeEnvironmentId ? { runtimeEnvironmentId } : {}) }
}
return sourceExecutionHostId
return sourceOwner
}

/** Renders virtual and inline-input rows using display-relative indentation but worktree-relative operation paths. */
Expand Down Expand Up @@ -155,8 +161,8 @@ export function FileExplorerVirtualRows(props: FileExplorerVirtualRowsProps): Re
const visibleSelectionCount = rowProjection.countVisiblePaths(selectedPaths)
// Resolved at dragstart, not per render: the virtualizer re-renders on every
// scroll frame and only a drag ever reads this.
const resolveDragSourceHostId = (paths: readonly string[]): ExecutionHostId | null =>
resolveDragSourceExecutionHostId(rowProjection, dirCache, paths)
const resolveSourceOwner = (paths: readonly string[]) =>
resolveDragSourceOwner(rowProjection, dirCache, paths)

return (
<div className="relative w-full" style={{ height: `${virtualizer.getTotalSize()}px` }}>
Expand Down Expand Up @@ -236,7 +242,7 @@ export function FileExplorerVirtualRows(props: FileExplorerVirtualRowsProps): Re
deleteShortcutLabel={deleteShortcutLabel}
connectionId={connectionId}
sourceWorkspaceId={sourceWorkspaceId}
resolveDragSourceHostId={resolveDragSourceHostId}
resolveDragSourceOwner={resolveSourceOwner}
runtimeDownloadContext={runtimeDownloadContext}
supportsFolderDownload={supportsFolderDownload}
canOpenInOrcaBrowser={canOpenInOrcaBrowser(n.path)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,30 +147,41 @@ describe('file explorer draggable rows carry the wheel-scroll marker', () => {

expect(readWorkspaceFileDragSource(transfer)).toEqual({
executionHostId: 'runtime:old-env',
workspaceId: 'old-workspace'
workspaceId: 'old-workspace',
runtimeEnvironmentId: 'old-env'
})
})

it('omits ownership when selected cached rows came from different hosts', async () => {
const localNode: TreeNode = { ...fileNode, operationOwner: { kind: 'local' } }
const sshNode: TreeNode = {
...directoryNode,
operationOwner: { kind: 'ssh', connectionId: 'remote-1' }
it.each([false, true])(
'omits mixed-owner selections, including nested SSH: %s',
async (nested) => {
const localNode: TreeNode = {
...fileNode,
operationOwner: nested
? { kind: 'runtime', executionHostId: 'ssh:remote-1', environmentId: 'env-a' }
: { kind: 'local' }
}
const sshNode: TreeNode = {
...directoryNode,
operationOwner: nested
? { kind: 'runtime', executionHostId: 'ssh:remote-1', environmentId: 'env-b' }
: { kind: 'ssh', connectionId: 'remote-1' }
}
const container = await renderToBody(
virtualRowsElement([localNode, sshNode], {
selectedPaths: new Set([localNode.path, sshNode.path]),
sourceWorkspaceId: 'workspace-1'
})
)
const transfer = new DataTransfer()
const event = new Event('dragstart', { bubbles: true, cancelable: true })
Object.defineProperty(event, 'dataTransfer', { value: transfer })

container.querySelector('[data-file-explorer-row]')?.dispatchEvent(event)

expect(readWorkspaceFileDragSource(transfer)).toBeNull()
}
const container = await renderToBody(
virtualRowsElement([localNode, sshNode], {
selectedPaths: new Set([localNode.path, sshNode.path]),
sourceWorkspaceId: 'workspace-1'
})
)
const transfer = new DataTransfer()
const event = new Event('dragstart', { bubbles: true, cancelable: true })
Object.defineProperty(event, 'dataTransfer', { value: transfer })

container.querySelector('[data-file-explorer-row]')?.dispatchEvent(event)

expect(readWorkspaceFileDragSource(transfer)).toBeNull()
})
)

// A selection outlives the rows that showed it: nothing prunes selectedPaths
// when a directory collapses, and the drag still carries every selected path.
Expand Down
Loading
Loading