src/orders/status.test.ts
v0.3.0 · 6.6 KB
import { describe, expect, it } from 'vitest';
import type { InfoCallOptions, InfoRequest, InfoRequester } from '../transport/types.js';
import { HlOrderError } from './errors.js';
import { fetchOrderStatus, parseOrderStatusAnswer, reconcileByCloid, reconcilePlacement } from './status.js';
const USER = `0x${'AB'.repeat(20)}`;
const CLOID = `0x${'CD'.repeat(16)}`;
function infoSeq(answers: Array<unknown | Error>) {
const calls: { body: InfoRequest; opts: InfoCallOptions | undefined }[] = [];
const info = (async (body: InfoRequest, opts?: InfoCallOptions) => {
calls.push({ body, opts });
const a = answers[Math.min(calls.length - 1, answers.length - 1)];
if (a instanceof Error) throw a;
return a;
}) as InfoRequester;
return { info, calls };
}
const found = (status: string, extra: object = {}) => ({
status: 'order',
order: {
order: { coin: 'HYPE', side: 'B', limitPx: '50.0', sz: '0.0', origSz: '1.0', oid: 42, timestamp: 1, cloid: CLOID.toLowerCase(), ...extra },
status,
statusTimestamp: 1757000000000,
},
});
describe('fetchOrderStatus', () => {
it('queries by oid with weight 2 and a lowercase user', async () => {
const { info, calls } = infoSeq([found('filled')]);
const r = await fetchOrderStatus(info, USER, 42);
expect(calls).toEqual([{ body: { type: 'orderStatus', user: USER.toLowerCase(), oid: 42 }, opts: { weight: 2 } }]);
expect(r).toMatchObject({
state: 'found',
status: 'filled',
category: 'filled',
oid: 42,
cloid: CLOID.toLowerCase(),
coin: 'HYPE',
side: 'B',
limitPx: '50.0',
sz: '0.0',
origSz: '1.0',
statusTimestamp: 1757000000000,
});
});
it('queries by cloid (lowercased) and by a digit-string oid', async () => {
const a = infoSeq([{ status: 'unknownOid' }]);
expect(await fetchOrderStatus(a.info, USER, CLOID)).toEqual({ state: 'unknownOid' });
expect(a.calls[0]?.body.oid).toBe(CLOID.toLowerCase());
const b = infoSeq([{ status: 'unknownOid' }]);
await fetchOrderStatus(b.info, USER, '123');
expect(b.calls[0]?.body.oid).toBe(123);
});
it('rejects malformed ids and users before any request', async () => {
const { info, calls } = infoSeq([{ status: 'unknownOid' }]);
for (const bad of [0, -1, 1.5, Number.NaN, '0x12', 'abc', '0']) {
await expect(fetchOrderStatus(info, USER, bad)).rejects.toThrow(HlOrderError);
}
await expect(fetchOrderStatus(info, '0x1234', 1)).rejects.toThrow(/user/);
expect(calls).toHaveLength(0);
});
it('categorizes processing statuses', () => {
const cat = (s: string) => {
const r = parseOrderStatusAnswer(found(s));
return r.state === 'found' ? r.category : null;
};
expect(cat('open')).toBe('live');
expect(cat('triggered')).toBe('live');
expect(cat('filled')).toBe('filled');
for (const s of ['canceled', 'scheduledCancel', 'reduceOnlyCanceled', 'siblingFilledCanceled', 'marginCanceled']) expect(cat(s)).toBe('canceled');
for (const s of ['rejected', 'badAloPxRejected', 'perpMarginRejected', 'minTradeNtlRejected']) expect(cat(s)).toBe('rejected');
expect(cat('somethingNew')).toBe('other');
});
it('a failed read is query_failed, never a throw', async () => {
const err = new Error('HL info HTTP 502');
const { info } = infoSeq([err]);
expect(await fetchOrderStatus(info, USER, 1)).toEqual({ state: 'query_failed', message: 'HL info HTTP 502', error: err });
});
it('unexpected shapes are invalid_response', () => {
expect(parseOrderStatusAnswer(null)).toMatchObject({ state: 'invalid_response' });
expect(parseOrderStatusAnswer({ status: 'what' })).toMatchObject({ state: 'invalid_response' });
expect(parseOrderStatusAnswer({ status: 'order', order: { status: 'open' } })).toMatchObject({ state: 'invalid_response' });
expect(parseOrderStatusAnswer({ status: 'order', order: { order: {} } })).toMatchObject({ state: 'invalid_response' });
expect(parseOrderStatusAnswer(found('open', { oid: 2 ** 53 }))).toMatchObject({ state: 'invalid_response' });
const noCloid = parseOrderStatusAnswer(found('open', { cloid: null, side: 'X', origSz: undefined }));
expect(noCloid).toMatchObject({ state: 'found', cloid: null, side: null, origSz: null });
});
});
describe('reconcileByCloid', () => {
it('retries unknownOid after the delay and reports found', async () => {
const { info, calls } = infoSeq([{ status: 'unknownOid' }, found('open')]);
const sleeps: number[] = [];
const r = await reconcileByCloid(info, USER, CLOID, { sleep: async (ms) => void sleeps.push(ms) });
expect(r).toMatchObject({ resolution: 'found', attempts: 2, cloid: CLOID.toLowerCase(), lookup: { category: 'live' } });
expect(sleeps).toEqual([500]);
expect(calls).toHaveLength(2);
});
it('still unknownOid after all attempts: not_found (not proof)', async () => {
const { info, calls } = infoSeq([{ status: 'unknownOid' }]);
const r = await reconcileByCloid(info, USER, CLOID, { attempts: 3, delayMs: 0, sleep: async () => {} });
expect(r).toEqual({ resolution: 'not_found', cloid: CLOID.toLowerCase(), attempts: 3 });
expect(calls).toHaveLength(3);
});
it('failed reads leave it unresolved; malformed answers stop at once', async () => {
const failing = infoSeq([new Error('timeout')]);
expect(await reconcileByCloid(failing.info, USER, CLOID, { sleep: async () => {} })).toMatchObject({
resolution: 'unresolved',
attempts: 2,
lookup: { state: 'query_failed' },
});
const weird = infoSeq([{ status: 'nope' }]);
expect(await reconcileByCloid(weird.info, USER, CLOID, { sleep: async () => {} })).toMatchObject({ resolution: 'unresolved', attempts: 1 });
expect(weird.calls).toHaveLength(1);
});
it('validates inputs', async () => {
const { info } = infoSeq([{ status: 'unknownOid' }]);
await expect(reconcileByCloid(info, USER, '0x12')).rejects.toThrow(HlOrderError);
await expect(reconcileByCloid(info, USER, CLOID, { attempts: 0 })).rejects.toThrow(/attempts/);
await expect(reconcileByCloid(info, USER, CLOID, { delayMs: -1 })).rejects.toThrow(/delayMs/);
});
it('reconcilePlacement checks every cloid in order', async () => {
const other = `0x${'ef'.repeat(16)}`;
const { info, calls } = infoSeq([found('filled'), { status: 'unknownOid' }]);
const r = await reconcilePlacement(info, USER, { reconcileCloids: [CLOID.toLowerCase() as `0x${string}`, other as `0x${string}`] }, {
attempts: 1,
});
expect(r.map((x) => x.resolution)).toEqual(['found', 'not_found']);
expect(calls.map((c) => c.body.oid)).toEqual([CLOID.toLowerCase(), other]);
});
});