fix(assertions): send Balance directives in Fava's JSON shape (libra-#39)
format_balance returned a Beancount source string, but fava.add_entry
feeds PUT /add_entries whose deserialiser expects
{"t": "Balance", "amount": {"number", "currency"}, ...} — every
assertion create 500'd. Returns the dict shape now.
The seven strict-xfail reconciliation tests tracking this flip to
regular passing tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
cf1a0967bf
commit
c0c8acbe30
2 changed files with 15 additions and 30 deletions
|
|
@ -115,13 +115,18 @@ def format_balance(
|
||||||
account: str,
|
account: str,
|
||||||
amount: int,
|
amount: int,
|
||||||
currency: str = "SATS"
|
currency: str = "SATS"
|
||||||
) -> str:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Format a balance assertion directive for Beancount.
|
Format a balance assertion directive for Fava's JSON API.
|
||||||
|
|
||||||
Balance assertions verify that an account has an expected balance on a specific date.
|
Balance assertions verify that an account has an expected balance on a specific date.
|
||||||
They are checked automatically by Beancount when the file is loaded.
|
They are checked automatically by Beancount when the file is loaded.
|
||||||
|
|
||||||
|
Fava's `deserialise` (fava/serialisation.py) expects
|
||||||
|
`{"t": "Balance", "amount": {"number", "currency"}, ...}` — the
|
||||||
|
previous source-string return 500'd on every assertion create
|
||||||
|
(libra-#39).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
date_val: Date of the balance assertion
|
date_val: Date of the balance assertion
|
||||||
account: Account name (e.g., "Assets:Bitcoin:Lightning")
|
account: Account name (e.g., "Assets:Bitcoin:Lightning")
|
||||||
|
|
@ -129,15 +134,15 @@ def format_balance(
|
||||||
currency: Currency code (default: "SATS")
|
currency: Currency code (default: "SATS")
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Beancount balance directive as a string
|
Fava API Balance entry dict ready for `fava.add_entry`.
|
||||||
|
|
||||||
Example:
|
|
||||||
>>> format_balance(date(2025, 11, 10), "Assets:Bitcoin:Lightning", 1500000, "SATS")
|
|
||||||
'2025-11-10 balance Assets:Bitcoin:Lightning 1500000 SATS'
|
|
||||||
"""
|
"""
|
||||||
date_str = date_val.strftime('%Y-%m-%d')
|
return {
|
||||||
# Two spaces between account and amount (Beancount convention)
|
"t": "Balance",
|
||||||
return f"{date_str} balance {account} {amount} {currency}"
|
"date": date_val.strftime('%Y-%m-%d'),
|
||||||
|
"account": account,
|
||||||
|
"amount": {"number": str(amount), "currency": currency},
|
||||||
|
"meta": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def format_posting_with_cost(
|
def format_posting_with_cost(
|
||||||
|
|
|
||||||
|
|
@ -18,19 +18,6 @@ from uuid import uuid4
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
# Tests that try to actually create + check an assertion all hit issue #39:
|
|
||||||
# `format_balance` returns a Beancount source string but `fava.add_entry`
|
|
||||||
# expects a dict, so Fava 500s on every assertion-create call. The contract
|
|
||||||
# violation is on libra's side; mark these strict-xfail so they go green
|
|
||||||
# automatically once #39 lands and the format_balance return shape is fixed.
|
|
||||||
ASSERTION_CREATE_BROKEN = pytest.mark.xfail(
|
|
||||||
reason="libra/issues/39 — POST /assertions submits a Beancount source string "
|
|
||||||
"to Fava's JSON API and 500s. Drop this marker when the format_balance "
|
|
||||||
"return type is changed to a dict.",
|
|
||||||
strict=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# helpers (local — assertion endpoints don't have wrapper helpers yet)
|
# helpers (local — assertion endpoints don't have wrapper helpers yet)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -58,7 +45,6 @@ async def _create_assertion(
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@ASSERTION_CREATE_BROKEN
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_assertion_against_empty_account_passes(
|
async def test_assertion_against_empty_account_passes(
|
||||||
client, super_user_headers, standard_accounts,
|
client, super_user_headers, standard_accounts,
|
||||||
|
|
@ -79,7 +65,6 @@ async def test_assertion_against_empty_account_passes(
|
||||||
assert body.get("difference_sats", 0) == 0
|
assert body.get("difference_sats", 0) == 0
|
||||||
|
|
||||||
|
|
||||||
@ASSERTION_CREATE_BROKEN
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_assertion_with_wrong_balance_returns_409(
|
async def test_assertion_with_wrong_balance_returns_409(
|
||||||
client, super_user_headers, standard_accounts,
|
client, super_user_headers, standard_accounts,
|
||||||
|
|
@ -102,7 +87,6 @@ async def test_assertion_with_wrong_balance_returns_409(
|
||||||
assert detail.get("difference_sats") == 999_999 or detail.get("difference_sats") == -999_999
|
assert detail.get("difference_sats") == 999_999 or detail.get("difference_sats") == -999_999
|
||||||
|
|
||||||
|
|
||||||
@ASSERTION_CREATE_BROKEN
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_assertion_with_tolerance_accepts_small_diff(
|
async def test_assertion_with_tolerance_accepts_small_diff(
|
||||||
client, super_user_headers, standard_accounts,
|
client, super_user_headers, standard_accounts,
|
||||||
|
|
@ -119,7 +103,6 @@ async def test_assertion_with_tolerance_accepts_small_diff(
|
||||||
assert r.json().get("status") == "passed"
|
assert r.json().get("status") == "passed"
|
||||||
|
|
||||||
|
|
||||||
@ASSERTION_CREATE_BROKEN
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_list_assertions_returns_created(
|
async def test_list_assertions_returns_created(
|
||||||
client, super_user_headers, standard_accounts,
|
client, super_user_headers, standard_accounts,
|
||||||
|
|
@ -145,7 +128,6 @@ async def test_list_assertions_returns_created(
|
||||||
assert assertion_id in ids, f"created assertion {assertion_id} missing from list {ids}"
|
assert assertion_id in ids, f"created assertion {assertion_id} missing from list {ids}"
|
||||||
|
|
||||||
|
|
||||||
@ASSERTION_CREATE_BROKEN
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_get_assertion_by_id(
|
async def test_get_assertion_by_id(
|
||||||
client, super_user_headers, standard_accounts,
|
client, super_user_headers, standard_accounts,
|
||||||
|
|
@ -167,7 +149,6 @@ async def test_get_assertion_by_id(
|
||||||
assert r.json().get("id") == assertion_id
|
assert r.json().get("id") == assertion_id
|
||||||
|
|
||||||
|
|
||||||
@ASSERTION_CREATE_BROKEN
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_recheck_assertion_via_check_endpoint(
|
async def test_recheck_assertion_via_check_endpoint(
|
||||||
client, super_user_headers, standard_accounts,
|
client, super_user_headers, standard_accounts,
|
||||||
|
|
@ -190,7 +171,6 @@ async def test_recheck_assertion_via_check_endpoint(
|
||||||
assert r.json().get("status") == "passed"
|
assert r.json().get("status") == "passed"
|
||||||
|
|
||||||
|
|
||||||
@ASSERTION_CREATE_BROKEN
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_delete_assertion_removes_it(
|
async def test_delete_assertion_removes_it(
|
||||||
client, super_user_headers, standard_accounts,
|
client, super_user_headers, standard_accounts,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue