morphit/apps/indexer/scripts/anti-snipe-extension-smoke.ts

182 lines
6.6 KiB
TypeScript

#!/usr/bin/env tsx
/**
* anti-snipe-extension-smoke — Part 122 cp18.
*
* Static + behavioral smoke for the anti-snipe extension SQL.
* Does NOT exercise the live handler (covered by
* featurebid-handler-smoke); this isolates the rules of the
* extension UPDATE itself by testing what the predicate WOULD
* select on a fixed in-memory dataset.
*
* Scenarios (each one a separate boolean assertion):
* 1. Bid expiring outside the snipe window → NOT extended.
* 2. Bid expiring inside the snipe window → extended.
* 3. Bid at extension_count = MAX_EXTENSIONS → NOT extended (cap).
* 4. Bid at extension_count = MAX_EXTENSIONS - 1 → extended.
* 5. Cancelled bid in window → NOT extended.
* 6. Bid outside top-MAX_SLOTS → NOT extended (rank gate).
* 7. The triggering bid itself → NOT extended (trx_id self-skip).
* 8. Bid whose effective_at is in the future → NOT extended.
*
* Implementation: pure in-process simulation. Mirrors the
* UPDATE predicate from featureBid.ts as a TS predicate so the
* rules can be unit-tested without a Postgres connection. When
* the SQL changes, this file's predicate MUST change in lockstep
* — both live in the comments below as the source-of-truth
* spec.
*/
interface BidSnapshot {
bid_id: number;
bidder: string;
trx_id: string;
cancelled: boolean;
effective_at: Date;
expires_at: Date;
blurt_per_hour: number;
block_time_at: Date;
extension_count: number;
}
const NOW = new Date('2026-05-16T12:00:00Z');
const SNIPE_WINDOW_MINUTES = 5;
const MAX_EXTENSIONS = 6;
const MAX_SLOTS = 3;
const NEW_BID_TRX = 'new-bid-trx-id';
function minutesFromNow(m: number): Date {
return new Date(NOW.getTime() + m * 60_000);
}
/** Mirror of the featureBid.ts UPDATE predicate. Returns true
* if the row WOULD be extended by the cp18 anti-snipe step. */
function wouldExtend(b: BidSnapshot, rankAmongTopN: number | null): boolean {
// CTE predicate: in top-MAX_SLOTS active set
if (rankAmongTopN === null || rankAmongTopN > MAX_SLOTS) return false;
// UPDATE WHERE clauses
if (b.cancelled) return false;
if (b.effective_at > NOW) return false; // not yet active
if (b.expires_at <= NOW) return false; // already expired
if (b.trx_id === NEW_BID_TRX) return false; // self-skip
if (b.expires_at > minutesFromNow(SNIPE_WINDOW_MINUTES)) return false; // outside window
if (b.extension_count >= MAX_EXTENSIONS) return false; // cap
return true;
}
const scenarios: Array<{ name: string; ok: boolean; detail?: string }> = [];
function check(name: string, ok: boolean, detail?: string): void {
scenarios.push({ name, ok, detail });
}
const baseBid = (overrides: Partial<BidSnapshot> = {}): BidSnapshot => ({
bid_id: 1,
bidder: 'alice',
trx_id: 'alice-bid-1',
cancelled: false,
effective_at: new Date(NOW.getTime() - 60 * 60_000),
expires_at: minutesFromNow(2), // inside window
blurt_per_hour: 50,
block_time_at: new Date(NOW.getTime() - 60 * 60_000),
extension_count: 0,
...overrides
});
// ─── 1: outside window → not extended ──────────────────────
check(
'bid expiring 30 minutes from now is not extended',
!wouldExtend(baseBid({ expires_at: minutesFromNow(30) }), 1)
);
// ─── 2: inside window → extended ────────────────────────────
check(
'bid expiring 2 minutes from now is extended',
wouldExtend(baseBid({ expires_at: minutesFromNow(2) }), 1)
);
// ─── 3: at MAX_EXTENSIONS → not extended ────────────────────
check(
'bid at extension_count = MAX_EXTENSIONS is not extended (cap)',
!wouldExtend(baseBid({ extension_count: MAX_EXTENSIONS }), 1)
);
// ─── 4: at MAX_EXTENSIONS - 1 → extended ───────────────────
check(
'bid at extension_count = MAX_EXTENSIONS - 1 is still extended',
wouldExtend(baseBid({ extension_count: MAX_EXTENSIONS - 1 }), 1)
);
// ─── 5: cancelled → not extended ───────────────────────────
check(
'cancelled bid in window is not extended',
!wouldExtend(baseBid({ cancelled: true }), 1)
);
// ─── 6: outside top-MAX_SLOTS → not extended ───────────────
check(
'bid at rank MAX_SLOTS+1 is not extended (rank gate)',
!wouldExtend(baseBid(), MAX_SLOTS + 1)
);
check(
'bid not ranked at all (cancelled/expired) is not extended',
!wouldExtend(baseBid(), null)
);
// ─── 7: the triggering bid itself → not extended ───────────
check(
'the new bid that triggered the extension does not extend itself',
!wouldExtend(baseBid({ trx_id: NEW_BID_TRX }), 1)
);
// ─── 8: future effective_at → not extended ─────────────────
check(
'bid not yet active (effective_at in future) is not extended',
!wouldExtend(baseBid({ effective_at: minutesFromNow(10) }), 1)
);
// ─── 9: at MAX_EXTENSIONS-edge with cap = 6 ────────────────
// Sanity check that MAX_EXTENSIONS aligns with featureBid.ts.
check(
'MAX_EXTENSIONS cap matches handler constant (6)',
MAX_EXTENSIONS === 6
);
// ─── 10: SNIPE_WINDOW edge — exactly at boundary ───────────
// PostgreSQL `<=` is inclusive; mirror that semantics.
check(
'bid expiring at exactly snipe_window boundary IS extended (inclusive)',
wouldExtend(baseBid({ expires_at: minutesFromNow(SNIPE_WINDOW_MINUTES) }), 1)
);
// ─── 11: SNIPE_WINDOW edge — one second past ──────────────
check(
'bid expiring 1 second past snipe_window is NOT extended',
!wouldExtend(
baseBid({
expires_at: new Date(minutesFromNow(SNIPE_WINDOW_MINUTES).getTime() + 1000)
}),
1
)
);
// ─── Report ─────────────────────────────────────────────────
console.log(`anti-snipe-extension smoke: ${scenarios.length} scenarios\n`);
let failed = 0;
for (const s of scenarios) {
console.log(s.ok ? `${s.name}` : `${s.name}${s.detail ? `${s.detail}` : ''}`);
if (!s.ok) failed++;
}
console.log('');
if (failed === 0) {
console.log(`✓ all ${scenarios.length} anti-snipe scenarios pass`);
process.exit(0);
} else {
console.error(`${failed} anti-snipe scenarios failed`);
process.exit(1);
}
// cp474 — module marker. Without a top-level import/export tsc treats this
// file as a global script, so its `scenarios`/`failed` consts collide with every
// other script-style smoke when the suite is typechecked as one project. This
// has no runtime effect under tsx.
export {};