Files
minibits_wallet/jest.config.js
minibits-cash c56729aa38 Detect orphaned proofs; untangle the model layer so stores are testable
Two things, coupled because the first could not be tested without the second.

Detect the proofs/mint desync, and change nothing

`proofs.mintUrl` is a denormalized copy of a mint's LOCATOR, joined to
`mint.mintUrl` by string equality across two persistence engines — proofs in
SQLite, mint.mintUrl in the MMKV snapshot. A crash between those writes during
a mint-url edit desyncs them, and the balance view then counts the proofs in the
unit total while attributing them to no mint, which also makes them unspendable
(send and melt select by mint). Money visible in the header, owned by nothing.

reportOrphanedProofs() reports that and does nothing else. Deliberate: the sats
are the user's, and hiding them, refusing to start, or forcing a recovery are
all worse outcomes than a total that reads slightly high. The state self-heals
once a mint is (re-)added at that url.

It runs at STARTUP, not from `balances`, for two reasons. `balances` is a MobX
computed that re-runs on every proof and mint change; and mint removal
legitimately produces this exact state for a moment — MintsScreen destroys the
mint in one action and moves its proofs to SPENT in the next, so reactions
observe the gap. Detecting there would have fired on every normal removal, which
is how an alert teaches you to ignore it. At startup the tree is settled, so
anything found is a genuine persisted desync.

Untangle the model layer

MST stores could not be instantiated in a test at all, which is why this repo
has no store tests — only mirrored SQL and pure functions. The cause was not
jest: the model layer transitively imported most of the app. Five real defects:

- logService imported `../models` and destructured rootStoreInstance at MODULE
  SCOPE, while models/index eagerly instantiates the root store. A leaf logging
  service pulling the root store inverted the graph, and whether it worked came
  down to which module loaded first — the app has an entry order that survives
  it, a test importing a model directly reads RootStoreModel as undefined and
  throws during import. Now a deferred require, resolved on use.
- Mint imported the `../theme` BARREL, which re-exports useThemeColor ->
  ../services — the whole service layer, to read two colour constants.
- Five models imported the `../services` BARREL for log/Database, dragging in
  walletService -> syncQueueService -> notificationService (notifee).
- currency.ts imported the `../../components` BARREL for currency icons: a
  service module depending on the entire UI. (ChfIcon was already imported
  directly — it was inconsistent as well as wrong.)
- generateId lived in utils.ts beside a react-native-flash-message toast, so
  generating an id pulled in the UI stack. Split into its own module; utils.ts
  re-exports it, so existing callers are unaffected.

None of these change behaviour: same modules, narrower paths.

Test harness

jest already used the react-native preset; the gap was native and
source-shipped packages that the model layer reaches at import time. Mocks for
MMKV (Map-backed, behaves), op-sqlite, Sentry and nostr-tools, plus resolver
mappings for @scure/bip39 wordlists and nostr-tools' subpaths — its vendored
@noble copies ship source and importing them would cross versions on crypto
code, so the surface is mocked instead. op-sqlite and nostr-tools THROW if
actually called: a test that silently derives a bogus key and asserts on it is
worse than one that stops and explains. Real DB semantics stay where they were,
on node:sqlite with the production SQL.

Mocking Sentry rather than logService means suites can let the real logger load.
The full RootStore still cannot instantiate (AuthStore/NwcStore/
WalletProfileStore pull the service layer), so orphanedProofs.test.ts uses a
minimal root — enough for the money models, and enough for what comes next.

Also removed: Mint.setRandomColor and theme's getRandomIconColor. The helper's
only caller was that action, and the action had no callers at all.

Tests: 433 pass, including 12 new ones against the real MST models — the first
store tests in the repo. They pin the promise above: an orphaned proof still
counts in the unit total, reporting mutates nothing, and it heals when the mint
returns.

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

39 lines
2.4 KiB
JavaScript

module.exports = {
preset: 'react-native',
// Only treat *.test/*.spec files as tests. The default preset glob also
// matches every .js file under __tests__, which would pull in the i18n
// scripts (missingTranslations.js etc.) that are run via `yarn test:i18n`.
testMatch: ['**/*.(test|spec).[jt]s?(x)'],
transformIgnorePatterns: [
'node_modules/(?!((jest-)?react-native|@react-native|@react-native-community|@cashu|@noble|@scure|react-native-flash-message)/)',
],
// Several native or source-shipped packages are unusable under jest, and the
// MODEL layer reaches all of them at import time (Mint -> services barrel ->
// mmkvStorage / keyChain / db). That is what made MST stores impossible to
// instantiate in a test at all; these mappings are what make store tests possible.
moduleNameMapper: {
'^@noble/hashes/utils$': '@noble/hashes/utils.js',
// Same shape as the @noble mapping: the package only exports the explicit
// `.js` subpath, which jest's resolver will not infer.
'^@scure/bip39/wordlists/(.*)$': '@scure/bip39/wordlists/$1.js',
// quick-crypto's native module is unavailable under jest; route to a
// Node `crypto` shim so deps that import it at load time (e.g. bip32) work.
'^react-native-quick-crypto$': '<rootDir>/__mocks__/react-native-quick-crypto.js',
// MMKV and op-sqlite are native. MMKV is Map-backed and behaves; op-sqlite is
// only an import-time shim and throws if a statement is actually executed —
// use node:sqlite with the production SQL for real DB semantics (see the db
// suites).
'^react-native-mmkv$': '<rootDir>/__mocks__/react-native-mmkv.js',
'^@op-engineering/op-sqlite$': '<rootDir>/__mocks__/op-sqlite.js',
// Sentry ships untransformed ESM, so anything importing logService fails to
// parse. Mocking Sentry rather than logService lets the REAL logger load, so
// tests can cover code that logs instead of stubbing the logger away.
'^@sentry/react-native$': '<rootDir>/__mocks__/sentry-react-native.js',
// nostr-tools ships TS source and vendors its own @noble/curves + @noble/hashes
// (also source), importing subpaths jest cannot resolve. Mapping those would
// cross the nested copies onto the top-level @noble versions — on crypto code.
// Mock the surface instead; nothing in the model layer needs real nostr.
'^nostr-tools(/.*)?$': '<rootDir>/__mocks__/nostr-tools.js',
},
}