mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
ef6c47e674
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>
446 lines
16 KiB
Python
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 -->",
|
|
"",
|
|
"<!-- 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)
|