From da0ca821873a74933f9e1da7678b9416d8259bb5 Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Wed, 24 Jun 2026 09:20:41 +0530 Subject: [PATCH] fix: strip snapshot key for v4-migration tab Co-authored-by: Prateek Sunal <41370460+prateekmedia@users.noreply.github.com> --- .../bruno-app/src/utils/snapshot/index.js | 48 +++++++--- .../src/utils/snapshot/index.spec.js | 96 +++++++++++++++++++ 2 files changed, 133 insertions(+), 11 deletions(-) diff --git a/packages/bruno-app/src/utils/snapshot/index.js b/packages/bruno-app/src/utils/snapshot/index.js index 066b75be873..1900424596f 100644 --- a/packages/bruno-app/src/utils/snapshot/index.js +++ b/packages/bruno-app/src/utils/snapshot/index.js @@ -26,6 +26,10 @@ const NON_REPLACEABLE_SINGLETON_TAB_TYPES = new Set([ 'openapi-spec' ]); +const IGNORED_TAB_TYPES = new Set([ + 'v4-migration' +]); + export const SAVE_TRIGGERS = new Map([ ['app/setSnapshotReady', null], ['tabs/addTab', null], @@ -56,6 +60,24 @@ export const SAVE_TRIGGERS = new Map([ export const isRequestTab = (type) => REQUEST_TAB_TYPES.has(type); +const isIgnoredTab = (tab) => IGNORED_TAB_TYPES.has(tab?.type); + +const isIgnoredActiveTab = (activeTab) => activeTab?.accessor === 'type' && IGNORED_TAB_TYPES.has(activeTab.value); + +// Strip ignored tab types from a snapshot read on any path (lookups or ipc fallback), +// including an active tab that points at one. +const sanitizeSnapshotTabs = (tabsSnapshot) => { + if (!tabsSnapshot || !Array.isArray(tabsSnapshot.tabs)) { + return tabsSnapshot; + } + + return { + ...tabsSnapshot, + activeTab: isIgnoredActiveTab(tabsSnapshot.activeTab) ? null : tabsSnapshot.activeTab, + tabs: tabsSnapshot.tabs.filter((tab) => !isIgnoredTab(tab)) + }; +}; + export const shouldExcludeTab = (tab, transientDirectory) => { return transientDirectory && tab.pathname?.startsWith(transientDirectory); }; @@ -107,8 +129,8 @@ const normalizeCollectionSnapshotEntry = (pathname, entry = {}, tabsEntry = {}) isMounted: typeof entry.isMounted === 'boolean' ? entry.isMounted : false, activeTab: tabsEntry.activeTab ?? entry.activeTab ?? null, tabs: Array.isArray(tabsEntry.tabs) - ? tabsEntry.tabs.filter((tab) => isObject(tab)) - : (Array.isArray(entry.tabs) ? entry.tabs.filter((tab) => isObject(tab)) : []) + ? tabsEntry.tabs.filter((tab) => isObject(tab) && !isIgnoredTab(tab)) + : (Array.isArray(entry.tabs) ? entry.tabs.filter((tab) => isObject(tab) && !isIgnoredTab(tab)) : []) }; }; @@ -613,13 +635,15 @@ export const hydrateCollectionTabs = async ( ) => { const { ipcRenderer } = window; - const tabsSnapshot = getTabsSnapshotFromLookups( - collection.pathname, - snapshotLookups, - workspacePathname, - strictWorkspaceScope - ) - || await ipcRenderer.invoke('renderer:snapshot:get-tabs', collection.pathname, workspacePathname).catch(() => null); + const tabsSnapshot = sanitizeSnapshotTabs( + getTabsSnapshotFromLookups( + collection.pathname, + snapshotLookups, + workspacePathname, + strictWorkspaceScope + ) + || await ipcRenderer.invoke('renderer:snapshot:get-tabs', collection.pathname, workspacePathname).catch(() => null) + ); const hasPersistedTabs = Array.isArray(tabsSnapshot?.tabs) && tabsSnapshot.tabs.length > 0; const hasPersistedActiveTab = Boolean(tabsSnapshot?.activeTab); @@ -651,8 +675,10 @@ export const hydrateTabs = async (collections, dispatch, restoreTabs, snapshotLo export const getActiveTabFromSnapshot = async (collectionPathname, collection, snapshotLookups = null, workspacePathname = null) => { const { ipcRenderer } = window; - const tabsSnapshot = getTabsSnapshotFromLookups(collectionPathname, snapshotLookups, workspacePathname) - || await ipcRenderer.invoke('renderer:snapshot:get-tabs', collectionPathname, workspacePathname).catch(() => null); + const tabsSnapshot = sanitizeSnapshotTabs( + getTabsSnapshotFromLookups(collectionPathname, snapshotLookups, workspacePathname) + || await ipcRenderer.invoke('renderer:snapshot:get-tabs', collectionPathname, workspacePathname).catch(() => null) + ); if (!tabsSnapshot?.activeTab || !tabsSnapshot?.tabs?.length) return null; diff --git a/packages/bruno-app/src/utils/snapshot/index.spec.js b/packages/bruno-app/src/utils/snapshot/index.spec.js index a71ca644a5a..04cb7f75082 100644 --- a/packages/bruno-app/src/utils/snapshot/index.spec.js +++ b/packages/bruno-app/src/utils/snapshot/index.spec.js @@ -218,6 +218,27 @@ describe('hydrateSnapshotLookups', () => { expect(lookups.hasWorkspaceScopedTabs).toBe(true); }); + + it('drops legacy v4 migration tabs from snapshot lookups', () => { + const snapshot = { + collections: [ + { + pathname: '/collections/legacy', + activeTab: { accessor: 'type', value: 'v4-migration' }, + tabs: [ + { type: 'v4-migration', accessor: 'type', permanent: true }, + { type: 'variables', accessor: 'type', permanent: true } + ] + } + ] + }; + + const lookups = hydrateSnapshotLookups(snapshot); + + expect(lookups.tabsByCollectionPath['/collections/legacy'].tabs).toEqual([ + { type: 'variables', accessor: 'type', permanent: true } + ]); + }); }); describe('deserializeTab', () => { @@ -766,4 +787,79 @@ describe('hydrateCollectionTabs', () => { expect(dispatch).toHaveBeenCalledTimes(1); expect(restoreTabs).toHaveBeenCalledTimes(1); }); + + it('does not restore legacy v4 migration tabs from direct tab snapshots', async () => { + global.window.ipcRenderer.invoke.mockResolvedValue({ + tabs: [ + { type: 'v4-migration', accessor: 'type', permanent: true }, + { type: 'variables', accessor: 'type', permanent: true } + ], + activeTab: { + accessor: 'type', + value: 'v4-migration' + } + }); + + const dispatch = jest.fn(); + const restoreTabs = jest.fn((payload) => ({ + type: 'tabs/restoreTabs', + payload + })); + + await hydrateCollectionTabs( + { uid: 'collection-uid', pathname: '/collections/legacy' }, + dispatch, + restoreTabs, + null, + null, + true + ); + + expect(restoreTabs).toHaveBeenCalledWith( + expect.objectContaining({ + tabs: [{ type: 'variables', accessor: 'type', permanent: true }], + activeTab: null + }) + ); + }); +}); + +describe('getActiveTabFromSnapshot', () => { + beforeEach(() => { + global.window = { + ipcRenderer: { + invoke: jest.fn().mockResolvedValue(null) + } + }; + }); + + afterEach(() => { + delete global.window; + }); + + it('ignores a legacy v4 migration active tab snapshot', async () => { + const snapshot = { + collections: [ + { + pathname: '/collections/legacy', + tabs: [ + { type: 'v4-migration', accessor: 'type', permanent: true } + ], + activeTab: { + accessor: 'type', + value: 'v4-migration' + } + } + ] + }; + const lookups = hydrateSnapshotLookups(snapshot); + + const activeTab = await getActiveTabFromSnapshot( + '/collections/legacy', + { uid: 'collection-uid', pathname: '/collections/legacy' }, + lookups + ); + + expect(activeTab).toBeNull(); + }); });