refactor(linkedin-studio): M0-8 — voice read via getDataRoot (SC7)

This commit is contained in:
Kjell Tore Guttormsen 2026-06-18 11:45:14 +02:00
commit 289eab498e
2 changed files with 71 additions and 9 deletions

View file

@ -0,0 +1,65 @@
import { describe, test, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
// M0-8 / SC7: the UserPromptSubmit hook must point the voice-profile hint at the
// REAL artefact in the EXTERNAL data root (getDataRoot('voice-samples')), not the
// in-plugin placeholder. This hook is a procedural stdin->stdout CLI with no
// exports, so we test it end-to-end as a subprocess: pipe the prompt JSON, stub
// LINKEDIN_STUDIO_DATA + STATE_FILE, and inspect the injected systemMessage.
const hookPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'user-prompt-context.mjs');
function runHook(prompt, env) {
const out = execFileSync('node', [hookPath], {
input: JSON.stringify({ prompt }),
env: { ...process.env, ...env },
encoding: 'utf-8',
});
return JSON.parse(out);
}
describe('user-prompt-context — voice hint points at external data root (M0-8/SC7)', () => {
let dataDir;
afterEach(() => {
if (dataDir && existsSync(dataDir)) rmSync(dataDir, { recursive: true, force: true });
dataDir = undefined;
});
test('real external voice present → systemMessage references the external voice file', () => {
dataDir = mkdtempSync(join(tmpdir(), 'lis-upc-'));
mkdirSync(join(dataDir, 'voice-samples'), { recursive: true });
writeFileSync(join(dataDir, 'voice-samples', 'authentic-voice-samples.md'), '# My Voice\n\nreal content\n', 'utf-8');
const result = runHook('write a linkedin post', {
LINKEDIN_STUDIO_DATA: dataDir,
STATE_FILE: join(dataDir, 'no-state.local.md'),
});
assert.ok(result.systemMessage, 'a LinkedIn prompt must produce an enrichment systemMessage');
assert.ok(
result.systemMessage.includes(join(dataDir, 'voice-samples', 'authentic-voice-samples.md')),
'voice hint must point at the external voice file, not the in-plugin placeholder',
);
});
test('external voice absent → no voice-profile hint injected (graceful degradation)', () => {
dataDir = mkdtempSync(join(tmpdir(), 'lis-upc-'));
// no voice-samples dir/file seeded under the external root
const result = runHook('write a linkedin post', {
LINKEDIN_STUDIO_DATA: dataDir,
STATE_FILE: join(dataDir, 'no-state.local.md'),
});
assert.ok(result.systemMessage, 'still enriches (scorecard reminder), just without the voice hint');
assert.ok(
!result.systemMessage.includes('Voice Profile:'),
'no voice hint when the external voice artefact is absent',
);
});
});

View file

@ -13,13 +13,10 @@
// 0 - Always allow (informational hook)
import { readFileSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { join } from 'node:path';
import { getDataRoot, getStateFile } from './data-root.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const pluginRoot = join(__dirname, '..', '..');
const home = process.env.HOME || process.env.USERPROFILE || '';
const stateFile = join(home, '.claude', 'linkedin-studio.local.md');
const stateFile = getStateFile();
// Read stdin JSON
let input;
@ -98,10 +95,10 @@ if (!isLinkedin) {
// === Build context enrichment ===
let context = '**LinkedIn Context Enrichment (auto-injected):**\n\n';
// 1. Voice profile reference
const voiceFile = join(pluginRoot, 'assets', 'voice-samples', 'authentic-voice-samples.md');
// 1. Voice profile reference — the REAL artefact in the external data root (SC7).
const voiceFile = join(getDataRoot('voice-samples'), 'authentic-voice-samples.md');
if (existsSync(voiceFile)) {
context += '**Voice Profile:** Read `assets/voice-samples/authentic-voice-samples.md` for tone matching.\n\n';
context += `**Voice Profile:** Read \`${voiceFile}\` for tone matching.\n\n`;
}
// 2-4. State file data