mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
chore(workspaces): drop the dead workspaceCleanup:hasKillableLocalProcesses IPC (#18386)
This commit is contained in:
@@ -1,112 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ipcMain } from 'electron'
|
||||
import type { Store } from '../persistence'
|
||||
|
||||
const { getSshPtyProviderMock } = vi.hoisted(() => ({
|
||||
getSshPtyProviderMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: vi.fn(),
|
||||
removeHandler: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('./pty', () => ({
|
||||
getSshPtyProvider: getSshPtyProviderMock
|
||||
}))
|
||||
|
||||
vi.mock('../memory/pty-registry', () => ({
|
||||
listRegisteredPtys: vi.fn(() => [])
|
||||
}))
|
||||
|
||||
vi.mock('../workspace-cleanup-scan-snapshot', () => ({
|
||||
persistWorkspaceCleanupScanResult: vi.fn(async () => undefined),
|
||||
readWorkspaceCleanupScanSnapshot: vi.fn(async () => null)
|
||||
}))
|
||||
|
||||
vi.mock('../workspace-cleanup-removal-snapshot-prune', () => ({
|
||||
beginWorkspaceCleanupRemovalSnapshotPruneBatch: vi.fn(),
|
||||
finishWorkspaceCleanupRemovalSnapshotPruneBatch: vi.fn(async () => undefined),
|
||||
recordWorkspaceCleanupRemovalSnapshotPrune: vi.fn()
|
||||
}))
|
||||
|
||||
import { registerWorkspaceCleanupHandlers } from './workspace-cleanup'
|
||||
|
||||
function makeEmptyStore(): Store {
|
||||
return {
|
||||
getProfileStorageDirectory: () => '/profile-a',
|
||||
getRepos: () => [],
|
||||
getWorktreeMeta: () => ({}),
|
||||
getAllWorktreeMeta: () => ({}),
|
||||
getGitHubCache: () => ({ pr: {}, issue: {} })
|
||||
} as unknown as Store
|
||||
}
|
||||
|
||||
function getPreflightHandler(): ((...args: never[]) => unknown) | undefined {
|
||||
return vi
|
||||
.mocked(ipcMain.handle)
|
||||
.mock.calls.find(([channel]) => channel === 'workspaceCleanup:hasKillableLocalProcesses')?.[1]
|
||||
}
|
||||
|
||||
describe('workspace cleanup process preflight', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(ipcMain.handle).mockReset()
|
||||
getSshPtyProviderMock.mockReset()
|
||||
})
|
||||
|
||||
it('reports local processes that workspace deletion would kill', async () => {
|
||||
const localProvider = {
|
||||
listProcesses: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: 'repo-1::/repo-feature@@session-1',
|
||||
cwd: '/repo-feature',
|
||||
title: 'zsh'
|
||||
}
|
||||
])
|
||||
}
|
||||
registerWorkspaceCleanupHandlers(makeEmptyStore(), {
|
||||
runtime: {
|
||||
hasTerminalsForWorktree: vi.fn().mockResolvedValue(false)
|
||||
} as never,
|
||||
getLocalPtyProvider: () => localProvider as never
|
||||
})
|
||||
|
||||
await expect(
|
||||
getPreflightHandler()?.({} as never, { worktreeId: 'repo-1::/repo-feature' } as never)
|
||||
).resolves.toEqual({
|
||||
hasKillableProcesses: true
|
||||
})
|
||||
})
|
||||
|
||||
it('reports SSH processes inside the remote workspace path', async () => {
|
||||
getSshPtyProviderMock.mockReturnValue({
|
||||
listProcesses: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: 'remote-session-1',
|
||||
cwd: '/remote/repo-feature/subdir',
|
||||
title: 'codex'
|
||||
}
|
||||
])
|
||||
})
|
||||
registerWorkspaceCleanupHandlers(makeEmptyStore(), {
|
||||
runtime: {
|
||||
hasTerminalsForWorktree: vi.fn().mockResolvedValue(false)
|
||||
} as never
|
||||
})
|
||||
|
||||
await expect(
|
||||
getPreflightHandler()?.(
|
||||
{} as never,
|
||||
{
|
||||
worktreeId: 'repo-ssh::/remote/repo-feature',
|
||||
connectionId: 'ssh-1',
|
||||
worktreePath: '/remote/repo-feature'
|
||||
} as never
|
||||
)
|
||||
).resolves.toEqual({
|
||||
hasKillableProcesses: true
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,14 +1,8 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import type { Store } from '../persistence'
|
||||
import type { IPtyProvider } from '../providers/types'
|
||||
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
|
||||
import { listRegisteredPtys } from '../memory/pty-registry'
|
||||
import { getSshPtyProvider } from './pty'
|
||||
import {
|
||||
WORKSPACE_CLEANUP_CLASSIFIER_VERSION,
|
||||
type WorkspaceCleanupDismissArgs,
|
||||
type WorkspaceCleanupLocalProcessArgs,
|
||||
type WorkspaceCleanupLocalProcessResult,
|
||||
type WorkspaceCleanupScanArgs,
|
||||
type WorkspaceCleanupScanResult,
|
||||
type WorkspaceCleanupSnapshotPruneBatchArgs,
|
||||
@@ -30,11 +24,6 @@ import {
|
||||
|
||||
export { scanWorkspaceCleanup }
|
||||
|
||||
type WorkspaceCleanupHandlerDeps = {
|
||||
runtime?: OrcaRuntimeService
|
||||
getLocalPtyProvider?: () => IPtyProvider
|
||||
}
|
||||
|
||||
// Why: module scope — handler re-registration on a new main window must not
|
||||
// orphan the previous window's controllers in a discarded map.
|
||||
const activeScans = new Map<string, AbortController>()
|
||||
@@ -47,17 +36,13 @@ function getBroadScanModeKey(senderId: number, args: WorkspaceCleanupScanArgs):
|
||||
return `${senderId}\0${args.includeAllWorkspaces === true}`
|
||||
}
|
||||
|
||||
export function registerWorkspaceCleanupHandlers(
|
||||
store: Store,
|
||||
deps: WorkspaceCleanupHandlerDeps = {}
|
||||
): void {
|
||||
export function registerWorkspaceCleanupHandlers(store: Store): void {
|
||||
const snapshotDirectory = store.getProfileStorageDirectory()
|
||||
ipcMain.removeHandler('workspaceCleanup:scan')
|
||||
ipcMain.removeHandler('workspaceCleanup:cancelScan')
|
||||
ipcMain.removeHandler('workspaceCleanup:getCachedScan')
|
||||
ipcMain.removeHandler('workspaceCleanup:dismiss')
|
||||
ipcMain.removeHandler('workspaceCleanup:clearDismissals')
|
||||
ipcMain.removeHandler('workspaceCleanup:hasKillableLocalProcesses')
|
||||
ipcMain.removeHandler('workspaceCleanup:beginRemovalSnapshotPruneBatch')
|
||||
ipcMain.removeHandler('workspaceCleanup:recordRemovalSnapshotPrune')
|
||||
ipcMain.removeHandler('workspaceCleanup:finishRemovalSnapshotPruneBatch')
|
||||
@@ -161,16 +146,6 @@ export function registerWorkspaceCleanupHandlers(
|
||||
store.updateUI({ workspaceCleanup: { dismissals: {} } })
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
'workspaceCleanup:hasKillableLocalProcesses',
|
||||
async (
|
||||
_event,
|
||||
args: WorkspaceCleanupLocalProcessArgs
|
||||
): Promise<WorkspaceCleanupLocalProcessResult> => ({
|
||||
hasKillableProcesses: await hasKillableProcesses(args, deps)
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
'workspaceCleanup:beginRemovalSnapshotPruneBatch',
|
||||
(_event, args: WorkspaceCleanupSnapshotPruneBatchArgs) => {
|
||||
@@ -214,92 +189,3 @@ function getWorkspaceCleanupScanKey(senderId: number, scanId: unknown): string |
|
||||
? `${senderId}\0${scanId}`
|
||||
: null
|
||||
}
|
||||
|
||||
async function hasKillableProcesses(
|
||||
args: WorkspaceCleanupLocalProcessArgs,
|
||||
deps: WorkspaceCleanupHandlerDeps
|
||||
): Promise<boolean | null> {
|
||||
const { worktreeId } = args
|
||||
if (typeof worktreeId !== 'string' || worktreeId.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
let livenessUnknown = false
|
||||
if (deps.runtime) {
|
||||
try {
|
||||
if (await deps.runtime.hasTerminalsForWorktree(worktreeId)) {
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
livenessUnknown = true
|
||||
}
|
||||
}
|
||||
|
||||
if (args.connectionId) {
|
||||
return hasKillableSshProcesses(args.connectionId, args.worktreePath ?? '', livenessUnknown)
|
||||
}
|
||||
|
||||
const registryPtyIds = new Set(
|
||||
listRegisteredPtys()
|
||||
.filter((entry) => entry.worktreeId === worktreeId)
|
||||
.map((entry) => entry.ptyId)
|
||||
)
|
||||
|
||||
const provider = deps.getLocalPtyProvider?.()
|
||||
if (!provider) {
|
||||
return registryPtyIds.size > 0 ? true : null
|
||||
}
|
||||
|
||||
try {
|
||||
const prefix = `${worktreeId}@@`
|
||||
const sessions = await provider.listProcesses()
|
||||
if (
|
||||
sessions.some((session) => session.id.startsWith(prefix) || registryPtyIds.has(session.id))
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return livenessUnknown ? null : false
|
||||
} catch {
|
||||
return registryPtyIds.size > 0 ? true : null
|
||||
}
|
||||
}
|
||||
|
||||
async function hasKillableSshProcesses(
|
||||
connectionId: string,
|
||||
worktreePath: string,
|
||||
livenessUnknown: boolean
|
||||
): Promise<boolean | null> {
|
||||
const provider = getSshPtyProvider(connectionId)
|
||||
if (!provider) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const normalizedWorktreePath = normalizeRemotePath(worktreePath)
|
||||
const sessions = await provider.listProcesses()
|
||||
if (
|
||||
sessions.some((session) => {
|
||||
if (session.id.startsWith(`${worktreePath}@@`)) {
|
||||
return true
|
||||
}
|
||||
return (
|
||||
normalizedWorktreePath.length > 0 &&
|
||||
isPathWithin(normalizeRemotePath(session.cwd), normalizedWorktreePath)
|
||||
)
|
||||
})
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return livenessUnknown ? null : false
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeRemotePath(path: string): string {
|
||||
return path.replace(/\\/g, '/').replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
function isPathWithin(candidatePath: string, parentPath: string): boolean {
|
||||
return candidatePath === parentPath || candidatePath.startsWith(`${parentPath}/`)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { setWorktreeCatalogRemoteClientNotifier } from '../ipc/watched-worktree-
|
||||
import { registerWorktreeHandlers } from '../ipc/worktrees'
|
||||
import { registerWorkspaceCleanupHandlers } from '../ipc/workspace-cleanup'
|
||||
import {
|
||||
getLocalPtyProvider,
|
||||
registerPtyHandlers,
|
||||
type CodexHomePtySpawnedLifecycleArgs,
|
||||
type GetSelectedCodexHomePath,
|
||||
@@ -83,7 +82,7 @@ export function attachMainWindowServices(
|
||||
// Why: folder projects get no watch target, so an external `git init` needs its own
|
||||
// marker poll to upgrade them without a restart (#11477).
|
||||
startFolderRepoGitUpgradeWatch(store, mainWindow)
|
||||
registerWorkspaceCleanupHandlers(store, { runtime, getLocalPtyProvider })
|
||||
registerWorkspaceCleanupHandlers(store)
|
||||
registerPtyHandlers(
|
||||
mainWindow,
|
||||
runtime,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type {
|
||||
WorkspaceCleanupDismissArgs,
|
||||
WorkspaceCleanupLocalProcessArgs,
|
||||
WorkspaceCleanupLocalProcessResult,
|
||||
WorkspaceCleanupScanArgs,
|
||||
WorkspaceCleanupScanProgress,
|
||||
WorkspaceCleanupScanResult,
|
||||
@@ -24,9 +22,6 @@ export type WorkspaceCleanupApi = {
|
||||
getCachedScan: () => Promise<WorkspaceCleanupScanResult | null>
|
||||
dismiss: (args: WorkspaceCleanupDismissArgs) => Promise<void>
|
||||
clearDismissals: () => Promise<void>
|
||||
hasKillableLocalProcesses: (
|
||||
args: WorkspaceCleanupLocalProcessArgs
|
||||
) => Promise<WorkspaceCleanupLocalProcessResult>
|
||||
beginRemovalSnapshotPruneBatch?: (args: WorkspaceCleanupSnapshotPruneBatchArgs) => Promise<void>
|
||||
recordRemovalSnapshotPrune?: (args: WorkspaceCleanupSnapshotPruneRecordArgs) => Promise<void>
|
||||
finishRemovalSnapshotPruneBatch?: (args: WorkspaceCleanupSnapshotPruneBatchArgs) => Promise<void>
|
||||
|
||||
@@ -25,8 +25,6 @@ export const workspaceCleanupApi = {
|
||||
getCachedScan: () => ipcRenderer.invoke('workspaceCleanup:getCachedScan'),
|
||||
dismiss: (args) => ipcRenderer.invoke('workspaceCleanup:dismiss', args),
|
||||
clearDismissals: () => ipcRenderer.invoke('workspaceCleanup:clearDismissals'),
|
||||
hasKillableLocalProcesses: (args) =>
|
||||
ipcRenderer.invoke('workspaceCleanup:hasKillableLocalProcesses', args),
|
||||
beginRemovalSnapshotPruneBatch: (args) =>
|
||||
ipcRenderer.invoke('workspaceCleanup:beginRemovalSnapshotPruneBatch', args),
|
||||
recordRemovalSnapshotPrune: (args) =>
|
||||
|
||||
+1
-2
@@ -120,8 +120,7 @@ function installApi(cachedScan: WorkspaceCleanupScanResult | null): ScanRig {
|
||||
scan: rig.scan,
|
||||
getCachedScan: vi.fn().mockResolvedValue(cachedScan),
|
||||
dismiss: vi.fn().mockResolvedValue(undefined),
|
||||
clearDismissals: vi.fn().mockResolvedValue(undefined),
|
||||
hasKillableLocalProcesses: vi.fn().mockResolvedValue({ hasKillableProcesses: false })
|
||||
clearDismissals: vi.fn().mockResolvedValue(undefined)
|
||||
},
|
||||
workspaceSpace: {
|
||||
getCachedAnalysis: vi.fn().mockResolvedValue(null),
|
||||
|
||||
@@ -258,10 +258,7 @@ describe('workspace cleanup removal and protection', () => {
|
||||
})
|
||||
),
|
||||
dismiss: vi.fn().mockResolvedValue(undefined),
|
||||
clearDismissals: vi.fn().mockResolvedValue(undefined),
|
||||
hasKillableLocalProcesses: vi.fn().mockResolvedValue({
|
||||
hasKillableProcesses: false
|
||||
})
|
||||
clearDismissals: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -291,10 +288,7 @@ describe('workspace cleanup removal and protection', () => {
|
||||
workspaceCleanup: {
|
||||
scan,
|
||||
dismiss: vi.fn().mockResolvedValue(undefined),
|
||||
clearDismissals: vi.fn().mockResolvedValue(undefined),
|
||||
hasKillableLocalProcesses: vi.fn().mockResolvedValue({
|
||||
hasKillableProcesses: false
|
||||
})
|
||||
clearDismissals: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -333,10 +327,7 @@ describe('workspace cleanup removal and protection', () => {
|
||||
workspaceCleanup: {
|
||||
scan,
|
||||
dismiss: vi.fn().mockResolvedValue(undefined),
|
||||
clearDismissals: vi.fn().mockResolvedValue(undefined),
|
||||
hasKillableLocalProcesses: vi.fn().mockResolvedValue({
|
||||
hasKillableProcesses: false
|
||||
})
|
||||
clearDismissals: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -368,10 +359,7 @@ describe('workspace cleanup removal and protection', () => {
|
||||
workspaceCleanup: {
|
||||
scan,
|
||||
dismiss: vi.fn().mockResolvedValue(undefined),
|
||||
clearDismissals: vi.fn().mockResolvedValue(undefined),
|
||||
hasKillableLocalProcesses: vi.fn().mockResolvedValue({
|
||||
hasKillableProcesses: true
|
||||
})
|
||||
clearDismissals: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,10 +93,7 @@ export function installWorkspaceCleanupApi(
|
||||
scan,
|
||||
getCachedScan,
|
||||
dismiss: vi.fn().mockResolvedValue(undefined),
|
||||
clearDismissals: vi.fn().mockResolvedValue(undefined),
|
||||
hasKillableLocalProcesses: vi.fn().mockResolvedValue({
|
||||
hasKillableProcesses: false
|
||||
})
|
||||
clearDismissals: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,12 +100,6 @@ export type WorkspaceCleanupScanArgs = {
|
||||
|
||||
export const WORKSPACE_CLEANUP_TARGET_BATCH_LIMIT = 500
|
||||
|
||||
export type WorkspaceCleanupLocalProcessArgs = {
|
||||
worktreeId: string
|
||||
connectionId?: string | null
|
||||
worktreePath?: string
|
||||
}
|
||||
|
||||
export type WorkspaceCleanupSnapshotPruneBatchArgs = {
|
||||
batchId: string
|
||||
}
|
||||
@@ -144,10 +138,6 @@ export type WorkspaceCleanupUnverifiedRemovalConsent = {
|
||||
attemptId: string
|
||||
}
|
||||
|
||||
export type WorkspaceCleanupLocalProcessResult = {
|
||||
hasKillableProcesses: boolean | null
|
||||
}
|
||||
|
||||
export type WorkspaceCleanupDismissArgs = {
|
||||
dismissals: WorkspaceCleanupDismissal[]
|
||||
/** Removed worktrees' persisted dismissals are dead weight; prune them. */
|
||||
|
||||
Reference in New Issue
Block a user