"""``helix plugin`` — download/verify/unpack/register contract. No network, no real ``claude`` binary: REST calls are stubbed at the ``http_client.get`` seam, the artifact download at ``_download_zip``'s httpx client (via a local zip file + patched ``_download_zip`` for the error paths, and a real in-memory zip for the happy path), and Claude registration through the injectable ``runner``. """ from __future__ import annotations import hashlib import io import json import zipfile from pathlib import Path from typing import Any import pytest from helix_cli import plugin_dist from helix_cli.config import CliConfig from helix_cli.http_client import CliRestError, RestResponse _CFG = CliConfig( base_url="https://helix.example.com", token="helix_pat_" + "x" * 32, base_url_source="test", token_source="test", ) def _zip_bytes(extra_member: tuple[str, bytes] | None = None) -> bytes: """A minimal valid marketplace zip (helix-claude-plugin/ root).""" buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as zf: zf.writestr( "helix-claude-plugin/.claude-plugin/marketplace.json", json.dumps({"name": "helix", "plugins": []}), ) zf.writestr( "helix-claude-plugin/plugins/helix/.claude-plugin/plugin.json", json.dumps({"name": "helix", "version": "1.0.0"}), ) if extra_member: zf.writestr(*extra_member) return buf.getvalue() def _envelope(data: Any) -> RestResponse: return RestResponse( status_code=200, body={"success": True, "data": data, "meta": {}, "errors": []}, raw_text="", ) @pytest.fixture def stub_rest(monkeypatch: pytest.MonkeyPatch): """Patch the REST seam; returns a dict the test mutates to steer manifest/download responses.""" state: dict[str, Any] = { "manifest": {"available": True, "version": "1.0.0+gabc12345", "plugin_version": "1.0.0"}, "download": None, # filled per-test } def _get(config, path, *, token=None, timeout=30.0): assert token == _CFG.token # PAT attaches to Helix REST calls… if path.endswith("/plugin/manifest"): return _envelope(state["manifest"]) if path.endswith("/plugin/download"): return _envelope(state["download"]) raise AssertionError(f"unexpected path {path}") monkeypatch.setattr(plugin_dist.http_client, "get", _get) return state def _stub_download(monkeypatch: pytest.MonkeyPatch, payload: bytes): """Patch the artifact fetch; asserts the presigned URL is fetched WITHOUT any auth header (it must never see the PAT).""" def _download(url: str, dest: Path, *, timeout: float): assert "helix_pat_" not in url dest.write_bytes(payload) return hashlib.sha256(payload).hexdigest(), len(payload) monkeypatch.setattr(plugin_dist, "_download_zip", _download) def _ok_runner(calls: list[list[str]]): def _run(cmd: list[str]) -> tuple[bool, str]: calls.append(cmd) return True, "" return _run def test_install_happy_path(tmp_path, stub_rest, monkeypatch, cli_env): payload = _zip_bytes() stub_rest["download"] = { "url": "https://s3.example.com/plugin-dist/x/helix-claude-plugin.zip?sig=1", "sha256": hashlib.sha256(payload).hexdigest(), "version": "1.0.0+gabc12345", "plugin_version": "1.0.0", } _stub_download(monkeypatch, payload) monkeypatch.setattr(plugin_dist.shutil, "which", lambda _: "/usr/local/bin/claude") calls: list[list[str]] = [] res = plugin_dist.install(_CFG, timeout=30.0, base=tmp_path, runner=_ok_runner(calls)) assert res.first_install is True mp = tmp_path / "helix-claude-plugin" assert (mp / ".claude-plugin" / "marketplace.json").is_file() receipt = json.loads((tmp_path / "installed.json").read_text()) assert receipt["version"] == "1.0.0+gabc12345" # First install: marketplace add + plugin install, in that order. assert [c[1:4] for c in calls] == [ ["plugin", "marketplace", "add"], ["plugin", "install", "helix@helix"], ] assert res.registration["registered"] is True def test_update_uses_marketplace_update(tmp_path, stub_rest, monkeypatch, cli_env): payload = _zip_bytes() stub_rest["download"] = { "url": "https://s3.example.com/x.zip", "sha256": hashlib.sha256(payload).hexdigest(), "version": "1.0.1+gdef67890", "plugin_version": "1.0.1", } _stub_download(monkeypatch, payload) monkeypatch.setattr(plugin_dist.shutil, "which", lambda _: "/usr/local/bin/claude") # Pre-existing receipt → not a first install. (tmp_path / "installed.json").write_text(json.dumps({"version": "1.0.0+gabc12345"})) calls: list[list[str]] = [] res = plugin_dist.install(_CFG, timeout=30.0, base=tmp_path, runner=_ok_runner(calls)) assert res.first_install is False # marketplace update refreshes the listing; plugin update refreshes the # CACHED installed copy — both must run or users keep stale skills. assert [c[1:5] for c in calls] == [ ["plugin", "marketplace", "update", "helix"], ["plugin", "update", "helix@helix"], ] def test_sha256_mismatch_refuses_install(tmp_path, stub_rest, monkeypatch, cli_env): payload = _zip_bytes() stub_rest["download"] = { "url": "https://s3.example.com/x.zip", "sha256": "0" * 64, # wrong on purpose "version": "1.0.0+gabc12345", } _stub_download(monkeypatch, payload) with pytest.raises(CliRestError) as exc: plugin_dist.install(_CFG, timeout=30.0, base=tmp_path, runner=_ok_runner([])) assert exc.value.code == "INTEGRITY" assert not (tmp_path / "helix-claude-plugin").exists() assert plugin_dist.read_receipt(tmp_path) is None @pytest.mark.parametrize( "member", [ ("../evil.txt", b"x"), # traversal ("/abs/evil.txt", b"x"), # absolute ("unexpected-root/file.txt", b"x"), # foreign top-level dir ], ) def test_unsafe_archive_members_rejected(tmp_path, stub_rest, monkeypatch, cli_env, member): payload = _zip_bytes(extra_member=member) stub_rest["download"] = { "url": "https://s3.example.com/x.zip", "sha256": hashlib.sha256(payload).hexdigest(), "version": "1.0.0+gabc12345", } _stub_download(monkeypatch, payload) with pytest.raises(CliRestError) as exc: plugin_dist.install(_CFG, timeout=30.0, base=tmp_path, runner=_ok_runner([])) assert exc.value.code == "UNSAFE_ARCHIVE" assert not (tmp_path / "helix-claude-plugin").exists() def test_archive_without_marketplace_manifest_rejected(tmp_path, stub_rest, monkeypatch, cli_env): buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as zf: zf.writestr("helix-claude-plugin/README.md", "not a marketplace") payload = buf.getvalue() stub_rest["download"] = { "url": "https://s3.example.com/x.zip", "sha256": hashlib.sha256(payload).hexdigest(), "version": "1.0.0+gabc12345", } _stub_download(monkeypatch, payload) with pytest.raises(CliRestError) as exc: plugin_dist.install(_CFG, timeout=30.0, base=tmp_path, runner=_ok_runner([])) assert exc.value.code == "UNSAFE_ARCHIVE" def test_missing_claude_binary_yields_manual_steps(tmp_path, stub_rest, monkeypatch, cli_env): payload = _zip_bytes() stub_rest["download"] = { "url": "https://s3.example.com/x.zip", "sha256": hashlib.sha256(payload).hexdigest(), "version": "1.0.0+gabc12345", } _stub_download(monkeypatch, payload) monkeypatch.setattr(plugin_dist.shutil, "which", lambda _: None) res = plugin_dist.install(_CFG, timeout=30.0, base=tmp_path) assert res.registration["registered"] is False assert any("marketplace add" in step for step in res.registration["manual"]) # Download still landed — registration is best-effort. assert (tmp_path / "helix-claude-plugin" / ".claude-plugin" / "marketplace.json").is_file() def test_install_replaces_previous_version(tmp_path, stub_rest, monkeypatch, cli_env): old = tmp_path / "helix-claude-plugin" (old / ".claude-plugin").mkdir(parents=True) (old / ".claude-plugin" / "marketplace.json").write_text("{}") (old / "stale-file.txt").write_text("must disappear") (tmp_path / "installed.json").write_text(json.dumps({"version": "0.9.0"})) payload = _zip_bytes() stub_rest["download"] = { "url": "https://s3.example.com/x.zip", "sha256": hashlib.sha256(payload).hexdigest(), "version": "1.0.0+gabc12345", } _stub_download(monkeypatch, payload) plugin_dist.install(_CFG, timeout=30.0, base=tmp_path, register=False) assert not (tmp_path / "helix-claude-plugin" / "stale-file.txt").exists() assert not (tmp_path / "helix-claude-plugin.prev").exists() def test_status_states(tmp_path, stub_rest, monkeypatch, cli_env): # not_installed out = plugin_dist.status(_CFG, timeout=30.0, base=tmp_path) assert out["state"] == "not_installed" # up_to_date mp = tmp_path / "helix-claude-plugin" / ".claude-plugin" mp.mkdir(parents=True) (mp / "marketplace.json").write_text("{}") (tmp_path / "installed.json").write_text( json.dumps({"version": "1.0.0+gabc12345", "path": str(tmp_path)}) ) out = plugin_dist.status(_CFG, timeout=30.0, base=tmp_path) assert out["state"] == "up_to_date" assert out["installed"]["intact"] is True # update_available stub_rest["manifest"] = {"available": True, "version": "1.1.0+g99999999"} out = plugin_dist.status(_CFG, timeout=30.0, base=tmp_path) assert out["state"] == "update_available" # not_published stub_rest["manifest"] = {"available": False} out = plugin_dist.status(_CFG, timeout=30.0, base=tmp_path) assert out["state"] == "not_published" def test_cli_dispatch_plugin_status(cli_env, monkeypatch, capsys): """End-to-end через main(): `helix --json plugin status`.""" from helix_cli.main import main monkeypatch.setattr( plugin_dist, "status", lambda cfg, *, timeout, base=None: {"state": "not_installed", "installed": None, "latest": {"available": False}}, ) rc = main(["--json", "plugin", "status"]) assert rc == 0 out = json.loads(capsys.readouterr().out) assert out["state"] == "not_installed" def test_cli_dispatch_plugin_install_no_register(cli_env, monkeypatch, capsys, tmp_path): from helix_cli.main import main def _install(cfg, *, timeout, base=None, register=True, runner=plugin_dist._run_claude): assert register is False return plugin_dist.InstallResult( version="1.0.0+gabc12345", plugin_version="1.0.0", path=str(tmp_path), sha256="c" * 64, size=1, first_install=True, registration={ "registered": False, "reason": "skipped (--no-register)", "manual": ["claude plugin marketplace add …"], }, ) monkeypatch.setattr(plugin_dist, "install", _install) rc = main(["--json", "plugin", "install", "--no-register"]) assert rc == 0 out = json.loads(capsys.readouterr().out) assert out["version"] == "1.0.0+gabc12345" assert out["next_steps"] def test_manifest_rest_error_propagates(cli_env, monkeypatch): def _get(config, path, *, token=None, timeout=30.0): return RestResponse(status_code=401, body={"success": False}, raw_text="") monkeypatch.setattr(plugin_dist.http_client, "get", _get) with pytest.raises(CliRestError) as exc: plugin_dist.fetch_manifest(_CFG, timeout=30.0) assert exc.value.exit_code == 2 # UNAUTHORIZED per the CLI exit contract