Files
basicmachines-co-basic-memory/tests/scripts/test_generate_pr_infographic.py
phernandez ef6c47e674 feat(ci): ground PR images in delivery context, not just title/description
Mirror the ProjectUpdateContext shape the basic-memory.yml capture flow
collects: the image prompt now receives a compact change-shape digest —
labels, linked issues with titles, commit subjects (the PR's narrative
arc), and a churn-ranked changed-files summary with totals — from a
single 'gh pr view --json' call passed as --pr-context-file (replacing
--pr-title/--pr-body-file). The digest is explicitly context, not
captions: the prompt forbids rendering paths, stats, issue numbers, or
commit subjects verbatim in the image.

Signed-off-by: phernandez <paul@basicmachines.co>
2026-06-09 23:12:18 -05:00

446 lines
16 KiB
Python

import json
from pathlib import Path
import pytest
from click import unstyle
from typer.testing import CliRunner
from scripts import generate_infographic, generate_pr_infographic
def test_infographic_scripts_are_uv_typer_entrypoints() -> None:
for module in (generate_infographic, generate_pr_infographic):
source = module.__file__
assert source is not None
text = Path(source).read_text(encoding="utf-8")
assert text.startswith("#!/usr/bin/env -S uv run --script\n")
assert "# /// script" in text
assert "typer" in text
assert hasattr(module, "app")
def test_generate_pr_infographic_cli_help_exposes_useful_options() -> None:
result = CliRunner().invoke(generate_pr_infographic.app, ["--help"])
help_text = unstyle(result.output)
assert result.exit_code == 0
assert "--pr-number" in help_text
assert "--pr-context-file" in help_text
assert "--output" in help_text
assert "--theme" in help_text
assert "--provenance-output" in help_text
assert "--print-prompt" in help_text
assert "--dry-run" in help_text
def test_extract_pr_content_strips_managed_bot_blocks() -> None:
body = "\n".join(
[
"## Summary",
"Adds per-workspace rclone remotes for Team push/pull.",
"<!-- BM_BOSSBOT_SUMMARY:start -->",
"Reviewed SHA: abc123",
"Verdict: approve",
"<!-- BM_BOSSBOT_SUMMARY:end -->",
"<!-- pr-infographic:start -->",
"![BM Bossbot image for PR #42](https://example.test/img.webp)",
"<!-- pr-infographic:end -->",
"<!-- BM_INFOGRAPHIC_PROVENANCE:start -->",
"provenance details",
"<!-- BM_INFOGRAPHIC_PROVENANCE:end -->",
"## Test plan",
"pytest passes.",
]
)
content = generate_pr_infographic.extract_pr_content(body)
assert "Adds per-workspace rclone remotes" in content
assert "pytest passes." in content
assert "Verdict: approve" not in content
assert "Reviewed SHA" not in content
assert "provenance details" not in content
assert "BM Bossbot image for PR" not in content
def test_extract_pr_content_handles_body_without_managed_blocks() -> None:
assert generate_pr_infographic.extract_pr_content("Plain description") == "Plain description"
def test_build_change_shape_digests_delivery_context() -> None:
context = {
"labels": [{"name": "enhancement"}, {"name": "cloud"}],
"closingIssuesReferences": [{"number": 581, "title": "edit_note recovery"}],
"commits": [
{"messageHeadline": "fix(mcp): recover edit_note"},
{"messageHeadline": "fix(api): canonicalize sync-file paths"},
],
"files": [
{"path": "src/basic_memory/mcp/tools/edit_note.py", "additions": 120, "deletions": 30},
{"path": "tests/mcp/test_tool_edit_note.py", "additions": 80, "deletions": 5},
],
}
context["commits"].append({"messageHeadline": "Merge branch 'main' into fix/581"})
shape = generate_pr_infographic.build_change_shape(context)
assert "Labels: enhancement, cloud" in shape
assert "#581: edit_note recovery" in shape
assert "Commit subjects (2 total):" in shape
assert "Merge branch" not in shape
assert "fix(mcp): recover edit_note" in shape
assert "Files changed (2 total, +200/-35):" in shape
assert "src/basic_memory/mcp/tools/edit_note.py (+120/-30)" in shape
def test_build_change_shape_caps_long_lists_and_handles_empty_context() -> None:
context = {
"commits": [{"messageHeadline": f"commit {i}"} for i in range(15)],
"files": [{"path": f"file-{i}.py", "additions": i, "deletions": 0} for i in range(15)],
}
shape = generate_pr_infographic.build_change_shape(context)
assert "Commit subjects (15 total):" in shape
assert "- ... and 5 more" in shape
assert "- ... and 5 more files" in shape
# Files are ranked by churn, so the biggest file leads the list.
assert shape.index("file-14.py") < shape.index("file-5.py")
assert (
generate_pr_infographic.build_change_shape({}) == "(no additional change context available)"
)
def test_extract_infographic_theme_from_pr_body() -> None:
body = "\n".join(
[
"Before",
"<!-- BM_INFOGRAPHIC_THEME:start -->",
"Italian movie poster with a release-route map",
"<!-- BM_INFOGRAPHIC_THEME:end -->",
"After",
]
)
theme = generate_pr_infographic.extract_infographic_theme(body)
assert theme == "Italian movie poster with a release-route map"
def test_extract_infographic_theme_is_optional() -> None:
assert generate_pr_infographic.extract_infographic_theme("No theme") is None
def test_select_image_theme_reports_source() -> None:
body = "\n".join(
[
"<!-- BM_INFOGRAPHIC_THEME:start -->",
"paintings: Rembrandt-inspired merge gate",
"<!-- BM_INFOGRAPHIC_THEME:end -->",
]
)
from_body = generate_pr_infographic.select_image_theme(
pr_number=42,
pr_title="feat(ci): add a merge gate",
pr_body=body,
theme_override=None,
)
from_cli = generate_pr_infographic.select_image_theme(
pr_number=42,
pr_title="feat(ci): add a merge gate",
pr_body=body,
theme_override="80's action movies",
)
from_auto = generate_pr_infographic.select_image_theme(
pr_number=42,
pr_title="feat(ci): add a merge gate",
pr_body="No theme",
theme_override=None,
)
assert from_body.theme == "paintings: Rembrandt-inspired merge gate"
assert from_body.source == generate_pr_infographic.ThemeSource.PR_BODY
assert from_cli.theme == "80's action movies"
assert from_cli.source == generate_pr_infographic.ThemeSource.CLI
assert from_auto.theme in generate_pr_infographic.BM_IMAGE_THEME_POOL
assert from_auto.source == generate_pr_infographic.ThemeSource.AUTO
def test_build_infographic_prompt_depicts_pr_content_not_review_outcome() -> None:
prompt = generate_pr_infographic.build_infographic_prompt(
pr_number=42,
pr_title="feat(sync): stream large files during cloud sync",
pr_content="Streams PDFs in chunks instead of loading them fully into memory.",
change_shape="Labels: sync\nFiles changed (3 total, +90/-20):\n- src/basic_memory/sync/x.py (+80/-15)",
theme="WWII propaganda posters with home-front logistics routes",
theme_source=generate_pr_infographic.ThemeSource.CLI,
)
assert "PR #42" in prompt
assert "stream large files during cloud sync" in prompt
assert "Streams PDFs in chunks" in prompt
assert "WWII propaganda posters" in prompt
assert "User-supplied visual direction" in prompt
assert "style inspiration only" in prompt
assert "polished landscape WebP editorial image" in prompt
assert "image-first composition" in prompt
assert "symbolic tableau" in prompt
assert "Do not render an infographic" in prompt
assert "dashboard" in prompt
assert "flowchart" in prompt
assert "copyrighted characters" in prompt
# The subject is the change itself; review-process imagery is banned.
assert "CONTENT of the pull request" in prompt
assert "do not depict review verdicts" in prompt
assert "approval" in prompt.lower()
assert "stamps" in prompt
assert "checkmarks" in prompt
# Change shape grounds the imagery but must not become captions.
assert "Change shape" in prompt
assert "src/basic_memory/sync/x.py" in prompt
assert "never render file" in prompt
# The old prompt fed the review summary and named the approval status,
# which produced literal "BOSSBOT APPROVED" stamp images.
assert "BM Bossbot summary:" not in prompt
assert "BM Bossbot Approval" not in prompt
def test_build_infographic_provenance_block_includes_image_choices_without_prompt() -> None:
block = generate_pr_infographic.build_infographic_provenance_block(
pr_number=42,
output_path=Path("docs/assets/infographics/pr-42.webp"),
model="gpt-image-2",
size="1536x1024",
quality="high",
theme="classic black-and-white photography",
theme_source=generate_pr_infographic.ThemeSource.CLI,
)
assert generate_pr_infographic.PROVENANCE_START in block
assert generate_pr_infographic.PROVENANCE_END in block
assert "BM Bossbot image choices" in block
assert "Generated asset: `docs/assets/infographics/pr-42.webp`" in block
assert "Image model: `gpt-image-2`" in block
assert "Size: `1536x1024`" in block
assert "Quality: `high`" in block
assert "Image mode: `editorial-image`" in block
assert "Theme source: `cli`" in block
assert "classic black-and-white photography" in block
assert "Image prompt sent to" not in block
assert "Images API revised prompt" not in block
def test_upsert_managed_block_appends_and_replaces() -> None:
first = "\n".join(
[
generate_pr_infographic.PROVENANCE_START,
"first",
generate_pr_infographic.PROVENANCE_END,
]
)
second = "\n".join(
[
generate_pr_infographic.PROVENANCE_START,
"second",
generate_pr_infographic.PROVENANCE_END,
]
)
appended = generate_pr_infographic.upsert_managed_block(
"Existing body",
block=first,
start=generate_pr_infographic.PROVENANCE_START,
end=generate_pr_infographic.PROVENANCE_END,
)
replaced = generate_pr_infographic.upsert_managed_block(
appended,
block=second,
start=generate_pr_infographic.PROVENANCE_START,
end=generate_pr_infographic.PROVENANCE_END,
)
assert appended == f"Existing body\n\n{first}\n"
assert "first" not in replaced
assert "second" in replaced
assert replaced.count(generate_pr_infographic.PROVENANCE_START) == 1
def test_build_infographic_prompt_uses_auto_theme_as_visual_direction() -> None:
theme = generate_pr_infographic.select_image_theme(
pr_number=42,
pr_title="feat(ci): add a merge gate",
pr_body="No theme",
theme_override=None,
)
prompt = generate_pr_infographic.build_infographic_prompt(
pr_number=42,
pr_title="feat(ci): add a merge gate",
pr_content="Adds a deterministic merge gate for pull requests.",
change_shape="(no additional change context available)",
theme=theme.theme,
theme_source=theme.source,
)
assert "Selected BM visual direction" in prompt
assert theme.theme in prompt
assert "Use image-first composition" in prompt
assert "movie poster" in prompt
assert "painting" in prompt
assert "classic photograph" in prompt
assert "scene" in prompt
assert "poster" in prompt
assert "cover image" in prompt
assert "symbolic tableau" in prompt
assert "Use at most a short title" in prompt
assert "Do not render an infographic" in prompt
assert "dashboard" in prompt
assert "flowchart" in prompt
assert "bullet-list panel" in prompt
@pytest.mark.parametrize("flag", ["--print-prompt", "--dry-run"])
def test_generate_pr_infographic_can_print_prompt_without_image_call(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
flag: str,
) -> None:
context_file = tmp_path / "pr-context.json"
context_file.write_text(
json.dumps(
{
"title": "feat(ci): add a merge gate",
"body": "\n".join(
[
"Adds a deterministic merge gate for pull requests.",
"<!-- BM_BOSSBOT_SUMMARY:start -->",
"Verdict: approve",
"Summary: review artifact that must not reach the image.",
"<!-- BM_BOSSBOT_SUMMARY:end -->",
"<!-- BM_INFOGRAPHIC_THEME:start -->",
"space exploration and astronomy",
"<!-- BM_INFOGRAPHIC_THEME:end -->",
]
),
"labels": [{"name": "ci"}],
"commits": [{"messageHeadline": "feat(ci): add a merge gate"}],
"files": [{"path": ".github/workflows/gate.yml", "additions": 40, "deletions": 2}],
"closingIssuesReferences": [{"number": 7, "title": "merge gate"}],
}
),
encoding="utf-8",
)
def fail_generate_image_result(**_: object) -> generate_infographic.GeneratedImage:
raise AssertionError("print-prompt mode must not call image generation")
monkeypatch.setattr(
generate_pr_infographic, "generate_image_result", fail_generate_image_result
)
output = tmp_path / "docs/assets/infographics/pr-42.webp"
result = CliRunner().invoke(
generate_pr_infographic.app,
[
"--pr-number",
"42",
"--pr-context-file",
str(context_file),
"--output",
str(output),
flag,
],
)
assert result.exit_code == 0, result.output
assert (
"Create a polished landscape WebP editorial image for Basic Memory PR #42" in result.output
)
assert "feat(ci): add a merge gate" in result.output
assert "Adds a deterministic merge gate" in result.output
assert "space exploration and astronomy" in result.output
assert "Labels: ci" in result.output
assert ".github/workflows/gate.yml" in result.output
assert "#7: merge gate" in result.output
assert "image-first composition" in result.output
assert "Do not render an infographic" in result.output
assert "Verdict: approve" not in result.output
assert "must not reach the image" not in result.output
assert not output.exists()
def test_generate_pr_infographic_writes_provenance_after_image_generation(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
context_file = tmp_path / "pr-context.json"
context_file.write_text(
json.dumps(
{
"title": "feat(ci): add a merge gate",
"body": "\n".join(
[
"Adds a merge gate.",
"<!-- BM_INFOGRAPHIC_THEME:start -->",
"paintings: Rembrandt-inspired merge gate",
"<!-- BM_INFOGRAPHIC_THEME:end -->",
]
),
}
),
encoding="utf-8",
)
def fake_generate_image_result(**kwargs: object) -> generate_infographic.GeneratedImage:
output_path = kwargs["output_path"]
assert isinstance(output_path, Path)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_bytes(b"fake-webp")
return generate_infographic.GeneratedImage(
path=output_path,
revised_prompt="A Rembrandt-inspired painting of a robot guarding a merge gate.",
)
monkeypatch.setattr(
generate_pr_infographic, "generate_image_result", fake_generate_image_result
)
output = tmp_path / "docs/assets/infographics/pr-42.webp"
provenance = tmp_path / "provenance.md"
result = CliRunner().invoke(
generate_pr_infographic.app,
[
"--pr-number",
"42",
"--pr-context-file",
str(context_file),
"--output",
str(output),
"--provenance-output",
str(provenance),
],
)
assert result.exit_code == 0, result.output
assert output.exists()
text = provenance.read_text(encoding="utf-8")
assert "Generated asset:" in text
assert "Image mode: `editorial-image`" in text
assert "Theme source: `pr-body`" in text
assert "paintings: Rembrandt-inspired merge gate" in text
assert "Image prompt sent to" not in text
assert "Images API revised prompt" not in text
assert "robot guarding a merge gate" not in text
assert "Adds a merge gate" not in text
def test_validate_output_path_must_stay_under_docs_assets_infographics(tmp_path: Path) -> None:
good = tmp_path / "docs/assets/infographics/pr-42.webp"
bad = tmp_path / "docs/assets/pr-42.webp"
assert generate_infographic.validate_output_path(good, repo_root=tmp_path) == good
with pytest.raises(ValueError, match="docs/assets/infographics"):
generate_infographic.validate_output_path(bad, repo_root=tmp_path)