@@ -1440,7 +1440,12 @@ test.serial(
14401440 } )
14411441 } )
14421442
1443- // Mimics Tiingo's wsSelectUrl with a 1:1 primary:secondary ratio
1443+ // This inline URL function is a minimal stand-in for any adapter that uses
1444+ // the counter to alternate between a primary and secondary URL (e.g. Tiingo's
1445+ // wsSelectUrl with a 1:1 ratio). The framework test cannot import wsSelectUrl
1446+ // directly since ea-framework-js has no dependency on external-adapters-js.
1447+ // The production scenario using the actual wsSelectUrl is covered in:
1448+ // packages/sources/tiingo/test/integration/adapter-ws-reconnect.test.ts
14441449 const transport = new WebSocketTransport < WebSocketTypes > ( {
14451450 url : ( _context , _desiredSubs , params ) => {
14461451 const counter = params . streamHandlerInvocationsWithNoConnection
@@ -1536,3 +1541,179 @@ test.serial(
15361541 await t . context . clock . runToLastAsync ( )
15371542 } ,
15381543)
1544+
1545+ test . serial (
1546+ 'does not increment failover counter on normal closure (code 1000)' ,
1547+ async ( t ) => {
1548+ const base = 'ETH'
1549+ const quote = 'DOGE'
1550+
1551+ const counterValues : number [ ] = [ ]
1552+
1553+ mockWebSocketProvider ( WebSocketClassProvider )
1554+ const mockWsServer = new Server ( ENDPOINT_URL , { mock : false } )
1555+ mockWsServer . on ( 'connection' , ( socket ) => {
1556+ socket . on ( 'message' , ( ) => {
1557+ // Normal closure -- framework should NOT increment the failover counter
1558+ socket . close ( )
1559+ } )
1560+ } )
1561+
1562+ const transport = new WebSocketTransport < WebSocketTypes > ( {
1563+ url : ( _context , _desiredSubs , params ) => {
1564+ counterValues . push ( params . streamHandlerInvocationsWithNoConnection )
1565+ return ENDPOINT_URL
1566+ } ,
1567+ handlers : {
1568+ message ( message ) {
1569+ if ( ! message . pair ) {
1570+ return [ ]
1571+ }
1572+ const [ curBase , curQuote ] = message . pair . split ( '/' )
1573+ return [
1574+ {
1575+ params : { base : curBase , quote : curQuote } ,
1576+ response : {
1577+ data : { result : message . value } ,
1578+ result : message . value ,
1579+ } ,
1580+ } ,
1581+ ]
1582+ } ,
1583+ } ,
1584+ builders : {
1585+ subscribeMessage : ( params ) => ( {
1586+ request : 'subscribe' ,
1587+ pair : `${ params . base } /${ params . quote } ` ,
1588+ } ) ,
1589+ unsubscribeMessage : ( params ) => ( {
1590+ request : 'unsubscribe' ,
1591+ pair : `${ params . base } /${ params . quote } ` ,
1592+ } ) ,
1593+ } ,
1594+ } )
1595+
1596+ const webSocketEndpoint = new AdapterEndpoint ( {
1597+ name : 'TEST' ,
1598+ transport : transport ,
1599+ inputParameters,
1600+ } )
1601+
1602+ const config = new AdapterConfig (
1603+ { } ,
1604+ {
1605+ envDefaultOverrides : {
1606+ BACKGROUND_EXECUTE_MS_WS ,
1607+ WS_SUBSCRIPTION_UNRESPONSIVE_TTL : 180_000 ,
1608+ } ,
1609+ } ,
1610+ )
1611+
1612+ const adapter = new Adapter ( {
1613+ name : 'TEST' ,
1614+ defaultEndpoint : 'test' ,
1615+ config,
1616+ endpoints : [ webSocketEndpoint ] ,
1617+ } )
1618+
1619+ const testAdapter = await TestAdapter . startWithMockedCache ( adapter , t . context )
1620+
1621+ await testAdapter . request ( { base, quote } )
1622+
1623+ // Run through several background execute cycles where the server closes normally each time
1624+ await runAllUntilTime ( t . context . clock , BACKGROUND_EXECUTE_MS_WS * 5 + 100 )
1625+
1626+ t . true ( counterValues . length >= 3 , `Expected at least 3 url calls, got ${ counterValues . length } ` )
1627+
1628+ // Counter should remain 0 throughout -- normal closes must not increment it
1629+ for ( const value of counterValues ) {
1630+ t . is ( value , 0 , `Counter should stay at 0 after normal close, got ${ value } ` )
1631+ }
1632+
1633+ await testAdapter . api . close ( )
1634+ mockWsServer . close ( )
1635+ await t . context . clock . runToLastAsync ( )
1636+ } ,
1637+ )
1638+
1639+ test . serial (
1640+ 'sets ws_connection_failover_count metric on abnormal closure' ,
1641+ async ( t ) => {
1642+ const base = 'ETH'
1643+ const quote = 'DOGE'
1644+ process . env [ 'METRICS_ENABLED' ] = 'true'
1645+ eaMetrics . clear ( )
1646+
1647+ mockWebSocketProvider ( WebSocketClassProvider )
1648+ const mockWsServer = new Server ( ENDPOINT_URL , { mock : false } )
1649+ mockWsServer . on ( 'connection' , ( socket ) => {
1650+ socket . on ( 'message' , ( ) => {
1651+ socket . close ( { code : 4000 , reason : 'Abnormal' , wasClean : false } )
1652+ } )
1653+ } )
1654+
1655+ const adapter = createAdapter ( {
1656+ WS_SUBSCRIPTION_UNRESPONSIVE_TTL : 180_000 ,
1657+ } )
1658+
1659+ const testAdapter = await TestAdapter . startWithMockedCache ( adapter , t . context )
1660+
1661+ await testAdapter . request ( { base, quote } )
1662+
1663+ // One background execute cycle: connect -> subscribe -> close 4000 -> counter=1
1664+ await runAllUntilTime ( t . context . clock , BACKGROUND_EXECUTE_MS_WS + 100 )
1665+
1666+ const metrics = await testAdapter . getMetrics ( )
1667+ metrics . assert ( t , {
1668+ name : 'ws_connection_failover_count' ,
1669+ labels : { transport_name : 'default_single_transport' , url : 'wss://test-ws.com/asd' } ,
1670+ expectedValue : 1 ,
1671+ } )
1672+
1673+ process . env [ 'METRICS_ENABLED' ] = 'false'
1674+ await testAdapter . api . close ( )
1675+ mockWsServer . close ( )
1676+ await t . context . clock . runToLastAsync ( )
1677+ } ,
1678+ )
1679+
1680+ test . serial (
1681+ 'does not set ws_connection_failover_count metric on normal closure (code 1000)' ,
1682+ async ( t ) => {
1683+ const base = 'ETH'
1684+ const quote = 'DOGE'
1685+ process . env [ 'METRICS_ENABLED' ] = 'true'
1686+ eaMetrics . clear ( )
1687+
1688+ mockWebSocketProvider ( WebSocketClassProvider )
1689+ const mockWsServer = new Server ( ENDPOINT_URL , { mock : false } )
1690+ mockWsServer . on ( 'connection' , ( socket ) => {
1691+ socket . on ( 'message' , ( ) => {
1692+ // Normal closure -- metric must not be emitted
1693+ socket . close ( )
1694+ } )
1695+ } )
1696+
1697+ const adapter = createAdapter ( {
1698+ WS_SUBSCRIPTION_UNRESPONSIVE_TTL : 180_000 ,
1699+ } )
1700+
1701+ const testAdapter = await TestAdapter . startWithMockedCache ( adapter , t . context )
1702+
1703+ await testAdapter . request ( { base, quote } )
1704+
1705+ await runAllUntilTime ( t . context . clock , BACKGROUND_EXECUTE_MS_WS + 100 )
1706+
1707+ const metrics = await testAdapter . getMetrics ( )
1708+ // The failover count metric must not appear in the Prometheus output at all
1709+ const metricPresent = [ ...metrics . map . keys ( ) ] . some ( ( k ) =>
1710+ k . startsWith ( 'ws_connection_failover_count' ) ,
1711+ )
1712+ t . false ( metricPresent , 'ws_connection_failover_count should not be emitted after a normal close' )
1713+
1714+ process . env [ 'METRICS_ENABLED' ] = 'false'
1715+ await testAdapter . api . close ( )
1716+ mockWsServer . close ( )
1717+ await t . context . clock . runToLastAsync ( )
1718+ } ,
1719+ )
0 commit comments