import test from 'node:test';
import assert from 'node:assert/strict';
import { normalizeSymbol, normalizeAddress, fetchLeaderboardCohort, buildSnapshot, biasLabel, quality, coinLite, coinFull, scan, walletProfile, flowsFor, resolveCoin, aggregate, maskAddress, liqDistancePct, Incomplete, InputError, DISCLAIMER, LIMITS } from '../src/engine.js';
import { mockHL, A } from './mock.js';
const NFA = /Not financial advice\. Past performance does not guarantee future results\./;
const snap = async (fail = [], opts = {}) => (await buildSnapshot({ fetchImpl: mockHL({ fail: new Set(fail) }).fetchImpl, now: Date.now(), ...opts })).snapshot;
test('input normalization', () => {
for (const [i, o] of [['btc', 'BTC'], ['BTC-PERP', 'BTC'], ['eth-usd', 'ETH'], ['SOLUSDT', 'SOL'], [' hype ', 'HYPE'], ['kPEPE', 'kPEPE'], ['kpepe', 'KPEPE']]) assert.equal(normalizeSymbol(i), o);
for (const b of ['', '-', 'A B', 'x'.repeat(16), '../etc', 5]) assert.throws(() => normalizeSymbol(b));
assert.equal(normalizeAddress(` ${A(1).toUpperCase().replace('0X', '0x')} `), A(1));
for (const b of ['0x12', 'abc', A(1) + '0', 7]) assert.throws(() => normalizeAddress(b), InputError);
assert.match(DISCLAIMER, NFA);
});
test('leaderboard: streamed pretty-printed JSON in odd chunks; cohort rules exclude small, losing, all-time-losing and MM-like accounts', async () => {
const r = await fetchLeaderboardCohort(mockHL().fetchImpl);
assert.equal(r.leaderboard_rows, 114);
assert.deepEqual(r.cohort.map((c) => c.a), Array.from({ length: 10 }, (_, i) => A(i + 1)));
assert.equal(r.cohort[0].rank, 1); assert.equal(r.cohort[0].n, 'top dog'); assert.ok(r.cohort[0].mp > r.cohort[1].mp);
});
test('snapshot: aggregates per coin, bias labels, host fallback when the primary info host fails', async () => {
const s = await snap(['primary']);
assert.equal(s.covered, 10); assert.deepEqual(s.failed, []);
assert.equal(s.coins.BTC.tl, 1); assert.equal(s.coins.BTC.ts, 4); assert.equal(s.coins.BTC.sn, 104 * 80000);
assert.equal(biasLabel(s.coins.BTC), 'strong_short'); assert.equal(biasLabel(s.coins.ETH), 'strong_long'); assert.equal(biasLabel(s.coins.SOL), 'insufficient_data'); assert.equal(biasLabel(undefined), 'no_positions');
assert.equal(biasLabel({ tl: 3, ts: 3, ln: 50, sn: 50 }), 'mixed'); assert.equal(biasLabel({ tl: 3, ts: 2, ln: 60, sn: 40 }), 'lean_long');
});
test('FAIL-CLOSED: low coverage, stale snapshots and missing data are never a confident read', async () => {
// 50% of accounts unreadable -> refused (Incomplete = 503, never charged)
const half = await snap(['state:half']);
assert.equal(half.covered, 5); assert.ok(half.failed[0].startsWith('hyperliquid_positions:5_of_10'));
assert.throws(() => quality(half), Incomplete);
assert.throws(() => coinLite('BTC', half, []), Incomplete);
// 70% coverage -> answered, but bias "unknown", confidence low, sources_failed listed
const some = await snap(['state:some']);
assert.equal(some.covered, 7);
const r = coinLite('BTC', some, []);
assert.equal(r.bias, 'unknown'); assert.equal(r.confidence, 'low'); assert.ok(r.sources_failed.some((x) => x.startsWith('hyperliquid_positions')));
assert.equal(scan(some, []).most_net_short[0].bias, 'unknown');
// stale
const s = await snap();
assert.throws(() => coinLite('BTC', s, [], s.at + 31 * 60_000), Incomplete);
const st = coinLite('BTC', s, [], s.at + 21 * 60_000); assert.equal(st.confidence, 'low'); assert.ok(st.sources_failed.includes('snapshot_stale'));
assert.throws(() => quality(null), Incomplete);
// marks failed -> not high confidence, listed
const nm = await snap(['meta']); const m = coinLite('BTC', nm, []);
assert.notEqual(m.confidence, 'high'); assert.ok(m.sources_failed.includes('hyperliquid_meta')); assert.equal(m.mark_price, null);
// every account unreadable -> Incomplete; leaderboard down with no cohort -> Incomplete
await assert.rejects(snap(['state']), Incomplete);
await assert.rejects(snap(['leaderboard']), Incomplete);
// leaderboard down with a recent cohort -> last-good cohort used and listed
const good = await buildSnapshot({ fetchImpl: mockHL().fetchImpl, now: Date.now() });
const again = await buildSnapshot({ fetchImpl: mockHL({ fail: new Set(['leaderboard']) }).fetchImpl, now: Date.now() + 2 * 3600_000, cohortRecord: good.cohortRecord });
assert.ok(again.snapshot.failed.includes('hyperliquid_leaderboard')); assert.equal(again.snapshot.covered, 10);
});
test('coin full: flows from history, entries, liquidation clusters, top positions with cohort rank; k-markets and unknown coins', async () => {
const { snapshot: s, cohortRecord } = await buildSnapshot({ fetchImpl: mockHL().fetchImpl, now: Date.now() });
const hist = [{ at: s.at - 3600_000, coins: { BTC: { net: -1000, ln: 0, sn: 1000, tl: 0, ts: 1 } } }, { at: s.at - 24 * 3600_000, coins: {} }];
const f = flowsFor('BTC', s, hist);
assert.equal(f['1h'].net_notional_change_usd, s.coins.BTC.net + 1000); assert.equal(f['4h'], null); assert.equal(f['24h'].net_notional_change_usd, s.coins.BTC.net);
const r = coinFull('BTC', s, hist, cohortRecord.cohort);
assert.equal(r.bias, 'strong_short'); assert.equal(r.top_positions[0].address, '0x0000…0001'); assert.equal(r.top_positions[0].cohort_rank_30d_pnl, 1);
assert.equal(r.top_positions[0].liquidation_price, undefined); assert.equal(r.top_positions[0].liquidation_distance_pct, undefined);
assert.ok(r.concentration_note); assert.equal(r.liquidation_clusters.longs[0].positions, 1); // A5 long liq 78500 vs mark 80000 = 1.9%
assert.equal(r.liquidation_clusters.longs[0].notional_usd, null, 'single-position band: notional withheld');
assert.equal(r.liquidation_clusters.no_liquidation_price_usd, 80000);
assert.ok(r.avg_entry_short > 79000 && r.avg_entry_short < 80000); assert.equal(r.flows['24h'].traders_short_change, 4);
assert.equal(resolveCoin('PEPE', s), 'kPEPE'); assert.equal(coinLite('PEPE', s, []).bias, 'no_positions');
assert.throws(() => coinLite('ZZZQ', s, []), (e) => e instanceof InputError && e.status === 404);
});
test('scan: book, net long/short, one-sided, 24h flows only with history', async () => {
const s = await snap();
const r = scan(s, [], { limit: 5, minTraders: 3 });
assert.equal(r.most_net_short[0].symbol, 'BTC'); assert.equal(r.most_net_long[0].symbol, 'ETH'); assert.ok(!r.most_net_long.some((x) => x.symbol === 'SOL'), 'SOL has 1 trader');
assert.equal(r.cohort_book.accounts_with_positions, 8); assert.deepEqual(r.biggest_24h_buying, []); assert.ok(r.flows_note);
const r2 = scan(s, [{ at: s.at - 24 * 3600_000, coins: {} }], { limit: 5 });
assert.equal(r2.biggest_24h_buying[0].symbol, 'ETH'); assert.equal(r2.biggest_24h_selling[0].symbol, 'BTC');
});
test('wallet profile: live state + fills; FAIL-CLOSED fills failure listed (never estimated); state failure = Incomplete', async () => {
const cohort = (await fetchLeaderboardCohort(mockHL().fetchImpl)).cohort;
const w = await walletProfile(A(2), { fetchImpl: mockHL().fetchImpl, cohort });
assert.equal(w.open_positions, 2); assert.equal(w.in_smart_money_cohort, true); assert.equal(w.cohort_entry.rank_30d_pnl, 2);
assert.equal(w.last_30d.realized_pnl_usd, 800); assert.equal(w.last_30d.winning_closes_pct, 50); assert.equal(w.confidence, 'high');
const nf = await walletProfile(A(2), { fetchImpl: mockHL({ fail: new Set(['fills']) }).fetchImpl, cohort });
assert.equal(nf.last_30d, null); assert.deepEqual(nf.sources_failed, ['hyperliquid_fills']); assert.equal(nf.confidence, 'medium');
await assert.rejects(walletProfile(A(2), { fetchImpl: mockHL({ fail: new Set(['state']) }).fetchImpl, cohort }), Incomplete);
const out = await walletProfile(A(55), { fetchImpl: mockHL().fetchImpl, cohort });
assert.equal(out.in_smart_money_cohort, false); assert.equal(out.open_positions, 0);
});
test('makeStore takes a stub factory and makes a fresh stub per read (DO stubs are request-bound)', async () => {
const { makeStore, memoryStub } = await import('../src/store.js');
const base = memoryStub(); let made = 0;
let t = 0; const store = makeStore(() => { made++; return base; }, { now: () => t });
await store.read(); t += 30_000; await store.read();
assert.equal(made, 2);
});
test('privacy: address masking helper and liquidation distance', () => {
assert.equal(maskAddress('0x5b5d51203a0f9079f8aeb098a6523a13f298c060'), '0x5b5d…c060');
assert.equal(maskAddress(A(12)), '0x0000…0012');
for (const b of [null, undefined, '0x12', 'vitalik.eth', 7]) assert.equal(maskAddress(b), null);
assert.equal(liqDistancePct(95000, 80000), 19); assert.equal(liqDistancePct(78500, 80000), 2); assert.equal(liqDistancePct(null, 80000), null); assert.equal(liqDistancePct(1, null), null);
assert.ok(LIMITS.some((l) => /never return full account addresses/.test(l) && /harass/.test(l)));
});
test('privacy: coin tier never carries a full address or a per-account liquidation price; bands = price range + count + notional', async () => {
const { snapshot: s, cohortRecord } = await buildSnapshot({ fetchImpl: mockHL().fetchImpl, now: Date.now() });
for (const coin of ['BTC', 'ETH', 'SOL']) {
const r = coinFull(coin, s, [], cohortRecord.cohort);
const txt = JSON.stringify(r);
assert.ok(!/0x[0-9a-fA-F]{40}/.test(txt), `${coin}: no full address`);
assert.ok(!/liquidation_price"/.test(txt), `${coin}: no liquidation_price field`);
for (const p of r.top_positions) assert.match(p.address, /^0x[0-9a-f]{4}…[0-9a-f]{4}$/);
for (const b of [...r.liquidation_clusters.longs, ...r.liquidation_clusters.shorts]) {
assert.deepEqual(Object.keys(b).filter((k) => k !== 'notional_withheld'), ['within_pct', 'price_from', 'price_to', 'notional_usd', 'positions']);
if (b.positions > 0 && b.positions < 3) assert.equal(b.notional_usd, null);
}
}
const b = coinFull('BTC', s, [], cohortRecord.cohort).liquidation_clusters;
assert.deepEqual([b.longs[0].price_from, b.longs[0].price_to], [78400, 80000]); // longs liquidate below mark
assert.deepEqual([b.shorts[3].price_from, b.shorts[3].price_to], [88000, 100000]); // shorts above
assert.deepEqual([b.shorts[4].price_from, b.shorts[4].price_to], [100000, null]);
for (const x of [s, scan(s, [], {}), coinLite('BTC', s, [])]) assert.ok(!/0x[0-9a-fA-F]{40}/.test(JSON.stringify(x === s ? scan(s, [], {}) : x)), 'lite/scan: no address');
});
test('privacy: a band with 3+ positions shows its notional; fewer is withheld', () => {
const positions = [0, 1, 2].map((i) => ({ i, c: 'BTC', s: 1, sz: 1, ntl: 1000 * (i + 1), e: 80000, lev: 10, lt: 'cross', liq: 79000, u: 0 }));
positions.push({ i: 3, c: 'BTC', s: -1, sz: 1, ntl: 5000, e: 80000, lev: 10, lt: 'cross', liq: 81000, u: 0 });
const snap = { at: Date.now(), cohort_at: Date.now(), cohort_size: 4, covered: 4, failed: [], marks_ok: true, listed: ['BTC'], coins: aggregate(positions, { BTC: 80000 }), positions };
const cohort = [0, 1, 2, 3].map((i) => ({ a: A(500 + i), rank: i + 1, mp: 1 }));
const r = coinFull('BTC', snap, [], cohort);
assert.deepEqual([r.liquidation_clusters.longs[0].positions, r.liquidation_clusters.longs[0].notional_usd], [3, 6000]);
assert.deepEqual([r.liquidation_clusters.shorts[0].positions, r.liquidation_clusters.shorts[0].notional_usd, r.liquidation_clusters.shorts[0].notional_withheld], [1, null, 'fewer than 3 positions']);
assert.ok(!JSON.stringify(r).includes(A(500)));
});
test('privacy: wallet shows distance to liquidation in whole percent, never the price, plus the public-data note', async () => {
const r = await walletProfile(A(2), { fetchImpl: mockHL().fetchImpl, now: Date.now() });
assert.deepEqual(r.positions.map((p) => [p.symbol, p.liquidation_distance_pct]), [['ETH', 23], ['BTC', 50]]);
assert.ok(r.positions.every((p) => !('liquidation_price' in p)));
assert.match(r.data_note, /public on-chain/i); assert.match(r.data_note, /harass/);
});
agentgate-source: code mirror on Cloudflare Pages. All repos