import { describe, test } from "node:test"; import assert from "node:assert/strict"; import { detectAlerts, detectWeeklyAlerts } from "../src/utils/alerts.js"; import type { PostAnalytics } from "../src/models/types.js"; /** * Helper function to create PostAnalytics with default values. */ function makePost(overrides: Partial = {}): PostAnalytics { return { id: `post-${Math.random().toString(36).substr(2, 9)}`, title: "Test Post", publishedDate: "2026-01-15", metrics: { impressions: 1000, reactions: 50, comments: 10, shares: 5, clicks: 100, engagementRate: 5.0, }, importedAt: new Date().toISOString(), exportSource: "LinkedIn", ...overrides, }; } describe("alerts", () => { describe("detectAlerts", () => { test("should find spike posts", () => { // Create posts with one outlier high value // Need value that's > 2.0 standard deviations from mean (not >=) // Using more base values to create a scenario where outlier exceeds threshold const posts = [ makePost({ id: "1", title: "Normal Post 1", metrics: { impressions: 1000, reactions: 50, comments: 10, shares: 5, clicks: 100, engagementRate: 5.0 } }), makePost({ id: "2", title: "Normal Post 2", metrics: { impressions: 1200, reactions: 50, comments: 10, shares: 5, clicks: 100, engagementRate: 5.0 } }), makePost({ id: "3", title: "Normal Post 3", metrics: { impressions: 1100, reactions: 50, comments: 10, shares: 5, clicks: 100, engagementRate: 5.0 } }), makePost({ id: "4", title: "Normal Post 4", metrics: { impressions: 900, reactions: 50, comments: 10, shares: 5, clicks: 100, engagementRate: 5.0 } }), makePost({ id: "5", title: "Normal Post 5", metrics: { impressions: 1050, reactions: 50, comments: 10, shares: 5, clicks: 100, engagementRate: 5.0 } }), makePost({ id: "6", title: "Viral Post", metrics: { impressions: 10000, reactions: 50, comments: 10, shares: 5, clicks: 100, engagementRate: 5.0 } }), ]; const alerts = detectAlerts(posts, "impressions"); assert.ok(alerts.length > 0, "Should detect at least one alert"); const spikeAlert = alerts.find(a => a.type === "spike"); assert.ok(spikeAlert, "Should have a spike alert"); assert.equal(spikeAlert.severity, "info"); assert.equal(spikeAlert.postId, "6"); assert.ok(spikeAlert.message.includes("Viral Post")); }); test("should find drop posts", () => { // Create posts with one outlier low value const posts = [ makePost({ id: "1", title: "Normal Post 1", metrics: { impressions: 10000, reactions: 50, comments: 10, shares: 5, clicks: 100, engagementRate: 5.0 } }), makePost({ id: "2", title: "Normal Post 2", metrics: { impressions: 10000, reactions: 50, comments: 10, shares: 5, clicks: 100, engagementRate: 5.0 } }), makePost({ id: "3", title: "Normal Post 3", metrics: { impressions: 10000, reactions: 50, comments: 10, shares: 5, clicks: 100, engagementRate: 5.0 } }), makePost({ id: "4", title: "Low Reach Post", metrics: { impressions: 100, reactions: 50, comments: 10, shares: 5, clicks: 100, engagementRate: 5.0 } }), ]; const alerts = detectAlerts(posts, "impressions"); assert.equal(alerts.length, 1); assert.equal(alerts[0].type, "drop"); assert.equal(alerts[0].severity, "warning"); assert.equal(alerts[0].postId, "4"); assert.ok(alerts[0].message.includes("Low Reach Post")); }); test("should return empty for uniform data", () => { const posts = [ makePost({ id: "1", metrics: { impressions: 5000, reactions: 50, comments: 10, shares: 5, clicks: 100, engagementRate: 5.0 } }), makePost({ id: "2", metrics: { impressions: 5000, reactions: 50, comments: 10, shares: 5, clicks: 100, engagementRate: 5.0 } }), makePost({ id: "3", metrics: { impressions: 5000, reactions: 50, comments: 10, shares: 5, clicks: 100, engagementRate: 5.0 } }), ]; const alerts = detectAlerts(posts, "impressions"); assert.equal(alerts.length, 0); }); test("should handle empty posts array", () => { const alerts = detectAlerts([]); assert.equal(alerts.length, 0); }); test("should sort alerts by severity", () => { // Create scenario with multiple alerts of different severities // For this, we'd need to manually create alerts with different severities // Since detectAlerts only produces "info" spikes and "warning" drops, // let's just verify the sorting works with what we have const posts = [ makePost({ id: "1", metrics: { impressions: 5000, reactions: 50, comments: 10, shares: 5, clicks: 100, engagementRate: 5.0 } }), makePost({ id: "2", metrics: { impressions: 5000, reactions: 50, comments: 10, shares: 5, clicks: 100, engagementRate: 5.0 } }), makePost({ id: "3", metrics: { impressions: 100, reactions: 50, comments: 10, shares: 5, clicks: 100, engagementRate: 5.0 } }), // Drop makePost({ id: "4", metrics: { impressions: 50000, reactions: 50, comments: 10, shares: 5, clicks: 100, engagementRate: 5.0 } }), // Spike ]; const alerts = detectAlerts(posts, "impressions"); // Should have drop (warning) first, then spike (info) if (alerts.length > 1) { assert.equal(alerts[0].severity, "warning"); assert.equal(alerts[1].severity, "info"); } }); }); describe("detectWeeklyAlerts", () => { test("should detect critical drop in impressions", () => { const current = { impressions: 1000, engagementRate: 5.0 }; const previous = { impressions: 3000, engagementRate: 5.0 }; // -66.7% drop const alerts = detectWeeklyAlerts(current, previous); const impressionAlerts = alerts.filter((a) => a.metric === "impressions"); assert.ok(impressionAlerts.length > 0); assert.equal(impressionAlerts[0].severity, "critical"); assert.equal(impressionAlerts[0].type, "drop"); }); test("should detect warning drop in impressions", () => { const current = { impressions: 6000, engagementRate: 5.0 }; const previous = { impressions: 10000, engagementRate: 5.0 }; // -40% drop const alerts = detectWeeklyAlerts(current, previous); const impressionAlerts = alerts.filter((a) => a.metric === "impressions"); assert.ok(impressionAlerts.length > 0); assert.equal(impressionAlerts[0].severity, "warning"); assert.equal(impressionAlerts[0].type, "drop"); }); test("should detect spike in impressions", () => { const current = { impressions: 25000, engagementRate: 5.0 }; const previous = { impressions: 10000, engagementRate: 5.0 }; // +150% increase const alerts = detectWeeklyAlerts(current, previous); const impressionAlerts = alerts.filter((a) => a.metric === "impressions"); assert.ok(impressionAlerts.length > 0); assert.equal(impressionAlerts[0].severity, "info"); assert.equal(impressionAlerts[0].type, "spike"); }); test("should detect critical drop in engagement rate", () => { const current = { impressions: 10000, engagementRate: 2.0 }; const previous = { impressions: 10000, engagementRate: 6.0 }; // -66.7% drop const alerts = detectWeeklyAlerts(current, previous); const engagementAlerts = alerts.filter((a) => a.metric === "engagementRate"); assert.ok(engagementAlerts.length > 0); assert.equal(engagementAlerts[0].severity, "critical"); assert.equal(engagementAlerts[0].type, "drop"); }); test("should detect warning drop in engagement rate", () => { const current = { impressions: 10000, engagementRate: 3.0 }; const previous = { impressions: 10000, engagementRate: 5.0 }; // -40% drop const alerts = detectWeeklyAlerts(current, previous); const engagementAlerts = alerts.filter((a) => a.metric === "engagementRate"); assert.ok(engagementAlerts.length > 0); assert.equal(engagementAlerts[0].severity, "warning"); assert.equal(engagementAlerts[0].type, "drop"); }); test("should detect spike in engagement rate", () => { const current = { impressions: 10000, engagementRate: 12.0 }; const previous = { impressions: 10000, engagementRate: 5.0 }; // +140% increase const alerts = detectWeeklyAlerts(current, previous); const engagementAlerts = alerts.filter((a) => a.metric === "engagementRate"); assert.ok(engagementAlerts.length > 0); assert.equal(engagementAlerts[0].severity, "info"); assert.equal(engagementAlerts[0].type, "spike"); }); test("should return empty for stable metrics", () => { const current = { impressions: 10000, engagementRate: 5.0 }; const previous = { impressions: 10200, engagementRate: 5.1 }; // Small changes const alerts = detectWeeklyAlerts(current, previous); assert.equal(alerts.length, 0); }); test("should handle multiple alerts and sort by severity", () => { const current = { impressions: 1000, engagementRate: 2.0 }; const previous = { impressions: 3000, engagementRate: 6.0 }; // Both critical drops const alerts = detectWeeklyAlerts(current, previous); assert.ok(alerts.length >= 2); // All should be critical alerts.forEach((alert) => { assert.equal(alert.severity, "critical"); }); }); }); });