fix: scope MCP configuration to Codex and stabilize Windows launchers

This commit is contained in:
chenmingxuan committed 2026-10-08 18:01:15 +08:00
1 parent 18d17da966
commit c96935b47b
13 files changed
+290 -1125

No files matched your search

+37 -291
View File
@@ -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"])
+74 -59
View File
@@ -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)
+20
View File
@@ -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") == ""
+27
View File
@@ -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")