mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
.stamphog/ was a config directory at the repo root — policy.yml, review-guidance.md — read by the PR review agent. It is .stamp/ now, and the 34 files that name it follow: the workflows, tools/pr-approval-agent, tach.toml, the sandbox Dockerfile, bin/debrand. One behaviour change worth naming rather than burying: the agent finds its own previous PR comment by an HTML marker, `<!-- stamphog:v1 ... -->`, and that marker is now `<!-- stamp:v1 ... -->`. Open PRs will get one new comment instead of an edit, once, and then match again. insights/test/setup_receivers_baseline.txt records registered signal receivers by module path, so it names products.review.backend now. stamphog: zero occurrences anywhere in the tree. review_hog: only inside migration files, whose names are their identity in django_migrations. Co-authored-by: Hanzo Dev <[email protected]>
201 lines
7.7 KiB
Python
201 lines
7.7 KiB
Python
"""Tests for migration_risk.py — check-run interpretation."""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from migration_risk import migration_check_pending, safe_migration_files
|
|
|
|
|
|
def _check(
|
|
name: str = "Migration risk",
|
|
*,
|
|
status: str = "completed",
|
|
conclusion: str | None = "success",
|
|
completed_at: str = "2026-04-30T12:00:00Z",
|
|
analyzed_paths: list[str] | None = None,
|
|
summary_override: str | None = None,
|
|
) -> dict:
|
|
"""Build a fake check_run.
|
|
|
|
`analyzed_paths` becomes the JSON inside the `<!-- stamp:v1 [...] -->`
|
|
marker; pass `None` to omit the marker entirely (simulates older checks
|
|
or non-success conclusions). `summary_override` lets tests inject raw
|
|
summary text (e.g. malformed markers).
|
|
"""
|
|
if summary_override is not None:
|
|
summary = summary_override
|
|
elif analyzed_paths is None:
|
|
summary = "no marker here"
|
|
else:
|
|
summary = f"<!-- stamp:v1 {json.dumps(analyzed_paths)} -->\nreport body"
|
|
return {
|
|
"name": name,
|
|
"status": status,
|
|
"conclusion": conclusion,
|
|
"completed_at": completed_at,
|
|
"output": {"summary": summary},
|
|
}
|
|
|
|
|
|
# ── safe_migration_files ─────────────────────────────────────────
|
|
|
|
|
|
def test_success_check_pairs_each_migration_with_its_max_migration_txt() -> None:
|
|
"""Happy path. Every analyzed migration in the marker that's also in the PR
|
|
diff gets its directory's max_migration.txt paired automatically, across
|
|
multiple migration roots; non-migration paths are ignored."""
|
|
files = [
|
|
"insights/migrations/1125_x.py",
|
|
"products/signals/backend/migrations/0042_y.py",
|
|
"insights/api/some.py",
|
|
]
|
|
check = _check(
|
|
analyzed_paths=[
|
|
"insights/migrations/1125_x.py",
|
|
"products/signals/backend/migrations/0042_y.py",
|
|
]
|
|
)
|
|
|
|
assert safe_migration_files([check], files) == {
|
|
"insights/migrations/1125_x.py",
|
|
"insights/migrations/max_migration.txt",
|
|
"products/signals/backend/migrations/0042_y.py",
|
|
"products/signals/backend/migrations/max_migration.txt",
|
|
}
|
|
|
|
|
|
def test_bypass_scoped_to_intersection_of_marker_and_pr() -> None:
|
|
"""The marker is the source of truth. A path the analyzer never classified
|
|
(e.g. Datastore migration in the same PR) must not be bypassed even when
|
|
the Django side concluded success."""
|
|
files = [
|
|
"insights/migrations/1125_x.py",
|
|
"insights/datastore/migrations/0099_unrelated.py",
|
|
]
|
|
# Marker only lists the Django one — the analyzer doesn't touch Datastore.
|
|
check = _check(analyzed_paths=["insights/migrations/1125_x.py"])
|
|
|
|
assert safe_migration_files([check], files) == {
|
|
"insights/migrations/1125_x.py",
|
|
"insights/migrations/max_migration.txt",
|
|
}
|
|
|
|
|
|
def test_max_migration_txt_paired_only_for_analyzed_dirs() -> None:
|
|
"""Sibling pairing must follow the analyzer's scope — a Datastore
|
|
`max_migration.txt` should never be added just because some other dir
|
|
contributed a Safe migration."""
|
|
files = [
|
|
"insights/migrations/1125_x.py",
|
|
"insights/datastore/migrations/0099_x.py",
|
|
]
|
|
check = _check(analyzed_paths=["insights/migrations/1125_x.py"])
|
|
|
|
safe = safe_migration_files([check], files)
|
|
|
|
assert "insights/datastore/migrations/max_migration.txt" not in safe
|
|
assert "insights/migrations/max_migration.txt" in safe
|
|
|
|
|
|
def test_empty_marker_means_no_bypass() -> None:
|
|
"""Zero Django migrations to analyze still publishes success, but with an
|
|
empty marker — bypass set is empty and the deny-list applies normally."""
|
|
files = ["insights/datastore/migrations/0099_x.py"]
|
|
check = _check(analyzed_paths=[])
|
|
|
|
assert safe_migration_files([check], files) == set()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"summary",
|
|
[
|
|
pytest.param("no marker here at all", id="no-marker"),
|
|
pytest.param("<!-- stamp:v1 not-json-array -->", id="malformed-json"),
|
|
pytest.param('<!-- stamp:v1 ["missing close ', id="truncated"),
|
|
pytest.param('<!-- stamp:v1 {"shape":"wrong"} -->', id="wrong-shape"),
|
|
],
|
|
)
|
|
def test_malformed_or_missing_marker_falls_back_to_no_bypass(summary: str) -> None:
|
|
"""Any failure to parse the marker is treated as "no analyzer signal,"
|
|
which is the safe default — deny-list applies normally."""
|
|
check = _check(summary_override=summary)
|
|
assert safe_migration_files([check], ["insights/migrations/1125_x.py"]) == set()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"check_runs",
|
|
[
|
|
pytest.param([], id="no-checks"),
|
|
pytest.param([_check(name="other-ci")], id="different-check-name"),
|
|
pytest.param(
|
|
[_check(conclusion="failure", analyzed_paths=["insights/migrations/1125_x.py"])], id="completed-failure"
|
|
),
|
|
pytest.param(
|
|
[_check(conclusion="neutral", analyzed_paths=["insights/migrations/1125_x.py"])], id="completed-needs-review"
|
|
),
|
|
pytest.param([_check(status="in_progress", conclusion=None)], id="in-flight"),
|
|
],
|
|
)
|
|
def test_no_bypass_unless_check_completed_with_success(check_runs: list[dict]) -> None:
|
|
"""Bypass requires an explicit `success` conclusion on a completed
|
|
`Migration risk` check; everything else falls back to the deny-list,
|
|
even when the marker would otherwise list the file."""
|
|
assert safe_migration_files(check_runs, ["insights/migrations/1125_x.py"]) == set()
|
|
|
|
|
|
def test_latest_completed_run_wins_over_older_and_in_flight() -> None:
|
|
"""CI re-runs leave duplicate checks. The most recent completed run wins,
|
|
even when a newer in-flight run is present (it has no completed_at)."""
|
|
older_failure = _check(
|
|
conclusion="failure",
|
|
completed_at="2026-04-30T10:00:00Z",
|
|
analyzed_paths=["insights/migrations/1125_x.py"],
|
|
)
|
|
newer_success = _check(
|
|
conclusion="success",
|
|
completed_at="2026-04-30T12:00:00Z",
|
|
analyzed_paths=["insights/migrations/1125_x.py"],
|
|
)
|
|
in_flight = _check(status="in_progress", conclusion=None, completed_at="")
|
|
|
|
assert safe_migration_files([older_failure, newer_success, in_flight], ["insights/migrations/1125_x.py"]) == {
|
|
"insights/migrations/1125_x.py",
|
|
"insights/migrations/max_migration.txt",
|
|
}
|
|
|
|
|
|
# ── migration_check_pending ──────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"check_runs",
|
|
[
|
|
pytest.param([], id="no-check"),
|
|
pytest.param([_check(status="in_progress", conclusion=None)], id="in-progress"),
|
|
pytest.param([_check(status="queued", conclusion=None)], id="queued"),
|
|
],
|
|
)
|
|
def test_pending_when_pr_has_migrations_and_no_completed_check(check_runs: list[dict]) -> None:
|
|
"""Drives the deny-with-retry message: anything short of a completed check
|
|
means the verdict isn't in yet."""
|
|
assert migration_check_pending(check_runs, ["insights/migrations/1125_x.py"]) is True
|
|
|
|
|
|
@pytest.mark.parametrize("conclusion", ["success", "failure"])
|
|
def test_not_pending_once_check_has_completed(conclusion: str) -> None:
|
|
"""Verdict is in even when not success — caller chooses how to act on it."""
|
|
assert (
|
|
migration_check_pending(
|
|
[_check(conclusion=conclusion, analyzed_paths=["insights/migrations/1125_x.py"])],
|
|
["insights/migrations/1125_x.py"],
|
|
)
|
|
is False
|
|
)
|
|
|
|
|
|
def test_not_pending_when_pr_has_no_real_migrations() -> None:
|
|
"""No migration files means there's nothing to wait for, regardless of
|
|
what other checks are in flight on the same head SHA."""
|
|
assert migration_check_pending([_check(status="in_progress", conclusion=None)], ["insights/api/views.py"]) is False
|