201 lines
7.3 KiB
TypeScript
201 lines
7.3 KiB
TypeScript
/**
|
|
* Morphit smoke — explorer activity helpers (Batch K).
|
|
*
|
|
* Covers:
|
|
* - aggregateListingHistogram + totalListings.
|
|
* - decorateOp branches.
|
|
*/
|
|
|
|
import {
|
|
aggregateListingHistogram,
|
|
totalListings
|
|
} from '../../web/src/lib/explorer/listingsHistogram';
|
|
import { decorateOp } from '../../web/src/lib/explorer/decorate';
|
|
|
|
let scenarios = 0;
|
|
let failures = 0;
|
|
function scenario(name: string, fn: () => void): void {
|
|
scenarios++;
|
|
try {
|
|
fn();
|
|
console.log(` ✓ ${name}`);
|
|
} catch (err) {
|
|
failures++;
|
|
console.log(` ✗ ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
}
|
|
}
|
|
|
|
console.log('\n── explorer activity smoke ───────────────────────────────\n');
|
|
|
|
// ─── aggregateListingHistogram ──────────────────────────────────────
|
|
|
|
scenario('empty input → empty output', () => {
|
|
const r = aggregateListingHistogram([]);
|
|
if (r.length !== 0) throw new Error(`got ${r.length}`);
|
|
});
|
|
|
|
scenario('single buy → one row, buy=1', () => {
|
|
const r = aggregateListingHistogram([{ side: 'buy', asset: 'BTC' }]);
|
|
if (r.length !== 1) throw new Error(`got ${r.length}`);
|
|
if (r[0]!.asset !== 'BTC') throw new Error('asset');
|
|
if (r[0]!.buy_count !== 1) throw new Error('buy_count');
|
|
if (r[0]!.sell_count !== 0) throw new Error('sell_count');
|
|
});
|
|
|
|
scenario('mixed buys and sells aggregate per asset', () => {
|
|
const r = aggregateListingHistogram([
|
|
{ side: 'buy', asset: 'BTC' },
|
|
{ side: 'buy', asset: 'BTC' },
|
|
{ side: 'sell', asset: 'BTC' },
|
|
{ side: 'buy', asset: 'XMR' }
|
|
]);
|
|
if (r.length !== 2) throw new Error(`got ${r.length}`);
|
|
const btc = r.find((x) => x.asset === 'BTC')!;
|
|
if (btc.buy_count !== 2 || btc.sell_count !== 1) throw new Error('btc counts');
|
|
const xmr = r.find((x) => x.asset === 'XMR')!;
|
|
if (xmr.buy_count !== 1 || xmr.sell_count !== 0) throw new Error('xmr counts');
|
|
});
|
|
|
|
scenario('output sorted alphabetically by asset', () => {
|
|
const r = aggregateListingHistogram([
|
|
{ side: 'buy', asset: 'XMR' },
|
|
{ side: 'buy', asset: 'BLURT' },
|
|
{ side: 'buy', asset: 'BTC' }
|
|
]);
|
|
if (r.map((x) => x.asset).join(',') !== 'BLURT,BTC,XMR') {
|
|
throw new Error(r.map((x) => x.asset).join(','));
|
|
}
|
|
});
|
|
|
|
scenario('drops items with empty asset', () => {
|
|
const r = aggregateListingHistogram([
|
|
{ side: 'buy', asset: '' },
|
|
{ side: 'buy', asset: 'BTC' }
|
|
]);
|
|
if (r.length !== 1) throw new Error(`got ${r.length}`);
|
|
});
|
|
|
|
scenario('drops items with bad side', () => {
|
|
const r = aggregateListingHistogram([
|
|
{ side: 'foo' as 'buy', asset: 'BTC' },
|
|
{ side: 'buy', asset: 'BTC' }
|
|
]);
|
|
if (r[0]!.buy_count !== 1) throw new Error('count');
|
|
});
|
|
|
|
// ─── totalListings ──────────────────────────────────────────────────
|
|
|
|
scenario('totalListings counts only valid items', () => {
|
|
const total = totalListings([
|
|
{ side: 'buy', asset: 'BTC' },
|
|
{ side: 'sell', asset: 'BTC' },
|
|
{ side: 'buy', asset: '' }, // dropped
|
|
{ side: 'foo' as 'buy', asset: 'XMR' } // dropped
|
|
]);
|
|
if (total !== 2) throw new Error(`got ${total}`);
|
|
});
|
|
|
|
// ─── decorateOp ─────────────────────────────────────────────────────
|
|
|
|
scenario('decorates transfer', () => {
|
|
const d = decorateOp('transfer', { from: 'a', to: 'b', amount: '1.000 BLURT' });
|
|
if (d.kind !== 'transfer') throw new Error(d.kind);
|
|
if (d.isMorphitOp !== false) throw new Error('isMorphitOp');
|
|
});
|
|
|
|
scenario('decorates comment', () => {
|
|
// cp397: comment decoration interpolates the author, so the op must
|
|
// carry one (real `comment` ops always do — it's a required field).
|
|
const d = decorateOp('comment', { author: 'alice', permlink: 'hello-world' });
|
|
if (d.kind !== 'comment') throw new Error(d.kind);
|
|
if (d.labelKey !== 'comment') throw new Error(d.labelKey);
|
|
});
|
|
|
|
scenario('decorates comment reply (parent_author present)', () => {
|
|
const d = decorateOp('comment', { author: 'alice', parent_author: 'bob' });
|
|
if (d.kind !== 'comment') throw new Error(d.kind);
|
|
if (d.labelKey !== 'comment_reply') throw new Error(d.labelKey);
|
|
});
|
|
|
|
scenario('comment without author → native_unknown (cannot label it)', () => {
|
|
const d = decorateOp('comment', {});
|
|
if (d.kind !== 'native_unknown') throw new Error(d.kind);
|
|
});
|
|
|
|
scenario('decorates vote', () => {
|
|
// cp397: vote decoration interpolates voter + author (both required
|
|
// fields on a real `vote` op); weight sign picks up/down.
|
|
const d = decorateOp('vote', { voter: 'alice', author: 'bob', weight: 10000 });
|
|
if (d.kind !== 'vote') throw new Error(d.kind);
|
|
if (d.labelKey !== 'vote') throw new Error(d.labelKey);
|
|
});
|
|
|
|
scenario('decorates downvote (negative weight → vote_down)', () => {
|
|
const d = decorateOp('vote', { voter: 'alice', author: 'bob', weight: -10000 });
|
|
if (d.kind !== 'vote') throw new Error(d.kind);
|
|
if (d.labelKey !== 'vote_down') throw new Error(d.labelKey);
|
|
});
|
|
|
|
scenario('vote without voter/author → native_unknown (cannot label it)', () => {
|
|
const d = decorateOp('vote', {});
|
|
if (d.kind !== 'native_unknown') throw new Error(d.kind);
|
|
});
|
|
|
|
scenario('decorates morphit_order_v1 custom_json', () => {
|
|
const d = decorateOp('custom_json', { id: 'morphit_order_v1', json: '{}' });
|
|
if (d.kind !== 'morphit_order') throw new Error(d.kind);
|
|
if (d.isMorphitOp !== true) throw new Error('isMorphitOp');
|
|
});
|
|
|
|
scenario('decorates morphit_chat_v1 custom_json', () => {
|
|
const d = decorateOp('custom_json', { id: 'morphit_chat_v1', json: '{}' });
|
|
if (d.kind !== 'morphit_chat') throw new Error(d.kind);
|
|
});
|
|
|
|
scenario('decorates morphit_operator_block_v1 custom_json', () => {
|
|
const d = decorateOp('custom_json', {
|
|
id: 'morphit_operator_block_v1',
|
|
json: '{}'
|
|
});
|
|
if (d.kind !== 'morphit_operator_block') throw new Error(d.kind);
|
|
});
|
|
|
|
scenario('unknown custom_json id → custom_json_unknown', () => {
|
|
const d = decorateOp('custom_json', { id: 'unknown_app_v1', json: '{}' });
|
|
if (d.kind !== 'custom_json_unknown') throw new Error(d.kind);
|
|
if (d.isMorphitOp !== false) throw new Error('isMorphitOp');
|
|
});
|
|
|
|
scenario('custom_json with no id → custom_json_unknown', () => {
|
|
const d = decorateOp('custom_json', { json: '{}' });
|
|
if (d.kind !== 'custom_json_unknown') throw new Error(d.kind);
|
|
});
|
|
|
|
scenario('custom_json with non-string id → custom_json_unknown', () => {
|
|
const d = decorateOp('custom_json', { id: 123, json: '{}' });
|
|
if (d.kind !== 'custom_json_unknown') throw new Error(d.kind);
|
|
});
|
|
|
|
scenario('non-comment non-transfer non-custom_json native op → native_unknown', () => {
|
|
const d = decorateOp('account_create', {});
|
|
if (d.kind !== 'native_unknown') throw new Error(d.kind);
|
|
});
|
|
|
|
scenario('non-string opName → native_unknown', () => {
|
|
const d = decorateOp(null as unknown as string, {});
|
|
if (d.kind !== 'native_unknown') throw new Error(d.kind);
|
|
});
|
|
|
|
scenario('null opBody on custom_json → custom_json_unknown', () => {
|
|
const d = decorateOp('custom_json', null);
|
|
if (d.kind !== 'custom_json_unknown') throw new Error(d.kind);
|
|
});
|
|
|
|
console.log(`\n${'─'.repeat(54)}`);
|
|
if (failures === 0) {
|
|
console.log(`✓ all ${scenarios} scenarios passed`);
|
|
process.exit(0);
|
|
} else {
|
|
console.log(`✗ ${failures}/${scenarios} scenarios failed`);
|
|
process.exit(1);
|
|
}
|