"""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)