refactor(linkedin-studio): M0-1 — data-root.mjs resolver twin + test

This commit is contained in:
Kjell Tore Guttormsen 2026-06-18 11:08:52 +02:00
commit dc294495d6
2 changed files with 119 additions and 0 deletions

View file

@ -0,0 +1,80 @@
import { describe, test, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { getDataRoot, getStateFile } from '../data-root.mjs';
// Resolver contract for the per-user data dir (M0). The resolver reads env at
// CALL time (not module load) so these env stubs take effect; restore in
// afterEach. Pattern: scripts/analytics/tests/storage-root.test.ts:14-26.
describe('data-root resolver', () => {
const saved = {
HOME: process.env.HOME,
USERPROFILE: process.env.USERPROFILE,
LINKEDIN_STUDIO_DATA: process.env.LINKEDIN_STUDIO_DATA,
STATE_FILE: process.env.STATE_FILE,
};
afterEach(() => {
for (const [k, v] of Object.entries(saved)) {
if (v === undefined) delete process.env[k];
else process.env[k] = v;
}
});
describe('getDataRoot', () => {
test('default subdir form: getDataRoot("analytics") = HOME/.claude/linkedin-studio/analytics', () => {
process.env.HOME = '/home/tester';
delete process.env.USERPROFILE;
delete process.env.LINKEDIN_STUDIO_DATA;
assert.equal(
getDataRoot('analytics'),
join('/home/tester', '.claude', 'linkedin-studio', 'analytics'),
);
});
test('no-arg form returns the root itself (M5 — needed by the REMEMBER write)', () => {
process.env.HOME = '/home/tester';
delete process.env.USERPROFILE;
delete process.env.LINKEDIN_STUDIO_DATA;
assert.equal(getDataRoot(), join('/home/tester', '.claude', 'linkedin-studio'));
});
test('LINKEDIN_STUDIO_DATA override wins (subdir and root forms)', () => {
process.env.HOME = '/home/tester';
process.env.LINKEDIN_STUDIO_DATA = '/tmp/lis-data';
assert.equal(getDataRoot('analytics'), join('/tmp/lis-data', 'analytics'));
assert.equal(getDataRoot(), '/tmp/lis-data');
});
test('empty HOME + USERPROFILE falls back to os.homedir(), never "" (no relative-path trap)', () => {
delete process.env.HOME;
delete process.env.USERPROFILE;
delete process.env.LINKEDIN_STUDIO_DATA;
const root = getDataRoot('analytics');
assert.equal(root, join(homedir(), '.claude', 'linkedin-studio', 'analytics'));
assert.ok(!root.startsWith('.claude'), `root must be absolute, got relative: ${root}`);
});
});
describe('getStateFile', () => {
test('default = HOME/.claude/linkedin-studio.local.md (mirrors state-updater.mjs:12)', () => {
process.env.HOME = '/home/tester';
delete process.env.USERPROFILE;
delete process.env.STATE_FILE;
assert.equal(getStateFile(), join('/home/tester', '.claude', 'linkedin-studio.local.md'));
});
test('STATE_FILE override wins', () => {
process.env.HOME = '/home/tester';
process.env.STATE_FILE = '/tmp/custom-state.md';
assert.equal(getStateFile(), '/tmp/custom-state.md');
});
});
});

View file

@ -0,0 +1,39 @@
#!/usr/bin/env node
// CANONICAL — twin of scripts/analytics/src/utils/storage.ts:getDataRoot.
// MUST STAY IN SYNC: identical default (~/.claude/linkedin-studio) + identical
// LINKEDIN_STUDIO_DATA override semantics. A twin exists (not a shared import)
// because the runtimes differ: hooks are plain .mjs run by Node with zero npm
// deps and cannot import the TypeScript module (which needs tsx). Change one →
// change the other; the twin-consistency test in __tests__/data-root.test.mjs
// guards the contract behaviorally.
import { join } from 'node:path';
import { homedir } from 'node:os';
import { fileURLToPath } from 'node:url';
// HOME with the codebase idiom (state-updater.mjs:11), but NEVER fall through to
// '' — that would turn the absolute data path into a CWD-relative one. os.homedir()
// is the floor (risk-assessor Low #2).
function resolveHome() {
return process.env.HOME || process.env.USERPROFILE || homedir();
}
// Canonical per-user data root: ~/.claude/linkedin-studio (mirrors the state-file
// location), overridable via LINKEDIN_STUDIO_DATA (test + power-user seam).
// No-arg / '' returns the root itself; a subdir returns root/subdir.
export function getDataRoot(subdir = '') {
const dataBase = process.env.LINKEDIN_STUDIO_DATA || join(resolveHome(), '.claude', 'linkedin-studio');
return subdir ? join(dataBase, subdir) : dataBase;
}
// State file lives beside the data dir (mirrors state-updater.mjs:12).
export function getStateFile() {
return process.env.STATE_FILE || join(resolveHome(), '.claude', 'linkedin-studio.local.md');
}
// Standalone: print the resolved paths for debugging (pattern: personalization-score.mjs:120).
if (process.argv[1] === fileURLToPath(import.meta.url)) {
console.log('data root:', getDataRoot());
console.log('analytics:', getDataRoot('analytics'));
console.log('state file:', getStateFile());
}