Skip to content

fix(import): dedupe sibling folder/file names case-insensitively#7822

Open
EmilienDaulhiac wants to merge 1 commit intousebruno:mainfrom
EmilienDaulhiac:bugfix/postman-import-duplicate-folder-names
Open

fix(import): dedupe sibling folder/file names case-insensitively#7822
EmilienDaulhiac wants to merge 1 commit intousebruno:mainfrom
EmilienDaulhiac:bugfix/postman-import-duplicate-folder-names

Conversation

@EmilienDaulhiac
Copy link
Copy Markdown

@EmilienDaulhiac EmilienDaulhiac commented Apr 21, 2026

BRU-3177
Fixes #7821

Description

Collections imported from Postman/Insomnia/OpenAPI/WSDL that contain siblings with duplicate names or case-only variants (e.g. OAuth2 vs oAuth2) used to either crash with EEXIST (onboarding path) or silently merge two source folders into one on-disk directory (main UI path, case-insensitive filesystems like macOS APFS and default Windows NTFS). Either way the user ended up with a corrupted import.

This PR addresses both failure modes:

  1. Writer-layer dedup (primary fix) — in both parseCollectionItems implementations (IPC handler and onboarding util), track sibling names per parent directory via a Set<string> keyed on toLowerCase(), and resolve collisions via a new getUniqueSiblingName helper in utils/filesystem.js. Appends - N on collision (matches the existing findUniqueFolderName convention used for top-level collection names). Applies to folders, requests, and .js sidecar files. Covers all importers since they share the writer path.
  2. Converter-layer dedup upgradepostman-to-bruno.js now keys folderMap/requestMap case-insensitively; insomnia-to-bruno.js and wsdl-to-bruno.js compare addSuffixToDuplicateName case-insensitively. This keeps the in-memory tree consistent with what gets written to disk.
  3. Adds { recursive: true } to the mkdirSync call in utils/collection-import.js for parity with the IPC handler (same fix as fix(import): resolve EEXIST error when importing OpenAPI collections with paths folder arrangement #7499 for the OpenAPI path).

The dedup is case-insensitive on all platforms, not gated on OS — collections get shared across machines, and a Linux-clean import that breaks for a macOS teammate on re-import is a worse failure mode than seeing a numeric suffix on Linux.

Tests

  • packages/bruno-electron/tests/utils/filesystem.spec.js — 7 tests for the helper (no-collision, same-case collision, case-only collision, counter propagation past pre-reserved suffixes, file-extension handling, folder/file shared namespace, null/undefined ext handling).
  • packages/bruno-converters/tests/postman/postman-to-bruno/dedupe-folder-names.spec.js — 3 tests (exact dupe folders, case-only dupe folders, case-only dupe requests).
  • packages/bruno-converters/tests/insomnia/dedupe-names.spec.js — 2 tests (case-only dupe folders, case-only dupe requests).

All existing converter tests (890) still pass.

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. (N/A — no UI change)
  • I have read the contribution guidelines.
  • Create an issue and link to the pull request.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed collection import logic to properly handle items with names differing only by letter casing, preventing file overwrites on case-insensitive filesystems.
    • Improved deduplication in Insomnia, Postman, and WSDL converters to treat names case-insensitively.
  • Tests

    • Added comprehensive test coverage for deduplication behavior across collection imports and converters.

Collections from Postman/Insomnia/OpenAPI/WSDL that contain siblings with
duplicate names (`Returns` and `Returns`) or case-only variants (`OAuth2`
and `oAuth2`) previously either crashed with EEXIST on the onboarding-
service import path, or silently merged two folders into one on case-
insensitive filesystems (macOS APFS/HFS+, Windows NTFS by default) — both
leaving the user with a corrupted import.

Adds a per-directory `Set<string>` of already-claimed sibling names,
matched case-insensitively, and a `getUniqueSiblingName` helper that
appends ` - N` on collision. Applied in both `parseCollectionItems`
implementations (IPC handler + onboarding util), and the collection-
import.js mkdirSync now uses `{ recursive: true }` for parity.

Also makes the converter-level dedup case-insensitive in
postman-to-bruno, insomnia-to-bruno and wsdl-to-bruno so the tree the
user sees in the UI matches what is written to disk.

Tests: new unit tests for the helper (7), and converter regression
tests for Postman (3) and Insomnia (2) covering exact-duplicate and
case-only collisions for both folders and requests.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

Walkthrough

This PR fixes case-insensitive sibling name collisions in Postman, Insomnia, and WSDL collection imports. It updates converters to detect duplicates case-insensitively, introduces getUniqueSiblingName for filesystem-level deduplication, and integrates this utility into collection import logic to prevent overwrites on case-insensitive filesystems.

Changes

Cohort / File(s) Summary
Converter case-insensitive deduplication
packages/bruno-converters/src/insomnia/insomnia-to-bruno.js, packages/bruno-converters/src/postman/postman-to-bruno.js, packages/bruno-converters/src/wsdl/wsdl-to-bruno.js
Updated addSuffixToDuplicateName and collision detection logic to compare names case-insensitively while preserving original casing in output, ensuring sibling items differing only by case receive distinct suffixed names.
Filesystem uniqueness utility
packages/bruno-electron/src/utils/filesystem.js
Added new getUniqueSiblingName(baseName, extension, usedNamesLowercase) export that generates unique sibling names by appending - N suffix on case-insensitive collisions and mutating the used names set.
Collection import integration
packages/bruno-electron/src/ipc/collection.js, packages/bruno-electron/src/utils/collection-import.js
Integrated getUniqueSiblingName into recursive import logic to deduplicate request/folder filenames per-directory using usedNamesLowercase set tracking, preventing silent overwrites on case-insensitive filesystems.
Converter regression tests
packages/bruno-converters/tests/insomnia/dedupe-names.spec.js, packages/bruno-converters/tests/postman/postman-to-bruno/dedupe-folder-names.spec.js
Added test suites validating case-insensitive sibling name deduplication in Insomnia and Postman imports, ensuring distinct items receive appropriate suffixes.
Filesystem utility tests
packages/bruno-electron/tests/utils/filesystem.spec.js
Added test suite covering getUniqueSiblingName behavior including collision-free cases, same-case/case-only duplicates, extension handling, and namespace sharing between folders and files.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Possibly related PRs

  • #7499: Modifies collection import directory creation to use fs.mkdirSync(..., { recursive: true }) to avoid EEXIST errors on case-insensitive filesystems—directly related fix to the same issue.

Suggested labels

size/XL

Suggested reviewers

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

Poem

🎯 Case-insensitive names collide no more,
With suffixes appended to even the score.
OAuth2, oAuth2—now distinct on disk,
No silent overwrites, no import risk! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: deduplicating sibling folder and file names case-insensitively during imports.
Linked Issues check ✅ Passed All coding requirements from #7821 are met: case-insensitive dedup at both converter and writer layers, numeric suffixes applied, folder and request names handled, consistent behavior across all importers, and recursive mkdir added.
Out of Scope Changes check ✅ Passed All changes are directly scoped to #7821: converter-layer case-insensitive comparisons, new filesystem utility, writer-layer dedup integration, and corresponding test coverage.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Copy Markdown
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/bruno-converters/src/wsdl/wsdl-to-bruno.js (1)

116-128: ⚠️ Potential issue | 🟡 Minor

Add a WSDL regression test for case-only operation names.

This converter now changes duplicate detection semantics, but the provided tests only cover Postman/Insomnia paths. Please add a small WSDL case with sibling operations like GetUser / getuser and assert the suffixed Bruno names.

As per coding guidelines, "Add tests for any new functionality or meaningful changes. If code is added, removed, or significantly modified, corresponding tests should be updated or created."

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

In `@packages/bruno-converters/src/wsdl/wsdl-to-bruno.js` around lines 116 - 128,
Add a WSDL-specific regression test that exercises the duplicate-name logic in
addSuffixToDuplicateName (packages/bruno-converters/src/wsdl/wsdl-to-bruno.js):
create a minimal WSDL string with sibling operations that differ only by case
(e.g., GetUser and getuser), run the wsdl-to-bruno conversion path used by your
test harness, and assert the converted Bruno operation names include the suffix
on the later-occurring case-duplicate (e.g., first remains "GetUser", second
becomes "getuser_1"). Ensure the test covers the WSDL converter code path (not
just Postman/Insomnia) and fails if names are not suffixed as expected.
packages/bruno-converters/src/insomnia/insomnia-to-bruno.js (1)

23-35: ⚠️ Potential issue | 🟠 Major

Apply the same dedupe to Insomnia v5 folders.

The helper is now case-insensitive, but the v5 folder branch still assigns name: item.name || 'Untitled Folder' directly. A v5 export with sibling folders like OAuth2 / oAuth2 will still produce case-colliding folder names in the converter output.

🐛 Proposed fix
         } else if (item.children && Array.isArray(item.children)) {
+          const folderName = item.name || 'Untitled Folder';
+          const folderSiblings = allItems.map((sibling) => ({
+            name: sibling.name || 'Untitled Folder'
+          }));
+          const name = addSuffixToDuplicateName({ name: folderName }, index, folderSiblings);
+
           // Process folder
           return {
             uid: uuid(),
-            name: item.name || 'Untitled Folder',
+            name,
             type: 'folder',
             items: parseCollectionItems(item.children, item.children)
           };

Also applies to: 217-224

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

In `@packages/bruno-converters/src/insomnia/insomnia-to-bruno.js` around lines 23
- 35, The v5 folder conversion still sets name directly as `name: item.name ||
'Untitled Folder'`, bypassing case-insensitive dedupe; update the v5 folder
branch to use the existing addSuffixToDuplicateName helper instead (call
addSuffixToDuplicateName(item, index, allItems) or the appropriate
sibling-folder array used in the v5 conversion) so folders like `OAuth2` /
`oAuth2` get distinct suffixed names; ensure you pass the same allItems
collection and index semantics as used elsewhere so behavior matches the v4/path
request dedupe.
packages/bruno-converters/src/postman/postman-to-bruno.js (1)

368-380: ⚠️ Potential issue | 🟡 Minor

Use Set instead of plain objects for case-insensitive name deduplication.

When folderName or requestName (from Postman imports) is lowercased and stored in a plain object, it can collide with inherited properties like constructor, __proto__, or toString. This breaks deduplication — a request named "Constructor" would incorrectly pass the while (folderMap[folderName.toLowerCase()]) check. Since these maps only verify existence (not retrieve values), a Set avoids the pitfall entirely.

Proposed fix
-  const folderMap = {};
-  const requestMap = {};
+  const folderNames = new Set();
+  const requestNames = new Set();

-      while (folderMap[folderName.toLowerCase()]) {
+      while (folderNames.has(folderName.toLowerCase())) {
         folderName = `${baseFolderName}_${count}`;
         count++;
       }

-      folderMap[folderName.toLowerCase()] = brunoFolderItem;
+      folderNames.add(folderName.toLowerCase());

-      while (requestMap[requestName.toLowerCase()]) {
+      while (requestNames.has(requestName.toLowerCase())) {
         requestName = `${baseRequestName}_${count}`;
         count++;
       }

-      requestMap[requestName.toLowerCase()] = brunoRequestItem;
+      requestNames.add(requestName.toLowerCase());

Also applies to: 435, 449, 809

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

In `@packages/bruno-converters/src/postman/postman-to-bruno.js` around lines 368 -
380, The dedupe maps folderMap and requestMap are plain objects which can
collide with inherited property names when using folderName.toLowerCase() or
requestName.toLowerCase(); replace folderMap and requestMap with Set instances
and update all checks and inserts (e.g., the while loop that checks
folderMap[folderName.toLowerCase()] and similar checks for requestMap) to use
Set.prototype.has() and Set.prototype.add(), ensuring isItemAFolder/item.forEach
name deduplication uses lowercase keys consistently; also update the same
pattern found around the request-name handling (the requestName de-duplication
and any other occurrences referenced in the diff) to use Sets.
🤖 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/ipc/collection.js`:
- Around line 1169-1179: The per-directory name dedupe Set used in
parseCollectionItems (usedNamesLowercase) is created empty and therefore doesn't
reserve filenames that already exist on disk, allowing new items to overwrite
existing files; fix this by seeding usedNamesLowercase with the current
directory's existing filenames (lowercased) before calling Promise.all so
getUniqueSiblingName can avoid names already present on disk for the given
currentPath; use fs.readdir/fs.promises.readdir (or existing helper) to read
currentPath and add entries (normalized/lowercased and retaining extensions)
into usedNamesLowercase prior to iterating items.

---

Outside diff comments:
In `@packages/bruno-converters/src/insomnia/insomnia-to-bruno.js`:
- Around line 23-35: The v5 folder conversion still sets name directly as `name:
item.name || 'Untitled Folder'`, bypassing case-insensitive dedupe; update the
v5 folder branch to use the existing addSuffixToDuplicateName helper instead
(call addSuffixToDuplicateName(item, index, allItems) or the appropriate
sibling-folder array used in the v5 conversion) so folders like `OAuth2` /
`oAuth2` get distinct suffixed names; ensure you pass the same allItems
collection and index semantics as used elsewhere so behavior matches the v4/path
request dedupe.

In `@packages/bruno-converters/src/postman/postman-to-bruno.js`:
- Around line 368-380: The dedupe maps folderMap and requestMap are plain
objects which can collide with inherited property names when using
folderName.toLowerCase() or requestName.toLowerCase(); replace folderMap and
requestMap with Set instances and update all checks and inserts (e.g., the while
loop that checks folderMap[folderName.toLowerCase()] and similar checks for
requestMap) to use Set.prototype.has() and Set.prototype.add(), ensuring
isItemAFolder/item.forEach name deduplication uses lowercase keys consistently;
also update the same pattern found around the request-name handling (the
requestName de-duplication and any other occurrences referenced in the diff) to
use Sets.

In `@packages/bruno-converters/src/wsdl/wsdl-to-bruno.js`:
- Around line 116-128: Add a WSDL-specific regression test that exercises the
duplicate-name logic in addSuffixToDuplicateName
(packages/bruno-converters/src/wsdl/wsdl-to-bruno.js): create a minimal WSDL
string with sibling operations that differ only by case (e.g., GetUser and
getuser), run the wsdl-to-bruno conversion path used by your test harness, and
assert the converted Bruno operation names include the suffix on the
later-occurring case-duplicate (e.g., first remains "GetUser", second becomes
"getuser_1"). Ensure the test covers the WSDL converter code path (not just
Postman/Insomnia) and fails if names are not suffixed as expected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b51c013f-0de8-439c-b51e-0e259e8812fd

📥 Commits

Reviewing files that changed from the base of the PR and between c4dc0bc and eb1d6aa.

📒 Files selected for processing (9)
  • packages/bruno-converters/src/insomnia/insomnia-to-bruno.js
  • packages/bruno-converters/src/postman/postman-to-bruno.js
  • packages/bruno-converters/src/wsdl/wsdl-to-bruno.js
  • packages/bruno-converters/tests/insomnia/dedupe-names.spec.js
  • packages/bruno-converters/tests/postman/postman-to-bruno/dedupe-folder-names.spec.js
  • packages/bruno-electron/src/ipc/collection.js
  • packages/bruno-electron/src/utils/collection-import.js
  • packages/bruno-electron/src/utils/filesystem.js
  • packages/bruno-electron/tests/utils/filesystem.spec.js

Comment on lines 1169 to +1179
const parseCollectionItems = async (items = [], currentPath) => {
const usedNamesLowercase = new Set();

await Promise.all(items.map(async (item) => {
if (['http-request', 'graphql-request', 'grpc-request', 'ws-request'].includes(item.type)) {
let sanitizedFilename = sanitizeName(getFilenameWithFormat(item, format));
const sanitizedFilename = sanitizeName(getFilenameWithFormat(item, format));
const ext = path.extname(sanitizedFilename);
const base = ext ? sanitizedFilename.slice(0, -ext.length) : sanitizedFilename;
const uniqueFilename = getUniqueSiblingName(base, ext, usedNamesLowercase);
const content = await stringifyRequestViaWorker(item, { format });
const filePath = path.join(currentPath, sanitizedFilename);
const filePath = path.join(currentPath, uniqueFilename);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Seed dedupe with existing directory entries to avoid metadata overwrites.

The per-directory Set starts empty, so it does not reserve files already written in that directory. In bru imports, a request named collection can still write collection.bru over the root collection file, and a child request named folder can overwrite folder.bru inside a folder.

🐛 Proposed fix
         const parseCollectionItems = async (items = [], currentPath) => {
-          const usedNamesLowercase = new Set();
+          const usedNamesLowercase = new Set(
+            fs.existsSync(currentPath)
+              ? fs.readdirSync(currentPath).map((name) => name.toLowerCase())
+              : []
+          );

           await Promise.all(items.map(async (item) => {

Also applies to: 1183-1205

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

In `@packages/bruno-electron/src/ipc/collection.js` around lines 1169 - 1179, The
per-directory name dedupe Set used in parseCollectionItems (usedNamesLowercase)
is created empty and therefore doesn't reserve filenames that already exist on
disk, allowing new items to overwrite existing files; fix this by seeding
usedNamesLowercase with the current directory's existing filenames (lowercased)
before calling Promise.all so getUniqueSiblingName can avoid names already
present on disk for the given currentPath; use fs.readdir/fs.promises.readdir
(or existing helper) to read currentPath and add entries (normalized/lowercased
and retaining extensions) into usedNamesLowercase prior to iterating items.

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.

Postman/Insomnia import silently corrupts folders with duplicate or case-colliding sibling names

1 participant