diff options
| author | Adam Soltys <[email protected]> | 2026-08-01 22:49:26 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-01 22:49:26 -0700 |
| commit | 232e2935a3c390ef78f670d978ff98e934693526 (patch) | |
| tree | 5060df4554bbcef7f15f2a1f7289545b773781e0 | |
| parent | c8e6de17311e3eaa18a73c2aa0688c80e89db254 (diff) | |
| download | coinosv3-232e2935a3c390ef78f670d978ff98e934693526.tar.gz coinosv3-232e2935a3c390ef78f670d978ff98e934693526.zip | |
Restore two helpers an earlier edit deleted, and guard the class of bug
A bulk replacement removed lookupMine() and the flag along
with the block it was rewriting, so names setup threw at runtime — the
kind of break every existing test happily passed over because nothing
ever constructed a feature and started it.
tools/features-smoke-test.js does exactly that: builds every feature
against a stubbed wallet with the network cut, runs init/render/stop,
and fails on undefined identifiers. It caught the second missing helper
immediately.
Co-Authored-By: Claude Fable 5 <[email protected]>
| -rw-r--r-- | src/features/names.js | 14 | ||||
| -rw-r--r-- | tools/features-smoke-test.js | 77 |
2 files changed, 91 insertions, 0 deletions
diff --git a/src/features/names.js b/src/features/names.js index ec422be..4e4f704 100644 --- a/src/features/names.js +++ b/src/features/names.js @@ -85,10 +85,24 @@ export function namesFeature(ctx) { save({}); } + // An imported seed (or a new browser) doesn't know its username — the + // registrar does. Ask by the wallet's nostr pubkey and adopt the answer + // before deciding whether anything needs claiming. + async function lookupMine() { + try { + const pk = wallet.nostrPubkey && wallet.nostrPubkey(); + if (!pk) return; + const r = await withTimeout( + fetch(`${REGISTRAR}/pubkey/${pk}`).then((x) => x.json()), 12000, 'registrar'); + if (r && r.name) save({ ...load(), name: r.name, domain: r.domain || DOMAIN, uri: r.uri }); + } catch (e) { console.warn('names: lookup failed —', e.message); } + } + // Everyone gets an address without being asked: the default username is a // prefix of the wallet's npub — unique, boring, and free. Custom names are // an optional change (and may cost something one day, to keep squatters // out), so nothing here ever blocks the wallet. + let checked = false; // setup finished (the pane waits on this) let retryTimer = null; let deadline = null; let lastError = null; diff --git a/tools/features-smoke-test.js b/tools/features-smoke-test.js new file mode 100644 index 0000000..3bc7324 --- /dev/null +++ b/tools/features-smoke-test.js @@ -0,0 +1,77 @@ +// Every feature must survive being built, started, rendered and stopped +// against a stubbed wallet — the cheap check that catches typos and +// undefined helpers that only bite at runtime in the browser. +// Run: bun tools/features-smoke-test.js +import { buildFeatures } from '../src/features/index.js'; + +let fails = 0; +const check = (name, ok, detail = '') => { + console.log(` ${ok ? '✓' : '✗'} ${name}${detail ? ' — ' + String(detail).slice(0, 160) : ''}`); + if (!ok) fails++; +}; + +// Nothing may reach the network in a smoke test; failing fast also exercises +// each feature's error path, which is where the runtime typos hide. +const realFetch = globalThis.fetch; +globalThis.fetch = async () => { throw new Error('offline (smoke test)'); }; +globalThis.window = undefined; +globalThis.localStorage = { + _d: {}, getItem(k) { return this._d[k] ?? null; }, setItem(k, v) { this._d[k] = String(v); }, + removeItem(k) { delete this._d[k]; }, key(i) { return Object.keys(this._d)[i] ?? null; }, + get length() { return Object.keys(this._d).length; }, +}; + +const errors = []; +const store = {}; +const wallet = { + watchOnly: false, offline: false, mnemonic: 'x '.repeat(11) + 'y', + utxos: [], txs: [], receive: [], change: [], + account: () => null, + loadFeatureState: (n, fb) => JSON.parse(store[n] || 'null') || fb, + saveFeatureState: (n, st) => { store[n] = JSON.stringify(st); }, + _cacheKey: () => 'smoke', saveCache: () => {}, + nostrPubkey: () => 'ab'.repeat(32), + nostrNpub: () => 'npub1smoketestsmoketestsmoketestsmoketestsmoketestsmoke', + nostrSign: (e) => ({ ...e, id: 'ab'.repeat(32), pubkey: 'ab'.repeat(32), sig: 'cd'.repeat(64) }), + registerCacheExtension: () => {}, registerRealtimeHook: () => {}, registerLoadHook: () => {}, + registerScanHook: () => {}, registerCacheSavedHook: () => {}, registerCoinLock: () => {}, + loadArkState: () => null, saveArkState: () => {}, _arkNs: () => 'ns', _arkKey: () => 'k', + _arkHoldKey: () => 'h', _arkVtxoOwner: () => null, adoptArkState: () => null, + freshReceive: () => ({ address: 'bc1qsmoke' }), freshChange: () => ({ address: 'bc1qchange' }), + balance: () => ({ spendableSat: 0 }), emit: () => {}, on: () => {}, +}; +const ctx = { + h: (tag, attrs, ...kids) => ({ tag, attrs, kids }), + ui: {}, render: () => {}, wallet, + hook: () => null, openMnemonic: async () => {}, + fmtAmount: String, unitLabel: () => 'sats', unitTag: () => 'sats', getUnit: () => 'sats', + parseAmount: (v) => parseInt(v, 10) || 0, + copyBtn: () => null, pasteBtn: () => null, toast: () => {}, blankSend: () => ({}), + openExternal: () => {}, +}; + +let features = []; +try { + features = buildFeatures(ctx); + check('all features construct', features.length > 0, `${features.length} features`); +} catch (e) { check('all features construct', false, e.message); } + +// Surface async failures (an undefined helper inside an async init shows up +// as an unhandled rejection, not a throw). +process.on('unhandledRejection', (e) => errors.push(String(e && e.message || e))); + +for (const f of features) { + for (const m of ['init', 'settingsCards', 'receiveModes', 'historyEntries', 'stop']) { + if (typeof f[m] !== 'function') continue; + try { f[m](); } catch (e) { errors.push(`${f.id}.${m}: ${e.message}`); } + } +} +await new Promise((r) => setTimeout(r, 300)); // let async inits settle + +const runtime = errors.filter((e) => /is not defined|is not a function|Cannot read/.test(e)); +check('no undefined identifiers at runtime', runtime.length === 0, runtime.join(' | ')); +if (errors.length && !runtime.length) console.log(` (${errors.length} benign offline error(s) ignored)`); + +globalThis.fetch = realFetch; +console.log(fails ? `\n❌ ${fails} failure(s)` : '\n✅ features survive a cold start'); +process.exit(fails ? 1 : 0); |
