Files
air-watcher/test/priceWatcher.test.js
2026-03-05 11:00:45 +09:00

188 lines
4.7 KiB
JavaScript

"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" }],
departureDateWindow: { from: "2026-06-01" },
},
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" }],
departureDateWindow: { from: "2026-06-01" },
},
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" }],
departureDateWindow: { from: "2026-06-01" },
},
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");
});
test("pollAll skips when another poll cycle is already in progress", async () => {
let release = null;
let started = null;
const startedPromise = new Promise((resolve) => {
started = resolve;
});
const watcher = new PriceWatcher({
crawler: {
async getQuotes() {
started();
await new Promise((resolve) => {
release = resolve;
});
return [
{
provider: "slow-crawler",
price: 999000,
currency: "KRW",
},
];
},
},
notifier: {
async notify() {},
},
logger: createSilentLogger(),
});
watcher.addWatch({
rawInput: "인천-마드리드 추적",
searchParams: { segments: [{ from: "ICN", to: "MAD" }], departureDateWindow: { from: "2026-06-01" } },
alertRules: {
notifyOnPriceChange: true,
targetPrice: null,
},
});
const firstCycle = watcher.pollAll();
await startedPromise;
const skipped = await watcher.pollAll();
assert.equal(skipped.length, 1);
assert.equal(skipped[0].skipped.reason, "poll_cycle_in_progress");
release();
await firstCycle;
});