Skip to content

test: rewrite cli.claude config tests to assert JSON output directly#2311

Merged
felixweinberger merged 3 commits intomainfrom
test/cli-claude-config-no-subprocess
Mar 19, 2026
Merged

test: rewrite cli.claude config tests to assert JSON output directly#2311
felixweinberger merged 3 commits intomainfrom
test/cli-claude-config-no-subprocess

Conversation

@maxisbey
Copy link
Contributor

Motivation

test_command_execution in tests/client/test_config.py shelled out to uv run --frozen --with mcp[cli] mcp run <path> --help with a 20-second timeout to verify the generated config was "executable". This was:

  • Not hermetic — depended on uv being on PATH on the test host
  • Slow — could trigger uv's resolver
  • Not meaningful — appending --help only proves uv accepts --help; it doesn't validate the command would actually launch an MCP server (the test passed even though test_server.py didn't exist)

update_claude_config is a config-file-writing function. Its contract is producing a correct JSON blob. That's directly testable without any subprocess.

Changes

  • Assert the exact command + args JSON structure — replaces the subprocess smoke test with a direct assertion on what the function actually produces
  • Mock get_uv_path in the fixture — tests no longer depend on the host's uv install
  • Cover previously untested option paths: with_packages (sorted/deduplicated), with_editable, env_vars, env-var merge on reinstall, file specs without :object suffix, preserving other mcpServers entries, get_uv_path fallback behavior
  • Remove 7 stale # pragma: no cover annotations from src/mcp/cli/claude.py — these branches are now actually tested
  • Move tests/client/test_config.pytests/cli/test_claude.py to mirror the source tree (the file tests mcp.cli.claude, not anything in mcp.client)

2 tests → 11 tests, no subprocess, 100% branch coverage on src/mcp/cli/claude.py.

The previous test_command_execution shelled out to 'uv run --frozen --with
mcp[cli] mcp run <path> --help' with a 20s timeout to verify the generated
command was executable. Appending --help only proves uv is installed; it
doesn't validate the command would actually launch a server.

update_claude_config is a config-file writer — its contract is producing
correct JSON. Test that directly:

- Assert exact command + args structure (replaces the subprocess smoke test)
- Mock get_uv_path so tests don't depend on the host's uv install
- Cover previously-untested option paths: with_packages (sorted/deduped),
  with_editable, env_vars, env merge-on-reinstall, file specs without :object,
  preserving other mcpServers entries
- Add get_uv_path tests for shutil.which resolution and fallback

Removes 7 now-stale '# pragma: no cover' annotations from claude.py.
Moves the file to tests/cli/test_claude.py to mirror src/mcp/cli/claude.py.
Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

LGTM — pure test rewrite with no production logic changes.

Extended reasoning...

Overview

This PR rewrites the Claude Desktop config tests (tests/client/test_config.pytests/cli/test_claude.py). The only production code change in src/mcp/cli/claude.py is removing 7 # pragma: no cover annotations — zero logic changes. The new test file replaces 2 flaky subprocess-dependent tests with 11 hermetic, mocked tests covering all branch paths.

Security risks

None. The production code is unchanged in behavior. The test file uses standard pytest fixtures with tmp_path and monkeypatch — no security-sensitive patterns.

Level of scrutiny

Low scrutiny warranted. This is a test-only improvement that makes the test suite more reliable and comprehensive. The pragma removals are mechanical and correct since the branches are now covered by tests.

Other factors

The reported bug (Windows drive-letter colon parsing) is pre-existing and not introduced or modified by this PR. The new tests are well-structured, use proper mocking (monkeypatch.setattr over unittest.mock.patch), and the test file location now correctly mirrors the source tree. The PR description clearly explains the motivation and changes.

Copy link
Contributor

@felixweinberger felixweinberger left a comment

Choose a reason for hiding this comment

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

Nice thanks - LGTM.
Could consider fixing the pre-existing thing @claude caught above, but optional.

@claude
Copy link
Contributor

claude bot commented Mar 19, 2026

Claude finished @felixweinberger's task in 1m 58s —— View job


Fixed Windows drive-letter bug ✅

I've implemented and committed the fix for the pre-existing Windows path parsing bug.

Changes Made

Source code fix (src/mcp/cli/claude.py, lines 116-125):

# Convert file path to absolute before adding to command
# Split off any :object suffix first
# First check if we have a Windows path (e.g., C:\...)
has_windows_drive = len(file_spec) > 1 and file_spec[1] == ":"

# Split on the last colon, but only if it's not part of the Windows drive letter
if ":" in (file_spec[2:] if has_windows_drive else file_spec):
    file_path, server_object = file_spec.rsplit(":", 1)
    file_spec = f"{Path(file_path).resolve()}:{server_object}"
else:
    file_spec = str(Path(file_spec).resolve())

This applies the same Windows drive-letter detection logic that was already present in _parse_file_path (cli.py:97-102).

