UPDATE
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from rag.failure_contract import RAGServiceError
|
||||
from rag.services.pest_disease import get_pest_disease_detection
|
||||
from rag.services.soil_anomaly import get_soil_anomaly_insight
|
||||
from rag.services.water_need_prediction import get_water_need_prediction_insight
|
||||
from rag.services.yield_harvest import YieldHarvestRAGService
|
||||
|
||||
|
||||
class RAGFailureContractTests(SimpleTestCase):
|
||||
@patch("rag.services.soil_anomaly._create_audit_log", return_value=object())
|
||||
@patch("rag.services.soil_anomaly._fail_audit_log")
|
||||
@patch("rag.services.soil_anomaly._build_service_client")
|
||||
@patch("rag.services.soil_anomaly.build_rag_context", return_value="")
|
||||
@patch("rag.services.soil_anomaly._load_farm_or_error", return_value={"farm_uuid": "farm-1"})
|
||||
def test_soil_anomaly_invalid_json_raises_structured_error(
|
||||
self,
|
||||
_mock_load_farm,
|
||||
_mock_context,
|
||||
mock_build_client,
|
||||
_mock_fail,
|
||||
_mock_audit,
|
||||
):
|
||||
client = Mock()
|
||||
client.chat.completions.create.return_value = SimpleNamespace(
|
||||
choices=[SimpleNamespace(message=SimpleNamespace(content="not-json"))]
|
||||
)
|
||||
mock_build_client.return_value = (SimpleNamespace(system_prompt=""), client, "gpt-test")
|
||||
|
||||
with self.assertRaises(RAGServiceError) as exc_info:
|
||||
get_soil_anomaly_insight(farm_uuid="farm-1", anomaly_payload={})
|
||||
|
||||
self.assertEqual(exc_info.exception.contract.error_code, "invalid_json")
|
||||
|
||||
@patch("rag.services.water_need_prediction._create_audit_log", return_value=object())
|
||||
@patch("rag.services.water_need_prediction._fail_audit_log")
|
||||
@patch("rag.services.water_need_prediction._build_service_client")
|
||||
@patch("rag.services.water_need_prediction.build_rag_context", return_value="")
|
||||
@patch("rag.services.water_need_prediction._load_farm_or_error", return_value={"farm_uuid": "farm-1"})
|
||||
def test_water_need_invalid_json_raises_structured_error(
|
||||
self,
|
||||
_mock_load_farm,
|
||||
_mock_context,
|
||||
mock_build_client,
|
||||
_mock_fail,
|
||||
_mock_audit,
|
||||
):
|
||||
client = Mock()
|
||||
client.chat.completions.create.return_value = SimpleNamespace(
|
||||
choices=[SimpleNamespace(message=SimpleNamespace(content="not-json"))]
|
||||
)
|
||||
mock_build_client.return_value = (SimpleNamespace(system_prompt=""), client, "gpt-test")
|
||||
|
||||
with self.assertRaises(RAGServiceError) as exc_info:
|
||||
get_water_need_prediction_insight(farm_uuid="farm-1", prediction_payload={})
|
||||
|
||||
self.assertEqual(exc_info.exception.contract.error_code, "invalid_json")
|
||||
|
||||
def test_pest_detection_requires_image_with_structured_error(self):
|
||||
with self.assertRaises(RAGServiceError) as exc_info:
|
||||
get_pest_disease_detection(farm_uuid="farm-1", images=[])
|
||||
|
||||
self.assertEqual(exc_info.exception.contract.error_code, "missing_images")
|
||||
|
||||
@patch("rag.services.yield_harvest._create_audit_log", return_value=object())
|
||||
@patch("rag.services.yield_harvest._fail_audit_log")
|
||||
@patch("rag.services.yield_harvest.YieldHarvestRAGService._build_service_client")
|
||||
def test_yield_harvest_invalid_json_raises_structured_error(
|
||||
self,
|
||||
mock_build_client,
|
||||
_mock_fail,
|
||||
_mock_audit,
|
||||
):
|
||||
client = Mock()
|
||||
client.chat.completions.create.return_value = SimpleNamespace(
|
||||
choices=[SimpleNamespace(message=SimpleNamespace(content="not-json"))]
|
||||
)
|
||||
mock_build_client.return_value = (SimpleNamespace(system_prompt=""), client, "gpt-test")
|
||||
|
||||
with self.assertRaises(RAGServiceError) as exc_info:
|
||||
YieldHarvestRAGService().generate_narrative({"farm_uuid": "farm-1"})
|
||||
|
||||
self.assertEqual(exc_info.exception.contract.error_code, "invalid_json")
|
||||
@@ -0,0 +1,51 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from rag.embedding import embed_texts
|
||||
from rag.ingest import ingest
|
||||
from rag.observability import METRICS
|
||||
from rag.retrieve import search_with_query
|
||||
|
||||
|
||||
class RAGObservabilityTests(SimpleTestCase):
|
||||
def tearDown(self):
|
||||
METRICS.clear()
|
||||
|
||||
def test_embed_texts_records_empty_input_metric(self):
|
||||
result = embed_texts([])
|
||||
|
||||
self.assertEqual(result, [])
|
||||
self.assertEqual(METRICS["rag.embedding.empty_input|operation=embed_texts"], 1)
|
||||
|
||||
@patch("rag.retrieve.QdrantVectorStore")
|
||||
@patch("rag.retrieve.embed_single", return_value=[0.1, 0.2])
|
||||
@patch("rag.retrieve.load_rag_config")
|
||||
def test_search_with_query_records_empty_result_metric(self, mock_load_config, _mock_embed, mock_store_cls):
|
||||
mock_load_config.return_value = SimpleNamespace(
|
||||
embedding=SimpleNamespace(provider="gapgpt"),
|
||||
)
|
||||
mock_store = Mock()
|
||||
mock_store.search.return_value = []
|
||||
mock_store_cls.return_value = mock_store
|
||||
|
||||
result = search_with_query("query")
|
||||
|
||||
self.assertEqual(result, [])
|
||||
self.assertEqual(METRICS["rag.retrieve.empty_result|operation=search_with_query,service_id=None"], 1)
|
||||
|
||||
@patch("rag.ingest.load_sources", return_value=[])
|
||||
@patch("rag.ingest.QdrantVectorStore")
|
||||
@patch("rag.ingest.load_rag_config")
|
||||
def test_ingest_records_empty_sources_metric(self, mock_load_config, _mock_store_cls, _mock_sources):
|
||||
mock_load_config.return_value = SimpleNamespace(
|
||||
embedding=SimpleNamespace(provider="gapgpt"),
|
||||
)
|
||||
|
||||
result = ingest()
|
||||
|
||||
self.assertEqual(result["chunks_added"], 0)
|
||||
self.assertEqual(METRICS["rag.ingest.empty_sources|kb_name=None"], 1)
|
||||
@@ -4,10 +4,10 @@ from unittest.mock import Mock, patch
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from farm_data.models import SensorData
|
||||
from farm_data.models import PlantCatalogSnapshot, SensorData
|
||||
from farm_data.services import assign_farm_plants_from_backend_ids
|
||||
from irrigation.models import IrrigationMethod
|
||||
from location_data.models import SoilLocation
|
||||
from plant.models import Plant
|
||||
from rag.services.fertilization import get_fertilization_recommendation
|
||||
from rag.services.irrigation import get_irrigation_recommendation
|
||||
from weather.models import WeatherForecast
|
||||
@@ -27,8 +27,8 @@ class RecommendationServiceDefaultsTests(TestCase):
|
||||
temperature_max=23.0,
|
||||
temperature_mean=18.0,
|
||||
)
|
||||
self.plant = Plant.objects.create(name="گوجهفرنگی")
|
||||
self.onion = Plant.objects.create(name="پیاز")
|
||||
self.plant = PlantCatalogSnapshot.objects.create(backend_plant_id=101, name="گوجهفرنگی")
|
||||
self.onion = PlantCatalogSnapshot.objects.create(backend_plant_id=102, name="پیاز")
|
||||
self.irrigation_method = IrrigationMethod.objects.create(name="آبیاری قطرهای")
|
||||
self.farm_uuid = uuid.uuid4()
|
||||
self.farm = SensorData.objects.create(
|
||||
@@ -45,7 +45,7 @@ class RecommendationServiceDefaultsTests(TestCase):
|
||||
}
|
||||
},
|
||||
)
|
||||
self.farm.plants.set([self.plant])
|
||||
assign_farm_plants_from_backend_ids(self.farm, [self.plant.backend_plant_id])
|
||||
|
||||
def build_irrigation_optimizer_result(self):
|
||||
return {
|
||||
@@ -162,6 +162,39 @@ class RecommendationServiceDefaultsTests(TestCase):
|
||||
self.assertEqual(result["sections"][1]["type"], "tip")
|
||||
self.assertEqual(result["water_balance"]["active_kc"], 0.9)
|
||||
|
||||
@patch("rag.services.irrigation.calculate_forecast_water_needs", return_value=[])
|
||||
@patch("rag.services.irrigation.resolve_kc", return_value=0.9)
|
||||
@patch("rag.services.irrigation.resolve_crop_profile", return_value={})
|
||||
@patch("rag.services.irrigation.build_irrigation_method_text", return_value="method text")
|
||||
@patch("rag.services.irrigation.build_plant_text", return_value="plant text")
|
||||
@patch("rag.services.irrigation.build_rag_context", return_value="")
|
||||
@patch("rag.services.irrigation._get_optimizer")
|
||||
@patch("rag.services.irrigation.get_chat_client")
|
||||
def test_irrigation_recommendation_reads_from_canonical_farm_data_assignments(
|
||||
self,
|
||||
mock_get_chat_client,
|
||||
mock_get_optimizer,
|
||||
_mock_build_rag_context,
|
||||
mock_build_plant_text,
|
||||
_mock_build_irrigation_method_text,
|
||||
_mock_resolve_crop_profile,
|
||||
_mock_resolve_kc,
|
||||
_mock_calculate_forecast_water_needs,
|
||||
):
|
||||
assign_farm_plants_from_backend_ids(self.farm, [self.onion.backend_plant_id, self.plant.backend_plant_id])
|
||||
mock_get_optimizer.return_value.optimize_irrigation.return_value = self.build_irrigation_optimizer_result()
|
||||
mock_response = Mock()
|
||||
mock_response.choices = [Mock(message=Mock(content=self.build_irrigation_llm_result()))]
|
||||
mock_get_chat_client.return_value.chat.completions.create.return_value = mock_response
|
||||
|
||||
result = get_irrigation_recommendation(
|
||||
farm_uuid=str(self.farm_uuid),
|
||||
growth_stage="میوهدهی",
|
||||
)
|
||||
|
||||
self.assertEqual(result["selected_plant"]["name"], "پیاز")
|
||||
mock_build_plant_text.assert_called_once_with("پیاز", "میوهدهی")
|
||||
|
||||
@patch("rag.services.irrigation.calculate_forecast_water_needs", return_value=[])
|
||||
@patch("rag.services.irrigation.resolve_kc", return_value=0.9)
|
||||
@patch("rag.services.irrigation.resolve_crop_profile", return_value={})
|
||||
@@ -299,6 +332,34 @@ class RecommendationServiceDefaultsTests(TestCase):
|
||||
mock_build_plant_text.assert_called_once_with("پیاز", "flowering")
|
||||
self.assertEqual(result["data"]["primary_recommendation"]["npk_ratio"]["label"], "20-20-20")
|
||||
|
||||
@patch("rag.services.fertilization.build_plant_text", return_value="plant text")
|
||||
@patch("rag.services.fertilization.build_rag_context", return_value="")
|
||||
@patch("rag.services.fertilization._get_optimizer")
|
||||
@patch("rag.services.fertilization.get_chat_client")
|
||||
def test_fertilization_recommendation_uses_canonical_assignment_lookup_for_requested_catalog_plant(
|
||||
self,
|
||||
mock_get_chat_client,
|
||||
mock_get_optimizer,
|
||||
_mock_build_rag_context,
|
||||
mock_build_plant_text,
|
||||
):
|
||||
assign_farm_plants_from_backend_ids(self.farm, [self.plant.backend_plant_id, self.onion.backend_plant_id])
|
||||
mock_get_optimizer.return_value.optimize_fertilization.return_value = self.build_fertilization_optimizer_result()
|
||||
mock_response = Mock()
|
||||
mock_response.choices = [Mock(message=Mock(content="not-json"))]
|
||||
mock_get_chat_client.return_value.chat.completions.create.return_value = mock_response
|
||||
|
||||
result = get_fertilization_recommendation(
|
||||
farm_uuid=str(self.farm_uuid),
|
||||
plant_name="پیاز",
|
||||
growth_stage="گلدهی",
|
||||
)
|
||||
|
||||
optimizer_call = mock_get_optimizer.return_value.optimize_fertilization.call_args.kwargs
|
||||
self.assertEqual(getattr(optimizer_call["plant"], "name", None), "پیاز")
|
||||
mock_build_plant_text.assert_called_once_with("پیاز", "flowering")
|
||||
self.assertEqual(result["data"]["primary_recommendation"]["npk_ratio"]["label"], "20-20-20")
|
||||
|
||||
@patch("rag.services.fertilization.build_plant_text", return_value="plant text")
|
||||
@patch("rag.services.fertilization.build_rag_context", return_value="")
|
||||
@patch("rag.services.fertilization._get_optimizer")
|
||||
|
||||
Reference in New Issue
Block a user