mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 10:08:13 +00:00
fix(wsl-mover): never leave behind an agent whose key may be the selected project
Review of #32343: scoping the session check to the selected projects let an unqualified key for a repo id another host also registers fall out of both the transfer and the refusal. Such a key now refuses. Adds tests that fail on the old code: another project's live agent with its tab open and its draft don't refuse, the same kinds of state in the selected project still do. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -171,6 +171,22 @@ export function orcadMigrationOwnerMatchesScope(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* An unqualified key naming a moved repo that another host also registers: it may be this move's
|
||||
* row or the other host's, so a WSL move can neither carry nor ignore what it owns.
|
||||
*/
|
||||
export function orcadMigrationOwnerKeyIsAmbiguous(
|
||||
value: string | null | undefined,
|
||||
scope: OrcadMigrationSourceScope
|
||||
): boolean {
|
||||
if (!value || isWorktreeHostIdentity(value)) {
|
||||
return false
|
||||
}
|
||||
return [...scope.sharedRepoIds].some(
|
||||
(repoId) => scope.repoIds.has(repoId) && ownerKeyBelongsToRepo(value, repoId)
|
||||
)
|
||||
}
|
||||
|
||||
export function unqualifyOrcadMigrationOwnerKey(value: string): string {
|
||||
return isWorktreeHostIdentity(value) ? getWorktreeIdFromHostIdentity(value) : value
|
||||
}
|
||||
|
||||
+5
-1
@@ -6,6 +6,7 @@ import type {
|
||||
WorkspaceSessionState
|
||||
} from '../../../shared/workspace-session-state-types'
|
||||
import {
|
||||
orcadMigrationOwnerKeyIsAmbiguous,
|
||||
orcadMigrationOwnerMatchesScope,
|
||||
orcadMigrationOwnsRepoId,
|
||||
orcadMigrationScopeOwnsWholeHost,
|
||||
@@ -33,7 +34,10 @@ export function collectUnsupportedSessionItems(
|
||||
// A WSL move takes some of host `local`'s projects; the others, and rows a past move retained,
|
||||
// stay on this desktop untouched, so only the selected projects' rows can refuse it.
|
||||
const wholeHost = sourceHostPartition && orcadMigrationScopeOwnsWholeHost(scope)
|
||||
const matches = (ownerKey: string): boolean => Boolean(ownerKey) && (wholeHost || owns(ownerKey))
|
||||
// An unattributable key that may be the selected project's refuses rather than staying behind.
|
||||
const matches = (ownerKey: string): boolean =>
|
||||
Boolean(ownerKey) &&
|
||||
(wholeHost || owns(ownerKey) || orcadMigrationOwnerKeyIsAmbiguous(ownerKey, scope))
|
||||
const items: string[] = []
|
||||
// Another project's rows in this host's copy stay behind; only an unsaved draft there is work.
|
||||
if (wholeHost) {
|
||||
|
||||
@@ -313,6 +313,84 @@ describe('exportWslMoveManifest', () => {
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
describe('only the selected projects can refuse it', () => {
|
||||
const agentIn = (worktreeId: string, tabId: string, leafId: string): SleepingAgentSessionRecord => ({
|
||||
paneKey: `${tabId}:${leafId}`,
|
||||
tabId,
|
||||
worktreeId,
|
||||
agent: 'codex',
|
||||
providerSession: { key: 'session_id', id: `session-${tabId}` },
|
||||
prompt: 'Saved prompt',
|
||||
state: 'done',
|
||||
capturedAt: 1,
|
||||
updatedAt: 1,
|
||||
connectionId: null,
|
||||
origin: 'live'
|
||||
})
|
||||
const layout = (leafId: string) => ({
|
||||
root: { type: 'leaf' as const, leafId },
|
||||
activeLeafId: leafId,
|
||||
expandedLeafId: null
|
||||
})
|
||||
|
||||
it('is not refused by a live agent with its tab open, and a draft, in another project', () => {
|
||||
const state = sourceState()
|
||||
const other = agentIn(OTHER_WORKTREE, 'windows-shell', 'win-leaf')
|
||||
state.workspaceSession.terminalLayoutsByTabId = { 'windows-shell': layout('elsewhere') }
|
||||
state.workspaceSession.sleepingAgentSessionsByPaneKey = { [other.paneKey]: other }
|
||||
state.workspaceSession.openFilesByWorktree = {
|
||||
[OTHER_WORKTREE]: [
|
||||
{
|
||||
filePath: 'C:\\code\\win\\notes.md',
|
||||
relativePath: 'notes.md',
|
||||
worktreeId: OTHER_WORKTREE,
|
||||
language: 'markdown',
|
||||
dirtyDraftContent: 'unsaved'
|
||||
}
|
||||
]
|
||||
}
|
||||
expect(exportWslMoveManifest(storeFor(state), REQUEST).blockers).toEqual([])
|
||||
})
|
||||
|
||||
it('is still refused by the same kinds of state in the selected project', () => {
|
||||
const state = sourceState()
|
||||
const own = agentIn(WORKTREE, 'feature-shell', 'feature-leaf')
|
||||
state.workspaceSession.terminalLayoutsByTabId = { 'feature-shell': layout('elsewhere') }
|
||||
state.workspaceSession.sleepingAgentSessionsByPaneKey = { [own.paneKey]: own }
|
||||
state.workspaceSession.openFilesByWorktree = {
|
||||
[WORKTREE]: [
|
||||
{
|
||||
filePath: '/home/me/app-feature/notes.md',
|
||||
relativePath: 'notes.md',
|
||||
worktreeId: WORKTREE,
|
||||
language: 'markdown',
|
||||
dirtyDraftContent: 'unsaved',
|
||||
runtimeEnvironmentId: 'some-other-server'
|
||||
}
|
||||
]
|
||||
}
|
||||
const { blockers } = exportWslMoveManifest(storeFor(state), REQUEST)
|
||||
expect(blockers).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringMatching(/codex session saved in/),
|
||||
expect.stringMatching(/unsaved changes to/)
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it('never silently leaves behind an agent whose key may be the selected project', () => {
|
||||
// `app` is also registered by an SSH host, so an unqualified `app::` key names either.
|
||||
const state = sourceState()
|
||||
state.repos.push(repo('app', '/srv/app', { connectionId: 'box' }))
|
||||
const own = agentIn(WORKTREE, 'feature-shell', 'feature-leaf')
|
||||
state.workspaceSession.terminalLayoutsByTabId = { 'feature-shell': layout('elsewhere') }
|
||||
state.workspaceSession.sleepingAgentSessionsByPaneKey = { [own.paneKey]: own }
|
||||
expect(exportWslMoveManifest(storeFor(state), REQUEST).blockers).toEqual([
|
||||
expect.stringMatching(/codex session saved in/)
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it('refuses to sign a scope naming a project that is no longer here', () => {
|
||||
expect(() =>
|
||||
exportWslMoveManifest(storeFor(sourceState()), { ...REQUEST, repoIds: ['app', 'gone'] })
|
||||
|
||||
Reference in New Issue
Block a user