- Add/remove competitor handles (public Business/Creator accounts); refreshed nightly, one snapshot a day - Compare with the client: followers, 30-day change, trend, posts/week, typical post (median), engagement rate, format mix - Engagement-rate chart and each competitor's top recent posts - MCP tools competitors and competitor_add (audits can add the AI's suggestions) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
34 lines
1.9 KiB
JavaScript
34 lines
1.9 KiB
JavaScript
// Competitor comparison maths, offline (no Instagram calls): median engagement, rate, growth, format mix, top posts.
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "quorla-comp-"));
|
|
process.env.QUORLA_COMPETITORS_FILE = path.join(TMP, "competitors.json");
|
|
const { PROJECT_ROOT } = await import("../src/env.js");
|
|
const client = fs.readdirSync(path.join(PROJECT_ROOT, "clients")).find((d) => !d.startsWith("_") && fs.existsSync(path.join(PROJECT_ROOT, "clients", d, "client.json")));
|
|
|
|
const day = (n) => new Date(Date.now() - n * 86400000);
|
|
const media = [5, 10, 20, 40, 1000].map((likes, i) => ({ id: `m${i}`, at: day(i * 3).toISOString(), likes, comments: 0, type: i % 2 ? "Reel" : "Image", permalink: `https://x/${i}` }));
|
|
|
|
test("compare: median engagement ignores one viral post; growth from history", { skip: !client && "no client folder" }, async () => {
|
|
fs.writeFileSync(process.env.QUORLA_COMPETITORS_FILE, JSON.stringify({
|
|
[client]: { accounts: [{ username: "rival", name: "Rival", source: "manual", snapshots: [
|
|
{ date: day(20).toISOString().slice(0, 10), followers: 900 },
|
|
{ date: day(0).toISOString().slice(0, 10), followers: 1000, media },
|
|
] }] },
|
|
}));
|
|
const c = await import("../src/competitors.js");
|
|
const r = c.compare(client, { username: "me", followers: 500, posts: [], history: [] }).competitors[0];
|
|
assert.equal(r.avgEngagement, 20, "median of 5,10,20,40,1000");
|
|
assert.equal(r.engagementRate, 2);
|
|
assert.equal(r.growth30, 100);
|
|
assert.equal(r.postsPerWeek, 1.3, "5 posts in 28 days");
|
|
assert.deepEqual(r.formatMix, { Image: 3, Reel: 2 });
|
|
assert.equal(r.topPosts[0].likes, 1000);
|
|
assert.equal(c.listTracked(client)[0].username, "rival");
|
|
await assert.rejects(c.add(client, "not a handle!!"), /Instagram handle/);
|
|
});
|