"""Stage 7 integration tests — the AgentLoop end-to-end (live LLM).""" import os import pytest from backend.agent.kb import KnowledgeBase from backend.agent.loop import AgentLoop LIVE = os.environ.get("GEMINI_API_KEY") is not None pytestmark = pytest.mark.skipif(not LIVE, reason="GEMINI_API_KEY not set") @pytest.fixture(scope="module") def kb(): return KnowledgeBase("PO") def _new_loop(kb): return AgentLoop(kb) def test_turn_1_create_po_intent_detected(kb): loop = _new_loop(kb) r = loop.handle_turn("I want to create a new purchase order") assert loop.state.current_journey == "Create Direct Purchase Order" assert r.reply # At least one trace event of kind 'intent_classified' kinds = [e["kind"] for e in r.trace_events] assert "intent_classified" in kinds def test_turn_offtopic_redirect(kb): loop = _new_loop(kb) loop.state.current_journey = "Create Direct Purchase Order" r = loop.handle_turn("tell me a joke") kinds = [e["kind"] for e in r.trace_events] assert "route_decided" in kinds def test_capital_adhoc_violation_surfaced(kb): """Capital + adhoc should be caught by the resolver and the agent should explain it.""" loop = _new_loop(kb) loop.state.current_journey = "Create Direct Purchase Order" # Force the forbidden combination directly loop.state.set_slot("potypeenum", "4") loop.state.set_slot("adhocitemclassml", "ML01") r = loop.handle_turn("here are some details") kinds = [e["kind"] for e in r.trace_events] assert "resolved" in kinds # Look at the most recent resolved event resolved_events = [e for e in r.trace_events if e["kind"] == "resolved"] assert resolved_events last = resolved_events[-1] assert last["data"]["locked_status"] == "blocked_by_violation" assert len(last["data"]["violations"]) >= 1 def test_multi_slot_extraction_advances(kb): """Several slots in one user turn should all land in state.""" loop = _new_loop(kb) loop.state.current_journey = "Create Direct Purchase Order" r = loop.handle_turn("buyer is Maria, currency USD, supplier ABC") # Buyer or currency should be in state now assert any(k in loop.state.slots for k in ("buyerhdr", "currencycode", "supplier_code")) def test_question_mid_flow_keeps_state(kb): loop = _new_loop(kb) loop.state.current_journey = "Create Direct Purchase Order" loop.state.set_slot("potypeenum", "2") r = loop.handle_turn("what does PO type capital mean exactly?") # State should be preserved assert loop.state.current_journey == "Create Direct Purchase Order" assert loop.state.slots.get("potypeenum") == "2" kinds = [e["kind"] for e in r.trace_events] # Q&A should have fired assert "qa_answered" in kinds or "route_decided" in kinds def test_termination_when_locked(kb): """After enough slots are filled and no violations, an explicit 'go ahead' fires the APIs.""" loop = _new_loop(kb) loop.state.current_journey = "Create Direct Purchase Order" # Fill enough slots to pass resolver (avoid violations). pilot = kb.journey("Create Direct Purchase Order") # Required slots come from resolver — fill them with placeholder values. from backend.agent.resolver import resolve as _resolve res = _resolve(pilot, {}) for req in res.active_requirements: loop.state.set_slot(req.slot, "X") # Also fill gating dropdowns with their first vocab value for gs in pilot.gating_slots[:6]: vocab = pilot.slot_value_vocab.get(gs, []) if vocab: loop.state.set_slot(gs, vocab[0]) r = loop.handle_turn("go ahead and create it") # Either we terminated (success) OR we got blocked by a violation surfaced by the # additional constraints — both are legitimate outcomes for this synthetic input. kinds = [e["kind"] for e in r.trace_events] assert any(k in kinds for k in ("fire_apis_done", "fire_apis_failed", "fire_apis_begin", "resolved"))