linkedin-studio/hooks/scripts/__tests__/migrate-data.test.mjs

109 lines
5.5 KiB
JavaScript

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/content-history.md'), '# history');
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, content-history moved', () => {
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/content-history.md')), 'content-history 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/content-history.md')));
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');
});
});