Merge remote-tracking branch 'origin/brennanb2025/worktree-inactive-filter-finder' into brennanb2025/review-pr-13405-snapshots

This commit is contained in:
Brennan Benson
2026-08-10 16:36:56 -07:00
6 changed files with 112 additions and 14 deletions
@@ -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
}))
}
+13 -6
View File
@@ -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()
})
}