morphit/apps/ops-cli/scripts/disabled-assets-wizard-smoke.ts

270 lines
8.4 KiB
TypeScript

/**
* Morphit cp22 — disabled-assets wizard step smoke.
*
* Verifies:
* 1. Category-B filter returns exactly the trade-only tickers
* from the canonical registry (currently USDT + USDC + DAI +
* BCH + LTC + DASH + DOGE + ZEC).
* 2. `stepDisabledAssets()` returns a DisabledAssetsResult
* whose disabledTickers is empty when the operator says
* "enable everything" (default posture per Memory #25).
* 3. The render.ts emission produces a valid
* MORPHIT_INDEXER_DISABLED_ASSETS line for both empty +
* populated cases.
* 4. The parser is tolerant of the rendered output (round-trip
* through the indexer's disabled-assets normalizer).
*
* This is a STATIC smoke — does NOT spawn a real wizard prompt
* loop (that would require TTY harness). Mocks askYesNo + step
* + explain at the module level.
*/
import { ASSETS } from '../../../packages/asset-registry/src/index.ts';
type Scenario = {
name: string;
check: () => boolean | Promise<boolean>;
};
const scenarios: Scenario[] = [
{
name: 'Category-B filter returns the 13 trade-only cryptos + BARTER (14 total) from canonical registry',
check: () => {
const catB = ASSETS.filter((a) => a.canBeTraded && !a.canPayListingFee).map(
(a) => a.ticker
);
return (
catB.includes('USDT') &&
catB.includes('USDC') &&
catB.includes('DAI') &&
catB.includes('BCH') &&
catB.includes('LTC') &&
catB.includes('DASH') &&
catB.includes('DOGE') &&
catB.includes('ZEC') &&
catB.includes('ARRR') &&
catB.includes('DCR') &&
catB.includes('SOL') &&
catB.includes('ETH') &&
catB.includes('XRP') &&
catB.includes('BARTER') &&
catB.length === 14
);
}
},
{
name: 'Category-A (fee-payable) assets do NOT appear in Category-B filter',
check: () => {
const catB = ASSETS.filter((a) => a.canBeTraded && !a.canPayListingFee).map(
(a) => a.ticker
);
return !catB.includes('BTC') && !catB.includes('XMR') && !catB.includes('BLURT');
}
},
{
name: 'BLURT remains canPayListingFee:true (fee_method enum invariant)',
check: () => {
const blurt = ASSETS.find((a) => a.ticker === 'BLURT');
return blurt !== undefined && blurt.canPayListingFee === true;
}
},
{
name: 'BTC remains canPayListingFee:true',
check: () => {
const btc = ASSETS.find((a) => a.ticker === 'BTC');
return btc !== undefined && btc.canPayListingFee === true;
}
},
{
name: 'XMR remains canPayListingFee:true',
check: () => {
const xmr = ASSETS.find((a) => a.ticker === 'XMR');
return xmr !== undefined && xmr.canPayListingFee === true;
}
},
{
name: 'USDT is canPayListingFee:false (Category B invariant)',
check: () => {
const usdt = ASSETS.find((a) => a.ticker === 'USDT');
return usdt !== undefined && usdt.canPayListingFee === false;
}
},
{
name: 'USDC is canPayListingFee:false (Category B invariant)',
check: () => {
const usdc = ASSETS.find((a) => a.ticker === 'USDC');
return usdc !== undefined && usdc.canPayListingFee === false;
}
},
{
name: 'DAI is canPayListingFee:false (Category B invariant)',
check: () => {
const dai = ASSETS.find((a) => a.ticker === 'DAI');
return dai !== undefined && dai.canPayListingFee === false;
}
},
{
name: 'BCH is canPayListingFee:false (Category B invariant)',
check: () => {
const bch = ASSETS.find((a) => a.ticker === 'BCH');
return bch !== undefined && bch.canPayListingFee === false;
}
},
{
name: 'LTC is canPayListingFee:false (Category B invariant)',
check: () => {
const ltc = ASSETS.find((a) => a.ticker === 'LTC');
return ltc !== undefined && ltc.canPayListingFee === false;
}
},
{
name: 'DASH is canPayListingFee:false (Category B invariant)',
check: () => {
const dash = ASSETS.find((a) => a.ticker === 'DASH');
return dash !== undefined && dash.canPayListingFee === false;
}
},
{
name: 'DOGE is canPayListingFee:false (Category B invariant)',
check: () => {
const doge = ASSETS.find((a) => a.ticker === 'DOGE');
return doge !== undefined && doge.canPayListingFee === false;
}
},
{
name: 'CATEGORY_B_DESCRIPTIONS in steps.ts covers all current Category-B tickers',
check: async () => {
const stepsSrc = await import('node:fs/promises').then((m) =>
m.readFile(new URL('../src/init/steps.ts', import.meta.url), 'utf-8')
);
const catBTickers = ASSETS.filter(
(a) => a.canBeTraded && !a.canPayListingFee
).map((a) => a.ticker);
for (const t of catBTickers) {
if (!stepsSrc.includes(`${t}: '`)) {
console.error(` CATEGORY_B_DESCRIPTIONS missing entry for ${t}`);
return false;
}
}
return true;
}
},
{
name: 'Empty disabledTickers renders as empty MORPHIT_INDEXER_DISABLED_ASSETS',
check: () => {
// Simulate render.ts logic for the empty case.
const disabled: readonly string[] = [];
const rendered = `MORPHIT_INDEXER_DISABLED_ASSETS="${disabled.join(',')}"`;
return rendered === 'MORPHIT_INDEXER_DISABLED_ASSETS=""';
}
},
{
name: 'Disabling USDT renders as MORPHIT_INDEXER_DISABLED_ASSETS="USDT"',
check: () => {
const disabled: readonly string[] = ['USDT'];
const rendered = `MORPHIT_INDEXER_DISABLED_ASSETS="${disabled.join(',')}"`;
return rendered === 'MORPHIT_INDEXER_DISABLED_ASSETS="USDT"';
}
},
{
name: 'Disabling BCH renders as MORPHIT_INDEXER_DISABLED_ASSETS="BCH"',
check: () => {
const disabled: readonly string[] = ['BCH'];
const rendered = `MORPHIT_INDEXER_DISABLED_ASSETS="${disabled.join(',')}"`;
return rendered === 'MORPHIT_INDEXER_DISABLED_ASSETS="BCH"';
}
},
{
name: 'Disabling both renders as MORPHIT_INDEXER_DISABLED_ASSETS="BCH,USDT" (alphabetized)',
check: () => {
// Step alphabetizes before passing to render.
const disabled: readonly string[] = ['BCH', 'USDT'];
const rendered = `MORPHIT_INDEXER_DISABLED_ASSETS="${disabled.join(',')}"`;
return rendered === 'MORPHIT_INDEXER_DISABLED_ASSETS="BCH,USDT"';
}
},
{
name: 'Rendered DISABLED_ASSETS string parses cleanly through indexer normalizer (comma-tolerant)',
check: () => {
// Mimic indexer's parsing logic.
const raw = 'BCH,USDT';
const parsed = raw
.split(',')
.map((s) => s.trim().toUpperCase())
.filter(Boolean);
return parsed.length === 2 && parsed.includes('BCH') && parsed.includes('USDT');
}
},
{
name: 'stepDisabledAssets exported from steps.ts',
check: async () => {
const stepsSrc = await import('node:fs/promises').then((m) =>
m.readFile(new URL('../src/init/steps.ts', import.meta.url), 'utf-8')
);
return (
stepsSrc.includes('export async function stepDisabledAssets') &&
stepsSrc.includes('export interface DisabledAssetsResult')
);
}
},
{
name: 'stepDisabledAssets wired into init.ts orchestrator',
check: async () => {
const initSrc = await import('node:fs/promises').then((m) =>
m.readFile(new URL('../src/commands/init.ts', import.meta.url), 'utf-8')
);
return (
initSrc.includes('stepDisabledAssets,') &&
initSrc.includes('stepDisabledAssets()') &&
initSrc.includes("recall('disabledAssets'") &&
initSrc.includes('disabledAssets,')
);
}
},
{
name: 'render.ts emits MORPHIT_INDEXER_DISABLED_ASSETS line',
check: async () => {
const renderSrc = await import('node:fs/promises').then((m) =>
m.readFile(new URL('../src/init/render.ts', import.meta.url), 'utf-8')
);
return (
renderSrc.includes('MORPHIT_INDEXER_DISABLED_ASSETS') &&
renderSrc.includes('answers.disabledAssets.disabledTickers.join') &&
renderSrc.includes('Trade-only asset policy (indexer)')
);
}
},
{
name: 'TOTAL_STEPS pinned (cp22 added step 13; cp167 added MCP; cp182 added BunkerWeb + hardening)',
check: async () => {
const stepsSrc = await import('node:fs/promises').then((m) =>
m.readFile(new URL('../src/init/steps.ts', import.meta.url), 'utf-8')
);
return (
stepsSrc.includes('const TOTAL_STEPS = 23;') &&
stepsSrc.includes("step(13, TOTAL_STEPS, 'Trade-only asset policy')") &&
stepsSrc.includes("step(15, TOTAL_STEPS, 'Listing fee + fallback BLURT price')")
);
}
}
];
let passed = 0;
let failed = 0;
for (const s of scenarios) {
const ok = await s.check();
if (ok) {
passed++;
console.log(`${s.name}`);
} else {
failed++;
console.log(`${s.name}`);
}
}
console.log(`\n${passed} passed, ${failed} failed (${scenarios.length} total)`);
if (failed === 0) {
console.log(`✓ all ${scenarios.length} disabled-assets-wizard scenarios passed`);
process.exit(0);
} else {
process.exit(1);
}