blob: 49070975826c4e49d4c2ed06fc3213dbe2e48054 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Tests for `skill-surface-hash.py`, the generator that stamps a
reconciliation fingerprint into every `skills/*/SKILL.md`."""
from __future__ import annotations
import hashlib
import importlib.util
from pathlib import Path
from types import ModuleType
REPO = Path(__file__).resolve().parents[3]
def _load() -> ModuleType:
spec = importlib.util.spec_from_file_location(
"skill_surface_hash", REPO / "tools" / "dev" / "skill-surface-hash.py"
)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
MOD = _load()
SKILL = """---
name: magpie-demo
family: issue
requires_config:
- project.md
- demo.md
description: |
A demo skill.
license: Apache-2.0
---
# Demo skill
<!-- BEGIN MAGPIE PREFLIGHT — generated from tools/dev/preflight-block.md -->
## Pre-flight — is this project set up?
Shared text that every skill carries.
<!-- END MAGPIE PREFLIGHT -->
## Step 1 — gather
Some prose that may be reworded freely.
**Golden rule 1 — propose, never apply.**
### Step 1a — the narrow case
"""
def _make_skill(root: Path, skill_md: str = SKILL, **details: str) -> Path:
"""Build a skill directory at `root`: `SKILL.md` plus any sibling
`*.md` detail files named by `details` (e.g. `**{"guide.md": "..."}`)."""
root.mkdir(parents=True, exist_ok=True)
(root / "SKILL.md").write_text(skill_md)
for name, content in details.items():
(root / name).write_text(content)
return root
def test_inputs_are_requires_config_and_anchors(tmp_path: Path) -> None:
skill_dir = _make_skill(tmp_path)
requires, anchors = MOD.surface_inputs(skill_dir)
assert requires == ["demo.md", "project.md"]
assert anchors == [
"Golden rule 1 — propose, never apply.",
"Step 1 — gather",
"Step 1a — the narrow case",
]
def test_preflight_block_is_excluded(tmp_path: Path) -> None:
skill_dir = _make_skill(tmp_path)
_, anchors = MOD.surface_inputs(skill_dir)
assert not any("Pre-flight" in a for a in anchors)
# A declared block, generated by `check-shared-blocks.py` for a name other
# than the one auto block (`preflight`). Its heading is shared framework
# prose the moment a later task extracts one of these into a skill; it must
# be just as invisible to the fingerprint as the pre-flight block always
# was. This is the case a `PREFLIGHT_RE`-only strip cannot see: it fails
# without `strip_generated_regions()` folding in `DECLARED_RE` too.
DECLARED_BLOCK = (
"<!-- BEGIN MAGPIE BLOCK: widget — generated from tools/dev/blocks/widget.md -->\n\n"
"## Shared Widget Heading\n\nShared prose every carrying skill repeats verbatim.\n\n"
"<!-- END MAGPIE BLOCK: widget -->\n"
)
def test_declared_block_heading_is_excluded_from_the_hash(tmp_path: Path) -> None:
with_block = _make_skill(tmp_path / "with-block", SKILL + "\n" + DECLARED_BLOCK)
without_block = _make_skill(tmp_path / "without-block", SKILL)
_, anchors = MOD.surface_inputs(with_block)
assert not any("Shared Widget Heading" in a for a in anchors)
assert MOD.surface_hash(with_block) == MOD.surface_hash(without_block)
def test_declared_block_heading_in_detail_file_is_excluded_from_the_hash(tmp_path: Path) -> None:
with_block = _make_skill(tmp_path / "with-block", SKILL, **{"guide.md": DECLARED_BLOCK})
without_block = _make_skill(tmp_path / "without-block", SKILL, **{"guide.md": ""})
_, anchors = MOD.surface_inputs(with_block)
assert not any("Shared Widget Heading" in a for a in anchors)
assert MOD.surface_hash(with_block) == MOD.surface_hash(without_block)
def test_prose_edit_does_not_move_the_hash(tmp_path: Path) -> None:
reworded = SKILL.replace("Some prose that may be reworded freely.", "Entirely different prose here.")
base = _make_skill(tmp_path / "base")
changed = _make_skill(tmp_path / "changed", reworded)
assert MOD.surface_hash(changed) == MOD.surface_hash(base)
def test_renamed_heading_moves_the_hash(tmp_path: Path) -> None:
renamed = SKILL.replace("## Step 1 — gather", "## Step 1 — collect")
base = _make_skill(tmp_path / "base")
changed = _make_skill(tmp_path / "changed", renamed)
assert MOD.surface_hash(changed) != MOD.surface_hash(base)
def test_changed_requires_config_moves_the_hash(tmp_path: Path) -> None:
changed_text = SKILL.replace(" - demo.md\n", " - demo.md\n - extra.md\n")
base = _make_skill(tmp_path / "base")
changed = _make_skill(tmp_path / "changed", changed_text)
assert MOD.surface_hash(changed) != MOD.surface_hash(base)
def test_requires_config_order_does_not_matter(tmp_path: Path) -> None:
reordered_text = SKILL.replace(" - project.md\n - demo.md\n", " - demo.md\n - project.md\n")
base = _make_skill(tmp_path / "base")
reordered = _make_skill(tmp_path / "reordered", reordered_text)
assert MOD.surface_hash(reordered) == MOD.surface_hash(base)
def test_heading_renamed_in_detail_file_moves_the_hash(tmp_path: Path) -> None:
detail = "## Detail heading\n\nSome detail prose.\n"
renamed_detail = "## Detail heading renamed\n\nSome detail prose.\n"
base = _make_skill(tmp_path / "base", SKILL, **{"guide.md": detail})
renamed = _make_skill(tmp_path / "renamed", SKILL, **{"guide.md": renamed_detail})
assert MOD.surface_hash(renamed) != MOD.surface_hash(base)
def test_prose_edit_in_detail_file_does_not_move_the_hash(tmp_path: Path) -> None:
detail = "## Detail heading\n\nOriginal detail prose.\n"
reworded_detail = "## Detail heading\n\nCompletely different detail prose.\n"
base = _make_skill(tmp_path / "base", SKILL, **{"guide.md": detail})
reworded = _make_skill(tmp_path / "reworded", SKILL, **{"guide.md": reworded_detail})
assert MOD.surface_hash(reworded) == MOD.surface_hash(base)
def test_adding_a_detail_file_moves_the_hash(tmp_path: Path) -> None:
without = _make_skill(tmp_path / "without")
with_detail = _make_skill(tmp_path / "with-detail", SKILL, **{"guide.md": "## New heading\n"})
assert MOD.surface_hash(with_detail) != MOD.surface_hash(without)
def test_swapping_anchors_between_detail_files_moves_the_hash(tmp_path: Path) -> None:
"""Same combined anchor text, split the opposite way across two detail
files, must hash differently — the file an anchor came from is part of
the payload, not just the anchor text. Rename-within-a-file and
add-a-file coverage would not catch a regression that dropped the
per-file tagging; this is the property that would."""
swapped_a = _make_skill(
tmp_path / "swapped-a",
SKILL,
**{"alpha.md": "## Heading One\n", "beta.md": "## Heading Two\n"},
)
swapped_b = _make_skill(
tmp_path / "swapped-b",
SKILL,
**{"alpha.md": "## Heading Two\n", "beta.md": "## Heading One\n"},
)
assert MOD.surface_hash(swapped_a) != MOD.surface_hash(swapped_b)
def test_hash_does_not_depend_on_file_creation_order(tmp_path: Path) -> None:
forward = tmp_path / "forward"
forward.mkdir()
(forward / "SKILL.md").write_text(SKILL)
(forward / "alpha.md").write_text("## Alpha heading\n")
(forward / "zulu.md").write_text("## Zulu heading\n")
reverse = tmp_path / "reverse"
reverse.mkdir()
(reverse / "zulu.md").write_text("## Zulu heading\n")
(reverse / "alpha.md").write_text("## Alpha heading\n")
(reverse / "SKILL.md").write_text(SKILL)
assert MOD.surface_hash(forward) == MOD.surface_hash(reverse)
def test_no_detail_files_matches_legacy_text_only_algorithm(tmp_path: Path) -> None:
"""A skill directory with no detail files must hash exactly as the
pre-widening algorithm (over `SKILL.md`'s own text alone) did, so the
57 single-file skills do not churn when the generator is widened."""
skill_dir = _make_skill(tmp_path)
front = MOD.FRONTMATTER_RE.match(SKILL)
assert front is not None
body = MOD.PREFLIGHT_RE.sub("", SKILL[front.end() :])
requires_block = MOD.REQUIRES_RE.search(front.group(1) + "\n")
assert requires_block is not None
legacy_requires = sorted(MOD.ITEM_RE.findall(requires_block.group(1)))
legacy_anchors = sorted(
{MOD._normalise(m) for m in MOD.HEADING_RE.findall(body)}
| {MOD._normalise(m) for m in MOD.GOLDEN_RE.findall(body)}
)
legacy_payload = "\n".join(["requires_config:", *legacy_requires, "anchors:", *legacy_anchors])
legacy_digest = "sha256:" + hashlib.sha256(legacy_payload.encode()).hexdigest()[:16]
assert MOD.surface_hash(skill_dir) == legacy_digest
def test_fourth_level_heading_is_an_anchor(tmp_path: Path) -> None:
"""`#### Pass B — Security` is a structural sub-step an override can
anchor to, not prose: it is recorded, and renaming it moves the hash."""
with_h4 = _make_skill(tmp_path / "with-h4", SKILL + "\n#### Pass B — Security\n")
renamed = _make_skill(tmp_path / "renamed", SKILL + "\n#### Pass C — Security\n")
base = _make_skill(tmp_path / "base")
_, anchors = MOD.surface_inputs(with_h4)
assert "Pass B — Security" in anchors
assert MOD.surface_hash(with_h4) != MOD.surface_hash(base)
assert MOD.surface_hash(renamed) != MOD.surface_hash(with_h4)
def test_fourth_level_heading_in_a_detail_file_is_an_anchor(tmp_path: Path) -> None:
base = _make_skill(tmp_path / "base", SKILL, **{"guide.md": "#### Sub-step one\n"})
renamed = _make_skill(tmp_path / "renamed", SKILL, **{"guide.md": "#### Sub-step two\n"})
_, anchors = MOD.surface_inputs(base)
assert "guide.md: Sub-step one" in anchors
assert MOD.surface_hash(renamed) != MOD.surface_hash(base)
def test_fifth_level_heading_is_not_an_anchor(tmp_path: Path) -> None:
"""The widening stops at `####`; deeper headings stay prose."""
base = _make_skill(tmp_path / "base")
with_h5 = _make_skill(tmp_path / "with-h5", SKILL + "\n##### Deep aside\n")
assert MOD.surface_hash(with_h5) == MOD.surface_hash(base)
def test_subdirectories_are_ignored(tmp_path: Path) -> None:
without = _make_skill(tmp_path / "without")
with_subdirs = _make_skill(tmp_path / "with-subdirs")
guards = with_subdirs / "guards"
guards.mkdir()
(guards / "check.py").write_text("# not markdown, and not top-level\n")
(guards / "notes.md").write_text("## Should be ignored\n")
fixtures = with_subdirs / "fixtures"
fixtures.mkdir()
(fixtures / "case.md").write_text("## Also ignored\n")
assert MOD.surface_hash(with_subdirs) == MOD.surface_hash(without)
def test_apply_is_idempotent(tmp_path: Path) -> None:
skill_dir = _make_skill(tmp_path)
path = skill_dir / "SKILL.md"
digest = MOD.surface_hash(skill_dir)
assert MOD.apply(path, digest) == (True, None)
first = path.read_text()
assert MOD.apply(path, digest) == (False, None)
assert path.read_text() == first
assert f"surface_hash: {digest}" in first
def test_apply_replaces_a_stale_value(tmp_path: Path) -> None:
path = tmp_path / "SKILL.md"
path.write_text(SKILL.replace("license: Apache-2.0", "surface_hash: sha256:dead\nlicense: Apache-2.0"))
digest = MOD.surface_hash(tmp_path)
assert MOD.apply(path, digest) == (True, None)
assert "sha256:dead" not in path.read_text()
def test_missing_frontmatter_is_an_error(tmp_path: Path) -> None:
path = tmp_path / "SKILL.md"
path.write_text("# No frontmatter\n")
changed, error = MOD.apply(path, "sha256:abc")
assert changed is False
assert error is not None and "frontmatter" in error
def test_every_live_skill_is_current() -> None:
stale = [
p
for p in sorted((REPO / "skills").glob("*/SKILL.md"))
if f"surface_hash: {MOD.surface_hash(p.parent)}" not in p.read_text()
]
assert stale == [], f"run `python3 tools/dev/skill-surface-hash.py --fix`: {stale}"