Skip to content

Comments

test: add unit tests for MCP modules#432

Open
nikolasdehor wants to merge 1 commit intoSynkraAI:mainfrom
nikolasdehor:test/consolidated-mcp
Open

test: add unit tests for MCP modules#432
nikolasdehor wants to merge 1 commit intoSynkraAI:mainfrom
nikolasdehor:test/consolidated-mcp

Conversation

@nikolasdehor
Copy link
Contributor

@nikolasdehor nikolasdehor commented Feb 19, 2026

Add test coverage for all MCP modules. All tests pass locally. Closes #431

Summary by CodeRabbit

  • Tests
    • Added comprehensive unit test suites for core MCP modules, covering configuration management, OS detection across platforms, symlink operations with cross-platform support, and module facade validation.
    • Tests validate both success and error paths for directory creation, config file operations, link management, and platform-specific behaviors.

Copilot AI review requested due to automatic review settings February 19, 2026 19:18
@vercel
Copy link

vercel bot commented Feb 19, 2026

@nikolasdehor is attempting to deploy a commit to the Pedro Valério Lopez's projects Team on Vercel.

A member of the Team first needs to authorize it.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds comprehensive unit test coverage for the MCP (Model Context Protocol) modules, specifically targeting four core modules: os-detector, global-config-manager, symlink-manager, and the index barrel file. The tests ensure cross-platform compatibility and validate all public APIs of these modules, addressing issue #431.

Changes:

  • Added 197 lines of tests for os-detector covering OS detection, path utilities, and symlink support checks
  • Added 322 lines of tests for global-config-manager covering CRUD operations, server management, and directory structure creation
  • Added 626 lines of tests for symlink-manager with extensive cross-platform testing for Windows junctions and Unix symlinks
  • Added 117 lines of tests for the MCP index module validating both spread and namespaced exports

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
tests/core/mcp/os-detector.test.js Tests OS detection, platform checks, path utilities, and link type determination across Windows, macOS, and Linux
tests/core/mcp/global-config-manager.test.js Tests global MCP configuration management including structure creation, config CRUD, server operations, and template handling
tests/core/mcp/symlink-manager.test.js Comprehensive tests for symlink/junction creation, status checking, and removal with extensive Windows and Unix scenarios
tests/core/mcp/mcp-index.test.js Tests the barrel file's export patterns, validating both spread exports and namespaced objects for consistency

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

});

