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>
570 lines
17 KiB
TypeScript
570 lines
17 KiB
TypeScript
import { describe, test, afterEach } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { mkdtempSync, rmSync, existsSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import {
|
|
getISOWeek,
|
|
getCurrentISOWeek,
|
|
getPostsForWeek,
|
|
generateWeeklyReport,
|
|
} from "../src/reports/weekly.js";
|
|
import { saveBatch, saveWeeklyReport } from "../src/utils/storage.js";
|
|
import type { PostAnalytics, AnalyticsBatch } from "../src/models/types.js";
|
|
|
|
// Helper function to create test post data
|
|
function createTestPost(overrides?: Partial<PostAnalytics>): PostAnalytics {
|
|
return {
|
|
id: overrides?.id || "test-post-1",
|
|
title: overrides?.title || "Test post content",
|
|
publishedDate: overrides?.publishedDate || "2026-01-15",
|
|
metrics: {
|
|
impressions: 1000,
|
|
reactions: 50,
|
|
comments: 10,
|
|
shares: 5,
|
|
clicks: 20,
|
|
engagementRate: 8.5,
|
|
...(overrides?.metrics || {}),
|
|
},
|
|
importedAt: overrides?.importedAt || "2026-01-20T10:00:00Z",
|
|
exportSource: overrides?.exportSource || "test-export.csv",
|
|
};
|
|
}
|
|
|
|
// Helper function to create test batch data
|
|
function createTestBatch(overrides?: Partial<AnalyticsBatch>): AnalyticsBatch {
|
|
const posts = overrides?.posts || [createTestPost()];
|
|
return {
|
|
batchId: overrides?.batchId || "12345678-1234-1234-1234-123456789abc",
|
|
importedAt: overrides?.importedAt || "2026-01-20T10:00:00Z",
|
|
exportFilename: overrides?.exportFilename || "test-export.csv",
|
|
dateRange: overrides?.dateRange || { from: "2026-01-15", to: "2026-01-20" },
|
|
postCount: posts.length,
|
|
posts,
|
|
};
|
|
}
|
|
|
|
describe("weekly", () => {
|
|
let tempDir: string;
|
|
|
|
// Create temp directory before each test
|
|
function setupTempDir(): string {
|
|
return mkdtempSync(join(tmpdir(), "analytics-test-"));
|
|
}
|
|
|
|
// Clean up temp directory after each test
|
|
afterEach(() => {
|
|
if (tempDir && existsSync(tempDir)) {
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe("getISOWeek", () => {
|
|
test("should return correct ISO week for 2026-01-01", () => {
|
|
// 2026-01-01 is Thursday, should be week 1 of 2026
|
|
const result = getISOWeek(new Date("2026-01-01"));
|
|
assert.equal(result, "2026-W01");
|
|
});
|
|
|
|
test("should return correct ISO week for 2025-12-29", () => {
|
|
// 2025-12-29 is Monday, first day of ISO week 1, 2026
|
|
const result = getISOWeek(new Date("2025-12-29"));
|
|
assert.equal(result, "2026-W01");
|
|
});
|
|
|
|
test("should return correct ISO week for 2025-12-28", () => {
|
|
// 2025-12-28 is Sunday, last day of 2025 week 52
|
|
const result = getISOWeek(new Date("2025-12-28"));
|
|
assert.equal(result, "2025-W52");
|
|
});
|
|
|
|
test("should handle year boundaries - early January", () => {
|
|
// 2025-01-01 is Wednesday, should be in 2025-W01
|
|
const result = getISOWeek(new Date("2025-01-01"));
|
|
assert.equal(result, "2025-W01");
|
|
});
|
|
|
|
test("should handle year boundaries - late December", () => {
|
|
// 2024-12-30 is Monday, should be in 2025-W01
|
|
const result = getISOWeek(new Date("2024-12-30"));
|
|
assert.equal(result, "2025-W01");
|
|
});
|
|
|
|
test("should handle mid-year dates", () => {
|
|
// 2026-06-15 is Monday
|
|
const result = getISOWeek(new Date("2026-06-15"));
|
|
assert.equal(result, "2026-W25");
|
|
});
|
|
|
|
test("should handle leap year", () => {
|
|
// 2024 is a leap year, Feb 29 should be in week 9
|
|
const result = getISOWeek(new Date("2024-02-29"));
|
|
assert.equal(result, "2024-W09");
|
|
});
|
|
});
|
|
|
|
describe("getCurrentISOWeek", () => {
|
|
test("should return a valid ISO week format", () => {
|
|
const result = getCurrentISOWeek();
|
|
|
|
// Should match YYYY-WXX format
|
|
assert.match(result, /^\d{4}-W\d{2}$/);
|
|
});
|
|
|
|
test("should return a week in reasonable range", () => {
|
|
const result = getCurrentISOWeek();
|
|
const year = parseInt(result.split("-")[0]);
|
|
const week = parseInt(result.split("-W")[1]);
|
|
|
|
// Year should be current or adjacent
|
|
const currentYear = new Date().getFullYear();
|
|
assert.ok(year >= currentYear - 1 && year <= currentYear + 1);
|
|
|
|
// Week should be 1-53
|
|
assert.ok(week >= 1 && week <= 53);
|
|
});
|
|
});
|
|
|
|
describe("getPostsForWeek", () => {
|
|
test("should filter posts to correct week", () => {
|
|
const posts: PostAnalytics[] = [
|
|
createTestPost({
|
|
id: "post1",
|
|
publishedDate: "2026-01-05", // 2026-W02
|
|
}),
|
|
createTestPost({
|
|
id: "post2",
|
|
publishedDate: "2026-01-12", // 2026-W03
|
|
}),
|
|
createTestPost({
|
|
id: "post3",
|
|
publishedDate: "2026-01-13", // 2026-W03
|
|
}),
|
|
createTestPost({
|
|
id: "post4",
|
|
publishedDate: "2026-01-19", // 2026-W04
|
|
}),
|
|
];
|
|
|
|
const week3Posts = getPostsForWeek(posts, "2026-W03");
|
|
|
|
assert.equal(week3Posts.length, 2);
|
|
assert.ok(week3Posts.some(p => p.id === "post2"));
|
|
assert.ok(week3Posts.some(p => p.id === "post3"));
|
|
});
|
|
|
|
test("should return empty for weeks with no posts", () => {
|
|
const posts: PostAnalytics[] = [
|
|
createTestPost({
|
|
id: "post1",
|
|
publishedDate: "2026-01-05", // 2026-W02
|
|
}),
|
|
];
|
|
|
|
const result = getPostsForWeek(posts, "2026-W03");
|
|
|
|
assert.deepEqual(result, []);
|
|
});
|
|
|
|
test("should handle empty posts array", () => {
|
|
const result = getPostsForWeek([], "2026-W03");
|
|
|
|
assert.deepEqual(result, []);
|
|
});
|
|
|
|
test("should handle posts across year boundary", () => {
|
|
const posts: PostAnalytics[] = [
|
|
createTestPost({
|
|
id: "post1",
|
|
publishedDate: "2025-12-29", // 2026-W01 (Monday)
|
|
}),
|
|
createTestPost({
|
|
id: "post2",
|
|
publishedDate: "2026-01-01", // 2026-W01 (Thursday)
|
|
}),
|
|
createTestPost({
|
|
id: "post3",
|
|
publishedDate: "2025-12-28", // 2025-W52 (Sunday)
|
|
}),
|
|
];
|
|
|
|
const week1Posts = getPostsForWeek(posts, "2026-W01");
|
|
|
|
assert.equal(week1Posts.length, 2);
|
|
assert.ok(week1Posts.some(p => p.id === "post1"));
|
|
assert.ok(week1Posts.some(p => p.id === "post2"));
|
|
});
|
|
});
|
|
|
|
describe("generateWeeklyReport", () => {
|
|
test("should handle no posts", () => {
|
|
tempDir = setupTempDir();
|
|
|
|
const report = generateWeeklyReport(tempDir, "2026-W03");
|
|
|
|
assert.equal(report.week, "2026-W03");
|
|
assert.equal(report.summary.totalPosts, 0);
|
|
assert.equal(report.summary.totalImpressions, 0);
|
|
assert.equal(report.summary.avgEngagementRate, 0);
|
|
assert.deepEqual(report.topPerformers, []);
|
|
assert.deepEqual(report.underperformers, []);
|
|
});
|
|
|
|
test("should calculate correct summary metrics", () => {
|
|
tempDir = setupTempDir();
|
|
|
|
const posts: PostAnalytics[] = [
|
|
createTestPost({
|
|
id: "post1",
|
|
publishedDate: "2026-01-12", // 2026-W03
|
|
metrics: {
|
|
impressions: 1000,
|
|
reactions: 50,
|
|
comments: 10,
|
|
shares: 5,
|
|
clicks: 20,
|
|
engagementRate: 8.5,
|
|
},
|
|
}),
|
|
createTestPost({
|
|
id: "post2",
|
|
publishedDate: "2026-01-13", // 2026-W03
|
|
metrics: {
|
|
impressions: 2000,
|
|
reactions: 100,
|
|
comments: 20,
|
|
shares: 10,
|
|
clicks: 40,
|
|
engagementRate: 8.5,
|
|
},
|
|
}),
|
|
createTestPost({
|
|
id: "post3",
|
|
publishedDate: "2026-01-14", // 2026-W03
|
|
metrics: {
|
|
impressions: 1500,
|
|
reactions: 75,
|
|
comments: 15,
|
|
shares: 7,
|
|
clicks: 30,
|
|
engagementRate: 8.47,
|
|
},
|
|
}),
|
|
];
|
|
|
|
const batch = createTestBatch({
|
|
dateRange: { from: "2026-01-12", to: "2026-01-14" },
|
|
posts,
|
|
});
|
|
|
|
saveBatch(tempDir, batch);
|
|
|
|
const report = generateWeeklyReport(tempDir, "2026-W03");
|
|
|
|
assert.equal(report.week, "2026-W03");
|
|
assert.equal(report.summary.totalPosts, 3);
|
|
assert.equal(report.summary.totalImpressions, 4500);
|
|
assert.equal(report.summary.totalReactions, 225);
|
|
assert.equal(report.summary.totalComments, 45);
|
|
assert.equal(report.summary.totalShares, 22);
|
|
assert.equal(report.summary.totalClicks, 90);
|
|
assert.equal(report.summary.avgImpressionsPerPost, 1500);
|
|
|
|
// Average engagement rate: (8.5 + 8.5 + 8.47) / 3 ≈ 8.49
|
|
assert.ok(Math.abs(report.summary.avgEngagementRate - 8.49) < 0.01);
|
|
});
|
|
|
|
test("should identify top performers and underperformers", () => {
|
|
tempDir = setupTempDir();
|
|
|
|
const posts: PostAnalytics[] = [
|
|
createTestPost({
|
|
id: "high1",
|
|
publishedDate: "2026-01-12", // 2026-W03
|
|
metrics: {
|
|
impressions: 1000,
|
|
reactions: 50,
|
|
comments: 10,
|
|
shares: 5,
|
|
clicks: 20,
|
|
engagementRate: 10.0,
|
|
},
|
|
}),
|
|
createTestPost({
|
|
id: "high2",
|
|
publishedDate: "2026-01-13", // 2026-W03
|
|
metrics: {
|
|
impressions: 1000,
|
|
reactions: 50,
|
|
comments: 10,
|
|
shares: 5,
|
|
clicks: 20,
|
|
engagementRate: 9.0,
|
|
},
|
|
}),
|
|
createTestPost({
|
|
id: "medium",
|
|
publishedDate: "2026-01-13", // 2026-W03
|
|
metrics: {
|
|
impressions: 1000,
|
|
reactions: 50,
|
|
comments: 10,
|
|
shares: 5,
|
|
clicks: 20,
|
|
engagementRate: 5.0,
|
|
},
|
|
}),
|
|
createTestPost({
|
|
id: "low1",
|
|
publishedDate: "2026-01-14", // 2026-W03
|
|
metrics: {
|
|
impressions: 1000,
|
|
reactions: 50,
|
|
comments: 10,
|
|
shares: 5,
|
|
clicks: 20,
|
|
engagementRate: 3.0,
|
|
},
|
|
}),
|
|
createTestPost({
|
|
id: "low2",
|
|
publishedDate: "2026-01-14", // 2026-W03
|
|
metrics: {
|
|
impressions: 1000,
|
|
reactions: 50,
|
|
comments: 10,
|
|
shares: 5,
|
|
clicks: 20,
|
|
engagementRate: 2.0,
|
|
},
|
|
}),
|
|
];
|
|
|
|
const batch = createTestBatch({
|
|
dateRange: { from: "2026-01-12", to: "2026-01-14" },
|
|
posts,
|
|
});
|
|
|
|
saveBatch(tempDir, batch);
|
|
|
|
const report = generateWeeklyReport(tempDir, "2026-W03");
|
|
|
|
// Top 3 performers (highest engagement)
|
|
assert.equal(report.topPerformers.length, 3);
|
|
assert.equal(report.topPerformers[0].id, "high1"); // 10.0
|
|
assert.equal(report.topPerformers[1].id, "high2"); // 9.0
|
|
assert.equal(report.topPerformers[2].id, "medium"); // 5.0
|
|
|
|
// Bottom 3 underperformers (lowest engagement)
|
|
assert.equal(report.underperformers.length, 3);
|
|
assert.equal(report.underperformers[0].id, "low2"); // 2.0
|
|
assert.equal(report.underperformers[1].id, "low1"); // 3.0
|
|
assert.equal(report.underperformers[2].id, "medium"); // 5.0
|
|
});
|
|
|
|
test("should handle fewer than 3 posts", () => {
|
|
tempDir = setupTempDir();
|
|
|
|
const posts: PostAnalytics[] = [
|
|
createTestPost({
|
|
id: "post1",
|
|
publishedDate: "2026-01-12", // 2026-W03
|
|
metrics: {
|
|
impressions: 1000,
|
|
reactions: 50,
|
|
comments: 10,
|
|
shares: 5,
|
|
clicks: 20,
|
|
engagementRate: 10.0,
|
|
},
|
|
}),
|
|
createTestPost({
|
|
id: "post2",
|
|
publishedDate: "2026-01-13", // 2026-W03
|
|
metrics: {
|
|
impressions: 1000,
|
|
reactions: 50,
|
|
comments: 10,
|
|
shares: 5,
|
|
clicks: 20,
|
|
engagementRate: 5.0,
|
|
},
|
|
}),
|
|
];
|
|
|
|
const batch = createTestBatch({
|
|
dateRange: { from: "2026-01-12", to: "2026-01-13" },
|
|
posts,
|
|
});
|
|
|
|
saveBatch(tempDir, batch);
|
|
|
|
const report = generateWeeklyReport(tempDir, "2026-W03");
|
|
|
|
// Should have 2 top performers
|
|
assert.equal(report.topPerformers.length, 2);
|
|
// Should have 2 underperformers (same posts, reversed)
|
|
assert.equal(report.underperformers.length, 2);
|
|
});
|
|
|
|
test("should calculate trends when previous week data exists", () => {
|
|
tempDir = setupTempDir();
|
|
|
|
// Create previous week data
|
|
const prevWeekPosts: PostAnalytics[] = [
|
|
createTestPost({
|
|
id: "prev1",
|
|
publishedDate: "2026-01-05", // 2026-W02
|
|
metrics: {
|
|
impressions: 1000,
|
|
reactions: 50,
|
|
comments: 10,
|
|
shares: 5,
|
|
clicks: 20,
|
|
engagementRate: 8.5,
|
|
},
|
|
}),
|
|
createTestPost({
|
|
id: "prev2",
|
|
publishedDate: "2026-01-06", // 2026-W02
|
|
metrics: {
|
|
impressions: 1000,
|
|
reactions: 50,
|
|
comments: 10,
|
|
shares: 5,
|
|
clicks: 20,
|
|
engagementRate: 8.5,
|
|
},
|
|
}),
|
|
];
|
|
|
|
const prevBatch = createTestBatch({
|
|
dateRange: { from: "2026-01-05", to: "2026-01-06" },
|
|
posts: prevWeekPosts,
|
|
});
|
|
|
|
saveBatch(tempDir, prevBatch);
|
|
|
|
// Generate previous week report
|
|
generateWeeklyReport(tempDir, "2026-W02");
|
|
|
|
// Create current week data with higher metrics
|
|
const currentWeekPosts: PostAnalytics[] = [
|
|
createTestPost({
|
|
id: "curr1",
|
|
publishedDate: "2026-01-12", // 2026-W03
|
|
metrics: {
|
|
impressions: 1500,
|
|
reactions: 75,
|
|
comments: 15,
|
|
shares: 7,
|
|
clicks: 30,
|
|
engagementRate: 8.47,
|
|
},
|
|
}),
|
|
createTestPost({
|
|
id: "curr2",
|
|
publishedDate: "2026-01-13", // 2026-W03
|
|
metrics: {
|
|
impressions: 1500,
|
|
reactions: 75,
|
|
comments: 15,
|
|
shares: 7,
|
|
clicks: 30,
|
|
engagementRate: 8.47,
|
|
},
|
|
}),
|
|
];
|
|
|
|
const currentBatch = createTestBatch({
|
|
dateRange: { from: "2026-01-12", to: "2026-01-13" },
|
|
posts: currentWeekPosts,
|
|
});
|
|
|
|
saveBatch(tempDir, currentBatch);
|
|
|
|
const report = generateWeeklyReport(tempDir, "2026-W03");
|
|
|
|
// Current impressions: 3000, Previous: 2000 → 50% increase
|
|
assert.equal(report.trends.comparedTo, "2026-W02");
|
|
assert.equal(report.trends.impressionsTrend, "up");
|
|
assert.equal(report.trends.percentChange.impressions, 50);
|
|
|
|
// Engagement rate essentially the same
|
|
assert.equal(report.trends.engagementTrend, "stable");
|
|
});
|
|
|
|
test("should default to stable trends when no previous week data", () => {
|
|
tempDir = setupTempDir();
|
|
|
|
const posts: PostAnalytics[] = [
|
|
createTestPost({
|
|
id: "post1",
|
|
publishedDate: "2026-01-12", // 2026-W03
|
|
}),
|
|
];
|
|
|
|
const batch = createTestBatch({
|
|
dateRange: { from: "2026-01-12", to: "2026-01-12" },
|
|
posts,
|
|
});
|
|
|
|
saveBatch(tempDir, batch);
|
|
|
|
const report = generateWeeklyReport(tempDir, "2026-W03");
|
|
|
|
assert.equal(report.trends.impressionsTrend, "stable");
|
|
assert.equal(report.trends.engagementTrend, "stable");
|
|
assert.equal(report.trends.percentChange.impressions, 0);
|
|
assert.equal(report.trends.percentChange.engagement, 0);
|
|
});
|
|
|
|
test("should filter posts correctly for target week", () => {
|
|
tempDir = setupTempDir();
|
|
|
|
const posts: PostAnalytics[] = [
|
|
createTestPost({
|
|
id: "w02-post",
|
|
publishedDate: "2026-01-05", // 2026-W02
|
|
}),
|
|
createTestPost({
|
|
id: "w03-post1",
|
|
publishedDate: "2026-01-12", // 2026-W03
|
|
}),
|
|
createTestPost({
|
|
id: "w03-post2",
|
|
publishedDate: "2026-01-13", // 2026-W03
|
|
}),
|
|
createTestPost({
|
|
id: "w04-post",
|
|
publishedDate: "2026-01-19", // 2026-W04
|
|
}),
|
|
];
|
|
|
|
const batch = createTestBatch({
|
|
dateRange: { from: "2026-01-05", to: "2026-01-19" },
|
|
posts,
|
|
});
|
|
|
|
saveBatch(tempDir, batch);
|
|
|
|
const report = generateWeeklyReport(tempDir, "2026-W03");
|
|
|
|
// Should only include W03 posts
|
|
assert.equal(report.summary.totalPosts, 2);
|
|
assert.equal(report.topPerformers.length, 2);
|
|
assert.ok(report.topPerformers.some(p => p.id === "w03-post1"));
|
|
assert.ok(report.topPerformers.some(p => p.id === "w03-post2"));
|
|
});
|
|
|
|
test("should use current week if week parameter not provided", () => {
|
|
tempDir = setupTempDir();
|
|
|
|
const report = generateWeeklyReport(tempDir);
|
|
|
|
// Should match current ISO week format
|
|
assert.match(report.week, /^\d{4}-W\d{2}$/);
|
|
});
|
|
});
|
|
});
|