"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const { PriceWatcher } = require("../src/priceWatcher"); function createSequenceCrawler(prices) { let index = 0; return { async getQuotes() { const safeIndex = Math.min(index, prices.length - 1); const price = prices[safeIndex]; index += 1; return [ { provider: "sequence-crawler", price, currency: "KRW", }, ]; }, }; } function createSilentLogger() { return { log() {}, error() {}, }; } test("emits threshold alerts when crossing and improving below threshold", async () => { const notifications = []; const watcher = new PriceWatcher({ crawler: createSequenceCrawler([1000, 950, 890, 870]), notifier: { async notify(event) { notifications.push(event); }, }, logger: createSilentLogger(), }); const watchId = watcher.addWatch({ rawInput: "인천-마드리드 추적", searchParams: { segments: [{ from: "ICN", to: "MAD" }], }, alertRules: { targetPrice: 900, notifyOnPriceChange: false, }, }); await watcher.pollWatch(watchId); await watcher.pollWatch(watchId); await watcher.pollWatch(watchId); await watcher.pollWatch(watchId); assert.equal(notifications.length, 2); assert.equal(notifications[0].eventType, "target_price"); assert.equal(notifications[0].currentBestPrice, 890); assert.equal(notifications[1].currentBestPrice, 870); }); test("emits price change alerts when price changes", async () => { const notifications = []; const watcher = new PriceWatcher({ crawler: createSequenceCrawler([1000, 980, 980, 950]), notifier: { async notify(event) { notifications.push(event); }, }, logger: createSilentLogger(), }); const watchId = watcher.addWatch({ rawInput: "인천-마드리드 추적", searchParams: { segments: [{ from: "ICN", to: "MAD" }], }, alertRules: { notifyOnPriceChange: true, targetPrice: null, }, }); await watcher.pollWatch(watchId); await watcher.pollWatch(watchId); await watcher.pollWatch(watchId); await watcher.pollWatch(watchId); assert.equal(notifications.length, 2); assert.equal(notifications[0].eventType, "price_changed"); assert.equal(notifications[0].previousBestPrice, 1000); assert.equal(notifications[0].currentBestPrice, 980); assert.equal(notifications[1].previousBestPrice, 980); assert.equal(notifications[1].currentBestPrice, 950); }); test("keeps crawl snapshot even when notifier fails", async () => { const watcher = new PriceWatcher({ crawler: createSequenceCrawler([950000]), notifier: { async notify() { throw new Error("telegram timeout"); }, }, logger: createSilentLogger(), }); const watchId = watcher.addWatch({ rawInput: "인천-마드리드 추적", searchParams: { segments: [{ from: "ICN", to: "MAD" }], }, alertRules: { targetPrice: 980000, notifyOnPriceChange: false, }, }); const result = await watcher.pollWatch(watchId); const watch = watcher.getWatch(watchId); assert.equal(result.notificationSent, false); assert.equal(result.alert.eventType, "target_price"); assert.equal(result.snapshot.bestPrice, 950000); assert.equal(result.error.phase, "notify"); assert.equal(watch.lastSnapshot.bestPrice, 950000); assert.equal(watch.lastError.phase, "notify"); });