Move shared file watches when the selected remote host changes (#26632)

* Invalidate shared file watch targets when the selected host changes

* Watch the selected host's own workspace root when hosts share a workspace id

* Watch each open editor on its own execution host instead of the selected host

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
Neil
2026-10-09 05:47:35 -07:00
committed by GitHub
co-authored by m4air
parent 05e7040f28
commit 05ab94bd65
7 changed files with 470 additions and 64 deletions
@@ -290,6 +290,7 @@ it.each([
'src/renderer/src/components/right-sidebar/useFileExplorerWatch.ts',
'src/renderer/src/components/right-sidebar/file-explorer-operation-owner.ts',
'src/renderer/src/components/right-sidebar/FileExplorer.tsx',
'src/renderer/src/hooks/editor-external-watch-targets.ts',
'src/renderer/src/components/right-sidebar/useFileExplorerTree.ts',
'src/renderer/src/components/right-sidebar/use-file-explorer-tree-load-effects.ts'
])('routes %s to the template-building selected-host explorer lane', (file) => {
+1
View File
@@ -75,6 +75,7 @@ export const PR_E2E_SOURCE_ROUTES = [
(file === 'src/renderer/src/components/right-sidebar/useFileExplorerWatch.ts' ||
file === 'src/renderer/src/components/right-sidebar/file-explorer-operation-owner.ts' ||
file === 'src/renderer/src/components/right-sidebar/FileExplorer.tsx' ||
file === 'src/renderer/src/hooks/editor-external-watch-targets.ts' ||
file === 'src/renderer/src/components/right-sidebar/useFileExplorerTree.ts' ||
file === 'src/renderer/src/components/right-sidebar/use-file-explorer-tree-load-effects.ts')
},
@@ -0,0 +1,109 @@
// @vitest-environment happy-dom
import { act, cleanup, renderHook } from '@testing-library/react'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { makeWorktree } from '@/store/slices/worktrees-slice-test-fixtures'
import { buildEditorExternalWatchEventHandler } from '@/hooks/editor-external-watch-event-reconciliation'
import { selectEditorExternalWatchTargets } from '@/hooks/editor-external-watch-targets'
import { getDefaultSettings } from '../../../../shared/constants'
import { useFileExplorerWatch } from './useFileExplorerWatch'
vi.mock('@/store', () => ({
useAppStore: Object.assign((selector: (state: unknown) => unknown) => selector({}), {
getState: () => ({ openFiles: [] })
})
}))
vi.mock('./file-explorer-operation-owner', () => {
const owner = { kind: 'runtime', environmentId: 'host-b', executionHostId: 'runtime:host-b' }
return {
getFileExplorerOperationOwner: () => owner,
getFileExplorerOperationOwnerFromState: () => owner
}
})
vi.mock('@/runtime/runtime-file-client', () => ({ subscribeRuntimeFileChanges: vi.fn() }))
const refreshDir = vi.fn(async () => {})
const refreshTree = vi.fn(async () => 'refreshed' as const)
beforeEach(() => {
vi.useFakeTimers()
vi.clearAllMocks()
Object.defineProperty(window, 'api', {
configurable: true,
value: { fs: { onFsChanged: vi.fn(() => vi.fn()) } }
})
})
afterEach(() => {
cleanup()
vi.useRealTimers()
})
it('refreshes the selected host Explorer when another host publishes the same worktree id at a different root', async () => {
const { targets } = selectEditorExternalWatchTargets({
settings: getDefaultSettings('/home/me'),
openFiles: [],
activeWorktreeId: 'same-worktree',
activeWorkspaceExecutionHostId: 'runtime:host-b',
repos: [],
worktreesByRepo: {
repo: (['host-a', 'host-b'] as const).map((host) =>
makeWorktree({
id: 'same-worktree',
repoId: 'repo',
path: `/${host}/repo`,
hostId: `runtime:${host}`
})
)
},
folderWorkspaces: [],
projectGroups: [],
rightSidebarOpen: true,
rightSidebarTab: 'explorer',
rightSidebarExplorerView: 'files',
gitStatusHugeByWorktree: {},
sshConnectionStates: new Map()
})
const [target] = targets
const adapter = buildEditorExternalWatchEventHandler((worktreePath, runtimeEnvironmentId) =>
targets.find(
(candidate) =>
candidate.worktreePath === worktreePath &&
candidate.runtimeEnvironmentId === runtimeEnvironmentId
)
)
renderHook(() =>
useFileExplorerWatch({
worktreePath: '/host-b/repo',
activeWorktreeId: 'same-worktree',
dirCache: { '/host-b/repo': { children: [] } },
setDirCache: vi.fn(),
expanded: new Set(),
setSelectedPath: vi.fn(),
refreshDir,
refreshTree,
inlineInput: null,
dragSourcePath: null,
isNativeDragOver: false
})
)
// The runtime watch client roots each batch at the watched target's path.
act(() => {
adapter.handleFsChanged(
{
worktreePath: target.worktreePath,
events: [
{
kind: 'create',
absolutePath: `${target.worktreePath}/new.ts`,
isDirectory: false
}
]
},
target.runtimeEnvironmentId
)
})
await act(async () => {
await vi.advanceTimersByTimeAsync(0)
})
adapter.dispose()
expect(refreshDir).toHaveBeenCalledExactlyOnceWith('/host-b/repo')
})
@@ -0,0 +1,164 @@
import { expect, it } from 'vitest'
import type { AppState } from '@/store/types'
import { makeFolderWorkspace, makeWorktree } from '@/store/slices/worktrees-slice-test-fixtures'
import { getDefaultSettings } from '../../../shared/constants'
import {
selectEditorExternalWatchTargets,
type EditorExternalWatchTargetState
} from './editor-external-watch-targets'
type SelectedHostState = EditorExternalWatchTargetState &
Pick<AppState, 'activeWorkspaceExecutionHostId'>
const hosts = ['host-a', 'host-b'] as const
function makeState(
workspace: 'worktree' | 'folder',
rootFor: (host: string) => string = () => (workspace === 'folder' ? '/folder' : '/repo')
): SelectedHostState {
const id = workspace === 'folder' ? 'folder:same-folder' : 'same-worktree'
return {
settings: getDefaultSettings('/home/me'),
openFiles: [],
activeWorktreeId: id,
activeWorkspaceExecutionHostId: 'runtime:host-a',
repos: [],
worktreesByRepo:
workspace === 'worktree'
? {
repo: hosts.map((host) =>
makeWorktree({ id, repoId: 'repo', path: rootFor(host), hostId: `runtime:${host}` })
)
}
: {},
folderWorkspaces:
workspace === 'folder'
? hosts.map((host) =>
makeFolderWorkspace({
id: 'same-folder',
folderPath: rootFor(host),
executionHostId: `runtime:${host}`
})
)
: [],
projectGroups: [],
rightSidebarOpen: true,
rightSidebarTab: 'explorer',
rightSidebarExplorerView: 'files',
gitStatusHugeByWorktree: {},
sshConnectionStates: new Map()
}
}
it.each(['worktree', 'folder'] as const)(
'changes the %s watcher when only the selected execution host changes',
(workspace) => {
const state = makeState(workspace)
const first = selectEditorExternalWatchTargets(state)
expect(first.targets.map((target) => target.runtimeEnvironmentId)).toEqual(['host-a'])
const switched = selectEditorExternalWatchTargets({
...state,
activeWorkspaceExecutionHostId: 'runtime:host-b'
})
expect(switched.targets.map((target) => target.runtimeEnvironmentId)).toEqual(['host-b'])
expect(switched.targetsKey).not.toBe(first.targetsKey)
expect(selectEditorExternalWatchTargets(state).targetsKey).toBe(first.targetsKey)
}
)
it.each(['worktree', 'folder'] as const)(
'preserves the %s watcher snapshot when selection and watched roots stay unchanged',
(workspace) => {
const state = makeState(workspace)
const first = selectEditorExternalWatchTargets(state)
expect(selectEditorExternalWatchTargets({ ...state })).toBe(first)
}
)
it.each(['worktree', 'folder'] as const)(
"watches the selected host's %s root when hosts publish the same id at different roots",
(workspace) => {
const state = makeState(workspace, (host) => `/${host}/repo`)
const onB = selectEditorExternalWatchTargets({
...state,
activeWorkspaceExecutionHostId: 'runtime:host-b'
})
expect(onB.targets.map((target) => [target.runtimeEnvironmentId, target.worktreePath])).toEqual(
[['host-b', '/host-b/repo']]
)
const onA = selectEditorExternalWatchTargets(state)
expect(onA.targets.map((target) => [target.runtimeEnvironmentId, target.worktreePath])).toEqual(
[['host-a', '/host-a/repo']]
)
}
)
it('watches each open file owner at its own host root', () => {
const state = makeState('worktree', (host) => `/${host}/repo`)
const { targets } = selectEditorExternalWatchTargets({
...state,
rightSidebarOpen: false,
openFiles: (['host-a', 'host-b'] as const).map((host) => ({
id: `${host}:a.ts`,
filePath: `/${host}/repo/a.ts`,
relativePath: 'a.ts',
worktreeId: 'same-worktree',
language: 'typescript',
isDirty: false,
mode: 'edit' as const,
runtimeEnvironmentId: host
}))
})
expect(targets.map((target) => [target.runtimeEnvironmentId, target.worktreePath])).toEqual([
['host-a', '/host-a/repo'],
['host-b', '/host-b/repo']
])
})
it('keeps an open local editor watched locally when a same-id direct-SSH folder is selected', () => {
const localFile = {
id: 'local:a.txt',
filePath: '/local/notes/a.txt',
relativePath: 'a.txt',
worktreeId: 'folder:notes',
language: 'plaintext',
isDirty: false,
mode: 'edit' as const,
runtimeEnvironmentId: null,
operationProvenance: {
generation: {
route: { executionHostId: 'local' as const, runtimeEnvironmentId: null },
runtimeConnectionGeneration: null,
runtimePairingRevision: undefined,
runtimeSshGeneration: null,
nestedSshGeneration: null,
directSshGeneration: null
},
ownershipProjection: 'explicit' as const
}
}
const state: SelectedHostState = {
...makeState('folder'),
activeWorktreeId: 'folder:notes',
activeWorkspaceExecutionHostId: 'ssh:ssh-a',
openFiles: [localFile],
folderWorkspaces: [
makeFolderWorkspace({ id: 'notes', folderPath: '/local/notes', executionHostId: 'local' }),
makeFolderWorkspace({
id: 'notes',
folderPath: '/remote/notes',
executionHostId: 'ssh:ssh-a'
})
]
}
const watched = (next: SelectedHostState): [string, string | undefined][] =>
selectEditorExternalWatchTargets(next).targets.map((target) => [
target.worktreePath,
target.connectionId
])
expect(watched({ ...state, rightSidebarOpen: false })).toEqual([['/local/notes', undefined]])
expect(watched(state)).toEqual([
['/local/notes', undefined],
['/remote/notes', 'ssh-a']
])
})
@@ -0,0 +1,93 @@
import type { AppState } from '@/store'
import { getIndexedWorktreesById } from '@/store/worktree-repo-index'
import { findRepoForHost } from '@/store/slices/repo-host-identity'
import { getFolderWorkspaceConnectionId } from '@/lib/folder-workspace-connection'
import {
parseExecutionHostId,
toRuntimeExecutionHostId,
type ExecutionHostId
} from '../../../shared/execution-host'
import { parseWorkspaceKey } from '../../../shared/workspace-scope'
type WatchTargetRowState = Pick<
AppState,
'worktreesByRepo' | 'repos' | 'folderWorkspaces' | 'projectGroups'
>
export type EditorExternalWatchTargetRows = {
worktree: AppState['worktreesByRepo'][string][number] | undefined
repo: AppState['repos'][number] | undefined
folderWorkspace: AppState['folderWorkspaces'][number] | undefined
projectGroup: AppState['projectGroups'][number] | undefined
connectionId: string | null | undefined
}
function pickHostRow<T>(
rows: readonly T[],
hostId: ExecutionHostId | null,
rowHostId: (row: T) => string | null | undefined
): T | undefined {
// Why: hosts can publish the same workspace id with different roots; a first-row pick would watch another host's path.
const onHost = hostId
? rows.find((row) => parseExecutionHostId(rowHostId(row))?.id === hostId)
: undefined
return onHost ?? rows[0]
}
/** The catalog rows for one watch consumer, taken from the row on that consumer's own execution host. */
export function resolveEditorExternalWatchTargetRows(
state: WatchTargetRowState,
worktreeId: string,
hostId: ExecutionHostId | null
): EditorExternalWatchTargetRows | null {
const worktree = pickHostRow(
getIndexedWorktreesById(state.worktreesByRepo, worktreeId),
hostId,
(row) =>
row.hostId ??
(row.runtimeOwnerEnvironmentId?.trim()
? toRuntimeExecutionHostId(row.runtimeOwnerEnvironmentId.trim())
: null)
)
const workspaceScope = parseWorkspaceKey(worktreeId)
const folderWorkspace =
workspaceScope?.type === 'folder'
? pickHostRow(
state.folderWorkspaces.filter(
(workspace) => workspace.id === workspaceScope.folderWorkspaceId
),
hostId,
(row) => row.executionHostId
)
: undefined
if (!worktree && !folderWorkspace) {
return null
}
const worktreeHost = parseExecutionHostId(worktree?.hostId)
const repo = worktree
? worktreeHost
? (findRepoForHost(state.repos, worktree.repoId, { hostId: worktreeHost.id }) ??
(worktreeHost.kind === 'local'
? undefined
: state.repos.find((candidate) => candidate.id === worktree.repoId)))
: state.repos.find((candidate) => candidate.id === worktree.repoId)
: undefined
const folderHost = parseExecutionHostId(folderWorkspace?.executionHostId)
const projectGroup = folderWorkspace
? state.projectGroups.find(
(group) =>
group.id === folderWorkspace.projectGroupId &&
parseExecutionHostId(group.executionHostId)?.id === folderHost?.id
)
: undefined
const connectionId = folderWorkspace
? folderHost
? folderHost.kind === 'ssh'
? folderHost.targetId
: null
: getFolderWorkspaceConnectionId(state, folderWorkspace.id)
: repo
? (repo.connectionId ?? null)
: undefined
return { worktree, repo, folderWorkspace, projectGroup, connectionId }
}
@@ -2,13 +2,17 @@ import type { AppState } from '@/store'
import type { OpenFile } from '@/store/slices/editor'
import { findWorktreeById } from '@/store/slices/worktree-helpers'
import { findRepoForHost } from '@/store/slices/repo-host-identity'
import { getFolderWorkspaceConnectionId } from '@/lib/folder-workspace-connection'
import { isLocalWindowsDesktopClient } from '@/lib/desktop-window-chrome'
import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner'
import { isWindowsAbsolutePathLike } from '../../../shared/cross-platform-path'
import { parseExecutionHostId } from '../../../shared/execution-host'
import {
parseExecutionHostId,
toRuntimeExecutionHostId,
toSshExecutionHostId,
type ExecutionHostId
} from '../../../shared/execution-host'
import { isGitRepoKind } from '../../../shared/repo-kind'
import { parseWorkspaceKey } from '../../../shared/workspace-scope'
import { resolveEditorExternalWatchTargetRows } from './editor-external-watch-target-rows'
export type EditorExternalWatchTarget = {
worktreeId: string
@@ -33,7 +37,7 @@ export type EditorExternalWatchTargetState = Pick<
| 'folderWorkspaces'
| 'projectGroups'
> &
Partial<Pick<AppState, 'rightSidebarEffectiveTab'>>
Partial<Pick<AppState, 'activeWorkspaceExecutionHostId' | 'rightSidebarEffectiveTab'>>
type WatchedTargetsSnapshot = {
targets: EditorExternalWatchTarget[]
@@ -44,6 +48,7 @@ let cachedOpenFiles: AppState['openFiles'] | null = null
let cachedWorktreesByRepo: AppState['worktreesByRepo'] | null = null
let cachedRepos: AppState['repos'] | null = null
let cachedActiveWorktreeId: string | null = null
let cachedActiveWorkspaceExecutionHostId: AppState['activeWorkspaceExecutionHostId'] = null
let cachedRuntimeEnvironmentId: string | undefined
let cachedRightSidebarOpen: boolean | null = null
let cachedRightSidebarTab: AppState['rightSidebarTab'] | null = null
@@ -74,6 +79,52 @@ export function getLocalWindowsWslAliasOption(
: {}
}
type WatchConsumer = { owner: string | null; hostId: ExecutionHostId | null }
/** The host an open file was opened on; never the current selection, which may name another host's same-id workspace. */
function getOpenFileWatchConsumer(
file: Pick<OpenFile, 'externalSshTargetId' | 'operationProvenance' | 'runtimeEnvironmentId'>
): WatchConsumer {
const owner = getOpenFileRuntimeOwner(file)
const capturedHostId = file.operationProvenance?.generation.route.executionHostId
return {
owner,
hostId:
capturedHostId ??
(file.externalSshTargetId
? toSshExecutionHostId(file.externalSshTargetId)
: owner
? toRuntimeExecutionHostId(owner)
: null)
}
}
function getSidebarWatchConsumer(
state: EditorExternalWatchTargetState,
worktreeId: string
): WatchConsumer {
// Why: sidebar watcher must follow the selected worktree's host owner, not the host currently focused in the UI.
const owner = getRuntimeEnvironmentIdForWorktree(state, worktreeId)
if (owner) {
return { owner, hostId: toRuntimeExecutionHostId(owner) }
}
const selectedHost = parseExecutionHostId(state.activeWorkspaceExecutionHostId)
return { owner, hostId: selectedHost && selectedHost.kind !== 'runtime' ? selectedHost.id : null }
}
function addWatchConsumer(
consumersByWorktreeId: Map<string, Map<string, WatchConsumer>>,
worktreeId: string,
consumer: WatchConsumer
): void {
let consumers = consumersByWorktreeId.get(worktreeId)
if (!consumers) {
consumers = new Map()
consumersByWorktreeId.set(worktreeId, consumers)
}
consumers.set(`${consumer.owner ?? ''}::${consumer.hostId ?? ''}`, consumer)
}
function isLocalHostStamp(value: string | null | undefined): boolean {
return parseExecutionHostId(value)?.kind === 'local'
}
@@ -113,11 +164,13 @@ export function selectEditorExternalWatchTargets(
state: EditorExternalWatchTargetState
): WatchedTargetsSnapshot {
const runtimeEnvironmentId = state.settings?.activeRuntimeEnvironmentId?.trim() || undefined
const activeWorkspaceExecutionHostId = state.activeWorkspaceExecutionHostId ?? null
if (
cachedOpenFiles === state.openFiles &&
cachedWorktreesByRepo === state.worktreesByRepo &&
cachedRepos === state.repos &&
cachedActiveWorktreeId === state.activeWorktreeId &&
cachedActiveWorkspaceExecutionHostId === activeWorkspaceExecutionHostId &&
cachedRuntimeEnvironmentId === runtimeEnvironmentId &&
cachedRightSidebarOpen === state.rightSidebarOpen &&
cachedRightSidebarTab === state.rightSidebarTab &&
@@ -131,16 +184,10 @@ export function selectEditorExternalWatchTargets(
return cachedWatchedTargetsSnapshot
}
const targetOwnersByWorktreeId = new Map<string, Set<string | null>>()
// Why: watcher ownership is scoped by worktree + runtime owner — the same path can be open locally and in a runtime workspace, and reads/saves already route per owner.
const consumersByWorktreeId = new Map<string, Map<string, WatchConsumer>>()
// Why: watcher ownership is scoped by worktree + execution host — the same workspace id can be open locally, over SSH and in a runtime, and reads/saves already route per host.
for (const file of state.openFiles) {
let owners = targetOwnersByWorktreeId.get(file.worktreeId)
if (!owners) {
owners = new Set()
targetOwnersByWorktreeId.set(file.worktreeId, owners)
}
// Why: persisted/restored tabs may have runtimeEnvironmentId undefined; new openFile calls resolve inheritance before storing, so an ownerless tab stays local.
owners.add(getOpenFileRuntimeOwner(file))
addWatchConsumer(consumersByWorktreeId, file.worktreeId, getOpenFileWatchConsumer(file))
}
const activeWorktreeId = state.activeWorktreeId
const activeWorktree = activeWorktreeId
@@ -170,56 +217,21 @@ export function selectEditorExternalWatchTargets(
(shownRightSidebarTab === 'source-control' && sourceControlCanConsumeWatch))
if (activeWorktreeNeedsSidebarWatch) {
// Why: this app-level watcher owns Explorer/Source-Control subscriptions so downstream consumers don't fight over watch/unwatch IPC.
let owners = targetOwnersByWorktreeId.get(activeWorktreeId)
if (!owners) {
owners = new Set()
targetOwnersByWorktreeId.set(activeWorktreeId, owners)
}
// Why: sidebar watcher must follow the selected worktree's host owner, not the host currently focused in the UI.
owners.add(getRuntimeEnvironmentIdForWorktree(state, activeWorktreeId))
addWatchConsumer(
consumersByWorktreeId,
activeWorktreeId,
getSidebarWatchConsumer(state, activeWorktreeId)
)
}
const nextTargets: EditorExternalWatchTarget[] = []
const parts: string[] = []
const sortedWorktreeIds = Array.from(targetOwnersByWorktreeId.keys()).sort()
for (const id of sortedWorktreeIds) {
const worktree = findWorktreeById(state.worktreesByRepo, id)
const workspaceScope = parseWorkspaceKey(id)
const folderWorkspace =
workspaceScope?.type === 'folder'
? state.folderWorkspaces.find(
(workspace) => workspace.id === workspaceScope.folderWorkspaceId
)
: undefined
if (!worktree && !folderWorkspace) {
continue
}
const worktreeHost = parseExecutionHostId(worktree?.hostId)
const repo = worktree
? worktreeHost?.kind === 'local'
? (findRepoForHost(state.repos, worktree.repoId, { hostId: worktreeHost.id }) ?? undefined)
: state.repos.find((candidate) => candidate.id === worktree.repoId)
: undefined
const folderHostId = parseExecutionHostId(folderWorkspace?.executionHostId)?.id
const projectGroup = folderWorkspace
? state.projectGroups.find(
(group) =>
group.id === folderWorkspace.projectGroupId &&
parseExecutionHostId(group.executionHostId)?.id === folderHostId
)
: undefined
const connectionId = folderWorkspace
? getFolderWorkspaceConnectionId(state, folderWorkspace.id)
: repo
? (repo.connectionId ?? null)
: undefined
if (connectionId === undefined && folderWorkspace) {
continue
}
const owners = Array.from(targetOwnersByWorktreeId.get(id) ?? []).sort((left, right) =>
(left ?? '').localeCompare(right ?? '')
)
for (const owner of owners) {
const targetsByKey = new Map<string, EditorExternalWatchTarget>()
for (const [id, consumers] of consumersByWorktreeId) {
for (const { owner, hostId } of consumers.values()) {
const rows = resolveEditorExternalWatchTargetRows(state, id, hostId)
if (!rows || (rows.connectionId === undefined && rows.folderWorkspace)) {
continue
}
const { worktree, repo, folderWorkspace, projectGroup, connectionId } = rows
const target = {
worktreeId: id,
worktreePath: worktree?.path ?? folderWorkspace!.folderPath,
@@ -237,16 +249,21 @@ export function selectEditorExternalWatchTargets(
? { allowLocalWindowsWslAliases: true as const }
: {})
}
nextTargets.push(target)
parts.push(getEditorExternalWatchTargetKey(target))
targetsByKey.set(getEditorExternalWatchTargetKey(target), target)
}
}
const targetsKey = parts.join('|')
// Why: consumers on one host (an unqualified legacy tab and its captured sibling) collapse to one watch.
const sortedEntries = Array.from(targetsByKey).sort(([left], [right]) =>
left.localeCompare(right)
)
const nextTargets = sortedEntries.map(([, target]) => target)
const targetsKey = sortedEntries.map(([key]) => key).join('|')
cachedOpenFiles = state.openFiles
cachedWorktreesByRepo = state.worktreesByRepo
cachedRepos = state.repos
cachedActiveWorktreeId = state.activeWorktreeId
cachedActiveWorkspaceExecutionHostId = activeWorkspaceExecutionHostId
cachedRuntimeEnvironmentId = runtimeEnvironmentId
cachedRightSidebarOpen = state.rightSidebarOpen
cachedRightSidebarTab = state.rightSidebarTab
@@ -224,6 +224,27 @@ for (const selectedHost of ['A', 'B'] as const) {
await expect(page.getByText(`NESTED_${otherName}.txt`, { exact: true })).toBeVisible()
await expect(page.getByText(`NESTED_${selectedHost}.txt`, { exact: true })).toHaveCount(0)
await page.screenshot({ path: testInfo.outputPath('same-path-host-switch-refreshed.png') })
await page.waitForTimeout(1_000)
const switchedMarker = `SWITCHED_HOST_${otherName}.txt`
const switchedChange = page.waitForEvent('console', {
predicate: (message) =>
message.text().startsWith('[file-change-event]') &&
message.text().includes(switchedMarker) &&
message.text().includes(otherEnvironment.id),
timeout: 15_000
})
otherHost.exec?.(`printf 'switched host\\n' > '${folderPath}/${switchedMarker}'`)
console.log(
'[switched-host-created-file]',
await serverCall(page, otherEnvironment.id, 'files.read', {
worktree: `id:${worktreeId}`,
relativePath: switchedMarker
})
)
await page.screenshot({ path: testInfo.outputPath('switched-host-new-file-observed.png') })
await switchedChange
await expect(page.getByText(switchedMarker, { exact: true })).toBeVisible({ timeout: 10_000 })
await page.screenshot({ path: testInfo.outputPath('switched-host-new-file-visible.png') })
await page
.getByRole('option')
.filter({ hasText: `Folder host ${selectedHost}` })