chore(ci): delete bossbot script and workflow guard tests

CI tooling doesn't need product-suite tests burning time on every
matrix leg. The bossbot status script is exercised by every PR run.

Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
phernandez
2026-06-10 10:21:55 -05:00
parent 666ec94e0f
commit 859c9453a4
8 changed files with 2 additions and 1703 deletions
-4
View File
@@ -54,10 +54,6 @@ jobs:
run: |
just typecheck
- name: Run CI tooling tests
run: |
just test-ci-tooling
- name: Run linting
run: |
just lint
+2 -6
View File
@@ -40,15 +40,11 @@ test-postgres: test-unit-postgres test-int-postgres
# Run unit tests against SQLite
test-unit-sqlite: testmon-seed
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{TESTMON_FLAGS}} --testmon-env=unit-sqlite --ignore=tests/scripts --ignore=tests/ci tests
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{TESTMON_FLAGS}} --testmon-env=unit-sqlite tests
# Run unit tests against Postgres
test-unit-postgres: testmon-seed
BASIC_MEMORY_ENV=test BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest -p pytest_mock -v --no-cov {{TESTMON_FLAGS}} --testmon-env=unit-postgres --ignore=tests/scripts --ignore=tests/ci tests
# Run CI-tooling tests (bossbot scripts, workflow guards) — once, not per matrix leg
test-ci-tooling:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov tests/scripts tests/ci
BASIC_MEMORY_ENV=test BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest -p pytest_mock -v --no-cov {{TESTMON_FLAGS}} --testmon-env=unit-postgres tests
# Run integration tests against SQLite (excludes semantic tests and on-demand benchmarks —
# use just test-semantic / run benchmark files explicitly)
-205
View File
@@ -1,205 +0,0 @@
from pathlib import Path
import yaml
WORKFLOW_PATH = Path(".github/workflows/bm-bossbot.yml")
PROMPT_PATH = Path(".github/basic-memory/bm-bossbot-review.md")
def _workflow() -> dict:
return yaml.safe_load(WORKFLOW_PATH.read_text(encoding="utf-8"))
def test_bm_bossbot_runs_after_successful_tests_workflow() -> None:
workflow = _workflow()
review_job = workflow["jobs"]["review"]
assert workflow["name"] == "BM Bossbot"
assert "pull_request_target" not in workflow["on"]
assert workflow["on"]["workflow_run"]["workflows"] == ["Tests"]
assert workflow["on"]["workflow_run"]["types"] == ["completed"]
assert "workflow_dispatch" in workflow["on"]
assert "github.event.workflow_run.conclusion == 'success'" in review_job["if"]
assert "github.event.workflow_run.pull_requests[0].number != ''" in review_job["if"]
assert review_job["outputs"]["should_review"] == "${{ steps.pr.outputs.should_review }}"
permissions = workflow["permissions"]
assert permissions["contents"] == "read"
assert permissions["pull-requests"] == "write"
assert permissions["statuses"] == "write"
asset_permissions = workflow["jobs"]["assets"]["permissions"]
assert asset_permissions["contents"] == "write"
assert asset_permissions["pull-requests"] == "write"
def test_bm_bossbot_workflow_never_checks_out_untrusted_head() -> None:
workflow = _workflow()
checkout_steps = [
step
for job in workflow["jobs"].values()
for step in job["steps"]
if step.get("uses") == "actions/checkout@v6"
]
assert checkout_steps
for checkout_step in checkout_steps:
assert checkout_step["with"]["ref"] == "${{ github.event.repository.default_branch }}"
assert "${{ github.event.pull_request.head.sha }}" not in str(checkout_step)
assert "github.event.pull_request" not in WORKFLOW_PATH.read_text(encoding="utf-8")
assert "cancel-in-progress: true" in WORKFLOW_PATH.read_text(encoding="utf-8")
def test_bm_bossbot_workflow_has_deterministic_status_steps() -> None:
workflow = _workflow()
steps = workflow["jobs"]["review"]["steps"]
names = [step["name"] for step in steps]
assert "Set up uv" in names
assert "Mark BM Bossbot approval pending" in names
assert "Run BM Bossbot review with Codex" in names
assert "Finalize BM Bossbot approval" in names
run_codex = next(step for step in steps if step["name"] == "Run BM Bossbot review with Codex")
assert run_codex["uses"] == "openai/codex-action@v1"
assert run_codex["with"]["openai-api-key"] == "${{ secrets.OPENAI_API_KEY }}"
assert "--output-schema" in run_codex["with"]["codex-args"]
assert "steps.pr.outputs.should_review == 'true'" in run_codex["if"]
pending = next(step for step in steps if step["name"] == "Mark BM Bossbot approval pending")
assert pending["if"] == "steps.pr.outputs.should_review == 'true'"
finalize = next(step for step in steps if step["name"] == "Finalize BM Bossbot approval")
assert finalize["if"] == "always() && steps.pr.outputs.should_review == 'true'"
assert "BM Bossbot Approval" in WORKFLOW_PATH.read_text(encoding="utf-8")
assert "uv run --script scripts/bm_bossbot_status.py pending" in WORKFLOW_PATH.read_text(
encoding="utf-8"
)
assert "uv run --script scripts/bm_bossbot_status.py finalize" in WORKFLOW_PATH.read_text(
encoding="utf-8"
)
def test_bm_bossbot_rejects_stale_successful_test_runs_before_codex() -> None:
workflow_text = WORKFLOW_PATH.read_text(encoding="utf-8")
workflow = _workflow()
steps = workflow["jobs"]["review"]["steps"]
normalize = next(step for step in steps if step["name"] == "Normalize PR event")
classify = next(step for step in steps if step["name"] == "Classify PR author")
assert "tested_sha" in normalize["run"]
assert "current_head_sha" in normalize["run"]
assert "actions/workflows/test.yml/runs" in normalize["run"]
assert "-f event=push" in normalize["run"]
assert "-f event=pull_request" not in normalize["run"]
assert "-f head_sha=\"${current_head_sha}\"" in normalize["run"]
assert 'select(.conclusion == "success")' in normalize["run"]
assert "no successful Tests workflow for ${current_head_sha}" in workflow_text
stale_sha_guard = '[ -n "${tested_sha}" ] && [ "${tested_sha}" != "${current_head_sha}" ]'
assert stale_sha_guard in normalize["run"]
assert "should_review=false" in normalize["run"]
assert "Tests passed for ${tested_sha}, but current head is ${current_head_sha}" in workflow_text
assert classify["if"] == "steps.pr.outputs.should_review == 'true'"
def test_bm_bossbot_assets_are_non_gating_and_separate_from_review_job() -> None:
workflow = _workflow()
review_steps = workflow["jobs"]["review"]["steps"]
asset_job = workflow["jobs"]["assets"]
asset_steps = asset_job["steps"]
assert asset_job["needs"] == "review"
assert asset_job["if"] == (
"needs.review.result == 'success' && needs.review.outputs.should_review == 'true'"
)
assert not any(step["name"] == "Generate non-gating PR image" for step in review_steps)
assert not any(step["name"] == "Publish non-gating PR image" for step in review_steps)
generate = next(step for step in asset_steps if step["name"] == "Generate non-gating PR image")
publish = next(step for step in asset_steps if step["name"] == "Publish non-gating PR image")
assert generate["continue-on-error"] is True
assert publish["continue-on-error"] is True
assert "uv run --script scripts/generate_pr_infographic.py" in WORKFLOW_PATH.read_text(
encoding="utf-8"
)
assert "--provenance-output" in WORKFLOW_PATH.read_text(encoding="utf-8")
assert "git rm -rf --ignore-unmatch ." in WORKFLOW_PATH.read_text(encoding="utf-8")
assert "<!-- pr-infographic:start -->" in WORKFLOW_PATH.read_text(encoding="utf-8")
assert "BM Bossbot image for PR" in WORKFLOW_PATH.read_text(encoding="utf-8")
assert "gh pr edit" in WORKFLOW_PATH.read_text(encoding="utf-8")
assert "--body-file" in WORKFLOW_PATH.read_text(encoding="utf-8")
assert "BM_INFOGRAPHIC_PROVENANCE:start" in WORKFLOW_PATH.read_text(encoding="utf-8")
assert "BM_INFOGRAPHIC_PROVENANCE:end" in WORKFLOW_PATH.read_text(encoding="utf-8")
def test_bm_bossbot_rejects_oversized_diffs_without_partial_approval() -> None:
workflow_text = WORKFLOW_PATH.read_text(encoding="utf-8")
workflow = _workflow()
steps = workflow["jobs"]["review"]["steps"]
run_codex = next(step for step in steps if step["name"] == "Run BM Bossbot review with Codex")
assert "max_diff_bytes=120000" in workflow_text
assert "diff_truncated=true" in workflow_text
assert "review_complete: false" in workflow_text
assert 'verdict: "needs_human"' in workflow_text
assert "Diff exceeds BM Bossbot review limit" in workflow_text
assert (
run_codex["if"]
== "steps.pr.outputs.should_review == 'true' && "
"steps.trust.outputs.trusted_author == 'true' && "
"steps.context.outputs.diff_truncated != 'true'"
)
assert "head -c 120000" not in workflow_text
def test_bm_bossbot_does_not_run_codex_for_outside_contributors() -> None:
workflow_text = WORKFLOW_PATH.read_text(encoding="utf-8")
workflow = _workflow()
steps = workflow["jobs"]["review"]["steps"]
classify = next(step for step in steps if step["name"] == "Classify PR author")
outside = next(step for step in steps if step["name"] == "Decline outside contributor PRs")
collect = next(step for step in steps if step["name"] == "Collect sanitized PR context")
run_codex = next(step for step in steps if step["name"] == "Run BM Bossbot review with Codex")
select_review = next(step for step in steps if step["name"] == "Select BM Bossbot review output")
finalize = next(step for step in steps if step["name"] == "Finalize BM Bossbot approval")
assert "OWNER|MEMBER|COLLABORATOR" in classify["run"]
assert (
outside["if"]
== "steps.pr.outputs.should_review == 'true' && steps.trust.outputs.trusted_author != 'true'"
)
assert (
collect["if"]
== "steps.pr.outputs.should_review == 'true' && steps.trust.outputs.trusted_author == 'true'"
)
assert (
run_codex["if"]
== "steps.pr.outputs.should_review == 'true' && "
"steps.trust.outputs.trusted_author == 'true' && "
"steps.context.outputs.diff_truncated != 'true'"
)
assert select_review["if"] == "always() && steps.pr.outputs.should_review == 'true'"
assert finalize["if"] == "always() && steps.pr.outputs.should_review == 'true'"
assert "BM Bossbot does not run for outside contributors" in workflow_text
assert "missing-bm-bossbot-review.json" in workflow_text
assert '--review "${{ steps.review_output.outputs.review_file }}"' in finalize["run"]
def test_bm_bossbot_prompt_references_engineering_style_and_json_bullets() -> None:
prompt = PROMPT_PATH.read_text(encoding="utf-8")
assert "docs/ENGINEERING_STYLE.md" in prompt
assert "- Set `reviewed_head_sha`" in prompt
assert "- Do not include Markdown outside the JSON." in prompt
def test_claude_code_review_is_manual_advisory_only() -> None:
workflow = yaml.safe_load(
Path(".github/workflows/claude-code-review.yml").read_text(encoding="utf-8")
)
assert "pull_request" not in workflow["on"]
assert "workflow_dispatch" in workflow["on"]
assert workflow["on"]["workflow_dispatch"]["inputs"]["pr_number"]["required"] is True
-718
View File
@@ -1,718 +0,0 @@
import json
from pathlib import Path
import pytest
import yaml
from pydantic import ValidationError
from basic_memory.ci.project_updates import (
AgentSynthesis,
ProjectUpdateConfig,
ProjectUpdateContext,
build_project_update_note,
collect_project_update_context,
detect_github_repo,
load_project_update_config,
parse_github_remote,
render_agent_synthesis_schema,
render_capture_prompt,
render_soul_template,
render_workflow,
schema_seed_specs,
)
from basic_memory.ci import project_updates
def _write_json(path: Path, payload: dict) -> Path:
path.write_text(json.dumps(payload), encoding="utf-8")
return path
def _pr_payload(*, merged: bool = True) -> dict:
return {
"action": "closed",
"repository": {
"full_name": "basicmachines-co/basic-memory",
"html_url": "https://github.com/basicmachines-co/basic-memory",
},
"pull_request": {
"number": 123,
"title": "Remember project updates",
"body": "Adds Auto BM capture.\n\nCloses #77",
"html_url": "https://github.com/basicmachines-co/basic-memory/pull/123",
"merged": merged,
"merged_at": "2026-06-04T18:42:00Z" if merged else None,
"merge_commit_sha": "abc123",
"changed_files": 4,
"labels": [{"name": "feature"}, {"name": "ci"}],
"user": {"login": "octocat"},
},
}
def _synthesis_payload(**overrides: object) -> dict[str, object]:
payload: dict[str, object] = {
"summary": "Auto BM now records project updates.",
"story": (
"GitHub delivery events were losing their useful narrative after merge. "
"Auto BM collects source facts, lets the agent explain the change, and "
"publishes the result as durable project memory."
),
"problem_addressed": "Project delivery context was not preserved after GitHub events.",
"solution": "Collect GitHub facts and publish an idempotent Basic Memory note.",
"system_impact": "Future humans and agents can recover the delivery narrative.",
"why_it_matters": "Future agents can recover project context.",
"components_changed": ["basic_memory.ci.project_updates"],
"complexity_introduced": [],
"refactors_or_removals": [],
"user_facing_changes": [],
"internal_changes": [],
"verification": [],
"follow_ups": [],
"decision_candidates": [],
"task_candidates": [],
}
payload.update(overrides)
return payload
def test_collect_merged_pull_request_context(tmp_path: Path) -> None:
event_path = _write_json(tmp_path / "event.json", _pr_payload())
context = collect_project_update_context(
event_name="pull_request",
event_path=event_path,
config=ProjectUpdateConfig(project="team-memory"),
)
assert context.eligible is True
assert context.source_event == "pull_request_merged"
assert context.repo == "basicmachines-co/basic-memory"
assert context.idempotency_key == "github:basicmachines-co/basic-memory:pull_request_merged:123"
assert context.pr_number == 123
assert context.sha == "abc123"
assert context.labels == ["feature", "ci"]
assert context.linked_issues == ["#77"]
assert context.source_url == "https://github.com/basicmachines-co/basic-memory/pull/123"
def test_collect_enriches_pull_request_context_from_github_api(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
def fake_github_api_get(path: str, token: str) -> list[dict] | dict:
assert token == "github-token"
if path.startswith("/repos/basicmachines-co/basic-memory/pulls/123/files"):
return [
{
"filename": "src/basic_memory/ci/project_updates.py",
"status": "modified",
"additions": 42,
"deletions": 7,
"changes": 49,
}
]
if path.startswith("/repos/basicmachines-co/basic-memory/pulls/123/commits"):
return [
{
"sha": "abc123def456",
"commit": {
"message": "fix ci synthesis schema\n\nRequire all fields.",
"author": {"name": "Pat"},
},
}
]
if path == "/repos/basicmachines-co/basic-memory/issues/77":
return {
"number": 77,
"title": "Codex structured output rejects optional schema fields",
"body": "Auto BM failed before publish when optional fields were omitted.",
"html_url": "https://github.com/basicmachines-co/basic-memory/issues/77",
"state": "closed",
}
raise AssertionError(f"unexpected GitHub API path: {path}")
monkeypatch.setenv("GITHUB_TOKEN", "github-token")
monkeypatch.setattr(project_updates, "_github_api_get", fake_github_api_get, raising=False)
event_path = _write_json(tmp_path / "event.json", _pr_payload())
context = collect_project_update_context(
event_name="pull_request",
event_path=event_path,
config=ProjectUpdateConfig(project="team-memory"),
)
assert context.changed_files[0].filename == "src/basic_memory/ci/project_updates.py"
assert context.changed_files[0].status == "modified"
assert context.commits[0].message == "fix ci synthesis schema\n\nRequire all fields."
assert context.linked_issue_details[0].number == 77
assert (
context.linked_issue_details[0].title
== "Codex structured output rejects optional schema fields"
)
def test_github_api_get_list_fetches_multiple_pages(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[str] = []
def fake_github_api_get(path: str, token: str) -> list[dict]:
assert token == "github-token"
calls.append(path)
if path.endswith("page=1"):
return [{"filename": f"file-{index}.py"} for index in range(100)]
if path.endswith("page=2"):
return [{"filename": "file-100.py"}]
raise AssertionError(f"unexpected GitHub API path: {path}")
monkeypatch.setattr(project_updates, "_github_api_get", fake_github_api_get, raising=False)
files = project_updates._github_api_get_list(
"/repos/basicmachines-co/basic-memory/pulls/123/files",
"github-token",
)
assert len(files) == 101
assert calls == [
"/repos/basicmachines-co/basic-memory/pulls/123/files?per_page=100&page=1",
"/repos/basicmachines-co/basic-memory/pulls/123/files?per_page=100&page=2",
]
def test_collect_handles_sparse_pull_request_payload(tmp_path: Path) -> None:
payload = {
"action": "closed",
"repository": {},
"pull_request": {
"number": 123,
"merged": True,
"labels": "not-a-list",
},
}
event_path = _write_json(tmp_path / "event.json", payload)
context = collect_project_update_context(
event_name="pull_request",
event_path=event_path,
config=ProjectUpdateConfig(project="team-memory"),
)
assert context.eligible is True
assert context.repo is None
assert context.repo_url is None
assert context.labels == []
assert context.linked_issues == []
def test_collect_handles_missing_repository_payload(tmp_path: Path) -> None:
payload = {
"action": "closed",
"pull_request": {
"number": 123,
"merged": True,
},
}
event_path = _write_json(tmp_path / "event.json", payload)
context = collect_project_update_context(
event_name="pull_request",
event_path=event_path,
config=ProjectUpdateConfig(project="team-memory"),
)
assert context.eligible is True
assert context.repo is None
assert context.repo_url is None
def test_collect_rejects_missing_payload_shapes(tmp_path: Path) -> None:
pr_context = collect_project_update_context(
event_name="pull_request",
event_path=_write_json(tmp_path / "pr.json", {"action": "closed"}),
config=ProjectUpdateConfig(project="team-memory"),
)
workflow_context = collect_project_update_context(
event_name="workflow_run",
event_path=_write_json(tmp_path / "workflow.json", {"action": "completed"}),
config=ProjectUpdateConfig(project="team-memory"),
)
assert pr_context.eligible is False
assert pr_context.skip_reason == "pull request payload missing"
assert workflow_context.eligible is False
assert workflow_context.skip_reason == "workflow run payload missing"
def test_collect_ignores_non_closed_pull_request_action(tmp_path: Path) -> None:
payload = _pr_payload()
payload["action"] = "opened"
event_path = _write_json(tmp_path / "event.json", payload)
context = collect_project_update_context(
event_name="pull_request",
event_path=event_path,
config=ProjectUpdateConfig(project="team-memory"),
)
assert context.eligible is False
assert context.skip_reason == "pull request action was not closed"
def test_collect_ignores_closed_unmerged_pull_request(tmp_path: Path) -> None:
event_path = _write_json(tmp_path / "event.json", _pr_payload(merged=False))
context = collect_project_update_context(
event_name="pull_request",
event_path=event_path,
config=ProjectUpdateConfig(project="team-memory"),
)
assert context.eligible is False
assert context.skip_reason == "pull request was closed without merging"
def test_collect_successful_configured_production_deploy(tmp_path: Path) -> None:
payload = {
"action": "completed",
"repository": {
"full_name": "basicmachines-co/basic-memory-cloud",
"html_url": "https://github.com/basicmachines-co/basic-memory-cloud",
},
"workflow_run": {
"id": 98765,
"name": "Deploy Production",
"conclusion": "success",
"html_url": "https://github.com/basicmachines-co/basic-memory-cloud/actions/runs/98765",
"head_sha": "def456",
"updated_at": "2026-06-04T19:10:00Z",
},
}
event_path = _write_json(tmp_path / "event.json", payload)
context = collect_project_update_context(
event_name="workflow_run",
event_path=event_path,
config=ProjectUpdateConfig(
project="cloud-memory",
deploy_workflows=["Deploy Production"],
production_environments=["production"],
),
)
assert context.eligible is True
assert context.source_event == "production_deploy_succeeded"
assert context.workflow_run_id == "98765"
assert context.environment == "production"
assert context.idempotency_key == (
"github:basicmachines-co/basic-memory-cloud:production_deploy_succeeded:production:98765"
)
def test_collect_ignores_failed_or_unconfigured_deploy(tmp_path: Path) -> None:
payload = {
"action": "completed",
"repository": {"full_name": "basicmachines-co/basic-memory"},
"workflow_run": {"id": 1, "name": "Tests", "conclusion": "failure"},
}
event_path = _write_json(tmp_path / "event.json", payload)
context = collect_project_update_context(
event_name="workflow_run",
event_path=event_path,
config=ProjectUpdateConfig(project="team-memory"),
)
assert context.eligible is False
assert context.skip_reason == "workflow conclusion was failure"
def test_collect_ignores_successful_unconfigured_deploy(tmp_path: Path) -> None:
payload = {
"action": "completed",
"repository": {"full_name": "basicmachines-co/basic-memory"},
"workflow_run": {"id": 1, "name": "Tests", "conclusion": "success"},
}
event_path = _write_json(tmp_path / "event.json", payload)
context = collect_project_update_context(
event_name="workflow_run",
event_path=event_path,
config=ProjectUpdateConfig(project="team-memory"),
)
assert context.eligible is False
assert context.skip_reason == "workflow 'Tests' is not configured for project updates"
def test_collect_ignores_unsupported_event(tmp_path: Path) -> None:
event_path = _write_json(tmp_path / "event.json", {})
context = collect_project_update_context(
event_name="push",
event_path=event_path,
config=ProjectUpdateConfig(project="team-memory"),
)
assert context.eligible is False
assert context.skip_reason == "unsupported GitHub event: push"
def test_collect_rejects_missing_or_invalid_event_payload(tmp_path: Path) -> None:
with pytest.raises(ValueError, match="not found"):
collect_project_update_context(
event_name="pull_request",
event_path=tmp_path / "missing.json",
config=ProjectUpdateConfig(project="team-memory"),
)
invalid_json = tmp_path / "invalid.json"
invalid_json.write_text("{", encoding="utf-8")
with pytest.raises(ValueError, match="not valid JSON"):
collect_project_update_context(
event_name="pull_request",
event_path=invalid_json,
config=ProjectUpdateConfig(project="team-memory"),
)
list_json = tmp_path / "list.json"
list_json.write_text("[]", encoding="utf-8")
with pytest.raises(ValueError, match="JSON object"):
collect_project_update_context(
event_name="pull_request",
event_path=list_json,
config=ProjectUpdateConfig(project="team-memory"),
)
def test_build_project_update_note_uses_deterministic_identity_fields(tmp_path: Path) -> None:
event_path = _write_json(tmp_path / "event.json", _pr_payload())
context = collect_project_update_context(
event_name="pull_request",
event_path=event_path,
config=ProjectUpdateConfig(project="team-memory"),
)
synthesis = AgentSynthesis.model_validate(
_synthesis_payload(
why_it_matters="Future agents can recover the delivery narrative.",
repo="evil/repo",
source_event="production_deploy_succeeded",
verification=["Unit tests cover event normalization."],
)
)
note = build_project_update_note(context=context, synthesis=synthesis)
assert note.title == "PR #123: Remember project updates"
assert note.directory == "project-updates/github/basicmachines-co/basic-memory"
assert note.metadata["repo"] == "basicmachines-co/basic-memory"
assert note.metadata["source_event"] == "pull_request_merged"
assert note.metadata["idempotency_key"] == context.idempotency_key
assert "evil/repo" not in note.content
def test_build_project_update_note_renders_story_sections(tmp_path: Path) -> None:
event_path = _write_json(tmp_path / "event.json", _pr_payload())
context = collect_project_update_context(
event_name="pull_request",
event_path=event_path,
config=ProjectUpdateConfig(project="team-memory"),
)
synthesis = AgentSynthesis.model_validate(
{
"summary": "Auto BM now publishes durable project updates.",
"story": (
"Auto BM needed to preserve the delivery narrative, not just the mechanics. "
"The change adds a CI handoff where Codex synthesizes context and bm publishes it."
),
"problem_addressed": "Project context was lost after meaningful GitHub delivery events.",
"solution": "Collect GitHub facts, let Codex synthesize intent, then publish idempotently.",
"system_impact": "Merges now leave durable memory for future humans and agents.",
"why_it_matters": "Future work can recover why the delivery happened.",
"components_changed": [
"basic_memory.ci.project_updates",
"basic_memory.cli.commands.ci",
],
"complexity_introduced": ["Adds a CI-only agent synthesis boundary."],
"refactors_or_removals": ["Keeps Basic Memory auth out of the agent step."],
"verification": ["Unit tests cover collect and publish behavior."],
}
)
note = build_project_update_note(context=context, synthesis=synthesis)
assert "## Story" in note.content
assert "## Problem Addressed" in note.content
assert "## How The Change Solves It" in note.content
assert "## Impact On The System" in note.content
assert "## Project Memory" in note.content
assert "## Why It Matters" not in note.content
assert "## Components Changed" in note.content
assert "basic_memory.ci.project_updates" in note.content
assert "## Complexity Introduced" in note.content
assert "## Refactors Or Removals" in note.content
def test_build_project_update_note_renders_linked_issue_details_as_links() -> None:
context = ProjectUpdateContext(
eligible=True,
source_event="pull_request_merged",
repo="basicmachines-co/basic-memory",
repo_url="https://github.com/basicmachines-co/basic-memory",
source_url="https://github.com/basicmachines-co/basic-memory/pull/123",
idempotency_key="github:basicmachines-co/basic-memory:pull_request_merged:123",
pr_number=123,
title="Remember project updates",
linked_issues=["#77", "#88"],
linked_issue_details=[
project_updates.LinkedIssueDetail(
number=77,
title="Codex structured output rejects optional schema fields",
state="closed",
url="https://github.com/basicmachines-co/basic-memory/issues/77",
)
],
)
synthesis = AgentSynthesis.model_validate(_synthesis_payload())
note = build_project_update_note(context=context, synthesis=synthesis)
assert (
"- Linked issue: [#77 Codex structured output rejects optional schema fields "
"(closed)](https://github.com/basicmachines-co/basic-memory/issues/77)" in note.content
)
assert (
"- Linked issue: [#88](https://github.com/basicmachines-co/basic-memory/issues/88)"
in note.content
)
assert "- Linked issues: #77, #88" not in note.content
def test_build_project_update_note_for_production_deploy(tmp_path: Path) -> None:
payload = {
"action": "completed",
"repository": {
"full_name": "basicmachines-co/basic-memory-cloud",
"html_url": "https://github.com/basicmachines-co/basic-memory-cloud",
},
"workflow_run": {
"id": 98765,
"name": "Deploy Production",
"conclusion": "success",
"html_url": "https://github.com/basicmachines-co/basic-memory-cloud/actions/runs/98765",
"head_sha": "def456",
"updated_at": "2026-06-04T19:10:00Z",
},
}
context = collect_project_update_context(
event_name="workflow_run",
event_path=_write_json(tmp_path / "event.json", payload),
config=ProjectUpdateConfig(
project="cloud-memory",
deploy_workflows=["Deploy Production"],
production_environments=["production"],
),
)
synthesis = AgentSynthesis.model_validate(
_synthesis_payload(
summary="Production deploy completed.",
story=(
"A configured production workflow completed successfully. "
"The deploy SHA is now recorded as durable project memory."
),
problem_addressed="Production delivery needed a durable deployment record.",
solution="Publish a project update for the successful workflow run.",
system_impact="The production deploy is connected to its workflow run and SHA.",
why_it_matters="The latest project update reached users.",
)
)
note = build_project_update_note(context=context, synthesis=synthesis)
assert note.title == "Production deploy: 2026-06-04"
assert note.metadata["workflow_run_id"] == "98765"
assert note.metadata["environment"] == "production"
assert "https://github.com/basicmachines-co/basic-memory-cloud/actions/runs/98765" in (
note.content
)
def test_build_project_update_note_rejects_invalid_context() -> None:
synthesis = AgentSynthesis.model_validate(
_synthesis_payload(
summary="Auto BM records project updates.",
why_it_matters="Future agents can recover context.",
)
)
with pytest.raises(ValueError, match="ineligible"):
build_project_update_note(
context=ProjectUpdateContext(eligible=False, skip_reason="not useful"),
synthesis=synthesis,
)
with pytest.raises(ValueError, match="deterministic identity"):
build_project_update_note(
context=ProjectUpdateContext(
eligible=True,
source_event="pull_request_merged",
repo="basicmachines-co/basic-memory",
),
synthesis=synthesis,
)
def test_agent_synthesis_requires_summary_and_why_it_matters() -> None:
missing_why = _synthesis_payload()
missing_why.pop("why_it_matters")
with pytest.raises(ValidationError):
AgentSynthesis.model_validate(missing_why)
with pytest.raises(ValidationError):
AgentSynthesis.model_validate(_synthesis_payload(summary=" "))
def test_agent_synthesis_requires_delivery_narrative_fields() -> None:
with pytest.raises(ValidationError):
AgentSynthesis.model_validate(
{
"summary": "Auto BM records project updates.",
"why_it_matters": "Future agents can recover context.",
}
)
def test_project_update_config_requires_non_empty_lists() -> None:
with pytest.raises(ValueError, match="at least one"):
ProjectUpdateConfig(deploy_workflows=[" "])
def test_render_workflow_invokes_codex_read_only_without_basic_memory_secret() -> None:
workflow = render_workflow(
ProjectUpdateConfig(
project="team-memory",
deploy_workflows=["Deploy Production"],
production_environments=["production"],
)
)
assert "openai/codex-action@v1" in workflow
assert "sandbox: read-only" in workflow
assert "output-schema-file: ${{ runner.temp }}/agent-synthesis.schema.json" in workflow
assert "BASIC_MEMORY_CLOUD_API_KEY: ${{ secrets.BASIC_MEMORY_API_KEY }}" in workflow
assert "BASIC_MEMORY_CLOUD_HOST: ${{ vars.BASIC_MEMORY_CLOUD_HOST || '' }}" not in workflow
assert "BASIC_MEMORY_CI_CLOUD_HOST: ${{ vars.BASIC_MEMORY_CLOUD_HOST }}" in workflow
assert 'if [ -n "$BASIC_MEMORY_CI_CLOUD_HOST" ]' in workflow
assert "--context .github/basic-memory/project-update-context.json" in workflow
assert "GITHUB_TOKEN: ${{ github.token }}" in workflow
assert "--cloud \\" in workflow
codex_step = workflow.split("- name: Synthesize project update with Codex", 1)[1].split(
"- name: Publish project update", 1
)[0]
assert "BASIC_MEMORY_API_KEY" not in codex_step
def test_render_workflow_outputs_valid_github_actions_yaml() -> None:
workflow = render_workflow(ProjectUpdateConfig(project="team-memory"))
parsed = yaml.safe_load(workflow)
assert isinstance(parsed, dict)
assert parsed["on"]["pull_request"]["types"] == ["closed"]
assert parsed["on"]["workflow_run"]["types"] == ["completed"]
def test_render_capture_prompt_uses_workspace_context_path() -> None:
prompt = render_capture_prompt()
assert ".github/basic-memory/project-update-context.json" in prompt
assert ".github/basic-memory/SOUL.md" in prompt
assert "${{ runner.temp }}" not in prompt
assert "Do not write a fill-in-the-blanks note" in prompt
assert "Read the PR diff before writing" in prompt
assert "problem -> solution -> impact" in prompt
assert "It is okay to say when the code is messy" in prompt
assert "Ground all judgments" in prompt
def test_render_soul_template_guides_personality_without_overriding_facts() -> None:
soul = render_soul_template()
assert soul.startswith("# Auto BM Soul")
assert "It is okay to say when code is messy" in soul
assert "Notice good simplifications" in soul
assert "Do not invent intent, impact, tests, or drama" in soul
assert "Keep personality in service of memory" in soul
def test_render_agent_synthesis_schema_is_ci_guardrail_not_domain_schema() -> None:
schema = json.loads(render_agent_synthesis_schema())
assert schema["title"] == "AgentSynthesis"
assert "summary" in schema["required"]
assert "story" in schema["required"]
assert "problem_addressed" in schema["required"]
assert "solution" in schema["required"]
assert "system_impact" in schema["required"]
assert "components_changed" in schema["required"]
assert "why_it_matters" in schema["required"]
assert set(schema["required"]) == set(schema["properties"])
assert "project_update" not in json.dumps(schema)
def test_schema_seed_specs_are_basic_memory_schema_notes() -> None:
specs = schema_seed_specs()
assert {spec.entity for spec in specs} == {
"ProjectUpdate",
"GitHubPullRequestUpdate",
"GitHubProductionDeployUpdate",
}
assert all(spec.metadata["type"] == "schema" for spec in specs)
assert all(spec.metadata["settings"]["validation"] == "warn" for spec in specs)
project_update = next(spec for spec in specs if spec.entity == "ProjectUpdate")
assert "story" in project_update.metadata["schema"]
assert "problem_addressed" in project_update.metadata["schema"]
def test_parse_github_remote_accepts_https_and_ssh() -> None:
assert parse_github_remote("https://github.com/basicmachines-co/basic-memory.git") == (
"basicmachines-co",
"basic-memory",
)
assert parse_github_remote("git@github.com:basicmachines-co/basic-memory.git") == (
"basicmachines-co",
"basic-memory",
)
def test_parse_github_remote_rejects_non_github_remote() -> None:
with pytest.raises(ValueError, match="GitHub remote"):
parse_github_remote("https://example.com/basicmachines-co/basic-memory.git")
def test_detect_github_repo_requires_origin_remote(tmp_path: Path) -> None:
with pytest.raises(ValueError, match="No remote.origin.url"):
detect_github_repo(tmp_path)
def test_load_project_update_config_handles_missing_and_invalid_yaml(tmp_path: Path) -> None:
assert load_project_update_config(tmp_path / "missing.yml") == ProjectUpdateConfig()
invalid = tmp_path / "invalid.yml"
invalid.write_text("- not\n- an\n- object\n", encoding="utf-8")
with pytest.raises(ValueError, match="YAML object"):
load_project_update_config(invalid)
def test_private_note_helpers_reject_invalid_repo_shape() -> None:
context = ProjectUpdateContext(eligible=True, repo="not-owner-repo")
with pytest.raises(ValueError, match="owner/repo"):
project_updates._note_directory(context, ProjectUpdateConfig(project="team-memory"))
missing_repo = ProjectUpdateContext(eligible=True)
with pytest.raises(ValueError, match="missing repo"):
project_updates._note_directory(missing_repo, ProjectUpdateConfig(project="team-memory"))
def test_private_note_title_uses_generic_fallback_for_unknown_event() -> None:
context = ProjectUpdateContext(eligible=True, source_event="unknown")
assert project_updates._note_title(context) == "Project update"
-114
View File
@@ -1,114 +0,0 @@
from pathlib import Path
import pytest
from scripts import testmon_cache
def _write_testmon_file(directory: Path, filename: str, content: str) -> Path:
directory.mkdir(parents=True, exist_ok=True)
path = directory / filename
path.write_text(content, encoding="utf-8")
return path
def test_seed_testmon_data_reports_missing_shared_cache(tmp_path: Path) -> None:
repo_root = tmp_path / "repo"
cache_dir = tmp_path / "cache"
repo_root.mkdir()
result = testmon_cache.seed_testmon_data(repo_root=repo_root, cache_dir=cache_dir)
assert result.status == "missing"
assert result.copied == ()
assert not (repo_root / ".testmondata").exists()
def test_seed_testmon_data_keeps_existing_local_data(tmp_path: Path) -> None:
repo_root = tmp_path / "repo"
cache_dir = tmp_path / "cache"
local_datafile = _write_testmon_file(repo_root, ".testmondata", "local")
_write_testmon_file(cache_dir, ".testmondata", "shared")
result = testmon_cache.seed_testmon_data(repo_root=repo_root, cache_dir=cache_dir)
assert result.status == "exists"
assert result.copied == ()
assert local_datafile.read_text(encoding="utf-8") == "local"
def test_seed_testmon_data_replaces_stale_sidecars(tmp_path: Path) -> None:
repo_root = tmp_path / "repo"
cache_dir = tmp_path / "cache"
_write_testmon_file(repo_root, ".testmondata-shm", "stale sidecar")
_write_testmon_file(cache_dir, ".testmondata", "shared main")
_write_testmon_file(cache_dir, ".testmondata-wal", "shared wal")
result = testmon_cache.seed_testmon_data(repo_root=repo_root, cache_dir=cache_dir)
assert result.status == "seeded"
assert {path.name for path in result.copied} == {".testmondata", ".testmondata-wal"}
assert (repo_root / ".testmondata").read_text(encoding="utf-8") == "shared main"
assert (repo_root / ".testmondata-wal").read_text(encoding="utf-8") == "shared wal"
assert not (repo_root / ".testmondata-shm").exists()
def test_refresh_testmon_data_requires_local_data(tmp_path: Path) -> None:
repo_root = tmp_path / "repo"
cache_dir = tmp_path / "cache"
repo_root.mkdir()
with pytest.raises(FileNotFoundError):
testmon_cache.refresh_testmon_data(repo_root=repo_root, cache_dir=cache_dir)
def test_refresh_testmon_data_replaces_shared_cache(tmp_path: Path) -> None:
repo_root = tmp_path / "repo"
cache_dir = tmp_path / "cache"
_write_testmon_file(repo_root, ".testmondata", "local main")
_write_testmon_file(repo_root, ".testmondata-shm", "local shm")
_write_testmon_file(cache_dir, ".testmondata", "old main")
_write_testmon_file(cache_dir, ".testmondata-wal", "old wal")
result = testmon_cache.refresh_testmon_data(repo_root=repo_root, cache_dir=cache_dir)
assert result.status == "refreshed"
assert {path.name for path in result.copied} == {".testmondata", ".testmondata-shm"}
assert (cache_dir / ".testmondata").read_text(encoding="utf-8") == "local main"
assert (cache_dir / ".testmondata-shm").read_text(encoding="utf-8") == "local shm"
assert not (cache_dir / ".testmondata-wal").exists()
def test_resolve_cache_dir_prefers_explicit_path_over_env(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
repo_root = tmp_path / "repo"
env_cache_dir = tmp_path / "env-cache"
explicit_cache_dir = tmp_path / "explicit-cache"
repo_root.mkdir()
monkeypatch.setenv(testmon_cache.TESTMON_CACHE_ENV, str(env_cache_dir))
assert testmon_cache.resolve_cache_dir(repo_root) == env_cache_dir.resolve()
assert (
testmon_cache.resolve_cache_dir(repo_root, explicit_cache_dir)
== explicit_cache_dir.resolve()
)
def test_status_command_prints_local_and_shared_paths(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
repo_root = tmp_path / "repo"
cache_dir = tmp_path / "cache"
repo_root.mkdir()
_write_testmon_file(repo_root, ".testmondata", "local main")
exit_code = testmon_cache.main(
["--repo-root", str(repo_root), "--cache-dir", str(cache_dir), "status"]
)
assert exit_code == 0
output = capsys.readouterr().out
assert f"Repo root: {repo_root.resolve()}" in output
assert "Worktree ready: True" in output
assert "Cache ready: False" in output
-92
View File
@@ -1,92 +0,0 @@
from pathlib import Path
import pytest
from scripts.validate_skills import parse_frontmatter
def test_parse_frontmatter_rejects_unquoted_mapping_colon(tmp_path: Path) -> None:
skill = tmp_path / "SKILL.md"
skill.write_text(
"\n".join(
[
"---",
"name: bm-qa",
"description: Use when validating fixes. Drives the full loop: map issue to commit.",
"---",
"# Skill",
"",
]
),
encoding="utf-8",
)
with pytest.raises(SystemExit, match="invalid YAML"):
parse_frontmatter(skill)
def test_parse_frontmatter_allows_url_colons_in_plain_values(tmp_path: Path) -> None:
skill = tmp_path / "SKILL.md"
skill.write_text(
"\n".join(
[
"---",
"name: memory-notes",
"description: See https://docs.basicmemory.com for usage.",
"---",
"# Skill",
"",
]
),
encoding="utf-8",
)
frontmatter = parse_frontmatter(skill)
assert frontmatter["description"] == "See https://docs.basicmemory.com for usage."
def test_parse_frontmatter_strips_matching_single_quotes(tmp_path: Path) -> None:
skill = tmp_path / "SKILL.md"
skill.write_text(
"\n".join(
[
"---",
"name: memory-notes",
"description: 'Use when values contain mapping-like text: safely.'",
"---",
"# Skill",
"",
]
),
encoding="utf-8",
)
frontmatter = parse_frontmatter(skill)
assert frontmatter["description"] == "Use when values contain mapping-like text: safely."
def test_parse_frontmatter_keeps_nested_fields_nested(tmp_path: Path) -> None:
schema = tmp_path / "schema.md"
schema.write_text(
"\n".join(
[
"---",
"type: schema",
"entity: Task",
"schema:",
" type: object",
"---",
"# Task",
"",
]
),
encoding="utf-8",
)
frontmatter = parse_frontmatter(schema)
assert frontmatter["type"] == "schema"
assert frontmatter["entity"] == "Task"
assert frontmatter["schema"] == ""
-204
View File
@@ -1,204 +0,0 @@
import json
from pathlib import Path
from typing import Mapping
import pytest
from typer.testing import CliRunner
from scripts import bm_bossbot_status
def _event_payload(body: str = "Event snapshot body") -> dict[str, object]:
return {
"repository": {"full_name": "basicmachines-co/basic-memory"},
"pull_request": {
"number": 925,
"body": body,
"head": {"sha": "abc123"},
},
}
def test_status_script_is_uv_typer_entrypoint() -> None:
source = bm_bossbot_status.__file__
assert source is not None
text = open(source, encoding="utf-8").read()
assert text.startswith("#!/usr/bin/env -S uv run --script\n")
assert "# /// script" in text
assert "typer" in text
assert hasattr(bm_bossbot_status, "app")
def _review_payload(**overrides: object) -> dict[str, object]:
payload: dict[str, object] = {
"reviewed_head_sha": "abc123",
"review_complete": True,
"verdict": "approve",
"blocking_findings": [],
"nonblocking_findings": [],
"summary": "The change is ready.",
}
payload.update(overrides)
return payload
def test_validate_review_accepts_matching_approved_head_sha() -> None:
result = bm_bossbot_status.validate_review(_review_payload(), expected_head_sha="abc123")
assert result.approved is True
assert result.state == "success"
assert result.description == "BM Bossbot approved this head SHA"
def test_validate_review_rejects_stale_head_sha() -> None:
result = bm_bossbot_status.validate_review(_review_payload(), expected_head_sha="def456")
assert result.approved is False
assert result.state == "failure"
assert result.description == "BM Bossbot reviewed a stale head SHA"
def test_validate_review_rejects_blocking_findings() -> None:
result = bm_bossbot_status.validate_review(
_review_payload(blocking_findings=[{"title": "Missing test", "body": "Add coverage."}]),
expected_head_sha="abc123",
)
assert result.approved is False
assert result.state == "failure"
assert result.description == "BM Bossbot requested changes"
def test_status_payload_uses_required_context() -> None:
payload = bm_bossbot_status.build_status_payload(
state="pending",
description="BM Bossbot is reviewing this head SHA",
target_url="https://github.com/basicmachines-co/basic-memory/actions/runs/1",
)
assert payload == {
"state": "pending",
"context": "BM Bossbot Approval",
"description": "BM Bossbot is reviewing this head SHA",
"target_url": "https://github.com/basicmachines-co/basic-memory/actions/runs/1",
}
def test_upsert_summary_block_replaces_existing_block() -> None:
body = "\n".join(
[
"Intro",
"<!-- BM_BOSSBOT_SUMMARY:start -->",
"Old summary",
"<!-- BM_BOSSBOT_SUMMARY:end -->",
"Footer",
]
)
updated = bm_bossbot_status.upsert_summary_block(body, "New summary")
assert "Old summary" not in updated
assert "New summary" in updated
assert updated.startswith("Intro")
assert updated.endswith("Footer")
def test_finalize_review_fetches_current_pr_body_before_upserting(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
event_path = tmp_path / "event.json"
review_path = tmp_path / "review.json"
event_path.write_text(json.dumps(_event_payload()), encoding="utf-8")
review_path.write_text(json.dumps(_review_payload()), encoding="utf-8")
monkeypatch.setenv("GITHUB_TOKEN", "token")
updated_bodies: list[str] = []
statuses: list[Mapping[str, str]] = []
def fake_get_pull_request_body(*, token: str, repo: str, number: int) -> str:
assert token == "token"
assert repo == "basicmachines-co/basic-memory"
assert number == 925
return "Current body edited while the workflow was running"
def fake_update_pull_request_body(*, token: str, repo: str, number: int, body: str) -> None:
updated_bodies.append(body)
def fake_set_commit_status(
*,
token: str,
repo: str,
sha: str,
payload: Mapping[str, str],
) -> None:
statuses.append(payload)
monkeypatch.setattr(bm_bossbot_status, "get_pull_request_body", fake_get_pull_request_body)
monkeypatch.setattr(bm_bossbot_status, "update_pull_request_body", fake_update_pull_request_body)
monkeypatch.setattr(bm_bossbot_status, "set_commit_status", fake_set_commit_status)
result = bm_bossbot_status.finalize_review(
event_path=event_path,
review_path=review_path,
repo=None,
run_url="https://github.com/basicmachines-co/basic-memory/actions/runs/1",
token_env="GITHUB_TOKEN",
)
assert result.approved is True
assert "Current body edited while the workflow was running" in updated_bodies[0]
assert "Event snapshot body" not in updated_bodies[0]
assert statuses[0]["state"] == "success"
def test_finalize_cli_marks_failure_when_review_file_is_missing(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
event_path = tmp_path / "event.json"
missing_review_path = tmp_path / "missing-review.json"
event_path.write_text(json.dumps(_event_payload(body="Current body")), encoding="utf-8")
monkeypatch.setenv("GITHUB_TOKEN", "token")
updated_bodies: list[str] = []
statuses: list[Mapping[str, str]] = []
def fake_get_pull_request_body(*, token: str, repo: str, number: int) -> str:
return "Current body"
def fake_update_pull_request_body(*, token: str, repo: str, number: int, body: str) -> None:
updated_bodies.append(body)
def fake_set_commit_status(
*,
token: str,
repo: str,
sha: str,
payload: Mapping[str, str],
) -> None:
statuses.append(payload)
monkeypatch.setattr(bm_bossbot_status, "get_pull_request_body", fake_get_pull_request_body)
monkeypatch.setattr(bm_bossbot_status, "update_pull_request_body", fake_update_pull_request_body)
monkeypatch.setattr(bm_bossbot_status, "set_commit_status", fake_set_commit_status)
result = CliRunner().invoke(
bm_bossbot_status.app,
[
"finalize",
"--event",
str(event_path),
"--review",
str(missing_review_path),
"--repo",
"basicmachines-co/basic-memory",
"--run-url",
"https://github.com/basicmachines-co/basic-memory/actions/runs/1",
],
)
assert result.exit_code == 1
assert "BM Bossbot review output was invalid" in updated_bodies[0]
assert statuses[0]["state"] == "failure"
@@ -1,360 +0,0 @@
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-body-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_bossbot_summary_from_pr_body() -> None:
body = "\n".join(
[
"Before",
"<!-- BM_BOSSBOT_SUMMARY:start -->",
"Reviewed SHA: abc123",
"Verdict: approve",
"<!-- BM_BOSSBOT_SUMMARY:end -->",
"After",
]
)
summary = generate_pr_infographic.extract_bossbot_summary(body)
assert summary == "Reviewed SHA: abc123\nVerdict: approve"
def test_extract_bossbot_summary_requires_managed_block() -> None:
with pytest.raises(ValueError, match="BM Bossbot summary block"):
generate_pr_infographic.extract_bossbot_summary("No managed summary")
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,
summary="Summary: Adds a merge gate.",
pr_body=body,
theme_override=None,
)
from_cli = generate_pr_infographic.select_image_theme(
pr_number=42,
summary="Summary: Adds a merge gate.",
pr_body=body,
theme_override="80's action movies",
)
from_auto = generate_pr_infographic.select_image_theme(
pr_number=42,
summary="Summary: Adds 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_uses_summary_without_making_gate_claims() -> None:
prompt = generate_pr_infographic.build_infographic_prompt(
pr_number=42,
summary="Verdict: approve\nSummary: Adds a merge gate.",
theme="WWII propaganda posters with home-front logistics routes",
theme_source=generate_pr_infographic.ThemeSource.CLI,
)
assert "PR #42" in prompt
assert "Adds a merge gate" 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 "scene" in prompt
assert "poster" in prompt
assert "painting" in prompt
assert "classic photograph" in prompt
assert "symbolic tableau" in prompt
assert "before/after value story" in prompt
assert "Do not render an infographic" in prompt
assert "dashboard" in prompt
assert "flowchart" in prompt
assert "copyrighted characters" in prompt
assert "restrained" not in prompt
assert "non-gating" in prompt
assert "BM Bossbot Approval" 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,
summary="Verdict: approve\nSummary: Adds a merge gate.",
pr_body="No theme",
theme_override=None,
)
prompt = generate_pr_infographic.build_infographic_prompt(
pr_number=42,
summary="Verdict: approve\nSummary: Adds a merge gate.",
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:
body_file = tmp_path / "pr-body.md"
body_file.write_text(
"\n".join(
[
"<!-- BM_BOSSBOT_SUMMARY:start -->",
"Verdict: approve",
"Summary: Adds a merge gate.",
"<!-- BM_BOSSBOT_SUMMARY:end -->",
"<!-- BM_INFOGRAPHIC_THEME:start -->",
"space exploration and astronomy",
"<!-- BM_INFOGRAPHIC_THEME:end -->",
]
),
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-body-file",
str(body_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 "Adds a merge gate" in result.output
assert "space exploration and astronomy" in result.output
assert "image-first composition" in result.output
assert "Do not render an infographic" in result.output
assert "BM Bossbot Approval" in result.output
assert not output.exists()
def test_generate_pr_infographic_writes_provenance_after_image_generation(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
body_file = tmp_path / "pr-body.md"
body_file.write_text(
"\n".join(
[
"<!-- BM_BOSSBOT_SUMMARY:start -->",
"Verdict: approve",
"Summary: Adds a merge gate.",
"<!-- BM_BOSSBOT_SUMMARY:end -->",
"<!-- 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-body-file",
str(body_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)