hugin/server/tests/test_bundle.py
2026-10-27 08:33:21 +01:00

93 lines
3.7 KiB
Python

"""Bundles gegen das Schema und gegen die goldenen Bundles in contract/examples/bundles.
Die goldenen Bundles sind zugleich Testdaten des Go-CLI. Nach einer gewollten
Änderung neu schreiben mit: uv run pytest --update
"""
import io
import json
import shutil
import zipfile
from datetime import UTC, datetime
import jsonschema
import pytest
import yaml
from conftest import EXAMPLES, CONTRACT, example
from hugin_core.bundle import INSTRUCTIONS, BundleError, Entry, generate
from hugin_core.translators import Config, Tool
TIME = datetime(2026, 10, 18, 12, 0, tzinfo=UTC)
SCENARIOS = yaml.safe_load((EXAMPLES / "scenarios.yaml").read_text())
SCHEMA = json.loads((CONTRACT / "hugin-bundle.schema.json").read_text())
def bundle_for(scenario: dict):
mandatory = set(scenario.get("mandatory", []))
entries = [Entry(example(n), n.split("/")[0] in mandatory) for n in scenario["packages"]]
installed = [example(n) for n in scenario.get("installed", [])]
tools = [Tool(w) for w in scenario["tools"]]
return generate(entries, tools, Config(), TIME, installed)
@pytest.mark.parametrize("scenario", SCENARIOS, ids=[s["name"] for s in SCENARIOS])
def test_golden(scenario, update):
bundle = bundle_for(scenario)
jsonschema.validate(bundle.instructions, SCHEMA, format_checker=jsonschema.FormatChecker())
target = EXAMPLES / "bundles" / scenario["name"]
expected = {INSTRUCTIONS: (json.dumps(bundle.instructions, ensure_ascii=False, indent=2) + "\n").encode()}
expected |= bundle.files
if update:
shutil.rmtree(target, ignore_errors=True)
for path, data in expected.items():
(target / path).parent.mkdir(parents=True, exist_ok=True)
(target / path).write_bytes(data)
existing = {p.relative_to(target).as_posix(): p.read_bytes() for p in target.rglob("*") if p.is_file()}
assert existing == expected
def test_first_install_content():
instructions = bundle_for(SCENARIOS[0]).instructions
ops = [(o["op"], o.get("path") or o.get("file")) for o in instructions["operations"]]
assert ops == [
("file", ".claude/skills/pub-munin-nutzen/SKILL.md"),
("file", ".claude/skills/pub-munin-nutzen/referenz/beispiele.md"),
("file", ".hugin/rules/pub-hausregeln.md"),
("json", ".mcp.json"),
("json", "opencode.json"),
("json", "opencode.json"),
("block", "CLAUDE.md"),
]
assert [p["mandatory"] for p in instructions["packages"]] == [False, True, False]
def test_update_removes_old():
ops = bundle_for(SCENARIOS[1]).instructions["operations"]
remove = [o for o in ops if o["op"] in ("remove", "json_remove")]
assert remove == [
{"op": "remove", "package": "pub-munin-nutzen", "path": ".claude/skills/pub-munin-nutzen/referenz/beispiele.md"},
{"op": "remove", "package": "pub-hausregeln", "path": ".hugin/rules/pub-hausregeln.md"},
{"op": "json_remove", "file": "opencode.json", "key": ["instructions"],
"mode": "list_item", "value": ".hugin/rules/*.md"},
]
assert ops[-1] == {"op": "block", "file": "CLAUDE.md", "block": "hugin", "content": ""}
def test_only_claude_without_opencode_entries():
ops = bundle_for(SCENARIOS[2]).instructions["operations"]
assert not [o for o in ops if o.get("file") == "opencode.json"]
def test_zip_is_reproducible():
a = bundle_for(SCENARIOS[0]).as_zip()
assert a == bundle_for(SCENARIOS[0]).as_zip()
with zipfile.ZipFile(io.BytesIO(a)) as z:
assert z.namelist()[0] == INSTRUCTIONS
assert all(n == INSTRUCTIONS or n.startswith("files/") for n in z.namelist())
def test_without_tool():
with pytest.raises(BundleError):
generate([], [], Config(), TIME)