@@ -27,6 +27,26 @@ function poolWith(exchange: unknown): Record<string, BrokerPoolEntry> {
2727 } ;
2828}
2929
30+ type RecordedCounter = {
31+ name : string ;
32+ value : number ;
33+ labels : Record < string , string | number > ;
34+ } ;
35+
36+ function fakeMetrics ( sink : RecordedCounter [ ] ) {
37+ return {
38+ recordCounter : async (
39+ name : string ,
40+ value : number ,
41+ labels : Record < string , string | number > ,
42+ ) => {
43+ sink . push ( { name, value, labels } ) ;
44+ } ,
45+ } as unknown as ConstructorParameters <
46+ typeof DepositArchivePoller
47+ > [ 0 ] [ "metrics" ] ;
48+ }
49+
3050function fakeArchiver ( sink : BrokerArchiveRow [ ] ) : BrokerExecutionArchiver {
3151 return {
3252 isEnabled : ( ) => true ,
@@ -392,3 +412,111 @@ describe("DepositArchivePoller.pollAllOnce", () => {
392412 }
393413 } ) ;
394414} ) ;
415+
416+ describe ( "DepositArchivePoller liveness signal" , ( ) => {
417+ test ( "records a heartbeat for a successful poll" , async ( ) => {
418+ const counters : RecordedCounter [ ] = [ ] ;
419+ const exchange = {
420+ has : { fetchDeposits : true } ,
421+ fetchDeposits : async ( ) => [ ] ,
422+ } ;
423+ const poller = new DepositArchivePoller ( {
424+ brokers : poolWith ( exchange ) ,
425+ archiver : fakeArchiver ( [ ] ) ,
426+ metrics : fakeMetrics ( counters ) ,
427+ } ) ;
428+
429+ await poller . pollAllOnce ( ) ;
430+
431+ expect ( counters ) . toEqual ( [
432+ {
433+ name : "cex_deposit_poller_polls_total" ,
434+ value : 1 ,
435+ labels : { exchange : "binance" , outcome : "ok" } ,
436+ } ,
437+ ] ) ;
438+ } ) ;
439+
440+ test ( "records a heartbeat for an account without fetchDeposits" , async ( ) => {
441+ const info = spyOn ( log , "info" ) . mockImplementation ( ( ) => { } ) ;
442+ try {
443+ const counters : RecordedCounter [ ] = [ ] ;
444+ const poller = new DepositArchivePoller ( {
445+ brokers : poolWith ( { has : { fetchDeposits : false } } ) ,
446+ archiver : fakeArchiver ( [ ] ) ,
447+ metrics : fakeMetrics ( counters ) ,
448+ } ) ;
449+
450+ await poller . pollAllOnce ( ) ;
451+
452+ expect ( counters ) . toEqual ( [
453+ {
454+ name : "cex_deposit_poller_polls_total" ,
455+ value : 1 ,
456+ labels : { exchange : "binance" , outcome : "unsupported" } ,
457+ } ,
458+ ] ) ;
459+ } finally {
460+ info . mockRestore ( ) ;
461+ }
462+ } ) ;
463+
464+ test ( "counts a hung fetchDeposits as a failed poll and polls again" , async ( ) => {
465+ const warn = spyOn ( log , "warn" ) . mockImplementation ( ( ) => { } ) ;
466+ try {
467+ let calls = 0 ;
468+ const exchange = {
469+ has : { fetchDeposits : true } ,
470+ fetchDeposits : async ( ) => {
471+ calls += 1 ;
472+ if ( calls === 1 ) {
473+ // Never settles: the silent-death mode the timeout exists for.
474+ return new Promise < unknown [ ] > ( ( ) => { } ) ;
475+ }
476+ return [
477+ {
478+ txid : "0xafter-hang" ,
479+ currency : "USDC" ,
480+ amount : "5" ,
481+ status : "ok" ,
482+ timestamp : Date . now ( ) + 10_000 ,
483+ } ,
484+ ] ;
485+ } ,
486+ } ;
487+ const counters : RecordedCounter [ ] = [ ] ;
488+ const sink : BrokerArchiveRow [ ] = [ ] ;
489+ const poller = new DepositArchivePoller ( {
490+ brokers : poolWith ( exchange ) ,
491+ archiver : fakeArchiver ( sink ) ,
492+ metrics : fakeMetrics ( counters ) ,
493+ config : { fetchTimeoutMs : 10 } ,
494+ } ) ;
495+
496+ expect ( await poller . pollAllOnce ( ) ) . toBe ( true ) ;
497+
498+ expect ( warn . mock . calls [ 0 ] ?. [ 0 ] ) . toBe ( "Deposit archive poll failed" ) ;
499+ expect (
500+ String ( ( warn . mock . calls [ 0 ] ?. [ 1 ] as { error : unknown } ) . error ) ,
501+ ) . toContain ( "fetchDeposits timed out after 10ms" ) ;
502+ expect ( counters ) . toEqual ( [
503+ {
504+ name : "cex_deposit_poller_errors_total" ,
505+ value : 1 ,
506+ labels : { exchange : "binance" } ,
507+ } ,
508+ {
509+ name : "cex_deposit_poller_polls_total" ,
510+ value : 1 ,
511+ labels : { exchange : "binance" , outcome : "error" } ,
512+ } ,
513+ ] ) ;
514+
515+ expect ( await poller . pollAllOnce ( ) ) . toBe ( true ) ;
516+ expect ( sink ) . toHaveLength ( 1 ) ;
517+ expect ( sink [ 0 ] ?. row ) . toMatchObject ( { external_id : "0xafter-hang" } ) ;
518+ } finally {
519+ warn . mockRestore ( ) ;
520+ }
521+ } ) ;
522+ } ) ;
0 commit comments