blob: 696348b8796208727e64f1920d107757af96faad [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 `check-duplication.py`, the gate that fails the build on new
cross-file near-duplicate prose in `skills/`."""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
from types import ModuleType
REPO = Path(__file__).resolve().parents[3]
def _load() -> ModuleType:
spec = importlib.util.spec_from_file_location(
"check_duplication", REPO / "tools" / "dev" / "check-duplication.py"
)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
# Registered in sys.modules before exec: the module defines dataclasses,
# and the dataclass machinery looks its own module up by name to resolve
# forward-referenced annotations — an unregistered module fails that
# lookup with a confusing AttributeError instead of the real error.
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
MOD = _load()
# A 30-word filler paragraph, repeated with small variations below. Comfortably
# over the 25-word floor on its own.
LONG_PARAGRAPH = (
"This paragraph exists purely to carry enough words past the floor for "
"the duplication scanner to score it, since a short admonition or a "
"heading is never long enough to be mistaken for prose duplication here."
)
assert len(MOD.WORD_RE.findall(LONG_PARAGRAPH)) > MOD.WORD_FLOOR
SHORT_PARAGRAPH = "This paragraph is short and must never be scored."
assert len(MOD.WORD_RE.findall(SHORT_PARAGRAPH)) <= MOD.WORD_FLOOR
def _skill(name: str, body: str) -> str:
return (
"---\n"
f"name: magpie-{name}\n"
"family: issue\n"
"description: |\n"
" A demo skill.\n"
"license: Apache-2.0\n"
"---\n\n"
f"# {name}\n\n{body}\n"
)
# --- extraction: word floor + paragraph splitting ------------------------------------
def test_short_paragraph_is_ignored(tmp_path: Path) -> None:
path = tmp_path / "demo.md"
path.write_text(f"{SHORT_PARAGRAPH}\n")
assert MOD.extract_paragraphs(path) == []
def test_long_paragraph_is_kept(tmp_path: Path) -> None:
path = tmp_path / "demo.md"
path.write_text(f"{LONG_PARAGRAPH}\n")
paragraphs = MOD.extract_paragraphs(path)
assert len(paragraphs) == 1
assert paragraphs[0].text.startswith("This paragraph exists")
def test_blank_line_separates_paragraphs(tmp_path: Path) -> None:
path = tmp_path / "demo.md"
path.write_text(f"{LONG_PARAGRAPH}\n\n{LONG_PARAGRAPH} And then some more words follow right here.\n")
paragraphs = MOD.extract_paragraphs(path)
assert len(paragraphs) == 2
# --- exclusions: generated regions, frontmatter, code fences --------------------------
def test_identical_text_inside_generated_regions_is_invisible() -> None:
"""Two skills carrying the byte-identical auto pre-flight block must
produce zero paragraphs from that block — the whole reason
`strip_generated_regions` exists. Reuses a real propagated block from the
live tree so the test tracks the real marker text, not a hand-written
stand-in."""
# Not every skill carries the block (the setup skills do not), and glob
# order is filesystem-dependent, so pick the first carrier in sorted order.
matches = (MOD.PREFLIGHT_RE.search(p.read_text()) for p in sorted((REPO / "skills").glob("*/SKILL.md")))
match = next((m for m in matches if m), None)
assert match, "expected at least one live skill to carry the auto pre-flight block"
block = match.group(0)
paragraphs = MOD.extract_paragraphs(
Path("virtual.md"), text=f"# Heading\n\n{block}\n## Next\n\nSome unrelated text.\n"
)
# The block's own long paragraphs must not survive into the result.
assert not any("first, before anything else" in p.text for p in paragraphs)
def test_declared_block_region_is_invisible(tmp_path: Path) -> None:
body = (
"# Heading\n\n"
"<!-- BEGIN MAGPIE BLOCK: widget — generated from tools/dev/blocks/widget.md -->\n\n"
f"{LONG_PARAGRAPH}\n\n"
"<!-- END MAGPIE BLOCK: widget -->\n\n"
"## Next\n"
)
paragraphs = MOD.extract_paragraphs(Path("virtual.md"), text=body)
assert paragraphs == []
def test_generated_region_removal_does_not_fuse_neighbouring_paragraphs(tmp_path: Path) -> None:
"""A generated region sits between two ordinary paragraphs; stripping it
must leave a paragraph boundary behind rather than joining the paragraph
before it directly onto the paragraph after it into one blob."""
other_long_paragraph = (
"Here is a second, unrelated paragraph that also comfortably clears "
"the word floor on its own, so it should be counted as its own "
"distinct unit once the block between the two has been removed."
)
body = (
f"{LONG_PARAGRAPH}\n\n"
"<!-- BEGIN MAGPIE BLOCK: widget — generated from tools/dev/blocks/widget.md -->\n\n"
"Shared widget body text that must not appear in the result at all.\n\n"
"<!-- END MAGPIE BLOCK: widget -->\n\n"
f"{other_long_paragraph}\n"
)
paragraphs = MOD.extract_paragraphs(Path("virtual.md"), text=body)
assert len(paragraphs) == 2
assert paragraphs[0].text == LONG_PARAGRAPH
assert paragraphs[1].text == other_long_paragraph
def test_frontmatter_is_excluded() -> None:
text = _skill("demo", LONG_PARAGRAPH)
paragraphs = MOD.extract_paragraphs(Path("virtual.md"), text=text)
assert len(paragraphs) == 1
assert "family: issue" not in paragraphs[0].text
def test_code_fences_are_excluded(tmp_path: Path) -> None:
fenced = LONG_PARAGRAPH.replace("This paragraph exists", "This fenced sentence exists")
body = f"{LONG_PARAGRAPH}\n\n```bash\n{fenced}\n```\n\n## Next\n"
paragraphs = MOD.extract_paragraphs(Path("virtual.md"), text=body)
assert len(paragraphs) == 1
assert "fenced sentence" not in paragraphs[0].text
def test_code_fence_removal_does_not_fuse_neighbouring_paragraphs() -> None:
other_long_paragraph = (
"A second paragraph after the fence, long enough on its own to clear "
"the floor, and it must stay a separate paragraph from the one above "
"the fence once the fenced block itself has been removed entirely."
)
body = f"{LONG_PARAGRAPH}\n\n```bash\necho hello\n```\n\n{other_long_paragraph}\n"
paragraphs = MOD.extract_paragraphs(Path("virtual.md"), text=body)
assert len(paragraphs) == 2
# --- scoring ---------------------------------------------------------------------------
def test_exact_duplicate_scores_one() -> None:
a = MOD.extract_paragraphs(Path("a.md"), text=LONG_PARAGRAPH)[0]
b = MOD.extract_paragraphs(Path("b.md"), text=LONG_PARAGRAPH)[0]
pairs = MOD.find_pairs([a, b], min_score=0.0)
assert len(pairs) == 1
assert pairs[0].score == 1.0
def test_unrelated_paragraphs_score_nothing() -> None:
other = (
"Completely unrelated content about an entirely different subject "
"matter that shares essentially no nine-word sequence with the "
"filler paragraph used everywhere else across this particular test."
)
a = MOD.extract_paragraphs(Path("a.md"), text=LONG_PARAGRAPH)[0]
b = MOD.extract_paragraphs(Path("b.md"), text=other)[0]
pairs = MOD.find_pairs([a, b], min_score=0.0)
assert pairs == []
def test_same_file_pairs_are_never_compared() -> None:
a = MOD.extract_paragraphs(Path("a.md"), text=LONG_PARAGRAPH)[0]
a_again = MOD.extract_paragraphs(Path("a.md"), text=LONG_PARAGRAPH)[0]
pairs = MOD.find_pairs([a, a_again], min_score=0.0)
assert pairs == []
def test_score_is_symmetric() -> None:
"""`A <-> B` and `B <-> A` must score identically — the formula's
denominator (`min(|A|, |B|)`) does not depend on which side is which."""
variant = LONG_PARAGRAPH.replace("purely to carry", "specifically to carry along")
a = MOD.extract_paragraphs(Path("a.md"), text=LONG_PARAGRAPH)[0]
b = MOD.extract_paragraphs(Path("b.md"), text=variant)[0]
forward = MOD.find_pairs([a, b], min_score=0.0)[0].score
backward = MOD.find_pairs([b, a], min_score=0.0)[0].score
assert forward == backward
def test_pairing_is_stable_regardless_of_input_order() -> None:
variant = LONG_PARAGRAPH.replace("purely to carry", "specifically to carry along")
other = (
"Completely unrelated content about an entirely different subject "
"matter that shares essentially no nine-word sequence with the "
"filler paragraph used everywhere else across this particular test."
)
a = MOD.extract_paragraphs(Path("a.md"), text=LONG_PARAGRAPH)[0]
b = MOD.extract_paragraphs(Path("b.md"), text=variant)[0]
c = MOD.extract_paragraphs(Path("c.md"), text=other)[0]
forward = {(p.a.path, p.b.path, round(p.score, 6)) for p in MOD.find_pairs([a, b, c], min_score=0.0)}
reverse = {(p.a.path, p.b.path, round(p.score, 6)) for p in MOD.find_pairs([c, b, a], min_score=0.0)}
# Compare as unordered-file pairs, since which paragraph lands in `.a`
# vs `.b` may differ with input order, but the set of (file-pair, score)
# facts discovered must not.
normalise = lambda pairs: {(frozenset((f1, f2)), score) for f1, f2, score in pairs} # noqa: E731
assert normalise(forward) == normalise(reverse)
def test_report_band_pair_does_not_fail() -> None:
"""A pair scored between REPORT_THRESHOLD and FAIL_THRESHOLD is reported
but does not cross the fail line. Mutating every 15th word of the
36-word `LONG_PARAGRAPH` (indices 0, 15, 30) lands the containment score
at 0.4286 — precomputed once and fixed here, not re-derived per run."""
words = LONG_PARAGRAPH.split()
mutated = list(words)
for i in range(0, len(mutated), 15):
mutated[i] = f"altered{i}"
variant = " ".join(mutated)
a = MOD.extract_paragraphs(Path("a.md"), text=LONG_PARAGRAPH)[0]
b = MOD.extract_paragraphs(Path("b.md"), text=variant)[0]
pairs = MOD.find_pairs([a, b], min_score=0.0)
assert pairs, "expected the mutated paragraph to still register some overlap"
score = pairs[0].score
assert MOD.REPORT_THRESHOLD <= score <= MOD.FAIL_THRESHOLD, score
def test_pair_above_fail_threshold_fails(tmp_path: Path) -> None:
skills = tmp_path / "skills"
(skills / "one").mkdir(parents=True)
(skills / "two").mkdir(parents=True)
(skills / "one" / "SKILL.md").write_text(_skill("one", LONG_PARAGRAPH))
(skills / "two" / "SKILL.md").write_text(_skill("two", LONG_PARAGRAPH))
targets = MOD.discover_targets(
skills_root=skills, blocks_dir=tmp_path / "no-blocks", preflight_source=tmp_path / "no-preflight.md"
)
paragraphs = MOD.scan(targets)
pairs = MOD.find_pairs(paragraphs)
fail_pairs = [p for p in pairs if p.score > MOD.FAIL_THRESHOLD]
assert len(fail_pairs) == 1
assert fail_pairs[0].score == 1.0
# --- discovery ---------------------------------------------------------------------------
def test_discover_targets_follows_symlinked_skill_directories(tmp_path: Path) -> None:
"""`skills/<name>` is a symlink into `plugins/magpie-<family>/skills/<name>`
in this repo's own self-adoption layout — `discover_targets` must follow
it (`os.walk(..., followlinks=True)`), not silently skip every skill."""
real_root = tmp_path / "real" / "demo"
real_root.mkdir(parents=True)
(real_root / "SKILL.md").write_text(_skill("demo", LONG_PARAGRAPH))
skills_root = tmp_path / "skills"
skills_root.mkdir()
(skills_root / "demo").symlink_to(real_root, target_is_directory=True)
targets = MOD.discover_targets(
skills_root=skills_root,
blocks_dir=tmp_path / "no-blocks",
preflight_source=tmp_path / "no-preflight.md",
)
assert (skills_root / "demo" / "SKILL.md") in targets
def test_discover_targets_skips_cache_directories(tmp_path: Path) -> None:
skills_root = tmp_path / "skills"
cache = skills_root / ".pytest_cache"
cache.mkdir(parents=True)
(cache / "README.md").write_text("cache readme\n")
targets = MOD.discover_targets(
skills_root=skills_root,
blocks_dir=tmp_path / "no-blocks",
preflight_source=tmp_path / "no-preflight.md",
)
assert targets == []
def test_discover_targets_includes_blocks_and_preflight_source(tmp_path: Path) -> None:
blocks_dir = tmp_path / "blocks"
blocks_dir.mkdir()
(blocks_dir / "widget.md").write_text("widget body\n")
preflight = tmp_path / "preflight-block.md"
preflight.write_text("preflight body\n")
targets = MOD.discover_targets(
skills_root=tmp_path / "no-skills", blocks_dir=blocks_dir, preflight_source=preflight
)
assert blocks_dir / "widget.md" in targets
assert preflight in targets
# --- the real tree ---------------------------------------------------------------------
def test_real_tree_scan_does_not_crash_and_is_deterministic() -> None:
"""Not a duplication-content assertion (the live tree's actual duplicate
count is covered by running the CLI directly, not asserted in-suite,
since it is expected to change as skills are added or reworded) — this
just guards that a full real-tree scan runs cleanly and produces the
same result on a second pass. Paths are REPO-relative rather than the
module's bare defaults, since the workspace test runner's cwd is
`tools/dev`, not the repository root."""
targets = MOD.discover_targets(
skills_root=REPO / "skills",
blocks_dir=REPO / "tools" / "dev" / "blocks",
preflight_source=REPO / "tools" / "dev" / "preflight-block.md",
)
assert targets, "expected at least one file in scope"
first = MOD.scan(targets)
second = MOD.scan(targets)
assert len(first) == len(second)