feat: extract in-process MCP action layer, concurrent task dispatch, and spawn/reconcile hardening
- Extract device action execution into artemis/mcp action layer: typed ActionResult, capability manifest with startup validation, AdbActuator as the single physical implementation, in-memory ActionSession (replaces stdio subprocess MCP in validator), and McpActionExecutor preserving the legacy executor contract. adb_server keeps its external stdio/SSE surface unchanged and now reuses the actuator implementation; import-time side effects moved into configure_stdio_mode(). - Capability-gated prompt assembly (prompt_assembly.py): tool enums and teaching segments render per available backend tools; full-set output is byte-identical to the previous static text (locked by tests). - Admin console task queue: serial worker replaced by a per-device concurrent dispatcher (ARTEMIS_CONCURRENCY_MODE=global restores serial semantics), per-session cancellation, strict device binding on enqueue, unified stdio piping with tee to trace stdout.log, and shutdown cleanup of all active runs. Fix _pid_exists exception order so PermissionError is treated as alive. - mcp_server reliability: spawn watchdog kills runners stuck before interpreter startup and releases device reservations; task state reconcile uses psutil liveness, prefers runner-reported pids, and adds a 45s startup grace to stop false failure reports on Windows; conversation_id is now optional end-to-end; explicit device_serial is validated at submission. - CLI install now writes ~/.claude/rules/artemis.md for Claude Code and migrates/removes the legacy CLAUDE.md injection block; ~/.claude.json is never rewritten when it fails to parse. - Tests: Windows-portable stdio lifecycle and path assertions; new suites for action layer, prompt assembly, spawn watchdog, and task reconcile (895 passed, 3 skipped).
This commit is contained in:
1 parent
60312abbb0
commit
cbdca1497c
52 files changed
+5735
-1649
No files matched your search
+66
-1
@@ -222,7 +222,9 @@ def test_cli_mcp_install_all(tmp_path, monkeypatch):
|
||||
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()
|
||||
# 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()
|
||||
@@ -236,6 +238,69 @@ def test_cli_mcp_install_all(tmp_path, monkeypatch):
|
||||
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)
|
||||
|
||||
Reference in new issue
Block a user