From 91ba3c7717fd09e01a4c1c4d9865788ca0f60bdf Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Tue, 8 Sep 2026 20:01:54 +0800 Subject: [PATCH 1/3] fix: honor ignore paths on initial collection scan --- .../src/app/collection-watcher.js | 3 + .../src/app/collection-watcher.spec.js | 98 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 packages/bruno-electron/src/app/collection-watcher.spec.js diff --git a/packages/bruno-electron/src/app/collection-watcher.js b/packages/bruno-electron/src/app/collection-watcher.js index 35a68ac91a3..63b2aff379e 100644 --- a/packages/bruno-electron/src/app/collection-watcher.js +++ b/packages/bruno-electron/src/app/collection-watcher.js @@ -818,6 +818,9 @@ class CollectionWatcher { this.startCollectionDiscovery(win, collectionUid); + // Seed the dynamic config lookup before chokidar evaluates the initial tree. + setBrunoConfig(collectionUid, brunoConfig); + // Always ignore node_modules and .git, regardless of user config // This prevents infinite loops with symlinked directories (e.g., npm workspaces) const defaultIgnores = ['node_modules', '.git']; diff --git a/packages/bruno-electron/src/app/collection-watcher.spec.js b/packages/bruno-electron/src/app/collection-watcher.spec.js new file mode 100644 index 00000000000..25c3e7fc426 --- /dev/null +++ b/packages/bruno-electron/src/app/collection-watcher.spec.js @@ -0,0 +1,98 @@ +const path = require('path'); + +const watcherHandlers = {}; +const mockWatcher = { + on: jest.fn((event, handler) => { + watcherHandlers[event] = handler; + return mockWatcher; + }), + close: jest.fn() +}; + +jest.mock('chokidar', () => ({ + watch: jest.fn(() => mockWatcher) +})); + +jest.mock('../utils/filesystem', () => ({ + hasRequestExtension: jest.fn(), + isWSLPath: jest.fn(() => false), + normalizeAndResolvePath: jest.fn((pathname) => pathname), + sizeInMB: jest.fn(), + getCollectionFormat: jest.fn(() => 'bru') +})); + +jest.mock('@usebruno/filestore', () => ({ + parseEnvironment: jest.fn(), + parseRequest: jest.fn(), + parseRequestViaWorker: jest.fn(), + parseCollection: jest.fn(), + parseFolder: jest.fn() +}), { virtual: true }); + +jest.mock('@usebruno/common/utils', () => ({ + parseValueByDataType: jest.fn() +}), { virtual: true }); + +jest.mock('../utils/common', () => ({ + uuid: jest.fn(() => 'uuid') +})); + +jest.mock('../cache/requestUids', () => ({ + getRequestUid: jest.fn() +})); + +jest.mock('../utils/encryption', () => ({ + decryptStringSafe: jest.fn() +})); + +jest.mock('../store/env-secrets', () => jest.fn().mockImplementation(() => ({}))); + +jest.mock('../services/snapshot', () => ({ + getCollection: jest.fn() +})); + +jest.mock('../utils/collection', () => ({ + parseFileMeta: jest.fn(), + hydrateRequestWithUuid: jest.fn() +})); + +jest.mock('../utils/parse', () => ({ + parseLargeRequestWithRedaction: jest.fn() +})); + +jest.mock('../utils/transformBrunoConfig', () => ({ + transformBrunoConfigAfterRead: jest.fn() +})); + +jest.mock('./dotenv-watcher', () => ({ + addCollectionWatcher: jest.fn(), + removeCollectionWatcher: jest.fn() +})); + +const { getBrunoConfig } = require('../store/bruno-config'); +const collectionWatcher = require('./collection-watcher'); + +describe('CollectionWatcher', () => { + afterEach(() => { + collectionWatcher.closeAllWatchers(); + Object.keys(watcherHandlers).forEach((event) => delete watcherHandlers[event]); + jest.clearAllMocks(); + }); + + it('honors configured ignore paths during the initial scan', () => { + const watchPath = path.join('tmp', 'collection'); + const collectionUid = 'collection-uid'; + const brunoConfig = { ignore: ['myfolder'] }; + const win = { webContents: { send: jest.fn() } }; + + collectionWatcher.addWatcher(win, watchPath, collectionUid, brunoConfig); + + const ignored = mockWatcher.on.mock.calls.length > 0 + ? require('chokidar').watch.mock.calls[0][1].ignored + : null; + + expect(getBrunoConfig(collectionUid)).toEqual(brunoConfig); + expect(ignored(path.join(watchPath, 'myfolder', 'somefile.yml'))).toBe(true); + expect(ignored(path.join(watchPath, 'visible', 'somefile.yml'))).toBe(false); + }); +}); From b66e4a2e817dbc9fa8b9b26ffa7a8f1857e490d0 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Wed, 9 Sep 2026 06:45:13 +0800 Subject: [PATCH 2/3] test: cover initial ignore callback timing --- .../src/app/collection-watcher.spec.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/bruno-electron/src/app/collection-watcher.spec.js b/packages/bruno-electron/src/app/collection-watcher.spec.js index 25c3e7fc426..d800e5d7e36 100644 --- a/packages/bruno-electron/src/app/collection-watcher.spec.js +++ b/packages/bruno-electron/src/app/collection-watcher.spec.js @@ -1,6 +1,7 @@ const path = require('path'); const watcherHandlers = {}; +const mockInitialIgnoreResults = []; const mockWatcher = { on: jest.fn((event, handler) => { watcherHandlers[event] = handler; @@ -10,7 +11,10 @@ const mockWatcher = { }; jest.mock('chokidar', () => ({ - watch: jest.fn(() => mockWatcher) + watch: jest.fn((watchPath, options) => { + mockInitialIgnoreResults.push(options.ignored(require('path').join(watchPath, 'myfolder', 'somefile.yml'))); + return mockWatcher; + }) })); jest.mock('../utils/filesystem', () => ({ @@ -76,6 +80,7 @@ describe('CollectionWatcher', () => { afterEach(() => { collectionWatcher.closeAllWatchers(); Object.keys(watcherHandlers).forEach((event) => delete watcherHandlers[event]); + mockInitialIgnoreResults.length = 0; jest.clearAllMocks(); }); @@ -87,12 +92,10 @@ describe('CollectionWatcher', () => { collectionWatcher.addWatcher(win, watchPath, collectionUid, brunoConfig); - const ignored = mockWatcher.on.mock.calls.length > 0 - ? require('chokidar').watch.mock.calls[0][1].ignored - : null; - expect(getBrunoConfig(collectionUid)).toEqual(brunoConfig); - expect(ignored(path.join(watchPath, 'myfolder', 'somefile.yml'))).toBe(true); + expect(mockInitialIgnoreResults).toEqual([true]); + + const ignored = require('chokidar').watch.mock.calls[0][1].ignored; expect(ignored(path.join(watchPath, 'visible', 'somefile.yml'))).toBe(false); }); }); From 827ceca9701077f4b9658acf3d70e828c4fa0a6b Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Fri, 11 Sep 2026 20:12:01 +0800 Subject: [PATCH 3/3] test: clarify initial ignore coverage --- .../bruno-electron/src/app/collection-watcher.spec.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/bruno-electron/src/app/collection-watcher.spec.js b/packages/bruno-electron/src/app/collection-watcher.spec.js index d800e5d7e36..e68b58829ec 100644 --- a/packages/bruno-electron/src/app/collection-watcher.spec.js +++ b/packages/bruno-electron/src/app/collection-watcher.spec.js @@ -1,7 +1,6 @@ const path = require('path'); const watcherHandlers = {}; -const mockInitialIgnoreResults = []; const mockWatcher = { on: jest.fn((event, handler) => { watcherHandlers[event] = handler; @@ -12,7 +11,8 @@ const mockWatcher = { jest.mock('chokidar', () => ({ watch: jest.fn((watchPath, options) => { - mockInitialIgnoreResults.push(options.ignored(require('path').join(watchPath, 'myfolder', 'somefile.yml'))); + // Chokidar evaluates the predicate during its initial scan, before watch returns. + expect(options.ignored(require('path').join(watchPath, 'myfolder', 'somefile.yml'))).toBe(true); return mockWatcher; }) })); @@ -80,7 +80,6 @@ describe('CollectionWatcher', () => { afterEach(() => { collectionWatcher.closeAllWatchers(); Object.keys(watcherHandlers).forEach((event) => delete watcherHandlers[event]); - mockInitialIgnoreResults.length = 0; jest.clearAllMocks(); }); @@ -93,9 +92,11 @@ describe('CollectionWatcher', () => { collectionWatcher.addWatcher(win, watchPath, collectionUid, brunoConfig); expect(getBrunoConfig(collectionUid)).toEqual(brunoConfig); - expect(mockInitialIgnoreResults).toEqual([true]); const ignored = require('chokidar').watch.mock.calls[0][1].ignored; + expect(ignored(path.join(watchPath, 'myfolder', 'somefile.yml'))).toBe(true); expect(ignored(path.join(watchPath, 'visible', 'somefile.yml'))).toBe(false); + expect(ignored(path.join(watchPath, '.git', 'config'))).toBe(true); + expect(ignored(path.join(watchPath, 'node_modules', 'package', 'index.js'))).toBe(true); }); });