import pytest import datetime from unittest.mock import MagicMock from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from app.database.base import Base from app.database.models_intents import IntentDB from app.services.outcome_service import record_outcome, OutcomeConflictError from agentic_reliability_framework.core.governance.intents import ( ProvisionResourceIntent, ResourceType, ) @pytest.fixture def db_session(): engine = create_engine("sqlite:///:memory:", future=True) TestingSessionLocal = sessionmaker(bind=engine, future=True) Base.metadata.create_all(bind=engine) sess = TestingSessionLocal() yield sess sess.close() @pytest.fixture def mock_risk_engine(): engine = MagicMock() engine.update_outcome = MagicMock() return engine def test_record_outcome_creates_row_and_updates_engine( db_session, mock_risk_engine): oss_intent = ProvisionResourceIntent( resource_type=ResourceType.VM, region="eastus", size="Standard", environment="dev", requester="test_user" ) oss_payload = oss_intent.model_dump(mode='json') intent = IntentDB( deterministic_id="intent_abc", intent_type="ProvisionResourceIntent", payload={}, oss_payload=oss_payload, created_at=datetime.datetime.utcnow() ) db_session.add(intent) db_session.commit() db_session.refresh(intent) outcome = record_outcome( db=db_session, deterministic_id="intent_abc", success=True, recorded_by="tester", notes="works", risk_engine=mock_risk_engine, idempotency_key="key123" ) assert outcome.success is True assert outcome.recorded_by == "tester" assert outcome.idempotency_key == "key123" mock_risk_engine.update_outcome.assert_called_once() # Idempotent call with same key should return existing outcome and not # call engine again outcome2 = record_outcome( db=db_session, deterministic_id="intent_abc", success=True, recorded_by="tester", notes="again", risk_engine=mock_risk_engine, idempotency_key="key123" ) assert outcome2.id == outcome.id mock_risk_engine.update_outcome.assert_called_once() # still once def test_conflict_different_result(db_session, mock_risk_engine): intent = IntentDB( deterministic_id="intent_def", intent_type="ProvisionResourceIntent", payload={}, created_at=datetime.datetime.utcnow() ) db_session.add(intent) db_session.commit() record_outcome( db_session, "intent_def", True, None, None, mock_risk_engine) with pytest.raises(OutcomeConflictError): record_outcome( db_session, "intent_def", False, None, None, mock_risk_engine) def test_nonexistent_intent(db_session, mock_risk_engine): with pytest.raises(ValueError): record_outcome( db_session, "missing", True, None, None, mock_risk_engine) def test_record_outcome_reconstruction_failure_does_not_update_engine( db_session, mock_risk_engine): # Create an intent with invalid oss_payload (missing required fields) intent = IntentDB( deterministic_id="intent_bad", intent_type="ProvisionResourceIntent", payload={}, oss_payload={"intent_type": "provision_resource"}, # missing fields created_at=datetime.datetime.utcnow() ) db_session.add(intent) db_session.commit() # This should NOT call risk_engine.update_outcome (no dummy fallback) outcome = record_outcome( db=db_session, deterministic_id="intent_bad", success=True, recorded_by="tester", notes="fallback test", risk_engine=mock_risk_engine ) assert outcome.success is True # The engine should NOT be updated because reconstruction failed mock_risk_engine.update_outcome.assert_not_called()