# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Unit tests for ARTEMIS Unified CLI application.""" from typer.testing import CliRunner from artemis.interfaces.cli.main import app runner = CliRunner() def test_cli_help(): """Verify top-level CLI help returns status 0 and lists core subcommands.""" result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 assert "Artemis: Autonomous Multimodal Android Agent" in result.output assert "run" in result.output assert "batch" in result.output assert "server" in result.output assert "trace" in result.output assert "mcp" in result.output def test_cli_version(): """Verify --version returns version banner.""" result = runner.invoke(app, ["--version"]) assert result.exit_code == 0 assert "Artemis Agent Platform" in result.output def test_cli_run_help(): """Verify 'artemis run --help' displays execution options.""" result = runner.invoke(app, ["run", "--help"]) assert result.exit_code == 0 assert "--profile" in result.output assert "--locked-app" in result.output assert "--traces-path" in result.output def test_cli_batch_help(): """Verify 'artemis batch --help' displays batch options.""" result = runner.invoke(app, ["batch", "--help"]) assert result.exit_code == 0 assert "--file" in result.output assert "--delay" in result.output def test_cli_trace_help(): """Verify 'artemis trace --help' lists trace subcommands.""" result = runner.invoke(app, ["trace", "--help"]) assert result.exit_code == 0 assert "list" in result.output assert "view" in result.output def test_cli_mcp_help(): """Verify 'artemis mcp --help' lists server options.""" result = runner.invoke(app, ["mcp", "--help"]) assert result.exit_code == 0 assert "--type" in result.output assert "--generate-config" in result.output def test_cli_mcp_generate_config(): """Verify 'artemis mcp --generate-config cursor' produces valid configuration.""" result = runner.invoke(app, ["mcp", "--generate-config", "cursor"]) assert result.exit_code == 0 assert "mcpServers" in result.output assert "mcp_server" in result.output def test_cli_mcp_generate_config_antigravity(): """Verify 'artemis mcp --generate-config antigravity' produces valid configuration with tools.""" result = runner.invoke(app, ["mcp", "--generate-config", "antigravity"]) assert result.exit_code == 0 assert "mcpServers" in result.output assert "mobile_run_task" in result.output assert "eager" in result.output def test_cli_mcp_generate_config_all(): """Verify 'artemis mcp --generate-config all' includes every supported client.""" result = runner.invoke(app, ["mcp", "--generate-config", "all"]) assert result.exit_code == 0 assert "antigravity" in result.output assert "cursor" in result.output assert "windsurf" in result.output assert "claude" in result.output assert "vscode" in result.output assert "cline" in result.output assert "roo" in result.output assert "codex" in result.output def test_cli_mcp_generate_config_codex(): """Verify Codex generation produces a ready-to-use TOML server block.""" result = runner.invoke(app, ["mcp", "--generate-config", "codex"]) assert result.exit_code == 0 assert "[mcp_servers.artemis]" in result.output assert "[mcp_servers.artemis.env]" in result.output def test_cli_mcp_install_antigravity(tmp_path, monkeypatch): """Verify 'artemis mcp --install antigravity' writes configuration and global rules into target files.""" monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path) monkeypatch.setattr("mcp_server.utils.env_utils.get_project_root", lambda: str(tmp_path)) result = runner.invoke(app, ["mcp", "--install", "antigravity"]) assert result.exit_code == 0 assert "Successfully installed ARTEMIS MCP server configuration & rules" in result.output jetski_file = tmp_path / ".gemini" / "jetski" / "mcp_config.json" assert jetski_file.exists() import json data = json.loads(jetski_file.read_text()) assert "artemis" in data["mcpServers"] assert "mobile_run_task" in data["mcpServers"]["artemis"]["tools"] assert "PYTHONPATH" in data["mcpServers"]["artemis"]["env"] # Verify global rule file installed gemini_md = tmp_path / ".gemini" / "GEMINI.md" assert gemini_md.exists() assert "Mobile Testing Mindset" in gemini_md.read_text(encoding="utf-8") rule_file = tmp_path / ".gemini" / "rules" / "artemis.md" assert rule_file.exists() assert "Mobile Testing Mindset" in rule_file.read_text(encoding="utf-8") def test_cli_mcp_install_all(tmp_path, monkeypatch): """Verify 'artemis mcp --install all' installs configs and global rules to all supported IDE locations.""" monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path) monkeypatch.setattr("mcp_server.utils.env_utils.get_project_root", lambda: str(tmp_path)) result = runner.invoke(app, ["mcp", "--install", "all"]) assert result.exit_code == 0 assert "Successfully installed ARTEMIS MCP server configuration & rules" in result.output assert (tmp_path / ".cursor" / "mcp.json").exists() assert (tmp_path / ".codeium" / "windsurf" / "mcp_config.json").exists() assert (tmp_path / ".openclaw" / "openclaw.json").exists() assert (tmp_path / ".claude.json").exists() assert (tmp_path / ".codex" / "config.toml").exists() # Verify global rule files installed across IDEs assert (tmp_path / ".gemini" / "GEMINI.md").exists() assert (tmp_path / ".gemini" / "rules" / "artemis.md").exists() assert (tmp_path / ".cursorrules").exists() assert (tmp_path / ".cursor" / "rules" / "artemis.mdc").exists() assert (tmp_path / ".claude" / "CLAUDE.md").exists() assert (tmp_path / ".claude" / "rules" / "artemis.md").exists() assert (tmp_path / ".codeium" / "windsurf" / "memories" / "global_rules.md").exists() assert (tmp_path / ".codeium" / "windsurf" / "rules" / "artemis.md").exists() assert (tmp_path / ".vscode" / "rules" / "artemis.md").exists() assert (tmp_path / ".clinerules").exists() assert (tmp_path / ".cline" / "rules" / "artemis.md").exists() assert (tmp_path / ".roorules").exists() assert (tmp_path / ".roo" / "rules" / "artemis.md").exists() assert (tmp_path / ".openclaw" / "OPENCLAW.md").exists() assert (tmp_path / ".openclaw" / "rules" / "artemis.md").exists() assert (tmp_path / ".codex" / "AGENTS.md").exists() def test_cli_mcp_install_codex_preserves_config_and_is_idempotent(tmp_path, monkeypatch): """Verify Codex TOML merging preserves other servers and updates only Artemis.""" monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path) monkeypatch.setattr("mcp_server.utils.env_utils.get_project_root", lambda: str(tmp_path)) codex_dir = tmp_path / ".codex" codex_dir.mkdir(parents=True) config_file = codex_dir / "config.toml" config_file.write_text( 'model = "test-model"\n\n' '[mcp_servers.existing]\ncommand = "echo"\n\n' '[mcp_servers.artemis]\ncommand = "old-command"\n', encoding="utf-8", ) result = runner.invoke(app, ["mcp", "--install", "codex"]) assert result.exit_code == 0 result2 = runner.invoke(app, ["mcp", "--install", "codex"]) assert result2.exit_code == 0 import tomllib config_text = config_file.read_text(encoding="utf-8") data = tomllib.loads(config_text) assert data["model"] == "test-model" assert data["mcp_servers"]["existing"]["command"] == "echo" assert data["mcp_servers"]["artemis"]["args"] == ["-m", "mcp_server"] assert data["mcp_servers"]["artemis"]["env"]["PYTHONPATH"] == str(tmp_path) assert config_text.count("# BEGIN ARTEMIS MCP CONFIG") == 1 agents_file = codex_dir / "AGENTS.md" agents_text = agents_file.read_text(encoding="utf-8") assert "Mobile Testing Mindset" in agents_text assert agents_text.count("") == 1 def test_cli_mcp_install_jsonc_and_backup(tmp_path, monkeypatch): """Verify merging configuration preserves valid JSONC and creates .bak for unparseable JSON.""" monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path) monkeypatch.setattr("mcp_server.utils.env_utils.get_project_root", lambda: str(tmp_path)) cursor_dir = tmp_path / ".cursor" cursor_dir.mkdir(parents=True, exist_ok=True) mcp_json = cursor_dir / "mcp.json" # 1. Valid JSONC with comments and trailing comma mcp_json.write_text( '{\n // My existing server\n "mcpServers": {\n "test": {"command": "echo"},\n }\n}', encoding="utf-8", ) result = runner.invoke(app, ["mcp", "--install", "cursor"]) assert result.exit_code == 0 import json data = json.loads(mcp_json.read_text(encoding="utf-8")) assert "test" in data["mcpServers"] assert "artemis" in data["mcpServers"] # 2. Corrupt / unparseable file triggers backup mcp_json.write_text("INVALID JSON DATA {{{{", encoding="utf-8") result2 = runner.invoke(app, ["mcp", "--install", "cursor"]) assert result2.exit_code == 0 backup_file = cursor_dir / "mcp.json.bak" assert backup_file.exists() assert "INVALID JSON DATA" in backup_file.read_text(encoding="utf-8")