2 Commits
Author SHA1 Message Date
minibits-cashandClaude Opus 4.8 73610e4db8 Run the db tests against the real code, not copies of it
The db suites hand-copied both the schema and each repo's SQL, then asserted
against the copy. That proves nothing about the code the app runs, and it drifted
six times across this branch while staying green — counters.test.ts was still
asserting the OLD (mintUrl, keysetId) key long after it was gone, and
transactionsMintUrl.test.ts kept passing while testing a function that had been
deleted.

Rather than make the copies track production, this removes them.

The op-sqlite jest mock now backs connection.ts's driver seam with node:sqlite.
connection.ts documents itself as "the single seam between the rest of the app
and the native SQLite library", so mocking exactly there means tests run the
production path end to end: real connection.ts (param sanitizing, result
adaptation, BEGIN/COMMIT batch emulation), real instance.ts (schema creation AND
the real migration runner), real repos. Net -418 lines, and no production code
changed.

- counters, proofReservation, onchainQuotes, meltRecovery, inFlightRequests and
  nut20's counter half now call Database.* directly. No copied DDL, no copied SQL.
- The migration suites import their SQL from the MIGRATIONS registry. Their
  PRE-migration shapes stay hand-written ON PURPOSE — those are frozen history and
  must never track today's schema, which is the whole reason v26/v28/v29/v31 froze
  their column lists. That distinction is now stated in each file so the next
  person does not "helpfully" point them at schema.ts and reintroduce the replay
  bug.

It found a bug on contact: walletCountersRepo allocates an index with a single
`INSERT … ON CONFLICT DO UPDATE … RETURNING`. The mock routed statements by
leading keyword, sent it down .run(), and the allocation failed — exactly the
point, since the mirror had RE-IMPLEMENTED that statement rather than running it
and so could never exercise RETURNING.

Two smaller gains from using the real path: counters' rollback test now trips the
real sanitizeParams guard instead of a hand-rolled NOT NULL violation, and
proofReservation locks real MST Proof nodes, because the repo calls isAlive() —
which only answers for an actual node, so plain objects never took production's
path.

Limits, recorded in the mock: Node's SQLite is not op-sqlite's build (version and
compile flags may differ), so this proves our SQL and our logic, not the exact
native binary — device testing still owns that. And each test FILE shares one
in-memory database (instance.ts caches its connection), so suites clear tables in
beforeEach rather than rebuilding.

Tests: 441 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 23:13:36 +02:00
minibits-cashandClaude Opus 4.8 2f7a2763f2 Reference mints by stable id, not by url
Completes the mint-identity work. A mint url is a network locator and mints
move, but the url had become the de facto foreign key for nearly every
persisted row, so a url edit had to fan out across the schema and mostly did
not. Each table now references the mint by an identity that a move cannot
disturb, and setMintUrl shrinks to the two things that genuinely hold a url:
Mint.mintUrl and the proofs.mintUrl cache.

Which identity, per table

mintId (Mint.id) where the row needs the mint itself:

- onchain_mint_quotes. The critical one: a quote's address stays creditable for
  as long as the mint exists (rows are never deleted), so the reference has to
  outlive a move. It followed row.mintUrl, and the watcher swallows errors by
  design — a renamed mint stranded deposits permanently and silently.
- reservations. A url edit racing an open send did not merely misfile the
  proofs; commitReservation resolved the mint by url, so it threw "Mint not
  found" and aborted the commit of an operation the mint had already performed.
- transactions. `mint` was two things at once, switched by status: a historical
  record of where a finished payment happened, AND a live pointer dialled for an
  open one. That conflation is why a rename had to rewrite in-flight rows —
  rewriting the very column that records the past. mintId takes the identity
  job, so `mint` is frozen as history and the status-scoped rewrite is retired.

No reference at all where the row already has a parent:

- inflight_requests, melt_recovery are CHILD rows of a transaction (their
  primary key IS transactionId), so the parent owns "which mint". Their mintUrl
  and keysetId copies had ZERO readers — the keyset that is used comes from
  inside meltPreview. Both dropped; the one mint-scoped query joins through
  transactions.mintId. Nothing left to go stale.

Mint.id gets referential authority only, never identity authority: findById
answers "which mint is this row about?" and must never answer "are these the
same mint?" — it is random and unrelated to the keys, so that question stays
with the keysets. The backfills are IS NULL-guarded so a resolved row can never
be re-pointed at whichever mint now answers an old url.

Backfills run from JS (v38 seed), not SQL: mints live in the MST/MMKV snapshot,
so nothing in SQL can map url -> id. Matching on url is trustworthy at exactly
that moment and no other — until now a url could not change without these rows
being rewritten to match. The join is spent once, at rest, instead of on every
rename.

Migration-system fixes found along the way

- _dbVersion is now DERIVED from the migration list. It was a hand-maintained
  literal, and it was already wrong: it said 33 while migration 34 existed, so
  34 would never have run. The failure is silent and asymmetric — fresh installs
  build from schema.ts and are fine, while upgrading devices land on a schema
  the code does not have. dbMigrationRegistry.test.ts pins this and the ordering
  invariants.
- Migrations 26/28/29/31 built their tables from the LIVE schema constants. A
  device replaying them would get today's shape, and the later ALTER adding the
  column would fail with "duplicate column name" — breaking upgrades from
  exactly the versions those migrations serve. Historical shapes are frozen
  locally now, and the schema.ts header no longer recommends the sharing.
- rootStoreModelVersion was left at 37 while the seed guarded on < 38, so the
  backfill would have re-run on every launch forever.

Tests: 421 pass. Six hand-mirrored suites had drifted from the schema they
claim to mirror; transactionsMintUrl.test.ts was worse — still passing while
testing a function this commit deletes, so it is removed. The mirroring pattern
is worth revisiting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:41:00 +02:00