import { describe, test, afterEach } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync, statSync, } from 'node:fs'; import { join, dirname } from 'node:path'; import { tmpdir } from 'node:os'; import { migrateData } from '../migrate-data.mjs'; function writeDeep(p, content) { mkdirSync(dirname(p), { recursive: true }); writeFileSync(p, content); } describe('migrateData', () => { let pluginRoot; let dataRoot; const savedData = process.env.LINKEDIN_STUDIO_DATA; afterEach(() => { for (const d of [pluginRoot, dataRoot]) { if (d && existsSync(d)) rmSync(d, { recursive: true, force: true }); } pluginRoot = undefined; dataRoot = undefined; if (savedData === undefined) delete process.env.LINKEDIN_STUDIO_DATA; else process.env.LINKEDIN_STUDIO_DATA = savedData; }); function setup({ withRuntime = false, withScaffolds = false } = {}) { pluginRoot = mkdtempSync(join(tmpdir(), 'lis-plugin-')); dataRoot = mkdtempSync(join(tmpdir(), 'lis-data-')); process.env.LINKEDIN_STUDIO_DATA = dataRoot; // never the real $HOME if (withRuntime) { writeDeep(join(pluginRoot, 'assets/voice-samples/authentic-voice-samples.local.md'), 'REAL VOICE 227 lines'); writeDeep(join(pluginRoot, 'assets/drafts/queue.json'), '{"version":1,"queue":[]}'); writeDeep(join(pluginRoot, 'assets/analytics/exports/content-2026-W22.csv'), 'a,b\n1,2\n'); writeDeep(join(pluginRoot, 'assets/analytics/posts/2026-05-26.json'), '{"x":1}'); writeDeep(join(pluginRoot, 'assets/analytics/weekly-reports/2026-W22.json'), '{"w":22}'); } if (withScaffolds) { writeDeep(join(pluginRoot, 'assets/examples/high-engagement-posts.md'), 'MY REAL POST'); writeDeep(join(pluginRoot, 'assets/audience-insights/demographics.md'), 'demo'); writeDeep(join(pluginRoot, 'assets/audience-insights/engagement-patterns.md'), 'patterns'); writeDeep(join(pluginRoot, 'assets/templates/my-post-templates.md'), 'my templates'); } } test('(a) gitignored runtime files MOVED: source gone, dest byte-equal, voice drops .local', () => { setup({ withRuntime: true }); migrateData({ pluginRoot }); assert.ok(!existsSync(join(pluginRoot, 'assets/voice-samples/authentic-voice-samples.local.md')), 'voice source gone'); assert.ok(!existsSync(join(pluginRoot, 'assets/drafts/queue.json')), 'queue source gone'); assert.ok(!existsSync(join(pluginRoot, 'assets/analytics/exports/content-2026-W22.csv')), 'export source gone'); assert.equal(readFileSync(join(dataRoot, 'voice-samples/authentic-voice-samples.md'), 'utf-8'), 'REAL VOICE 227 lines'); assert.equal(readFileSync(join(dataRoot, 'drafts/queue.json'), 'utf-8'), '{"version":1,"queue":[]}'); assert.ok(existsSync(join(dataRoot, 'analytics/exports/content-2026-W22.csv'))); assert.ok(existsSync(join(dataRoot, 'analytics/posts/2026-05-26.json'))); assert.ok(existsSync(join(dataRoot, 'analytics/weekly-reports/2026-W22.json'))); assert.ok(existsSync(join(dataRoot, '.migrated')), 'marker written'); }); test('(b) tracked scaffolds COPIED: external instance written, tracked source preserved', () => { setup({ withScaffolds: true }); migrateData({ pluginRoot }); assert.ok(existsSync(join(pluginRoot, 'assets/examples/high-engagement-posts.md')), 'scaffold source preserved'); assert.ok(existsSync(join(pluginRoot, 'assets/templates/my-post-templates.md')), 'scaffold source preserved'); assert.equal(readFileSync(join(dataRoot, 'examples/high-engagement-posts.md'), 'utf-8'), 'MY REAL POST'); assert.equal(readFileSync(join(dataRoot, 'audience-insights/demographics.md'), 'utf-8'), 'demo'); assert.equal(readFileSync(join(dataRoot, 'audience-insights/engagement-patterns.md'), 'utf-8'), 'patterns'); assert.equal(readFileSync(join(dataRoot, 'templates/my-post-templates.md'), 'utf-8'), 'my templates'); }); test('(c) re-run is an idempotent no-op (.migrated short-circuits, no throw, dest unchanged)', () => { setup({ withRuntime: true, withScaffolds: true }); migrateData({ pluginRoot }); const voiceDest = join(dataRoot, 'voice-samples/authentic-voice-samples.md'); const before = readFileSync(voiceDest, 'utf-8'); const mtimeBefore = statSync(voiceDest).mtimeMs; const r2 = migrateData({ pluginRoot }); assert.equal(r2.status, 'already-migrated'); assert.equal(readFileSync(voiceDest, 'utf-8'), before); assert.equal(statSync(voiceDest).mtimeMs, mtimeBefore); }); test('(d) empty fixture (no user data) completes without error', () => { setup({}); const r = migrateData({ pluginRoot }); assert.ok(r); assert.ok(existsSync(join(dataRoot, '.migrated'))); }); test('(e) collision: existing external file kept, in-plugin source NOT clobbered', () => { setup({ withRuntime: true }); writeDeep(join(dataRoot, 'voice-samples/authentic-voice-samples.md'), 'EXTERNAL CANONICAL'); migrateData({ pluginRoot }); assert.equal(readFileSync(join(dataRoot, 'voice-samples/authentic-voice-samples.md'), 'utf-8'), 'EXTERNAL CANONICAL'); assert.equal(readFileSync(join(pluginRoot, 'assets/voice-samples/authentic-voice-samples.local.md'), 'utf-8'), 'REAL VOICE 227 lines'); }); });