morphit/apps/ops-cli/scripts/fastpath-always-on-smoke.ts

124 lines
5.1 KiB
TypeScript

#!/usr/bin/env tsx
/**
* fastpath-always-on — v1.7.0, ADR-0051.
*
* THE DECISION THIS PINS. The head-block fast path has NO on/off switch, and
* that is deliberate rather than an oversight someone should "fix" later.
*
* ADR-0048 shipped `MORPHIT_INDEXER_CHAT_FASTPATH_ENABLED` as an opt-out for
* operators who wanted nothing shown until it was irreversible. v1.7.0 removed
* it — REMOVED, not renamed — because the reasoning didn't survive contact with
* what the tailer actually is: it never writes the database, so the worst a
* broken fast path can do is fail to make things fast. There is nothing to
* protect an operator from, and nobody prefers slow. A flag that is always true
* is a branch that can be wrong, config that can drift, a second path every
* smoke must cover, and — via the old `Fast chat: on` health line — an
* invitation for an operator to conclude that slow is a thing they might want.
*
* This replaced `upgrade-fastpath-ensure-smoke`, which existed to check the
* knob was on. Guarding "the knob is gone" is the same job for the opposite
* world, so it keeps the registration slot rather than shifting every chunk
* index after it.
*
* Tamper tests (each must turn this smoke red):
* - Re-add MORPHIT_INDEXER_CHAT_FASTPATH_ENABLED to the env schema → fails.
* - Re-add an `enabled` gate to HeadTailer.run() → fails.
* - Put the dead var back in ops/env/indexer.env.example → fails.
* - Re-add `enabled` to HeadTailerStatus → fails.
*/
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const REPO = join(HERE, '..', '..', '..');
const read = (rel: string): string => readFileSync(join(REPO, rel), 'utf8');
let passed = 0;
let failed = 0;
const check = (name: string, cond: boolean, detail = ''): void => {
if (cond) {
console.log(`${name}`);
passed++;
} else {
console.log(`${name}${detail ? `: ${detail}` : ''}`);
failed++;
}
};
console.log('\n── fastpath-always-on (v1.7.0 / ADR-0051) ─────────────\n');
const config = read('apps/indexer/src/config/index.ts');
const tailer = read('apps/indexer/src/indexer/headTailer.ts');
const envExample = read('ops/env/indexer.env.example');
const opsHealth = read('apps/ops-cli/src/commands/health.ts');
// ─── the knob is gone from every layer it lived in ───────────────
// Match a DECLARATION, not a mention: the files explain WHY the var was
// removed, and a guard that punishes documentation is a guard people delete.
check(
'env schema declares no *_FASTPATH_ENABLED var',
!/^\s*MORPHIT_[A-Z_]*FASTPATH_ENABLED\s*:/m.test(config),
'fast is not an operator preference — see ADR-0051'
);
check(
'Config has no fastPathEnabled / chatFastPathEnabled field',
!/readonly\s+(chat)?[fF]astPathEnabled\s*:/.test(config)
);
check(
'the interval knob survives (a straining node needs a real lever)',
/MORPHIT_INDEXER_FASTPATH_INTERVAL_MS\s*:/.test(config) &&
/readonly fastPathIntervalMs: number;/.test(config)
);
// ─── run() must not be gateable ──────────────────────────────────
check(
'HeadTailer.run() has no enabled gate',
!/if \(!this\.config\.[a-zA-Z]*[fF]astPathEnabled\)/.test(tailer),
'an early return here silently restores the opt-out'
);
check(
'HeadTailerStatus reports no always-true `enabled`',
!/export interface HeadTailerStatus \{[^}]*readonly enabled:/s.test(tailer),
'a status field that cannot vary is noise at best, misleading at worst'
);
// ─── operator-facing surfaces ────────────────────────────────────
check(
'env example ships no dead FASTPATH_ENABLED assignment',
!/^MORPHIT_[A-Z_]*FASTPATH_ENABLED=/m.test(envExample)
);
check(
'env example ships the interval under its new name',
/^MORPHIT_INDEXER_FASTPATH_INTERVAL_MS=/m.test(envExample)
);
check(
'health reports LAG, not an on/off line',
/FASTPATH_HEALTHY_LAG_BLOCKS/.test(opsHealth) && /behind head/.test(opsHealth),
'"running" is not the question — "is it keeping up" is'
);
check(
'health parses the `fastpath` block, not `chat_fastpath`',
/parseFastPath\(b\.fastpath\)/.test(opsHealth)
);
// ─── the invariant that makes losing the switch safe ─────────────
// This is the load-bearing premise of the whole decision. If the tailer ever
// starts writing to the DB, removing the operator's off switch stops being
// defensible and this file's reasoning is void.
check(
'the tailer still NEVER writes the database (premise of all the above)',
!/\bINSERT\b|\bUPDATE\b|\bDELETE\b|withTx\(/i.test(tailer),
'if the fast path can write, a reorg can corrupt state and the opt-out has to come back'
);
console.log(`\n${'─'.repeat(54)}`);
if (failed === 0) {
console.log(`✓ all ${passed} fastpath-always-on checks passed`);
process.exit(0);
} else {
console.log(`${failed}/${passed + failed} fastpath-always-on checks failed`);
process.exit(1);
}