185 lines
6.1 KiB
Python
185 lines
6.1 KiB
Python
"""Tests for meta-codereview-current-diff (Round-2 combinator).
|
|
|
|
Three independent reviewers (safety + tests + style) run in parallel
|
|
over the diff; arbitrate applies strict priority CRITICAL > WARNING/
|
|
MISSING > clean.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import AsyncIterator
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from opensquilla.engine.types import AgentEvent, DoneEvent, TextDeltaEvent
|
|
from opensquilla.skills.loader import SkillLoader
|
|
from opensquilla.skills.meta.orchestrator import MetaOrchestrator
|
|
from opensquilla.skills.meta.parser import parse_meta_plan
|
|
from opensquilla.skills.meta.types import MetaMatch
|
|
|
|
_BUNDLED = (
|
|
Path(__file__).resolve().parents[2] / "src" / "opensquilla" / "skills" / "bundled"
|
|
)
|
|
_EXP = Path(__file__).resolve().parents[2] / "src" / "opensquilla" / "skills" / "exp"
|
|
|
|
|
|
def _bundle_loader(tmp_path: Path) -> SkillLoader:
|
|
loader = SkillLoader(
|
|
bundled_dir=_BUNDLED,
|
|
extra_dirs=[_EXP],
|
|
snapshot_path=tmp_path / "snap.json",
|
|
)
|
|
loader.invalidate_cache()
|
|
loader.load_all()
|
|
return loader
|
|
|
|
|
|
def test_parses_with_expected_topology(tmp_path: Path) -> None:
|
|
loader = _bundle_loader(tmp_path)
|
|
spec = loader.get_by_name("meta-codereview-current-diff")
|
|
assert spec is not None
|
|
plan = parse_meta_plan(spec)
|
|
assert plan is not None
|
|
|
|
step_ids = [s.id for s in plan.steps]
|
|
assert step_ids == [
|
|
"read_diff",
|
|
"review_safety",
|
|
"review_tests",
|
|
"review_style",
|
|
"arbitrate",
|
|
]
|
|
by_id = {s.id: s for s in plan.steps}
|
|
for r in ("review_safety", "review_tests", "review_style"):
|
|
assert by_id[r].kind == "llm_chat"
|
|
assert by_id[r].depends_on == ("read_diff",)
|
|
assert by_id["arbitrate"].kind == "llm_chat"
|
|
assert set(by_id["arbitrate"].depends_on) == {
|
|
"review_safety",
|
|
"review_tests",
|
|
"review_style",
|
|
}
|
|
|
|
|
|
def _classify(user_message: str) -> str:
|
|
# Arbitrate first (embeds the per-reviewer step ids).
|
|
if "Three reviewers ran on the diff" in user_message:
|
|
return "arbitrate"
|
|
if "git diff --cached HEAD" in user_message:
|
|
return "read_diff"
|
|
if "safety reviewer" in user_message:
|
|
return "review_safety"
|
|
if "test-coverage reviewer" in user_message:
|
|
return "review_tests"
|
|
if "style / idiom reviewer" in user_message:
|
|
return "review_style"
|
|
return "other"
|
|
|
|
|
|
def _arbitrate(safety: str, tests: str, style: str) -> str:
|
|
if safety.startswith("CRITICAL"):
|
|
return f"BLOCK: {safety}"
|
|
if safety.startswith("WARNING") or tests.startswith("MISSING_TESTS"):
|
|
bits = []
|
|
if safety.startswith("WARNING"):
|
|
bits.append(safety)
|
|
if tests.startswith("MISSING_TESTS"):
|
|
bits.append(tests)
|
|
return f"BLOCK_WITH_OVERRIDE: {'; '.join(bits)}"
|
|
note = style if style.startswith("ANTIPATTERNS") else "clean"
|
|
return f"PASS_WITH_NOTES: {note}"
|
|
|
|
|
|
async def _run(
|
|
tmp_path: Path,
|
|
*,
|
|
safety_verdict: str,
|
|
tests_verdict: str,
|
|
style_verdict: str,
|
|
user_message: str = "multi-reviewer diff",
|
|
) -> dict[str, str]:
|
|
loader = _bundle_loader(tmp_path)
|
|
spec = loader.get_by_name("meta-codereview-current-diff")
|
|
plan = parse_meta_plan(spec)
|
|
assert plan is not None
|
|
|
|
async def runner(_system: str, user_msg: str) -> AsyncIterator[AgentEvent]:
|
|
which = _classify(user_msg)
|
|
if which == "read_diff":
|
|
yield TextDeltaEvent(
|
|
text="diff --git a/foo.py b/foo.py\n+def new_public():\n+ pass\n",
|
|
)
|
|
elif which == "review_safety":
|
|
yield TextDeltaEvent(text=safety_verdict)
|
|
elif which == "review_tests":
|
|
yield TextDeltaEvent(text=tests_verdict)
|
|
elif which == "review_style":
|
|
yield TextDeltaEvent(text=style_verdict)
|
|
elif which == "arbitrate":
|
|
yield TextDeltaEvent(
|
|
text=_arbitrate(safety_verdict, tests_verdict, style_verdict),
|
|
)
|
|
else:
|
|
yield TextDeltaEvent(text="memory record saved")
|
|
yield DoneEvent(text="")
|
|
|
|
orch = MetaOrchestrator(agent_runner=runner, skill_loader=loader)
|
|
result = await orch.run(
|
|
MetaMatch(plan=plan, inputs={"user_message": user_message}),
|
|
)
|
|
assert result.ok, f"plan failed: {result.error}"
|
|
return result.step_outputs
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_safety_critical_blocks(tmp_path: Path) -> None:
|
|
outputs = await _run(
|
|
tmp_path,
|
|
safety_verdict="CRITICAL: SQL injection via f-string in user_query",
|
|
tests_verdict="PASS: tests adequate",
|
|
style_verdict="CLEAN: no style issues found",
|
|
)
|
|
verdict = outputs["arbitrate"]
|
|
assert verdict.startswith("BLOCK:"), f"got {verdict!r}"
|
|
assert "SQL injection" in verdict
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_missing_tests_block_with_override(tmp_path: Path) -> None:
|
|
outputs = await _run(
|
|
tmp_path,
|
|
safety_verdict="CLEAR: no safety concerns found",
|
|
tests_verdict="MISSING_TESTS: new_public lacks tests",
|
|
style_verdict="CLEAN: no style issues found",
|
|
)
|
|
verdict = outputs["arbitrate"]
|
|
assert verdict.startswith("BLOCK_WITH_OVERRIDE"), f"got {verdict!r}"
|
|
assert "new_public" in verdict
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_all_clean_pass_with_notes(tmp_path: Path) -> None:
|
|
outputs = await _run(
|
|
tmp_path,
|
|
safety_verdict="CLEAR: no safety concerns found",
|
|
tests_verdict="PASS: tests adequate",
|
|
style_verdict="CLEAN: no style issues found",
|
|
)
|
|
verdict = outputs["arbitrate"]
|
|
assert verdict.startswith("PASS_WITH_NOTES"), f"got {verdict!r}"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_safety_critical_overrides_missing_tests(tmp_path: Path) -> None:
|
|
"""Strict priority: safety CRITICAL wins over tests MISSING_TESTS."""
|
|
outputs = await _run(
|
|
tmp_path,
|
|
safety_verdict="CRITICAL: hardcoded credential sk-xxxx",
|
|
tests_verdict="MISSING_TESTS: tests not added",
|
|
style_verdict="ANTIPATTERNS: bare except at foo.py:12",
|
|
)
|
|
verdict = outputs["arbitrate"]
|
|
assert verdict.startswith("BLOCK:"), f"got {verdict!r}"
|
|
assert "sk-xxxx" in verdict
|