93 lines
3.7 KiB
Python
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)
|