test('getGlobalMcpDir returns ~/.aios/mcp', () => {
expect(getGlobalMcpDir()).toMatch(/\.aios.mcp$/);
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The regular expression pattern .aios.mcp$ is missing the path separator character(s) between .aios and mcp. This test assertion will pass for paths like /home/user/.aiosFOOmcp when it should only match paths ending with .aios/mcp. The pattern should be updated to properly match the path separator, for example: /\.aios[\/\\]mcp$/ or more simply, since we know getGlobalMcpDir returns path.join(getGlobalAiosDir(), 'mcp'), just check if it includes '.aios' and ends with 'mcp'.

Suggested change
expect(getGlobalMcpDir()).toMatch(/\.aios.mcp$/);
expect(getGlobalMcpDir()).toMatch(/\.aios[\/\\]mcp$/);

Copilot uses AI. Check for mistakes.
@Pedrovaleriolopez
Copy link
Contributor

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 21, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Feb 21, 2026

Walkthrough

Four new comprehensive unit test suites were added for MCP modules: global-config-manager, os-detector, symlink-manager, and mcp-index. These tests cover configuration operations, OS detection utilities, filesystem link management, and facade exports with mocked dependencies and both success and error scenarios.

Changes

Cohort / File(s) Summary
MCP Core Tests
tests/core/mcp/global-config-manager.test.js, tests/core/mcp/os-detector.test.js, tests/core/mcp/symlink-manager.test.js, tests/core/mcp/mcp-index.test.js
Added unit test suites validating MCP module functionality including configuration CRUD operations, OS/path detection, symbolic link management across Unix/Windows platforms, and facade export consistency. Tests include success and error paths with mocked filesystem, environment, and child process interactions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

core, mcp, tests

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'test: add unit tests for MCP modules' directly and concisely describes the main change—adding comprehensive unit tests for MCP modules mentioned in the linked issue.
Linked Issues check ✅ Passed The PR adds comprehensive unit test suites for all four MCP modules specified in issue #431: os-detector, global-config-manager, symlink-manager, and index, fully meeting the stated objective.
Out of Scope Changes check ✅ Passed All changes are focused on adding unit tests for the four MCP modules specified in the linked issue, with no extraneous modifications detected.
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 docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

@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

🧹 Nitpick comments (4)
tests/core/mcp/symlink-manager.test.js (2)

405-420: Force-remove-symlink and Windows junction tests are missing result.success assertions.

Both tests verify a side effect (unlinkSync/execSync called) but don't assert the overall operation outcome, leaving the return value unvalidated.

✨ Proposed additions
       const result = createLink(projectRoot, { force: true });
       expect(fs.unlinkSync).toHaveBeenCalledWith(linkPath);
+      expect(result.success).toBe(true);

And for the Windows junction test (lines 463–468):

       expect(execSync).toHaveBeenCalledWith(
         expect.stringContaining('mklink /J'),
         expect.objectContaining({ windowsHide: true })
       );
+      expect(result.success).toBe(true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/mcp/symlink-manager.test.js` around lines 405 - 420, The tests
that exercise createLink (in tests/core/mcp/symlink-manager.test.js) currently
assert side-effects (fs.unlinkSync / child_process.execSync) but do not assert
the returned result; update both the "force-remove-symlink" test and the
"Windows junction" test to capture the call result (const result =
createLink(projectRoot, { force: true }) / createLink(projectRoot, { windows:
true } ) as used) and add an assertion that result.success is true so the
overall operation outcome is validated in addition to the side-effect
assertions.

380-403: callCount-based lstatSync mock is brittle and implementation-coupled.

The counter assumes exactly 2 lstatSync calls occur before removal and the third starts returning a symlink. Any refactor that changes the internal call sequence will silently cause this test to exercise a different code path without failing. Additionally, the test asserts only that fs.rmSync was called, not that result.success is true.

♻️ Proposed refactor — use sequential mock and add outcome assertion
-      let callCount = 0;
-      fs.lstatSync.mockImplementation(() => {
-        callCount++;
-        if (callCount <= 2) return { isSymbolicLink: () => false };
-        return { isSymbolicLink: () => true };
-      });
-      fs.rmSync.mockReturnValue(undefined);
-      fs.symlinkSync.mockReturnValue(undefined);
-      fs.readlinkSync.mockReturnValue(globalPath);
+      // First lstatSync (check existing path) returns directory.
+      // Second lstatSync (post-creation verification) returns symlink.
+      fs.lstatSync
+        .mockReturnValueOnce({ isSymbolicLink: () => false })
+        .mockReturnValue({ isSymbolicLink: () => true });
+      fs.rmSync.mockReturnValue(undefined);
+      fs.symlinkSync.mockReturnValue(undefined);
+      fs.readlinkSync.mockReturnValue(globalPath);

       const result = createLink(projectRoot, { force: true });
       expect(fs.rmSync).toHaveBeenCalledWith(linkPath, { recursive: true });
+      expect(result.success).toBe(true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/mcp/symlink-manager.test.js` around lines 380 - 403, The lstatSync
call-count pattern is brittle—replace the callCount logic by sequencing explicit
mocks (e.g., use fs.lstatSync.mockImplementationOnce(...) twice returning {
isSymbolicLink: () => false } and then mockImplementationOnce(...) returning {
isSymbolicLink: () => true }) so the test behavior is stable regardless of call
ordering changes, and extend assertions after invoking createLink(projectRoot, {
force: true }) to check result.success === true in addition to verifying
fs.rmSync was calledWith(linkPath, { recursive: true }); keep references to
createLink, fs.lstatSync, fs.rmSync and result.success so the change is easy to
locate.
tests/core/mcp/os-detector.test.js (1)

124-126: Unescaped . in regex matches any character, not specifically the path separator.

/\.aios.mcp$/ passes for /home/user/.aios/mcp only because . happens to match /, but it would also match .aiosXmcp for any character X. Use a character class to be explicit.

✨ Proposed fix
-      expect(getGlobalMcpDir()).toMatch(/\.aios.mcp$/);
+      expect(getGlobalMcpDir()).toMatch(/\.aios[/\\]mcp$/);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/mcp/os-detector.test.js` around lines 124 - 126, The test's regex
`/\.aios.mcp$/` uses an unescaped dot that matches any character; update the
assertion in the test for getGlobalMcpDir to require an explicit path separator
between ".aios" and "mcp" by replacing the pattern with one that uses a
character class for separators, e.g. change the regex to /\.aios[\/\\]mcp$/ so
it matches both POSIX and Windows separators and still escapes the leading dot
before "aios".
tests/core/mcp/global-config-manager.test.js (1)

79-87: globalMcpDirExists and globalConfigExists tests lack path assertions.

Unlike globalDirExists (line 76), these tests don't verify the exact path passed to existsSync, so a path regression in the implementation would go undetected.

✨ Proposed fix
     test('globalMcpDirExists checks mcp directory', () => {
       fs.existsSync.mockReturnValue(false);
       expect(globalMcpDirExists()).toBe(false);
+      expect(fs.existsSync).toHaveBeenCalledWith('/home/user/.aios/mcp');
     });

     test('globalConfigExists checks config file', () => {
       fs.existsSync.mockReturnValue(true);
       expect(globalConfigExists()).toBe(true);
+      expect(fs.existsSync).toHaveBeenCalledWith('/home/user/.aios/mcp/global-config.json');
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/mcp/global-config-manager.test.js` around lines 79 - 87, The tests
for globalMcpDirExists and globalConfigExists should also assert that
fs.existsSync was called with the exact expected path: after mocking
fs.existsSync return value, add
expect(fs.existsSync).toHaveBeenCalledWith(expectedPath) for globalMcpDirExists
and for globalConfigExists; compute expectedPath using the same path-building
logic or helper used by the implementation (so refer to the module's path
builder or constants) and use those values in the assertion to catch any path
regressions; reference the functions globalMcpDirExists, globalConfigExists and
the mocked fs.existsSync when adding these assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/core/mcp/mcp-index.test.js`:
- Around line 75-93: The tests for mcpIndex namespaces (mcpIndex.osDetector,
mcpIndex.globalConfigManager, mcpIndex.symlinkManager, mcpIndex.configMigrator)
currently allow null because they only use toBeDefined() and typeof ===
'object'; update each test to assert that the property is both an object and not
null—for example replace the two assertions with a single stricter assertion
such as expect(mcpIndex.osDetector).toEqual(expect.any(Object)) (and similarly
for globalConfigManager, symlinkManager, configMigrator) or assert typeof ===
'object' and expect(...).not.toBeNull() so null values will fail.

In `@tests/core/mcp/symlink-manager.test.js`:
- Around line 84-87: The trailing-separator test for getProjectMcpPath is
ineffective because path.join normalizes the trailing slash; update the 'should
handle project root with trailing separator' test to assert against a canonical
hardcoded path string (e.g., '/my/project/.aios-core/tools/mcp') or assert
equality with getProjectMcpPath('/my/project') to explicitly test idempotency of
getProjectMcpPath when given a trailing separator.

---

Nitpick comments:
In `@tests/core/mcp/global-config-manager.test.js`:
- Around line 79-87: The tests for globalMcpDirExists and globalConfigExists
should also assert that fs.existsSync was called with the exact expected path:
after mocking fs.existsSync return value, add
expect(fs.existsSync).toHaveBeenCalledWith(expectedPath) for globalMcpDirExists
and for globalConfigExists; compute expectedPath using the same path-building
logic or helper used by the implementation (so refer to the module's path
builder or constants) and use those values in the assertion to catch any path
regressions; reference the functions globalMcpDirExists, globalConfigExists and
the mocked fs.existsSync when adding these assertions.

In `@tests/core/mcp/os-detector.test.js`:
- Around line 124-126: The test's regex `/\.aios.mcp$/` uses an unescaped dot
that matches any character; update the assertion in the test for getGlobalMcpDir
to require an explicit path separator between ".aios" and "mcp" by replacing the
pattern with one that uses a character class for separators, e.g. change the
regex to /\.aios[\/\\]mcp$/ so it matches both POSIX and Windows separators and
still escapes the leading dot before "aios".

In `@tests/core/mcp/symlink-manager.test.js`:
- Around line 405-420: The tests that exercise createLink (in
tests/core/mcp/symlink-manager.test.js) currently assert side-effects
(fs.unlinkSync / child_process.execSync) but do not assert the returned result;
update both the "force-remove-symlink" test and the "Windows junction" test to
capture the call result (const result = createLink(projectRoot, { force: true })
/ createLink(projectRoot, { windows: true } ) as used) and add an assertion that
result.success is true so the overall operation outcome is validated in addition
to the side-effect assertions.
- Around line 380-403: The lstatSync call-count pattern is brittle—replace the
callCount logic by sequencing explicit mocks (e.g., use
fs.lstatSync.mockImplementationOnce(...) twice returning { isSymbolicLink: () =>
false } and then mockImplementationOnce(...) returning { isSymbolicLink: () =>
true }) so the test behavior is stable regardless of call ordering changes, and
extend assertions after invoking createLink(projectRoot, { force: true }) to
check result.success === true in addition to verifying fs.rmSync was
calledWith(linkPath, { recursive: true }); keep references to createLink,
fs.lstatSync, fs.rmSync and result.success so the change is easy to locate.

Comment on lines +75 to +93
it('exports osDetector namespace object', () => {
expect(mcpIndex.osDetector).toBeDefined();
expect(typeof mcpIndex.osDetector).toBe('object');
});

it('exports globalConfigManager namespace object', () => {
expect(mcpIndex.globalConfigManager).toBeDefined();
expect(typeof mcpIndex.globalConfigManager).toBe('object');
});

it('exports symlinkManager namespace object', () => {
expect(mcpIndex.symlinkManager).toBeDefined();
expect(typeof mcpIndex.symlinkManager).toBe('object');
});

it('exports configMigrator namespace object', () => {
expect(mcpIndex.configMigrator).toBeDefined();
expect(typeof mcpIndex.configMigrator).toBe('object');
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Namespace type assertions are too permissive — typeof null === 'object'.

Both toBeDefined() and typeof x === 'object' return truthy for null, so the four namespace tests would still pass if the facade accidentally exported null for any of those properties.

🛡️ Proposed fix for namespace type assertions
-    it('exports osDetector namespace object', () => {
-      expect(mcpIndex.osDetector).toBeDefined();
-      expect(typeof mcpIndex.osDetector).toBe('object');
-    });
+    it('exports osDetector namespace object', () => {
+      expect(mcpIndex.osDetector).toEqual(expect.any(Object));
+    });

-    it('exports globalConfigManager namespace object', () => {
-      expect(mcpIndex.globalConfigManager).toBeDefined();
-      expect(typeof mcpIndex.globalConfigManager).toBe('object');
-    });
+    it('exports globalConfigManager namespace object', () => {
+      expect(mcpIndex.globalConfigManager).toEqual(expect.any(Object));
+    });

-    it('exports symlinkManager namespace object', () => {
-      expect(mcpIndex.symlinkManager).toBeDefined();
-      expect(typeof mcpIndex.symlinkManager).toBe('object');
-    });
+    it('exports symlinkManager namespace object', () => {
+      expect(mcpIndex.symlinkManager).toEqual(expect.any(Object));
+    });

-    it('exports configMigrator namespace object', () => {
-      expect(mcpIndex.configMigrator).toBeDefined();
-      expect(typeof mcpIndex.configMigrator).toBe('object');
-    });
+    it('exports configMigrator namespace object', () => {
+      expect(mcpIndex.configMigrator).toEqual(expect.any(Object));
+    });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('exports osDetector namespace object', () => {
expect(mcpIndex.osDetector).toBeDefined();
expect(typeof mcpIndex.osDetector).toBe('object');
});
it('exports globalConfigManager namespace object', () => {
expect(mcpIndex.globalConfigManager).toBeDefined();
expect(typeof mcpIndex.globalConfigManager).toBe('object');
});
it('exports symlinkManager namespace object', () => {
expect(mcpIndex.symlinkManager).toBeDefined();
expect(typeof mcpIndex.symlinkManager).toBe('object');
});
it('exports configMigrator namespace object', () => {
expect(mcpIndex.configMigrator).toBeDefined();
expect(typeof mcpIndex.configMigrator).toBe('object');
});
it('exports osDetector namespace object', () => {
expect(mcpIndex.osDetector).toEqual(expect.any(Object));
});
it('exports globalConfigManager namespace object', () => {
expect(mcpIndex.globalConfigManager).toEqual(expect.any(Object));
});
it('exports symlinkManager namespace object', () => {
expect(mcpIndex.symlinkManager).toEqual(expect.any(Object));
});
it('exports configMigrator namespace object', () => {
expect(mcpIndex.configMigrator).toEqual(expect.any(Object));
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/mcp/mcp-index.test.js` around lines 75 - 93, The tests for
mcpIndex namespaces (mcpIndex.osDetector, mcpIndex.globalConfigManager,
mcpIndex.symlinkManager, mcpIndex.configMigrator) currently allow null because
they only use toBeDefined() and typeof === 'object'; update each test to assert
that the property is both an object and not null—for example replace the two
assertions with a single stricter assertion such as
expect(mcpIndex.osDetector).toEqual(expect.any(Object)) (and similarly for
globalConfigManager, symlinkManager, configMigrator) or assert typeof ===
'object' and expect(...).not.toBeNull() so null values will fail.

Comment on lines +84 to +87
it('should handle project root with trailing separator', () => {
const result = getProjectMcpPath('/my/project/');
expect(result).toBe(path.join('/my/project/', '.aios-core', 'tools', 'mcp'));
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Trailing-separator test doesn't exercise distinct behavior.

path.join('/my/project/', '.aios-core', 'tools', 'mcp') normalizes to the same path as without the trailing slash, so the expected value equals the result of the first test. A stronger assertion would compare the output directly against a hardcoded canonical string, or compare to getProjectMcpPath('/my/project') to explicitly assert idempotency.

✨ Proposed alternative assertion
     it('should handle project root with trailing separator', () => {
       const result = getProjectMcpPath('/my/project/');
-      expect(result).toBe(path.join('/my/project/', '.aios-core', 'tools', 'mcp'));
+      expect(result).toBe(getProjectMcpPath('/my/project'));
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/mcp/symlink-manager.test.js` around lines 84 - 87, The
trailing-separator test for getProjectMcpPath is ineffective because path.join
normalizes the trailing slash; update the 'should handle project root with
trailing separator' test to assert against a canonical hardcoded path string
(e.g., '/my/project/.aios-core/tools/mcp') or assert equality with
getProjectMcpPath('/my/project') to explicitly test idempotency of
getProjectMcpPath when given a trailing separator.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add unit tests for MCP modules

2 participants