Skip to content

fix(collection-watcher): prevent crash when deleting collections#7470

Merged
bijin-bruno merged 6 commits intousebruno:mainfrom
abhishek-bruno:fix/chokidar-crash-on-collection-delete
Mar 13, 2026
Merged

fix(collection-watcher): prevent crash when deleting collections#7470
bijin-bruno merged 6 commits intousebruno:mainfrom
abhishek-bruno:fix/chokidar-crash-on-collection-delete

Conversation

@abhishek-bruno
Copy link
Member

@abhishek-bruno abhishek-bruno commented Mar 13, 2026

Description

  • Fix uncaught "No collection configuration found" errors when deleting a collection that has OpenAPI sync configured
  • Add fs.existsSync(collectionPath) guards to the change, unlink, and unlinkDir chokidar event handlers

JIRA

Problem

When deleting an OpenAPI-synced collection, saveBrunoConfig() writes to bruno.json which creates buffered chokidar events (due to awaitWriteFinish with 80ms stability threshold). If the collection directory is removed before those buffered events fire, getCollectionFormat() throws "No collection configuration found" — once per .bru file in the collection.

Contribution Checklist:

  • I've used AI significantly to create this pull request
  • The pull request only addresses one issue or adds one feature.
  • The pull request does not introduce any breaking changes
  • I have added screenshots or gifs to help explain the change if applicable.
  • I have read the contribution guidelines.
  • Create an issue and link to the pull request.

Note: Keeping the PR small and focused helps make it easier to review and merge. If you have multiple changes you want to make, please consider submitting them as separate pull requests.

Publishing to New Package Managers

Please see here for more information.

Summary by CodeRabbit

  • Bug Fixes

    • Made collection watcher more defensive: robustly handles collection/folder deletions, guards edge cases, and logs errors to prevent crashes.
  • Tests

    • Added an end-to-end test confirming collection deletion removes UI items and the on-disk directory with no uncaught errors.
    • Added a test utility to automate deleting a collection from the workspace overview.

… deletion

When deleting an OpenAPI-synced collection, saveBrunoConfig() writes to
bruno.json which creates buffered chokidar events (80ms stabilityThreshold).
If the collection directory is removed before those events fire,
getCollectionFormat() throws "No collection configuration found" for each
.bru file in the collection.

Add fs.existsSync(collectionPath) guards in the change, unlink, and
unlinkDir handlers to bail out early when the collection root no longer exists.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds defensive try/catch guards and existence checks to collection watcher unlink/unlinkDir handlers, plus explicit error logging; introduces an end-to-end Playwright test for collection deletion and a test helper to perform the deletion from the Workspace Overview.

Changes

Cohort / File(s) Summary
Watcher Error Handling
packages/bruno-electron/src/app/collection-watcher.js
Wraps unlink and unlinkDir handlers in try/catch, adds existsSync early checks, guards collection-format retrieval, keeps special-case for opencollection.yml/environments, and logs errors instead of throwing.
Collection Deletion E2E Test
tests/collection/delete/delete-collection.spec.ts
New Playwright test that creates a collection, verifies on-disk presence, deletes it via Workspace Overview UI (confirming with delete), asserts UI entries removed and directory deleted, and captures uncaught page errors.
Test Helper - Actions
tests/utils/page/actions.ts
Adds and exports deleteCollectionFromOverview(page: Page, collectionName: string) to encapsulate Overview navigation, collection menu, delete flow, and modal confirmation/waiting.

Sequence Diagram(s)

(Skipped — changes are defensive handlers and a test; no new multi-component control flow requiring diagram.)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • helloanoop
  • lohit-bruno
  • naman-bruno
  • bijin-bruno

Poem

