PocketAccountant: custom ledger UI + deterministic agent (engine, ledger, retrieval, classifier)
c55ab5e verified | """Tests for the regulation retriever and the cite-or-abstain guardrail. | |
| Run from the project root: python -m unittest discover -s tests | |
| """ | |
| import unittest | |
| from src.agent import Agent, AssistantTurn, ScriptedClient, ToolCall, ToolContext, build_tools | |
| from src.ledger import Ledger | |
| from src.retrieval import Document, Retriever, tokenize | |
| def tiny_corpus(): | |
| return [ | |
| Document("ded-computo", "Deducci贸n de equipo de c贸mputo", "LISR Art. 34", "MX", 2024, | |
| "Puedes deducir tu laptop o computadora (equipo de c贸mputo) por " | |
| "depreciaci贸n al 30% anual si es indispensable y tiene CFDI."), | |
| Document("iva-mensual", "C谩lculo mensual del IVA", "LIVA Art. 5-D", "MX", 2024, | |
| "Cada mes se resta el IVA acreditable del IVA trasladado para obtener " | |
| "el IVA a cargo o el saldo a favor."), | |
| Document("us-se", "Self-employment tax", "IRS Schedule SE", "US", 2024, | |
| "Self-employment tax is 15.3% on 92.35% of net profit for Social " | |
| "Security and Medicare."), | |
| ] | |
| class TestTokenize(unittest.TestCase): | |
| def test_deaccent_stopwords_and_stemming(self): | |
| toks = tokenize("驴C贸mputo y depreciaci贸n de la laptop?") | |
| self.assertIn("computo", toks) # accent stripped | |
| self.assertNotIn("la", toks) # stopword removed | |
| # 'depreciaci贸n' and 'depreciaciones' must share a stem so plurals/derivations match | |
| self.assertEqual(tokenize("depreciaci贸n"), tokenize("depreciaciones")) | |
| def test_derivations_share_stem(self): | |
| # the flagship case: these must collapse to one stem for recall | |
| stems = {tokenize(w)[0] for w in ["deducir", "deducci贸n", "deducible"]} | |
| self.assertEqual(len(stems), 1) | |
| class TestRetriever(unittest.TestCase): | |
| def setUp(self): | |
| self.r = Retriever(tiny_corpus()) | |
| def test_finds_relevant_doc(self): | |
| top = self.r.search("puedo deducir mi laptop")[0] | |
| self.assertEqual(top.source, "LISR Art. 34") | |
| def test_jurisdiction_filter(self): | |
| results = self.r.search("tax", jurisdiction="US") | |
| self.assertTrue(all(p.jurisdiction == "US" for p in results)) | |
| def test_grounded_query(self): | |
| res = self.r.cite("c贸mo se calcula el IVA mensual") | |
| self.assertTrue(res.grounded) | |
| self.assertIn("5-D", res.passages[0].source) | |
| def test_abstains_on_unrelated(self): | |
| res = self.r.cite("xyzzy quux flibberty nonsense") | |
| self.assertFalse(res.grounded) | |
| self.assertIn("contador", res.message.lower()) | |
| class TestRealCorpusLoads(unittest.TestCase): | |
| def test_corpus_dir_loads(self): | |
| r = Retriever.from_corpus_dir("data/regulation") | |
| self.assertGreaterEqual(len(r.docs), 10) | |
| # The flagship "can I deduct my laptop?" question must ground correctly. | |
| res = r.cite("puedo deducir mi laptop o computadora") | |
| self.assertTrue(res.grounded) | |
| self.assertEqual(res.passages[0].source, "LISR Art. 34") | |
| class TestCiteToolInAgent(unittest.TestCase): | |
| def test_tool_present_only_with_retriever(self): | |
| lg = Ledger(":memory:") | |
| self.assertNotIn("cite_regulation", build_tools(ToolContext(lg))) | |
| ctx = ToolContext(lg, retriever=Retriever(tiny_corpus())) | |
| self.assertIn("cite_regulation", build_tools(ctx)) | |
| lg.close() | |
| def test_agent_grounds_a_rule_question(self): | |
| lg = Ledger(":memory:") | |
| ctx = ToolContext(lg, retriever=Retriever(tiny_corpus())) | |
| tools = build_tools(ctx) | |
| client = ScriptedClient([ | |
| AssistantTurn(tool_calls=[ToolCall("cite_regulation", | |
| {"query": "puedo deducir mi laptop"})]), | |
| AssistantTurn(text="S铆, por depreciaci贸n al 30% (LISR Art. 34)."), | |
| ]) | |
| trace = Agent(client, tools).run("驴Puedo deducir mi laptop?") | |
| self.assertEqual(trace.steps[0].tool, "cite_regulation") | |
| self.assertTrue(trace.steps[0].result["grounded"]) | |
| self.assertEqual(trace.steps[0].result["citations"][0]["source"], "LISR Art. 34") | |
| lg.close() | |
| def test_agent_abstains_when_ungrounded(self): | |
| lg = Ledger(":memory:") | |
| ctx = ToolContext(lg, retriever=Retriever(tiny_corpus())) | |
| tools = build_tools(ctx) | |
| r = tools["cite_regulation"].handler(query="zzzqqq unrelated gibberish") | |
| self.assertFalse(r["grounded"]) | |
| lg.close() | |
| if __name__ == "__main__": | |
| unittest.main() | |