Oracle / tests /test_parser.py
spacedout-bits's picture
Conversational fallback with memory; reject zero-amount expenses
260fe4e verified
Raw
History Blame Contribute Delete
5.07 kB
from datetime import date
import pytest
from finbot.parser import parse_date_hint, parse_expense
TODAY = date(2026, 8, 15) # a Saturday
class TestParseDateHint:
def test_defaults_to_today(self):
resolved, span = parse_date_hint("coffee 250", TODAY)
assert resolved == TODAY
assert span is None
@pytest.mark.parametrize(
"text,expected",
[
("lunch yesterday", date(2026, 8, 14)),
("coffee today", TODAY),
("dinner day before yesterday", date(2026, 8, 13)),
("paid 3 days ago", date(2026, 8, 12)),
("rent on 2026-08-01", date(2026, 8, 1)),
("bought 12 aug", date(2026, 8, 12)),
("bought aug 12", date(2026, 8, 12)),
],
)
def test_resolves(self, text, expected):
resolved, span = parse_date_hint(text, TODAY)
assert resolved == expected
assert span is not None
def test_future_dates_roll_back_a_year(self):
# "12 dec" typed in August means last December, not a future spend.
resolved, _ = parse_date_hint("gift 12 dec", TODAY)
assert resolved == date(2025, 12, 12)
def test_bare_weekday_means_the_most_recent_past_one(self):
# TODAY is a Saturday; "friday" is yesterday.
resolved, _ = parse_date_hint("drinks friday", TODAY)
assert resolved == date(2026, 8, 14)
def test_same_weekday_goes_back_a_full_week(self):
resolved, _ = parse_date_hint("shopping saturday", TODAY)
assert resolved == date(2026, 8, 8)
def test_day_first_preferred_for_ambiguous_slashes(self):
resolved, _ = parse_date_hint("paid 03/04", TODAY)
assert resolved == date(2026, 4, 3)
def test_falls_back_to_month_first_when_day_first_impossible(self):
# 25 is not a month, so 12/25 can only be December 25th.
resolved, _ = parse_date_hint("gift 12/25", TODAY)
assert resolved == date(2025, 12, 25)
def test_invalid_date_does_not_crash(self):
resolved, _ = parse_date_hint("paid 2026-02-30", TODAY)
assert resolved == TODAY
class TestParseExpense:
def test_returns_none_without_an_amount(self):
assert parse_expense("hello there", "INR", TODAY) is None
def test_returns_none_for_empty(self):
assert parse_expense("", "INR", TODAY) is None
assert parse_expense(" ", "INR", TODAY) is None
def test_simple_entry(self):
draft = parse_expense("coffee 250", "INR", TODAY)
assert draft is not None
assert draft.amount_minor == 25000
assert draft.currency == "INR"
assert "coffee" in draft.description
assert draft.category == "food_dining"
assert draft.occurred_on == TODAY
def test_strips_the_amount_from_the_description(self):
draft = parse_expense("₹1,200 groceries at bigbasket", "INR", TODAY)
assert draft is not None
assert "1" not in draft.description
assert "₹" not in draft.description
assert draft.category == "groceries"
def test_strips_filler_words(self):
draft = parse_expense("spent 500 on dinner", "INR", TODAY)
assert draft is not None
assert draft.description == "dinner"
def test_date_phrase_leaves_the_description(self):
draft = parse_expense("45.50 lunch yesterday", "INR", TODAY)
assert draft is not None
assert draft.occurred_on == date(2026, 8, 14)
assert "yesterday" not in draft.description
assert draft.description == "lunch"
def test_hashtag_overrides_the_keyword_guess(self):
draft = parse_expense("320 lunch #transport", "INR", TODAY)
assert draft is not None
# "lunch" would normally be food_dining; the tag wins.
assert draft.category == "transport"
assert "#" not in draft.description
def test_unknown_hashtag_falls_back_to_other(self):
draft = parse_expense("100 thing #nonsense", "INR", TODAY)
assert draft is not None
assert draft.category == "other"
def test_explicit_currency_beats_the_default(self):
draft = parse_expense("€40 dinner", "INR", TODAY)
assert draft is not None
assert draft.currency == "EUR"
assert draft.amount_minor == 4000
def test_description_never_empty(self):
draft = parse_expense("250", "INR", TODAY)
assert draft is not None
assert draft.description == "unlabelled"
def test_keeps_the_raw_input(self):
draft = parse_expense(" coffee 250 ", "INR", TODAY)
assert draft is not None
assert draft.raw == "coffee 250"
class TestZeroAmounts:
"""Zero is not an expense -- otherwise ordinary sentences get logged."""
@pytest.mark.parametrize("text", ["question 0", "0", "round 0 of talks", "0.00"])
def test_zero_is_rejected(self, text):
assert parse_expense(text, "INR", TODAY) is None
def test_positive_amounts_still_parse(self):
assert parse_expense("coffee 1", "INR", TODAY).amount_minor == 100