Spaces:
Running on Zero
Running on Zero
Download tests/test_ingest.py from spacedout-bits/Oracle: direct link, hf CLI and curl.
- Browser
- Download file 10.1 kB
-
https://huggingface.co/spaces/spacedout-bits/Oracle/resolve/main/tests/test_ingest.py
- Command line
-
hf download hf://spaces/spacedout-bits/Oracle/tests/test_ingest.py
-
curl -L -o test_ingest.py https://huggingface.co/spaces/spacedout-bits/Oracle/resolve/main/tests/test_ingest.py
10.1 kB
| from datetime import date | |
| import pytest | |
| from finbot.extract import ExtractedDocument | |
| from finbot.ingest import Ingestor, _coerce_amount, _draft_from_obj, _parse_any_date | |
| from finbot.llm import LLMUnavailable | |
| TODAY = date(2026, 8, 15) | |
| class StubLLM: | |
| """Stands in for LLMClient without touching the network.""" | |
| def __init__(self, payload=None, enabled=True, raises=None): | |
| self.payload = payload | |
| self.enabled = enabled | |
| self.raises = raises | |
| self.calls = [] | |
| def chat_json(self, system, user, **kwargs): | |
| self.calls.append(("chat_json", system, user)) | |
| if self.raises: | |
| raise self.raises | |
| return self.payload | |
| def vision_json(self, system, user, image_bytes, mime_type, **kwargs): | |
| self.calls.append(("vision_json", system, user)) | |
| if self.raises: | |
| raise self.raises | |
| return self.payload | |
| class TestHelpers: | |
| def test_coerce_amount(self, raw, expected): | |
| assert _coerce_amount(raw) == expected | |
| def test_parse_any_date(self, raw, expected): | |
| assert _parse_any_date(raw, TODAY) == expected | |
| def test_draft_from_obj(self): | |
| draft = _draft_from_obj( | |
| { | |
| "amount": 450, | |
| "currency": "inr", | |
| "description": "Swiggy order", | |
| "date": "2026-08-01", | |
| "category": "food_dining", | |
| }, | |
| "USD", | |
| TODAY, | |
| ) | |
| assert draft.amount_minor == 45000 | |
| assert draft.currency == "INR" | |
| assert draft.category == "food_dining" | |
| def test_draft_rejects_zero_and_missing_amounts(self): | |
| assert _draft_from_obj({"amount": 0}, "INR", TODAY) is None | |
| assert _draft_from_obj({}, "INR", TODAY) is None | |
| assert _draft_from_obj("not a dict", "INR", TODAY) is None | |
| def test_draft_falls_back_to_keyword_category(self): | |
| # Model gave an unusable category, but the text is obviously transport. | |
| draft = _draft_from_obj( | |
| {"amount": 100, "description": "uber ride", "category": "???"}, "INR", TODAY | |
| ) | |
| assert draft.category == "transport" | |
| def test_draft_defaults_currency(self): | |
| draft = _draft_from_obj({"amount": 10}, "SGD", TODAY) | |
| assert draft.currency == "SGD" | |
| class TestFromText: | |
| def test_deterministic_path_does_not_call_the_model(self): | |
| llm = StubLLM() | |
| draft = Ingestor(llm, "INR").from_text("coffee 250", TODAY) | |
| assert draft.amount_minor == 25000 | |
| assert llm.calls == [] # the whole point of the fast path | |
| def test_falls_back_to_the_model_without_an_amount(self): | |
| llm = StubLLM(payload={"amount": 12, "currency": "USD", "description": "a coffee"}) | |
| draft = Ingestor(llm, "INR").from_text("bought a coffee for twelve dollars", TODAY) | |
| assert draft is not None | |
| assert draft.amount_minor == 1200 | |
| assert len(llm.calls) == 1 | |
| def test_returns_none_when_the_model_is_disabled(self): | |
| llm = StubLLM(enabled=False) | |
| assert Ingestor(llm, "INR").from_text("no numbers here", TODAY) is None | |
| def test_model_failure_degrades_quietly(self): | |
| llm = StubLLM(raises=LLMUnavailable("down")) | |
| assert Ingestor(llm, "INR").from_text("no numbers here", TODAY) is None | |
| class TestFromReceipt: | |
| def test_reads_a_total(self): | |
| llm = StubLLM( | |
| payload={ | |
| "merchant": "Blue Tokai", | |
| "amount": 480, | |
| "currency": "INR", | |
| "date": "2026-08-14", | |
| "category": "food_dining", | |
| "confidence": 0.9, | |
| } | |
| ) | |
| draft, error = Ingestor(llm, "INR").from_receipt(b"jpeg", "image/jpeg", TODAY) | |
| assert error == "" | |
| assert draft.amount_minor == 48000 | |
| assert draft.merchant == "Blue Tokai" | |
| def test_explains_when_disabled(self): | |
| llm = StubLLM(enabled=False) | |
| draft, error = Ingestor(llm, "INR").from_receipt(b"x", "image/jpeg", TODAY) | |
| assert draft is None | |
| assert "HF_TOKEN" in error | |
| def test_explains_an_unreadable_image(self): | |
| llm = StubLLM(payload={"amount": 0}) | |
| draft, error = Ingestor(llm, "INR").from_receipt(b"x", "image/jpeg", TODAY) | |
| assert draft is None | |
| assert "couldn't find a total" in error | |
| class TestFromDocument: | |
| def _doc(self): | |
| return ExtractedDocument(kind="csv", rows=[["Date", "Desc", "Amt"], ["01/08/2026", "Swiggy", "450"]]) | |
| def test_extracts_debits(self): | |
| llm = StubLLM( | |
| payload={ | |
| "transactions": [ | |
| { | |
| "date": "2026-08-01", | |
| "description": "Swiggy", | |
| "amount": 450, | |
| "currency": "INR", | |
| "direction": "debit", | |
| "category": "food_dining", | |
| } | |
| ] | |
| } | |
| ) | |
| pending, error = Ingestor(llm, "INR").from_document(self._doc(), user_id=1, today=TODAY) | |
| assert error == "" | |
| assert len(pending.drafts) == 1 | |
| assert pending.total_by_currency() == {"INR": 45000} | |
| def test_credits_are_counted_not_stored(self): | |
| # Storing income alongside spend would corrupt every total. | |
| llm = StubLLM( | |
| payload={ | |
| "transactions": [ | |
| {"date": "2026-08-01", "description": "Swiggy", "amount": 450, | |
| "currency": "INR", "direction": "debit"}, | |
| {"date": "2026-08-03", "description": "Salary", "amount": 85000, | |
| "currency": "INR", "direction": "credit"}, | |
| ] | |
| } | |
| ) | |
| pending, error = Ingestor(llm, "INR").from_document(self._doc(), user_id=1, today=TODAY) | |
| assert len(pending.drafts) == 1 | |
| assert "credits skipped" in pending.label | |
| def test_counts_unreadable_rows(self): | |
| llm = StubLLM( | |
| payload={ | |
| "transactions": [ | |
| {"date": "2026-08-01", "description": "Swiggy", "amount": 450, | |
| "currency": "INR", "direction": "debit"}, | |
| {"description": "junk"}, # no amount | |
| ] | |
| } | |
| ) | |
| pending, _ = Ingestor(llm, "INR").from_document(self._doc(), user_id=1, today=TODAY) | |
| assert len(pending.drafts) == 1 | |
| assert pending.skipped == 1 | |
| def test_accepts_a_bare_array(self): | |
| llm = StubLLM( | |
| payload=[{"date": "2026-08-01", "description": "Swiggy", "amount": 450, | |
| "currency": "INR", "direction": "debit"}] | |
| ) | |
| pending, error = Ingestor(llm, "INR").from_document(self._doc(), user_id=1, today=TODAY) | |
| assert error == "" | |
| assert len(pending.drafts) == 1 | |
| def test_reports_when_nothing_spendable_is_found(self): | |
| llm = StubLLM(payload={"transactions": []}) | |
| pending, error = Ingestor(llm, "INR").from_document(self._doc(), user_id=1, today=TODAY) | |
| assert pending is None | |
| assert "no spending transactions" in error | |
| def test_unsupported_document_short_circuits(self): | |
| llm = StubLLM() | |
| doc = ExtractedDocument(kind="unsupported", note="nope") | |
| pending, error = Ingestor(llm, "INR").from_document(doc, user_id=1, today=TODAY) | |
| assert pending is None | |
| assert error == "nope" | |
| assert llm.calls == [] # never spend a call on a file we cannot read | |
| def test_flags_truncation(self): | |
| llm = StubLLM( | |
| payload={"transactions": [{"date": "2026-08-01", "description": "x", | |
| "amount": 1, "currency": "INR", "direction": "debit"}]} | |
| ) | |
| doc = self._doc() | |
| doc.truncated = True | |
| pending, _ = Ingestor(llm, "INR").from_document(doc, user_id=1, today=TODAY) | |
| assert "truncated" in pending.label | |
| def test_disabled_model_explains_itself(self): | |
| llm = StubLLM(enabled=False) | |
| pending, error = Ingestor(llm, "INR").from_document(self._doc(), user_id=1, today=TODAY) | |
| assert pending is None | |
| assert "HF_TOKEN" in error | |
| class TestFailuresDoNotLeakInternals: | |
| """An infra failure is not the user's fault and must not surface tracebacks. | |
| Regression: a missing torchvision made every receipt reply | |
| "I couldn't read that receipt (local vision generation failed: 'ImportError'). | |
| Try a clearer photo?" -- which both leaked internals and blamed the photo. | |
| """ | |
| def _doc(self): | |
| return ExtractedDocument(kind="csv", rows=[["Date", "Amt"], ["01/08/2026", "450"]]) | |
| def test_receipt_infra_failure_message(self): | |
| llm = StubLLM(raises=LLMUnavailable("local vision generation failed: 'ImportError'")) | |
| draft, error = Ingestor(llm, "INR").from_receipt(b"jpeg", "image/jpeg", TODAY) | |
| assert draft is None | |
| assert "ImportError" not in error | |
| assert "clearer photo" not in error | |
| assert "type the amount" in error | |
| def test_statement_infra_failure_message(self): | |
| llm = StubLLM(raises=LLMUnavailable("CUDA out of memory")) | |
| pending, error = Ingestor(llm, "INR").from_document(self._doc(), user_id=1, today=TODAY) | |
| assert pending is None | |
| assert "CUDA" not in error | |
| def test_unreadable_image_still_blames_the_image_not_the_bot(self): | |
| # Model ran fine and found no total: here it IS about the picture. | |
| llm = StubLLM(payload={"amount": 0}) | |
| draft, error = Ingestor(llm, "INR").from_receipt(b"jpeg", "image/jpeg", TODAY) | |
| assert draft is None | |
| assert "couldn't find a total" in error | |