feat(linkedin-thought-leadership): v1.0.0 — initial open-source import
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>
This commit is contained in:
parent
7194a37129
commit
39f8b275a6
143 changed files with 32662 additions and 0 deletions
|
|
@ -0,0 +1,570 @@
|
|||
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}$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue