mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
fix(ssh): let a second desktop move onto a host another desktop already set up (#35879)
Moving a desktop's SSH state onto the host's Orca server refused with orcad_migration_dormant_focus_conflict whenever the host already had a focused project from another desktop. Focus is each desktop's own selection, not saved state, so two desktops always differ and the second could never move, on every retry. The host now keeps its own focus as a unit and takes a desktop's focus only when it has none, which is what the merge already did for the fields it borrowed. Older servers still refuse until they update. Fixes #35585
This commit is contained in:
+14
-19
@@ -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<WorkspaceSessionState> {
|
||||
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(
|
||||
|
||||
+59
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user