Files
forja/tests/unit/test_api_promotions.py
T
2026-06-10 18:21:33 +02:00

174 lines
5.1 KiB
Python

"""Governance promotion flow: train → evaluate → request → approve."""
import shutil
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from forja_core.api import deps
from forja_core.main import create_app
FIXTURES = Path(__file__).parent.parent / "fixtures"
@pytest.fixture(autouse=True)
def patch_settings(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
agents_dir = tmp_path / "agents"
shutil.copytree(FIXTURES / "agents", agents_dir)
monkeypatch.setenv("DATA_DIR", str(tmp_path))
monkeypatch.setenv("AGENTS_DIR", str(agents_dir))
monkeypatch.setenv("POLICIES_DIR", str(FIXTURES / "policies"))
monkeypatch.setenv("DATASETS_DIR", str(FIXTURES / "datasets"))
monkeypatch.setenv("MODELS_DIR", str(FIXTURES / "models"))
monkeypatch.setenv("TRAINING_BACKEND", "mock")
for fn in (
deps.get_settings,
deps.get_registry,
deps.get_policy_store,
deps.get_dataset_store,
deps.get_model_store,
deps.get_training_backend,
deps.get_orchestrator,
deps.get_llm_provider,
deps.get_guardrail_engine,
):
fn.cache_clear()
def test_promotion_flow_approve_activates_draft_version() -> None:
client = TestClient(create_app())
run = client.post(
"/api/training/runs",
json={
"dataset_name": "incident_sft",
"model_name": "gpt4o_lora_base",
"agent_name": "test_agent",
"agent_version": "v2",
},
).json()
run_id = run["id"]
# Evaluation must cover the candidate version, not the active one.
eval_report = client.post(f"/api/training/runs/{run_id}/evaluate").json()
assert eval_report["passed"] is True
assert eval_report["agent_version"] == "v2"
promo = client.post(
"/api/promotions",
json={
"agent_name": "test_agent",
"agent_version": "v2",
"training_run_id": run_id,
"evaluation_report_id": eval_report["id"],
},
)
assert promo.status_code == 201
request_id = promo.json()["id"]
approve = client.post(
f"/api/promotions/{request_id}/approve",
json={"approved_by": "ops-lead", "comment": "Eval gate passed"},
)
assert approve.status_code == 200
agent = client.get("/api/agents/test_agent").json()
assert agent["version"] == "v2"
assert agent["state"] == "active"
def test_promotion_blocked_when_eval_failed(tmp_path: Path) -> None:
"""Promotion returns 422 if linked evaluation did not pass."""
from datetime import UTC, datetime
from uuid import uuid4
from forja_core.api.evaluation_persistence import save_evaluation
from forja_core.api.training_persistence import save_training_run
from forja_core.domain.training import (
EvaluationReport,
TrainingHyperparameters,
TrainingJobSpec,
TrainingRun,
)
client = TestClient(create_app())
settings = deps.get_settings()
run_id = uuid4()
now = datetime.now(UTC)
spec = TrainingJobSpec(
dataset_name="incident_sft",
dataset_version="v1",
model_name="gpt4o_lora_base",
model_version="v1",
agent_name="test_agent",
hyperparameters=TrainingHyperparameters(),
)
save_training_run(
settings.data_dir,
TrainingRun(
id=run_id,
backend="mock",
status="succeeded",
spec=spec,
external_job_id="mock:x",
created_at=now,
updated_at=now,
),
)
report = EvaluationReport(
id=uuid4(),
training_run_id=run_id,
agent_name="test_agent",
agent_version="v1",
passed=False,
scenario_count=1,
violation_count=1,
created_at=now,
notes="forced failure",
)
save_evaluation(settings.data_dir, report)
r = client.post(
"/api/promotions",
json={
"agent_name": "test_agent",
"agent_version": "v2",
"training_run_id": str(run_id),
"evaluation_report_id": str(report.id),
},
)
assert r.status_code == 422
def test_promotion_blocked_when_eval_covers_other_version() -> None:
"""Evaluating the active version must not unlock promoting a different draft."""
client = TestClient(create_app())
# No agent_version on the run → evaluation covers the active version (v1).
run = client.post(
"/api/training/runs",
json={
"dataset_name": "incident_sft",
"model_name": "gpt4o_lora_base",
"agent_name": "test_agent",
},
).json()
run_id = run["id"]
eval_report = client.post(f"/api/training/runs/{run_id}/evaluate").json()
assert eval_report["passed"] is True
assert eval_report["agent_version"] == "v1"
r = client.post(
"/api/promotions",
json={
"agent_name": "test_agent",
"agent_version": "v2",
"training_run_id": run_id,
"evaluation_report_id": eval_report["id"],
},
)
assert r.status_code == 422
assert "evaluate the candidate version" in r.json()["detail"]