Test coverage (tests/cli/test_claude.py):

  • Added test_windows_drive_letter_without_object — verifies C:\Users\foo\server.py doesn't split on the drive letter colon
  • Added test_windows_drive_letter_with_object — verifies C:\Users\foo\server.py:app only splits on the :app suffix, not C:

What This Fixes

Before this fix, on Windows:

  • mcp install C:\Users\foo\server.py would split on C:, treating \Users\foo\server.py as an object suffix
  • This would generate a broken config entry with an invalid path
  • The MCP server would be unlaunchable from Claude Desktop

After this fix:

  • Windows drive letters are detected and preserved
  • Only actual :object suffixes (after position 2) trigger splitting
  • Config entries are correctly generated for Windows paths

Commit

Pushed to test/cli-claude-config-no-subprocess as commit 1b1d404.

CI will verify tests pass, coverage remains at 100%, and formatting/type checking succeed.

--- | Branch: test/cli-claude-config-no-subprocess

Before this fix, `update_claude_config` would incorrectly split Windows paths
like `C:\Users\foo\server.py` on the drive letter colon, treating
`\Users\foo\server.py` as an object suffix. This would generate broken config
entries that made MCP servers unlaunchable from Claude Desktop on Windows.

Apply the same Windows drive-letter detection logic already present in
`_parse_file_path` (cli.py:97-102): check if position 1 is a colon, and if so,
only consider colons after position 2 as object-suffix separators.

Add test coverage for Windows paths with and without `:object` suffixes.

Co-authored-by: Felix Weinberger <felixweinberger@users.noreply.github.com>
The previous tests mocked Path.resolve with a conditional that fell through
to the original implementation — but with the fix in place, only the full
path ever hits resolve(), so the fallthrough was dead code and missed
coverage (lines 129, 146).

Replace with a single parametrized test that records what resolve() was
called with. Before the fix, a bare 'C:\path\server.py' would rsplit on
the drive colon and call resolve(Path('C')); now it calls resolve on the
full path. The recorder asserts that directly — no dead branches.

Also covers the has_windows_drive=True + :object-suffix branch that was
previously missed.
Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

LGTM — straightforward test rewrite replacing non-hermetic subprocess tests with direct JSON assertions, plus a clean bug fix for Windows drive-letter handling.

Extended reasoning...

Overview

This PR replaces two flaky, subprocess-dependent tests in tests/client/test_config.py with 11+ hermetic unit tests in tests/cli/test_claude.py. The source changes in src/mcp/cli/claude.py are limited to removing stale # pragma: no cover annotations (now that the branches are tested) and adding a Windows drive-letter guard that mirrors the existing pattern in _parse_file_path.

Security risks

None. The source changes are a bug fix for path parsing and removal of coverage pragmas. No auth, crypto, or permissions code is touched.

Level of scrutiny

Low scrutiny is appropriate. This is a test-quality improvement PR. The only behavioral change in production code is the Windows drive-letter fix, which applies an existing, well-understood pattern (has_windows_drive = len(file_spec) > 1 and file_spec[1] == ":") that is already used elsewhere in the same codebase.

Other factors

The new tests are well-structured: they mock external dependencies (get_claude_config_path, get_uv_path), test edge cases (env-var merging, package deduplication, preserving other servers, missing config dir), and cover the Windows drive-letter fix with two dedicated parameterized tests. The PR description clearly explains the motivation and scope. No outstanding reviewer comments exist.

Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

LGTM — clean test rewrite with good coverage, and the Windows drive-letter fix follows the existing pattern from _parse_file_path.

Extended reasoning...

Overview

This PR rewrites the mcp.cli.claude config tests, moving them from tests/client/test_config.py to tests/cli/test_claude.py to mirror the source tree. It replaces 2 flaky subprocess-based tests (which depended on uv being on PATH and had a 20-second timeout) with 11 hermetic unit tests that directly assert the JSON config output. The source changes in claude.py are limited to removing 7 stale # pragma: no cover annotations and adding a Windows drive-letter guard that mirrors the existing pattern in _parse_file_path.

Security risks

None. The production code changes are minimal — removing pragma comments and adding a defensive check for Windows path parsing. No auth, crypto, or permissions code is touched.

Level of scrutiny

Low scrutiny is appropriate. This is primarily a test improvement PR. The only logic change in production code is the Windows drive-letter fix, which applies an identical pattern already established elsewhere in the same file (cli.py:97-102). The fix was identified in a previous review round and has been addressed with corresponding test coverage.

Other factors

  • The pre-existing Windows drive-letter bug I flagged in my previous review has been fixed and the inline comment is resolved.
  • Test coverage went from 2 tests to 11, covering previously untested branches (env var merging, package deduplication, editable installs, missing config dir, get_uv_path fallback).
  • The tests are well-structured with proper mocking and clear assertions.
  • No CODEOWNERS concerns — this touches test infrastructure and a utility module.

@felixweinberger felixweinberger merged commit 883d893 into main Mar 19, 2026
33 checks passed
@felixweinberger felixweinberger deleted the test/cli-claude-config-no-subprocess branch March 19, 2026 15:16
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.

2 participants