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>
440 lines
13 KiB
TypeScript
440 lines
13 KiB
TypeScript
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");
|
|
});
|
|
});
|
|
});
|