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,124 @@
|
|||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { parseLinkedInCSV } from "../src/parsers/csv-parser.js";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const fixturesDir = join(__dirname, "fixtures");
|
||||
|
||||
describe("CSV Parser", () => {
|
||||
it("should parse standard CSV export", () => {
|
||||
const filePath = join(fixturesDir, "sample-export.csv");
|
||||
const batch = parseLinkedInCSV(filePath, "sample-export.csv");
|
||||
|
||||
assert.equal(batch.postCount, 8, "Should have 8 posts");
|
||||
assert.equal(batch.posts.length, 8, "Posts array should have 8 items");
|
||||
assert.equal(batch.exportFilename, "sample-export.csv");
|
||||
assert.ok(batch.batchId, "Should have a batchId");
|
||||
assert.ok(batch.importedAt, "Should have importedAt timestamp");
|
||||
|
||||
// Check first post
|
||||
const firstPost = batch.posts[0];
|
||||
assert.ok(firstPost.id, "Post should have an ID");
|
||||
assert.ok(
|
||||
firstPost.title.includes("uncomfortable truth"),
|
||||
"Title should match"
|
||||
);
|
||||
assert.equal(firstPost.publishedDate, "2026-01-28");
|
||||
assert.equal(firstPost.metrics.impressions, 4523);
|
||||
assert.equal(firstPost.metrics.reactions, 87);
|
||||
assert.equal(firstPost.metrics.comments, 23);
|
||||
assert.equal(firstPost.metrics.shares, 12);
|
||||
assert.equal(firstPost.metrics.clicks, 156);
|
||||
assert.ok(firstPost.metrics.engagementRate > 0, "Should have engagement rate");
|
||||
});
|
||||
|
||||
it("should handle European format", () => {
|
||||
const filePath = join(fixturesDir, "european-export.csv");
|
||||
const batch = parseLinkedInCSV(filePath, "european-export.csv");
|
||||
|
||||
assert.equal(batch.postCount, 2, "Should have 2 posts");
|
||||
|
||||
// Check that European number format is parsed correctly
|
||||
const firstPost = batch.posts[0];
|
||||
assert.equal(firstPost.metrics.impressions, 4523, "Should parse 4.523 as 4523");
|
||||
assert.equal(firstPost.publishedDate, "2026-01-28", "Should normalize date from DD.MM.YYYY");
|
||||
|
||||
const secondPost = batch.posts[1];
|
||||
assert.equal(secondPost.metrics.impressions, 2891, "Should parse 2.891 as 2891");
|
||||
assert.equal(secondPost.publishedDate, "2026-01-26", "Should normalize date from DD.MM.YYYY");
|
||||
});
|
||||
|
||||
it("should handle empty CSV", () => {
|
||||
const filePath = join(fixturesDir, "empty-export.csv");
|
||||
const batch = parseLinkedInCSV(filePath, "empty-export.csv");
|
||||
|
||||
assert.equal(batch.postCount, 0, "Should have 0 posts");
|
||||
assert.equal(batch.posts.length, 0, "Posts array should be empty");
|
||||
assert.equal(batch.dateRange.from, "", "Date range from should be empty");
|
||||
assert.equal(batch.dateRange.to, "", "Date range to should be empty");
|
||||
});
|
||||
|
||||
it("should handle BOM", () => {
|
||||
const filePath = join(fixturesDir, "bom-export.csv");
|
||||
const batch = parseLinkedInCSV(filePath, "bom-export.csv");
|
||||
|
||||
assert.equal(batch.postCount, 8, "Should parse BOM file correctly");
|
||||
assert.ok(
|
||||
batch.posts[0].title.includes("uncomfortable truth"),
|
||||
"Should parse first post correctly despite BOM"
|
||||
);
|
||||
});
|
||||
|
||||
it("should calculate engagement rate", () => {
|
||||
const filePath = join(fixturesDir, "sample-export.csv");
|
||||
const batch = parseLinkedInCSV(filePath, "sample-export.csv");
|
||||
|
||||
const firstPost = batch.posts[0];
|
||||
// (87+23+12+156)/4523 * 100 = 6.14...
|
||||
const expectedRate = ((87 + 23 + 12 + 156) / 4523) * 100;
|
||||
assert.ok(
|
||||
Math.abs(firstPost.metrics.engagementRate - expectedRate) < 0.01,
|
||||
`Engagement rate should be ~${expectedRate}, got ${firstPost.metrics.engagementRate}`
|
||||
);
|
||||
});
|
||||
|
||||
it("should generate deterministic post IDs", () => {
|
||||
const filePath = join(fixturesDir, "sample-export.csv");
|
||||
const batch1 = parseLinkedInCSV(filePath, "sample-export.csv");
|
||||
const batch2 = parseLinkedInCSV(filePath, "sample-export.csv");
|
||||
|
||||
// Same post should have same ID
|
||||
assert.equal(
|
||||
batch1.posts[0].id,
|
||||
batch2.posts[0].id,
|
||||
"Same post should generate same ID"
|
||||
);
|
||||
|
||||
// Different posts should have different IDs
|
||||
assert.notEqual(
|
||||
batch1.posts[0].id,
|
||||
batch1.posts[1].id,
|
||||
"Different posts should have different IDs"
|
||||
);
|
||||
});
|
||||
|
||||
it("should normalize dates to YYYY-MM-DD", () => {
|
||||
const filePath = join(fixturesDir, "sample-export.csv");
|
||||
const batch = parseLinkedInCSV(filePath, "sample-export.csv");
|
||||
|
||||
// All dates should be in YYYY-MM-DD format
|
||||
batch.posts.forEach((post) => {
|
||||
assert.match(
|
||||
post.publishedDate,
|
||||
/^\d{4}-\d{2}-\d{2}$/,
|
||||
`Date ${post.publishedDate} should be in YYYY-MM-DD format`
|
||||
);
|
||||
});
|
||||
|
||||
// Check date range
|
||||
assert.equal(batch.dateRange.from, "2026-01-13", "Date range from should be earliest date");
|
||||
assert.equal(batch.dateRange.to, "2026-01-28", "Date range to should be latest date");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue