| # 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-quickstart-recording.py``. | |
| Every test builds a miniature repository in ``tmp_path`` and runs one check | |
| against it. The point of each is the **red** case: a gate that cannot fail is | |
| worse than no gate, because a green run reads as evidence while measuring | |
| nothing. So each check is exercised in both directions — the broken tree is | |
| reported, and the corrected one is silent. | |
| The staleness check is the one worth the most care. It is the whole reason the | |
| renderer is byte-deterministic, and it is the only thing standing between an | |
| authored screenshot and silent drift from its source, so it is tested by | |
| actually mutating a rendered file rather than by asserting on a mock. | |
| """ | |
| from __future__ import annotations | |
| import importlib.util | |
| import os | |
| import subprocess | |
| from collections.abc import Iterator | |
| from pathlib import Path | |
| from types import ModuleType | |
| import pytest | |
| _HERE = Path(__file__).resolve().parents[1] | |
| _SCRIPT = _HERE / "check-quickstart-recording.py" | |
| _RENDERER = _HERE / "render-screenshot.sh" | |
| _WIZARD = _HERE / "render-wizard.py" | |
| def _load() -> ModuleType: | |
| spec = importlib.util.spec_from_file_location("check_quickstart_recording", _SCRIPT) | |
| assert spec and spec.loader | |
| module = importlib.util.module_from_spec(spec) | |
| spec.loader.exec_module(module) | |
| return module | |
| mod = _load() | |
| LICENCE = "Licensed to the Apache Software Foundation" | |
| TRANSCRIPT = "> /magpie-pairing:self-review\n\n Nothing was sent.\n" | |
| def _skill(repo: Path, name: str, family: str) -> None: | |
| """A skill in the flat tree, and the plugin directory that ships it.""" | |
| d = repo / "skills" / name | |
| d.mkdir(parents=True, exist_ok=True) | |
| (d / "SKILL.md").write_text(f"---\nname: {name}\nfamily: {family}\n---\n", encoding="utf-8") | |
| def _alias(repo: Path, family: str, alias: str) -> None: | |
| (repo / "plugins" / f"magpie-{family}" / "skills" / alias).mkdir(parents=True, exist_ok=True) | |
| def _svg(path: Path, *, licence: bool = True, valid: bool = True) -> None: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| header = f"<!--\n {LICENCE}\n-->\n" if licence else "" | |
| body = '<svg xmlns="http://www.w3.org/2000/svg"></svg>\n' if valid else "<svg>\n" | |
| path.write_text(header + body, encoding="utf-8") | |
| def _render(repo: Path, txt: Path) -> None: | |
| """Render through the real script, so a test tree matches a real one.""" | |
| subprocess.run( | |
| ["bash", str(_RENDERER), str(txt.relative_to(repo))], | |
| cwd=repo, | |
| check=True, | |
| capture_output=True, | |
| ) | |
| @pytest.fixture | |
| def repo(tmp_path: Path) -> Iterator[Path]: | |
| """A miniature repo, cd'd into — the script resolves paths relative to cwd. | |
| One family, one skill, one screenshot, and the setup recording: the | |
| smallest tree the checker calls complete. | |
| """ | |
| (tmp_path / "tools" / "dev").mkdir(parents=True) | |
| (tmp_path / "tools" / "dev" / "render-screenshot.sh").symlink_to(_RENDERER) | |
| # The wizard generator derives from frontmatter across the whole tree, which | |
| # a two-file fixture cannot stand in for. Give the checker a generator that | |
| # is present and agrees the (empty) wizard directory is correct, so the rest | |
| # of the tree is what these tests are measuring. | |
| stub = tmp_path / "tools" / "dev" / "render-wizard.py" | |
| stub.write_text("import sys\nsys.exit(0)\n", encoding="utf-8") | |
| _skill(tmp_path, "pairing-self-review", "pairing") | |
| _alias(tmp_path, "pairing", "self-review") | |
| shots = tmp_path / "assets" / "quickstart" / "families" / "pairing" | |
| shots.mkdir(parents=True) | |
| (shots / "self-review.txt").write_text(TRANSCRIPT, encoding="utf-8") | |
| for top in ( | |
| "magpie-setup.svg", | |
| "install.svg", | |
| "step-install.svg", | |
| "step-isolation.svg", | |
| "step-use.svg", | |
| "step-adopt.svg", | |
| ): | |
| _svg(tmp_path / "assets" / "quickstart" / top) | |
| (tmp_path / "docs" / "pairing").mkdir(parents=True) | |
| (tmp_path / "docs" / "pairing" / "install.md").write_text( | |
| "\n", | |
| encoding="utf-8", | |
| ) | |
| (tmp_path / "docs" / "quick-start.md").write_text( | |
| "".join( | |
| f"\\n" | |
| for n in ( | |
| "install.svg", | |
| "step-install.svg", | |
| "step-isolation.svg", | |
| "step-use.svg", | |
| "step-adopt.svg", | |
| "magpie-setup.svg", | |
| ) | |
| ), | |
| encoding="utf-8", | |
| ) | |
| walk = tmp_path / "assets" / "quickstart" / "walkthrough" | |
| walk.mkdir(parents=True) | |
| for step in ("1-stops", "2-wizard"): | |
| (walk / f"{step}.txt").write_text(f"> step {step}\n", encoding="utf-8") | |
| (tmp_path / "docs" / "quick-start").mkdir(parents=True) | |
| (tmp_path / "docs" / "quick-start" / "first-run.md").write_text( | |
| "\n" | |
| "\n", | |
| encoding="utf-8", | |
| ) | |
| cwd = Path.cwd() | |
| os.chdir(tmp_path) | |
| try: | |
| _render(tmp_path, shots / "self-review.txt") | |
| for step in ("1-stops", "2-wizard"): | |
| _render(tmp_path, walk / f"{step}.txt") | |
| yield tmp_path | |
| finally: | |
| os.chdir(cwd) | |
| def test_a_complete_tree_is_silent(repo: Path) -> None: | |
| assert mod.main() == 0 | |
| # --------------------------------------------------------------------------- | |
| # Pairing: a transcript and its rendered SVG travel together | |
| # --------------------------------------------------------------------------- | |
| def test_a_transcript_with_no_svg_is_reported(repo: Path) -> None: | |
| (repo / "assets/quickstart/families/pairing/self-review.svg").unlink() | |
| errs = mod.check_screenshots(["pairing"]) | |
| assert any("self-review.svg" in e and "missing" in e for e in errs) | |
| def test_an_svg_with_no_transcript_is_reported(repo: Path) -> None: | |
| _svg(repo / "assets/quickstart/families/pairing/orphan.svg") | |
| errs = mod.check_screenshots(["pairing"]) | |
| assert any("orphan.svg" in e and "no transcript" in e for e in errs) | |
| # --------------------------------------------------------------------------- | |
| # Staleness: the guard a hand-authored image cannot have | |
| # --------------------------------------------------------------------------- | |
| def test_an_svg_that_no_longer_matches_its_transcript_is_reported(repo: Path) -> None: | |
| svg = repo / "assets/quickstart/families/pairing/self-review.svg" | |
| svg.write_text(svg.read_text().replace("Nothing was sent.", "Everything was sent.")) | |
| errs = mod.check_regenerates() | |
| assert any("stale" in e for e in errs), errs | |
| def test_editing_the_transcript_alone_is_reported(repo: Path) -> None: | |
| """The direction that actually happens: someone fixes a line in the .txt | |
| and forgets to re-render.""" | |
| txt = repo / "assets/quickstart/families/pairing/self-review.txt" | |
| txt.write_text(TRANSCRIPT + " One more line.\n", encoding="utf-8") | |
| assert any("stale" in e for e in mod.check_regenerates()) | |
| def test_re_rendering_clears_the_staleness(repo: Path) -> None: | |
| txt = repo / "assets/quickstart/families/pairing/self-review.txt" | |
| txt.write_text(TRANSCRIPT + " One more line.\n", encoding="utf-8") | |
| _render(repo, txt) | |
| assert mod.check_regenerates() == [] | |
| # --------------------------------------------------------------------------- | |
| # Coverage and naming | |
| # --------------------------------------------------------------------------- | |
| def test_a_family_with_no_screenshots_is_reported(repo: Path) -> None: | |
| _skill(repo, "security-issue-triage", "security") | |
| _alias(repo, "security", "issue-triage") | |
| errs = mod.check_screenshots(["pairing", "security"]) | |
| assert any("security" in e and "no screenshots" in e for e in errs) | |
| def test_a_screenshot_directory_no_family_claims_is_reported(repo: Path) -> None: | |
| (repo / "assets/quickstart/families/ghost").mkdir() | |
| errs = mod.check_screenshots(["pairing"]) | |
| assert any("ghost" in e for e in errs) | |
| def test_a_screenshot_naming_a_skill_the_family_does_not_ship_is_reported(repo: Path) -> None: | |
| shots = repo / "assets/quickstart/families/pairing" | |
| (shots / "not-a-skill.txt").write_text(TRANSCRIPT, encoding="utf-8") | |
| _render(repo, shots / "not-a-skill.txt") | |
| errs = mod.check_screenshots(["pairing"]) | |
| assert any("not-a-skill" in e and "ships no skill" in e for e in errs) | |
| def test_a_screenshot_the_install_page_does_not_embed_is_reported(repo: Path) -> None: | |
| (repo / "docs/pairing/install.md").write_text("no images here\n", encoding="utf-8") | |
| errs = mod.check_screenshots(["pairing"]) | |
| assert any("does not embed" in e for e in errs) | |
| # --------------------------------------------------------------------------- | |
| # The SVG itself | |
| # --------------------------------------------------------------------------- | |
| def test_an_svg_without_the_licence_header_is_reported(repo: Path) -> None: | |
| path = repo / "assets/quickstart/magpie-setup.svg" | |
| _svg(path, licence=False) | |
| assert any("licence header" in e for e in mod.check_svg(path)) | |
| def test_an_unparsable_svg_is_reported(repo: Path) -> None: | |
| path = repo / "assets/quickstart/magpie-setup.svg" | |
| _svg(path, valid=False) | |
| assert any("not parsable" in e for e in mod.check_svg(path)) | |
| def test_an_oversized_svg_is_reported(repo: Path) -> None: | |
| path = repo / "assets/quickstart/magpie-setup.svg" | |
| _svg(path) | |
| path.write_text(path.read_text() + "<!--" + "x" * (mod.MAX_BYTES + 1) + "-->") | |
| assert any("exceeds" in e for e in mod.check_svg(path)) | |
| # --------------------------------------------------------------------------- | |
| # Retirement: three sets are gone and must stay gone | |
| # --------------------------------------------------------------------------- | |
| def test_a_surviving_first_run_recording_is_reported(repo: Path) -> None: | |
| _svg(repo / "assets/quickstart/families/pairing-first-run.svg") | |
| assert any("first-run" in e for e in mod.check_no_retired_references()) | |
| def test_a_doc_referencing_a_first_run_recording_is_reported(repo: Path) -> None: | |
| (repo / "docs/pairing/README.md").write_text( | |
| "\n", encoding="utf-8" | |
| ) | |
| assert any("retired asset" in e for e in mod.check_no_retired_references()) | |
| def test_a_revived_examples_directory_is_reported(repo: Path) -> None: | |
| (repo / "assets/examples").mkdir(parents=True) | |
| assert any("assets/examples" in e for e in mod.check_no_retired_references()) | |
| def test_a_new_png_still_is_reported(repo: Path) -> None: | |
| (repo / "assets/quickstart/install.png").write_bytes(b"\x89PNG\r\n") | |
| assert any("install.png" in e for e in mod.check_no_retired_references()) | |
| def test_a_design_document_may_name_what_was_retired(repo: Path) -> None: | |
| """A design records what was replaced. Rewriting the record to satisfy a | |
| linter would make it wrong.""" | |
| designs = repo / "docs" / "designs" | |
| designs.mkdir(parents=True) | |
| (designs / "2026-01-01-x.md").write_text( | |
| "the nine assets/quickstart/families/pairing-first-run.svg files are gone\n", | |
| encoding="utf-8", | |
| ) | |
| assert mod.check_no_retired_references() == [] | |
| # --------------------------------------------------------------------------- | |
| # The one real recording | |
| # --------------------------------------------------------------------------- | |
| def test_the_quick_start_must_embed_the_recording(repo: Path) -> None: | |
| (repo / "docs/quick-start.md").write_text("no recording here\n", encoding="utf-8") | |
| errs = mod.check_embedded(Path("docs/quick-start.md"), mod.SETUP_RECORDING) | |
| assert any("does not embed" in e for e in errs) | |
| def test_a_missing_recording_is_an_error_not_a_note(repo: Path) -> None: | |
| """There is no placeholder state any more: one recording is a thing a | |
| person does once.""" | |
| (repo / "assets/quickstart/magpie-setup.svg").unlink() | |
| assert any("missing" in e for e in mod.check_svg(mod.SETUP_RECORDING)) | |
| # --------------------------------------------------------------------------- | |
| # The first-run walkthrough: every step pictured, in the order it runs | |
| # --------------------------------------------------------------------------- | |
| def test_a_walkthrough_step_the_chapter_does_not_embed_is_reported(repo: Path) -> None: | |
| (repo / "docs/quick-start/first-run.md").write_text( | |
| "\n", encoding="utf-8" | |
| ) | |
| assert any("2-wizard.svg" in e and "does not embed" in e for e in mod.check_walkthrough()) | |
| def test_walkthrough_steps_embedded_out_of_order_are_reported(repo: Path) -> None: | |
| """The one thing no link check can see: every image resolves, and the page | |
| still teaches the wrong sequence.""" | |
| (repo / "docs/quick-start/first-run.md").write_text( | |
| "\n" | |
| "\n", | |
| encoding="utf-8", | |
| ) | |
| assert any("out of order" in e for e in mod.check_walkthrough()) | |
| def test_a_walkthrough_transcript_with_no_svg_is_reported(repo: Path) -> None: | |
| (repo / "assets/quickstart/walkthrough/2-wizard.svg").unlink() | |
| assert any("2-wizard.svg" in e and "missing" in e for e in mod.check_walkthrough()) | |
| def test_a_walkthrough_svg_with_no_transcript_is_reported(repo: Path) -> None: | |
| _svg(repo / "assets/quickstart/walkthrough/3-orphan.svg") | |
| assert any("3-orphan.svg" in e and "no transcript" in e for e in mod.check_walkthrough()) | |
| def test_a_missing_first_run_chapter_is_reported(repo: Path) -> None: | |
| (repo / "docs/quick-start/first-run.md").unlink() | |
| assert any("first-run.md" in e for e in mod.check_walkthrough()) | |
| # --------------------------------------------------------------------------- | |
| # The per-family wizard animations | |
| # --------------------------------------------------------------------------- | |
| def test_a_stale_wizard_animation_is_reported(repo: Path) -> None: | |
| """The generator owns the comparison; the checker must surface what it says | |
| rather than swallowing a non-zero exit.""" | |
| (repo / "tools/dev/render-wizard.py").write_text( | |
| "import sys\nprint('assets/quickstart/wizard/x.svg: stale', file=sys.stderr)\nsys.exit(1)\n", | |
| encoding="utf-8", | |
| ) | |
| assert any("stale" in e for e in mod.check_wizards()) | |
| def test_a_wizard_animation_its_family_does_not_embed_is_reported(repo: Path) -> None: | |
| wizard = repo / "assets/quickstart/wizard" | |
| wizard.mkdir(parents=True) | |
| _svg(wizard / "pairing.svg") | |
| assert any("does not embed" in e for e in mod.check_wizards()) | |
| def test_an_embedded_wizard_animation_is_silent(repo: Path) -> None: | |
| wizard = repo / "assets/quickstart/wizard" | |
| wizard.mkdir(parents=True) | |
| _svg(wizard / "pairing.svg") | |
| (repo / "docs/pairing/install.md").write_text( | |
| "\n" | |
| "\n", | |
| encoding="utf-8", | |
| ) | |
| assert mod.check_wizards() == [] | |
| def test_a_missing_generator_is_reported(repo: Path) -> None: | |
| (repo / "tools/dev/render-wizard.py").unlink() | |
| assert any("nothing can verify" in e for e in mod.check_wizards()) | |
| def test_the_quick_start_must_open_with_the_hero(repo: Path) -> None: | |
| """The hero exists to be the first thing on the page. A quick start that | |
| merely contains it somewhere has lost the point of generating it.""" | |
| (repo / "docs/quick-start.md").write_text( | |
| "\n", encoding="utf-8" | |
| ) | |
| errs = mod.check_embedded(Path("docs/quick-start.md"), mod.HERO) | |
| assert any("does not embed" in e for e in errs) |