import { describe, test, afterEach } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { calculateScore } from '../personalization-score.mjs'; // M0-7: the scorer now reads INSTANCE data from the EXTERNAL data root // (getDataRoot, stubbed via LINKEDIN_STUDIO_DATA), mirroring the migration's // dests, and falls back to the plugin tree only for back-compat. The placeholder // sentinel still keys voice detection (Step-5 remediation): the placeholder earns // 0 of the 25 voice points; a populated profile (sentinel removed) earns them. const SENTINEL = ''; // A >50-line body with NEITHER the sentinel NOR `[Your Name]`, so the sentinel is // the only thing distinguishing placeholder from real profile. const LONG_BODY = Array.from({ length: 60 }, (_, i) => `- voice characteristic line ${i + 1}`).join('\n'); function makeRoots() { const dataDir = mkdtempSync(join(tmpdir(), 'lis-score-data-')); const pluginRoot = mkdtempSync(join(tmpdir(), 'lis-score-plugin-')); mkdirSync(join(dataDir, 'voice-samples'), { recursive: true }); return { dataDir, pluginRoot }; } function writeExternalVoice(dataDir, content) { writeFileSync(join(dataDir, 'voice-samples', 'authentic-voice-samples.md'), content, 'utf-8'); } describe('calculateScore — reads external instance data (M0-7)', () => { let dataDir, pluginRoot; const saved = { LINKEDIN_STUDIO_DATA: process.env.LINKEDIN_STUDIO_DATA }; afterEach(() => { for (const d of [dataDir, pluginRoot]) { if (d && existsSync(d)) rmSync(d, { recursive: true, force: true }); } dataDir = pluginRoot = undefined; if (saved.LINKEDIN_STUDIO_DATA === undefined) delete process.env.LINKEDIN_STUDIO_DATA; else process.env.LINKEDIN_STUDIO_DATA = saved.LINKEDIN_STUDIO_DATA; }); test('external placeholder with the sentinel scores 0 voice points (even >50 lines, no [Your Name])', () => { ({ dataDir, pluginRoot } = makeRoots()); process.env.LINKEDIN_STUDIO_DATA = dataDir; writeExternalVoice(dataDir, `# Voice Profile (placeholder)\n${SENTINEL}\n\n${LONG_BODY}\n`); const { score, personalized } = calculateScore(pluginRoot); assert.equal(score, 0, 'placeholder must not earn the 25 voice points'); assert.equal(personalized, 0); }); test('a real EXTERNAL profile (no sentinel, >50 lines) earns the 25 voice points', () => { ({ dataDir, pluginRoot } = makeRoots()); process.env.LINKEDIN_STUDIO_DATA = dataDir; writeExternalVoice(dataDir, `# My Voice Profile\n\n${LONG_BODY}\n`); const { score, personalized } = calculateScore(pluginRoot); assert.equal(score, 25, 'a populated external profile must earn the 25 voice points'); assert.equal(personalized, 1); }); test('profile absent at external profile/user-profile.md → 0, no crash (graceful degradation)', () => { ({ dataDir, pluginRoot } = makeRoots()); process.env.LINKEDIN_STUDIO_DATA = dataDir; // nothing seeded — no voice, no profile, no scaffolds anywhere const { score, personalized } = calculateScore(pluginRoot); assert.equal(score, 0); assert.equal(personalized, 0); }); test('a short external placeholder (<50 lines) also scores 0 voice points', () => { ({ dataDir, pluginRoot } = makeRoots()); process.env.LINKEDIN_STUDIO_DATA = dataDir; writeExternalVoice(dataDir, `# Voice Profile (placeholder)\n${SENTINEL}\n`); const { score } = calculateScore(pluginRoot); assert.equal(score, 0); }); }); describe('calculateScore — scaffold categories read the external instance (M0-13)', () => { let dataDir, pluginRoot; const saved = { LINKEDIN_STUDIO_DATA: process.env.LINKEDIN_STUDIO_DATA }; afterEach(() => { for (const d of [dataDir, pluginRoot]) { if (d && existsSync(d)) rmSync(d, { recursive: true, force: true }); } dataDir = pluginRoot = undefined; if (saved.LINKEDIN_STUDIO_DATA === undefined) delete process.env.LINKEDIN_STUDIO_DATA; else process.env.LINKEDIN_STUDIO_DATA = saved.LINKEDIN_STUDIO_DATA; }); test('a populated external high-engagement-posts instance earns the 10 points', () => { ({ dataDir, pluginRoot } = makeRoots()); process.env.LINKEDIN_STUDIO_DATA = dataDir; mkdirSync(join(dataDir, 'examples'), { recursive: true }); const posts = ['## Post 1', '## Post 2', '## Post 3'].join('\n\n'); writeFileSync(join(dataDir, 'examples', 'high-engagement-posts.md'), `# Posts\n\n${posts}\n`, 'utf-8'); const { score, personalized } = calculateScore(pluginRoot); assert.equal(score, 10, '3+ saved posts in the external instance earn the 10 points'); assert.equal(personalized, 1); }); test('the generic placeholder seed (no line-leading ## Post N) scores 0 — no crash', () => { ({ dataDir, pluginRoot } = makeRoots()); process.env.LINKEDIN_STUDIO_DATA = dataDir; mkdirSync(join(dataDir, 'examples'), { recursive: true }); writeFileSync(join(dataDir, 'examples', 'high-engagement-posts.md'), `# Posts\n\nPlaceholder — add a ## Post N section per saved post.\n`, 'utf-8'); const { score, personalized } = calculateScore(pluginRoot); assert.equal(score, 0, 'a placeholder with no ## Post [0-9] section scores 0'); assert.equal(personalized, 0); }); test('scaffold absent at the external root → 0, no crash (graceful degradation)', () => { ({ dataDir, pluginRoot } = makeRoots()); process.env.LINKEDIN_STUDIO_DATA = dataDir; // examples/ dir never created — the category is simply skipped, never throws const { score, personalized } = calculateScore(pluginRoot); assert.equal(score, 0); assert.equal(personalized, 0); }); });