from common_cents.category_tree import build_category_tree def _by_name(nodes): return {n.name: n for n in nodes} def test_flat_paths(): nodes = build_category_tree({"Food": 1000, "Travel": 500}) by_name = _by_name(nodes) assert set(by_name) == {"Food", "Travel"} assert by_name["Food"].self_total == 1000 assert by_name["Food"].total() == 1000 def test_nested_paths_roll_up(): nodes = build_category_tree( {"Food": 1000, "Food:Restaurants": 2000, "Food:Restaurants:Sushi": 500} ) food = _by_name(nodes)["Food"] assert food.self_total == 1000 assert food.total() == 3500 rest = food.children["Restaurants"] assert rest.self_total == 2000 assert rest.total() == 2500 def test_intermediate_ancestors_have_zero_self_total(): nodes = build_category_tree({"A:B:C": 100}) a = _by_name(nodes)["A"] assert a.self_total == 0 assert a.total() == 100 assert a.children["B"].self_total == 0 assert a.children["B"].children["C"].self_total == 100 def test_prev_totals_separate_from_current(): nodes = build_category_tree({"Food": 100}, prev_totals={"Food": 200, "Travel": 50}) by_name = _by_name(nodes) assert by_name["Food"].self_total == 100 assert by_name["Food"].self_prev_total == 200 # Travel exists only in prev — appears in tree. assert by_name["Travel"].self_total == 0 assert by_name["Travel"].self_prev_total == 50 def test_extra_paths_create_empty_nodes(): nodes = build_category_tree({}, extra_paths=["Food:Restaurants"]) food = _by_name(nodes)["Food"] assert food.total() == 0 assert "Restaurants" in food.children def test_empty_inputs(): assert build_category_tree({}) == [] assert build_category_tree({}, {}) == [] def test_blank_segments_skipped(): """A path like ``::A`` should not create empty-named nodes.""" nodes = build_category_tree({"::Food": 100}) by_name = _by_name(nodes) assert "" not in by_name assert "Food" in by_name