grantforge-api / backend /tests /test_expectation_loop.py
GrantForge Bot
Deploy sha-565ad85979610064f6d1c18ab3b6404357d61073 — source build (no GHCR)
ce8f04a
Raw
History Blame Contribute Delete
6.69 kB
"""Expectation quality loop pure control plane (no LLM)."""
from __future__ import annotations
from agents.world_class_advisor import evaluate_application
from core.generation.expectation_loop import (
is_regulation_ready,
run_advisor_expectation_loop,
run_expectation_loop,
)
BRIEF = {
"key_rules": ["Wnioskodawca musi posiadać status MŚP", "Projekt musi spełniać zasadę DNSH"],
"required_sections": ["Opis projektu", "Budżet"],
"required_attachments": [],
"attention_points": ["Sprawdź DNSH / wpływ środowiskowy w opisie projektu."],
"usable": True,
}
def test_is_regulation_ready_false_for_structure_only():
assert (
is_regulation_ready(
{
"passed": True,
"score": 95,
"regulation_grounded_pass": False,
"blockers": [],
"grounding_mode": "structure_only",
}
)
is False
)
def test_loop_stops_when_ready():
sections = {
"Opis projektu": (
"Projekt MŚP z DNSH i pełnym opisem innowacji. " * 10
),
"Budżet": (
"Budżet z wkładem własnym i kosztami kwalifikowalnymi. " * 10
),
}
def evaluate(state):
return evaluate_application(
sections=state.get("generated_sections") or {},
brief=BRIEF,
grounding_mode="regulation",
)
result = run_expectation_loop(
evaluate=evaluate,
apply_fixes=None,
initial_state={"generated_sections": sections},
max_iterations=3,
min_score=70,
)
assert result.ready is True
assert result.stop_reason == "ready"
assert result.regulation_grounded_pass is True
assert result.iterations >= 1
assert not result.remaining_blockers
def test_loop_max_iter_with_blockers():
weak = {"Opis projektu": "za mało"}
def evaluate(state):
return evaluate_application(
sections=state.get("generated_sections") or {},
brief=BRIEF,
grounding_mode="regulation",
)
def fix(state, report):
# Improves slightly but not enough to cover Budżet
gen = dict(state.get("generated_sections") or {})
gen["Opis projektu"] = (gen.get("Opis projektu") or "") + " dodatek MŚP DNSH " * 5
state = dict(state)
state["generated_sections"] = gen
state["fixed_sections"] = ["Opis projektu"]
return state
result = run_expectation_loop(
evaluate=evaluate,
apply_fixes=fix,
initial_state={"generated_sections": weak},
max_iterations=2,
min_score=70,
)
assert result.ready is False
assert result.stop_reason == "max_iter"
assert result.iterations == 2
assert result.remaining_blockers
assert result.regulation_grounded_pass is False
def test_loop_structure_only_never_ready():
sections = {
"Opis projektu": "x" * 250,
"Budżet": "y" * 250,
}
def evaluate(state):
return evaluate_application(
sections=state.get("generated_sections") or {},
brief=BRIEF,
grounding_mode="structure_only",
)
result = run_expectation_loop(
evaluate=evaluate,
apply_fixes=lambda s, r: s,
initial_state={
"generated_sections": sections,
"external_context": {"grounding_mode": "structure_only"},
"grounding_mode": "structure_only",
},
max_iterations=2,
)
assert result.ready is False
assert result.regulation_grounded_pass is False
assert result.stop_reason in ("max_iter", "blocked_grounding")
def test_quality_expectation_step_wired_for_production():
"""Production helper run_quality_expectation_step drives real advisor + fix path."""
from core.generation.quality_loop import run_quality_expectation_step
state = {
"sections_plan": [
{"title": "Opis projektu", "type": "desc"},
{"title": "Budżet", "type": "budget"},
],
"generated_sections": {
"Opis projektu": "Pełny opis projektu MŚP z DNSH i celami. " * 8,
"Budżet": "Budżet z wkładem własnym 30% i kosztami kwalifikowalnymi. " * 8,
},
"external_context": {
"grounding_mode": "regulation",
"advisor_brief": BRIEF,
"required_sections": BRIEF["required_sections"],
"key_rules": BRIEF["key_rules"],
},
}
out = run_quality_expectation_step(state, source="holistic", min_score=70)
assert "advisor_before" in out
assert "regulation_ready" in out
assert out["stop_reason"] in ("ready", "needs_retry")
# Strong content with brief signals should be ready without needing rewrite
assert out["regulation_ready"] is True
assert out["stop_reason"] == "ready"
def test_soft_pass_gate_requires_regulation_grounded_pass_semantics():
"""Mirror generator gate: usable brief → only regulation_grounded_pass is ready."""
from core.generation.expectation_loop import is_regulation_ready
fluff_rep = evaluate_application(
sections={
"Opis projektu": "Projekt innowacyjny z bogatym doświadczeniem zespołu. " * 20,
"Budżet": "Budżet obejmuje koszty osobowe i sprzęt w pełnym zakresie. " * 20,
},
brief=BRIEF,
grounding_mode="regulation",
)
assert fluff_rep.regulation_grounded_pass is False
assert is_regulation_ready(fluff_rep) is False
def test_run_advisor_expectation_loop_improves_to_ready():
state = {
"generated_sections": {
"Opis projektu": "krótki start",
},
"external_context": {
"grounding_mode": "regulation",
"advisor_brief": BRIEF,
"required_sections": BRIEF["required_sections"],
},
}
def pure_fix(st, report):
# Simulate targeted rewrite filling required sections
st = dict(st)
st["generated_sections"] = {
"Opis projektu": (
"Pełny opis projektu MŚP z DNSH, innowacją i celami programu. " * 8
),
"Budżet": (
"Szczegółowy budżet, wkład własny 30%, koszty kwalifikowalne. " * 8
),
}
st["fixed_sections"] = ["Opis projektu", "Budżet"]
return st
result = run_advisor_expectation_loop(
state,
max_iterations=3,
min_score=70,
apply_fixes=pure_fix,
)
assert result.ready is True
assert result.stop_reason == "ready"
assert result.regulation_grounded_pass is True