mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
fix(runtime): collapse mirrored Markdown aliases with shared backing identity (#31984)
* fix(runtime): collapse mirrored editor tab variants that differ only by environment A snapshot that already lists one file under several per-environment host tab ids (editor:<worktree>:<env>:<path> or the bare path) fanned out into one mirrored tab per variant. Keep one minted variant per (worktree, mode, path) when building mirrored editor tabs; tabs with opaque ids (deliberate splits) are untouched. Protects receivers from older peers that still republish mirrored files and from snapshots that are already polluted. * fix(runtime): match encoded editor tab ids and keep the navigation target when collapsing variants Host tab ids come from buildOwnedEditorFileId, which URL-encodes the worktree, environment and path, so the raw-path predicate never matched a real variant. Each candidate is now checked by rebuilding it with buildOwnedEditorFileId, and the tab a pending focus/follow intent targets wins its bucket ahead of an active one. Tests build their ids the same way. * fix(runtime): preserve editor identity while collapsing aliases * test(runtime): keep headless mirror regression across project boundaries --------- Co-authored-by: Neil <neil@stably.ai>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import type { RuntimeMobileSessionMarkdownTab } from '../../../shared/runtime-types'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { posix as pathPosix } from 'node:path'
|
||||
import type { BrowserPage, BrowserWorkspace } from '../../../shared/browser-workspace-types'
|
||||
@@ -5,6 +6,9 @@ import type { Tab } from '../../../shared/tab-types'
|
||||
import type { OpenFile } from '../store/slices/editor'
|
||||
import { buildOwnedEditorFileId } from '../store/slices/editor/file-ids/editor-file-ids'
|
||||
import { applyWebSessionTabsSnapshot } from './web-session-tabs-sync/snapshot-api'
|
||||
import { recordWebSessionFocusIntent } from './web-session-focus-intent'
|
||||
import { buildMobileSessionTabSnapshots } from './sync-runtime-graph/mobile-session-snapshots'
|
||||
import { makeState as makePublisherState } from './sync-runtime-graph-test-harness'
|
||||
import type { WebSessionTabsSyncState } from './web-session-tabs-sync/state'
|
||||
import {
|
||||
ENV,
|
||||
@@ -553,3 +557,299 @@ describe('applyWebSessionTabsSnapshot', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('applyWebSessionTabsSnapshot host-invariant editor dedup', () => {
|
||||
beforeEach(resetWebSessionTabsSyncTestState)
|
||||
|
||||
const variant = (id: string, isActive: boolean): RuntimeMobileSessionMarkdownTab => ({
|
||||
type: 'markdown',
|
||||
id,
|
||||
title: 'README.md',
|
||||
filePath: '/repo/README.md',
|
||||
relativePath: 'README.md',
|
||||
language: 'markdown',
|
||||
mode: 'edit',
|
||||
isDirty: false,
|
||||
isActive,
|
||||
sourceFileId: README_ID,
|
||||
sourceFilePath: '/repo/README.md',
|
||||
sourceRelativePath: 'README.md',
|
||||
documentVersion: 'file:/repo/README.md'
|
||||
})
|
||||
const envVariantId = (env: string) => buildOwnedEditorFileId('/repo/README.md', WT, env)
|
||||
|
||||
function stateWithVariant(id: string, retained: OpenFile): WebSessionTabsSyncState {
|
||||
return makeState({
|
||||
activeFileId: retained.id,
|
||||
activeFileIdByWorktree: { [WT]: retained.id },
|
||||
activeTabType: 'editor',
|
||||
activeTabTypeByWorktree: { [WT]: 'editor' },
|
||||
activeGroupIdByWorktree: { [WT]: 'host-group-1' },
|
||||
openFiles: [retained],
|
||||
editorDrafts: { [retained.id]: '# unsaved client edit' },
|
||||
unifiedTabsByWorktree: {
|
||||
[WT]: [
|
||||
{
|
||||
id,
|
||||
entityId: retained.id,
|
||||
worktreeId: WT,
|
||||
groupId: 'host-group-1',
|
||||
contentType: 'editor',
|
||||
label: 'README.md',
|
||||
customLabel: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: NOW - 1,
|
||||
isPreview: false
|
||||
}
|
||||
]
|
||||
},
|
||||
groupsByWorktree: {
|
||||
[WT]: [
|
||||
{
|
||||
id: 'host-group-1',
|
||||
worktreeId: WT,
|
||||
activeTabId: id,
|
||||
tabOrder: [id],
|
||||
recentTabIds: [id]
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const ownedFile: OpenFile = {
|
||||
id: README_ID,
|
||||
filePath: '/repo/README.md',
|
||||
relativePath: 'README.md',
|
||||
worktreeId: WT,
|
||||
language: 'markdown',
|
||||
mode: 'edit',
|
||||
isDirty: true,
|
||||
runtimeEnvironmentId: ENV,
|
||||
mirroredFromRuntimeSession: true
|
||||
}
|
||||
|
||||
it('mirrors one tab when a snapshot lists the same file under several environment variants', () => {
|
||||
const ids = [
|
||||
envVariantId('env-a'),
|
||||
envVariantId('env-b'),
|
||||
envVariantId('env-c'),
|
||||
buildOwnedEditorFileId('/repo/README.md', WT, null),
|
||||
'/repo/README.md'
|
||||
]
|
||||
const published = buildMobileSessionTabSnapshots(
|
||||
makePublisherState({
|
||||
openFiles: [ownedFile],
|
||||
unifiedTabsByWorktree: {
|
||||
[WT]: ids.flatMap((id) => stateWithVariant(id, ownedFile).unifiedTabsByWorktree[WT] ?? [])
|
||||
}
|
||||
})
|
||||
)[0]
|
||||
if (!published) {
|
||||
throw new Error('Desktop did not publish its editor tabs')
|
||||
}
|
||||
expect(published.tabs).toHaveLength(5)
|
||||
const tabs = published.tabs.filter((tab) => tab.type === 'markdown')
|
||||
expect(tabs).toHaveLength(5)
|
||||
const patch = applyWebSessionTabsSnapshot(
|
||||
makeState(),
|
||||
makeSnapshot(tabs, { activeTabId: null, activeTabType: null }),
|
||||
ENV,
|
||||
NOW
|
||||
)
|
||||
|
||||
expect(patch.openFiles).toHaveLength(1)
|
||||
expect(patch.unifiedTabsByWorktree?.[WT]).toHaveLength(1)
|
||||
expect(patch.unifiedTabsByWorktree?.[WT]?.[0]?.id).toBe(envVariantId('env-a'))
|
||||
})
|
||||
|
||||
it('keeps the variant a follow navigation targets over an active one', () => {
|
||||
const patch = applyWebSessionTabsSnapshot(
|
||||
makeState(),
|
||||
makeSnapshot([variant(envVariantId('env-a'), true), variant(envVariantId('env-b'), false)], {
|
||||
activeTabId: envVariantId('env-b'),
|
||||
activeTabType: 'markdown',
|
||||
navigationIntent: 'follow'
|
||||
}),
|
||||
ENV,
|
||||
NOW
|
||||
)
|
||||
|
||||
expect(patch.unifiedTabsByWorktree?.[WT]).toHaveLength(1)
|
||||
expect(patch.unifiedTabsByWorktree?.[WT]?.[0]?.id).toBe(envVariantId('env-b'))
|
||||
})
|
||||
|
||||
it('leaves split tabs with opaque ids alone', () => {
|
||||
const patch = applyWebSessionTabsSnapshot(
|
||||
makeState(),
|
||||
makeSnapshot([variant(envVariantId('env-a'), false), variant('split-tab-7f3a', false)], {
|
||||
activeTabId: null,
|
||||
activeTabType: null
|
||||
}),
|
||||
ENV,
|
||||
NOW
|
||||
)
|
||||
|
||||
expect(patch.unifiedTabsByWorktree?.[WT]).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('keeps the visible local variant when a status snapshot marks another active', () => {
|
||||
const visibleId = envVariantId('env-b')
|
||||
const state = stateWithVariant(visibleId, ownedFile)
|
||||
const patch = applyWebSessionTabsSnapshot(
|
||||
state,
|
||||
makeSnapshot([variant(envVariantId('env-a'), true), variant(visibleId, false)], {
|
||||
activeTabId: envVariantId('env-a'),
|
||||
activeTabType: 'markdown'
|
||||
}),
|
||||
ENV,
|
||||
NOW
|
||||
)
|
||||
|
||||
expect(patch.unifiedTabsByWorktree?.[WT]?.map((tab) => tab.id)).toEqual([visibleId])
|
||||
expect((patch.groupsByWorktree ?? state.groupsByWorktree)[WT]?.[0]?.activeTabId).toBe(visibleId)
|
||||
expect((patch.openFiles ?? state.openFiles)[0]).toMatchObject({
|
||||
id: ownedFile.id,
|
||||
isDirty: true
|
||||
})
|
||||
expect(patch.editorDrafts ?? state.editorDrafts).toEqual(state.editorDrafts)
|
||||
})
|
||||
|
||||
it('retains a pending relay draft identity even when navigation selects another variant', () => {
|
||||
const retained: OpenFile = {
|
||||
...ownedFile,
|
||||
id: '/repo/README.md',
|
||||
runtimeEnvironmentId: null,
|
||||
pendingOwnerMigration: true
|
||||
}
|
||||
const state = stateWithVariant(envVariantId('env-b'), retained)
|
||||
const selectedId = envVariantId('env-a')
|
||||
const patch = applyWebSessionTabsSnapshot(
|
||||
state,
|
||||
makeSnapshot([variant(selectedId, true), variant(envVariantId('env-b'), false)], {
|
||||
activeTabId: selectedId,
|
||||
activeTabType: 'markdown',
|
||||
navigationIntent: 'follow'
|
||||
}),
|
||||
ENV,
|
||||
NOW
|
||||
)
|
||||
|
||||
expect(patch.openFiles ?? state.openFiles).toEqual([retained])
|
||||
expect((patch.openFiles ?? state.openFiles)[0]).toBe(retained)
|
||||
expect(patch.unifiedTabsByWorktree?.[WT]).toMatchObject([
|
||||
{ id: selectedId, entityId: retained.id }
|
||||
])
|
||||
expect(patch.unifiedTabsByWorktree?.[WT]).toHaveLength(1)
|
||||
expect(patch.editorDrafts ?? state.editorDrafts).toEqual(state.editorDrafts)
|
||||
})
|
||||
|
||||
it('honors a pending caller focus intent over the visible and host-active variants', () => {
|
||||
const visibleId = envVariantId('env-b')
|
||||
const requestedId = envVariantId('env-c')
|
||||
const state = stateWithVariant(visibleId, ownedFile)
|
||||
recordWebSessionFocusIntent({ environmentId: ENV }, WT, requestedId)
|
||||
const patch = applyWebSessionTabsSnapshot(
|
||||
state,
|
||||
makeSnapshot([
|
||||
variant(envVariantId('env-a'), true),
|
||||
variant(visibleId, false),
|
||||
variant(requestedId, false)
|
||||
]),
|
||||
ENV,
|
||||
NOW
|
||||
)
|
||||
|
||||
expect(patch.unifiedTabsByWorktree?.[WT]?.map((tab) => tab.id)).toEqual([requestedId])
|
||||
expect((patch.groupsByWorktree ?? state.groupsByWorktree)[WT]?.[0]?.activeTabId).toBe(
|
||||
requestedId
|
||||
)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ filePath: '/workspace/notes/Read me #1%.md', worktree: 'folder:/workspace' },
|
||||
{ filePath: 'C:\\workspace\\docs\\Read me.md', worktree: 'folder:C:\\workspace' },
|
||||
{ filePath: '\\\\server\\share\\Read me.md', worktree: 'folder:\\\\server\\share' }
|
||||
])('collapses encoded variants for the host path $filePath', ({ filePath, worktree }) => {
|
||||
const tabs = ['host:a', 'host:b'].map((environment) => ({
|
||||
...variant(buildOwnedEditorFileId(filePath, worktree, environment), false),
|
||||
filePath,
|
||||
sourceFilePath: filePath
|
||||
}))
|
||||
const state = makeState({ activeWorktreeId: worktree })
|
||||
const snapshot = makeSnapshot(tabs, { worktree, activeTabId: null, activeTabType: null })
|
||||
const patch = applyWebSessionTabsSnapshot(state, snapshot, ENV, NOW)
|
||||
const nextState = { ...state, ...patch }
|
||||
const replay = applyWebSessionTabsSnapshot(nextState, snapshot, ENV, NOW + 1)
|
||||
|
||||
expect(patch.openFiles).toMatchObject([{ filePath, worktreeId: worktree }])
|
||||
expect(patch.openFiles).toHaveLength(1)
|
||||
expect(patch.unifiedTabsByWorktree?.[worktree]).toHaveLength(1)
|
||||
expect((replay.openFiles ?? nextState.openFiles)[0]?.id).toBe(patch.openFiles?.[0]?.id)
|
||||
expect(
|
||||
(replay.unifiedTabsByWorktree ?? nextState.unifiedTabsByWorktree)[worktree]
|
||||
).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('keeps edit and preview views distinct while collapsing edit aliases', () => {
|
||||
const preview = { ...variant(envVariantId('env-c'), false), mode: 'markdown-preview' as const }
|
||||
const patch = applyWebSessionTabsSnapshot(
|
||||
makeState(),
|
||||
makeSnapshot([
|
||||
variant(envVariantId('env-a'), false),
|
||||
variant(envVariantId('env-b'), false),
|
||||
preview
|
||||
]),
|
||||
ENV,
|
||||
NOW
|
||||
)
|
||||
|
||||
expect(patch.unifiedTabsByWorktree?.[WT]).toHaveLength(2)
|
||||
expect(patch.openFiles).toMatchObject([
|
||||
{ id: README_ID, mode: 'edit' },
|
||||
{ mode: 'markdown-preview', markdownPreviewSourceFileId: README_ID }
|
||||
])
|
||||
})
|
||||
|
||||
it('leaves a malformed environment segment opaque', () => {
|
||||
const malformedId = `editor:${encodeURIComponent(WT)}:%:${encodeURIComponent('/repo/README.md')}`
|
||||
const patch = applyWebSessionTabsSnapshot(
|
||||
makeState(),
|
||||
makeSnapshot([variant(envVariantId('env-a'), false), variant(malformedId, false)]),
|
||||
ENV,
|
||||
NOW
|
||||
)
|
||||
|
||||
expect(patch.unifiedTabsByWorktree?.[WT]?.map((tab) => tab.id)).toEqual([
|
||||
envVariantId('env-a'),
|
||||
malformedId
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps distinct backing source identities at the same path', () => {
|
||||
const first = { ...variant(envVariantId('env-a'), true), sourceFileId: 'local-file-owner' }
|
||||
const second = { ...variant(envVariantId('env-b'), false), sourceFileId: 'remote-file-owner' }
|
||||
const patch = applyWebSessionTabsSnapshot(makeState(), makeSnapshot([first, second]), ENV, NOW)
|
||||
|
||||
expect(patch.unifiedTabsByWorktree?.[WT]?.map((tab) => tab.id)).toEqual([first.id, second.id])
|
||||
})
|
||||
|
||||
it('keeps legacy file views without a backing source identity', () => {
|
||||
const tabs = ['env-a', 'env-b'].map((environment) => ({
|
||||
type: 'file' as const,
|
||||
id: envVariantId(environment),
|
||||
title: 'README.md',
|
||||
filePath: '/repo/README.md',
|
||||
relativePath: 'README.md',
|
||||
language: 'plaintext',
|
||||
isDirty: false,
|
||||
isActive: false
|
||||
}))
|
||||
const patch = applyWebSessionTabsSnapshot(makeState(), makeSnapshot(tabs), ENV, NOW)
|
||||
|
||||
expect(patch.unifiedTabsByWorktree?.[WT]?.map((tab) => tab.id)).toEqual(
|
||||
tabs.map((tab) => tab.id)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { buildWebSessionExistingTabIndex } from '../web-session-existing-tab-index'
|
||||
import { resolveWebSessionVisibleTabId } from '../web-session-focus-intent'
|
||||
import { chooseTargetGroupId, buildHostGroupIdByTabId } from './tab-group-layout-tree'
|
||||
import {
|
||||
buildMirroredBrowserTabs,
|
||||
@@ -35,7 +36,8 @@ export function prepareWebSessionTabsSnapshotBrowser(
|
||||
mirroredTerminalIds,
|
||||
removedTerminalIds,
|
||||
reconcilesNonAgentTabs,
|
||||
batchContext
|
||||
batchContext,
|
||||
navigationIntentTab
|
||||
} = base
|
||||
const targetGroupId = chooseTargetGroupId(state, snapshot)
|
||||
const hostGroupIdByTabId = buildHostGroupIdByTabId(snapshot.tabGroups)
|
||||
@@ -123,7 +125,8 @@ export function prepareWebSessionTabsSnapshotBrowser(
|
||||
targetGroupId,
|
||||
mirroredTerminalTabEntries.length + mirroredBrowserTabs.length,
|
||||
now,
|
||||
(fileId) => state.editorDrafts?.[fileId] !== undefined
|
||||
(fileId) => state.editorDrafts?.[fileId] !== undefined,
|
||||
navigationIntentTab?.id ?? resolveWebSessionVisibleTabId(state, worktreeId)
|
||||
)
|
||||
const mirroredAgentTabs = buildMirroredAgentTabs(
|
||||
snapshot,
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { ReadyEditorSurface, MirroredEditorTab } from './state'
|
||||
import type { WebSessionExistingTabIndex } from '../web-session-existing-tab-index'
|
||||
import { isReadyEditorTab } from './terminal-surfaces'
|
||||
import { createMirroredEditorFileResolver } from './mirrored-editor-file-identity'
|
||||
import { buildOwnedEditorFileId } from '../../store/slices/editor/file-ids/editor-file-ids'
|
||||
|
||||
export function buildTerminalUnifiedTab(
|
||||
tab: TerminalTab,
|
||||
@@ -100,6 +101,71 @@ export function buildEditorUnifiedTab(
|
||||
}
|
||||
}
|
||||
|
||||
// Older peers can echo one file under IDs minted by several environments.
|
||||
function mintedEnvironmentSegment(tab: ReadyEditorSurface, worktreeId: string): string | null {
|
||||
if (
|
||||
tab.type !== 'markdown' ||
|
||||
typeof tab.sourceFileId !== 'string' ||
|
||||
!tab.sourceFileId ||
|
||||
tab.sourceFileId === tab.sourceFilePath
|
||||
) {
|
||||
return null
|
||||
}
|
||||
if (tab.id === tab.filePath) {
|
||||
return ''
|
||||
}
|
||||
const prefix = `editor:${encodeURIComponent(worktreeId)}:`
|
||||
const suffix = `:${encodeURIComponent(tab.filePath)}`
|
||||
if (
|
||||
!tab.id.startsWith(prefix) ||
|
||||
!tab.id.endsWith(suffix) ||
|
||||
tab.id.length <= prefix.length + suffix.length
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const segment = tab.id.slice(prefix.length, tab.id.length - suffix.length)
|
||||
let runtimeKey: string
|
||||
try {
|
||||
runtimeKey = decodeURIComponent(segment)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
return buildOwnedEditorFileId(tab.filePath, worktreeId, runtimeKey) === tab.id ? segment : null
|
||||
}
|
||||
|
||||
function dedupeReadyEditorTabsBySource(
|
||||
tabs: readonly ReadyEditorSurface[],
|
||||
worktreeId: string,
|
||||
preferredHostTabId: string | null
|
||||
): ReadyEditorSurface[] {
|
||||
const bucketOf = (tab: ReadyEditorSurface): string =>
|
||||
JSON.stringify([
|
||||
worktreeId,
|
||||
tab.type === 'markdown' ? tab.mode : 'edit',
|
||||
tab.filePath,
|
||||
tab.type === 'markdown' ? tab.sourceFileId : null
|
||||
])
|
||||
const rank = (tab: ReadyEditorSurface): number =>
|
||||
tab.id === preferredHostTabId ? 2 : tab.isActive ? 1 : 0
|
||||
const winnerByBucket = new Map<string, { segment: string; rank: number }>()
|
||||
for (const tab of tabs) {
|
||||
const segment = mintedEnvironmentSegment(tab, worktreeId)
|
||||
if (segment === null) {
|
||||
continue
|
||||
}
|
||||
const bucket = bucketOf(tab)
|
||||
const current = winnerByBucket.get(bucket)
|
||||
const tabRank = rank(tab)
|
||||
if (!current || tabRank > current.rank) {
|
||||
winnerByBucket.set(bucket, { segment, rank: tabRank })
|
||||
}
|
||||
}
|
||||
return tabs.filter((tab) => {
|
||||
const segment = mintedEnvironmentSegment(tab, worktreeId)
|
||||
return segment === null || segment === winnerByBucket.get(bucketOf(tab))?.segment
|
||||
})
|
||||
}
|
||||
|
||||
export function buildMirroredEditorTabs(
|
||||
snapshot: RuntimeMobileSessionTabsResult,
|
||||
environmentId: string,
|
||||
@@ -109,7 +175,8 @@ export function buildMirroredEditorTabs(
|
||||
fallbackGroupId: string,
|
||||
sortOffset: number,
|
||||
now: number,
|
||||
hasLocalDraft: (fileId: string) => boolean
|
||||
hasLocalDraft: (fileId: string) => boolean,
|
||||
preferredHostTabId: string | null = null
|
||||
): MirroredEditorTab[] {
|
||||
const readyTabs = snapshot.tabs.filter(isReadyEditorTab)
|
||||
if (readyTabs.length === 0) {
|
||||
@@ -120,7 +187,12 @@ export function buildMirroredEditorTabs(
|
||||
for (const tab of readyTabs) {
|
||||
resolveFile(tab, existingTabIndex.getEditorUnifiedTabByHostId(tab.id))
|
||||
}
|
||||
return readyTabs.map((tab, index) => {
|
||||
const distinctTabs = dedupeReadyEditorTabsBySource(
|
||||
readyTabs,
|
||||
snapshot.worktree,
|
||||
preferredHostTabId
|
||||
)
|
||||
return distinctTabs.map((tab, index) => {
|
||||
const identity = resolveFile(tab, existingTabIndex.getEditorUnifiedTabByHostId(tab.id))
|
||||
const { fileId, existingFile, sourceFileId } = identity
|
||||
const existingUnifiedTab = existingTabIndex.getEditorUnifiedTab(fileId, tab.id)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { buildHeadlessMobileSessionEditorTabs } from '../../src/main/runtime/mobile-session-editor-projection'
|
||||
import type { WorkspaceSessionState } from '../../src/shared/workspace-session-state-types'
|
||||
import { buildOwnedEditorFileId } from '../../src/renderer/src/store/slices/editor/file-ids/editor-file-ids'
|
||||
import { applyWebSessionTabsSnapshot } from '../../src/renderer/src/runtime/web-session-tabs-sync/snapshot-api'
|
||||
import {
|
||||
ENV,
|
||||
NOW,
|
||||
WT,
|
||||
makeSnapshot,
|
||||
makeState
|
||||
} from '../../src/renderer/src/runtime/web-session-tabs-sync-test-harness'
|
||||
|
||||
const host = vi.hoisted(() => ({ layoutOwnerIsMain: true }))
|
||||
vi.mock('../../src/shared/layout-owner-switch', () => ({
|
||||
get ORCA_LAYOUT_OWNER_IS_MAIN() {
|
||||
return host.layoutOwnerIsMain
|
||||
}
|
||||
}))
|
||||
|
||||
describe('headless editor identity in an older desktop mirror', () => {
|
||||
it('keeps two owners when the producer represents both source identities by the path', () => {
|
||||
const filePath = '/repo/README.md'
|
||||
const owners = [null, 'other-host']
|
||||
const session: WorkspaceSessionState = {
|
||||
activeRepoId: null,
|
||||
activeWorktreeId: WT,
|
||||
activeTabId: null,
|
||||
tabsByWorktree: {},
|
||||
terminalLayoutsByTabId: {},
|
||||
openFilesByWorktree: {
|
||||
[WT]: owners.map((owner) => ({
|
||||
filePath,
|
||||
relativePath: 'README.md',
|
||||
worktreeId: WT,
|
||||
language: 'markdown',
|
||||
runtimeEnvironmentId: owner,
|
||||
...(owner ? { dirtyDraftContent: 'remote draft' } : {})
|
||||
}))
|
||||
},
|
||||
unifiedTabs: {
|
||||
[WT]: owners.map((owner, index) => ({
|
||||
id: buildOwnedEditorFileId(filePath, WT, owner),
|
||||
entityId: buildOwnedEditorFileId(filePath, WT, owner),
|
||||
groupId: 'host-group-1',
|
||||
worktreeId: WT,
|
||||
contentType: 'editor',
|
||||
label: 'README.md',
|
||||
customLabel: null,
|
||||
color: null,
|
||||
sortOrder: index,
|
||||
createdAt: NOW,
|
||||
isPreview: false
|
||||
}))
|
||||
}
|
||||
}
|
||||
const tabs = buildHeadlessMobileSessionEditorTabs(WT, session)
|
||||
expect(tabs).toHaveLength(2)
|
||||
expect(tabs.map((tab) => tab.id)).toEqual(
|
||||
owners.map((owner) => buildOwnedEditorFileId(filePath, WT, owner))
|
||||
)
|
||||
expect(tabs.map((tab) => tab.isDirty)).toEqual([false, true])
|
||||
|
||||
host.layoutOwnerIsMain = false
|
||||
const patch = applyWebSessionTabsSnapshot(makeState(), makeSnapshot(tabs), ENV, NOW)
|
||||
|
||||
expect(patch.unifiedTabsByWorktree?.[WT]?.map((tab) => tab.id)).toEqual(
|
||||
tabs.map((tab) => tab.id)
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user