[Test Improver] test: add unit tests for MCPIntegrator (0% -> ~75%)#632
[Test Improver] test: add unit tests for MCPIntegrator (0% -> ~75%)#632danielmeppiel wants to merge 2 commits intomainfrom
Conversation
Cover pure-logic methods: deduplicate, get_server_names, get_server_configs, _append_drifted_to_install_list, _detect_mcp_config_drift, _detect_runtimes, _build_self_defined_info, _apply_overlay, update_lockfile, remove_stale (vscode + copilot), and collect_transitive edge cases. 70 new tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…264-5b2037248fd572ad
There was a problem hiding this comment.
Pull request overview
Adds a dedicated unit-test suite for the MCPIntegrator orchestration helpers, improving confidence in MCP dependency handling (deduplication, config building, drift/runtime detection, lockfile persistence, and stale cleanup) without requiring installed runtimes or network access.
Changes:
- Introduces
tests/unit/integration/test_mcp_integrator.pywith focused unit tests for pure-logic/static methods inMCPIntegrator. - Covers filesystem-based behaviors via temporary directories and targeted patching of
Path.cwd()/Path.home().
Show a summary per file
| File | Description |
|---|---|
tests/unit/integration/test_mcp_integrator.py |
New unit tests exercising MCPIntegrator helpers and edge cases (dedup, overlays, drift detection, runtime detection, lockfile updates, stale removal). |
Copilot's findings
- Files reviewed: 1/1 changed files
- Comments generated: 5
| import tempfile | ||
| from pathlib import Path | ||
| from unittest.mock import MagicMock, patch |
There was a problem hiding this comment.
Unused imports add noise in this test module (e.g., tempfile and MagicMock are never referenced). Please remove unused imports to keep the test file minimal and avoid future linting failures if/when enforced.
| import tempfile | |
| from pathlib import Path | |
| from unittest.mock import MagicMock, patch | |
| from pathlib import Path | |
| from unittest.mock import patch |
| def _write_vscode_mcp(self, path: Path, servers: dict) -> None: | ||
| path.mkdir(parents=True, exist_ok=True) | ||
| mcp_json = path / "mcp.json" | ||
| mcp_json.write_text(json.dumps({"servers": servers}), encoding="utf-8") | ||
| return mcp_json |
There was a problem hiding this comment.
Helper has a return statement but is annotated as returning None. Either drop the return value or update the annotation to return Path for accuracy.
| deps = [_make_dep("alpha"), _make_dep("beta"), _make_dep("alpha")] | ||
| result = MCPIntegrator.deduplicate(deps) | ||
| assert len(result) == 2 |
There was a problem hiding this comment.
This test name suggests mixed string/object inputs, but the deps list only contains MCPDependency objects. Rename the test or adjust it to actually include a string entry so the intent matches the coverage.
| deps = [_make_dep("alpha"), _make_dep("beta"), _make_dep("alpha")] | |
| result = MCPIntegrator.deduplicate(deps) | |
| assert len(result) == 2 | |
| deps = ["alpha", _make_dep("beta"), "alpha"] | |
| result = MCPIntegrator.deduplicate(deps) | |
| assert len(result) == 2 | |
| assert result[0] == "alpha" | |
| assert result[1].name == "beta" |
| def test_nameless_items_kept_by_identity(self): | ||
| # Items with no name should be kept (not merged) | ||
| dep1 = {"other": "x"} | ||
| dep2 = {"other": "y"} | ||
| result = MCPIntegrator.deduplicate([dep1, dep2]) | ||
| assert len(result) == 2 |
There was a problem hiding this comment.
The comment says nameless items are kept "by identity", but MCPIntegrator.deduplicate() uses dep not in result which deduplicates by equality (e.g., two distinct dicts with the same contents will be treated as duplicates). Please reword the comment to match the actual behavior being exercised.
| def test_stdio_env_vars_in_packages(self): | ||
| dep = _make_self_defined( | ||
| "pkg-svc", | ||
| transport="stdio", | ||
| command="pkg-svc", | ||
| env={"KEY": "val"}, | ||
| ) | ||
| info = MCPIntegrator._build_self_defined_info(dep) | ||
| # For stdio without raw: check packages contains env vars | ||
| packages = info.get("packages", []) | ||
| if packages: | ||
| env_vars = packages[0].get("environment_variables", []) | ||
| assert any(e["name"] == "KEY" for e in env_vars) | ||
|
|
||
| def test_no_tools_no_override_key(self): | ||
| dep = _make_self_defined("simple", transport="stdio") | ||
| info = MCPIntegrator._build_self_defined_info(dep) | ||
| assert "_apm_tools_override" not in info | ||
|
|
||
| def test_list_args_in_packages(self): | ||
| dep = _make_self_defined( | ||
| "args-svc", | ||
| transport="stdio", | ||
| command="args-svc", | ||
| args=["--arg1", "--arg2"], | ||
| ) | ||
| info = MCPIntegrator._build_self_defined_info(dep) | ||
| packages = info.get("packages", []) | ||
| if packages: | ||
| rt_args = packages[0].get("runtime_arguments", []) | ||
| hints = [a["value_hint"] for a in rt_args] | ||
| assert "--arg1" in hints | ||
|
|
There was a problem hiding this comment.
These assertions are guarded by if packages:, which makes the tests pass even if _build_self_defined_info() stops emitting packages for stdio deps (a regression you likely want to catch). Consider asserting that packages is present/non-empty first, then asserting on its contents.
🤖 Test Improver — automated AI assistant
Goal and Rationale
MCPIntegrator(src/apm_cli/integration/mcp_integrator.py, 1395 lines) is a large, high-risk orchestrator with zero dedicated tests. It owns MCP dependency resolution, deduplication, server-info building, drift detection, runtime detection, lockfile persistence, and stale cleanup across five runtimes (VS Code, Copilot, Codex, Cursor, OpenCode).Bugs here could silently corrupt MCP configuration files across all runtimes. This PR adds focused unit tests for every pure-logic method.
Approach
Tests target the methods that can be verified without live runtimes or network:
TestIsVscodeAvailable_is_vscode_available()— PATH + dir detectionTestDeduplicatededuplicate()— first-wins, dict/str/nameless entries, orderingTestGetServerNamesget_server_names()— dep objects, strings, mixedTestGetServerConfigsget_server_configs()— serialization, plain-string fallbackTestAppendDrifted_append_drifted_to_install_list()— sorted append, dedupTestDetectMcpConfigDrift_detect_mcp_config_drift()— changed/unchanged/new configsTestDetectRuntimes_detect_runtimes()— word-boundary regex, multi-runtimeTestBuildSelfDefinedInfo_build_self_defined_info()— stdio, http/sse, env, args, headers, toolsTestApplyOverlay_apply_overlay()— transport, package filter, headers, args (list+dict), tools, warningTestUpdateLockfileupdate_lockfile()— temp file writes, sorted servers, empty clearTestRemoveStaleVscoderemove_stale()— server removal, short-name matching, runtime targetingTestRemoveStaleCopilotremove_stale()— copilot config cleanupTestCollectTransitivecollect_transitive()— missing/empty dir edge casesCoverage Impact
mcp_integrator.pycoverageTest Status
All 70 new tests pass. Full unit suite passes with no regressions.
Reproducibility