Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# readme2demo — verified tutorials & demo videos from your README

[![tests](https://github.com/alphacrack/readme2demo/actions/workflows/ci.yml/badge.svg)](https://github.com/alphacrack/readme2demo/actions/workflows/ci.yml)
[![CI tests status](https://github.com/alphacrack/readme2demo/actions/workflows/ci.yml/badge.svg)](https://github.com/alphacrack/readme2demo/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/alphacrack/readme2demo/blob/main/LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://github.com/alphacrack/readme2demo/blob/main/pyproject.toml)

Expand Down Expand Up @@ -183,9 +183,9 @@ MIT licensed. The CLI and verification pipeline are, and will stay, free and ope

A huge thank you to everyone who has contributed to readme2demo!

[![Contributors](https://img.shields.io/github/contributors/alphacrack/readme2demo?style=flat-square)](https://github.com/alphacrack/readme2demo/graphs/contributors)
[![Contributor count](https://img.shields.io/github/contributors/alphacrack/readme2demo?style=flat-square)](https://github.com/alphacrack/readme2demo/graphs/contributors)

<a href="https://github.com/alphacrack/readme2demo/graphs/contributors">
<img src="https://contrib.rocks/image?repo=alphacrack/readme2demo" alt="Contributors" />
<img src="https://contrib.rocks/image?repo=alphacrack/readme2demo" alt="Contributor avatars" />
</a>

4 changes: 2 additions & 2 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ readme2demo run against its own repo: install, run the tests, explore the CLI,
and read a verified example — each step replayed in a fresh container before
this was rendered.

![readme2demo self-demo](readme2demo/demo.gif)
![Verified demo: readme2demo installing and running its own CLI in a clean container](readme2demo/demo.gif)

### toolhive — another example

![toolhive demo](toolhive/demo.gif)
![Verified demo: installing and running stacklok/toolhive step by step](toolhive/demo.gif)

## What's in each folder

Expand Down
2 changes: 1 addition & 1 deletion src/readme2demo/templates/tutorial.md.j2
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Expected output:
{% if has_video %}
## Demo

![Demo video: every step above executing in a clean container](demo.gif)
![Demo video: {{ title }} — every step above executing in a clean container](demo.gif)

Full video: [demo.mp4](demo.mp4)

Expand Down
73 changes: 73 additions & 0 deletions tests/test_tutorial.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,79 @@ def test_tutorial_md_front_matter_and_provenance(tmp_path, monkeypatch):
assert "generator: readme2demo" in sbs


def test_tutorial_md_demo_alt_is_repo_specific(tmp_path, monkeypatch):
"""Regression (#197): demo.gif alt must include the tutorial title, not a generic constant.

Every generated tutorial used the same hardcoded alt string, so screen
readers (and SEO) could not tell one repo's demo from another.
"""
from readme2demo import llm as llm_mod
from readme2demo.tutorial import run_tutorial
from readme2demo.types import (
AgentResult, CommandLog, Plan, SuccessCriteria, TutorialOutline,
TutorialStep,
)

outline = TutorialOutline(
title="Install ToolHive",
intro="A container tool.",
steps=[TutorialStep(title="Run", command="thv --help", explanation="Shows help.")],
)
monkeypatch.setattr(
llm_mod, "complete_json", lambda *a, **k: (outline.model_copy(deep=True), 0.01)
)
plan = Plan(
quickstart_summary="q",
success_criteria=SuccessCriteria(command="thv --help"),
prereqs=[],
)
log = CommandLog(engine="claude-code", result=AgentResult(outcome="success"))
# has_video is a filesystem check — create a gif so the Demo block renders.
(tmp_path / "demo.gif").write_bytes(b"GIF89a" + b"\x00" * 100)
run_tutorial(
tmp_path, plan, log, outline, "m", verified=True, base_image="img",
commit_sha="abc1234", repo_url="https://github.com/stacklok/toolhive",
)
text = (tmp_path / "tutorial.md").read_text(encoding="utf-8")
assert "![Demo video: Install ToolHive" in text
assert "every step above executing in a clean container](demo.gif)" in text
# Must not ship the old generic-only alt (title missing).
assert "![Demo video: every step above executing in a clean container](demo.gif)" not in text


def test_tutorial_md_demo_alt_guide_only_has_title(tmp_path, monkeypatch):
"""Regression (#197): guide-only runs (empty repo_url) still get sensible alt."""
from readme2demo import llm as llm_mod
from readme2demo.tutorial import run_tutorial
from readme2demo.types import (
AgentResult, CommandLog, Plan, SuccessCriteria, TutorialOutline,
TutorialStep,
)

outline = TutorialOutline(
title="Guide-only walkthrough",
intro="No repo.",
steps=[TutorialStep(title="Run", command="echo hi", explanation="Says hi.")],
)
monkeypatch.setattr(
llm_mod, "complete_json", lambda *a, **k: (outline.model_copy(deep=True), 0.01)
)
plan = Plan(
quickstart_summary="q",
success_criteria=SuccessCriteria(command="echo hi"),
)
log = CommandLog(engine="claude-code", result=AgentResult(outcome="success"))
(tmp_path / "demo.gif").write_bytes(b"GIF89a" + b"\x00" * 100)
run_tutorial(
tmp_path, plan, log, outline, "m", verified=True, base_image="img",
commit_sha=None, repo_url="",
)
text = (tmp_path / "tutorial.md").read_text(encoding="utf-8")
assert "![Demo video: Guide-only walkthrough" in text
assert "— —" not in text # no stray double dash from empty fields



def test_guide_front_matter_does_not_break_tape_parsing(tmp_path):
from readme2demo.distill import parse_guide_steps

Expand Down