A watcher wrapped in careful code,
A test that walks the deletion road,
Cards and folders gently cleared,
Try/catch keeps the runtime steered,
Disk and UI tidy as we go.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(collection-watcher): prevent crash when deleting collections' directly matches the main change—adding defensive error handling to the collection-watcher to prevent crashes during collection deletion.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/bruno-electron/src/app/collection-watcher.js`:
- Line 424: The TOCTOU gap occurs because getCollectionFormat(collectionPath) is
called after an initial fs.existsSync check in change, unlink, and unlinkDir; if
the directory vanishes in between it throws an unhandled "No collection
configuration found" error. Immediately after each existsSync(collectionPath)
guard in the change, unlink, and unlinkDir code paths, call
getCollectionFormat(collectionPath) inside a try-catch (before any other
dependent operations) and handle the error by logging or returning early (same
behavior as when the existsSync guard fails) so the functions don't proceed when
the collection config is gone.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5109df1a-a2c8-48be-b4f9-95ea3abadb9c

📥 Commits

Reviewing files that changed from the base of the PR and between c0a2d74 and da1823f.

📒 Files selected for processing (1)
  • packages/bruno-electron/src/app/collection-watcher.js

};

const change = async (win, pathname, collectionUid, collectionPath) => {
if (!fs.existsSync(collectionPath)) return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd packages/bruno-electron/src/app && wc -l collection-watcher.js

Repository: usebruno/bruno

Length of output: 83


🏁 Script executed:

cd packages/bruno-electron/src/app && sed -n '420,430p' collection-watcher.js

Repository: usebruno/bruno

Length of output: 518


🏁 Script executed:

cd packages/bruno-electron/src/app && sed -n '550,570p' collection-watcher.js

Repository: usebruno/bruno

Length of output: 701


🏁 Script executed:

cd packages/bruno-electron/src/app && sed -n '580,595p' collection-watcher.js

Repository: usebruno/bruno

Length of output: 498


🏁 Script executed:

cd packages/bruno-electron/src/app && sed -n '424,530p' collection-watcher.js

Repository: usebruno/bruno

Length of output: 2999


🏁 Script executed:

cd packages/bruno-electron/src/app && rg -A 5 'const isFolderRootFile|function isFolderRootFile' collection-watcher.js

Repository: usebruno/bruno

Length of output: 277


🏁 Script executed:

cd packages/bruno-electron/src/app && rg -B 3 -A 3 'getCollectionFormat' collection-watcher.js | head -80

Repository: usebruno/bruno

Length of output: 1946


🏁 Script executed:

cd packages/bruno-electron/src && rg -A 15 'const getCollectionFormat|function getCollectionFormat' utils/filesystem.js

Repository: usebruno/bruno

Length of output: 570


🏁 Script executed:

cd packages/bruno-electron/src && rg -B 2 -A 2 'No collection configuration found' .

Repository: usebruno/bruno

Length of output: 254


🏁 Script executed:

cd packages/bruno-electron/src/app && sed -n '506,520p' collection-watcher.js

Repository: usebruno/bruno

Length of output: 493


🏁 Script executed:

cd packages/bruno-electron/src/app && sed -n '560,575p' collection-watcher.js

Repository: usebruno/bruno

Length of output: 520


🏁 Script executed:

cd packages/bruno-electron/src/app && sed -n '588,605p' collection-watcher.js

Repository: usebruno/bruno

Length of output: 532


Harden against TOCTOU after the new existsSync guards.

The guards at lines 424, 556, and 584 help, but getCollectionFormat(collectionPath) is still called later without protection in all three functions. In unlink (line 563) and unlinkDir (line 591), the call is entirely outside any try-catch. In change (line 527), there's an uncaught call to getCollectionFormat. If the collection folder disappears between the initial existsSync check and the getCollectionFormat call, an unhandled "No collection configuration found" error will be thrown.

Wrap getCollectionFormat(collectionPath) in a try-catch block immediately after each existsSync check, before any dependent operations, to close the TOCTOU window.

Suggested hardening patch
 const change = async (win, pathname, collectionUid, collectionPath) => {
   if (!fs.existsSync(collectionPath)) return;
+  let format;
+  try {
+    format = getCollectionFormat(collectionPath);
+  } catch (err) {
+    return;
+  }
@@
-  if (isFolderRootFile(pathname, collectionPath)) {
+  if (path.basename(pathname) === `folder.${format}`) {
@@
-      let format = getCollectionFormat(collectionPath);
       let content = fs.readFileSync(pathname, 'utf8');
       file.data = await parseFolder(content, { format });
@@
-  const format = getCollectionFormat(collectionPath);
   if (hasRequestExtension(pathname, format)) {
@@
 const unlink = (win, pathname, collectionUid, collectionPath) => {
   if (!fs.existsSync(collectionPath)) return;
+  let format;
+  try {
+    format = getCollectionFormat(collectionPath);
+  } catch (err) {
+    return;
+  }
@@
-  const format = getCollectionFormat(collectionPath);
   if (hasRequestExtension(pathname, format)) {
@@
 const unlinkDir = async (win, pathname, collectionUid, collectionPath) => {
   if (!fs.existsSync(collectionPath)) return;
+  let format;
+  try {
+    format = getCollectionFormat(collectionPath);
+  } catch (err) {
+    return;
+  }
@@
-  const format = getCollectionFormat(collectionPath);
   const folderFilePath = path.join(pathname, `folder.${format}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-electron/src/app/collection-watcher.js` at line 424, The
TOCTOU gap occurs because getCollectionFormat(collectionPath) is called after an
initial fs.existsSync check in change, unlink, and unlinkDir; if the directory
vanishes in between it throws an unhandled "No collection configuration found"
error. Immediately after each existsSync(collectionPath) guard in the change,
unlink, and unlinkDir code paths, call getCollectionFormat(collectionPath)
inside a try-catch (before any other dependent operations) and handle the error
by logging or returning early (same behavior as when the existsSync guard fails)
so the functions don't proceed when the collection config is gone.

Added logic to remove the collection from the watcher when deleting files, preventing chokidar from firing events on a directory that is being removed. This change enhances stability during collection deletions by ensuring the watcher is properly managed.
@abhishek-bruno abhishek-bruno changed the title fix(collection-watcher): prevent crash when deleting OpenAPI-synced collections fix(collection-watcher): prevent crash when deleting collections Mar 13, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js`:
- Around line 161-167: The current flow calls
ipcRenderer.invoke('renderer:remove-collection', ...) which mutates
workspace.yml before you stop the watcher, causing removedCollection to become
null and preventing deleteFiles from removing the folder; change the sequence so
you first tear down the watcher without mutating the workspace (invoke the
watcher-stop IPC — use 'renderer:remove-collection-from-workspace' or a
dedicated watcher-stop IPC) using collection.pathname/collection.uid/workspaceId
to stop chokidar, then call ipcRenderer.invoke('renderer:remove-collection',
collection.pathname, collection.uid, workspaceId) to mutate workspace.yml and
finally run the deleteFiles branch (or rely on the returned removedCollection)
so the directory removal actually executes.

In `@packages/bruno-electron/src/app/collection-watcher.js`:
- Around line 555-582: The unlink handler currently logs all exceptions as
errors even when the collection root is missing during normal delete races;
update the logic in the try/catch around getCollectionFormat/hasRequestExtension
and the overall unlink block (the function handling unlink events with variables
pathname, collectionPath, collectionUid, and calls to isEnvironmentsFolder and
unlinkEnvironmentFile) to treat a missing collection root as an expected flow:
either pre-check that the collection root exists (fs.existsSync(collectionPath)
or equivalent) before proceeding, or in the catch inspect the error (e.g.,
err.code === 'ENOENT' or similar) and return silently instead of calling
console.error; preserve error logging for unexpected errors only.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: aba4e126-650a-4107-a24f-b833d6577a9c

📥 Commits

Reviewing files that changed from the base of the PR and between da1823f and c26dfce.

📒 Files selected for processing (2)
  • packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js
  • packages/bruno-electron/src/app/collection-watcher.js

Comment on lines +161 to +167
// Remove collection from the watcher
// When deleting files, stop the collection watcher first to prevent
// chokidar from firing events on a directory that's being removed
if (deleteFiles && collection) {
const workspaceId = workspace.pathname || workspace.uid || 'default';
await ipcRenderer.invoke('renderer:remove-collection', collection.pathname, collection.uid, workspaceId);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Pre-removal IPC can prevent deleteFiles from deleting the collection directory.

renderer:remove-collection already removes the collection from workspace.yml. When Line 169 then calls renderer:remove-collection-from-workspace, removedCollection can be null, so the deleteFiles branch may never remove the folder.

Suggested fix (keep watcher teardown, avoid first workspace mutation)
-      if (deleteFiles && collection) {
-        const workspaceId = workspace.pathname || workspace.uid || 'default';
-        await ipcRenderer.invoke('renderer:remove-collection', collection.pathname, collection.uid, workspaceId);
-      }
+      if (deleteFiles && collection) {
+        // teardown watcher/runtime state only; let remove-collection-from-workspace
+        // own workspace.yml mutation + file deletion
+        await ipcRenderer.invoke('renderer:remove-collection', collection.pathname, collection.uid);
+      }

Also applies to: 169-173

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js`
around lines 161 - 167, The current flow calls
ipcRenderer.invoke('renderer:remove-collection', ...) which mutates
workspace.yml before you stop the watcher, causing removedCollection to become
null and preventing deleteFiles from removing the folder; change the sequence so
you first tear down the watcher without mutating the workspace (invoke the
watcher-stop IPC — use 'renderer:remove-collection-from-workspace' or a
dedicated watcher-stop IPC) using collection.pathname/collection.uid/workspaceId
to stop chokidar, then call ipcRenderer.invoke('renderer:remove-collection',
collection.pathname, collection.uid, workspaceId) to mutate workspace.yml and
finally run the deleteFiles branch (or rely on the returned removedCollection)
so the directory removal actually executes.

…g deletion

Eliminated the logic for stopping the collection watcher before deletion, streamlining the action for removing collections from workspaces. This change simplifies the code and maintains functionality without unnecessary complexity.
…onality

Introduced a new test suite to verify the deletion of collections from the workspace overview. The test ensures that collections are properly removed from both the UI and the file system, confirming the absence of any uncaught errors during the deletion process. This addition enhances the test coverage for collection management features.
@pull-request-size pull-request-size bot added size/L and removed size/M labels Mar 13, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
tests/collection/delete/delete-collection.spec.ts (2)

23-36: Extract locators to variables for improved maintainability.

Per guidelines, locators should be stored in variables. This improves readability and makes updates easier if selectors change.

♻️ Suggested locator extraction
+    // Define locators
+    const homeButton = page.locator('.home-button');
+    const overviewTab = page.locator('.request-tab').filter({ hasText: 'Overview' });
+    const collectionCard = page.locator('.collection-card').filter({ hasText: collectionName });
+    const deleteDropdownItem = page.locator('.dropdown-item').filter({ hasText: 'Delete' });
+    const deleteModal = page.locator('.bruno-modal').filter({ hasText: 'Delete Collection' });
+
     // Navigate to Workspace
-    await page.locator('.home-button').click();
-
-    // Navigate to workspace overview
-    const overviewTab = page.locator('.request-tab').filter({ hasText: 'Overview' });
+    await homeButton.click();
     await overviewTab.click();
 
-    // Find the collection card and open its menu
-    const collectionCard = page.locator('.collection-card').filter({ hasText: collectionName });
     await collectionCard.waitFor({ state: 'visible', timeout: 5000 });
     await collectionCard.locator('.collection-menu').click();
 
-    // Click Delete from the dropdown
-    await page.locator('.dropdown-item').filter({ hasText: 'Delete' }).click();
-
-    // Wait for delete confirmation modal
-    const deleteModal = page.locator('.bruno-modal').filter({ hasText: 'Delete Collection' });
+    await deleteDropdownItem.click();
     await deleteModal.waitFor({ state: 'visible', timeout: 5000 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/collection/delete/delete-collection.spec.ts` around lines 23 - 36,
Extract the inline locators into clearly named variables to improve
maintainability: create variables like homeButtonLocator for
page.locator('.home-button'), overviewTabLocator for
page.locator('.request-tab').filter({ hasText: 'Overview' }),
collectionCardLocator for page.locator('.collection-card').filter({ hasText:
collectionName }), collectionMenuLocator from
collectionCardLocator.locator('.collection-menu'), and deleteDropdownItemLocator
for page.locator('.dropdown-item').filter({ hasText: 'Delete' }); then replace
the inline uses in the sequence (clicks and waitFor) with these variables so
selectors are centralized and easier to update.

7-66: Wrap logical sections in test.step for improved report readability.

The test covers the deletion flow well, but per coding guidelines, using test.step makes generated reports easier to read. Consider grouping the logical phases: setup, navigation, deletion, and verification.

♻️ Suggested refactor with test.step
   test('Delete collection from workspace overview removes files from disk', async ({ page, createTmpDir }) => {
     const collectionName = 'delete-test-collection';
     const tmpDir = await createTmpDir(collectionName);
     const collectionPath = path.join(tmpDir, collectionName);
 
-    // Create a collection with a request
-    await createCollection(page, collectionName, tmpDir);
-    await createRequest(page, 'ping', collectionName, { url: 'http://localhost:8081/ping' });
-
-    // Verify collection directory exists on disk
-    expect(fs.existsSync(collectionPath)).toBe(true);
+    await test.step('Create collection with a request', async () => {
+      await createCollection(page, collectionName, tmpDir);
+      await createRequest(page, 'ping', collectionName, { url: 'http://localhost:8081/ping' });
+      expect(fs.existsSync(collectionPath)).toBe(true);
+    });
 
     // Capture any uncaught errors during deletion
     const pageErrors: Error[] = [];
     page.on('pageerror', (error) => pageErrors.push(error));
 
-    // Navigate to Workspace
-    await page.locator('.home-button').click();
-
-    // Navigate to workspace overview
-    const overviewTab = page.locator('.request-tab').filter({ hasText: 'Overview' });
-    await overviewTab.click();
-
-    // Find the collection card and open its menu
-    const collectionCard = page.locator('.collection-card').filter({ hasText: collectionName });
-    await collectionCard.waitFor({ state: 'visible', timeout: 5000 });
-    await collectionCard.locator('.collection-menu').click();
-
-    // Click Delete from the dropdown
-    await page.locator('.dropdown-item').filter({ hasText: 'Delete' }).click();
-
-    // Wait for delete confirmation modal
-    const deleteModal = page.locator('.bruno-modal').filter({ hasText: 'Delete Collection' });
-    await deleteModal.waitFor({ state: 'visible', timeout: 5000 });
-
-    // Type 'delete' to confirm
-    await deleteModal.locator('#delete-confirm-input').fill('delete');
-
-    // Click the Delete button
-    await deleteModal.getByRole('button', { name: 'Delete', exact: true }).click();
-
-    // Wait for modal to close and success toast
-    await deleteModal.waitFor({ state: 'hidden', timeout: 10000 });
+    await test.step('Delete collection via workspace overview', async () => {
+      await page.locator('.home-button').click();
+      const overviewTab = page.locator('.request-tab').filter({ hasText: 'Overview' });
+      await overviewTab.click();
+
+      const collectionCard = page.locator('.collection-card').filter({ hasText: collectionName });
+      await collectionCard.waitFor({ state: 'visible', timeout: 5000 });
+      await collectionCard.locator('.collection-menu').click();
+
+      await page.locator('.dropdown-item').filter({ hasText: 'Delete' }).click();
+
+      const deleteModal = page.locator('.bruno-modal').filter({ hasText: 'Delete Collection' });
+      await deleteModal.waitFor({ state: 'visible', timeout: 5000 });
+      await deleteModal.locator('#delete-confirm-input').fill('delete');
+      await deleteModal.getByRole('button', { name: 'Delete', exact: true }).click();
+      await deleteModal.waitFor({ state: 'hidden', timeout: 10000 });
+    });
 
-    // Verify collection is removed from workspace overview
-    await expect(
-      page.locator('.collection-card').filter({ hasText: collectionName })
-    ).not.toBeVisible();
-
-    // Verify collection is removed from sidebar
-    await expect(
-      page.locator('#sidebar-collection-name').filter({ hasText: collectionName })
-    ).not.toBeVisible();
-
-    // Verify collection directory is deleted from disk
-    expect(fs.existsSync(collectionPath)).toBe(false);
-
-    // Verify no uncaught JS errors occurred during deletion
-    expect(pageErrors).toHaveLength(0);
+    await test.step('Verify collection removed from UI and disk', async () => {
+      await expect(
+        page.locator('.collection-card').filter({ hasText: collectionName })
+      ).not.toBeVisible();
+      await expect(
+        page.locator('#sidebar-collection-name').filter({ hasText: collectionName })
+      ).not.toBeVisible();
+      expect(fs.existsSync(collectionPath)).toBe(false);
+      expect(pageErrors).toHaveLength(0);
+    });
   });

As per coding guidelines: "Promote the use of test.step as much as possible so the generated reports are easier to read."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/collection/delete/delete-collection.spec.ts` around lines 7 - 66, The
test "Delete collection from workspace overview removes files from disk" should
be split into explicit test.step blocks to improve report readability: wrap the
setup phase (tmpDir creation, createCollection, createRequest, and initial
fs.existsSync check) in an await test.step('setup', async () => { ... }); wrap
navigation actions (clicking .home-button, opening Overview tab, locating
collection card) in await test.step('navigation', ...); wrap the deletion
interaction (opening menu, clicking Delete, filling '#delete-confirm-input',
confirming) in await test.step('deletion', ...); and wrap verifications
(collection not visible in overview/sidebar, fs.existsSync check, and pageErrors
assertion) in await test.step('verification', ...); update references to the
existing symbols collectionName, createTmpDir, createCollection, createRequest,
collectionCard, deleteModal, and pageErrors when moving their related code into
the respective steps.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/collection/delete/delete-collection.spec.ts`:
- Around line 23-36: Extract the inline locators into clearly named variables to
improve maintainability: create variables like homeButtonLocator for
page.locator('.home-button'), overviewTabLocator for
page.locator('.request-tab').filter({ hasText: 'Overview' }),
collectionCardLocator for page.locator('.collection-card').filter({ hasText:
collectionName }), collectionMenuLocator from
collectionCardLocator.locator('.collection-menu'), and deleteDropdownItemLocator
for page.locator('.dropdown-item').filter({ hasText: 'Delete' }); then replace
the inline uses in the sequence (clicks and waitFor) with these variables so
selectors are centralized and easier to update.
- Around line 7-66: The test "Delete collection from workspace overview removes
files from disk" should be split into explicit test.step blocks to improve
report readability: wrap the setup phase (tmpDir creation, createCollection,
createRequest, and initial fs.existsSync check) in an await test.step('setup',
async () => { ... }); wrap navigation actions (clicking .home-button, opening
Overview tab, locating collection card) in await test.step('navigation', ...);
wrap the deletion interaction (opening menu, clicking Delete, filling
'#delete-confirm-input', confirming) in await test.step('deletion', ...); and
wrap verifications (collection not visible in overview/sidebar, fs.existsSync
check, and pageErrors assertion) in await test.step('verification', ...); update
references to the existing symbols collectionName, createTmpDir,
createCollection, createRequest, collectionCard, deleteModal, and pageErrors
when moving their related code into the respective steps.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b4504b1b-58cd-4c54-ac02-42db53a32d9b

📥 Commits

Reviewing files that changed from the base of the PR and between c26dfce and 4d48d9b.

📒 Files selected for processing (1)
  • tests/collection/delete/delete-collection.spec.ts

…tion

Added a new utility function to delete a collection directly from the workspace overview page. This function encapsulates the steps required to navigate the UI, confirm deletion, and ensure the collection is removed from both the interface and the file system. Updated the corresponding test to utilize this new function, enhancing code reusability and test clarity.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/utils/page/actions.ts (1)

326-360: Add verification that collection is removed after deletion.

The function completes without confirming the collection card is no longer visible. Compare with removeCollection (lines 457-459), which verifies the element is gone. Adding a similar assertion here would catch silent failures and align with Playwright best practices for multiple assertions.

Suggested verification
     // Wait for modal to close
     await deleteModal.waitFor({ state: 'hidden', timeout: 10000 });
+
+    // Verify collection card is removed from overview
+    await expect(collectionCard).not.toBeVisible();
   });
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/utils/page/actions.ts` around lines 326 - 360, The function
deleteCollectionFromOverview currently does not verify the collection was
actually removed; after the existing wait for the deleteModal to be hidden, add
a post-delete verification that the collection card for collectionName is
gone—either by awaiting collectionCard.waitFor({ state: 'hidden', timeout: 5000
}) or using Playwright's assertion (expect(collectionCard).not.toBeVisible()) to
match the pattern used in removeCollection; place this check at the end of
deleteCollectionFromOverview (after deleteModal.waitFor) to ensure deletion
succeeded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/utils/page/actions.ts`:
- Around line 326-360: The function deleteCollectionFromOverview currently does
not verify the collection was actually removed; after the existing wait for the
deleteModal to be hidden, add a post-delete verification that the collection
card for collectionName is gone—either by awaiting collectionCard.waitFor({
state: 'hidden', timeout: 5000 }) or using Playwright's assertion
(expect(collectionCard).not.toBeVisible()) to match the pattern used in
removeCollection; place this check at the end of deleteCollectionFromOverview
(after deleteModal.waitFor) to ensure deletion succeeded.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 35494f23-ccdd-4320-a602-414dffd2f531

📥 Commits

Reviewing files that changed from the base of the PR and between 4d48d9b and 7ab3295.

📒 Files selected for processing (2)
  • tests/collection/delete/delete-collection.spec.ts
  • tests/utils/page/actions.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/collection/delete/delete-collection.spec.ts

… error handling

Enhanced the unlink and unlinkDir functions to check for the existence of the collection path before proceeding. Added error handling for the getCollectionFormat function to prevent crashes when the collection format cannot be retrieved. These changes improve stability and robustness during collection deletion operations.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
packages/bruno-electron/src/app/collection-watcher.js (2)

568-570: ⚠️ Potential issue | 🟡 Minor

Treat missing collection config during delete race as expected flow (silent return).

At Line 569 and Line 609, getCollectionFormat failures are logged as errors. In deletion races this is expected and can spam logs; prefer silent return here and reserve error logs for unexpected failures only.

Suggested noise-reduction patch
     try {
       format = getCollectionFormat(collectionPath);
-    } catch (error) {
-      console.error(`Error getting collection format for: ${collectionPath}`, error);
+    } catch (_error) {
       return;
     }
@@
     try {
       format = getCollectionFormat(collectionPath);
-    } catch (error) {
-      console.error(`Error getting collection format for: ${collectionPath}`, error);
+    } catch (_error) {
       return;
     }

Also applies to: 608-610

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-electron/src/app/collection-watcher.js` around lines 568 -
570, The catch blocks around the getCollectionFormat call (referenced as
getCollectionFormat and using collectionPath) should treat
missing-config/delete-race errors as silent returns: update both catch handlers
to detect expected "missing collection config" conditions (e.g. error.code ===
'ENOENT' or an error.message that indicates no config) and simply return without
logging; for all other unexpected errors, keep the existing console.error call
and rethrow or return as before. Ensure you change both occurrences that handle
getCollectionFormat failures so delete races do not spam the logs.

555-558: ⚠️ Potential issue | 🟠 Major

change() still has an unguarded format lookup path.

This hardens unlink/unlinkDir, but the same failure mode remains in change: isFolderRootFile(...) at Line 501 and getCollectionFormat(...) at Line 525 can still throw when config disappears mid-delete, leaving a crash path open.

Suggested patch to close the remaining `change()` gap
 const change = async (win, pathname, collectionUid, collectionPath) => {
+  if (!fs.existsSync(collectionPath)) {
+    return;
+  }
+
+  let format;
+  try {
+    format = getCollectionFormat(collectionPath);
+  } catch (_error) {
+    return;
+  }
+
   if (isBrunoConfigFile(pathname, collectionPath)) {
@@
-  if (isFolderRootFile(pathname, collectionPath)) {
+  if (path.basename(pathname) === `folder.${format}`) {
@@
-      let format = getCollectionFormat(collectionPath);
       let content = fs.readFileSync(pathname, 'utf8');
       file.data = await parseFolder(content, { format });
@@
-  const format = getCollectionFormat(collectionPath);
   if (hasRequestExtension(pathname, format)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-electron/src/app/collection-watcher.js` around lines 555 -
558, In change(), guard against mid-delete config disappearance by checking that
collectionPath exists (fs.existsSync(collectionPath)) before calling
isFolderRootFile(...) and getCollectionFormat(...), or wrap those calls in a
try/catch that returns early on error; specifically update the change function
to either return when the path is missing or catch exceptions from
isFolderRootFile and getCollectionFormat and treat them as a benign
disappearance (no crash), ensuring the function exits cleanly instead of
throwing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/bruno-electron/src/app/collection-watcher.js`:
- Around line 568-570: The catch blocks around the getCollectionFormat call
(referenced as getCollectionFormat and using collectionPath) should treat
missing-config/delete-race errors as silent returns: update both catch handlers
to detect expected "missing collection config" conditions (e.g. error.code ===
'ENOENT' or an error.message that indicates no config) and simply return without
logging; for all other unexpected errors, keep the existing console.error call
and rethrow or return as before. Ensure you change both occurrences that handle
getCollectionFormat failures so delete races do not spam the logs.
- Around line 555-558: In change(), guard against mid-delete config
disappearance by checking that collectionPath exists
(fs.existsSync(collectionPath)) before calling isFolderRootFile(...) and
getCollectionFormat(...), or wrap those calls in a try/catch that returns early
on error; specifically update the change function to either return when the path
is missing or catch exceptions from isFolderRootFile and getCollectionFormat and
treat them as a benign disappearance (no crash), ensuring the function exits
cleanly instead of throwing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5eb0909c-1952-42b6-a46d-a6e3c0b3b1b3

📥 Commits

Reviewing files that changed from the base of the PR and between 7ab3295 and edce010.

📒 Files selected for processing (1)
  • packages/bruno-electron/src/app/collection-watcher.js

@bijin-bruno bijin-bruno merged commit 1e25825 into usebruno:main Mar 13, 2026
10 checks passed
lohit-bruno pushed a commit to lohit-bruno/bruno that referenced this pull request Mar 18, 2026
…bruno#7470)

* fix(collection-watcher): guard against events firing after collection deletion

When deleting an OpenAPI-synced collection, saveBrunoConfig() writes to
bruno.json which creates buffered chokidar events (80ms stabilityThreshold).
If the collection directory is removed before those events fire,
getCollectionFormat() throws "No collection configuration found" for each
.bru file in the collection.

Add fs.existsSync(collectionPath) guards in the change, unlink, and
unlinkDir handlers to bail out early when the collection root no longer exists.

* fix(workspaces): ensure collection watcher stops before deletion

Added logic to remove the collection from the watcher when deleting files, preventing chokidar from firing events on a directory that is being removed. This change enhances stability during collection deletions by ensuring the watcher is properly managed.

* refactor(workspaces): remove redundant collection watcher logic during deletion

Eliminated the logic for stopping the collection watcher before deletion, streamlining the action for removing collections from workspaces. This change simplifies the code and maintains functionality without unnecessary complexity.

* test(collection): add integration test for collection deletion functionality

Introduced a new test suite to verify the deletion of collections from the workspace overview. The test ensures that collections are properly removed from both the UI and the file system, confirming the absence of any uncaught errors during the deletion process. This addition enhances the test coverage for collection management features.

* feat(collection): implement deleteCollectionFromOverview utility function

Added a new utility function to delete a collection directly from the workspace overview page. This function encapsulates the steps required to navigate the UI, confirm deletion, and ensure the collection is removed from both the interface and the file system. Updated the corresponding test to utilize this new function, enhancing code reusability and test clarity.

* fix(collection-watcher): add guards for collection path existence and error handling

Enhanced the unlink and unlinkDir functions to check for the existence of the collection path before proceeding. Added error handling for the getCollectionFormat function to prevent crashes when the collection format cannot be retrieved. These changes improve stability and robustness during collection deletion operations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants