feat(linkedin-studio): honest newsletter distribution + profile-SEO + outreach pipeline
Step 17 (Wave 4 S4) of the audit remediation. Applies research/03 §D5 + the two S2 residual fixes folded in. No new commands/agents (counts stay 27/19). Newsletter (commands/newsletter.md): new "Distribution channel" section after Step 10 teaching the HONEST native-newsletter mechanics — bypasses organic feed ranking via ONE deduplicated notification per subscriber per edition (NOT a three-touchpoint blast); the mass invite fires once → ~1-2K follower floor (wait until you can spend it); realistic cold-start 0-100 subs months 1-3; discloses non-export / no-canonical / no-read-analytics / per-subscriber decay; explicit below-vs-above-floor decision rule. Sourced to research/03 D5. Profile (commands/profile.md): new "Profile SEO" section — headline as the highest-weight search field + a per-section keyword-target table (headline/about/experience/skills/featured), consistency-over-stuffing rule. Outreach (commands/outreach.md): Step 8c persists the pipeline board to tracked state via the new recordOutreachContact mutation (mirrors Step 16's recordFirstHourPlan): additive last_outreach_date/outreach_active scalars + a non-R-initial ## Outreach Pipeline section in state-updater.mjs + config/state-file.template.md + --record-outreach CLI branch. +7 tests (state-updater 26→33, full hook suite 83→90). Residual 1 (growth-playbook:216): 9:16 "distribution boost" → 4:5/1:1 guidance (9:16 mobile-only opt-in; "immersive distribution" = uncorroborated heuristic). Residual 2 (video-strategy-guide:300): "3-second test determines 70% retention" → "front-load value for muted autoplay" (three-second hook is folklore, not a LinkedIn signal). Verify: grep checks 1-5 pass; test-runner.sh exit 0 (stat-consistency green); state-updater 33/33. [skip-docs] — tre-doc + version bump deferred to Step 21. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
743867f90a
commit
0b4e1bd097
8 changed files with 284 additions and 3 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { updatePostTracking, pruneContentHistory, updateFollowerCount, recordFirstHourPlan } from '../state-updater.mjs';
|
||||
import { updatePostTracking, pruneContentHistory, updateFollowerCount, recordFirstHourPlan, recordOutreachContact } from '../state-updater.mjs';
|
||||
|
||||
const SAMPLE_STATE = `---
|
||||
last_post_date: "2026-04-05"
|
||||
|
|
@ -365,3 +365,77 @@ describe('recordFirstHourPlan', () => {
|
|||
assert.ok(result.changes.length > 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('recordOutreachContact', () => {
|
||||
const CONTACT_OPTS = {
|
||||
contactDate: '2026-05-30 14:00',
|
||||
track: 'collab',
|
||||
partner: '@bigvoice',
|
||||
stage: 'pitched',
|
||||
nextAction: 'follow up if no reply',
|
||||
dueDate: '2026-06-06'
|
||||
};
|
||||
|
||||
test('creates a non-R-initial Outreach Pipeline section when absent', () => {
|
||||
// SAMPLE_STATE has no ## Outreach Pipeline section → must be created (additive)
|
||||
assert.ok(!SAMPLE_STATE.includes('## Outreach Pipeline'));
|
||||
const result = recordOutreachContact(SAMPLE_STATE, CONTACT_OPTS);
|
||||
assert.notEqual(result, null);
|
||||
assert.ok(result.content.includes('## Outreach Pipeline'));
|
||||
});
|
||||
|
||||
test('records track, partner, stage, next action and due in the entry', () => {
|
||||
const result = recordOutreachContact(SAMPLE_STATE, CONTACT_OPTS);
|
||||
assert.notEqual(result, null);
|
||||
assert.ok(result.content.includes('[2026-05-30 14:00]'));
|
||||
assert.ok(result.content.includes('@bigvoice'));
|
||||
assert.ok(result.content.includes('collab'));
|
||||
assert.ok(result.content.includes('pitched'));
|
||||
assert.ok(result.content.includes('follow up if no reply'));
|
||||
assert.ok(result.content.includes('2026-06-06'));
|
||||
});
|
||||
|
||||
test('is additive — inserts last_outreach_date when the field is absent', () => {
|
||||
assert.ok(!/^last_outreach_date:/m.test(SAMPLE_STATE));
|
||||
const result = recordOutreachContact(SAMPLE_STATE, CONTACT_OPTS);
|
||||
assert.notEqual(result, null);
|
||||
assert.match(result.content, /^last_outreach_date: "2026-05-30 14:00"$/m);
|
||||
});
|
||||
|
||||
test('updates an existing last_outreach_date without duplicating it', () => {
|
||||
const withField = SAMPLE_STATE.replace(
|
||||
'last_post_date: "2026-04-05"',
|
||||
'last_post_date: "2026-04-05"\nlast_outreach_date: null'
|
||||
);
|
||||
const result = recordOutreachContact(withField, { ...CONTACT_OPTS, contactDate: '2026-05-31 09:00' });
|
||||
assert.notEqual(result, null);
|
||||
const hits = result.content.match(/^last_outreach_date:/gm) || [];
|
||||
assert.equal(hits.length, 1, 'field must not be duplicated');
|
||||
assert.match(result.content, /^last_outreach_date: "2026-05-31 09:00"$/m);
|
||||
});
|
||||
|
||||
test('leaves existing fields and sections untouched (round-trip)', () => {
|
||||
const result = recordOutreachContact(SAMPLE_STATE, CONTACT_OPTS);
|
||||
assert.notEqual(result, null);
|
||||
assert.match(result.content, /^last_post_date: "2026-04-05"$/m);
|
||||
assert.match(result.content, /^follower_count: 850$/m);
|
||||
assert.ok(result.content.includes('- [2026-04-05] "AI governance is not about..."'));
|
||||
});
|
||||
|
||||
test('gracefully defaults empty optional fields', () => {
|
||||
const result = recordOutreachContact(SAMPLE_STATE, {
|
||||
contactDate: '2026-05-30 14:00',
|
||||
partner: 'minimal-contact'
|
||||
});
|
||||
assert.notEqual(result, null);
|
||||
assert.ok(result.content.includes('## Outreach Pipeline'));
|
||||
assert.ok(result.content.includes('minimal-contact'));
|
||||
});
|
||||
|
||||
test('returns a changes array describing what changed', () => {
|
||||
const result = recordOutreachContact(SAMPLE_STATE, CONTACT_OPTS);
|
||||
assert.notEqual(result, null);
|
||||
assert.ok(Array.isArray(result.changes));
|
||||
assert.ok(result.changes.length > 0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue