The LAST second-brain slice; the S0–S3e arc is now complete.
(b) Retire the dead, zero-reader content-history.md across its 8 plumbing
surfaces: the flaky Stop-hook writer prose, the template (git-rm'd), the
migrate-data.mjs B1 MOVE entry + its test assertions, .gitignore, the gate
SC2_CLASSES guard, the data-path ref-doc, and a session-start comment. SC1
grep = 0; migrate suite 5/5 (R8 idempotency demonstrated).
(c) `brain reconcile` — read-side triple-post reconciliation joining silo 1
(## Recent Posts, auto-tracked creation) to the silo 2↔3 graph, surfacing the
coverage gap: posts created via the plugin but never `brain ingest`-ed. New
pure core scripts/brain/src/reconcile.ts (parseRecentPosts tracks the WRITER
format state-updater.mjs:116, NOT the date-only pruner; reconcileRecentPosts
matches hook→record.body→graph for the in-graph/in-brain-only/orphaned tiers;
loadRecentPosts reads STATE_FILE via the canonical getStateFile() chain — a new
cross-seam read, the state file lives outside the brain dataRoot). Wired as the
`brain reconcile` CLI subcommand (inline parsePublishedRecord loader, not the
body-less listPublished). Read-only: never writes the state silo.
Tests (verified live): gate 99/0/0 (ASSERT floor 82→84; new Section 16f
self-test + CLI grep) · brain 127/127 (floor 114→127, +13 reconcile) · hook
suite 136/136. SC4/SC6 end-to-end run is a real behavioural pass (STATE_FILE
seam read + fallback). Honest limit: read-side cannot reconstruct un-captured
specifics/trends — auto-capture is the flagged follow-up.
Brief/plan: docs/second-brain/{brief,plan}-sb-s3e.md (fbad29d). Go-before-code
gate cleared (operator: retire · read-side · build-now).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
106 lines
5.2 KiB
JavaScript
106 lines
5.2 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/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');
|
|
});
|
|
});
|