The three credit_balance unit tests mock the DB session but let
credit_balance call the real store_cashu_transaction_with_retry, which
opens its own session against the global engine. In CI that database has
no cashu_transactions table; since storage failures now propagate
(a60b04ae) instead of being silently swallowed, the tests failed with
sqlite3.OperationalError. Patch the audit store like the existing
propagation test already does.
Comprehensive audit of all money-moving code paths on current main.
Found 8 live vulnerabilities where users, providers, or node runners
can lose funds, plus 1 false-green in the existing emergency refund
test suite.
Live vulnerabilities (all RED — tests assert correct/safe behaviour):
V-E1 send_refund() swallows DB failure after minting a refund token
base.py ~line 3625 — except Exception: pass
V-E2 Emergency refund (chat) — same except: pass
base.py ~line 3992 (existing test is a false green — 500-char
window too short)
V-E3 Emergency refund (responses API) — identical pattern
base.py ~line 4972
V-E4 Balance refund endpoint swallows DB failure
balance.py ~line 628
V-E5 credit_balance() swallows 'in' transaction DB failure
wallet.py ~line 1715
V-E6 EHBP refund token — except: pass after store
ehbp.py ~line 762
V-E7 EHBP 'in' transaction — except: pass after store
ehbp.py ~line 1028
V-E8 Admin withdraw returns token even when DB store fails
admin.py ~line 475
V-E9 Window regression guard (GREEN) — documents the false-green in
the existing test_emergency_refund_no_try_except_pass
Test results: 8 failed, 1 passed.
When the mint no longer has a Lightning quote (e.g. after TTL purge or
restart), check_invoice_payment() was logging an ERROR and returning False.
This caused the periodic_invoice_watcher to keep polling the same dead
quote every 10s forever, producing infinite log spam.
Now _is_quote_not_found() detects 'Mint Error: quote not found (Code: 0)'
and returns True, allowing _expire_invoice_if_authoritatively_unpaid()
to mark the invoice as expired so the watcher stops polling it.
The check is case-insensitive and requires code 0 to avoid false positives
from other quote-related errors.
Provider scoping (items 1/2/6):
- Key visibility maps on (model_id.lower(), upstream_provider_id), matching
refresh_model_maps, so a disable/override row on one provider never leaks
onto another provider's model, and matching is case-insensitive.
Data safety (items 3/5):
- Degraded OpenRouter fetches (network error, 429, non-200, bad payload)
return None (unknown) instead of []; a provider whose path set is unknown
keeps its previously persisted rows instead of being wiped.
- Endpoint payload parsing moved fully inside try, with a list guard, so
endpoints:null or non-list shapes are swallowed as documented.
- refresh with an empty live upstream list is a no-op; the unfiltered
DELETE in the prune path is gone (prune now keys off enabled DB rows).
Hot path (items 4/12/14):
- Persist uses chunked bulk INSERTs (one statement per 500 rows) instead of
per-row ORM adds; redundant ix_model_paths_model_id index dropped.
- Read routes filter in SQL instead of materializing the whole table, and
output ordering is deterministic (public id + path), independent of rowid.
- Visibility no longer rebuilds fully priced Model objects per override row;
it reads id/forwarded_model_id/canonical_slug straight off ModelRow.
Path/id contract (items 7/8/9/11):
- discovery_path_for_subprovider/discovery_base_paths hooks on
BaseUpstreamProvider, overridden by OpenRouterUpstreamProvider, mirror
_apply_provider_field so discovery and response stamping cannot drift
(openrouter:OpenRouter now correctly maps to unknown).
- openrouter_author_slug falls back to a slash-containing forwarded_model_id,
so admin-created alias rows are discoverable.
- public_model_id splits on the first slash, same as get_base_model_id, so
discovery ids can be sent to chat completions verbatim.
Lifecycle (items 10/13):
- ENABLE_MODEL_PATHS_REFRESH kill switch; interval and flag re-read every
loop iteration, and the task idles (not exits) while disabled.
- First 429 latches and aborts the remaining fan-out for the cycle; a
per-cycle cache dedupes fetches across providers sharing a base URL.
- refresh_model_maps prunes paths of disabled/deleted providers so admin
mutations take effect immediately; rows carry updated_at and both
endpoints expose it.
Tests (item 15) rewritten through the public refresh entry point with
transport-level httpx.MockTransport fakes, FK enforcement on, and coverage
for the periodic loop. Migration re-chained onto 9c4d8e2f1a6b.