113 lines
3.7 KiB
TypeScript
113 lines
3.7 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { evaluateAlert, type SnapshotInput } from '@/lib/alerts/evaluate'
|
|
|
|
function snap(price: number, daysAgo: number): SnapshotInput {
|
|
const d = new Date()
|
|
d.setDate(d.getDate() - daysAgo)
|
|
return { price, scrapedAt: d }
|
|
}
|
|
|
|
describe('evaluateAlert', () => {
|
|
describe('target_price', () => {
|
|
it('triggers when current price <= threshold', () => {
|
|
const r = evaluateAlert({
|
|
alert: { type: 'target_price', config: { threshold: 100 }, lastTriggeredAt: null },
|
|
currentPrice: 99,
|
|
history: [],
|
|
})
|
|
expect(r.triggered).toBe(true)
|
|
})
|
|
|
|
it('does not trigger when price > threshold', () => {
|
|
const r = evaluateAlert({
|
|
alert: { type: 'target_price', config: { threshold: 100 }, lastTriggeredAt: null },
|
|
currentPrice: 101,
|
|
history: [],
|
|
})
|
|
expect(r.triggered).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('all_time_low', () => {
|
|
it('triggers when current is below all previous successful prices', () => {
|
|
const r = evaluateAlert({
|
|
alert: { type: 'all_time_low', config: {}, lastTriggeredAt: null },
|
|
currentPrice: 50,
|
|
history: [snap(60, 1), snap(70, 5), snap(80, 10)],
|
|
})
|
|
expect(r.triggered).toBe(true)
|
|
expect(r.context.prevMin).toBe(60)
|
|
})
|
|
|
|
it('does not trigger when current equals previous min', () => {
|
|
const r = evaluateAlert({
|
|
alert: { type: 'all_time_low', config: {}, lastTriggeredAt: null },
|
|
currentPrice: 60,
|
|
history: [snap(60, 1)],
|
|
})
|
|
expect(r.triggered).toBe(false)
|
|
})
|
|
|
|
it('does not trigger with no history (first scrape)', () => {
|
|
const r = evaluateAlert({
|
|
alert: { type: 'all_time_low', config: {}, lastTriggeredAt: null },
|
|
currentPrice: 50,
|
|
history: [],
|
|
})
|
|
expect(r.triggered).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('percent_drop', () => {
|
|
it('triggers when price drops >= percent vs lookback avg', () => {
|
|
const r = evaluateAlert({
|
|
alert: { type: 'percent_drop', config: { lookback_days: 7, percent: 10 }, lastTriggeredAt: null },
|
|
currentPrice: 80,
|
|
history: [snap(100, 1), snap(100, 3), snap(100, 6), snap(50, 30)],
|
|
})
|
|
expect(r.triggered).toBe(true)
|
|
expect(r.context.percent).toBeGreaterThanOrEqual(10)
|
|
})
|
|
|
|
it('does not trigger when drop is smaller than threshold', () => {
|
|
const r = evaluateAlert({
|
|
alert: { type: 'percent_drop', config: { lookback_days: 7, percent: 10 }, lastTriggeredAt: null },
|
|
currentPrice: 95,
|
|
history: [snap(100, 1), snap(100, 3)],
|
|
})
|
|
expect(r.triggered).toBe(false)
|
|
})
|
|
|
|
it('does not trigger with no history in lookback window', () => {
|
|
const r = evaluateAlert({
|
|
alert: { type: 'percent_drop', config: { lookback_days: 7, percent: 10 }, lastTriggeredAt: null },
|
|
currentPrice: 50,
|
|
history: [snap(100, 30)],
|
|
})
|
|
expect(r.triggered).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('dedup', () => {
|
|
it('does not trigger if last triggered within 24h', () => {
|
|
const recent = new Date(Date.now() - 1000 * 60 * 60 * 12)
|
|
const r = evaluateAlert({
|
|
alert: { type: 'target_price', config: { threshold: 100 }, lastTriggeredAt: recent },
|
|
currentPrice: 50,
|
|
history: [],
|
|
})
|
|
expect(r.triggered).toBe(false)
|
|
})
|
|
|
|
it('triggers if last triggered > 24h ago', () => {
|
|
const old = new Date(Date.now() - 1000 * 60 * 60 * 25)
|
|
const r = evaluateAlert({
|
|
alert: { type: 'target_price', config: { threshold: 100 }, lastTriggeredAt: old },
|
|
currentPrice: 50,
|
|
history: [],
|
|
})
|
|
expect(r.triggered).toBe(true)
|
|
})
|
|
})
|
|
})
|