morphit/apps/indexer/scripts/stablecoin-depeg-detector-smoke.ts

143 lines
4.8 KiB
TypeScript

#!/usr/bin/env tsx
/**
* apps/indexer/scripts/stablecoin-depeg-detector-smoke.ts
*
* Structural defense (cp127) — invariants for the cross-stablecoin
* depeg detector module.
*
* These are STRUCTURAL checks (constants, types, contracts) — not
* end-to-end DB-driven checks. The DB-driven path requires a
* Postgres fixture and is covered by Vitest in a separate run.
* Structural smokes execute in CI without infrastructure.
*
* Scenarios:
* SD-1 Module exports the expected public API
* SD-2 Constants are sensible (threshold > 0 and < 0.5, window > 0)
* SD-3 DepegStatus union has exactly 3 variants
* SD-4 Empty stablecoinKeys → returns "unknown" status set
* SD-5 Single-stablecoin input → returns "unknown" for the lone coin
* SD-6 StablecoinDepegReport type fields complete
*/
import {
detectStablecoinDepeg,
DEPEG_RATIO_THRESHOLD,
DEPEG_WINDOW_HOURS,
DEPEG_MIN_TRADERS_PER_PAIR,
DEPEG_ORDER_AGE_GRACE_MINUTES,
type DepegStatus,
type StablecoinDepegReport
} from '../src/indexer/price/stablecoinDepegDetector';
let failed = 0;
let passed = 0;
function pass(name: string): void {
console.log(`${name}`);
passed++;
}
function fail(name: string, detail: string): void {
console.error(`${name}`);
console.error(` ${detail}`);
failed++;
}
console.log('\n── stablecoin-depeg-detector invariants smoke (cp127) ───\n');
// SD-1
{
const hasDetector = typeof detectStablecoinDepeg === 'function';
const hasConstants =
typeof DEPEG_RATIO_THRESHOLD === 'number' &&
typeof DEPEG_WINDOW_HOURS === 'number' &&
typeof DEPEG_MIN_TRADERS_PER_PAIR === 'number' &&
typeof DEPEG_ORDER_AGE_GRACE_MINUTES === 'number';
if (hasDetector && hasConstants) pass('SD-1 module exports the expected public API');
else fail('SD-1', `detector=${hasDetector}, constants=${hasConstants}`);
}
// SD-2 sensible thresholds
{
const sane =
DEPEG_RATIO_THRESHOLD > 0 &&
DEPEG_RATIO_THRESHOLD < 0.5 &&
DEPEG_WINDOW_HOURS > 0 &&
DEPEG_WINDOW_HOURS <= 168 && // ≤ 1 week
DEPEG_MIN_TRADERS_PER_PAIR >= 3 &&
DEPEG_ORDER_AGE_GRACE_MINUTES >= 5;
if (sane)
pass(
`SD-2 constants sane: threshold=${DEPEG_RATIO_THRESHOLD}, window=${DEPEG_WINDOW_HOURS}h, traders=${DEPEG_MIN_TRADERS_PER_PAIR}, grace=${DEPEG_ORDER_AGE_GRACE_MINUTES}min`
);
else fail('SD-2', 'one or more constants outside sane range');
}
// SD-3 DepegStatus union completeness — type-level check via satisfies
{
const variants: DepegStatus[] = ['pegged', 'depegged', 'unknown'];
if (variants.length === 3 && variants.includes('pegged') && variants.includes('depegged') && variants.includes('unknown')) {
pass('SD-3 DepegStatus union has exactly 3 variants: pegged | depegged | unknown');
} else {
fail('SD-3', `got: ${JSON.stringify(variants)}`);
}
}
// SD-4, SD-5 require a DB mock — we use a minimal stub
//
// The detector calls db.query() with parameters; we return empty
// rowsets so the detector exercises its <2-stablecoin guard cleanly.
const stubDb = {
query: async <T = unknown>(_sql: string, _params?: unknown[]): Promise<{ rows: T[] }> => {
return { rows: [] as T[] };
}
// Cast through unknown because the real Database interface has many more methods we don't need here
} as unknown as Parameters<typeof detectStablecoinDepeg>[0];
(async () => {
// SD-4 empty stablecoinKeys
const empty = await detectStablecoinDepeg(stubDb, { stablecoinKeys: [], operatorAccountName: 'morphit' });
if (
empty.usable_pair_count === 0 &&
Object.keys(empty.status).length === 0 &&
empty.pair_details.length === 0
) {
pass('SD-4 empty stablecoinKeys → returns empty status set');
} else {
fail('SD-4', `unexpected: ${JSON.stringify(empty)}`);
}
// SD-5 single-stablecoin input
const single = await detectStablecoinDepeg(stubDb, { stablecoinKeys: ['usdt'], operatorAccountName: 'morphit' });
if (
single.usable_pair_count === 0 &&
single.status.usdt === 'unknown' &&
single.pair_details.length === 0
) {
pass('SD-5 single-stablecoin input → returns "unknown" for the lone coin (no cross-ratio possible)');
} else {
fail('SD-5', `unexpected: ${JSON.stringify(single)}`);
}
// SD-6 type contract check via satisfies
{
const sample: StablecoinDepegReport = {
status: { usdt: 'pegged', usdc: 'pegged', dai: 'depegged' },
usable_pair_count: 3,
pair_details: [{ a: 'dai', b: 'usdc', ratio: 0.97, trader_count: 5 }],
window_hours: 8,
threshold: 0.03
};
if (sample.status.usdt === 'pegged' && sample.pair_details.length === 1) {
pass('SD-6 StablecoinDepegReport type contract holds');
} else {
fail('SD-6', 'unreachable');
}
}
const total = passed + failed;
console.log(`\n${passed} passed, ${failed} failed (${total} total)`);
if (failed > 0) {
console.error('\nstablecoin-depeg-detector-smoke FAILED');
process.exit(1);
}
console.log(`✓ all ${total} stablecoin-depeg-detector scenarios passed`);
})();