fix(migration): remaped snapshot tab paths and preserve uids on bru-t… (#8450)

* fix(migration): remaped snapshot tab paths and preserve uids on bru-to-yml migration

* ci: re-trigger checks

---------

Co-authored-by: Utkarsh <utkarsh@usebruno.com>
This commit is contained in:
ravindra-bruno
2026-07-06 18:47:26 +05:30
committed by GitHub
parent 153f0cd91f
commit e9158123bd
4 changed files with 277 additions and 0 deletions

View File

@@ -2684,6 +2684,8 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
// Track all written yml files so we can roll back on failure
const writtenYmlFiles = [];
const tabPathMap = {};
try {
const brunoJsonPath = path.join(collectionPathname, 'bruno.json');
const brunoJsonContent = fs.readFileSync(brunoJsonPath, 'utf8');
@@ -2743,6 +2745,8 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
const ymlContent = stringifyRequest(requestData, { format: 'yml' });
const ymlFilePath = bruFilePath.replace(/\.bru$/, '.yml');
await writeFile(ymlFilePath, ymlContent);
moveRequestUid(bruFilePath, ymlFilePath);
tabPathMap[bruFilePath] = ymlFilePath;
writtenYmlFiles.push(ymlFilePath);
bruFilesToDelete.push(bruFilePath);
}
@@ -2755,6 +2759,7 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
const ymlContent = stringifyEnvironment(envData, { format: 'yml' });
const ymlFilePath = envBruFilePath.replace(/\.bru$/, '.yml');
await writeFile(ymlFilePath, ymlContent);
moveRequestUid(envBruFilePath, ymlFilePath);
writtenYmlFiles.push(ymlFilePath);
bruFilesToDelete.push(envBruFilePath);
}
@@ -2765,6 +2770,11 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
}
fs.unlinkSync(brunoJsonPath);
try {
snapshotManager.remapCollectionTabPaths(collectionPathname, tabPathMap);
} catch (_) {
}
const { size, filesCount } = await getCollectionStats(collectionPathname);
ymlBrunoConfig.size = size;
ymlBrunoConfig.filesCount = filesCount;

View File

