src/transport/interpret.test.ts
v0.2.0 · 5.3 KB
import { describe, expect, it } from 'vitest';
import { NadoHttpError, NadoNetworkError, NadoRejection, NadoResponseParseError, NadoTimeoutError } from './errors.js';
import { hasUsableDigest, interpretExecuteError, NADO_ERROR_CODES } from './interpret.js';
const rej = (code: number, error = 'text') => new NadoRejection({ code, error, path: 'execute' });
const URL_ = 'https://gateway.example/v1/execute';
describe('NADO_ERROR_CODES', () => {
it('lists the knowledge-base codes', () => {
expect(NADO_ERROR_CODES).toEqual({
ORDER_NOT_FOUND: 2020,
REDUCE_ONLY_OVERSIZED: 2064,
RESTING_REDUCE_ONLY: 2067,
MARKET_NOT_TRADABLE: 2069,
MARKET_POST_ONLY: 2117,
});
});
});
describe('interpretExecuteError: failure envelopes are final', () => {
it('2117 on a resting DEFAULT -> resend as POST_ONLY; on an IOC -> do not retry (never rewrite a taker)', () => {
const resting = interpretExecuteError(rej(2117, 'Market is in post-only mode'), {
action: 'place',
orderType: 'default',
});
expect(resting).toMatchObject({
kind: 'postOnlyMarket',
code: 2117,
outcome: 'not-applied',
advice: 'resend-as-post-only',
rejection: true,
});
expect(resting.message).toBe('Market is in post-only mode');
expect(interpretExecuteError(rej(2117), { orderType: 'ioc' }).advice).toBe('do-not-retry');
expect(interpretExecuteError(rej(2117), { orderType: 'fok' }).advice).toBe('do-not-retry');
expect(interpretExecuteError(rej(2117), { orderType: 'post_only' }).advice).toBe('do-not-retry');
expect(interpretExecuteError(rej(2117)).advice).toBe('do-not-retry'); // order type unknown: stay conservative
});
it('2069 -> market blocked, do not retry (next tick)', () => {
expect(interpretExecuteError(rej(2069))).toMatchObject({
kind: 'marketBlocked',
advice: 'do-not-retry',
outcome: 'not-applied',
});
});
it('2067 -> resting reduce-only, do not retry', () => {
expect(interpretExecuteError(rej(2067))).toMatchObject({ kind: 'restingReduceOnly', advice: 'do-not-retry' });
});
it('2064 on a full close -> retry once with the exact position size; otherwise do not retry', () => {
expect(interpretExecuteError(rej(2064), { fullClose: true })).toMatchObject({
kind: 'reduceOnlyOversized',
advice: 'retry-exact-position-size',
});
expect(interpretExecuteError(rej(2064), { fullClose: false })).toMatchObject({
kind: 'reduceOnlyOversized',
advice: 'do-not-retry',
});
});
it('2020 on a cancel -> order gone (cancelled or filled): reconcile, never confirm the cancel', () => {
expect(interpretExecuteError(rej(2020, 'OrderNotFound'), { action: 'cancel', idempotent: true })).toMatchObject({
kind: 'orderNotFound',
advice: 'reconcile',
outcome: 'not-applied',
});
});
it('unknown codes -> rejected, do not retry', () => {
expect(interpretExecuteError(rej(9999, 'new thing'))).toMatchObject({
kind: 'rejected',
code: 9999,
advice: 'do-not-retry',
rejection: true,
});
});
});
describe('interpretExecuteError: transport', () => {
it('429 -> retry for any action', () => {
const f = interpretExecuteError(new NadoHttpError({ status: 429, url: URL_ }));
expect(f).toMatchObject({ kind: 'rateLimited', outcome: 'not-applied', advice: 'retry', rejection: false });
});
it('5xx / timeout / network / parse -> unknown outcome; retry only when idempotent', () => {
for (const err of [
new NadoHttpError({ status: 502, url: URL_ }),
new NadoTimeoutError({ timeoutMs: 10_000, url: URL_ }),
new NadoNetworkError({ url: URL_, cause: 'x' }),
new NadoResponseParseError({ status: 200, bodyText: 'html', url: URL_, cause: 1 }),
]) {
expect(interpretExecuteError(err)).toMatchObject({
kind: 'transportUnknown',
outcome: 'unknown',
advice: 'reconcile',
});
expect(interpretExecuteError(err, { idempotent: true })).toMatchObject({
kind: 'transportUnknown',
outcome: 'unknown',
advice: 'retry',
});
}
});
it('other 4xx, validation errors -> not applied; aborts and unknown errors -> reconcile', () => {
expect(interpretExecuteError(new NadoHttpError({ status: 403, url: URL_ }))).toMatchObject({
kind: 'notApplied',
advice: 'do-not-retry',
});
expect(interpretExecuteError(new RangeError('weight 400 exceeds'))).toMatchObject({
kind: 'notApplied',
advice: 'do-not-retry',
});
expect(interpretExecuteError(new TypeError('bad body'))).toMatchObject({ kind: 'notApplied' });
expect(interpretExecuteError({ name: 'AbortError', message: 'aborted' })).toMatchObject({
kind: 'transportUnknown',
advice: 'reconcile',
});
expect(interpretExecuteError(new Error('something odd'))).toMatchObject({
kind: 'transportUnknown',
outcome: 'unknown',
advice: 'reconcile',
});
expect(interpretExecuteError('string error').message).toBe('string error');
});
});
describe('hasUsableDigest', () => {
it('requires 0x + 64 hex', () => {
expect(hasUsableDigest({ digest: `0x${'ab'.repeat(32)}` })).toBe(true);
expect(hasUsableDigest({ digest: '0xab' })).toBe(false);
expect(hasUsableDigest({})).toBe(false);
expect(hasUsableDigest(null)).toBe(false);
});
});