fix: scope MCP configuration to Codex and stabilize Windows launchers
This commit is contained in:
1 parent
18d17da966
commit
c96935b47b
13 files changed
+290
-1125
No files matched your search
+37
-291
@@ -14,6 +14,8 @@
|
||||
|
||||
"""Unit tests for ARTEMIS Unified CLI application."""
|
||||
|
||||
import pytest
|
||||
|
||||
from typer.testing import CliRunner
|
||||
from artemis.interfaces.cli.main import app
|
||||
|
||||
@@ -169,56 +171,41 @@ def test_cli_mcp_help():
|
||||
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 current Antigravity uses only documented, load-safe fields."""
|
||||
from artemis.interfaces.cli.commands.mcp import _get_config_snippet
|
||||
|
||||
result = runner.invoke(app, ["mcp", "--generate-config", "antigravity"])
|
||||
assert result.exit_code == 0
|
||||
assert "mcpServers" in result.output
|
||||
server_config = _get_config_snippet("antigravity", "python", "/project")["mcpServers"][
|
||||
"artemis"
|
||||
]
|
||||
assert server_config["disabledTools"] == []
|
||||
assert "tools" not in server_config
|
||||
|
||||
legacy_config = _get_config_snippet("jetski", "python", "/project")["mcpServers"]["artemis"]
|
||||
assert set(legacy_config["tools"]) == {
|
||||
"mobile_run_task",
|
||||
"mobile_manage_task",
|
||||
"mobile_get_device_state",
|
||||
"mobile_inspect_trace",
|
||||
"mobile_diagnose",
|
||||
"mobile_workspace",
|
||||
"mobile_config",
|
||||
"mobile_task_file",
|
||||
"mobile_batch",
|
||||
"mobile_search_history",
|
||||
"mobile_export_report",
|
||||
}
|
||||
assert all(tool["eager"] is True for tool in legacy_config["tools"].values())
|
||||
|
||||
|
||||
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
|
||||
@pytest.mark.parametrize("option", ["--install", "--generate-config"])
|
||||
@pytest.mark.parametrize(
|
||||
"client",
|
||||
[
|
||||
"cursor",
|
||||
"vscode",
|
||||
"claude",
|
||||
"claude_code",
|
||||
"claude_desktop",
|
||||
"antigravity",
|
||||
"jetski",
|
||||
"windsurf",
|
||||
"cline",
|
||||
"roo",
|
||||
"roo_code",
|
||||
"openclaw",
|
||||
"all",
|
||||
],
|
||||
)
|
||||
def test_cli_mcp_rejects_other_clients_without_modifying_files(
|
||||
tmp_path, monkeypatch, option, client
|
||||
):
|
||||
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
|
||||
monkeypatch.setenv("CODEX_HOME", str(tmp_path / ".codex"))
|
||||
monkeypatch.setenv("APPDATA", str(tmp_path / "AppData/Roaming"))
|
||||
monkeypatch.setattr("mcp_server.utils.env_utils.get_project_root", lambda: str(tmp_path))
|
||||
sentinel = tmp_path / ".cursor/mcp.json"
|
||||
sentinel.parent.mkdir()
|
||||
sentinel.write_bytes(b"{invalid json with user data}")
|
||||
before = {p.relative_to(tmp_path): p.read_bytes() for p in tmp_path.rglob("*") if p.is_file()}
|
||||
result = runner.invoke(app, ["mcp", option, client])
|
||||
assert result.exit_code == 1
|
||||
assert "Codex" in result.output
|
||||
after = {p.relative_to(tmp_path): p.read_bytes() for p in tmp_path.rglob("*") if p.is_file()}
|
||||
assert after == before
|
||||
|
||||
|
||||
def test_cli_mcp_generate_config_codex():
|
||||
@@ -251,165 +238,6 @@ def test_cli_mcp_generate_config_codex():
|
||||
]
|
||||
|
||||
|
||||
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"]
|
||||
|
||||
current_file = tmp_path / ".gemini" / "config" / "mcp_config.json"
|
||||
current_data = json.loads(current_file.read_text())
|
||||
assert current_data["mcpServers"]["artemis"]["disabledTools"] == []
|
||||
assert "tools" not in current_data["mcpServers"]["artemis"]
|
||||
|
||||
# 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.setenv("APPDATA", str(tmp_path / "AppData" / "Roaming"))
|
||||
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()
|
||||
|
||||
import json
|
||||
|
||||
cursor_data = json.loads((tmp_path / ".cursor" / "mcp.json").read_text())
|
||||
assert cursor_data["mcpServers"]["artemis"]["command"]
|
||||
|
||||
windsurf_data = json.loads((tmp_path / ".codeium" / "windsurf" / "mcp_config.json").read_text())
|
||||
assert windsurf_data["mcpServers"]["artemis"]["command"]
|
||||
|
||||
claude_data = json.loads((tmp_path / ".claude.json").read_text())
|
||||
assert claude_data["mcpServers"]["artemis"]["type"] == "stdio"
|
||||
|
||||
from artemis.interfaces.cli.commands.mcp import _get_vscode_user_dir
|
||||
|
||||
vscode_data = json.loads((_get_vscode_user_dir() / "mcp.json").read_text())
|
||||
assert vscode_data["servers"]["artemis"]["type"] == "stdio"
|
||||
assert "mcpServers" not in vscode_data
|
||||
|
||||
copilot_data = json.loads((tmp_path / ".copilot" / "mcp-config.json").read_text())
|
||||
assert copilot_data["servers"]["artemis"]["type"] == "stdio"
|
||||
|
||||
openclaw_data = json.loads((tmp_path / ".openclaw" / "openclaw.json").read_text())
|
||||
assert openclaw_data["mcp"]["servers"]["artemis"]["enabled"] is True
|
||||
|
||||
cline_data = json.loads(
|
||||
(tmp_path / ".cline" / "data" / "settings" / "cline_mcp_settings.json").read_text()
|
||||
)
|
||||
assert cline_data["mcpServers"]["artemis"]["disabled"] is False
|
||||
|
||||
roo_data = json.loads((tmp_path / ".roo" / "mcp.json").read_text())
|
||||
assert roo_data["mcpServers"]["artemis"]["disabled"] is False
|
||||
|
||||
# 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()
|
||||
# Claude Code loads both CLAUDE.md and rules/*.md, so rules are installed
|
||||
# only as the standalone rule file to avoid duplicated context.
|
||||
assert not (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_claude_migrates_legacy_claude_md(tmp_path, monkeypatch):
|
||||
"""Verify the claude target removes a previously injected CLAUDE.md block while preserving user content."""
|
||||
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
|
||||
monkeypatch.setenv("APPDATA", str(tmp_path / "AppData" / "Roaming"))
|
||||
monkeypatch.setattr("mcp_server.utils.env_utils.get_project_root", lambda: str(tmp_path))
|
||||
|
||||
claude_dir = tmp_path / ".claude"
|
||||
claude_dir.mkdir(parents=True)
|
||||
claude_md = claude_dir / "CLAUDE.md"
|
||||
claude_md.write_text(
|
||||
"# My own instructions\n\n"
|
||||
"<!-- BEGIN ARTEMIS MOBILE TESTING RULES -->\nold injected rules\n"
|
||||
"<!-- END ARTEMIS MOBILE TESTING RULES -->\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = runner.invoke(app, ["mcp", "--install", "claude"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
remaining = claude_md.read_text(encoding="utf-8")
|
||||
assert "My own instructions" in remaining
|
||||
assert "ARTEMIS MOBILE TESTING RULES" not in remaining
|
||||
rule_file = claude_dir / "rules" / "artemis.md"
|
||||
assert rule_file.exists()
|
||||
assert "Mobile Testing Mindset" in rule_file.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_cli_mcp_install_claude_deletes_block_only_claude_md(tmp_path, monkeypatch):
|
||||
"""Verify a CLAUDE.md consisting solely of the injected block is deleted outright."""
|
||||
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
|
||||
monkeypatch.setenv("APPDATA", str(tmp_path / "AppData" / "Roaming"))
|
||||
monkeypatch.setattr("mcp_server.utils.env_utils.get_project_root", lambda: str(tmp_path))
|
||||
|
||||
claude_dir = tmp_path / ".claude"
|
||||
claude_dir.mkdir(parents=True)
|
||||
claude_md = claude_dir / "CLAUDE.md"
|
||||
claude_md.write_text(
|
||||
"<!-- BEGIN ARTEMIS MOBILE TESTING RULES -->\nold injected rules\n"
|
||||
"<!-- END ARTEMIS MOBILE TESTING RULES -->\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = runner.invoke(app, ["mcp", "--install", "claude"])
|
||||
assert result.exit_code == 0
|
||||
assert not claude_md.exists()
|
||||
assert (claude_dir / "rules" / "artemis.md").exists()
|
||||
|
||||
|
||||
def test_cli_mcp_install_claude_refuses_unparseable_claude_json(tmp_path, monkeypatch):
|
||||
"""Verify an unparseable ~/.claude.json is left untouched instead of being rewritten."""
|
||||
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
|
||||
monkeypatch.setenv("APPDATA", str(tmp_path / "AppData" / "Roaming"))
|
||||
monkeypatch.setattr("mcp_server.utils.env_utils.get_project_root", lambda: str(tmp_path))
|
||||
|
||||
corrupt = "{ this is not json at all"
|
||||
claude_json = tmp_path / ".claude.json"
|
||||
claude_json.write_text(corrupt, encoding="utf-8")
|
||||
|
||||
result = runner.invoke(app, ["mcp", "--install", "claude"])
|
||||
assert result.exit_code == 0
|
||||
assert claude_json.read_text(encoding="utf-8") == corrupt
|
||||
|
||||
|
||||
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)
|
||||
@@ -461,88 +289,6 @@ def test_cli_mcp_install_codex_preserves_config_and_is_idempotent(tmp_path, monk
|
||||
assert agents_text.count("<!-- BEGIN ARTEMIS MOBILE TESTING RULES -->") == 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")
|
||||
|
||||
|
||||
def test_cli_mcp_install_refuses_rewrite_when_backup_fails(tmp_path, monkeypatch):
|
||||
"""Backup failure for an unparseable config must abort the rewrite instead of destroying it."""
|
||||
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"
|
||||
original = "INVALID JSON DATA {{{{"
|
||||
mcp_json.write_text(original, encoding="utf-8")
|
||||
|
||||
import pathlib
|
||||
|
||||
real_write_text = pathlib.Path.write_text
|
||||
|
||||
def failing_bak_write(self, *args, **kwargs):
|
||||
if str(self).endswith(".bak"):
|
||||
raise OSError("disk full")
|
||||
return real_write_text(self, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr("pathlib.Path.write_text", failing_bak_write)
|
||||
|
||||
runner.invoke(app, ["mcp", "--install", "cursor"])
|
||||
|
||||
assert mcp_json.read_text(encoding="utf-8") == original
|
||||
assert not (cursor_dir / "mcp.json.bak").exists()
|
||||
|
||||
|
||||
def test_cli_mcp_install_openclaw_migrates_legacy_plugin_shape(tmp_path, monkeypatch):
|
||||
"""Verify reinstalling OpenClaw replaces the obsolete plugin wrapper with mcp.servers."""
|
||||
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
|
||||
monkeypatch.setattr("mcp_server.utils.env_utils.get_project_root", lambda: str(tmp_path))
|
||||
config_path = tmp_path / ".openclaw" / "openclaw.json"
|
||||
config_path.parent.mkdir(parents=True)
|
||||
config_path.write_text(
|
||||
'{"plugins":{"artemis_mcp":{"enabled":true},"keep":{"enabled":true}}}',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = runner.invoke(app, ["mcp", "--install", "openclaw"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
import json
|
||||
|
||||
data = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
assert "artemis_mcp" not in data["plugins"]
|
||||
assert data["plugins"]["keep"]["enabled"] is True
|
||||
assert data["mcp"]["servers"]["artemis"]["enabled"] is True
|
||||
assert data["mcp"]["servers"]["artemis"]["command"]
|
||||
|
||||
|
||||
def test_cli_restart_help():
|
||||
"""Verify 'artemis restart --help' displays lifecycle options."""
|
||||
result = runner.invoke(app, ["restart", "--help"])
|
||||
|
||||
@@ -120,33 +120,7 @@ command = "leave-alone"
|
||||
assert path.with_name(path.name + ".artemis-update.bak").read_text(encoding="utf-8") == original
|
||||
|
||||
|
||||
def test_json_migrates_existing_standard_mcp_to_current_checkout(tmp_path):
|
||||
path = tmp_path / "mcp.json"
|
||||
root = tmp_path / "project"
|
||||
config = {
|
||||
"cwd": str(root),
|
||||
"command": "old",
|
||||
"args": ["-m", "mcp_server"],
|
||||
"disabled": True,
|
||||
"env": {"CUSTOM": "preserved"},
|
||||
"autoApprove": [],
|
||||
}
|
||||
path.write_text(json.dumps({"mcpServers": {"artemis": config, "other": {"command": "keep"}}}))
|
||||
moved_root = tmp_path / "new release"
|
||||
assert update.refresh_json(path, ("mcpServers",), moved_root, "new", ("mobile_batch",))
|
||||
result = json.loads(path.read_text())["mcpServers"]
|
||||
assert result["artemis"]["env"]["CUSTOM"] == "preserved"
|
||||
assert result["artemis"]["env"]["PYTHONPATH"] == str(moved_root)
|
||||
assert result["artemis"]["cwd"] == str(moved_root)
|
||||
assert result["artemis"]["disabled"] is True
|
||||
assert result["other"] == {"command": "keep"}
|
||||
assert not update.refresh_json(path, ("mcpServers",), moved_root, "new", ("mobile_batch",))
|
||||
missing = tmp_path / "not-installed.json"
|
||||
assert not update.refresh_json(missing, ("mcpServers",), root, "new", ())
|
||||
assert not missing.exists()
|
||||
|
||||
|
||||
def test_mcp_sync_installs_detected_clients_and_migrates_codex(tmp_path, monkeypatch):
|
||||
def test_mcp_sync_updates_only_codex_and_preserves_other_clients(tmp_path, monkeypatch):
|
||||
from artemis.interfaces.cli.commands import mcp as mcp_command
|
||||
|
||||
home = tmp_path / "home"
|
||||
@@ -170,8 +144,11 @@ def test_mcp_sync_installs_detected_clients_and_migrates_codex(tmp_path, monkeyp
|
||||
cursor_home = home / ".cursor"
|
||||
cursor_home.mkdir()
|
||||
(cursor_home / "mcp.json").write_text('{"mcpServers":{"other":{"command":"keep"}}}')
|
||||
vscode_home = home / "AppData/Code/User"
|
||||
vscode_home.mkdir(parents=True)
|
||||
vscode_config = vscode_home / "mcp.json"
|
||||
vscode_config.write_bytes(b"{invalid VS Code JSONC}")
|
||||
monkeypatch.setattr("pathlib.Path.home", lambda: home)
|
||||
monkeypatch.setattr(mcp_command, "_get_vscode_user_dir", lambda: home / "vscode-user")
|
||||
monkeypatch.setenv("CODEX_HOME", str(codex_home))
|
||||
monkeypatch.setenv("APPDATA", str(home / "AppData"))
|
||||
root = tmp_path / "new release"
|
||||
@@ -187,26 +164,31 @@ def test_mcp_sync_installs_detected_clients_and_migrates_codex(tmp_path, monkeyp
|
||||
assert entry["env"]["PYTHONPATH"] == str(root)
|
||||
assert entry["enabled_tools"] == list(mcp_command.ARTEMIS_MCP_TOOLS)
|
||||
cursor = json.loads((cursor_home / "mcp.json").read_text(encoding="utf-8"))
|
||||
assert cursor["mcpServers"]["artemis"]["env"]["PYTHONPATH"] == str(root)
|
||||
assert "artemis" not in cursor["mcpServers"]
|
||||
assert cursor["mcpServers"]["other"] == {"command": "keep"}
|
||||
assert vscode_config.read_bytes() == b"{invalid VS Code JSONC}"
|
||||
assert not vscode_config.with_name("mcp.json.artemis-update.bak").exists()
|
||||
first = codex_config.read_text(encoding="utf-8")
|
||||
assert update.refresh_mcp(root) == 0
|
||||
assert codex_config.read_text(encoding="utf-8") == first
|
||||
|
||||
|
||||
def test_mcp_sync_fails_on_invalid_detected_config_without_overwriting(tmp_path, monkeypatch):
|
||||
from artemis.interfaces.cli.commands import mcp as mcp_command
|
||||
|
||||
def test_mcp_sync_fails_on_invalid_codex_config_without_overwriting(tmp_path, monkeypatch, capsys):
|
||||
home = tmp_path / "home"
|
||||
cursor_home = home / ".cursor"
|
||||
cursor_home.mkdir(parents=True)
|
||||
config = cursor_home / "mcp.json"
|
||||
config.write_text("{invalid json", encoding="utf-8")
|
||||
codex_home = home / ".codex"
|
||||
codex_home.mkdir(parents=True)
|
||||
config = codex_home / "config.toml"
|
||||
original = b'model = "keep-secret"\n[mcp_servers.artemis\n'
|
||||
config.write_bytes(original)
|
||||
monkeypatch.setattr("pathlib.Path.home", lambda: home)
|
||||
monkeypatch.setattr(mcp_command, "_get_vscode_user_dir", lambda: home / "vscode-user")
|
||||
monkeypatch.delenv("CODEX_HOME", raising=False)
|
||||
monkeypatch.setenv("CODEX_HOME", str(codex_home))
|
||||
assert update.refresh_mcp(tmp_path / "project") == 1
|
||||
assert config.read_text(encoding="utf-8") == "{invalid json"
|
||||
assert config.read_bytes() == original
|
||||
assert not config.with_name(config.name + ".artemis-update.bak").exists()
|
||||
output = capsys.readouterr().out
|
||||
assert str(config) in output
|
||||
assert "Invalid TOML" in output
|
||||
assert "keep-secret" not in output
|
||||
|
||||
|
||||
def test_mcp_sync_installs_codex_when_client_directory_exists(tmp_path, monkeypatch):
|
||||
@@ -216,7 +198,6 @@ def test_mcp_sync_installs_codex_when_client_directory_exists(tmp_path, monkeypa
|
||||
codex_home = home / ".codex"
|
||||
codex_home.mkdir(parents=True)
|
||||
monkeypatch.setattr("pathlib.Path.home", lambda: home)
|
||||
monkeypatch.setattr(mcp_command, "_get_vscode_user_dir", lambda: home / "vscode-user")
|
||||
monkeypatch.setenv("CODEX_HOME", str(codex_home))
|
||||
root = tmp_path / "project"
|
||||
assert update.refresh_mcp(root) == 0
|
||||
@@ -225,6 +206,59 @@ def test_mcp_sync_installs_codex_when_client_directory_exists(tmp_path, monkeypa
|
||||
assert config["mcp_servers"]["artemis"]["enabled_tools"] == list(mcp_command.ARTEMIS_MCP_TOOLS)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("codex_home_setting", [None, "", "custom"])
|
||||
def test_codex_sync_reports_actual_path_and_ready_state(
|
||||
tmp_path, monkeypatch, capsys, codex_home_setting
|
||||
):
|
||||
home = tmp_path / "home"
|
||||
codex_home = home / ".codex" if codex_home_setting != "custom" else tmp_path / "custom Codex"
|
||||
codex_home.mkdir(parents=True)
|
||||
monkeypatch.setattr("pathlib.Path.home", lambda: home)
|
||||
monkeypatch.setenv("ARTEMIS_LAUNCH_LANG", "zh-CN")
|
||||
if codex_home_setting is None:
|
||||
monkeypatch.delenv("CODEX_HOME", raising=False)
|
||||
else:
|
||||
monkeypatch.setenv("CODEX_HOME", str(codex_home) if codex_home_setting == "custom" else "")
|
||||
root = tmp_path / "project"
|
||||
assert update.refresh_mcp(root) == 0
|
||||
first = capsys.readouterr().out
|
||||
assert str(codex_home / "config.toml") in first
|
||||
assert "已安装" in first
|
||||
assert update.refresh_mcp(root) == 0
|
||||
second = capsys.readouterr().out
|
||||
assert str(codex_home / "config.toml") in second
|
||||
assert "已是最新" in second
|
||||
if codex_home_setting == "custom":
|
||||
assert not (home / ".codex").exists()
|
||||
|
||||
|
||||
def test_codex_sync_reports_missing_directory_without_touching_other_clients(
|
||||
tmp_path, monkeypatch, capsys
|
||||
):
|
||||
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
|
||||
monkeypatch.delenv("CODEX_HOME", raising=False)
|
||||
monkeypatch.setenv("ARTEMIS_LAUNCH_LANG", "zh-CN")
|
||||
assert update.refresh_mcp(tmp_path / "project") == 0
|
||||
output = capsys.readouterr().out
|
||||
assert str(tmp_path / ".codex/config.toml") in output
|
||||
assert "未发现 Codex 配置目录" in output
|
||||
assert "--install codex" in output
|
||||
assert not (tmp_path / ".codex").exists()
|
||||
|
||||
|
||||
def test_custom_codex_command_is_preserved_and_reported(tmp_path, monkeypatch, capsys):
|
||||
codex_home = tmp_path / ".codex"
|
||||
codex_home.mkdir()
|
||||
config = codex_home / "config.toml"
|
||||
original = '[mcp_servers.artemis]\ncommand = "custom"\nargs = ["wrapper"]\n'
|
||||
config.write_text(original, encoding="utf-8")
|
||||
monkeypatch.setenv("CODEX_HOME", str(codex_home))
|
||||
monkeypatch.setenv("ARTEMIS_LAUNCH_LANG", "zh-CN")
|
||||
assert update.refresh_mcp(tmp_path / "project") == 0
|
||||
assert config.read_text(encoding="utf-8") == original
|
||||
assert "自定义" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_codex_install_preserves_existing_user_tables(tmp_path):
|
||||
path = tmp_path / "config.toml"
|
||||
path.write_text(
|
||||
@@ -277,25 +311,6 @@ def test_codex_inline_environment_sync_is_idempotent(tmp_path):
|
||||
)
|
||||
|
||||
|
||||
def test_custom_mcp_wrapper_is_left_unchanged(tmp_path):
|
||||
path = tmp_path / "mcp.json"
|
||||
original = '{"mcpServers":{"artemis":{"command":"uv","args":["run","custom"]}}}'
|
||||
path.write_text(original, encoding="utf-8")
|
||||
assert (
|
||||
update.refresh_json(
|
||||
path,
|
||||
("mcpServers",),
|
||||
tmp_path,
|
||||
"new-python",
|
||||
("mobile_run_task",),
|
||||
install_missing=True,
|
||||
template={"command": "new-python", "args": ["-m", "mcp_server"], "env": {}},
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert path.read_text(encoding="utf-8") == original
|
||||
|
||||
|
||||
def test_frontend_fingerprint_detects_lock_source_and_deleted_assets(tmp_path):
|
||||
ui = tmp_path / "apps/showcase_ui"
|
||||
(ui / "src").mkdir(parents=True)
|
||||
|
||||
@@ -132,3 +132,23 @@ def test_packaging_rejects_uncommitted_changes_and_credentials(repository, tmp_p
|
||||
)
|
||||
with pytest.raises(ValueError, match="credential-free HTTPS"):
|
||||
module.package_source(root, tmp_path / "source.zip")
|
||||
|
||||
|
||||
def test_archive_checks_out_chinese_batch_with_crlf_and_clean_git(repository, tmp_path):
|
||||
root, _, _, _ = repository
|
||||
project_root = Path(__file__).resolve().parents[2]
|
||||
(root / ".gitattributes").write_bytes((project_root / ".gitattributes").read_bytes())
|
||||
# The Git blob uses LF; the archive must contain the CRLF working-tree form.
|
||||
content = (project_root / "start-cn.bat").read_bytes().replace(b"\r\n", b"\n")
|
||||
(root / "start-cn.bat").write_bytes(content)
|
||||
git(root, "add", "--", ".gitattributes", "start-cn.bat")
|
||||
git(root, "commit", "-m", "Windows launcher attributes")
|
||||
archive_path = tmp_path / "source.zip"
|
||||
module.package_source(root, archive_path)
|
||||
with zipfile.ZipFile(archive_path) as archive:
|
||||
packed = archive.read("artemis/start-cn.bat")
|
||||
assert b"\r\n" in packed
|
||||
assert b"\n" not in packed.replace(b"\r\n", b"")
|
||||
assert packed.decode("utf-8").startswith("@echo off\r\n")
|
||||
archive.extractall(tmp_path / "unpacked")
|
||||
assert git(tmp_path / "unpacked/artemis", "status", "--porcelain") == ""
|
||||
@@ -146,3 +146,30 @@ def test_batch_preserves_exit_code_and_handles_space_and_exclamation_paths(tmp_p
|
||||
timeout=30,
|
||||
)
|
||||
assert result.returncode == exit_code, result.stdout + result.stderr
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exit_code", [0, 7])
|
||||
def test_chinese_batch_prints_complete_lines_and_preserves_exit_code(tmp_path, exit_code):
|
||||
root = tmp_path / "source with spaces!"
|
||||
root.mkdir()
|
||||
shutil.copy2(ROOT / "start-cn.bat", root / "start-cn.bat")
|
||||
(root / "start.bat").write_bytes(
|
||||
f'@echo off\r\nif not "%~1"=="-Mirror" exit /b 19\r\n'
|
||||
f'if not "%~2"=="China" exit /b 20\r\nexit /b {exit_code}\r\n'.encode("ascii")
|
||||
)
|
||||
result = subprocess.run(
|
||||
["cmd.exe", "/d", "/c", "call", str(root / "start-cn.bat")],
|
||||
capture_output=True,
|
||||
timeout=15,
|
||||
)
|
||||
assert result.returncode == exit_code, result.stdout + result.stderr
|
||||
assert not result.stderr, result.stderr
|
||||
assert "镜像地址和下载超时可在" in result.stdout.decode("utf-8")
|
||||
help_result = subprocess.run(
|
||||
["cmd.exe", "/d", "/c", "call", str(root / "start-cn.bat"), "-Help"],
|
||||
capture_output=True,
|
||||
timeout=15,
|
||||
)
|
||||
assert help_result.returncode == 0
|
||||
assert not help_result.stderr
|
||||
assert "Codex MCP" in help_result.stdout.decode("utf-8")
|
||||
Reference in new issue
Block a user