TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import { describe, expect, it } from 'vitest';2import { evaluateAssetAlert, evaluateCategoryAlert, evaluateIndexAlert, inCooldown, inQuietHours, targetHit, type AlertRow, type AssetState } from './evaluate.js';34const alert = (over: Partial<AlertRow>): AlertRow => ({ id: 'a', userId: 'u', alertType: 'price_below', targetType: 'asset', targetId: 'rare_1', threshold: 1000, active: true, lastTriggeredAt: null, cooldownMinutes: 1440, name: null, channel: 'both', ...over });5const state = (over: Partial<AssetState>): AssetState => ({ title: 'Charizard', slug: 'charizard', rivUsd: 900, rivConfidence: 0.8, rivSampleSize: 12, athUsd: 5000, latestSaleUsd: 950, latestSaleAt: new Date(), sales30d: 4, baselineSales30d: 2, newListings: [], newAuctionLots: [], endingLots: [], newRecordSale: null, populationChange: null, ...over });67describe('alert evaluation', () => {8 it('price thresholds', () => {9 expect(evaluateAssetAlert(alert({}), state({}))?.title).toContain('below');10 expect(evaluateAssetAlert(alert({}), state({ rivUsd: 1200 }))).toBeNull();11 expect(evaluateAssetAlert(alert({ alertType: 'price_above', threshold: 800 }), state({}))?.title).toContain('above');12 expect(evaluateAssetAlert(alert({}), state({ rivUsd: null }))).toBeNull();13 });14 it('respects cooldown and inactive', () => {15 const now = new Date();16 expect(inCooldown(alert({ lastTriggeredAt: new Date(now.getTime() - 60_000) }), now)).toBe(true);17 expect(inCooldown(alert({ lastTriggeredAt: new Date(now.getTime() - 2 * 86_400_000) }), now)).toBe(false);18 expect(evaluateAssetAlert(alert({ active: false }), state({}))).toBeNull();19 });20 it('listings, records, volume, population', () => {21 expect(evaluateAssetAlert(alert({ alertType: 'new_listing' }), state({ newListings: [{ id: 'l', priceUsd: 700, sourceId: 'ebay', firstSeenAt: new Date() }] }))?.facts).toContainEqual(['Lowest ask', '$700.00']);22 expect(evaluateAssetAlert(alert({ alertType: 'record_sale' }), state({ newRecordSale: { priceUsd: 6000, saleDate: new Date('2026-01-01'), sourceId: 'goldin' } }))?.title).toContain('$6,000');23 expect(evaluateAssetAlert(alert({ alertType: 'unusual_volume', threshold: 50 }), state({ sales30d: 6, baselineSales30d: 2 }))?.title).toContain('+200.0%');24 expect(evaluateAssetAlert(alert({ alertType: 'unusual_volume', threshold: 50 }), state({ sales30d: 2, baselineSales30d: 2 }))).toBeNull();25 expect(evaluateAssetAlert(alert({ alertType: 'population_update' }), state({ populationChange: { grader: 'psa', from: 100, to: 120, date: '2026-09-01' } }))?.title).toContain('100 → 120');26 const soon = new Date(Date.now() + 3600_000);27 expect(evaluateAssetAlert(alert({ alertType: 'auction_ending' }), state({ endingLots: [{ id: 'x', title: 'Lot 1', endsAt: soon, auctionHouse: 'Heritage' }] }))?.title).toContain('ends');28 });29 it('category and index alerts', () => {30 expect(evaluateCategoryAlert(alert({ alertType: 'market_move', targetType: 'category', threshold: 3 }), { name: 'Pokémon', slug: 'pokemon', change1d: 0.045, newRecordSale: null, radarFindings: [], newAuctionLots: 0, endingLots: 0, sales30d: 0, baselineSales30d: null })?.title).toContain('+4.5%');31 expect(evaluateCategoryAlert(alert({ alertType: 'market_move', targetType: 'category', threshold: 5 }), { name: 'Pokémon', slug: 'pokemon', change1d: 0.045, newRecordSale: null, radarFindings: [], newAuctionLots: 0, endingLots: 0, sales30d: 0, baselineSales30d: null })).toBeNull();32 expect(evaluateIndexAlert(alert({ alertType: 'market_move', targetType: 'index', threshold: 2 }), { ticker: 'RARE', name: 'RareIndex', change1d: -0.03, value: 1042 })?.title).toContain('-3.0%');33 });34 it('quiet hours and targets', () => {35 expect(inQuietHours({ quietStart: 22, quietEnd: 7 }, new Date('2026-01-01T23:00:00Z'))).toBe(true);36 expect(inQuietHours({ quietStart: 22, quietEnd: 7 }, new Date('2026-01-01T12:00:00Z'))).toBe(false);37 expect(inQuietHours({ quietStart: 9, quietEnd: 17 }, new Date('2026-01-01T12:00:00Z'))).toBe(true);38 expect(inQuietHours({}, new Date())).toBe(false);39 expect(targetHit('below', 1000, 900)).toBe(true);40 expect(targetHit('above', 1000, 900)).toBe(false);41 expect(targetHit('above', 1000, null)).toBe(false);42 });43});44