refactor(linkedin-studio): M0-7 — scorer reads external instance (mirrors MOVE/COPY_FILES), plugin fallback (HIGH #3)
This commit is contained in:
parent
abc19b5641
commit
3ddf9c5ffa
2 changed files with 99 additions and 44 deletions
|
|
@ -5,61 +5,80 @@ import { join } from 'node:path';
|
|||
import { tmpdir } from 'node:os';
|
||||
import { calculateScore } from '../personalization-score.mjs';
|
||||
|
||||
// Step 5 (remediation): the shipped voice profile is a PII-free placeholder
|
||||
// carrying the sentinel below. Detection must key on the sentinel — NOT the old
|
||||
// `[Your Name]` heuristic — so the placeholder earns 0 of the 25 voice points,
|
||||
// and a populated profile (sentinel removed) earns them.
|
||||
// M0-7: the scorer now reads INSTANCE data from the EXTERNAL data root
|
||||
// (getDataRoot, stubbed via LINKEDIN_STUDIO_DATA), mirroring the migration's
|
||||
// dests, and falls back to the plugin tree only for back-compat. The placeholder
|
||||
// sentinel still keys voice detection (Step-5 remediation): the placeholder earns
|
||||
// 0 of the 25 voice points; a populated profile (sentinel removed) earns them.
|
||||
const SENTINEL = '<!-- VOICE_PLACEHOLDER -->';
|
||||
|
||||
// A >50-line body that contains NEITHER the sentinel NOR `[Your Name]`, so the
|
||||
// only thing distinguishing placeholder from real profile is the sentinel.
|
||||
// A >50-line body with NEITHER the sentinel NOR `[Your Name]`, so the sentinel is
|
||||
// the only thing distinguishing placeholder from real profile.
|
||||
const LONG_BODY = Array.from({ length: 60 }, (_, i) => `- voice characteristic line ${i + 1}`).join('\n');
|
||||
|
||||
function makePluginRoot() {
|
||||
const root = mkdtempSync(join(tmpdir(), 'lis-personalization-'));
|
||||
mkdirSync(join(root, 'assets', 'voice-samples'), { recursive: true });
|
||||
return root;
|
||||
function makeRoots() {
|
||||
const dataDir = mkdtempSync(join(tmpdir(), 'lis-score-data-'));
|
||||
const pluginRoot = mkdtempSync(join(tmpdir(), 'lis-score-plugin-'));
|
||||
mkdirSync(join(dataDir, 'voice-samples'), { recursive: true });
|
||||
return { dataDir, pluginRoot };
|
||||
}
|
||||
|
||||
function writeVoice(root, content) {
|
||||
writeFileSync(join(root, 'assets', 'voice-samples', 'authentic-voice-samples.md'), content, 'utf-8');
|
||||
function writeExternalVoice(dataDir, content) {
|
||||
writeFileSync(join(dataDir, 'voice-samples', 'authentic-voice-samples.md'), content, 'utf-8');
|
||||
}
|
||||
|
||||
describe('calculateScore — voice samples (sentinel-based placeholder detection)', () => {
|
||||
let root;
|
||||
describe('calculateScore — reads external instance data (M0-7)', () => {
|
||||
let dataDir, pluginRoot;
|
||||
const saved = { LINKEDIN_STUDIO_DATA: process.env.LINKEDIN_STUDIO_DATA };
|
||||
|
||||
afterEach(() => {
|
||||
if (root && existsSync(root)) {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
for (const d of [dataDir, pluginRoot]) {
|
||||
if (d && existsSync(d)) rmSync(d, { recursive: true, force: true });
|
||||
}
|
||||
root = undefined;
|
||||
dataDir = pluginRoot = undefined;
|
||||
if (saved.LINKEDIN_STUDIO_DATA === undefined) delete process.env.LINKEDIN_STUDIO_DATA;
|
||||
else process.env.LINKEDIN_STUDIO_DATA = saved.LINKEDIN_STUDIO_DATA;
|
||||
});
|
||||
|
||||
test('placeholder with the sentinel scores 0 voice points (even >50 lines, no [Your Name])', () => {
|
||||
root = makePluginRoot();
|
||||
writeVoice(root, `# Voice Profile (placeholder)\n${SENTINEL}\n\n${LONG_BODY}\n`);
|
||||
test('external placeholder with the sentinel scores 0 voice points (even >50 lines, no [Your Name])', () => {
|
||||
({ dataDir, pluginRoot } = makeRoots());
|
||||
process.env.LINKEDIN_STUDIO_DATA = dataDir;
|
||||
writeExternalVoice(dataDir, `# Voice Profile (placeholder)\n${SENTINEL}\n\n${LONG_BODY}\n`);
|
||||
|
||||
const { score, personalized } = calculateScore(root);
|
||||
const { score, personalized } = calculateScore(pluginRoot);
|
||||
|
||||
assert.equal(score, 0, 'placeholder must not earn the 25 voice points');
|
||||
assert.equal(personalized, 0);
|
||||
});
|
||||
|
||||
test('a real profile (no sentinel, >50 lines) earns the 25 voice points', () => {
|
||||
root = makePluginRoot();
|
||||
writeVoice(root, `# My Voice Profile\n\n${LONG_BODY}\n`);
|
||||
test('a real EXTERNAL profile (no sentinel, >50 lines) earns the 25 voice points', () => {
|
||||
({ dataDir, pluginRoot } = makeRoots());
|
||||
process.env.LINKEDIN_STUDIO_DATA = dataDir;
|
||||
writeExternalVoice(dataDir, `# My Voice Profile\n\n${LONG_BODY}\n`);
|
||||
|
||||
const { score, personalized } = calculateScore(root);
|
||||
const { score, personalized } = calculateScore(pluginRoot);
|
||||
|
||||
assert.equal(score, 25, 'a populated profile must earn the 25 voice points');
|
||||
assert.equal(score, 25, 'a populated external profile must earn the 25 voice points');
|
||||
assert.equal(personalized, 1);
|
||||
});
|
||||
|
||||
test('a short placeholder (<50 lines) also scores 0 voice points', () => {
|
||||
root = makePluginRoot();
|
||||
writeVoice(root, `# Voice Profile (placeholder)\n${SENTINEL}\n`);
|
||||
test('profile absent at external profile/user-profile.md → 0, no crash (graceful degradation)', () => {
|
||||
({ dataDir, pluginRoot } = makeRoots());
|
||||
process.env.LINKEDIN_STUDIO_DATA = dataDir;
|
||||
// nothing seeded — no voice, no profile, no scaffolds anywhere
|
||||
|
||||
const { score } = calculateScore(root);
|
||||
const { score, personalized } = calculateScore(pluginRoot);
|
||||
|
||||
assert.equal(score, 0);
|
||||
assert.equal(personalized, 0);
|
||||
});
|
||||
|
||||
test('a short external placeholder (<50 lines) also scores 0 voice points', () => {
|
||||
({ dataDir, pluginRoot } = makeRoots());
|
||||
process.env.LINKEDIN_STUDIO_DATA = dataDir;
|
||||
writeExternalVoice(dataDir, `# Voice Profile (placeholder)\n${SENTINEL}\n`);
|
||||
|
||||
const { score } = calculateScore(pluginRoot);
|
||||
|
||||
assert.equal(score, 0);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue