fix(accounts): anchor duplicate-account detection to a real Open directive (libra-#48)

The existence check matched 'open <name>' anywhere in the chart source,
so a prior account's description metadata or a comment mentioning the
name produced a false 409, while a real directive with an inline comment
and no space ('open X;legacy') was missed → a duplicate Open was appended
and bean-check then rejected the file, breaking every later /api/source
write. Extract the check into a pure _open_directive_exists() anchored to
'^YYYY-MM-DD open <name>' with an account-boundary negative-lookahead, and
unit-test both failure directions plus prefix/child non-matches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-17 10:06:28 +02:00
commit 0ea96cd384
2 changed files with 77 additions and 9 deletions

View file

@ -60,6 +60,30 @@ def _escape_beancount_string(value: str) -> str:
)
def _open_directive_exists(source: str, account_name: str) -> bool:
"""Return True if `source` already contains an Open directive for exactly
`account_name`.
Anchored to a real `YYYY-MM-DD open <account>` directive line (re.MULTILINE)
so the account name can't match text inside another account's description
metadata or a comment (false positive spurious 409). The trailing
negative-lookahead `(?![\\w:-])` requires the next char not to be an
account-continuation char, so:
- a prefix (Expenses:Gas) does not match a longer sibling
(Expenses:GasStation / Expenses:Gas:Vehicle), and
- a real directive with an inline comment and no space
(`open Expenses:Gas;legacy`) is still detected (`;` ends the name),
which the previous `(?:\\s|$)` boundary missed duplicate write.
"""
return bool(
re.search(
rf"^\d{{4}}-\d{{2}}-\d{{2}} open {re.escape(account_name)}(?![\w:-])",
source,
re.MULTILINE,
)
)
class FavaClient:
"""
Async client for Fava REST API.
@ -1644,15 +1668,9 @@ class FavaClient:
source = source_data["source"]
# Step 2: Check if account already exists (may have been
# created by a concurrent request). Anchor on the Open
# directive and require the account to be followed by
# whitespace/end-of-line so a prefix (Expenses:Gas) does
# not match a longer sibling (Expenses:GasStation).
if re.search(
rf"open {re.escape(account_name)}(?:\s|$)",
source,
re.MULTILINE,
):
# created by a concurrent request). See
# _open_directive_exists for the anchoring rationale.
if _open_directive_exists(source, account_name):
logger.info(f"Account {account_name} already exists in {target_file}")
return {
"data": sha256sum,