Build LinkedIn thought leadership with algorithmic understanding, strategic consistency, and AI-assisted content creation. Updated for the January 2026 360Brew algorithm change. 16 agents, 25 commands, 6 skills, 9 hooks, 24 reference docs. Personal data sanitized: voice samples generalized to template, high-engagement posts cleared, region-specific references replaced with placeholders. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
205 lines
9.3 KiB
TypeScript
205 lines
9.3 KiB
TypeScript
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> = {}): 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");
|
|
});
|
|
});
|
|
});
|
|
});
|