"""Regression: dashboard field names must grade; prompt must not invent ids/names.""" from django.test import SimpleTestCase from apps.operations.services import cp707_knowledge as cp707 from apps.operations.services.insight_service import ( ANTI_HALLUCINATION_RULES, build_insight_user_prompt, enforce_mortality_wording, grade_context, ) class ResolveDashboardFieldsTests(SimpleTestCase): def test_resolve_bw_from_bobot_avg_gram(self): self.assertEqual(cp707._resolve_bw_grams({"bobot_avg_gram": 2100}), 2100.0) def test_resolve_bw_treats_zero_as_missing(self): self.assertIsNone(cp707._resolve_bw_grams({"bobot_avg_gram": 0})) self.assertIsNone(cp707._resolve_bw_grams({"averageWeight": 0})) def test_zero_bw_grades_as_unavailable_not_critical(self): graded = grade_context({"hari_ke": 48, "bobot_avg_gram": 0, "fcr_terakhir": 1.7}, "berat_ayam") bw = graded["analyses"]["bw"] self.assertEqual(bw["status"], "unknown") self.assertIn("tidak tersedia", bw["message"].lower()) def test_zero_fcr_grades_as_unavailable(self): graded = grade_context({"hari_ke": 48, "fcr_terakhir": 0, "bobot_avg_gram": 2100}, "fcr") fcr = graded["analyses"]["fcr"] self.assertEqual(fcr["status"], "unknown") self.assertIn("tidak tersedia", fcr["message"].lower()) def test_resolve_bw_from_average_weight(self): self.assertEqual(cp707._resolve_bw_grams({"averageWeight": 1950}), 1950.0) def test_resolve_mortality_from_mortalitas_kumulatif_persen(self): self.assertEqual( cp707._resolve_mortality({"mortalitas_kumulatif_persen": 2.4}), 2.4, ) def test_resolve_mortality_from_persen_hidup(self): self.assertAlmostEqual( cp707._resolve_mortality({"persen_hidup": 97.5}), 2.5, ) def test_resolve_mortality_from_deaths_over_doc(self): self.assertAlmostEqual( cp707._resolve_mortality( {"kematian_kumulatif_ekor": 50, "doc_in_ekor": 10000} ), 0.5, ) def test_dashboard_shaped_context_grades_bw_and_mortality(self): """Reproduce Kandang 2 day-48 payload: FCR present, BW/mort via dashboard keys.""" ctx = { "hari_ke": 48, "fcr_terakhir": 1.71, "bobot_avg_gram": 2100, "persen_hidup": 98.0, "mortalitas_hari_ini_ekor": 0, "kematian_kumulatif_ekor": 200, "doc_in_ekor": 10000, } graded = grade_context(ctx, "dashboard") analyses = graded["analyses"] self.assertIn("fcr", analyses) self.assertIn("bw", analyses) self.assertIn("mortality", analyses) self.assertEqual(analyses["bw"]["actual"], 2100.0) self.assertEqual(analyses["mortality"]["status"], "ok") self.assertIn("2.00%", analyses["mortality"]["message"]) self.assertEqual(graded.get("mortalitas_hari_ini_ekor"), 0) class MissingMetricsGradedAsUnknownTests(SimpleTestCase): def test_missing_bw_and_mortality_emit_unknown_not_silent(self): """When fields are absent, graded facts must say unknown so the LLM cannot use standards as actuals.""" ctx = {"hari_ke": 48, "fcr_terakhir": 1.71} graded = grade_context(ctx, "dashboard") self.assertEqual(graded["analyses"]["bw"]["status"], "unknown") self.assertEqual(graded["analyses"]["mortality"]["status"], "unknown") self.assertIn("tidak tersedia", graded["analyses"]["bw"]["message"].lower()) class InsightPromptNamingTests(SimpleTestCase): def test_user_prompt_uses_name_without_kandang_prefix_or_id(self): prompt = build_insight_user_prompt( topic="dashboard", kandang_name="Kandang 2", report_type="page", report_period="current", context={"hari_ke": 48, "kandangId": 6, "cycleId": 99}, ) self.assertIn("untuk unit bernama `Kandang 2`", prompt) self.assertNotIn("untuk kandang `Kandang 2`", prompt) self.assertNotIn("id=", prompt) self.assertNotIn('"kandangId"', prompt) self.assertNotIn('"cycleId"', prompt) def test_anti_hallucination_forbids_standard_as_actual_and_confusing_today_mortality(self): rules = ANTI_HALLUCINATION_RULES.lower() self.assertIn("standar", rules) self.assertIn("bukan nilai aktual", rules) self.assertIn("mortalitas_hari_ini", rules) self.assertIn("jangan menyebut id", rules) self.assertIn("severity_label", rules) class DuplicateKandangNameTests(SimpleTestCase): def test_collapse_kandang_kandang(self): from apps.operations.services.insight_service import collapse_duplicate_kandang self.assertEqual( collapse_duplicate_kandang( "Kandang Kandang 2 mengalami mortalitas kumulatif sebesar 10.44%." ), "Kandang 2 mengalami mortalitas kumulatif sebesar 10.44%.", ) def test_sanitize_insight_narrative_fields(self): from apps.operations.services.insight_service import sanitize_insight_narrative fixed = sanitize_insight_narrative( { "kesimpulan": "Kandang Kandang 2 mortalitas tinggi.", "insight": "Pantau kandang kandang 2 setiap hari.", } ) self.assertEqual(fixed["kesimpulan"], "Kandang 2 mortalitas tinggi.") self.assertEqual(fixed["insight"], "Pantau kandang 2 setiap hari.") class MortalityWordingConsistencyTests(SimpleTestCase): def test_critical_mortality_has_explicit_severity_label(self): result = cp707.analyze_mortality(9.49, 48) self.assertEqual(result["status"], "critical") self.assertEqual(result["severity_label"], "SANGAT TINGGI") self.assertEqual(result["actual_pct"], 9.49) self.assertIn("rendah", result["dilarang_kata"]) def test_enforce_rewrites_rendah_when_mortality_is_critical(self): graded = { "analyses": { "mortality": cp707.analyze_mortality(9.49, 48), } } parsed = { "kesimpulan": ( "Kandang 1 menunjukkan mortalitas kumulatif yang rendah pada hari ke-48, " "mencapai 9.49%, melebihi batas kritis CP707 (>7%)." ), "insight": "Lakukan nekropsi darurat.", } fixed = enforce_mortality_wording(parsed, graded) text = f"{fixed['kesimpulan']} {fixed['insight']}".lower() self.assertNotIn("rendah", text) self.assertIn("sangat tinggi", text) self.assertIn("9.49", fixed["kesimpulan"]) def test_enforce_leaves_ok_mortality_narrative_alone(self): graded = { "analyses": { "mortality": cp707.analyze_mortality(2.1, 20), } } parsed = { "kesimpulan": "Mortalitas kumulatif rendah (2.10%), masih dalam batas normal.", "insight": "Pertahankan biosekuriti.", } fixed = enforce_mortality_wording(parsed, graded) self.assertEqual(fixed["kesimpulan"], parsed["kesimpulan"]) class FeedBalanceGradingTests(SimpleTestCase): def test_grade_feed_balance_with_rebuild_context_fields(self): ctx = { "hari_ke": 34, "saldo_awal_karung": 500, "total_karung_masuk_iot": 1280, "total_karung_dituang_iot": 1200, "total_karung_keluar_iot": 50, } graded = grade_context(ctx, "hitung_karung") feed = graded["analyses"]["feed_balance"] self.assertEqual(feed["status"], "ok") self.assertEqual(feed["balance_karung"], 530.0) self.assertIn("saldo awal 500.0", feed["message"]) self.assertIn("masuk 1280.0", feed["message"]) def test_grade_feed_balance_with_scoped_derived_keys(self): ctx = { "hari_ke": 34, "saldo_awal_karung": 200, "karungMasuk_karung": 800, "karungDituang_karung": 750, "karungKeluar_karung": 0, } graded = grade_context(ctx, "hitung_karung") feed = graded["analyses"]["feed_balance"] self.assertEqual(feed["status"], "ok") self.assertEqual(feed["balance_karung"], 250.0) def test_grade_feed_balance_missing_emits_unknown(self): ctx = {"hari_ke": 34} graded = grade_context(ctx, "hitung_karung") feed = graded["analyses"]["feed_balance"] self.assertEqual(feed["status"], "unknown") self.assertIn("tidak tersedia", feed["message"].lower()) class DayZeroInsightTests(SimpleTestCase): def test_day_zero_grading_dashboard(self): ctx = { "hari_ke": 0, "bobot_avg_gram": 32.0, "persen_hidup": 99.8, "doc_in_ekor": 10000, "kematian_kumulatif_ekor": 20, "fcr_terakhir": None, } graded = grade_context(ctx, "dashboard") self.assertEqual(graded["hari_ke"], 0) analyses = graded["analyses"] self.assertIn("bw", analyses) self.assertIn("fcr", analyses) self.assertIn("mortality", analyses) # BW 32g is baseline DOC weight -> ok self.assertEqual(analyses["bw"]["status"], "ok") self.assertEqual(analyses["bw"]["standard"], 32.0) self.assertIn("bobot awal doc", analyses["bw"]["message"].lower()) # FCR not applicable on day 0 -> ok with explanatory message self.assertEqual(analyses["fcr"]["status"], "ok") self.assertIn("belum dihitung", analyses["fcr"]["message"].lower()) # Mortality 0.20% vs std 0.40% -> ok self.assertEqual(analyses["mortality"]["status"], "ok") def test_day_zero_standard_block_generation(self): ctx = {"hari_ke": 0} std_block = cp707.build_cp707_standard_block(ctx) self.assertIn("STANDAR CP 707 UNTUK HARI INI", std_block) self.assertIn("Bobot badan standar hari ke-0: Mengikuti bobot awal DOC masuk", std_block) self.assertIn("Mortalitas kumulatif standar hari ke-0: 0.4 %", std_block)