Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions src/apm_cli/policy/ci_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,23 +231,52 @@ def _check_config_consistency(
lock: LockFile,
) -> CheckResult:
"""Verify MCP server configs match lockfile baseline."""
from ..deps.path_anchoring import LocalResolutionError, resolve_local_dep_dir
from ..drift import detect_config_drift
from ..integration.mcp_integrator import MCPIntegrator
from ..models.apm_package import APMPackage

details: list[str] = []
mcp_deps = manifest.get_all_mcp_dependencies()
for locked_dep in lock.get_package_dependencies():
if locked_dep.source != "local":
continue
try:
package_dir = resolve_local_dep_dir(locked_dep, lock, manifest.package_path)
except LocalResolutionError as exc:
details.append(
f"{locked_dep.repo_url}: cannot resolve local package ({exc}) -- "
"fix the resolved_by chain or re-run 'apm install'"
)
continue
package_manifest = package_dir / "apm.yml"
if not package_manifest.exists():
details.append(
f"{locked_dep.repo_url}: local package manifest not found at "
f"{package_manifest} -- restore the source or re-run 'apm install'"
)
continue
try:
package = APMPackage.from_apm_yml(package_manifest)
except (OSError, ValueError) as exc:
details.append(
f"{locked_dep.repo_url}: cannot parse local package manifest ({exc}) -- "
"fix the manifest or re-run 'apm install'"
)
continue
mcp_deps.extend(package.get_mcp_dependencies())
mcp_deps = MCPIntegrator.deduplicate(mcp_deps)
current_configs = MCPIntegrator.get_server_configs(mcp_deps)
stored_configs = lock.mcp_configs or {}

# No MCP deps at all -- nothing to check
if not current_configs and not stored_configs:
if not current_configs and not stored_configs and not details:
return CheckResult(
name="config-consistency",
passed=True,
message="No MCP configs to check",
)

details: list[str] = []

# Detect drift on servers that exist in both sets
drifted = detect_config_drift(current_configs, stored_configs)
for name in sorted(drifted):
Expand Down
139 changes: 139 additions & 0 deletions tests/unit/policy/test_ci_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,145 @@ def test_fail_mcp_config_drift(self, tmp_path):
assert not result.passed
assert any("my-server" in d and "differs" in d for d in result.details)

def test_fail_local_path_subpackage_mcp_config_drift(self, tmp_path):
"""A local sub-package MCP source change must differ from its lock baseline."""
package = tmp_path / "packages" / "agent-config"
package.mkdir(parents=True)
_write_apm_yml(tmp_path, deps=["./packages/agent-config"])
(package / "apm.yml").write_text(
textwrap.dedent("""\
name: agent-config
version: 1.0.0
dependencies:
mcp:
- name: shadcn
registry: false
transport: stdio
command: echo
args: [changed]
"""),
encoding="utf-8",
)
_write_lockfile(
tmp_path,
textwrap.dedent("""\
lockfile_version: '1'
generated_at: '2025-01-01T00:00:00Z'
dependencies:
- repo_url: _local/agent-config
source: local
local_path: ./packages/agent-config
depth: 1
deployed_files: []
mcp_configs:
shadcn:
name: shadcn
registry: false
transport: stdio
command: echo
args: [ready]
mcp_config_provenance:
shadcn: agent-config
"""),
)
from apm_cli.deps.lockfile import LockFile, get_lockfile_path
from apm_cli.models.apm_package import APMPackage

manifest = APMPackage.from_apm_yml(tmp_path / "apm.yml")
lock = LockFile.read(get_lockfile_path(tmp_path))
result = _check_config_consistency(manifest, lock)

assert not result.passed
assert result.details == ["shadcn: config differs from lockfile baseline"]

def test_local_resolution_error_fails_gracefully(self, tmp_path):
"""A corrupt local dependency graph must fail without crashing audit."""
_write_apm_yml(tmp_path, deps=["./packages/agent-config"])
_write_lockfile(
tmp_path,
textwrap.dedent("""\
lockfile_version: '1'
generated_at: '2025-01-01T00:00:00Z'
dependencies:
- repo_url: _local/agent-config
source: local
local_path: ../agent-config
resolved_by: _local/missing-parent
depth: 2
deployed_files: []
"""),
)
from apm_cli.deps.lockfile import LockFile, get_lockfile_path
from apm_cli.models.apm_package import APMPackage

manifest = APMPackage.from_apm_yml(tmp_path / "apm.yml")
lock = LockFile.read(get_lockfile_path(tmp_path))
result = _check_config_consistency(manifest, lock)

assert not result.passed
assert len(result.details) == 1
assert "_local/agent-config: cannot resolve local package" in result.details[0]
assert "re-run 'apm install'" in result.details[0]

def test_invalid_local_package_manifest_fails_gracefully(self, tmp_path):
"""An invalid local package manifest must fail without crashing audit."""
package = tmp_path / "packages" / "agent-config"
package.mkdir(parents=True)
_write_apm_yml(tmp_path, deps=["./packages/agent-config"])
(package / "apm.yml").write_text("version: 1.0.0\n", encoding="utf-8")
_write_lockfile(
tmp_path,
textwrap.dedent("""\
lockfile_version: '1'
generated_at: '2025-01-01T00:00:00Z'
dependencies:
- repo_url: _local/agent-config
source: local
local_path: ./packages/agent-config
depth: 1
deployed_files: []
"""),
)
from apm_cli.deps.lockfile import LockFile, get_lockfile_path
from apm_cli.models.apm_package import APMPackage

manifest = APMPackage.from_apm_yml(tmp_path / "apm.yml")
lock = LockFile.read(get_lockfile_path(tmp_path))
result = _check_config_consistency(manifest, lock)

assert not result.passed
assert len(result.details) == 1
assert "_local/agent-config: cannot parse local package manifest" in result.details[0]
assert "Missing required field 'name'" in result.details[0]

def test_missing_local_package_manifest_fails_gracefully(self, tmp_path):
"""A missing local package manifest must fail without a false pass."""
_write_apm_yml(tmp_path, deps=["./packages/agent-config"])
_write_lockfile(
tmp_path,
textwrap.dedent("""\
lockfile_version: '1'
generated_at: '2025-01-01T00:00:00Z'
dependencies:
- repo_url: _local/agent-config
source: local
local_path: ./packages/agent-config
depth: 1
deployed_files: []
"""),
)
from apm_cli.deps.lockfile import LockFile, get_lockfile_path
from apm_cli.models.apm_package import APMPackage

manifest = APMPackage.from_apm_yml(tmp_path / "apm.yml")
lock = LockFile.read(get_lockfile_path(tmp_path))
result = _check_config_consistency(manifest, lock)

assert not result.passed
assert len(result.details) == 1
assert "_local/agent-config: local package manifest not found" in result.details[0]
assert "re-run 'apm install'" in result.details[0]

def test_transitive_mcp_server_not_flagged_as_orphan(self, tmp_path):
"""A server declared by a local-path sub-package is exempt from orphan.

Expand Down
Loading