Files
rune/tests/test_finance_tracker.py
T
Nick 853468783d
local-review.yml (push) Invalid workflow file
Auto-version and Release / release (push) Successful in 12s
CI Pipeline / lint (push) Failing after 31s
CI Pipeline / go-surface (push) Failing after 19s
CI Pipeline / canary (push) Failing after 1m32s
CI Pipeline / best-practices (push) Successful in 7s
CI Pipeline / documentation (push) Successful in 7s
CI Pipeline / type-check (push) Failing after 36s
CI Pipeline / gitea-offline (push) Successful in 19s
CI Pipeline / rust-surface (push) Successful in 4m36s
CI Pipeline / unit-tests (push) Failing after 5m50s
deploy / deploy (push) Failing after 15m12s
CI Pipeline / required-checks (push) Failing after 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 9s
feat(finance): stdlib-only finance tracker — #3 (contract BUILDER-2026-10-08T13:22:02Z-af7a8b)
- scripts/finance_tracker.py: CSV ledger (~/.rune/finance/, human-readable,
  portable), add/budget/report commands, month + week filters, per-category
  totals vs budgets with over-by flags, income/net, and recurring-payment
  detection (3+ same-note ~monthly payments) that surfaces subscription
  cancellation candidates — the exact 'flags subscriptions worth cancelling'
  acceptance.
- tests/test_finance_tracker.py 6/6: sign→kind, budget over-flag, net,
  week filter, monthly-subscription detection, one-off not flagged.
2026-10-08 14:24:29 +01:00

78 lines
2.6 KiB
Python

"""test_finance_tracker.py — #3: the ledger, budgets, report, and the
subscription flag that surfaces cancellation candidates. Isolated via
RUNE_FINANCE_DIR tmp override."""
import importlib.util
import json
import os
import sys
import tempfile
import unittest
from datetime import date, timedelta
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def _load(tmp: str):
os.environ["RUNE_FINANCE_DIR"] = tmp
spec = importlib.util.spec_from_file_location("finance_tracker", ROOT / "scripts/finance_tracker.py")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
class TestFinanceTracker(unittest.TestCase):
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.ft = _load(self._tmp.name)
def tearDown(self):
self._tmp.cleanup()
def test_add_records_negative_as_expense(self):
row = self.ft.add(-30.0, "Groceries", "weekly shop")
self.assertEqual(row["kind"], "out")
self.assertEqual(row["amount"], "30.00")
self.assertEqual(row["category"], "groceries")
def test_budget_and_report_flags_over(self):
self.ft.set_budget("groceries", 100)
self.ft.add(-60, "groceries", "shop 1")
self.ft.add(-60, "groceries", "shop 2")
r = self.ft.report()
self.assertEqual(r["spend"], 120.0)
self.assertEqual(
r["over_budget"], [{"category": "groceries", "spent": 120.0, "budget": 100.0, "over_by": 20.0}]
)
def test_income_and_net(self):
self.ft.add(1450, "salary", "October pay")
self.ft.add(-250, "bills", "electric")
r = self.ft.report()
self.assertEqual(r["income"], 1450.0)
self.assertEqual(r["net"], 1200.0)
def test_week_filter(self):
self.ft.add(-5, "coffee", "old", when=(date.today() - timedelta(days=10)).isoformat())
self.ft.add(-4, "coffee", "recent", when=(date.today() - timedelta(days=2)).isoformat())
r = self.ft.report(week=True)
self.assertEqual(r["spend"], 4.0)
def test_subscription_detection_monthly(self):
today = date.today()
for i in (3, 2, 1):
self.ft.add(-9.99, "subscriptions", "netflix", when=(today - timedelta(days=30 * i)).isoformat())
subs = self.ft.find_subscriptions()
self.assertEqual(len(subs), 1)
self.assertEqual(subs[0]["note"], "netflix")
self.assertEqual(subs[0]["monthly"], 9.99)
def test_one_off_not_flagged(self):
self.ft.add(-120, "electronics", "headphones")
self.assertEqual(self.ft.find_subscriptions(), [])
if __name__ == "__main__":
unittest.main()