from __future__ import annotations import json import sys import unittest from dataclasses import dataclass from pathlib import Path SRC = Path(__file__).resolve().parents[1] / "src" if str(SRC) not in sys.path: sys.path.insert(0, str(SRC)) from legawa.agents.surat import reply @dataclass class FakeSettings: strict_citations: bool = True class FakeLLM: def __init__(self, response: str): self.response = response self.calls: list[tuple[list[dict], dict]] = [] def chat(self, messages, **kwargs): self.calls.append((messages, kwargs)) return self.response class FakePool: def __init__(self, small_response: str, big_response: str, *, strict_citations: bool = True): self.small = FakeLLM(small_response) self.big = FakeLLM(big_response) self.settings = FakeSettings(strict_citations=strict_citations) class FakePasalClient: def __init__(self): self.calls: list[dict] = [] def search(self, **kwargs): self.calls.append(kwargs) if kwargs["q"] in {"UU 13/2003", "UU 6/2023"}: return { "results": [ { "title": f"Undang-Undang Nomor {kwargs['q'].split()[1].split('/')[0]} Tahun {kwargs['q'].split('/')[1]}", "frbr_uri": f"akn/id/act/uu/{kwargs['q'].split('/')[1]}/{kwargs['q'].split()[1].split('/')[0]}", "status": "berlaku", } ] } return {"results": []} class SuratReplyTests(unittest.TestCase): def _triase_json(self) -> str: return json.dumps( { "kategori": "aspirasi_kebijakan", "urgensi": "tinggi", "sentimen": "negatif", "dapil_relevance": True, "nama_pengirim": "Sukma", "lokasi": "Bekasi", "isu_utama": "Permenaker dianggap merugikan pekerja.", "permintaan_konkret": ["RDP", "Revisi aturan"], "peraturan_disebut": ["UU 13/2003"], "perlu_tindak_lanjut_staf": True, "rangkuman": "Surat menyoroti dampak kebijakan ketenagakerjaan.", } ) def test_reply_blocks_unverified_citation(self) -> None: pool = FakePool( self._triase_json(), "Kami merujuk pada UU 13/2003 dan Perpres 76/2021 dalam balasan ini.", ) pasal = FakePasalClient() with self.assertRaises(ValueError) as ctx: reply(pool, pasal, "surat uji", verify_law=True) self.assertIn("Perpres 76/2021", str(ctx.exception)) self.assertEqual(len(pool.big.calls), 1) self.assertGreaterEqual(len(pasal.calls), 2) def test_reply_warns_instead_of_blocking_when_strict_disabled(self) -> None: # With strict_citations=False, an unverifiable citation should warn, # not block — consistent with penyusun and analis_ruu behaviour. # Useful when pasal.id is rate-limited or returning transport errors. pool = FakePool( self._triase_json(), "Kami merujuk pada UU 13/2003 dan Perpres 76/2021 dalam balasan ini.", strict_citations=False, ) pasal = FakePasalClient() result = reply(pool, pasal, "surat uji", verify_law=True) self.assertIsNotNone(result.balasan) self.assertIn("Perpres 76/2021", result.balasan or "") def test_reply_accepts_verified_citation(self) -> None: pool = FakePool( self._triase_json(), "Kami merujuk pada UU 13/2003 dalam balasan ini.", ) pasal = FakePasalClient() result = reply(pool, pasal, "surat uji", verify_law=True) self.assertIsNotNone(result.balasan) self.assertEqual(len(pool.big.calls), 1) self.assertGreaterEqual(len(pasal.calls), 2) if __name__ == "__main__": unittest.main()