mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 10:08:13 +00:00
fix(ssh): a host converted to its managed server keeps its workspaces' visit recency (#30015)
Converting an SSH host drops its relay-era rows, but recency stayed under ssh:<target>|<id> keys, which the next startup prune discards because no row matches them any more. Re-key that host's visits to runtime:<env>|<id> in the same update, keeping the newer visit on a collision.
This commit is contained in:
@@ -143,6 +143,20 @@ describe('a host that just moved to its managed server', () => {
|
||||
expect(state.folderWorkspaces.map((entry) => entry.executionHostId)).toEqual(['runtime:env-1'])
|
||||
})
|
||||
|
||||
it('carries the host’s visit recency to the server instead of leaving it for the next prune', async () => {
|
||||
store.setState({
|
||||
lastVisitedAtByWorktreeId: { [`ssh:ssh-1|${worktreeId}`]: 500, [`local|${worktreeId}`]: 7 }
|
||||
})
|
||||
applySshManagedServerTransition('ssh-1', undefined, { kind: 'managed', environmentId: 'env-7' })
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(store.getState().lastVisitedAtByWorktreeId).toEqual({
|
||||
[`runtime:env-7|${worktreeId}`]: 500,
|
||||
[`local|${worktreeId}`]: 7
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('moves the open workspace onto the server so its panes stop dialing the stopped relay', async () => {
|
||||
store.setState({
|
||||
worktreesByRepo: {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useAppStore } from '../../store'
|
||||
import { withoutConvertedSshHostRows } from '../../store/repos/converted-ssh-host-rows'
|
||||
import { rekeyWorktreeVisitHost } from '@/lib/worktree-visit-recency'
|
||||
import { preserveConvertedSshBrowserPages } from '../../store/repos/converted-ssh-browser-pages'
|
||||
|
||||
type ManagedServerStatus = SshConnectionState['managedServer']
|
||||
@@ -154,7 +155,20 @@ async function loadManagedServerCatalogs(targetId: string, load: CatalogLoad): P
|
||||
if (!isCurrent()) {
|
||||
return
|
||||
}
|
||||
useAppStore.setState((state) => withoutConvertedSshHostRows(state, targetId))
|
||||
useAppStore.setState((state) => {
|
||||
// Why: the host's workspaces are the server's now; unmoved, the next prune drops their recency.
|
||||
const lastVisitedAtByWorktreeId = rekeyWorktreeVisitHost(
|
||||
state.lastVisitedAtByWorktreeId,
|
||||
toSshExecutionHostId(targetId),
|
||||
toRuntimeExecutionHostId(environmentId)
|
||||
)
|
||||
return {
|
||||
...withoutConvertedSshHostRows(state, targetId),
|
||||
...(lastVisitedAtByWorktreeId === state.lastVisitedAtByWorktreeId
|
||||
? {}
|
||||
: { lastVisitedAtByWorktreeId })
|
||||
}
|
||||
})
|
||||
rehomeActiveWorkspace(targetId, environmentId)
|
||||
if (!useAppStore.getState().startupWorktreeRefreshCompleted) {
|
||||
rehomeAfterStartupScan(targetId, environmentId, isCurrent)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
getWorktreeVisitKey,
|
||||
getWorktreeVisitTimestamp,
|
||||
rekeyWorktreeVisitHost,
|
||||
removeWorktreeVisitEntries
|
||||
} from './worktree-visit-recency'
|
||||
|
||||
@@ -42,3 +43,34 @@ describe('worktree visit recency identities', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('rekeyWorktreeVisitHost', () => {
|
||||
const id = 'repo::/srv/app'
|
||||
|
||||
it('moves one host’s visits to the host that now runs its workspaces, newer visit winning', () => {
|
||||
const moved = rekeyWorktreeVisitHost(
|
||||
{
|
||||
[getWorktreeVisitKey(id, 'ssh:box')]: 300,
|
||||
[getWorktreeVisitKey(id, 'runtime:env')]: 100,
|
||||
[getWorktreeVisitKey('repo::/srv/other', 'ssh:box')]: 50,
|
||||
[getWorktreeVisitKey(id, 'ssh:box-2')]: 70,
|
||||
[getWorktreeVisitKey(id, 'local')]: 20,
|
||||
[id]: 10
|
||||
},
|
||||
'ssh:box',
|
||||
'runtime:env'
|
||||
)
|
||||
expect(moved).toEqual({
|
||||
[getWorktreeVisitKey(id, 'runtime:env')]: 300,
|
||||
[getWorktreeVisitKey('repo::/srv/other', 'runtime:env')]: 50,
|
||||
[getWorktreeVisitKey(id, 'ssh:box-2')]: 70,
|
||||
[getWorktreeVisitKey(id, 'local')]: 20,
|
||||
[id]: 10
|
||||
})
|
||||
})
|
||||
|
||||
it('returns the same map when the host has no visits', () => {
|
||||
const timestamps = { [getWorktreeVisitKey(id, 'local')]: 1 }
|
||||
expect(rekeyWorktreeVisitHost(timestamps, 'ssh:box', 'runtime:env')).toBe(timestamps)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -97,3 +97,27 @@ export function removeWorktreeVisitEntriesForTargets(
|
||||
}
|
||||
return changed ? next : (timestamps as Record<string, number>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Move one host's recency to another host when the same workspaces now live there (an SSH host
|
||||
* converted to its managed server). Bare legacy keys and other hosts are untouched; a collision
|
||||
* keeps the newer visit.
|
||||
*/
|
||||
export function rekeyWorktreeVisitHost(
|
||||
timestamps: Readonly<Record<string, number>>,
|
||||
fromHostId: ExecutionHostId,
|
||||
toHostId: ExecutionHostId
|
||||
): Readonly<Record<string, number>> {
|
||||
const prefix = `${fromHostId}|`
|
||||
if (!Object.keys(timestamps).some((key) => key.startsWith(prefix))) {
|
||||
return timestamps
|
||||
}
|
||||
const next: Record<string, number> = {}
|
||||
for (const [key, timestamp] of Object.entries(timestamps)) {
|
||||
const target = key.startsWith(prefix)
|
||||
? composeWorktreeHostIdentity(toHostId, getWorktreeIdFromHostIdentity(key))
|
||||
: key
|
||||
next[target] = Math.max(next[target] ?? timestamp, timestamp)
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user