mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
fix(remote): a desktop no longer serves other runtimes its tabs on its servers' files (#33424) (#33752)
* fix(remote): a desktop ignores its own workspaces echoed back by a server it serves (#33424) When two desktops pair to each other, each server republishes the tabs it mirrors from its own servers. Desktop A then received B's mirror of A's own workspace under the real workspace id and grafted it onto its local copy as B-hosted tabs, so opening those files asked B (selector_not_found) and closing them closed B's tabs. A session-tabs frame from a paired server no longer writes a workspace this app lists as its own (a stamped local or direct SSH row) unless that server also lists a copy. Other servers' workspaces stay open to every runtime. * fix(remote): retire echoes already taken and replay frames a twin's late row releases An echo fenced out of this app's own workspace now applies as that server's tombstone, so rows it left before the fence (restored sessions, or frames that arrived before this app's own row listed) are retired without closing anything. A held frame replays once the server's own copy of the workspace lists, so a same-id twin that publishes before its row loads still mirrors. The e2e waits on a later frame from the same stream instead of a fixed delay. * fix(remote): serve a desktop's tabs on its servers' files only to its phone, not to other runtimes Redesign of the #33424 fix at the source. A desktop publishes every editor tab in its window, including ones on files of its paired servers; its phone uses those through the desktop's relay. Served to another runtime, such a tab routes the file to the desktop, and in a pairing cycle it is that runtime's own workspace coming back as the desktop's tab: file reads fail with selector_not_found and closing it closes the desktop's real tab. Publishers now stamp those rows with hostRuntimeEnvironmentId (window, layout-owner main projection and the windowless projection alike). projectSessionTabsForClient strips the stamp for every caller and drops the stamped rows for runtime-scope callers, keeping groups, layout and the active tab consistent. Phones and local callers see what they saw before. The receiver-side fence is removed. * refactor(remote): keep the file-host stamp helper outside the old-client boundary; test an all-server snapshot
This commit is contained in:
@@ -6,6 +6,7 @@ import type {
|
||||
} from '../../shared/runtime-types'
|
||||
import type { Tab } from '../../shared/tab-types'
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../shared/layout-owner-switch'
|
||||
import { editorRowHost } from '../../shared/session-tab-file-host'
|
||||
import {
|
||||
editorTabNamesFile,
|
||||
sameEditorFile
|
||||
@@ -58,7 +59,8 @@ function editorTab(
|
||||
!!activeFileId &&
|
||||
editorTabNamesFile(editorWorkspace(session, worktreeId), activeFileId, file),
|
||||
color: unifiedTab?.color ?? null,
|
||||
isPinned: unifiedTab?.isPinned === true
|
||||
isPinned: unifiedTab?.isPinned === true,
|
||||
...editorRowHost(file)
|
||||
}
|
||||
if (file.language === 'markdown') {
|
||||
return {
|
||||
|
||||
@@ -29,6 +29,14 @@ export function projectSessionTabBrowserPlacements(
|
||||
if (removedIds.size === 0) {
|
||||
return payload
|
||||
}
|
||||
return withoutSessionTabs(payload, removedIds)
|
||||
}
|
||||
|
||||
/** Hides tabs from a client, keeping its groups, layout and active tab consistent. */
|
||||
export function withoutSessionTabs(
|
||||
payload: RuntimeMobileSessionTabsResult,
|
||||
removedIds: ReadonlySet<string>
|
||||
): RuntimeMobileSessionTabsResult {
|
||||
const retainedTabs = payload.tabs.filter((tab) => !removedIds.has(tab.id))
|
||||
const active = selectProjectedActiveTab(payload.activeTabId, retainedTabs)
|
||||
const tabs = retainedTabs.map((tab) => ({ ...tab, isActive: tab.id === active?.id }))
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { RuntimeMobileSessionTabsResult } from '../../../../shared/runtime-types'
|
||||
import { projectSessionTabsForClient } from './session-tabs-inventory'
|
||||
|
||||
// #33424: this desktop opened README.md in a workspace of paired server A while A pairs back to it.
|
||||
function desktopSnapshot(): RuntimeMobileSessionTabsResult {
|
||||
return {
|
||||
worktree: 'repo::/work/app',
|
||||
publicationEpoch: 'epoch-1',
|
||||
snapshotVersion: 3,
|
||||
activeGroupId: 'group-1',
|
||||
activeTabId: 'server-readme',
|
||||
activeTabType: 'markdown',
|
||||
tabGroups: [
|
||||
{
|
||||
id: 'group-1',
|
||||
activeTabId: 'server-readme',
|
||||
tabOrder: ['local-notes', 'server-readme'],
|
||||
recentTabIds: ['local-notes', 'server-readme']
|
||||
}
|
||||
],
|
||||
tabs: [
|
||||
{
|
||||
type: 'file',
|
||||
id: 'local-notes',
|
||||
title: 'notes.txt',
|
||||
filePath: '/work/app/notes.txt',
|
||||
relativePath: 'notes.txt',
|
||||
language: 'plaintext',
|
||||
isDirty: false,
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
type: 'markdown',
|
||||
id: 'server-readme',
|
||||
title: 'README.md',
|
||||
filePath: '/work/app/README.md',
|
||||
relativePath: 'README.md',
|
||||
language: 'markdown',
|
||||
mode: 'edit',
|
||||
isDirty: false,
|
||||
isActive: true,
|
||||
sourceFileId: 'server-readme-file',
|
||||
sourceFilePath: '/work/app/README.md',
|
||||
sourceRelativePath: 'README.md',
|
||||
documentVersion: 'file:server-readme-file',
|
||||
hostRuntimeEnvironmentId: 'server-a'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
describe("a desktop's tab on a paired server's file", () => {
|
||||
it('is not served to another runtime, which would take it as its own', () => {
|
||||
const served = projectSessionTabsForClient(desktopSnapshot(), 'runtime', [])
|
||||
expect(served.tabs.map((tab) => tab.id)).toEqual(['local-notes'])
|
||||
expect(served.tabGroups?.[0]?.tabOrder).toEqual(['local-notes'])
|
||||
expect(served.activeTabId).toBe('local-notes')
|
||||
expect(served.activeGroupId).toBe('group-1')
|
||||
})
|
||||
|
||||
it('leaves another runtime no groups or focus when every row is on a server', () => {
|
||||
const snapshot = desktopSnapshot()
|
||||
const onlyServerRows = {
|
||||
...snapshot,
|
||||
tabs: snapshot.tabs.filter((tab) => tab.id === 'server-readme'),
|
||||
tabGroups: snapshot.tabGroups?.map((group) => ({
|
||||
...group,
|
||||
tabOrder: ['server-readme'],
|
||||
recentTabIds: ['server-readme']
|
||||
}))
|
||||
}
|
||||
const served = projectSessionTabsForClient(onlyServerRows, 'runtime', [])
|
||||
expect(served.tabs).toEqual([])
|
||||
expect(served.tabGroups).toBeUndefined()
|
||||
expect(served.activeTabId).toBeNull()
|
||||
expect(served.activeGroupId).toBeNull()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['its phone', 'mobile' as const],
|
||||
['a local caller', undefined]
|
||||
])('is served to %s without the host stamp', (_label, clientKind) => {
|
||||
const served = projectSessionTabsForClient(desktopSnapshot(), clientKind, [])
|
||||
expect(served.tabs.map((tab) => tab.id)).toEqual(['local-notes', 'server-readme'])
|
||||
expect(served.tabs.some((tab) => 'hostRuntimeEnvironmentId' in tab)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { RuntimeMobileSessionTabsResult } from '../../../../shared/runtime-types'
|
||||
import { withoutSessionTabs } from './session-tab-browser-placement-projection'
|
||||
|
||||
/** A desktop's tab on a file of one of its servers is for its phone, which reaches that server through
|
||||
* the desktop. Another runtime would route the file here, and in a pairing cycle it is that runtime's
|
||||
* own workspace coming back to it (#33424). */
|
||||
export function projectSessionTabFileHosts(
|
||||
payload: RuntimeMobileSessionTabsResult,
|
||||
clientKind: 'mobile' | 'runtime' | undefined
|
||||
): RuntimeMobileSessionTabsResult {
|
||||
const hosted = new Set(
|
||||
payload.tabs.filter((tab) => 'hostRuntimeEnvironmentId' in tab).map((tab) => tab.id)
|
||||
)
|
||||
if (hosted.size === 0) {
|
||||
return payload
|
||||
}
|
||||
const stripped = {
|
||||
...payload,
|
||||
tabs: payload.tabs.map((tab) => {
|
||||
if (!('hostRuntimeEnvironmentId' in tab)) {
|
||||
return tab
|
||||
}
|
||||
const { hostRuntimeEnvironmentId: _host, ...served } = tab
|
||||
return served
|
||||
})
|
||||
}
|
||||
return clientKind === 'runtime' ? withoutSessionTabs(stripped, hosted) : stripped
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import type { RuntimeMobileSessionTabsResult } from '../../../../shared/runtime-
|
||||
import type { RpcContext } from '../core'
|
||||
import { projectSessionTabAgentStatus } from './session-tab-agent-status-projection'
|
||||
import { projectSessionTabBrowserPlacements } from './session-tab-browser-placement-projection'
|
||||
import { projectSessionTabFileHosts } from './session-tab-file-host-projection'
|
||||
import { createSessionTabsRetirementProofDelta } from './session-tabs-retirement-proof-delta'
|
||||
import { restoreStructuredTabsIfSupported } from './structured-session-tab-restore'
|
||||
import { createSessionTabsTabDeltaEncoder } from '../../../../shared/session-tabs-tab-delta'
|
||||
@@ -32,7 +33,10 @@ export function projectSessionTabsForClient(
|
||||
clientCapabilities: Parameters<typeof projectSessionTabAgentStatus>[2]
|
||||
): RuntimeMobileSessionTabsResult {
|
||||
return projectSessionTabBrowserPlacements(
|
||||
projectSessionTabAgentStatus(snapshot, clientKind, clientCapabilities),
|
||||
projectSessionTabFileHosts(
|
||||
projectSessionTabAgentStatus(snapshot, clientKind, clientCapabilities),
|
||||
clientKind
|
||||
),
|
||||
clientCapabilities
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
RuntimeMobileSessionMarkdownTab
|
||||
} from '../../../../shared/runtime-types'
|
||||
import type { Tab } from '../../../../shared/tab-types'
|
||||
import { editorRowHost } from '../../../../shared/session-tab-file-host'
|
||||
import type { MobileSessionWorktreeInputs } from './types'
|
||||
import {
|
||||
isFileActiveEditorSurface,
|
||||
@@ -47,7 +48,8 @@ export function buildMobileMarkdownTab(
|
||||
...(sourceFile.isUntitled ? { isUntitled: true as const } : {}),
|
||||
documentVersion: draftVersion ?? `file:${sourceFile.id}`,
|
||||
color: unifiedTab?.color ?? null,
|
||||
isPinned: unifiedTab?.isPinned === true
|
||||
isPinned: unifiedTab?.isPinned === true,
|
||||
...editorRowHost(file)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +75,7 @@ export function buildMobileFileTab(
|
||||
isPinned: unifiedTab?.isPinned === true,
|
||||
isActive: unifiedTabId
|
||||
? isUnifiedTabActiveInActiveGroup(inputs, unifiedTabId)
|
||||
: isFileActiveEditorSurface(inputs, file)
|
||||
: isFileActiveEditorSurface(inputs, file),
|
||||
...editorRowHost(file)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,8 @@ export type RuntimeMobileSessionMarkdownTab = {
|
||||
documentVersion: string
|
||||
color?: string | null
|
||||
isPinned?: boolean
|
||||
/** The paired server the file lives on. Its publisher sets it; runtimes strip it before serving. */
|
||||
hostRuntimeEnvironmentId?: string
|
||||
}
|
||||
|
||||
export type RuntimeMobileSessionFileTab = {
|
||||
@@ -76,6 +78,8 @@ export type RuntimeMobileSessionFileTab = {
|
||||
color?: string | null
|
||||
isPinned?: boolean
|
||||
isActive: boolean
|
||||
/** The paired server the file lives on. Its publisher sets it; runtimes strip it before serving. */
|
||||
hostRuntimeEnvironmentId?: string
|
||||
}
|
||||
|
||||
export type RuntimeMobileSessionBrowserTab = {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { RuntimeMobileSessionFileTab } from './runtime-mobile-session-tab-contracts'
|
||||
|
||||
/** Names the paired server a row's file lives on, for runtimes to keep from other runtimes (#33424). */
|
||||
export function editorRowHost(file: {
|
||||
runtimeEnvironmentId?: string | null
|
||||
}): Pick<RuntimeMobileSessionFileTab, 'hostRuntimeEnvironmentId'> {
|
||||
const hostRuntimeEnvironmentId = file.runtimeEnvironmentId?.trim()
|
||||
return hostRuntimeEnvironmentId ? { hostRuntimeEnvironmentId } : {}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
RuntimeMobileSessionMarkdownTab
|
||||
} from '../../runtime-mobile-session-tab-contracts'
|
||||
import type { LayoutEditorView } from '../layout-editor-view'
|
||||
import { editorRowHost } from '../../session-tab-file-host'
|
||||
import type { DesktopLayoutView } from '../workspace-layout-beside'
|
||||
import { editorFileKey, editorTabFile } from '../workspace-layout-editor-tab-file'
|
||||
import type { LayoutContentTab, LayoutEditorFile, WorkspaceLayout } from '../workspace-layout-model'
|
||||
@@ -54,7 +55,8 @@ export function editorTabRow(tab: LayoutContentTab, scope: EditorRowScope): Edit
|
||||
filePath: file.filePath,
|
||||
relativePath: file.relativePath,
|
||||
isDirty: hasDraft(scope, file),
|
||||
...tabRowProps(tab, scope)
|
||||
...tabRowProps(tab, scope),
|
||||
...editorRowHost(file)
|
||||
}
|
||||
if (file.language === 'markdown') {
|
||||
return {
|
||||
@@ -106,7 +108,8 @@ function markdownPreviewRow(
|
||||
sourceFilePath: sourceFile.filePath,
|
||||
sourceRelativePath: sourceFile.relativePath,
|
||||
documentVersion: `file:${sourceId}`,
|
||||
...tabRowProps(tab, scope)
|
||||
...tabRowProps(tab, scope),
|
||||
...editorRowHost(preview)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +138,8 @@ export function diffTabRow(
|
||||
...(diffSource === 'staged' || diffSource === 'unstaged' ? { diffSource } : {}),
|
||||
// The model holds no unsaved edit of a diff's modified side; the window's row it lists wins.
|
||||
isDirty: false,
|
||||
...tabRowProps(tab, scope)
|
||||
...tabRowProps(tab, scope),
|
||||
...editorRowHost(file)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +163,7 @@ export function reviewTabRow(
|
||||
mode: 'edit',
|
||||
// The model holds no unsaved edit of a review; the window's row it lists wins.
|
||||
isDirty: false,
|
||||
...tabRowProps(tab, scope)
|
||||
...tabRowProps(tab, scope),
|
||||
...editorRowHost(file)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
// #33424: two desktops paired to each other. B's tab on a file in A's workspace must not reach A as B's
|
||||
// own tab, or A routes that file's reads and closes to B, which lacks the workspace.
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import type { Page } from '@stablyai/playwright-test'
|
||||
import { RuntimeClient } from '../../src/cli/runtime/client'
|
||||
import { expect, test } from './helpers/orca-app'
|
||||
import { createRestartSession } from './helpers/orca-restart'
|
||||
import { createRuntimeDesktopPairingOffer } from './helpers/paired-electron-client'
|
||||
|
||||
async function pairAndSelect(page: Page, pairingUrl: string, name: string): Promise<string> {
|
||||
return page.evaluate(
|
||||
async ({ name, pairingUrl }) => {
|
||||
const store = window.__store!
|
||||
const result = await window.api.runtimeEnvironments.verifyAndAddFromPairingCode({
|
||||
name,
|
||||
pairingCode: pairingUrl,
|
||||
allowLoopback: true
|
||||
})
|
||||
if (!result.ok) {
|
||||
throw new Error(`Pairing ${name} failed: ${JSON.stringify(result)}`)
|
||||
}
|
||||
const id = result.environment.id
|
||||
store.getState().setRuntimeEnvironments(await window.api.runtimeEnvironments.list())
|
||||
if (!(await store.getState().refreshRuntimeEnvironmentStatus(id))) {
|
||||
throw new Error(`Could not reach ${name}`)
|
||||
}
|
||||
if (!(await store.getState().setActiveRuntimeEnvironmentPreference(id))) {
|
||||
throw new Error(`Could not select ${name}`)
|
||||
}
|
||||
return id
|
||||
},
|
||||
{ name, pairingUrl }
|
||||
)
|
||||
}
|
||||
|
||||
async function addLocalFolder(page: Page, folderPath: string): Promise<string> {
|
||||
const repoId = await page.evaluate(async (folderPath) => {
|
||||
const added = await window.api.repos.add({ path: folderPath, kind: 'folder' })
|
||||
if ('error' in added) {
|
||||
throw new Error(added.error)
|
||||
}
|
||||
return added.repo.id
|
||||
}, folderPath)
|
||||
let worktreeId = ''
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
worktreeId = await page.evaluate(async (repoId) => {
|
||||
const store = window.__store!
|
||||
await store.getState().fetchRepos({ runtimeEnvironmentId: null, throwOnError: true })
|
||||
await store.getState().fetchWorktrees(repoId)
|
||||
return (
|
||||
store
|
||||
.getState()
|
||||
.allWorktrees()
|
||||
.find((entry) => entry.repoId === repoId)?.id ?? ''
|
||||
)
|
||||
}, repoId)
|
||||
return worktreeId
|
||||
},
|
||||
{ timeout: 30_000, message: 'Desktop B never listed its own folder' }
|
||||
)
|
||||
.not.toBe('')
|
||||
return worktreeId
|
||||
}
|
||||
|
||||
function tabIdsIn(page: Page, worktreeId: string): Promise<string[]> {
|
||||
return page.evaluate(
|
||||
(id) => (window.__store!.getState().unifiedTabsByWorktree[id] ?? []).map((tab) => tab.id),
|
||||
worktreeId
|
||||
)
|
||||
}
|
||||
|
||||
function openFilesIn(page: Page, worktreeId: string) {
|
||||
return page.evaluate(
|
||||
(id) =>
|
||||
window
|
||||
.__store!.getState()
|
||||
.openFiles.filter((file) => file.worktreeId === id)
|
||||
.map((file) => ({
|
||||
relativePath: file.relativePath,
|
||||
env: file.runtimeEnvironmentId ?? null
|
||||
})),
|
||||
worktreeId
|
||||
)
|
||||
}
|
||||
|
||||
test('a desktop gets no tab back for its own workspace from a server it serves', async ({
|
||||
orcaPage
|
||||
}, testInfo) => {
|
||||
// Two desktops launch and pair to each other.
|
||||
test.setTimeout(240_000)
|
||||
const desktopBFolder = mkdtempSync(path.join(os.tmpdir(), 'orca-e2e-desktop-b-'))
|
||||
writeFileSync(path.join(desktopBFolder, 'note.md'), '# desktop B\n')
|
||||
const hubBSession = createRestartSession(testInfo)
|
||||
const hubB = await hubBSession.launch()
|
||||
try {
|
||||
await hubB.page.waitForFunction(
|
||||
() => window.__store?.getState().workspaceSessionReady === true,
|
||||
null,
|
||||
{ timeout: 60_000 }
|
||||
)
|
||||
const worktree = await orcaPage.evaluate(() => {
|
||||
const listed = window.__store!.getState().allWorktrees()[0]
|
||||
return listed ? { id: listed.id, path: listed.path } : null
|
||||
})
|
||||
expect(worktree).not.toBeNull()
|
||||
const { id: worktreeId, path: worktreePath } = worktree!
|
||||
const desktopBWorktreeId = await addLocalFolder(hubB.page, desktopBFolder)
|
||||
const desktopATabIds = await tabIdsIn(orcaPage, worktreeId)
|
||||
|
||||
const desktopBOnA = await pairAndSelect(
|
||||
orcaPage,
|
||||
(await createRuntimeDesktopPairingOffer(hubB.page)).pairingUrl,
|
||||
'Desktop B'
|
||||
)
|
||||
const desktopAOnB = await pairAndSelect(
|
||||
hubB.page,
|
||||
(await createRuntimeDesktopPairingOffer(orcaPage)).pairingUrl,
|
||||
'Desktop A'
|
||||
)
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
hubB.page.evaluate(
|
||||
(id) =>
|
||||
window
|
||||
.__store!.getState()
|
||||
.allWorktrees()
|
||||
.some((entry) => entry.id === id),
|
||||
worktreeId
|
||||
),
|
||||
{
|
||||
timeout: 60_000,
|
||||
message: 'Desktop B never listed desktop A’s workspace'
|
||||
}
|
||||
)
|
||||
.toBe(true)
|
||||
|
||||
await hubB.page.evaluate(
|
||||
({ environmentId, worktreeId, worktreePath }) => {
|
||||
const state = window.__store!.getState()
|
||||
state.setActiveView('terminal')
|
||||
state.setActiveWorktree(worktreeId, `runtime:${environmentId}`)
|
||||
state.openFile({
|
||||
filePath: `${worktreePath}/README.md`,
|
||||
relativePath: 'README.md',
|
||||
worktreeId,
|
||||
runtimeEnvironmentId: environmentId,
|
||||
language: 'markdown',
|
||||
mode: 'edit'
|
||||
})
|
||||
},
|
||||
{ environmentId: desktopAOnB, worktreeId, worktreePath }
|
||||
)
|
||||
await expect
|
||||
.poll(() => openFilesIn(hubB.page, worktreeId), { timeout: 30_000 })
|
||||
.toEqual([{ relativePath: 'README.md', env: desktopAOnB }])
|
||||
|
||||
// B's runtime holds B's tab in A's workspace; locally it is listed, only other runtimes are spared it.
|
||||
const desktopBRuntime = new RuntimeClient(hubBSession.userDataDir, 30_000, null, null)
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const listed = await desktopBRuntime.call<{
|
||||
snapshots: { worktree: string; tabs: { type: string }[] }[]
|
||||
}>('session.tabs.listAll')
|
||||
return listed.result.snapshots.some(
|
||||
(snapshot) =>
|
||||
snapshot.worktree === worktreeId &&
|
||||
snapshot.tabs.some((tab) => tab.type === 'markdown')
|
||||
)
|
||||
},
|
||||
{ timeout: 30_000, message: 'Desktop B never published its tab in A’s workspace' }
|
||||
)
|
||||
.toBe(true)
|
||||
// A frame B publishes later reaches A after any echo on the same stream.
|
||||
await hubB.page.evaluate(
|
||||
({ worktreeId, filePath }) => {
|
||||
window.__store!.getState().openFile({
|
||||
filePath,
|
||||
relativePath: 'note.md',
|
||||
worktreeId,
|
||||
language: 'markdown',
|
||||
mode: 'edit'
|
||||
})
|
||||
},
|
||||
{ worktreeId: desktopBWorktreeId, filePath: path.join(desktopBFolder, 'note.md') }
|
||||
)
|
||||
await expect
|
||||
.poll(() => openFilesIn(orcaPage, desktopBWorktreeId), {
|
||||
timeout: 30_000,
|
||||
message: 'Desktop A never mirrored a tab in desktop B’s own workspace'
|
||||
})
|
||||
.toEqual([{ relativePath: 'note.md', env: desktopBOnA }])
|
||||
expect(await openFilesIn(orcaPage, worktreeId)).toEqual([])
|
||||
expect(await tabIdsIn(orcaPage, worktreeId)).toEqual(desktopATabIds)
|
||||
expect(await openFilesIn(hubB.page, worktreeId)).toEqual([
|
||||
{ relativePath: 'README.md', env: desktopAOnB }
|
||||
])
|
||||
} finally {
|
||||
await hubBSession.close(hubB.app)
|
||||
await hubBSession.dispose()
|
||||
rmSync(desktopBFolder, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user