Files
roboco/tests/unit/foundation/policy/content/test_content_models.py
T

234 lines
7.0 KiB
Python

"""Validation tests for the structured content schema models."""
from __future__ import annotations
import pytest
from roboco.foundation.policy.content import (
AuditorNote,
ContentValidationError,
DeveloperNote,
DocNote,
PrReviewContent,
QaNote,
ResumptionNote,
TaskDescription,
validate_content,
)
from roboco.foundation.policy.content.enums import Severity, Verdict
# --------------------------------------------------------------------------- #
# Valid construction
# --------------------------------------------------------------------------- #
def test_pr_review_valid() -> None:
c = PrReviewContent.model_validate(
{
"summary": "The change is correct and covered by tests.",
"findings": [
{
"file": "roboco/services/git.py",
"line": 42,
"severity": "major",
"expected": "raises on 422",
"actual": "swallows the error",
}
],
"verdict": "changes_requested",
}
)
assert c.findings[0].severity is Severity.MAJOR
assert c.verdict is Verdict.CHANGES_REQUESTED
def test_qa_valid() -> None:
c = QaNote.model_validate(
{
"summary": "Reviewed all acceptance criteria against the diff.",
"ac_verdicts": [
{
"criterion": "AC1 returns 400",
"status": "verified",
"how": "test passes",
}
],
"verdict": "passed",
}
)
assert c.ac_verdicts[0].status == "verified"
def test_task_description_valid() -> None:
c = TaskDescription.model_validate(
{
"objective": "Add a structured PR-review comment format.",
"what_this_builds": ["a reviewer schema"],
"the_work": [
{
"team": "backend",
"summary": "schema + gateway",
"items": ["model", "verb"],
}
],
"acceptance_criteria": ["reviewer notes land in their own slot"],
}
)
assert c.the_work[0].team.value == "backend"
def test_resumption_valid() -> None:
c = ResumptionNote(
done="schema landed", next="wire the gateway", where_to_look=["models.py"]
)
assert c.next == "wire the gateway"
# --------------------------------------------------------------------------- #
# Rejection
# --------------------------------------------------------------------------- #
def test_validate_content_unknown_type() -> None:
with pytest.raises(ContentValidationError) as exc:
validate_content("nonsense", {})
assert exc.value.field == "content_type"
def test_pr_review_missing_summary_rejected() -> None:
with pytest.raises(ContentValidationError) as exc:
validate_content("pr_review", {"verdict": "approved"})
assert exc.value.field == "summary"
def test_pr_review_trivial_summary_rejected() -> None:
with pytest.raises(ContentValidationError):
validate_content("pr_review", {"summary": "wip", "verdict": "approved"})
def test_pr_review_negative_verdict_allows_summary_only() -> None:
# A reviewer can fail on a summary alone (e.g. "CI is red"); findings are
# format-enforced (file/line/expected/actual) when present, not mandatory.
c = validate_content(
"pr_review",
{
"summary": "CI is red on this PR; the failing job blocks merge.",
"verdict": "failed",
"findings": [],
},
)
assert isinstance(c, PrReviewContent)
assert c.verdict.value == "failed"
def test_pr_review_approved_allows_empty_findings() -> None:
c = validate_content(
"pr_review",
{"summary": "All criteria met, nothing to change.", "verdict": "approved"},
)
assert isinstance(c, PrReviewContent)
def test_qa_verdict_must_be_pass_or_fail() -> None:
with pytest.raises(ContentValidationError) as exc:
validate_content(
"qa",
{
"summary": "Reviewed the acceptance criteria thoroughly.",
"ac_verdicts": [
{"criterion": "c", "status": "verified", "how": "test"}
],
"verdict": "approved",
},
)
assert exc.value.field == "verdict"
def test_qa_requires_ac_verdicts() -> None:
with pytest.raises(ContentValidationError) as exc:
validate_content(
"qa",
{
"summary": "Reviewed everything carefully here.",
"ac_verdicts": [],
"verdict": "passed",
},
)
assert exc.value.field == "ac_verdicts"
def test_task_description_requires_work() -> None:
with pytest.raises(ContentValidationError) as exc:
validate_content(
"task_description",
{
"objective": "Build the thing properly.",
"the_work": [],
"acceptance_criteria": ["x"],
},
)
assert exc.value.field == "the_work"
def test_work_unit_rejects_non_cell_team() -> None:
with pytest.raises(ContentValidationError):
validate_content(
"task_description",
{
"objective": "Build the thing properly.",
"the_work": [{"team": "board", "summary": "do it", "items": ["a"]}],
"acceptance_criteria": ["x"],
},
)
# --------------------------------------------------------------------------- #
# Graceful coercion (lone scalar -> one-element list)
# --------------------------------------------------------------------------- #
def test_findings_single_dict_coerced_to_list() -> None:
c = validate_content(
"pr_review",
{
"summary": "One finding passed as a bare dict.",
"verdict": "changes_requested",
"findings": {
"file": "a.py",
"severity": "nit",
"expected": "trailing newline",
"actual": "no newline",
},
},
)
assert isinstance(c, PrReviewContent)
assert len(c.findings) == 1
def test_where_to_look_single_string_coerced() -> None:
c = validate_content(
"resumption",
{
"done": "did the thing",
"next": "do next thing",
"where_to_look": "models.py",
},
)
assert isinstance(c, ResumptionNote)
assert c.where_to_look == ["models.py"]
def test_developer_and_doc_and_auditor_models() -> None:
assert isinstance(
validate_content("developer", {"summary": "Implemented the schema layer."}),
DeveloperNote,
)
assert isinstance(
validate_content("doc", {"summary": "Documented the new endpoints."}), DocNote
)
assert isinstance(
validate_content(
"auditor", {"summary": "No concerns with this change.", "severity": "info"}
),
AuditorNote,
)