From 4a484e856bab8f263ec1392c838d4919770bb372 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Mon, 22 Jun 2026 12:50:49 -0700 Subject: [PATCH] Harden worktree identity migration coverage Co-authored-by: Orca --- .../src/store/slices/worktrees.test.ts | 24 ++++++++++++++++++- src/renderer/src/store/slices/worktrees.ts | 15 +++++------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/renderer/src/store/slices/worktrees.test.ts b/src/renderer/src/store/slices/worktrees.test.ts index 13c545bf028..129c26db9f0 100644 --- a/src/renderer/src/store/slices/worktrees.test.ts +++ b/src/renderer/src/store/slices/worktrees.test.ts @@ -133,6 +133,7 @@ function createTestStore() { expandedDirs: {}, gitStatusByWorktree: {}, gitStatusHeadByWorktree: {}, + gitStatusHugeByWorktree: {}, gitIgnoredPathsByWorktree: {}, gitConflictOperationByWorktree: {}, trackedConflictPathsByWorktree: {}, @@ -144,6 +145,8 @@ function createTestStore() { activeBrowserTabIdByWorktree: {}, browserTabsByWorktree: {}, recentlyClosedBrowserTabsByWorktree: {}, + recentlyClosedEditorTabsByWorktree: {}, + remoteStatusesByWorktree: {}, activeTabTypeByWorktree: {}, rightSidebarTab: 'explorer' as const, rightSidebarTabByWorktree: {}, @@ -5003,6 +5006,11 @@ describe('migrateWorktreeIdentity', () => { const OLD = 'repo1::/ws/cunner' const NEW = 'repo1::/ws/worktree-creation-spinner' const CANONICAL = makeWorktreeKey({ hostId: 'local', repoId: 'repo1', path: '/ws/cunner' }) + const WORKTREE_ID_VALUE_MIGRATED_KEYS = new Set([ + 'browserPagesByWorkspace', + 'recentlyClosedBrowserPagesByWorkspace', + 'sleepingAgentSessionsByPaneKey' + ]) function makeMigrationMapValue( key: (typeof WORKTREE_ID_KEYED_MAP_KEYS)[number], @@ -5026,11 +5034,24 @@ describe('migrateWorktreeIdentity', () => { return [{ id: 'unified1', worktreeId }] case 'groupsByWorktree': return [{ id: 'group1', worktreeId }] + case 'recentlyClosedEditorTabsByWorktree': + return [{ id: 'file1', worktreeId }] default: return { marker: key } } } + it('keeps the worktree-id keyed map list in sync with store fields', () => { + const declared = new Set(WORKTREE_ID_KEYED_MAP_KEYS) + const state = createTestStore().getState() + const uncovered = Object.keys(state) + .filter((key) => key.includes('ByWorktree')) + .filter((key) => !declared.has(key as keyof AppState)) + .filter((key) => !WORKTREE_ID_VALUE_MIGRATED_KEYS.has(key)) + + expect(uncovered).toEqual([]) + }) + it('re-keys worktree-scoped maps, pointers, the Set, and openFiles old->new', () => { const store = createTestStore() store.setState({ @@ -5052,6 +5073,7 @@ describe('migrateWorktreeIdentity', () => { groupsByWorktree: { [OLD]: [{ id: 'group1', worktreeId: OLD }] }, gitStatusByWorktree: { [OLD]: [{ path: 'a.ts' }] }, gitStatusHeadByWorktree: { [OLD]: 'head-old' }, + gitStatusHugeByWorktree: { [OLD]: { limit: 10_000 } }, gitBranchCompareRequestStatusHeadByWorktree: { [OLD]: 'head-old' }, lastVisitedAtByWorktreeId: { [OLD]: 123 }, defaultTerminalTabsAppliedByWorktreeId: { [OLD]: true }, @@ -5095,12 +5117,12 @@ describe('migrateWorktreeIdentity', () => { expect(s.groupsByWorktree[NEW]?.[0]?.worktreeId).toBe(NEW) expect(s.gitStatusByWorktree[NEW]).toEqual([{ path: 'a.ts' }]) expect(s.gitStatusHeadByWorktree[NEW]).toBe('head-old') + expect(s.gitStatusHugeByWorktree[NEW]).toEqual({ limit: 10_000 }) expect(s.gitBranchCompareRequestStatusHeadByWorktree[NEW]).toBe('head-old') expect(s.rightSidebarExplorerViewByWorktree[OLD]).toBeUndefined() expect(s.rightSidebarExplorerViewByWorktree[NEW]).toBe('search') expect(s.lastVisitedAtByWorktreeId[NEW]).toBe(123) expect(s.defaultTerminalTabsAppliedByWorktreeId[NEW]).toBe(true) - // The two maps absent from the purge list are still re-keyed. expect(s.recentlyClosedEditorTabsByWorktree[NEW]).toEqual([{ id: 'f1', worktreeId: NEW }]) expect(s.remoteStatusesByWorktree[NEW]).toEqual({ ahead: 1 }) expect(s.everActivatedWorktreeIds.has(NEW)).toBe(true) diff --git a/src/renderer/src/store/slices/worktrees.ts b/src/renderer/src/store/slices/worktrees.ts index 5ac00f564a2..0f6bdfd1bd4 100644 --- a/src/renderer/src/store/slices/worktrees.ts +++ b/src/renderer/src/store/slices/worktrees.ts @@ -1272,6 +1272,7 @@ export const WORKTREE_ID_KEYED_MAP_KEYS = [ 'activeGroupIdByWorktree', 'gitStatusByWorktree', 'gitStatusHeadByWorktree', + 'gitStatusHugeByWorktree', 'gitIgnoredPathsByWorktree', 'gitConflictOperationByWorktree', 'trackedConflictPathsByWorktree', @@ -1279,6 +1280,8 @@ export const WORKTREE_ID_KEYED_MAP_KEYS = [ 'gitBranchCompareSummaryByWorktree', 'gitBranchCompareRequestKeyByWorktree', 'gitBranchCompareRequestStatusHeadByWorktree', + 'recentlyClosedEditorTabsByWorktree', + 'remoteStatusesByWorktree', 'showDotfilesByWorktree', 'expandedDirs', 'lastVisitedAtByWorktreeId', @@ -1371,7 +1374,9 @@ function buildWorktreeRenameState( pages: snapshot.pages.map(withNewWorktreeId) })), unifiedTabsByWorktree: (tabs: { worktreeId: string }[]) => tabs.map(withNewWorktreeId), - groupsByWorktree: (groups: { worktreeId: string }[]) => groups.map(withNewWorktreeId) + groupsByWorktree: (groups: { worktreeId: string }[]) => groups.map(withNewWorktreeId), + recentlyClosedEditorTabsByWorktree: (files: { worktreeId: string }[]) => + files.map(withNewWorktreeId) } for (const key of WORKTREE_ID_KEYED_MAP_KEYS) { renameKey(key, renameValueByKey[key] as ((value: unknown) => unknown) | undefined) @@ -1398,14 +1403,6 @@ function buildWorktreeRenameState( oldWorkspaceKey, newWorkspaceKey ) - // Re-key these on rename so a renamed worktree keeps its editor-undo + push/pull - // state. (Both removal paths — buildWorktreePurgeState and the single - // removeWorktree reducer — now also purge them on removal.) - renameKey('recentlyClosedEditorTabsByWorktree', (files: { worktreeId: string }[]) => - files.map(withNewWorktreeId) - ) - renameKey('remoteStatusesByWorktree') - const openFiles = s.openFiles?.some((f) => f.worktreeId === oldWorktreeId) ? s.openFiles.map((f) => f.worktreeId === oldWorktreeId ? { ...f, worktreeId: newWorktreeId } : f