linkedin-studio/hooks/scripts/__tests__/ical-generator.test.mjs
Kjell Tore Guttormsen 52fa87a255 refactor(linkedin)!: rename plugin linkedin-thought-leadership → linkedin-studio (v3.0.0)
BREAKING CHANGE: the marketplace slug, the agent namespace
(linkedin-studio:<agent>), and the runtime state-file path
(~/.claude/linkedin-studio.local.md) all change. Reinstall required;
existing state migrated in place (post metrics, streak, history preserved).
The /linkedin:* commands are unchanged — the command namespace is set
per-command in frontmatter and was always independent of the plugin slug.
Functionality is byte-identical to v2.4.0; this release is pure identity.

- dir + manifests: plugins/linkedin-studio + plugin.json + root marketplace.json
- agent namespace updated in commands/newsletter.md (only functional invoker)
- state path updated in 4 hook scripts + topic-rotation prompt + state template
- catch-all skill dir renamed skills/linkedin-studio (5 functional skills unchanged)
- docs + version bump to 3.0.0 across README badge, CHANGELOG, root README/CLAUDE.md
- historical records (CHANGELOG past entries, docs/ build artifacts,
  config-audit v5.0.0 snapshots) intentionally retain the old slug

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 11:32:02 +02:00

158 lines
5.6 KiB
JavaScript

import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { generateIcal, generateIcalFromQueue } from '../ical-generator.mjs';
const SAMPLE_EVENT = {
id: 'post-2026-04-14-ai-strategy',
title: 'LinkedIn: AI strategy in public sector',
description: 'Pillar: AI Strategy | Format: Standard | Draft: assets/drafts/week-W16/monday.md',
date: '2026-04-14',
time: '08:30',
duration: 30,
};
const SAMPLE_QUEUE_ENTRY = {
id: 'post-2026-04-14-ai-strategy',
draft_path: 'assets/drafts/week-W16/monday.md',
scheduled_date: '2026-04-14',
scheduled_time: '08:30',
pillar: 'AI Strategy',
format: 'Standard',
hook_preview: 'AI strategy in public sector',
character_count: 1450,
status: 'scheduled',
created_at: '2026-04-10',
};
describe('generateIcal', () => {
test('returns valid empty VCALENDAR for empty events array', () => {
const ical = generateIcal([]);
assert.match(ical, /^BEGIN:VCALENDAR\r\n/);
assert.match(ical, /\r\nEND:VCALENDAR\r\n$/);
assert.match(ical, /PRODID:-\/\/linkedin-studio\/\/EN/);
assert.match(ical, /VERSION:2\.0/);
assert.ok(!ical.includes('BEGIN:VEVENT'), 'should not contain VEVENT');
});
test('generates VEVENT with correct DTSTART, SUMMARY, UID', () => {
const ical = generateIcal([SAMPLE_EVENT]);
assert.match(ical, /BEGIN:VEVENT/);
assert.match(ical, /DTSTART;TZID=Europe\/Oslo:20260414T083000/);
assert.match(ical, /SUMMARY:LinkedIn: AI strategy in public sector/);
assert.match(ical, /UID:post-2026-04-14-ai-strategy@linkedin-studio/);
assert.match(ical, /END:VEVENT/);
});
test('generates correct DTEND from duration', () => {
const ical = generateIcal([SAMPLE_EVENT]);
assert.match(ical, /DTEND;TZID=Europe\/Oslo:20260414T090000/);
});
test('defaults duration to 30 minutes when not specified', () => {
const event = { ...SAMPLE_EVENT, duration: undefined };
const ical = generateIcal([event]);
assert.match(ical, /DTEND;TZID=Europe\/Oslo:20260414T090000/);
});
test('has CRLF line endings throughout', () => {
const ical = generateIcal([SAMPLE_EVENT]);
const lines = ical.split('\r\n');
assert.ok(lines.length > 5, 'should have multiple lines');
const bareLF = ical.replace(/\r\n/g, '').includes('\n');
assert.ok(!bareLF, 'should not contain bare LF without CR');
});
test('includes VALARM with 15-minute trigger', () => {
const ical = generateIcal([SAMPLE_EVENT]);
assert.match(ical, /BEGIN:VALARM/);
assert.match(ical, /TRIGGER:-PT15M/);
assert.match(ical, /ACTION:DISPLAY/);
assert.match(ical, /END:VALARM/);
});
test('includes DTSTAMP in UTC format', () => {
const ical = generateIcal([SAMPLE_EVENT]);
assert.match(ical, /DTSTAMP:\d{8}T\d{6}Z/);
});
test('folds lines longer than 75 octets', () => {
const longDescription = 'A'.repeat(200);
const event = { ...SAMPLE_EVENT, description: longDescription };
const ical = generateIcal([event]);
const lines = ical.split('\r\n');
for (const line of lines) {
const octets = Buffer.byteLength(line, 'utf-8');
assert.ok(octets <= 75, `Line exceeds 75 octets (${octets}): "${line.slice(0, 40)}..."`);
}
});
test('escapes special characters in SUMMARY and DESCRIPTION', () => {
const event = {
...SAMPLE_EVENT,
title: 'Test: commas, semicolons; and\\backslashes',
description: 'Line1\nLine2, with; special\\chars',
};
const ical = generateIcal([event]);
assert.match(ical, /SUMMARY:Test: commas\\, semicolons\; and\\\\backslashes/);
assert.match(ical, /DESCRIPTION:Line1\\nLine2\\, with\; special\\\\chars/);
});
test('handles multiple events', () => {
const event2 = {
...SAMPLE_EVENT,
id: 'post-2026-04-16-leadership',
title: 'LinkedIn: Leadership lessons',
date: '2026-04-16',
time: '12:00',
};
const ical = generateIcal([SAMPLE_EVENT, event2]);
const veventCount = (ical.match(/BEGIN:VEVENT/g) || []).length;
assert.equal(veventCount, 2);
});
test('includes VTIMEZONE for Europe/Oslo', () => {
const ical = generateIcal([SAMPLE_EVENT]);
assert.match(ical, /BEGIN:VTIMEZONE/);
assert.match(ical, /TZID:Europe\/Oslo/);
assert.match(ical, /END:VTIMEZONE/);
});
test('supports custom timezone parameter', () => {
const ical = generateIcal([SAMPLE_EVENT], { timezone: 'America/New_York' });
assert.match(ical, /TZID:America\/New_York/);
assert.match(ical, /DTSTART;TZID=America\/New_York/);
});
});
describe('generateIcalFromQueue', () => {
test('transforms queue entry format to event format', () => {
const events = generateIcalFromQueue([SAMPLE_QUEUE_ENTRY]);
assert.equal(events.length, 1);
const e = events[0];
assert.equal(e.id, 'post-2026-04-14-ai-strategy');
assert.equal(e.date, '2026-04-14');
assert.equal(e.time, '08:30');
assert.ok(e.title.includes('AI strategy in public sector'));
assert.ok(e.description.includes('AI Strategy'));
assert.ok(e.description.includes('Standard'));
});
test('handles missing scheduled_time gracefully', () => {
const entry = { ...SAMPLE_QUEUE_ENTRY, scheduled_time: undefined };
const events = generateIcalFromQueue([entry]);
assert.equal(events[0].time, '09:00');
});
test('handles empty array', () => {
const events = generateIcalFromQueue([]);
assert.deepEqual(events, []);
});
test('generates valid iCal when piped through generateIcal', () => {
const events = generateIcalFromQueue([SAMPLE_QUEUE_ENTRY]);
const ical = generateIcal(events);
assert.match(ical, /BEGIN:VCALENDAR/);
assert.match(ical, /BEGIN:VEVENT/);
assert.match(ical, /END:VCALENDAR/);
});
});