import { parseSources, normUrl, groupKeyOf, groupDrafts, candidateTitle, gradeWords, wordTargets, sourceOverlapCheck, pickChanges } from '../drafts'; import type { EntityRecord } from '../types'; function rec(id: string, name: string, data: Record): EntityRecord { return { id: id as unknown as number, entityType: 'draft', externalId: null, name, data, createdAt: '' }; } describe('parseSources', () => { it('parses the pipe-delimited string shape', () => { const out = parseSources('Arab News (2024-08-04) https://www.arabnews.com/node/misa-q2 | note without url'); expect(out).toHaveLength(1); expect(out[0].url).toBe('https://www.arabnews.com/node/misa-q2'); expect(out[0].outlet).toBe('Arab News'); expect(out[0].date).toBe('2024-08-04'); }); it('parses the JSON array shape', () => { const out = parseSources([{ url: 'https://www.thenationalnews.com/x', outlet: 'The National', date: '2026-06-27' }]); expect(out).toHaveLength(1); expect(out[0].outlet).toBe('The National'); }); it('returns [] for null/garbage', () => { expect(parseSources(null)).toEqual([]); expect(parseSources(42)).toEqual([]); }); }); describe('normUrl', () => { it('strips scheme/www/trailing slash and lowercases', () => { expect(normUrl('https://www.Invest.qa/')).toBe('invest.qa'); expect(normUrl('https://www.arabnews.com/node/misa-q2')).toBe('arabnews.com/node/misa-q2'); }); }); describe('groupDrafts', () => { const saudi = [1, 2, 3].map((i) => rec(`s${i}`, `Saudi ${i}`, { candidate_index: i, content_type: 'weekly_brief', chosen: false, sources: 'Arab News (2024-08-04) https://www.arabnews.com/node/misa-q2 | note' }), ); const qatar = [3, 1, 2].map((i) => rec(`q${i}`, `Qatar ${i}`, { candidate_index: i, content_type: 'weekly_brief', chosen: i === 1, status: i === 1 ? 'selected' : '', sources: [{ url: 'https://www.invest.qa/', outlet: 'Invest Qatar' }] }), ); it('clusters candidates by primary source, sorted by candidate_index', () => { const groups = groupDrafts([...saudi, ...qatar]); expect(groups).toHaveLength(2); const s = groups.find((g) => g.key.includes('arabnews'))!; expect(s.candidates.map((c) => c.data.candidate_index)).toEqual([1, 2, 3]); }); it('unresolved groups float above picked ones', () => { const groups = groupDrafts([...saudi, ...qatar]); expect(groups[0].key).toContain('arabnews'); expect(groups[0].chosen).toBeNull(); expect(groups[1].chosen).not.toBeNull(); }); it('prefers an explicit topic_ref over the source heuristic', () => { const a = rec('a', 'A', { candidate_index: 1, topic_ref: 't-42', topic_title: 'Parent topic', sources: 'x (2026-01-01) https://a.com/one' }); const b = rec('b', 'B', { candidate_index: 2, topic_ref: 't-42', topic_title: 'Parent topic', sources: 'y (2026-01-01) https://b.com/two' }); expect(groupKeyOf(a)).toBe('ref:t-42'); expect(groupKeyOf(a)).toBe(groupKeyOf(b)); const g = groupDrafts([a, b]); expect(g).toHaveLength(1); expect(g[0].label).toBe('Parent topic'); }); it('a ready candidate sinks the group and flags ready', () => { const done = [1, 2].map((i) => rec(`d${i}`, `Done ${i}`, { candidate_index: i, topic_ref: 'done', status: i === 1 ? 'ready' : '', chosen: i === 1 })); const open = [1, 2].map((i) => rec(`o${i}`, `Open ${i}`, { candidate_index: i, topic_ref: 'open' })); const groups = groupDrafts([...done, ...open]); expect(groups[groups.length - 1].ready).toBe(true); expect(groups[0].ready).toBe(false); }); }); describe('candidateTitle', () => { it('strips the "Candidate N —" scaffolding', () => { expect(candidateTitle(rec('x', 'Candidate 1 — UAE–China trade brief', {}))).toBe('UAE–China trade brief'); }); }); describe('gradeWords', () => { it('grades against the weekly-brief blog target', () => { const t = wordTargets('weekly_brief').blog; expect(gradeWords(462, t)).toBe('ok'); expect(gradeWords(200, t)).toBe('low'); expect(gradeWords(900, t)).toBe('high'); }); it('returns none when no target', () => { expect(gradeWords(100, wordTargets('lead_magnet').linkedin)).toBe('none'); }); }); describe('sourceOverlapCheck (768w.18.16.2 wiring)', () => { const SOURCE = 'A Houthi threat to impose a naval blockade against Saudi Arabia in the Red Sea could significantly ' + 'widen the Iran war and strain a U.S. military already focused on stopping attacks across the region.'; it('returns null when the draft carries no source_texts', () => { expect(sourceOverlapCheck(rec('d1', 'Draft', { blog: SOURCE }))).toBeNull(); expect(sourceOverlapCheck(rec('d2', 'Draft', { blog: SOURCE, source_texts: [] }))).toBeNull(); }); it('FAILS when the blog copies a long verbatim run from a source_text', () => { const check = sourceOverlapCheck(rec('d3', 'Draft', { blog: `Intro. ${SOURCE} Outro.`, source_texts: [SOURCE] })); expect(check).not.toBeNull(); expect(check!.id).toBe('source-overlap'); expect(check!.status).toBe('fail'); }); it('PASSES when the blog is original prose against the source_text', () => { const original = 'Foreign operators with Gulf shipping exposure should reassess route reliability, war-risk premiums, ' + 'and contract terms as regional tensions evolve this quarter.'; const check = sourceOverlapCheck(rec('d4', 'Draft', { blog: original, source_texts: [SOURCE] })); expect(check).not.toBeNull(); expect(check!.status).toBe('pass'); }); it('reads camelCased sourceTexts too', () => { const check = sourceOverlapCheck(rec('d5', 'Draft', { blog: `x ${SOURCE}`, sourceTexts: [SOURCE] })); expect(check!.status).toBe('fail'); }); }); describe('pickChanges (startsim-yvd3 — pick must not write an invalid status)', () => { it('marks the picked candidate chosen and writes NO status key', () => { const picked = pickChanges('a', 'a'); expect(picked).toEqual({ chosen: true }); expect('status' in picked).toBe(false); // status enum is the tenant's; picking must not touch it expect(pickChanges('b', 'a')).toEqual({ chosen: false }); }); it('compares ids as strings (handles number vs string ids)', () => { expect(pickChanges(7 as unknown as string, '7')).toEqual({ chosen: true }); expect(pickChanges(7 as unknown as string, 8 as unknown as string)).toEqual({ chosen: false }); }); });