@@ -206,6 +206,84 @@ class SnapshotManager {
}));
}
remapCollectionTabPaths(collectionPathname, pathMap) {
const normalizedCollectionPath = normalizeLookupKey(collectionPathname);
if (!normalizedCollectionPath || !isObject(pathMap)) {
return;
}
const normalizedPathMap = {};
Object.entries(pathMap).forEach(([oldPath, newPath]) => {
const normalizedOld = normalizeLookupKey(oldPath);
if (normalizedOld && typeof newPath === 'string' && newPath) {
normalizedPathMap[normalizedOld] = newPath;
}
});
if (Object.keys(normalizedPathMap).length === 0) {
return;
}
const remapPathname = (pathname) => {
if (typeof pathname !== 'string') {
return pathname;
}
const normalized = normalizeLookupKey(pathname);
return normalizedPathMap[normalized] || pathname;
};
const remapActiveTab = (activeTab) => {
if (!isObject(activeTab) || typeof activeTab.value !== 'string') {
return activeTab;
}
if (activeTab.accessor === 'pathname') {
return { accessor: activeTab.accessor, value: remapPathname(activeTab.value) };
}
if (activeTab.accessor === 'pathname::exampleName' || activeTab.accessor === 'pathname::exampleIndex') {
const separatorIndex = activeTab.value.indexOf('::');
if (separatorIndex === -1) {
return activeTab;
}
const pathnamePart = activeTab.value.slice(0, separatorIndex);
const suffix = activeTab.value.slice(separatorIndex);
return { accessor: activeTab.accessor, value: `${remapPathname(pathnamePart)}${suffix}` };
}
return activeTab;
};
const snapshot = this._normalizeSnapshot(this.store.store);
let changed = false;
snapshot.collections.forEach((collection) => {
if (normalizeLookupKey(collection.pathname) !== normalizedCollectionPath) {
return;
}
if (Array.isArray(collection.tabs)) {
collection.tabs.forEach((tab) => {
if (tab && typeof tab.pathname === 'string') {
const remapped = remapPathname(tab.pathname);
if (remapped !== tab.pathname) {
tab.pathname = remapped;
changed = true;
}
}
});
}
const remappedActiveTab = remapActiveTab(collection.activeTab);
if (remappedActiveTab !== collection.activeTab) {
collection.activeTab = remappedActiveTab;
changed = true;
}
});
if (changed) {
this.store.store = snapshot;
this._lookupCache = null;
}
}
setCollection(pathname, data) {
const normalizedPath = normalizeLookupKey(pathname);
if (!normalizedPath) {

View File

@@ -0,0 +1,115 @@
let mockStoreData = {};
jest.mock('electron-store', () => {
return jest.fn().mockImplementation((opts = {}) => {
mockStoreData = { ...(opts.defaults || {}) };
return {
get store() {
return mockStoreData;
},
set store(value) {
mockStoreData = value;
},
get: (key, fallback) => (key in mockStoreData ? mockStoreData[key] : fallback),
set: (key, value) => {
mockStoreData[key] = value;
},
delete: (key) => {
delete mockStoreData[key];
}
};
});
});
const snapshotManager = require('../../src/services/snapshot');
const collectionPath = '/collections/my-collection';
const seedSnapshot = () => {
snapshotManager.saveSnapshot({
version: '0.0.1',
activeWorkspacePath: null,
extras: { devTools: { open: false, activeTab: '', tabs: {} } },
workspaces: [],
collections: [
{
pathname: collectionPath,
workspacePathname: '',
environment: { collection: '', global: '' },
isOpen: true,
isMounted: true,
activeTab: { accessor: 'pathname', value: `${collectionPath}/ping.bru` },
tabs: [
{ type: 'http-request', accessor: 'pathname', pathname: `${collectionPath}/ping.bru`, permanent: true },
{ type: 'http-request', accessor: 'pathname', pathname: `${collectionPath}/api/get-users.bru`, permanent: true },
{
type: 'response-example',
accessor: 'pathname::exampleName',
pathname: `${collectionPath}/ping.bru`,
exampleName: 'success',
permanent: true
},
{ type: 'collection-settings', accessor: 'type', permanent: true }
]
}
]
});
};
describe('SnapshotManager.remapCollectionTabPaths', () => {
beforeEach(() => {
mockStoreData = {};
seedSnapshot();
});
it('remaps .bru request tab pathnames to their .yml equivalents', () => {
snapshotManager.remapCollectionTabPaths(collectionPath, {
[`${collectionPath}/ping.bru`]: `${collectionPath}/ping.yml`,
[`${collectionPath}/api/get-users.bru`]: `${collectionPath}/api/get-users.yml`
});
const { tabs } = snapshotManager.getTabs(collectionPath);
const pathnames = tabs.filter((t) => t.pathname).map((t) => t.pathname);
expect(pathnames).toContain(`${collectionPath}/ping.yml`);
expect(pathnames).toContain(`${collectionPath}/api/get-users.yml`);
expect(pathnames.some((p) => p.endsWith('.bru'))).toBe(false);
});
it('remaps the active tab value', () => {
snapshotManager.remapCollectionTabPaths(collectionPath, {
[`${collectionPath}/ping.bru`]: `${collectionPath}/ping.yml`
});
const { activeTab } = snapshotManager.getTabs(collectionPath);
expect(activeTab).toEqual({ accessor: 'pathname', value: `${collectionPath}/ping.yml` });
});
it('remaps the pathname prefix of example tabs while preserving the example suffix', () => {
snapshotManager.remapCollectionTabPaths(collectionPath, {
[`${collectionPath}/ping.bru`]: `${collectionPath}/ping.yml`
});
const { tabs } = snapshotManager.getTabs(collectionPath);
const exampleTab = tabs.find((t) => t.type === 'response-example');
expect(exampleTab.pathname).toBe(`${collectionPath}/ping.yml`);
expect(exampleTab.exampleName).toBe('success');
});
it('leaves non-request tabs and unmapped pathnames untouched', () => {
snapshotManager.remapCollectionTabPaths(collectionPath, {
[`${collectionPath}/ping.bru`]: `${collectionPath}/ping.yml`
});
const { tabs } = snapshotManager.getTabs(collectionPath);
expect(tabs.find((t) => t.type === 'collection-settings')).toBeTruthy();
expect(tabs.some((t) => t.pathname === `${collectionPath}/api/get-users.bru`)).toBe(true);
});
it('is a no-op when the path map is empty', () => {
snapshotManager.remapCollectionTabPaths(collectionPath, {});
const { tabs } = snapshotManager.getTabs(collectionPath);
expect(tabs.some((t) => t.pathname === `${collectionPath}/ping.bru`)).toBe(true);
});
});