| """ |
| Unit and Integration Tests for Multi-Agent Procurement System |
| |
| Run with: pytest test_procurement.py -v |
| |
| Tests cover: |
| - Individual agent node behavior |
| - State transitions and routing |
| - Budget validation logic |
| - Graph compilation |
| - Error handling |
| """ |
|
|
| import pytest |
| from typing import Optional |
|
|
| from langgraph.graph import StateGraph, START, END |
|
|
| from procurement_system import ( |
| ProcurementState, |
| ProcurementAgents, |
| build_procurement_graph, |
| ProcurementWorkflowExecutor, |
| create_llm, |
| mock_vendor_search, |
| validate_budget, |
| approval_gate, |
| _approval_router, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| @pytest.fixture |
| def llm(): |
| """Initialize language model for tests (Groq).""" |
| return create_llm() |
|
|
|
|
| @pytest.fixture |
| def agents(llm): |
| """Initialize agents for testing.""" |
| return ProcurementAgents(llm) |
|
|
|
|
| @pytest.fixture |
| def compiled_graph(llm): |
| """Compile procurement graph.""" |
| graph, memory, agents = build_procurement_graph(llm) |
| return graph, memory, agents |
|
|
|
|
| @pytest.fixture |
| def sample_state(): |
| """Create a sample procurement state.""" |
| return ProcurementState( |
| procurement_request="Enterprise cloud infrastructure", |
| vendor_options=[], |
| selected_vendor={}, |
| budget_limit=5000.0, |
| analysis_approved=False, |
| human_approved=False, |
| contract_draft="", |
| logs=[] |
| ) |
|
|
|
|
| @pytest.fixture |
| def state_with_vendors(sample_state): |
| """Create state with vendor options populated.""" |
| state = sample_state.copy() |
| state["vendor_options"] = [ |
| { |
| "vendor_id": "VENDOR_001", |
| "name": "CloudTech Solutions", |
| "service_type": "Cloud Infrastructure", |
| "price_per_month": 3500.0, |
| "capabilities": ["Auto-scaling", "99.99% Uptime"], |
| "reputation_score": 9.2, |
| "contract_terms": "12-month minimum" |
| }, |
| { |
| "vendor_id": "VENDOR_002", |
| "name": "InfraSpeed Inc", |
| "service_type": "Cloud Infrastructure", |
| "price_per_month": 2800.0, |
| "capabilities": ["Auto-scaling", "99.9% Uptime"], |
| "reputation_score": 8.5, |
| "contract_terms": "6-month minimum" |
| } |
| ] |
| return state |
|
|
|
|
| |
| |
| |
|
|
| class TestTools: |
| """Test LangChain @tool functions.""" |
|
|
| def test_mock_vendor_search_cloud(self): |
| """Test vendor search for cloud infrastructure.""" |
| result = mock_vendor_search("cloud infrastructure") |
|
|
| assert isinstance(result, list) |
| assert len(result) > 0 |
| assert all("vendor_id" in v for v in result) |
| assert all("price_per_month" in v for v in result) |
|
|
| def test_mock_vendor_search_software(self): |
| """Test vendor search for software licensing.""" |
| result = mock_vendor_search("software licensing") |
|
|
| assert isinstance(result, list) |
| assert len(result) > 0 |
|
|
| def test_mock_vendor_search_returns_complete_data(self): |
| """Verify vendor objects contain all required fields.""" |
| result = mock_vendor_search("cloud") |
| vendor = result[0] |
|
|
| required_fields = [ |
| "vendor_id", "name", "service_type", "price_per_month", |
| "capabilities", "reputation_score", "contract_terms" |
| ] |
| assert all(field in vendor for field in required_fields) |
|
|
| def test_validate_budget_within_limit(self): |
| """Test budget validation when within limit.""" |
| result = validate_budget(vendor_price=3000.0, budget_limit=5000.0) |
|
|
| assert result["within_budget"] == True |
| assert result["vendor_price"] == 3000.0 |
| assert result["budget_limit"] == 5000.0 |
| assert result["variance"] == 2000.0 |
| assert result["status"] == "APPROVED" |
|
|
| def test_validate_budget_exceeds_limit(self): |
| """Test budget validation when exceeding limit.""" |
| result = validate_budget(vendor_price=6000.0, budget_limit=5000.0) |
|
|
| assert result["within_budget"] == False |
| assert result["variance"] == -1000.0 |
| assert result["variance_percentage"] == -20.0 |
| assert result["status"] == "REJECTED" |
|
|
| def test_validate_budget_exact_limit(self): |
| """Test budget validation at exact limit.""" |
| result = validate_budget(vendor_price=5000.0, budget_limit=5000.0) |
|
|
| assert result["within_budget"] == True |
| assert result["variance"] == 0.0 |
| assert result["variance_percentage"] == 0.0 |
|
|
|
|
| |
| |
| |
|
|
| class TestAgentNodes: |
| """Test individual agent node behavior.""" |
|
|
| def test_research_node_populates_vendors(self, agents, sample_state): |
| """Test that research node finds vendors.""" |
| result = agents.research_node(sample_state) |
|
|
| |
| assert hasattr(result, 'update') |
| assert hasattr(result, 'goto') |
|
|
| |
| assert result.goto == "analysis_node" |
|
|
| |
| assert "vendor_options" in result.update |
| assert len(result.update["vendor_options"]) > 0 |
|
|
| def test_research_node_creates_log_entry(self, agents, sample_state): |
| """Test that research node creates audit log.""" |
| result = agents.research_node(sample_state) |
|
|
| assert "logs" in result.update |
| assert len(result.update["logs"]) > 0 |
| assert "ResearchNode" in result.update["logs"][0] |
|
|
| def test_analysis_node_within_budget(self, agents, state_with_vendors): |
| """Test analysis node with budget-acceptable vendor.""" |
| result = agents.analysis_node(state_with_vendors) |
|
|
| assert hasattr(result, 'update') |
| assert result.goto == "approval_gate" |
| assert result.update["analysis_approved"] == True |
| assert "selected_vendor" in result.update |
|
|
| def test_analysis_node_exceeds_budget(self, agents, state_with_vendors): |
| """Test analysis node with budget-exceeded vendor.""" |
| |
| state_with_vendors["budget_limit"] = 2000.0 |
|
|
| result = agents.analysis_node(state_with_vendors) |
|
|
| assert result.goto == END |
| assert result.update["analysis_approved"] == False |
|
|
| def test_legal_node_generates_contract(self, agents, sample_state): |
| """Test legal node generates contract draft.""" |
| sample_state["selected_vendor"] = { |
| "name": "CloudTech Solutions", |
| "service_type": "Cloud Infrastructure", |
| "price_per_month": 5000.0, |
| "capabilities": ["Auto-scaling", "99.99% Uptime SLA"], |
| "contract_terms": "12-month minimum" |
| } |
| sample_state["analysis_approved"] = True |
|
|
| result = agents.legal_node(sample_state) |
|
|
| assert "contract_draft" in result.update |
| assert len(result.update["contract_draft"]) > 0 |
| assert "PURCHASE" in result.update["contract_draft"].upper() or "CONTRACT" in result.update["contract_draft"].upper() |
| assert result.goto == END |
|
|
|
|
| |
| |
| |
|
|
| class TestRouting: |
| """Test graph routing and conditional logic.""" |
|
|
| def test_approval_gate_returns_dict_with_logs(self, sample_state): |
| """Test approval gate returns dict with a logs entry.""" |
| sample_state["selected_vendor"] = {"name": "TestVendor"} |
| result = approval_gate(sample_state) |
| assert isinstance(result, dict) |
| assert "logs" in result |
|
|
| def test_approval_router_routes_to_legal_when_approved(self, sample_state): |
| """Test _approval_router returns 'legal_node' when human_approved=True.""" |
| sample_state["human_approved"] = True |
| assert _approval_router(sample_state) == "legal_node" |
|
|
| def test_approval_router_routes_to_end_when_rejected(self, sample_state): |
| """Test _approval_router returns END when human_approved=False.""" |
| sample_state["human_approved"] = False |
| assert _approval_router(sample_state) == END |
|
|
|
|
| |
| |
| |
|
|
| class TestGraphCompilation: |
| """Test graph building and compilation.""" |
|
|
| def test_graph_compiles_successfully(self, compiled_graph): |
| """Test that graph compiles without errors.""" |
| graph, memory, agents = compiled_graph |
|
|
| assert graph is not None |
| assert memory is not None |
| assert agents is not None |
|
|
| def test_compiled_graph_has_required_nodes(self, compiled_graph): |
| """Test that compiled graph has all required nodes.""" |
| graph, _, _ = compiled_graph |
|
|
| |
| schema = graph.get_schema() |
|
|
| |
| assert callable(graph.invoke) or callable(graph.stream) |
|
|
| def test_graph_has_interrupt_before_legal(self, compiled_graph): |
| """Test that graph is configured to interrupt before legal_node.""" |
| graph, _, _ = compiled_graph |
|
|
| |
| |
| |
| assert graph is not None |
|
|
|
|
| |
| |
| |
|
|
| class TestIntegration: |
| """Integration tests of complete workflow.""" |
|
|
| def test_workflow_execution_happy_path(self, compiled_graph): |
| """Test complete workflow from start to approval point.""" |
| graph, memory, _ = compiled_graph |
| executor = ProcurementWorkflowExecutor(graph, memory) |
|
|
| state, _ = executor.start_workflow( |
| procurement_request="Cloud infrastructure", |
| budget_limit=5500.0, |
| ) |
|
|
| assert len(state["vendor_options"]) > 0 |
| assert state["selected_vendor"]["name"] is not None |
| assert state["analysis_approved"] == True |
| assert state["human_approved"] == False |
| assert len(state["logs"]) > 0 |
|
|
| def test_workflow_budget_rejection(self, compiled_graph): |
| """Test workflow rejection when budget is exceeded.""" |
| graph, memory, _ = compiled_graph |
| executor = ProcurementWorkflowExecutor(graph, memory) |
|
|
| state, _ = executor.start_workflow( |
| procurement_request="Cloud infrastructure", |
| budget_limit=2000.0, |
| ) |
|
|
| assert state["analysis_approved"] == False |
| assert state["contract_draft"] == "" |
|
|
| def test_workflow_approval_and_resumption(self, compiled_graph): |
| """Test workflow resumption after human approval.""" |
| graph, memory, _ = compiled_graph |
| executor = ProcurementWorkflowExecutor(graph, memory) |
|
|
| state, _ = executor.start_workflow( |
| procurement_request="Cloud infrastructure", |
| budget_limit=5500.0, |
| ) |
| assert state["human_approved"] == False |
|
|
| final_state, _ = executor.approve_vendor(approval=True) |
| assert final_state["human_approved"] == True |
| assert len(final_state["contract_draft"]) > 0 |
|
|
|
|
| |
| |
| |
|
|
| class TestStateManagement: |
| """Test state persistence and management.""" |
|
|
| def test_state_checkpoint_created(self, compiled_graph): |
| """Test that state is checkpointed at interruption.""" |
| graph, memory, _ = compiled_graph |
| executor = ProcurementWorkflowExecutor(graph, memory) |
|
|
| executor.start_workflow( |
| procurement_request="Cloud infrastructure", |
| budget_limit=5500.0, |
| ) |
| config = {"configurable": {"thread_id": executor.thread_id}} |
| saved_state = graph.get_state(config) |
| assert saved_state is not None |
| assert saved_state.values is not None |
|
|
| def test_state_recovery_after_interruption(self, compiled_graph): |
| """Test state can be retrieved after interruption.""" |
| graph, memory, _ = compiled_graph |
| executor = ProcurementWorkflowExecutor(graph, memory) |
|
|
| state, _ = executor.start_workflow( |
| procurement_request="Cloud infrastructure", |
| budget_limit=5500.0, |
| ) |
| current = executor.get_state() |
| assert current["budget_limit"] == state["budget_limit"] |
| assert current["selected_vendor"]["name"] == state["selected_vendor"]["name"] |
|
|
|
|
| |
| |
| |
|
|
| class TestErrorHandling: |
| """Test error handling and edge cases.""" |
|
|
| def test_empty_vendor_list_handling(self, agents, sample_state): |
| """Test handling of empty vendor search results.""" |
| |
| |
| sample_state["vendor_options"] = [] |
|
|
| |
| |
| |
|
|
| def test_missing_vendor_fields(self, agents, sample_state): |
| """Test handling of incomplete vendor data.""" |
| sample_state["vendor_options"] = [ |
| { |
| "vendor_id": "VENDOR_001", |
| |
| "name": "Incomplete Vendor" |
| } |
| ] |
|
|
| |
| result = agents.analysis_node(sample_state) |
|
|
| |
| assert hasattr(result, 'goto') |
|
|
| def test_invalid_budget_values(self): |
| """Test budget validation with edge case values.""" |
| |
| result = validate_budget(vendor_price=1000.0, budget_limit=0.0) |
| assert result["within_budget"] == False |
|
|
| |
| result = validate_budget(vendor_price=-100.0, budget_limit=1000.0) |
| assert result["within_budget"] == True |
|
|
| |
| result = validate_budget(vendor_price=999999.99, budget_limit=1000000.0) |
| assert result["within_budget"] == True |
|
|
|
|
| |
| |
| |
|
|
| class TestPerformance: |
| """Test performance characteristics.""" |
|
|
| def test_vendor_search_latency(self): |
| """Test vendor search completes quickly.""" |
| import time |
|
|
| start = time.time() |
| result = mock_vendor_search("cloud") |
| elapsed = time.time() - start |
|
|
| |
| assert elapsed < 1.0 |
| assert len(result) > 0 |
|
|
| def test_budget_validation_latency(self): |
| """Test budget validation completes quickly.""" |
| import time |
|
|
| start = time.time() |
| result = validate_budget(5000.0, 5500.0) |
| elapsed = time.time() - start |
|
|
| |
| assert elapsed < 0.1 |
| assert result["within_budget"] == True |
|
|
|
|
| |
| |
| |
|
|
| class TestConfiguration: |
| """Test system configuration and environment.""" |
|
|
| def test_langsmith_config_detection(self): |
| """Test that LangSmith config can be detected.""" |
| import os |
|
|
| |
| api_key = os.getenv("LANGSMITH_API_KEY", "") |
| project = os.getenv("LANGSMITH_PROJECT", "") |
|
|
| |
| assert isinstance(api_key, str) |
| assert isinstance(project, str) |
|
|
| def test_groq_llm_available(self, llm): |
| """Test that Groq LLM is configured and invocable.""" |
| assert llm is not None |
| assert hasattr(llm, 'invoke') |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| |
| pytest.main([__file__, "-v", "--tb=short"]) |
|
|