mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 08:03:37 +00:00
Merge remote-tracking branch 'origin/brennanb2025/worktree-inactive-filter-finder' into brennanb2025/review-pr-13405-snapshots
This commit is contained in:
@@ -193,19 +193,48 @@ describe('workspace cleanup broad scan opt-in', () => {
|
||||
})
|
||||
|
||||
it('reports folder workspaces with a folder-repo blocker', async () => {
|
||||
const result = await scanWorkspaceCleanup(makeStore([FOLDER_REPO]), {
|
||||
const instanceId = `${FOLDER_REPO.id}::${FOLDER_REPO.path}::workspace:11111111-2222-4333-8444-555555555555`
|
||||
const instanceMeta = makeWorktreeMeta({
|
||||
displayName: 'Folder session',
|
||||
lastActivityAt: NOW - 2 * DAY_MS
|
||||
})
|
||||
const result = await scanWorkspaceCleanup(
|
||||
makeStore([FOLDER_REPO], { [instanceId]: instanceMeta }),
|
||||
{ includeAllWorkspaces: true }
|
||||
)
|
||||
|
||||
expect(result.candidates).toEqual([
|
||||
expect.objectContaining({
|
||||
worktreeId: 'repo-folder::/folder-workspace',
|
||||
tier: 'protected',
|
||||
selectedByDefault: false,
|
||||
blockers: expect.arrayContaining(['folder-repo', 'main-worktree'])
|
||||
}),
|
||||
expect.objectContaining({
|
||||
worktreeId: instanceId,
|
||||
displayName: 'Folder session',
|
||||
path: FOLDER_REPO.path,
|
||||
tier: 'protected',
|
||||
selectedByDefault: false,
|
||||
blockers: ['folder-repo']
|
||||
})
|
||||
])
|
||||
})
|
||||
|
||||
it('does not mark connected SSH folder workspaces as disconnected', async () => {
|
||||
const sshFolderRepo: Repo = { ...FOLDER_REPO, connectionId: 'ssh-1' }
|
||||
getSshGitProviderMock.mockReturnValue({})
|
||||
|
||||
const result = await scanWorkspaceCleanup(makeStore([sshFolderRepo]), {
|
||||
includeAllWorkspaces: true
|
||||
})
|
||||
|
||||
expect(result.candidates).toHaveLength(1)
|
||||
expect(result.candidates[0]).toMatchObject({
|
||||
worktreeId: 'repo-folder::/folder-workspace',
|
||||
tier: 'protected',
|
||||
selectedByDefault: false
|
||||
connectionId: 'ssh-1',
|
||||
blockers: ['main-worktree', 'folder-repo']
|
||||
})
|
||||
expect(result.candidates[0]?.blockers).toEqual(
|
||||
expect.arrayContaining(['folder-repo', 'main-worktree'])
|
||||
)
|
||||
})
|
||||
|
||||
it('defers git evidence for rows that no cleanup decision can use', async () => {
|
||||
@@ -258,6 +287,27 @@ describe('workspace cleanup broad scan opt-in', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('uses the backing path for disconnected SSH folder instances', async () => {
|
||||
const sshFolderRepo: Repo = { ...FOLDER_REPO, connectionId: 'ssh-1' }
|
||||
const instanceId = `${sshFolderRepo.id}::${sshFolderRepo.path}::workspace:11111111-2222-4333-8444-555555555555`
|
||||
const allMeta = {
|
||||
[instanceId]: makeWorktreeMeta({ displayName: 'Remote folder session' })
|
||||
}
|
||||
|
||||
const result = await scanWorkspaceCleanup(makeStore([sshFolderRepo], allMeta), {
|
||||
includeAllWorkspaces: true
|
||||
})
|
||||
|
||||
expect(result.candidates).toEqual([
|
||||
expect.objectContaining({
|
||||
worktreeId: instanceId,
|
||||
path: sshFolderRepo.path,
|
||||
branch: 'folder-workspace',
|
||||
blockers: ['ssh-disconnected']
|
||||
})
|
||||
])
|
||||
})
|
||||
|
||||
it('streams every row through scan progress on an opt-in scan', async () => {
|
||||
const progress: { scannedWorktreeCount: number; totalWorktreeCount: number }[] = []
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
createWorkspaceCleanupFingerprint,
|
||||
type WorkspaceCleanupCandidate
|
||||
} from '../../shared/workspace-cleanup'
|
||||
import { splitWorktreeId } from '../../shared/worktree-id'
|
||||
import { splitWorktreeIdForFilesystem } from '../../shared/worktree-id'
|
||||
import {
|
||||
getNewestWorkspaceCleanupDiffCommentAt,
|
||||
getWorkspaceCleanupInactivityReasonsForWorkspace,
|
||||
@@ -52,7 +52,7 @@ function createDisconnectedSshCandidate(
|
||||
worktreeId: string,
|
||||
meta: WorktreeMeta
|
||||
): WorkspaceCleanupCandidate {
|
||||
const parsed = splitWorktreeId(worktreeId)
|
||||
const parsed = splitWorktreeIdForFilesystem(worktreeId)
|
||||
const path = parsed?.worktreePath ?? worktreeId
|
||||
const reasons = getWorkspaceCleanupInactivityReasonsForWorkspace(meta, scannedAt)
|
||||
return applyWorkspaceCleanupPolicy({
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { Store } from '../persistence'
|
||||
import { createFolderWorktree } from '../repo-worktrees'
|
||||
import type { Repo, Worktree } from '../../shared/types'
|
||||
import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR } from '../../shared/worktree-id'
|
||||
import { mergeWorktree } from './worktree-logic'
|
||||
|
||||
export function listWorkspaceCleanupFolderWorkspaces(store: Store, repo: Repo): Worktree[] {
|
||||
const rootId = `${repo.id}::${repo.path}`
|
||||
const instancePrefix = `${rootId}${FOLDER_WORKSPACE_INSTANCE_SEPARATOR}`
|
||||
const allMeta = store.getAllWorktreeMeta()
|
||||
const worktreeIds = [
|
||||
rootId,
|
||||
...Object.keys(allMeta).filter((worktreeId) => worktreeId.startsWith(instancePrefix))
|
||||
]
|
||||
const folderWorktree = createFolderWorktree(repo)
|
||||
|
||||
return worktreeIds.map((worktreeId) => ({
|
||||
...mergeWorktree(repo.id, folderWorktree, store.getWorktreeMeta(worktreeId), repo.displayName),
|
||||
id: worktreeId,
|
||||
isMainWorktree: worktreeId === rootId
|
||||
}))
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
withWorkspaceCleanupTimeout
|
||||
} from './workspace-cleanup-scan-primitives'
|
||||
import { getLocalProjectWorktreeGitOptions } from '../project-runtime-git-options'
|
||||
import { listWorkspaceCleanupFolderWorkspaces } from './workspace-cleanup-folder-workspaces'
|
||||
|
||||
const WORKTREE_SCAN_CONCURRENCY = 3
|
||||
|
||||
@@ -142,11 +143,14 @@ async function scanRepoWorkspaces(
|
||||
return { scannedAt, candidates, errors: [] }
|
||||
}
|
||||
|
||||
const mergedWorktrees = gitWorktrees.map((gitWorktree) => {
|
||||
const worktreeId = `${repo.id}::${gitWorktree.path}`
|
||||
const meta = store.getWorktreeMeta(worktreeId)
|
||||
return mergeWorktree(repo.id, gitWorktree, meta, repo.displayName)
|
||||
})
|
||||
const mergedWorktrees =
|
||||
repoIsFolder && includeAllWorkspaces
|
||||
? listWorkspaceCleanupFolderWorkspaces(store, repo)
|
||||
: gitWorktrees.map((gitWorktree) => {
|
||||
const worktreeId = `${repo.id}::${gitWorktree.path}`
|
||||
const meta = store.getWorktreeMeta(worktreeId)
|
||||
return mergeWorktree(repo.id, gitWorktree, meta, repo.displayName)
|
||||
})
|
||||
// Why: with includeAllWorkspaces the browser shows every workspace and lets
|
||||
// filters narrow it; an age threshold here would hide rows from all views.
|
||||
const candidateWorktrees = targetWorktreeId
|
||||
@@ -227,7 +231,10 @@ async function listCleanupGitWorktrees(
|
||||
repoIsFolder: boolean
|
||||
): Promise<{ provider: IGitProvider | null; gitWorktrees: GitWorktreeInfo[] }> {
|
||||
if (repoIsFolder) {
|
||||
return { provider: null, gitWorktrees: [createFolderWorktree(repo)] }
|
||||
return {
|
||||
provider: repo.connectionId ? (getSshGitProvider(repo.connectionId) ?? null) : null,
|
||||
gitWorktrees: [createFolderWorktree(repo)]
|
||||
}
|
||||
}
|
||||
if (repo.connectionId) {
|
||||
const provider = getSshGitProvider(repo.connectionId) ?? null
|
||||
|
||||
@@ -552,6 +552,24 @@ describe('workspace cleanup scan progress', () => {
|
||||
expect(store.getState().workspaceCleanupScan).toMatchObject(secondResult)
|
||||
})
|
||||
|
||||
it('does not join legacy and full-workspace broad scans', async () => {
|
||||
const legacyPending = deferred<WorkspaceCleanupScanResult>()
|
||||
const fullPending = deferred<WorkspaceCleanupScanResult>()
|
||||
const scan = vi.fn((args?: { includeAllWorkspaces?: boolean }) =>
|
||||
args?.includeAllWorkspaces ? fullPending.promise : legacyPending.promise
|
||||
)
|
||||
installWorkspaceCleanupApi(scan)
|
||||
const store = createCleanupTestStore()
|
||||
|
||||
const legacy = store.getState().scanWorkspaceCleanup()
|
||||
const full = store.getState().scanWorkspaceCleanup({ includeAllWorkspaces: true })
|
||||
|
||||
expect(scan).toHaveBeenCalledTimes(2)
|
||||
fullPending.resolve({ scannedAt: NOW, candidates: [], errors: [] })
|
||||
legacyPending.resolve({ scannedAt: NOW - 1, candidates: [], errors: [] })
|
||||
await expect(Promise.all([legacy, full])).resolves.toHaveLength(2)
|
||||
})
|
||||
|
||||
it('keeps stale cleanup results visible after a broad refresh failure', async () => {
|
||||
const previous = { scannedAt: NOW, candidates: [makeCandidate()], errors: [] }
|
||||
const scan = vi
|
||||
|
||||
@@ -367,6 +367,7 @@ export const createWorkspaceCleanupSlice: StateCreator<AppState, [], [], Workspa
|
||||
|
||||
function getWorkspaceCleanupScanKey(args: WorkspaceCleanupScanArgs): string {
|
||||
return JSON.stringify({
|
||||
includeAllWorkspaces: args.includeAllWorkspaces === true,
|
||||
skipGitWorktreeIds: [...new Set(args.skipGitWorktreeIds ?? [])].sort()
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user