From ae3e13423da41ae532a0621fd7f1d0f24df52933 Mon Sep 17 00:00:00 2001 From: Test Improver Date: Thu, 19 Mar 2026 01:15:57 +0000 Subject: [PATCH] =?UTF-8?q?test:=20add=20unit=20tests=20for=20config=20com?= =?UTF-8?q?mand=20(22%=20=E2=86=92=20100%)=20+=20fix=20missing=20auto-inte?= =?UTF-8?q?grate=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 23 unit tests covering all three config subcommands: - config (show): outside project, inside project, with compilation settings, Rich fallback - config set: enable/disable auto-integrate (all valid aliases), invalid value, unknown key - config get: by key, all keys, unknown key, formatting of auto_integrate key - Fix bug: add missing get_auto_integrate() and set_auto_integrate() to apm_cli/config.py; these functions were referenced by commands/config.py but did not exist, causing an ImportError at runtime for 'apm config set/get auto-integrate' - commands/config.py coverage: 22% → 100% Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/apm_cli/config.py | 35 +++- tests/unit/test_config_command.py | 297 ++++++++++++++++++++++++++++++ 2 files changed, 323 insertions(+), 9 deletions(-) create mode 100644 tests/unit/test_config_command.py diff --git a/src/apm_cli/config.py b/src/apm_cli/config.py index 1415c4de..0d453221 100644 --- a/src/apm_cli/config.py +++ b/src/apm_cli/config.py @@ -1,10 +1,9 @@ """Configuration management for APM.""" -import os import json +import os from typing import Optional - CONFIG_DIR = os.path.expanduser("~/.apm") CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json") @@ -15,7 +14,7 @@ def ensure_config_exists(): """Ensure the configuration directory and file exist.""" if not os.path.exists(CONFIG_DIR): os.makedirs(CONFIG_DIR) - + if not os.path.exists(CONFIG_FILE): with open(CONFIG_FILE, "w") as f: json.dump({"default_client": "vscode"}, f) @@ -23,9 +22,9 @@ def ensure_config_exists(): def get_config(): """Get the current configuration. - + Results are cached for the lifetime of the process. - + Returns: dict: Current configuration. """ @@ -46,14 +45,14 @@ def _invalidate_config_cache(): def update_config(updates): """Update the configuration with new values. - + Args: updates (dict): Dictionary of configuration values to update. """ _invalidate_config_cache() config = get_config() config.update(updates) - + with open(CONFIG_FILE, "w") as f: json.dump(config, f, indent=2) _invalidate_config_cache() @@ -61,7 +60,7 @@ def update_config(updates): def get_default_client(): """Get the default MCP client. - + Returns: str: Default MCP client type. """ @@ -70,8 +69,26 @@ def get_default_client(): def set_default_client(client_type): """Set the default MCP client. - + Args: client_type (str): Type of client to set as default. """ update_config({"default_client": client_type}) + + +def get_auto_integrate() -> bool: + """Get the auto-integrate setting. + + Returns: + bool: Whether auto-integration is enabled (default: True). + """ + return get_config().get("auto_integrate", True) + + +def set_auto_integrate(enabled: bool) -> None: + """Set the auto-integrate setting. + + Args: + enabled: Whether to enable auto-integration. + """ + update_config({"auto_integrate": enabled}) diff --git a/tests/unit/test_config_command.py b/tests/unit/test_config_command.py new file mode 100644 index 00000000..25995627 --- /dev/null +++ b/tests/unit/test_config_command.py @@ -0,0 +1,297 @@ +"""Tests for the apm config command.""" + +import os +import sys +import tempfile +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest +from click.testing import CliRunner + +from apm_cli.commands.config import config + + +class TestConfigShow: + """Tests for `apm config` (show current configuration).""" + + def setup_method(self): + self.runner = CliRunner() + self.original_dir = os.getcwd() + + def teardown_method(self): + try: + os.chdir(self.original_dir) + except (FileNotFoundError, OSError): + pass + + def test_config_show_outside_project(self): + """Show config when not in an APM project directory.""" + with tempfile.TemporaryDirectory() as tmp_dir: + os.chdir(tmp_dir) + with patch("apm_cli.commands.config.get_version", return_value="1.2.3"): + result = self.runner.invoke(config, []) + assert result.exit_code == 0 + + def test_config_show_inside_project(self): + """Show config when apm.yml is present.""" + with tempfile.TemporaryDirectory() as tmp_dir: + os.chdir(tmp_dir) + apm_yml = Path(tmp_dir) / "apm.yml" + apm_yml.write_text("name: myproject\nversion: '0.1'\n") + with ( + patch("apm_cli.commands.config.get_version", return_value="1.2.3"), + patch( + "apm_cli.commands.config._load_apm_config", + return_value={ + "name": "myproject", + "version": "0.1", + "entrypoint": "main.md", + }, + ), + ): + result = self.runner.invoke(config, []) + assert result.exit_code == 0 + + def test_config_show_inside_project_with_compilation(self): + """Show config when apm.yml has compilation settings.""" + with tempfile.TemporaryDirectory() as tmp_dir: + os.chdir(tmp_dir) + apm_yml = Path(tmp_dir) / "apm.yml" + apm_yml.write_text("name: myproject\ncompilation:\n output: AGENTS.md\n") + apm_config = { + "name": "myproject", + "version": "0.1", + "compilation": { + "output": "AGENTS.md", + "chatmode": "copilot", + "resolve_links": False, + }, + "dependencies": {"mcp": ["server1", "server2"]}, + } + with ( + patch("apm_cli.commands.config.get_version", return_value="1.2.3"), + patch( + "apm_cli.commands.config._load_apm_config", return_value=apm_config + ), + ): + result = self.runner.invoke(config, []) + assert result.exit_code == 0 + + def test_config_show_rich_import_error_fallback(self): + """Fallback plain-text display when Rich (rich.table.Table) is unavailable.""" + import rich.table + + mock_table_cls = MagicMock(side_effect=ImportError("no rich")) + with tempfile.TemporaryDirectory() as tmp_dir: + os.chdir(tmp_dir) + with ( + patch("apm_cli.commands.config.get_version", return_value="0.9.0"), + patch.object(rich.table, "Table", side_effect=ImportError("no rich")), + ): + result = self.runner.invoke(config, []) + assert result.exit_code == 0 + + def test_config_show_fallback_inside_project(self): + """Fallback display inside a project directory when console/table unavailable.""" + import rich.table + + with tempfile.TemporaryDirectory() as tmp_dir: + os.chdir(tmp_dir) + apm_yml = Path(tmp_dir) / "apm.yml" + apm_yml.write_text("name: proj\n") + apm_config = { + "name": "proj", + "version": "1.0", + "entrypoint": None, + "dependencies": {"mcp": []}, + } + with ( + patch("apm_cli.commands.config.get_version", return_value="0.9.0"), + patch( + "apm_cli.commands.config._load_apm_config", return_value=apm_config + ), + patch.object(rich.table, "Table", side_effect=ImportError("no rich")), + ): + result = self.runner.invoke(config, []) + assert result.exit_code == 0 + + +class TestConfigSet: + """Tests for `apm config set `.""" + + def setup_method(self): + self.runner = CliRunner() + + def test_set_auto_integrate_true(self): + """Enable auto-integration.""" + with ( + patch("apm_cli.config.set_auto_integrate") as mock_set, + patch("apm_cli.commands.config._rich_success"), + ): + result = self.runner.invoke(config, ["set", "auto-integrate", "true"]) + assert result.exit_code == 0 + mock_set.assert_called_once_with(True) + + def test_set_auto_integrate_yes(self): + """Enable auto-integration with 'yes' alias.""" + with ( + patch("apm_cli.config.set_auto_integrate") as mock_set, + patch("apm_cli.commands.config._rich_success"), + ): + result = self.runner.invoke(config, ["set", "auto-integrate", "yes"]) + assert result.exit_code == 0 + mock_set.assert_called_once_with(True) + + def test_set_auto_integrate_one(self): + """Enable auto-integration with '1' alias.""" + with ( + patch("apm_cli.config.set_auto_integrate") as mock_set, + patch("apm_cli.commands.config._rich_success"), + ): + result = self.runner.invoke(config, ["set", "auto-integrate", "1"]) + assert result.exit_code == 0 + mock_set.assert_called_once_with(True) + + def test_set_auto_integrate_false(self): + """Disable auto-integration.""" + with ( + patch("apm_cli.config.set_auto_integrate") as mock_set, + patch("apm_cli.commands.config._rich_success"), + ): + result = self.runner.invoke(config, ["set", "auto-integrate", "false"]) + assert result.exit_code == 0 + mock_set.assert_called_once_with(False) + + def test_set_auto_integrate_no(self): + """Disable auto-integration with 'no' alias.""" + with ( + patch("apm_cli.config.set_auto_integrate") as mock_set, + patch("apm_cli.commands.config._rich_success"), + ): + result = self.runner.invoke(config, ["set", "auto-integrate", "no"]) + assert result.exit_code == 0 + mock_set.assert_called_once_with(False) + + def test_set_auto_integrate_zero(self): + """Disable auto-integration with '0' alias.""" + with ( + patch("apm_cli.config.set_auto_integrate") as mock_set, + patch("apm_cli.commands.config._rich_success"), + ): + result = self.runner.invoke(config, ["set", "auto-integrate", "0"]) + assert result.exit_code == 0 + mock_set.assert_called_once_with(False) + + def test_set_auto_integrate_invalid_value(self): + """Reject an invalid value for auto-integrate.""" + with patch("apm_cli.commands.config._rich_error"): + result = self.runner.invoke(config, ["set", "auto-integrate", "maybe"]) + assert result.exit_code == 1 + + def test_set_unknown_key(self): + """Reject an unknown configuration key.""" + with ( + patch("apm_cli.commands.config._rich_error"), + patch("apm_cli.commands.config._rich_info"), + ): + result = self.runner.invoke(config, ["set", "nonexistent", "value"]) + assert result.exit_code == 1 + + def test_set_auto_integrate_case_insensitive(self): + """Value comparison is case-insensitive.""" + with ( + patch("apm_cli.config.set_auto_integrate") as mock_set, + patch("apm_cli.commands.config._rich_success"), + ): + result = self.runner.invoke(config, ["set", "auto-integrate", "TRUE"]) + assert result.exit_code == 0 + mock_set.assert_called_once_with(True) + + +class TestConfigGet: + """Tests for `apm config get [key]`.""" + + def setup_method(self): + self.runner = CliRunner() + + def test_get_auto_integrate(self): + """Get the auto-integrate setting.""" + with patch("apm_cli.config.get_auto_integrate", return_value=True): + result = self.runner.invoke(config, ["get", "auto-integrate"]) + assert result.exit_code == 0 + assert "auto-integrate: True" in result.output + + def test_get_auto_integrate_disabled(self): + """Get auto-integrate when disabled.""" + with patch("apm_cli.config.get_auto_integrate", return_value=False): + result = self.runner.invoke(config, ["get", "auto-integrate"]) + assert result.exit_code == 0 + assert "auto-integrate: False" in result.output + + def test_get_unknown_key(self): + """Reject an unknown key.""" + with ( + patch("apm_cli.commands.config._rich_error"), + patch("apm_cli.commands.config._rich_info"), + ): + result = self.runner.invoke(config, ["get", "nonexistent"]) + assert result.exit_code == 1 + + def test_get_all_config(self): + """Show all config when no key is provided.""" + fake_config = {"auto_integrate": True, "default_client": "vscode"} + with ( + patch("apm_cli.config.get_config", return_value=fake_config), + patch("apm_cli.commands.config._rich_info"), + ): + result = self.runner.invoke(config, ["get"]) + assert result.exit_code == 0 + assert "auto-integrate: True" in result.output + + def test_get_all_config_unknown_key_passthrough(self): + """Unknown config keys are shown as-is.""" + fake_config = {"some_other_key": "value"} + with ( + patch("apm_cli.config.get_config", return_value=fake_config), + patch("apm_cli.commands.config._rich_info"), + ): + result = self.runner.invoke(config, ["get"]) + assert result.exit_code == 0 + assert "some_other_key: value" in result.output + + +class TestAutoIntegrateFunctions: + """Tests for get_auto_integrate and set_auto_integrate in apm_cli.config.""" + + def test_get_auto_integrate_default(self): + """Default value is True when not set.""" + import apm_cli.config as cfg_module + + with patch.object(cfg_module, "get_config", return_value={}): + assert cfg_module.get_auto_integrate() is True + + def test_get_auto_integrate_false(self): + """Returns False when set to False.""" + import apm_cli.config as cfg_module + + with patch.object( + cfg_module, "get_config", return_value={"auto_integrate": False} + ): + assert cfg_module.get_auto_integrate() is False + + def test_set_auto_integrate_calls_update_config(self): + """set_auto_integrate delegates to update_config.""" + import apm_cli.config as cfg_module + + with patch.object(cfg_module, "update_config") as mock_update: + cfg_module.set_auto_integrate(True) + mock_update.assert_called_once_with({"auto_integrate": True}) + + def test_set_auto_integrate_false_calls_update_config(self): + """set_auto_integrate(False) passes False to update_config.""" + import apm_cli.config as cfg_module + + with patch.object(cfg_module, "update_config") as mock_update: + cfg_module.set_auto_integrate(False) + mock_update.assert_called_once_with({"auto_integrate": False})