ktg-plugin-marketplace/plugins/linkedin-thought-leadership/scripts/analytics/tests/storage.test.ts
Kjell Tore Guttormsen 39f8b275a6 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>
2026-04-07 22:09:03 +02:00

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");
});
});
});