char-slop/city-role-bot
discord auto role applicator bot for charlotte polycule city
git clone https://git.t4t.associates/char-slop/city-role-bot
a4093f0
main
1import assert from 'node:assert/strict' 2import { test } from 'node:test' 3import { Client , DiscordRESTError } from 'oceanic.js' 4import { retryDiscord } from '../src/retry.ts' 5 6function resetError () :TypeError { 7return new TypeError ( 'fetch failed' , { cause :Object . assign ( new Error ( 'read ECONNRESET' ), { code :'ECONNRESET' }) }) 8} 9 10test ( 'Oceanic rejects a connection reset and a retry on the same route succeeds' , { timeout :5000 }, async ( t ) => { 11const client = new Client ({ rest :{ baseURL :'https://discord.invalid/api/v10' } }) 12const errors :unknown [] = [] 13client . on ( 'error' , ( error ) => errors . push ( error )) 14t . mock . method ( console , 'warn' , () => {}) 15let requests = 0 16const error = resetError () 17t . mock . method ( globalThis , 'fetch' , async () => { 18if ( ++ requests === 1 ) throw error 19return new Response ( JSON . stringify ({ ok :true }), { headers :{ 'content-type' :'application/json' } }) 20}) 21const result = await retryDiscord (() => client . rest . request ({ method :'GET' , path :'/test' }), 'test' ) 22assert . deepEqual ( result , { ok :true }) 23assert . equal ( requests , 2 ) 24assert . deepEqual ( errors , [ error ]) 25}) 26 27test ( 'Oceanic rejects timed-out requests rather than leaving them pending' , { timeout :5000 }, async ( t ) => { 28const client = new Client ({ rest :{ baseURL :'https://discord.invalid/api/v10' , requestTimeout :10 } }) 29client . on ( 'error' , () => {}) 30t . mock . method ( globalThis , 'fetch' , ( _url :string , options :RequestInit ) => new Promise (( _resolve , reject ) => { 31options . signal ! . addEventListener ( 'abort' , () => reject ( options . signal ! . reason ), { once :true }) 32})) 33await assert . rejects ( client . rest . request ({ method :'GET' , path :'/test' }), { name :'AbortError' }) 34}) 35 36test ( 'transport retries have bounded exponential backoff and preserve the terminal error' , async ( t ) => { 37t . mock . timers . enable ({ apis :[ 'setTimeout' ] }) 38t . mock . method ( console , 'warn' , () => {}) 39const error = resetError () 40let attempts = 0 41const result = assert . rejects ( retryDiscord ( async () => { 42attempts ++ 43throw error 44}, 'test' ), ( actual ) => actual === error ) 45await new Promise < void >(( resolve ) => setImmediate ( resolve )) 46assert . equal ( attempts , 1 ) 47for ( const wait of [ 1000 , 2000 , 4000 , 8000 ]) { 48const before :number = attempts 49t . mock . timers . tick ( wait - 1 ) 50await new Promise < void >(( resolve ) => setImmediate ( resolve )) 51assert . equal ( attempts , before ) 52t . mock . timers . tick ( 1 ) 53await new Promise < void >(( resolve ) => setImmediate ( resolve )) 54assert . equal ( attempts , before + 1 ) 55} 56await result 57assert . equal ( attempts , 5 ) 58}) 59 60test ( 'permission errors and programming errors are not retried' , async () => { 61const response = new Response ( '' , { status :403 }) 62Object . defineProperty ( response , 'url' , { value :'https://discord.invalid/api/v10/test' }) 63for ( const error of [ new DiscordRESTError ( response , { code :50013 , message :'Missing Permissions' }, 'GET' ), new TypeError ( 'bad input' )]) { 64let attempts = 0 65await assert . rejects ( retryDiscord ( async () => { 66attempts ++ 67throw error 68}, 'test' ), ( actual ) => actual === error ) 69assert . equal ( attempts , 1 ) 70} 71}) 72 73test ( 'timeouts and server errors are retried' , async ( t ) => { 74t . mock . timers . enable ({ apis :[ 'setTimeout' ] }) 75t . mock . method ( console , 'warn' , () => {}) 76const response = new Response ( '' , { status :503 }) 77Object . defineProperty ( response , 'url' , { value :'https://discord.invalid/api/v10/test' }) 78for ( const error of [ new DOMException ( 'aborted' , 'AbortError' ), new DiscordRESTError ( response , { code :0 , message :'Unavailable' }, 'GET' )]) { 79let attempts = 0 80const result = retryDiscord ( async () => { 81if ( ++ attempts === 1 ) throw error 82return 'recovered' 83}, 'test' ) 84await new Promise < void >(( resolve ) => setImmediate ( resolve )) 85t . mock . timers . tick ( 1000 ) 86assert . equal ( await result , 'recovered' ) 87assert . equal ( attempts , 2 ) 88} 89})