diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-destination-workspace-session.ts b/src/main/persistence/migrating-orcad-catalog/orcad-destination-workspace-session.ts index c70330c6650..e11784bc2a8 100644 --- a/src/main/persistence/migrating-orcad-catalog/orcad-destination-workspace-session.ts +++ b/src/main/persistence/migrating-orcad-catalog/orcad-destination-workspace-session.ts @@ -42,31 +42,26 @@ export function prepareOrcadMigrationWorkspaceSession( return { incoming: undefined, merged: undefined } } assertNoSessionRecordConflicts(state.workspaceSession, incoming) - assertNoSessionFocusConflicts(state.workspaceSession, incoming) assertNoSessionEntityConflicts(state.workspaceSession, incoming) + const merged = mergeWorkspaceSessions(state.workspaceSession, incoming) + // Focus is one desktop's selection, not saved state: a host already focused keeps all of its own. return { incoming, - merged: mergeWorkspaceSessions(state.workspaceSession, incoming) + merged: hasSessionFocus(state.workspaceSession) + ? { ...merged, ...sessionFocus(state.workspaceSession) } + : merged } } -function assertNoSessionFocusConflicts( - existing: WorkspaceSessionState, - incoming: WorkspaceSessionState -): void { - for (const field of SESSION_FOCUS_FIELDS) { - const current = existing[field] - const next = incoming[field] - if ( - next !== undefined && - next !== null && - current !== undefined && - current !== null && - serializeOrcadMigrationValue(current) !== serializeOrcadMigrationValue(next) - ) { - throw new Error(`orcad_migration_dormant_focus_conflict:${field}`) - } - } +/** Every focus field, so none the host left unset is borrowed from the desktop. */ +function sessionFocus(session: WorkspaceSessionState): Partial { + return Object.fromEntries(SESSION_FOCUS_FIELDS.map((field) => [field, session[field] ?? null])) +} + +function hasSessionFocus(session: WorkspaceSessionState): boolean { + return SESSION_FOCUS_FIELDS.some( + (field) => session[field] !== undefined && session[field] !== null + ) } export function applyPreparedOrcadMigrationWorkspaceSession( diff --git a/src/main/persistence/migrating-orcad-catalog/persistence-orcad-migration-catalog.test.ts b/src/main/persistence/migrating-orcad-catalog/persistence-orcad-migration-catalog.test.ts index 7d9ee86314b..b465ff16692 100644 --- a/src/main/persistence/migrating-orcad-catalog/persistence-orcad-migration-catalog.test.ts +++ b/src/main/persistence/migrating-orcad-catalog/persistence-orcad-migration-catalog.test.ts @@ -13,6 +13,7 @@ import { DORMANT_NAMESPACE, DORMANT_WORKTREE_ID, FOLDER_WORKSPACE, + PROJECT_GROUP, REPOSITORY, dormantState, manifest @@ -124,6 +125,64 @@ describe('orcad migration catalog persistence', () => { }) }) + // Two desktops on one host each carry their own focus; the second must not be refused for it. + it('takes a second desktop onto a host another desktop already moved, keeping the host focus whole', () => { + const store = createStore() + const first = manifest() + commitCatalog(store, manifest({ payload: { ...first.payload, dormantState: dormantState() } })) + // The host names only a project; the optional focus fields stay unset. + const { + activeWorkspaceKey: _key, + activeWorkspaceExecutionHostId: _host, + ...hostSession + } = store.getWorkspaceSession() + store.setWorkspaceSession({ + ...hostSession, + activeRepoId: REPOSITORY.id, + activeWorktreeId: null, + activeTabId: null + }) + + const secondRepo = { ...REPOSITORY, id: 'repo-2', path: '/srv/repo-2' } + const secondWorktree = `${secondRepo.id}::/srv/repo-2` + const second = manifest({ + migrationId: 'migration-second-desktop', + source: { sshTargetId: 'ssh-prod-b', sshTargetGeneration: 1, targetLabel: 'Production' }, + payload: { + repositories: [secondRepo], + projectGroups: [PROJECT_GROUP], + folderWorkspaces: [], + dormantState: { + version: 1, + worktreeMeta: [], + worktreeLineage: [], + workspaceLineage: [], + sparsePresets: [], + retiredWorktreeNames: [], + retiredWorktreeNamespaces: [], + workspaceSession: { + ...getDefaultWorkspaceSession(), + activeRepoId: secondRepo.id, + activeWorktreeId: secondWorktree, + activeWorkspaceKey: worktreeWorkspaceKey(secondWorktree), + activeWorkspaceExecutionHostId: 'local' + } + } + } + }) + + expect(commitCatalog(store, second)).toMatchObject({ state: 'committed' }) + expect(store.getRepos().map((repo) => repo.id)).toEqual([REPOSITORY.id, secondRepo.id]) + const focus = store.getWorkspaceSession() + expect(focus).toMatchObject({ + activeRepoId: REPOSITORY.id, + activeWorktreeId: null, + activeTabId: null + }) + expect(focus.activeWorkspaceKey ?? null).toBeNull() + expect(focus.activeWorkspaceExecutionHostId ?? null).toBeNull() + }) + it('stages durably without publishing rows, then commits catalog and receipt together', async () => { const store = createStore() const input = manifest() diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-session-rewrite.test.ts b/src/main/persistence/moving-wsl-projects/wsl-move-session-rewrite.test.ts index 87426f0a780..c5a04153170 100644 --- a/src/main/persistence/moving-wsl-projects/wsl-move-session-rewrite.test.ts +++ b/src/main/persistence/moving-wsl-projects/wsl-move-session-rewrite.test.ts @@ -354,8 +354,5 @@ describe('WSL move carried session focus', () => { activeWorktreeId: 'repo-on-host::/home/dev/other', activeTabId: 'host-tab' }) - expect(() => prepareOrcadMigrationWorkspaceSession(desktop, host)).toThrow( - 'orcad_migration_dormant_focus_conflict' - ) }) }) diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-session-rewrite.ts b/src/main/persistence/moving-wsl-projects/wsl-move-session-rewrite.ts index 143d2f7b974..8aa701fc189 100644 --- a/src/main/persistence/moving-wsl-projects/wsl-move-session-rewrite.ts +++ b/src/main/persistence/moving-wsl-projects/wsl-move-session-rewrite.ts @@ -143,10 +143,7 @@ function isSessionField(field: string): field is keyof WorkspaceSessionState { return Object.hasOwn(WORKSPACE_SESSION_FIELD_OWNERSHIP, field) } -/** - * This desktop's focus, which names nothing on the host: the host keeps its own, and a host that - * already has one refuses a manifest naming another (`orcad_migration_dormant_focus_conflict`). - */ +/** This desktop's focus, which names nothing on the host: the host keeps its own. */ export function withoutDesktopFocus(session: WorkspaceSessionState): WorkspaceSessionState { const { activeWorkspaceKey: _key, activeWorkspaceExecutionHostId: _host, ...rest } = session return { ...rest, activeRepoId: null, activeWorktreeId: null, activeTabId: null }