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,440 @@
|
|||
import { describe, test, afterEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, rmSync, writeFileSync, existsSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import {
|
||||
ensureDirectories,
|
||||
saveBatch,
|
||||
loadAllBatches,
|
||||
loadAllPosts,
|
||||
listExports,
|
||||
saveWeeklyReport,
|
||||
loadWeeklyReport,
|
||||
loadAllWeeklyReports,
|
||||
} from "../src/utils/storage.js";
|
||||
import type { AnalyticsBatch, PostAnalytics, WeeklyReport } 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,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to create test weekly report
|
||||
function createTestWeeklyReport(overrides?: Partial<WeeklyReport>): WeeklyReport {
|
||||
return {
|
||||
week: overrides?.week || "2026-W03",
|
||||
generatedAt: overrides?.generatedAt || "2026-01-20T10:00:00Z",
|
||||
summary: {
|
||||
totalPosts: 5,
|
||||
totalImpressions: 5000,
|
||||
totalReactions: 250,
|
||||
totalComments: 50,
|
||||
totalShares: 25,
|
||||
totalClicks: 100,
|
||||
avgEngagementRate: 8.5,
|
||||
avgImpressionsPerPost: 1000,
|
||||
...(overrides?.summary || {}),
|
||||
},
|
||||
topPerformers: overrides?.topPerformers || [],
|
||||
underperformers: overrides?.underperformers || [],
|
||||
trends: {
|
||||
impressionsTrend: "up",
|
||||
engagementTrend: "stable",
|
||||
comparedTo: "2026-W02",
|
||||
percentChange: {
|
||||
impressions: 10,
|
||||
engagement: 2,
|
||||
},
|
||||
...(overrides?.trends || {}),
|
||||
},
|
||||
alerts: overrides?.alerts || [],
|
||||
};
|
||||
}
|
||||
|
||||
describe("storage", () => {
|
||||
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("ensureDirectories", () => {
|
||||
test("should create directories", () => {
|
||||
tempDir = setupTempDir();
|
||||
ensureDirectories(tempDir);
|
||||
|
||||
assert.ok(existsSync(join(tempDir, "exports")));
|
||||
assert.ok(existsSync(join(tempDir, "posts")));
|
||||
assert.ok(existsSync(join(tempDir, "weekly-reports")));
|
||||
});
|
||||
|
||||
test("should not fail if directories already exist", () => {
|
||||
tempDir = setupTempDir();
|
||||
ensureDirectories(tempDir);
|
||||
// Call again - should not throw
|
||||
ensureDirectories(tempDir);
|
||||
|
||||
assert.ok(existsSync(join(tempDir, "exports")));
|
||||
assert.ok(existsSync(join(tempDir, "posts")));
|
||||
assert.ok(existsSync(join(tempDir, "weekly-reports")));
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveBatch", () => {
|
||||
test("should write JSON file", () => {
|
||||
tempDir = setupTempDir();
|
||||
const batch = createTestBatch();
|
||||
|
||||
const filename = saveBatch(tempDir, batch);
|
||||
|
||||
assert.ok(filename.startsWith("2026-01-15-"));
|
||||
assert.ok(filename.endsWith(".json"));
|
||||
|
||||
const filepath = join(tempDir, "posts", filename);
|
||||
assert.ok(existsSync(filepath));
|
||||
|
||||
// Verify content
|
||||
const loadedBatches = loadAllBatches(tempDir);
|
||||
assert.equal(loadedBatches.length, 1);
|
||||
assert.equal(loadedBatches[0].batchId, batch.batchId);
|
||||
});
|
||||
|
||||
test("should create directories if they don't exist", () => {
|
||||
tempDir = setupTempDir();
|
||||
const batch = createTestBatch();
|
||||
|
||||
saveBatch(tempDir, batch);
|
||||
|
||||
assert.ok(existsSync(join(tempDir, "posts")));
|
||||
});
|
||||
|
||||
test("should use short batch ID in filename", () => {
|
||||
tempDir = setupTempDir();
|
||||
const batch = createTestBatch({
|
||||
batchId: "abcdef12-3456-7890-abcd-ef1234567890",
|
||||
dateRange: { from: "2026-01-15", to: "2026-01-20" },
|
||||
});
|
||||
|
||||
const filename = saveBatch(tempDir, batch);
|
||||
|
||||
assert.ok(filename.startsWith("2026-01-15-abcdef12"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadAllBatches", () => {
|
||||
test("should load saved batches", () => {
|
||||
tempDir = setupTempDir();
|
||||
|
||||
const batch1 = createTestBatch({
|
||||
batchId: "batch1",
|
||||
importedAt: "2026-01-20T10:00:00Z",
|
||||
});
|
||||
const batch2 = createTestBatch({
|
||||
batchId: "batch2",
|
||||
importedAt: "2026-01-21T10:00:00Z",
|
||||
dateRange: { from: "2026-01-21", to: "2026-01-21" },
|
||||
});
|
||||
|
||||
saveBatch(tempDir, batch1);
|
||||
saveBatch(tempDir, batch2);
|
||||
|
||||
const batches = loadAllBatches(tempDir);
|
||||
|
||||
assert.equal(batches.length, 2);
|
||||
assert.equal(batches[0].batchId, "batch1");
|
||||
assert.equal(batches[1].batchId, "batch2");
|
||||
});
|
||||
|
||||
test("should return empty array if posts directory doesn't exist", () => {
|
||||
tempDir = setupTempDir();
|
||||
|
||||
const batches = loadAllBatches(tempDir);
|
||||
|
||||
assert.deepEqual(batches, []);
|
||||
});
|
||||
|
||||
test("should sort by importedAt timestamp", () => {
|
||||
tempDir = setupTempDir();
|
||||
|
||||
const batch1 = createTestBatch({
|
||||
batchId: "batch1",
|
||||
importedAt: "2026-01-22T10:00:00Z",
|
||||
dateRange: { from: "2026-01-22", to: "2026-01-22" },
|
||||
});
|
||||
const batch2 = createTestBatch({
|
||||
batchId: "batch2",
|
||||
importedAt: "2026-01-20T10:00:00Z",
|
||||
});
|
||||
const batch3 = createTestBatch({
|
||||
batchId: "batch3",
|
||||
importedAt: "2026-01-21T10:00:00Z",
|
||||
dateRange: { from: "2026-01-21", to: "2026-01-21" },
|
||||
});
|
||||
|
||||
saveBatch(tempDir, batch1);
|
||||
saveBatch(tempDir, batch2);
|
||||
saveBatch(tempDir, batch3);
|
||||
|
||||
const batches = loadAllBatches(tempDir);
|
||||
|
||||
assert.equal(batches.length, 3);
|
||||
assert.equal(batches[0].batchId, "batch2"); // Earliest
|
||||
assert.equal(batches[1].batchId, "batch3");
|
||||
assert.equal(batches[2].batchId, "batch1"); // Latest
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadAllPosts", () => {
|
||||
test("should deduplicate by post ID", () => {
|
||||
tempDir = setupTempDir();
|
||||
|
||||
const post1 = createTestPost({
|
||||
id: "post1",
|
||||
title: "Old version",
|
||||
publishedDate: "2026-01-15",
|
||||
});
|
||||
const post1Updated = createTestPost({
|
||||
id: "post1",
|
||||
title: "New version",
|
||||
publishedDate: "2026-01-15",
|
||||
});
|
||||
const post2 = createTestPost({
|
||||
id: "post2",
|
||||
publishedDate: "2026-01-16",
|
||||
});
|
||||
|
||||
const batch1 = createTestBatch({
|
||||
batchId: "batch1",
|
||||
importedAt: "2026-01-20T10:00:00Z",
|
||||
posts: [post1, post2],
|
||||
});
|
||||
const batch2 = createTestBatch({
|
||||
batchId: "batch2",
|
||||
importedAt: "2026-01-21T10:00:00Z",
|
||||
dateRange: { from: "2026-01-21", to: "2026-01-21" },
|
||||
posts: [post1Updated], // Later import of post1
|
||||
});
|
||||
|
||||
saveBatch(tempDir, batch1);
|
||||
saveBatch(tempDir, batch2);
|
||||
|
||||
const posts = loadAllPosts(tempDir);
|
||||
|
||||
assert.equal(posts.length, 2);
|
||||
// Should have the updated version of post1
|
||||
const foundPost1 = posts.find(p => p.id === "post1");
|
||||
assert.equal(foundPost1?.title, "New version");
|
||||
});
|
||||
|
||||
test("should sort by publishedDate descending", () => {
|
||||
tempDir = setupTempDir();
|
||||
|
||||
const post1 = createTestPost({
|
||||
id: "post1",
|
||||
publishedDate: "2026-01-15",
|
||||
});
|
||||
const post2 = createTestPost({
|
||||
id: "post2",
|
||||
publishedDate: "2026-01-17",
|
||||
});
|
||||
const post3 = createTestPost({
|
||||
id: "post3",
|
||||
publishedDate: "2026-01-16",
|
||||
});
|
||||
|
||||
const batch = createTestBatch({
|
||||
posts: [post1, post2, post3],
|
||||
});
|
||||
|
||||
saveBatch(tempDir, batch);
|
||||
|
||||
const posts = loadAllPosts(tempDir);
|
||||
|
||||
assert.equal(posts.length, 3);
|
||||
assert.equal(posts[0].id, "post2"); // 2026-01-17
|
||||
assert.equal(posts[1].id, "post3"); // 2026-01-16
|
||||
assert.equal(posts[2].id, "post1"); // 2026-01-15
|
||||
});
|
||||
|
||||
test("should return empty array if no batches exist", () => {
|
||||
tempDir = setupTempDir();
|
||||
|
||||
const posts = loadAllPosts(tempDir);
|
||||
|
||||
assert.deepEqual(posts, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe("listExports", () => {
|
||||
test("should list CSV files", () => {
|
||||
tempDir = setupTempDir();
|
||||
ensureDirectories(tempDir);
|
||||
|
||||
const exportsDir = join(tempDir, "exports");
|
||||
writeFileSync(join(exportsDir, "export1.csv"), "data");
|
||||
writeFileSync(join(exportsDir, "export2.csv"), "data");
|
||||
writeFileSync(join(exportsDir, "other.txt"), "data"); // Non-CSV
|
||||
|
||||
const exports = listExports(tempDir);
|
||||
|
||||
assert.equal(exports.length, 2);
|
||||
assert.ok(exports.includes("export1.csv"));
|
||||
assert.ok(exports.includes("export2.csv"));
|
||||
});
|
||||
|
||||
test("should return empty for missing directory", () => {
|
||||
tempDir = setupTempDir();
|
||||
|
||||
const exports = listExports(tempDir);
|
||||
|
||||
assert.deepEqual(exports, []);
|
||||
});
|
||||
|
||||
test("should return sorted list", () => {
|
||||
tempDir = setupTempDir();
|
||||
ensureDirectories(tempDir);
|
||||
|
||||
const exportsDir = join(tempDir, "exports");
|
||||
writeFileSync(join(exportsDir, "c.csv"), "data");
|
||||
writeFileSync(join(exportsDir, "a.csv"), "data");
|
||||
writeFileSync(join(exportsDir, "b.csv"), "data");
|
||||
|
||||
const exports = listExports(tempDir);
|
||||
|
||||
assert.deepEqual(exports, ["a.csv", "b.csv", "c.csv"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveWeeklyReport", () => {
|
||||
test("should write report JSON", () => {
|
||||
tempDir = setupTempDir();
|
||||
const report = createTestWeeklyReport({ week: "2026-W03" });
|
||||
|
||||
const filename = saveWeeklyReport(tempDir, report);
|
||||
|
||||
assert.equal(filename, "2026-W03.json");
|
||||
|
||||
const filepath = join(tempDir, "weekly-reports", filename);
|
||||
assert.ok(existsSync(filepath));
|
||||
|
||||
// Verify content
|
||||
const loaded = loadWeeklyReport(tempDir, "2026-W03");
|
||||
assert.ok(loaded);
|
||||
assert.equal(loaded.week, "2026-W03");
|
||||
assert.equal(loaded.summary.totalPosts, 5);
|
||||
});
|
||||
|
||||
test("should create directories if they don't exist", () => {
|
||||
tempDir = setupTempDir();
|
||||
const report = createTestWeeklyReport();
|
||||
|
||||
saveWeeklyReport(tempDir, report);
|
||||
|
||||
assert.ok(existsSync(join(tempDir, "weekly-reports")));
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadWeeklyReport", () => {
|
||||
test("should return null for missing report", () => {
|
||||
tempDir = setupTempDir();
|
||||
|
||||
const report = loadWeeklyReport(tempDir, "2026-W99");
|
||||
|
||||
assert.equal(report, null);
|
||||
});
|
||||
|
||||
test("should load existing report", () => {
|
||||
tempDir = setupTempDir();
|
||||
const report = createTestWeeklyReport({ week: "2026-W03" });
|
||||
|
||||
saveWeeklyReport(tempDir, report);
|
||||
|
||||
const loaded = loadWeeklyReport(tempDir, "2026-W03");
|
||||
|
||||
assert.ok(loaded);
|
||||
assert.equal(loaded.week, "2026-W03");
|
||||
assert.equal(loaded.summary.totalPosts, 5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadAllWeeklyReports", () => {
|
||||
test("should load all reports sorted", () => {
|
||||
tempDir = setupTempDir();
|
||||
|
||||
const report1 = createTestWeeklyReport({ week: "2026-W03" });
|
||||
const report2 = createTestWeeklyReport({ week: "2026-W01" });
|
||||
const report3 = createTestWeeklyReport({ week: "2026-W05" });
|
||||
|
||||
saveWeeklyReport(tempDir, report1);
|
||||
saveWeeklyReport(tempDir, report2);
|
||||
saveWeeklyReport(tempDir, report3);
|
||||
|
||||
const reports = loadAllWeeklyReports(tempDir);
|
||||
|
||||
assert.equal(reports.length, 3);
|
||||
assert.equal(reports[0].week, "2026-W05"); // Newest first
|
||||
assert.equal(reports[1].week, "2026-W03");
|
||||
assert.equal(reports[2].week, "2026-W01");
|
||||
});
|
||||
|
||||
test("should return empty array if directory doesn't exist", () => {
|
||||
tempDir = setupTempDir();
|
||||
|
||||
const reports = loadAllWeeklyReports(tempDir);
|
||||
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
|
||||
test("should ignore non-JSON files", () => {
|
||||
tempDir = setupTempDir();
|
||||
ensureDirectories(tempDir);
|
||||
|
||||
const reportsDir = join(tempDir, "weekly-reports");
|
||||
const report = createTestWeeklyReport({ week: "2026-W03" });
|
||||
saveWeeklyReport(tempDir, report);
|
||||
writeFileSync(join(reportsDir, "readme.txt"), "data");
|
||||
|
||||
const reports = loadAllWeeklyReports(tempDir);
|
||||
|
||||
assert.equal(reports.length, 1);
|
||||
assert.equal(reports[0].week, "2026-W03");
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue