Files
2026-07-13 12:55:23 +08:00

266 lines
11 KiB
Python

"""Tests for matrix skill distribution (P1-4): co-installed assets and the
content lookup chain in cli_hub/matrix_skill.py."""
from pathlib import Path
from unittest.mock import patch
import pytest
import requests
import click.testing
from cli_hub import matrix_skill
from cli_hub.matrix_skill import (
get_rendered_matrix_skill_path,
render_matrix_skill_file,
)
from cli_hub.cli import main
def _make_content_dir(root, name="demo", with_pycache=False):
"""Create a fake repo checkout with cli-hub-matrix/<name>/ content."""
content = root / "cli-hub-matrix" / name
(content / "references").mkdir(parents=True)
(content / "scripts").mkdir(parents=True)
(content / "SKILL.md").write_text(
"# Demo Matrix\n\nRead [`references/guide.md`](references/guide.md) "
"and run `scripts/doctor.py`.\n",
encoding="utf-8",
)
(content / "references" / "guide.md").write_text("guide", encoding="utf-8")
(content / "scripts" / "doctor.py").write_text("print('ok')", encoding="utf-8")
if with_pycache:
pycache = content / "scripts" / "__pycache__"
pycache.mkdir()
(pycache / "doctor.cpython-310.pyc").write_bytes(b"\x00")
(content / "scripts" / "stray.pyc").write_bytes(b"\x00")
return content
def _demo_matrix(name="demo"):
return {
"name": name,
"display_name": "Demo Matrix",
"description": "A demo matrix.",
"skill_md": f"cli-hub-matrix/{name}/SKILL.md",
"clis": [],
}
class TestAssetCoInstall:
"""references/ and scripts/ land beside the rendered SKILL.md."""
def test_references_and_scripts_copied_beside_skill(self, tmp_path, monkeypatch):
repo = tmp_path / "repo"
_make_content_dir(repo)
skill_dir = tmp_path / "home" / ".cli-hub" / "matrix"
monkeypatch.setattr(matrix_skill, "MATRIX_SKILL_DIR", skill_dir)
monkeypatch.setattr(matrix_skill, "_find_repo_root", lambda: repo)
rendered = render_matrix_skill_file(_demo_matrix(), installed={})
assert rendered == skill_dir / "demo" / "SKILL.md"
assert rendered.exists()
# Relative links in the skill resolve in the installed layout.
assert (rendered.parent / "references" / "guide.md").exists()
assert (rendered.parent / "scripts" / "doctor.py").exists()
content = rendered.read_text(encoding="utf-8")
assert "references/guide.md" in content
assert "MATRIX_SKILL_PATHS:START" in content
# Assets were found locally, so no remote-fallback note is injected.
assert "will not resolve locally" not in content
def test_pycache_and_pyc_excluded(self, tmp_path, monkeypatch):
repo = tmp_path / "repo"
_make_content_dir(repo, with_pycache=True)
skill_dir = tmp_path / "home" / ".cli-hub" / "matrix"
monkeypatch.setattr(matrix_skill, "MATRIX_SKILL_DIR", skill_dir)
monkeypatch.setattr(matrix_skill, "_find_repo_root", lambda: repo)
rendered = render_matrix_skill_file(_demo_matrix(), installed={})
installed_files = [p.name for p in rendered.parent.rglob("*")]
assert "doctor.py" in installed_files
assert "__pycache__" not in installed_files
assert not any(name.endswith(".pyc") for name in installed_files)
def test_reinstall_is_idempotent_and_removes_stale_files(self, tmp_path, monkeypatch):
repo = tmp_path / "repo"
content = _make_content_dir(repo)
skill_dir = tmp_path / "home" / ".cli-hub" / "matrix"
monkeypatch.setattr(matrix_skill, "MATRIX_SKILL_DIR", skill_dir)
monkeypatch.setattr(matrix_skill, "_find_repo_root", lambda: repo)
first = render_matrix_skill_file(_demo_matrix(), installed={})
# Simulate drift: stale file in the installed copy, updated source.
(first.parent / "references" / "stale.md").write_text("old", encoding="utf-8")
(content / "references" / "guide.md").write_text("guide v2", encoding="utf-8")
second = render_matrix_skill_file(_demo_matrix(), installed={})
assert second == first
assert not (second.parent / "references" / "stale.md").exists()
assert (second.parent / "references" / "guide.md").read_text(encoding="utf-8") == "guide v2"
class TestLookupChain:
"""Checkout -> bundled data -> published URL -> stub."""
def test_bundled_data_used_when_no_checkout(self, tmp_path, monkeypatch):
bundled_root = tmp_path / "bundled"
_make_content_dir(bundled_root)
skill_dir = tmp_path / "home" / ".cli-hub" / "matrix"
monkeypatch.setattr(matrix_skill, "MATRIX_SKILL_DIR", skill_dir)
monkeypatch.setattr(matrix_skill, "_find_repo_root", lambda: None)
monkeypatch.setattr(
matrix_skill, "BUNDLED_MATRIX_DATA_DIR", bundled_root / "cli-hub-matrix"
)
rendered = render_matrix_skill_file(_demo_matrix(), installed={})
content = rendered.read_text(encoding="utf-8")
assert "# Demo Matrix" in content
assert (rendered.parent / "references" / "guide.md").exists()
assert (rendered.parent / "scripts" / "doctor.py").exists()
def test_published_url_used_when_no_local_content(self, tmp_path, monkeypatch):
skill_dir = tmp_path / "home" / ".cli-hub" / "matrix"
monkeypatch.setattr(matrix_skill, "MATRIX_SKILL_DIR", skill_dir)
monkeypatch.setattr(matrix_skill, "_find_repo_root", lambda: None)
monkeypatch.setattr(
matrix_skill, "BUNDLED_MATRIX_DATA_DIR", tmp_path / "missing"
)
class FakeResponse:
status_code = 200
text = "# Demo Matrix (published)\n\npublished body\n"
requested = {}
def fake_get(url, timeout):
requested["url"] = url
return FakeResponse()
monkeypatch.setattr(matrix_skill.requests, "get", fake_get)
rendered = render_matrix_skill_file(_demo_matrix(), installed={})
content = rendered.read_text(encoding="utf-8")
assert "# Demo Matrix (published)" in content
assert requested["url"] == (
f"{matrix_skill.MATRIX_CONTENT_BASE_URL}/demo/SKILL.md"
)
# No local assets: the rendered skill points at the published copies.
assert not (rendered.parent / "references").exists()
assert "will not resolve locally" in content
assert f"{matrix_skill.MATRIX_CONTENT_BASE_URL}/demo/" in content
def test_stub_used_when_everything_else_fails(self, tmp_path, monkeypatch):
skill_dir = tmp_path / "home" / ".cli-hub" / "matrix"
monkeypatch.setattr(matrix_skill, "MATRIX_SKILL_DIR", skill_dir)
monkeypatch.setattr(matrix_skill, "_find_repo_root", lambda: None)
monkeypatch.setattr(
matrix_skill, "BUNDLED_MATRIX_DATA_DIR", tmp_path / "missing"
)
def fake_get(url, timeout):
raise requests.ConnectionError("offline")
monkeypatch.setattr(matrix_skill.requests, "get", fake_get)
rendered = render_matrix_skill_file(_demo_matrix(), installed={})
content = rendered.read_text(encoding="utf-8")
assert "# Demo Matrix" in content
assert "A demo matrix." in content
assert "Install with `cli-hub matrix install demo`." in content
def test_published_url_non_200_falls_back_to_stub(self, tmp_path, monkeypatch):
skill_dir = tmp_path / "home" / ".cli-hub" / "matrix"
monkeypatch.setattr(matrix_skill, "MATRIX_SKILL_DIR", skill_dir)
monkeypatch.setattr(matrix_skill, "_find_repo_root", lambda: None)
monkeypatch.setattr(
matrix_skill, "BUNDLED_MATRIX_DATA_DIR", tmp_path / "missing"
)
class FakeResponse:
status_code = 404
text = "Not Found"
monkeypatch.setattr(matrix_skill.requests, "get", lambda url, timeout: FakeResponse())
rendered = render_matrix_skill_file(_demo_matrix(), installed={})
assert "Install with `cli-hub matrix install demo`." in rendered.read_text(encoding="utf-8")
class TestRenderedPathCompat:
"""get_rendered_matrix_skill_path prefers the new layout, keeps legacy."""
def test_prefers_directory_layout(self, tmp_path, monkeypatch):
monkeypatch.setattr(matrix_skill, "MATRIX_SKILL_DIR", tmp_path)
new_path = tmp_path / "demo" / "SKILL.md"
new_path.parent.mkdir(parents=True)
new_path.write_text("new", encoding="utf-8")
(tmp_path / "demo.SKILL.md").write_text("legacy", encoding="utf-8")
assert get_rendered_matrix_skill_path("demo") == new_path
def test_falls_back_to_legacy_flat_file(self, tmp_path, monkeypatch):
monkeypatch.setattr(matrix_skill, "MATRIX_SKILL_DIR", tmp_path)
legacy = tmp_path / "demo.SKILL.md"
legacy.write_text("legacy", encoding="utf-8")
assert get_rendered_matrix_skill_path("demo") == legacy
def test_defaults_to_directory_layout_when_nothing_exists(self, tmp_path, monkeypatch):
monkeypatch.setattr(matrix_skill, "MATRIX_SKILL_DIR", tmp_path)
assert get_rendered_matrix_skill_path("demo") == tmp_path / "demo" / "SKILL.md"
class TestSkillOnlyInstall:
"""`cli-hub matrix install <name> --skill-only` renders without CLI installs."""
def setup_method(self):
self.runner = click.testing.CliRunner()
self.human_detection = {
"is_agent": False,
"traffic_type": "human",
"category": "human",
"reason": "human",
"signals": [],
"stdin_tty": True,
"is_interactive": True,
}
@patch("cli_hub.cli.track_first_run")
@patch("cli_hub.cli.track_visit")
@patch("cli_hub.cli.detect_invocation_context")
@patch("cli_hub.cli.render_matrix_skill_file")
@patch("cli_hub.cli.get_installed", return_value={})
@patch("cli_hub.cli.get_matrix")
@patch("cli_hub.cli.install_matrix")
def test_skill_only_renders_and_skips_cli_installs(
self, mock_install_matrix, mock_get_matrix, mock_installed,
mock_render, mock_detect, mock_visit, mock_first_run, tmp_path,
):
mock_detect.return_value = self.human_detection
mock_get_matrix.return_value = _demo_matrix()
mock_render.return_value = tmp_path / "demo" / "SKILL.md"
result = self.runner.invoke(main, ["matrix", "install", "demo", "--skill-only"])
assert result.exit_code == 0
assert "Local matrix skill:" in result.output
mock_render.assert_called_once()
mock_install_matrix.assert_not_called()
@patch("cli_hub.cli.track_first_run")
@patch("cli_hub.cli.track_visit")
@patch("cli_hub.cli.detect_invocation_context")
@patch("cli_hub.cli.get_matrix", return_value=None)
def test_skill_only_unknown_matrix_fails(
self, mock_get_matrix, mock_detect, mock_visit, mock_first_run,
):
mock_detect.return_value = self.human_detection
result = self.runner.invoke(main, ["matrix", "install", "missing", "--skill-only"])
assert result.exit_code == 1