2026-09-29 14:42:44 -04:00
const test = require ( 'node:test' );
const assert = require ( 'node:assert/strict' );
const fs = require ( 'node:fs' );
const vm = require ( 'node:vm' );
const html = fs . readFileSync ( 'docker/lnd-ui/index.html' , 'utf8' );
function extract ( name ) {
const start = html . indexOf ( ' function ' + name + '(' );
const end = html . indexOf ( '\n }' , start ) + 10 ;
assert . ok ( start >= 0 && end > start );
return html . slice ( start , end );
}
function fixture () {
const elements = new Map ();
const el = id => { if ( ! elements . has ( id )) elements . set ( id ,{ style : {}, textContent : '' , className : '' }); return elements . get ( id ) };
const ctx = { state : { info : null , readiness : null }, document : { getElementById : el }, setText : ( id , v )=> el ( id ). textContent = v , fmtCount : String };
vm . createContext ( ctx );
vm . runInContext ( extract ( 'renderHeader' ) + '\n' + extract ( 'validBalance' ), ctx );
return { ctx , el };
}
test ( 'missing, starting and syncing Bitcoin each show waiting and recover' ,()=>{
const { ctx , el } = fixture ();
for ( const [ state , message ] of [[ 'waiting_install' , 'Waiting for Bitcoin to be installed' ],[ 'waiting_start' , 'Waiting for Bitcoin to start' ],[ 'waiting_sync' , 'Waiting for Bitcoin to sync' ]]) {
ctx . state . readiness = { state , message }; ctx . renderHeader ();
assert . equal ( el ( 'headerStatusText' ). textContent , message );
assert . equal ( el ( 'syncCard' ). style . display , '' );
assert . match ( el ( 'syncSubtitle' ). textContent , /automatically/ );
assert . equal ( el ( 'syncPercent' ). textContent , '' );
}
ctx . state . readiness = { state : 'bitcoin_ready' };
ctx . state . info = { synced_to_chain : true , synced_to_graph : true };
ctx . renderHeader ();
assert . equal ( el ( 'headerStatusText' ). textContent , 'Running' );
assert . equal ( el ( 'syncCard' ). style . display , 'none' );
ctx . state . info = null ; ctx . state . readiness = null ; ctx . renderHeader ();
assert . equal ( el ( 'headerStatusText' ). textContent , 'Connecting to LND' );
});
test ( 'balances reject missing, malformed, fractional and negative values; real zero remains valid' ,()=>{
const { ctx } = fixture ();
for ( const v of [ null , undefined , '' ,{}, false , - 1 , '-1' , 'garbage' , 1.5 , '1.5' , Infinity , Number . MAX_SAFE_INTEGER + 1 ]) assert . equal ( ctx . validBalance ( v ), false , String ( v ));
for ( const v of [ 0 , '0' , 123 , '123' ]) assert . equal ( ctx . validBalance ( v ), true , String ( v ));
});
test ( 'failed and partial balance polls retain known balances and label them stale; recovery clears flags' , async ()=>{
const { ctx , el } = fixture ();
el ( 'refreshIcon' ). classList = { add (){}, remove (){}};
const start = html . indexOf ( ' async function refreshAll()' );
const end = html . indexOf ( '\n }' , start ) + 10 ;
vm . runInContext ( html . slice ( start , end ), ctx );
let responses = {
'/v1/getinfo' : { synced_to_chain : true },
'/v1/balance/blockchain' : { confirmed_balance : '500' , unconfirmed_balance : '0' },
'/v1/balance/channels' : { local_balance : { sat : '250' }},
};
ctx . lndSafe = async ( path , fallback )=> responses [ path ] ?? fallback ;
ctx . renderAll = ()=>{};
await ctx . refreshAll ();
assert . equal ( ctx . state . onchain . confirmed_balance , '500' );
assert . equal ( ctx . state . chanbal . local_balance . sat , '250' );
responses = {}; await ctx . refreshAll ();
assert . equal ( ctx . state . onchain . confirmed_balance , '500' );
assert . equal ( ctx . state . chanbal . local_balance . sat , '250' );
assert . equal ( ctx . state . onchainStale , true ); assert . equal ( ctx . state . chanbalStale , true );
responses = { '/v1/balance/blockchain' : { confirmed_balance : '0' }, '/v1/balance/channels' : { error : 'locked' }};
await ctx . refreshAll ();
assert . equal ( ctx . state . onchain . confirmed_balance , '0' );
assert . equal ( ctx . state . chanbal . local_balance . sat , '250' );
assert . equal ( ctx . state . onchainStale , false ); assert . equal ( ctx . state . chanbalStale , true );
responses = { '/v1/balance/blockchain' : { confirmed_balance : '600' }, '/v1/balance/channels' : { local_balance : { sat : '300' }}};
await ctx . refreshAll ();
assert . equal ( ctx . state . onchain . confirmed_balance , '600' );
assert . equal ( ctx . state . chanbal . local_balance . sat , '300' );
assert . equal ( ctx . state . onchainStale , false ); assert . equal ( ctx . state . chanbalStale , false );
});
test ( 'waiting renders promptly without querying unavailable LND endpoints, then resumes after Bitcoin sync' , async ()=>{
const { ctx , el } = fixture ();
el ( 'refreshIcon' ). classList = { add (){}, remove (){}};
const start = html . indexOf ( ' async function refreshAll()' );
vm . runInContext ( html . slice ( start , html . indexOf ( '\n }' , start ) + 10 ), ctx );
let readiness = { state : 'waiting_sync' , message : 'Waiting for Bitcoin to sync' };
const calls = []; let renders = 0 ;
ctx . lndSafe = async ( path , fallback )=>{ calls . push ( path ); return path === '/archy-status' ? readiness : fallback };
ctx . renderAll = ()=>{ renders ++ ; ctx . renderHeader ()};
await ctx . refreshAll ();
assert . deepEqual ( calls ,[ '/archy-status' ]);
assert . equal ( renders , 1 );
assert . equal ( el ( 'headerStatusText' ). textContent , 'Waiting for Bitcoin to sync' );
assert . equal ( ctx . state . onchain , undefined );
assert . equal ( ctx . state . refreshing , false );
readiness = { state : 'bitcoin_ready' , message : 'Bitcoin is ready' };
await ctx . refreshAll ();
assert . ok ( calls . includes ( '/v1/getinfo' ));
assert . ok ( calls . includes ( '/v1/balance/blockchain' ));
});
2026-09-29 14:59:08 -04:00
test ( 'cold waiting never invents zero channel capacity or an empty wallet recommendation' ,()=>{
const { ctx , el } = fixture ();
Object . assign ( ctx ,{ num : v => Number ( v ) || 0 , fmtAmount : String , fmtAmountShort : String , setBalance : ( id , v )=> el ( id ). value = v });
vm . runInContext ( extract ( 'renderBalances' ) + '\n' + extract ( 'renderSummary' ) + '\n' + extract ( 'renderChannels' ), ctx );
ctx . state . channels = [];
ctx . renderBalances (); ctx . renderSummary (); ctx . renderChannels ();
for ( const id of [ 'liqLocal' , 'liqRemote' , 'statActiveChannels' , 'healthPending' , 'chActive' ]) assert . equal ( el ( id ). textContent , '—' );
assert . equal ( el ( 'balTotal' ). value , null );
assert . match ( el ( 'liqHint' ). textContent , /waiting for LND/ );
assert . doesNotMatch ( el ( 'channelList' ). innerHTML , /No payment channels yet/ );
ctx . state . info = {}; ctx . state . chanbal = { local_balance : { sat : '0' }};
ctx . renderBalances ();
assert . equal ( el ( 'liqLocal' ). textContent , '0' );
});