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', ); }); });