"""Plan 07.1-01 Task 2: lint + structural assertions for the LaunchAgent plist template. The template ``scripts/com.iai-mcp.daemon.plist.template`` is rendered by ``scripts/install.sh`` (Wave 2): ``{PYTHON_PATH}`` and ``{HOME}`` are substituted, then the result is written to ``~/Library/LaunchAgents/com.iai-mcp.daemon.plist`` and registered with ``launchctl load -w``. These tests guard the *template itself*: * ``test_template_renders_to_valid_plist`` — substitute the placeholders with realistic values, write to a tmp file, run ``plutil -lint``, and assert exit 0 + ``OK`` in stdout. * ``test_template_has_required_keys`` — string-level presence of every D7.1-01 field (Sockets, RunAtLoad, SockPathMode=384, KeepAlive, IAI_MCP_LAUNCHD_MANAGED). * ``test_template_does_not_have_RunAtLoad_true`` — regression trap: the legacy ``deploy/launchd/com.iai-mcp.daemon.plist`` uses ``RunAtLoad`` which defeats socket activation; we must NOT reintroduce that pattern in the new template. The whole module skips on non-Darwin hosts (``plutil`` is macOS-only). """ from __future__ import annotations import platform import re import subprocess from pathlib import Path import pytest pytestmark = pytest.mark.skipif( platform.system() != "Darwin", reason="plutil is macOS-only", ) REPO = Path(__file__).resolve().parent.parent TEMPLATE = REPO / "scripts" / "com.iai-mcp.daemon.plist.template" def test_template_renders_to_valid_plist(tmp_path: Path) -> None: """Rendered plist (post-substitution) passes plutil -lint.""" template_text = TEMPLATE.read_text() rendered = template_text.replace( "{PYTHON_PATH}", "/usr/bin/python3" ).replace("{HOME}", "/tmp/iai-fake-home") rendered_path = tmp_path / "com.iai-mcp.daemon.plist" rendered_path.write_text(rendered) result = subprocess.run( ["plutil", "-lint", str(rendered_path)], capture_output=True, text=True, check=False, ) assert result.returncode == 0, ( f"plutil -lint FAILED on rendered template:\n" f"--- STDOUT ---\n{result.stdout}\n" f"--- STDERR ---\n{result.stderr}\n" ) assert "OK" in result.stdout, result.stdout def test_template_has_required_keys() -> None: """All D7.1-01 fields present (string-level, no regex).""" text = TEMPLATE.read_text() required_markers = [ "Sockets", "RunAtLoad", "", "SockPathMode", "384", "KeepAlive", "IAI_MCP_LAUNCHD_MANAGED", ] missing = [m for m in required_markers if m not in text] assert not missing, f"template missing required markers: {missing}" def test_template_does_not_have_RunAtLoad_true() -> None: """Regression trap: legacy deploy/launchd plist's bug must NOT appear. The legacy ``deploy/launchd/com.iai-mcp.daemon.plist`` uses ``RunAtLoad`` which defeats socket activation (eager spawn at user login = no listener pre-bind). The Phase 7.1 template MUST use ```` so launchd defers spawn until the first incoming connection on the pre-bound socket. """ text = TEMPLATE.read_text() match = re.search(r"RunAtLoad\s*", text) assert match is None, ( "REGRESSION: template contains RunAtLoad... which " "defeats socket activation. Use instead." )