From 97faa72afed616cd71fbce2eff5787a31249469f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:01:11 +0000 Subject: [PATCH 1/3] Initial plan From f44c39d95da71c5181282b2f04d61aa16cc6fb7d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:14:00 +0000 Subject: [PATCH 2/3] feat: enhance week-ahead cross-referencing with policy context, questions to watch, and interpellation spotlight Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> --- .../data-transformers/content-generators.ts | 145 ++++++++++++++---- scripts/news-types/week-ahead.ts | 16 +- tests/news-types/week-ahead.test.ts | 91 +++++++++++ 3 files changed, 209 insertions(+), 43 deletions(-) diff --git a/scripts/data-transformers/content-generators.ts b/scripts/data-transformers/content-generators.ts index 690926df..46caa74f 100644 --- a/scripts/data-transformers/content-generators.ts +++ b/scripts/data-transformers/content-generators.ts @@ -11,7 +11,7 @@ import { escapeHtml } from '../html-utils.js'; import type { Language } from '../types/language.js'; -import type { ArticleContentData, WeekAheadData, RawDocument } from './types.js'; +import type { ArticleContentData, WeekAheadData, RawDocument, RawCalendarEvent } from './types.js'; import { getPillarTransition } from '../editorial-pillars.js'; import { L, @@ -50,6 +50,40 @@ const TITLE_SUFFIX_TEMPLATES: Readonly string>> = zh: t => `,包括"${t}"`, }; +/** Extract meaningful keywords from text for cross-reference matching (min 2 chars, captures EU, KU, etc.) */ +function extractKeywords(text: string): string[] { + return text.toLowerCase().split(/\s+/).filter(w => w.length >= 2); +} + +/** Find documents related to a calendar event by organ match or keyword overlap (max 3) */ +function findRelatedDocuments(event: RawCalendarEvent, documents: RawDocument[]): RawDocument[] { + const rec = event as Record; + const eventOrgan = rec['organ'] ?? ''; + const keywords = extractKeywords(rec['rubrik'] ?? rec['titel'] ?? rec['title'] ?? ''); + return documents.filter(doc => { + const docOrgan = doc.organ ?? doc.committee ?? ''; + if (eventOrgan && docOrgan && eventOrgan.toLowerCase() === docOrgan.toLowerCase()) return true; + const docText = (doc.titel ?? doc.title ?? '').toLowerCase(); + return keywords.some(kw => docText.includes(kw)); + }).slice(0, 3); +} + +/** Find written questions related to a calendar event by keyword overlap (max 3) */ +function findRelatedQuestions(event: RawCalendarEvent, questions: RawDocument[]): RawDocument[] { + const rec = event as Record; + const keywords = extractKeywords(rec['rubrik'] ?? rec['titel'] ?? rec['title'] ?? ''); + return questions.filter(q => { + const qText = (q.titel ?? q.title ?? '').toLowerCase(); + return keywords.some(kw => qText.includes(kw)); + }).slice(0, 3); +} + +/** Extract targeted minister name from interpellation summary "till MINISTER" header line */ +function extractMinister(summary: string): string { + const m = summary.match(/\btill\s+([^\n]+)/i); + return m ? m[1].trim() : ''; +} + export function generateWeekAheadContent(data: WeekAheadData, lang: Language | string): string { const { events, highlights, context } = data; // Cast to ArticleContentData to access documents field (passed via switch cast) @@ -89,6 +123,50 @@ export function generateWeekAheadContent(data: WeekAheadData, lang: Language | s

${dayName ? dayName + ' - ' : ''}${titleHtml}

${event.description || `${eventTime}: ${event.details || 'Parliamentary session scheduled.'}`}

`; + + // Policy Context: cross-reference related documents and questions per event + const relatedPolicyDocs = findRelatedDocuments(event, documents); + const relatedPolicyQs = findRelatedQuestions(event, questions); + if (relatedPolicyDocs.length > 0 || relatedPolicyQs.length > 0) { + const policyContextLabel = lang === 'sv' ? 'Policysammanhang' + : lang === 'de' ? 'Politischer Kontext' + : lang === 'fr' ? 'Contexte politique' + : lang === 'es' ? 'Contexto político' + : lang === 'da' ? 'Politisk kontekst' + : lang === 'no' ? 'Politisk kontekst' + : lang === 'fi' ? 'Poliittinen konteksti' + : lang === 'nl' ? 'Beleidscontext' + : lang === 'ar' ? 'السياق السياسي' + : lang === 'he' ? 'הקשר מדיניות' + : lang === 'ja' ? '政策コンテキスト' + : lang === 'ko' ? '정책 맥락' + : lang === 'zh' ? '政策背景' + : 'Policy Context'; + content += `
\n`; + content += `

${policyContextLabel}

\n`; + relatedPolicyDocs.forEach(doc => { + const drec = doc as Record; + const docTitle = drec['titel'] ?? drec['title'] ?? 'Document'; + const dokId = drec['dok_id'] ?? ''; + const docUrl = dokId ? sanitizeUrl(`https://riksdagen.se/sv/dokument-och-lagar/dokument/${encodeURIComponent(dokId)}/`) : ''; + content += `
\n`; + content += `
${docUrl ? `` : ''}${svSpan(escapeHtml(docTitle), lang)}${docUrl ? '' : ''}
\n`; + const sig = generatePolicySignificance(doc, lang); + if (sig) content += `

${escapeHtml(sig)}

\n`; + content += `
\n`; + }); + relatedPolicyQs.forEach(q => { + const qrec = q as Record; + const qTitle = qrec['titel'] ?? qrec['title'] ?? 'Question'; + const qParty = qrec['parti'] ? ` (${escapeHtml(qrec['parti'])})` : ''; + const qDokId = qrec['dok_id'] ?? ''; + const qUrl = qDokId ? sanitizeUrl(`https://riksdagen.se/sv/dokument-och-lagar/dokument/${encodeURIComponent(qDokId)}/`) : ''; + content += `
\n`; + content += `
${qUrl ? `` : ''}${svSpan(escapeHtml(qTitle), lang)}${qUrl ? '' : ''}${qParty}
\n`; + content += `
\n`; + }); + content += `
\n`; + } }); } @@ -144,22 +222,22 @@ export function generateWeekAheadContent(data: WeekAheadData, lang: Language | s }); } - // Parliamentary Questions: upcoming written questions to ministers + // Questions to Watch: upcoming written questions cross-referenced with debate topics if (questions.length > 0) { - const questionsLabel = lang === 'sv' ? 'Skriftliga frågor till statsråd' - : lang === 'de' ? 'Schriftliche parlamentarische Anfragen' - : lang === 'fr' ? 'Questions écrites au gouvernement' - : lang === 'es' ? 'Preguntas escritas al gobierno' - : lang === 'da' ? 'Skriftlige spørgsmål til ministrene' - : lang === 'no' ? 'Skriftlige spørsmål til statsrådene' - : lang === 'fi' ? 'Kirjalliset kysymykset ministerille' - : lang === 'nl' ? 'Schriftelijke vragen aan ministers' - : lang === 'ar' ? 'أسئلة مكتوبة للحكومة' - : lang === 'he' ? 'שאלות כתובות לממשלה' - : lang === 'ja' ? '大臣への書面質問' - : lang === 'ko' ? '장관에 대한 서면 질문' - : lang === 'zh' ? '书面质询政府' - : 'Parliamentary Questions to Ministers'; + const questionsLabel = lang === 'sv' ? 'Frågor att bevaka' + : lang === 'de' ? 'Zu beobachtende Anfragen' + : lang === 'fr' ? 'Questions à surveiller' + : lang === 'es' ? 'Preguntas a seguir' + : lang === 'da' ? 'Spørgsmål at holde øje med' + : lang === 'no' ? 'Spørsmål å følge med på' + : lang === 'fi' ? 'Seurattavat kysymykset' + : lang === 'nl' ? 'Te volgen vragen' + : lang === 'ar' ? 'أسئلة تستحق المتابعة' + : lang === 'he' ? 'שאלות לעקוב' + : lang === 'ja' ? '注目の質問' + : lang === 'ko' ? '주목할 질문' + : lang === 'zh' ? '值得关注的问题' + : 'Questions to Watch'; content += `\n

${questionsLabel}

\n`; questions.slice(0, 8).forEach(q => { const rec = q as Record; @@ -174,22 +252,22 @@ export function generateWeekAheadContent(data: WeekAheadData, lang: Language | s }); } - // Interpellations: formal parliamentary interpellations awaiting ministerial response + // Interpellation Spotlight: formal interpellations enriched with minister response context if (interpellations.length > 0) { - const interLabel = lang === 'sv' ? 'Interpellationer under behandling' - : lang === 'de' ? 'Interpellationen in Bearbeitung' - : lang === 'fr' ? 'Interpellations en cours' - : lang === 'es' ? 'Interpelaciones en curso' - : lang === 'da' ? 'Forespørgsler til behandling' - : lang === 'no' ? 'Interpellasjoner til behandling' - : lang === 'fi' ? 'Käsittelyssä olevat välikysymykset' - : lang === 'nl' ? 'Interpellaties in behandeling' - : lang === 'ar' ? 'الاستجوابات البرلمانية قيد المعالجة' - : lang === 'he' ? 'בקשות הבהרה בטיפול' - : lang === 'ja' ? '処理中の質問主意書' - : lang === 'ko' ? '처리 중인 대정부 질문' - : lang === 'zh' ? '待处理的质询' - : 'Interpellations Pending'; + const interLabel = lang === 'sv' ? 'Interpellationer i fokus' + : lang === 'de' ? 'Interpellationen im Fokus' + : lang === 'fr' ? 'Interpellations en vedette' + : lang === 'es' ? 'Interpelaciones destacadas' + : lang === 'da' ? 'Forespørgsler i fokus' + : lang === 'no' ? 'Interpellasjoner i fokus' + : lang === 'fi' ? 'Välikysymykset valokeilassa' + : lang === 'nl' ? 'Interpellaties in de spotlight' + : lang === 'ar' ? 'أبرز الاستجوابات البرلمانية' + : lang === 'he' ? 'בקשות הבהרה בזרקור' + : lang === 'ja' ? '注目の質問主意書' + : lang === 'ko' ? '주목할 대정부 질문' + : lang === 'zh' ? '质询聚焦' + : 'Interpellation Spotlight'; content += `\n

${interLabel}

\n`; interpellations.slice(0, 8).forEach(interp => { const rec = interp as Record; @@ -197,9 +275,9 @@ export function generateWeekAheadContent(data: WeekAheadData, lang: Language | s const party = rec['parti'] ? ` (${escapeHtml(rec['parti'])})` : ''; const dok_id = rec['dok_id'] ?? ''; const iUrl = dok_id ? sanitizeUrl(`https://riksdagen.se/sv/dokument-och-lagar/dokument/${encodeURIComponent(dok_id)}/`) : ''; - // Extract clean summary: content starts after "till MINISTER\n" line + // Extract minister and clean summary from the header lines const rawSummary = rec['summary'] ?? ''; - // Find start of actual content after the header lines (Interpellation NNN / av AUTHOR / till MINISTER) + const ministerName = extractMinister(rawSummary); const tillMatch = rawSummary.match(/\btill\s+[^\n]+\n\s*/i); const contentStart = tillMatch ? rawSummary.indexOf(tillMatch[0]) + tillMatch[0].length @@ -215,6 +293,7 @@ export function generateWeekAheadContent(data: WeekAheadData, lang: Language | s content += `
\n`; content += `

${iUrl ? `` : ''}${svSpan(escapeHtml(titleText), lang)}${iUrl ? '' : ''}

\n`; if (party) content += `

${escapeHtml(party)}

\n`; + if (ministerName) content += `

→ ${svSpan(escapeHtml(ministerName), lang)}

\n`; if (cleanedSummary) content += `

${svSpan(escapeHtml(cleanedSummary) + '…', lang)}

\n`; content += `
\n`; }); diff --git a/scripts/news-types/week-ahead.ts b/scripts/news-types/week-ahead.ts index cf31a9ff..bcf31c96 100644 --- a/scripts/news-types/week-ahead.ts +++ b/scripts/news-types/week-ahead.ts @@ -41,16 +41,12 @@ * 6. **Party Positioning**: Known party stances on upcoming votes * 7. **International Context**: EU/Nordic cooperation dimensions * - * **MCP DATA SOURCE:** - * Primary tool: get_calendar_events - * - Retrieves riksdag calendar for specified date range - * - Includes session times, committee assignments, topics - * - Enables systematic prospective coverage - * - * TODO: Implement additional tools for comprehensive analysis: - * - search_dokument: Find related policy documents for calendar items - * - get_fragor: Written questions related to upcoming debates - * - get_interpellationer: Interpellations (parliamentary questions) for upcoming sessions + * **MCP DATA SOURCES (all five tools actively used):** + * - get_calendar_events: upcoming committee/chamber sessions (primary driver) + * - search_dokument: policy documents cross-referenced per calendar event (Policy Context boxes) + * - search_anforanden: recent speeches providing debate context + * - get_fragor: written questions linked to upcoming debates (Questions to Watch section) + * - get_interpellationer: interpellations enriched with minister response context (Interpellation Spotlight) * * **OPERATIONAL WORKFLOW:** * 1. Calculate Date Range: Get calendar for next 7 calendar days diff --git a/tests/news-types/week-ahead.test.ts b/tests/news-types/week-ahead.test.ts index 8982e261..9bd9c456 100644 --- a/tests/news-types/week-ahead.test.ts +++ b/tests/news-types/week-ahead.test.ts @@ -436,6 +436,97 @@ describe('Week-Ahead Article Generation', () => { }); }); + describe('Enhanced Cross-Referencing', () => { + it('should show Policy Context box when event organ matches document organ', async () => { + // Provide a high-priority event (contains 'EU' to pass isHighPriority) with organ matching the doc + mockClientInstance.fetchCalendarEvents.mockResolvedValue([{ + id: '1', title: 'EU budget vote', date: '2026-02-16', type: 'chamber', organ: 'Kammaren', + }]); + mockClientInstance.searchDocuments.mockResolvedValue([{ + titel: 'Budget Proposition 2026', + dok_id: 'H901prop1', + doktyp: 'prop', + organ: 'Kammaren', + }]); + + const result = await weekAheadModule.generateWeekAhead({ languages: ['en'] }); + expect(result.success).toBe(true); + const article = result.articles[0]!; + // 'EU budget vote' event has organ 'Kammaren', matching the document's organ + expect(article.html).toContain('Policy Context'); + expect(article.html).toContain('policy-context-box'); + }); + + it('should show Questions to Watch section label', async () => { + mockClientInstance.fetchWrittenQuestions.mockResolvedValue([{ + titel: 'Question about budget funding', + dok_id: 'H901fr1', + parti: 'V', + }]); + + const result = await weekAheadModule.generateWeekAhead({ languages: ['en'] }); + expect(result.success).toBe(true); + const article = result.articles[0]!; + expect(article.html).toContain('Questions to Watch'); + }); + + it('should show Swedish Questions to Watch label in sv version', async () => { + mockClientInstance.fetchWrittenQuestions.mockResolvedValue([{ + titel: 'Fråga om budgetanslaget', + dok_id: 'H901fr2', + parti: 'S', + }]); + + const result = await weekAheadModule.generateWeekAhead({ languages: ['sv'] }); + expect(result.success).toBe(true); + const article = result.articles[0]!; + expect(article.html).toContain('Frågor att bevaka'); + }); + + it('should show Interpellation Spotlight section label', async () => { + mockClientInstance.fetchInterpellations.mockResolvedValue([{ + titel: 'Question about housing policy', + dok_id: 'H901ip1', + parti: 'S', + summary: 'Interpellation 2025/26:1\nav John Doe\ntill Statsminister Ulf Kristersson\nDetta är en fråga om bostadspolitiken.', + }]); + + const result = await weekAheadModule.generateWeekAhead({ languages: ['en'] }); + expect(result.success).toBe(true); + const article = result.articles[0]!; + expect(article.html).toContain('Interpellation Spotlight'); + }); + + it('should extract and display minister name from interpellation summary', async () => { + mockClientInstance.fetchInterpellations.mockResolvedValue([{ + titel: 'Question about housing policy', + dok_id: 'H901ip2', + parti: 'MP', + summary: 'Interpellation 2025/26:2\nav Jane Doe\ntill Statsminister Ulf Kristersson\nFråga om bostadspolitiken.', + }]); + + const result = await weekAheadModule.generateWeekAhead({ languages: ['en'] }); + expect(result.success).toBe(true); + const article = result.articles[0]!; + expect(article.html).toContain('minister-target'); + expect(article.html).toContain('Statsminister Ulf Kristersson'); + }); + + it('should show Swedish Interpellation Spotlight label in sv version', async () => { + mockClientInstance.fetchInterpellations.mockResolvedValue([{ + titel: 'Interpellation om miljöpolitik', + dok_id: 'H901ip3', + parti: 'MP', + summary: 'Interpellation 2025/26:3\nav Eva Svensson\ntill Klimatminister Romina Pourmokhtari\nFråga om klimatpolitiken.', + }]); + + const result = await weekAheadModule.generateWeekAhead({ languages: ['sv'] }); + expect(result.success).toBe(true); + const article = result.articles[0]!; + expect(article.html).toContain('Interpellationer i fokus'); + }); + }); + describe('Integration with Writer', () => { it('should call writeArticle function if provided', async () => { const mockWriter = vi.fn(); From 15fbb2ab55d1d42262b9b55f72c274c14ccad0f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 06:49:47 +0000 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94=20?= =?UTF-8?q?typed=20organ=20field,=20tokenizer,=20minister=20regex=20robust?= =?UTF-8?q?ness,=20and=20edge-case=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> --- .../data-transformers/content-generators.ts | 36 +++-- scripts/data-transformers/types.ts | 2 + tests/news-types/week-ahead.test.ts | 136 +++++++++++++++++- 3 files changed, 158 insertions(+), 16 deletions(-) diff --git a/scripts/data-transformers/content-generators.ts b/scripts/data-transformers/content-generators.ts index 9f7b63d9..da840c31 100644 --- a/scripts/data-transformers/content-generators.ts +++ b/scripts/data-transformers/content-generators.ts @@ -50,16 +50,15 @@ const TITLE_SUFFIX_TEMPLATES: Readonly string>> = zh: t => `,包括"${t}"`, }; -/** Extract meaningful keywords from text for cross-reference matching (min 2 chars, captures EU, KU, etc.) */ +/** Extract meaningful keywords from text for cross-reference matching (min 2 chars, captures EU, KU, etc.; splits on whitespace, hyphens, and commas) */ function extractKeywords(text: string): string[] { - return text.toLowerCase().split(/\s+/).filter(w => w.length >= 2); + return text.toLowerCase().split(/[\s,–-]+/u).filter(w => w.length >= 2); } /** Find documents related to a calendar event by organ match or keyword overlap (max 3) */ function findRelatedDocuments(event: RawCalendarEvent, documents: RawDocument[]): RawDocument[] { - const rec = event as Record; - const eventOrgan = rec['organ'] ?? ''; - const keywords = extractKeywords(rec['rubrik'] ?? rec['titel'] ?? rec['title'] ?? ''); + const eventOrgan = event.organ ?? ''; + const keywords = extractKeywords(event.rubrik ?? event.titel ?? event.title ?? ''); return documents.filter(doc => { const docOrgan = doc.organ ?? doc.committee ?? ''; if (eventOrgan && docOrgan && eventOrgan.toLowerCase() === docOrgan.toLowerCase()) return true; @@ -70,18 +69,35 @@ function findRelatedDocuments(event: RawCalendarEvent, documents: RawDocument[]) /** Find written questions related to a calendar event by keyword overlap (max 3) */ function findRelatedQuestions(event: RawCalendarEvent, questions: RawDocument[]): RawDocument[] { - const rec = event as Record; - const keywords = extractKeywords(rec['rubrik'] ?? rec['titel'] ?? rec['title'] ?? ''); + const keywords = extractKeywords(event.rubrik ?? event.titel ?? event.title ?? ''); return questions.filter(q => { const qText = (q.titel ?? q.title ?? '').toLowerCase(); return keywords.some(kw => qText.includes(kw)); }).slice(0, 3); } -/** Extract targeted minister name from interpellation summary "till MINISTER" header line */ +/** Extract targeted minister name from interpellation summary "till MINISTER" header line. + * Strips trailing topic clauses ("om X", "angående Y", etc.) and punctuation. */ function extractMinister(summary: string): string { - const m = summary.match(/\btill\s+([^\n]+)/i); - return m ? m[1].trim() : ''; + // Use non-newline whitespace ([^\S\n]+) so we don't cross into the next line + const m = summary.match(/\btill[^\S\n]+([^\n]+)/i); + if (!m) return ''; + const raw = m[1].trim(); + if (!raw) return ''; + + // Remove common trailing topic clauses and punctuation + const lowerRaw = raw.toLowerCase(); + const stopPhrases = [' om ', ' angående ', ' rörande ', ' beträffande ']; + let end = raw.length; + for (const phrase of stopPhrases) { + const idx = lowerRaw.indexOf(phrase); + if (idx !== -1 && idx < end) end = idx; + } + // Cut at terminating punctuation if it comes earlier + const punctIdx = raw.search(/[?:;.,]/); + if (punctIdx !== -1 && punctIdx < end) end = punctIdx; + + return raw.slice(0, end).trim(); } export function generateWeekAheadContent(data: WeekAheadData, lang: Language | string): string { diff --git a/scripts/data-transformers/types.ts b/scripts/data-transformers/types.ts index 92f51766..a3a254fd 100644 --- a/scripts/data-transformers/types.ts +++ b/scripts/data-transformers/types.ts @@ -21,6 +21,8 @@ export interface RawCalendarEvent { description?: string; details?: string; dayName?: string; + /** Organ/committee identifier returned by the MCP calendar API (e.g. 'Kammaren', 'FiU') */ + organ?: string; } /** Raw document from MCP server */ diff --git a/tests/news-types/week-ahead.test.ts b/tests/news-types/week-ahead.test.ts index 9bd9c456..8b47449e 100644 --- a/tests/news-types/week-ahead.test.ts +++ b/tests/news-types/week-ahead.test.ts @@ -451,7 +451,9 @@ describe('Week-Ahead Article Generation', () => { const result = await weekAheadModule.generateWeekAhead({ languages: ['en'] }); expect(result.success).toBe(true); - const article = result.articles[0]!; + expect(result.articles.length).toBeGreaterThan(0); + const article = result.articles[0]; + if (!article) throw new Error('Expected article to be generated'); // 'EU budget vote' event has organ 'Kammaren', matching the document's organ expect(article.html).toContain('Policy Context'); expect(article.html).toContain('policy-context-box'); @@ -466,7 +468,9 @@ describe('Week-Ahead Article Generation', () => { const result = await weekAheadModule.generateWeekAhead({ languages: ['en'] }); expect(result.success).toBe(true); - const article = result.articles[0]!; + expect(result.articles.length).toBeGreaterThan(0); + const article = result.articles[0]; + if (!article) throw new Error('Expected article to be generated'); expect(article.html).toContain('Questions to Watch'); }); @@ -479,7 +483,9 @@ describe('Week-Ahead Article Generation', () => { const result = await weekAheadModule.generateWeekAhead({ languages: ['sv'] }); expect(result.success).toBe(true); - const article = result.articles[0]!; + expect(result.articles.length).toBeGreaterThan(0); + const article = result.articles[0]; + if (!article) throw new Error('Expected article to be generated'); expect(article.html).toContain('Frågor att bevaka'); }); @@ -493,7 +499,9 @@ describe('Week-Ahead Article Generation', () => { const result = await weekAheadModule.generateWeekAhead({ languages: ['en'] }); expect(result.success).toBe(true); - const article = result.articles[0]!; + expect(result.articles.length).toBeGreaterThan(0); + const article = result.articles[0]; + if (!article) throw new Error('Expected article to be generated'); expect(article.html).toContain('Interpellation Spotlight'); }); @@ -507,7 +515,9 @@ describe('Week-Ahead Article Generation', () => { const result = await weekAheadModule.generateWeekAhead({ languages: ['en'] }); expect(result.success).toBe(true); - const article = result.articles[0]!; + expect(result.articles.length).toBeGreaterThan(0); + const article = result.articles[0]; + if (!article) throw new Error('Expected article to be generated'); expect(article.html).toContain('minister-target'); expect(article.html).toContain('Statsminister Ulf Kristersson'); }); @@ -522,9 +532,123 @@ describe('Week-Ahead Article Generation', () => { const result = await weekAheadModule.generateWeekAhead({ languages: ['sv'] }); expect(result.success).toBe(true); - const article = result.articles[0]!; + expect(result.articles.length).toBeGreaterThan(0); + const article = result.articles[0]; + if (!article) throw new Error('Expected article to be generated'); expect(article.html).toContain('Interpellationer i fokus'); }); + + it('should not render minister-target when summary has no "till" line', async () => { + mockClientInstance.fetchInterpellations.mockResolvedValue([{ + titel: 'Question about transport policy', + dok_id: 'H901ip4', + parti: 'M', + summary: 'Interpellation 2025/26:4\nav Alex Example\nDetta är en fråga om transportpolitiken utan specifik ministerrad.', + }]); + + const result = await weekAheadModule.generateWeekAhead({ languages: ['en'] }); + expect(result.success).toBe(true); + expect(result.articles.length).toBeGreaterThan(0); + const article = result.articles[0]; + if (!article) throw new Error('Expected article to be generated'); + expect(article.html).not.toContain('minister-target'); + }); + + it('should not render minister-target when "till" line has no name', async () => { + mockClientInstance.fetchInterpellations.mockResolvedValue([{ + titel: 'Question about education policy', + dok_id: 'H901ip5', + parti: 'L', + summary: 'Interpellation 2025/26:5\nav Chris Example\ntill \nDetta är en fråga om utbildningspolitiken.', + }]); + + const result = await weekAheadModule.generateWeekAhead({ languages: ['en'] }); + expect(result.success).toBe(true); + expect(result.articles.length).toBeGreaterThan(0); + const article = result.articles[0]; + if (!article) throw new Error('Expected article to be generated'); + expect(article.html).not.toContain('minister-target'); + }); + + it('should handle multiple "till" patterns without breaking rendering', async () => { + mockClientInstance.fetchInterpellations.mockResolvedValue([{ + titel: 'Question about climate and finance policy', + dok_id: 'H901ip6', + parti: 'C', + summary: 'Interpellation 2025/26:6\nav Pat Example\ntill Klimatminister Romina Pourmokhtari\noch till Finansminister Elisabeth Svantesson\nDetta är en fråga om klimat- och finanspolitiken.', + }]); + + const result = await weekAheadModule.generateWeekAhead({ languages: ['en'] }); + expect(result.success).toBe(true); + expect(result.articles.length).toBeGreaterThan(0); + const article = result.articles[0]; + if (!article) throw new Error('Expected article to be generated'); + expect(article.html).toContain('Interpellation Spotlight'); + }); + + it('should not render Policy Context box when no documents or questions match', async () => { + mockClientInstance.fetchCalendarEvents.mockResolvedValue([{ + id: '1', title: 'EU summit debate', date: '2026-02-16', type: 'chamber', organ: 'FiU', + }]); + // Documents have a completely unrelated organ, so no match occurs + mockClientInstance.searchDocuments.mockResolvedValue([{ + titel: 'Miljöpolitik rapport', + dok_id: 'H901mot1', + doktyp: 'mot', + organ: 'MjU', + }]); + + const result = await weekAheadModule.generateWeekAhead({ languages: ['en'] }); + expect(result.success).toBe(true); + expect(result.articles.length).toBeGreaterThan(0); + const article = result.articles[0]; + if (!article) throw new Error('Expected article to be generated'); + expect(article.html).not.toContain('policy-context-box'); + }); + + it('should limit Policy Context related documents to 3 per event', async () => { + mockClientInstance.fetchCalendarEvents.mockResolvedValue([{ + id: '1', title: 'EU summit vote', date: '2026-02-16', type: 'chamber', organ: 'Kammaren', + }]); + // 5 matching documents — only up to 3 should appear in the policy context box + mockClientInstance.searchDocuments.mockResolvedValue([ + { titel: 'EU Doc 1', dok_id: 'doc1', organ: 'Kammaren' }, + { titel: 'EU Doc 2', dok_id: 'doc2', organ: 'Kammaren' }, + { titel: 'EU Doc 3', dok_id: 'doc3', organ: 'Kammaren' }, + { titel: 'EU Doc 4', dok_id: 'doc4', organ: 'Kammaren' }, + { titel: 'EU Doc 5', dok_id: 'doc5', organ: 'Kammaren' }, + ]); + + const result = await weekAheadModule.generateWeekAhead({ languages: ['en'] }); + expect(result.success).toBe(true); + expect(result.articles.length).toBeGreaterThan(0); + const article = result.articles[0]; + if (!article) throw new Error('Expected article to be generated'); + // Should show a policy context box with at most 3 entries + expect(article.html).toContain('policy-context-box'); + const contextBoxCount = (article.html.match(/policy-context-box/g) ?? []).length; + expect(contextBoxCount).toBeGreaterThanOrEqual(1); + }); + + it('should still render Interpellation Spotlight for high-priority event with no keyword match', async () => { + mockClientInstance.fetchCalendarEvents.mockResolvedValue([{ + id: '1', title: 'Vote on EU directive', date: '2026-02-18', type: 'chamber', organ: 'Kammaren', + }]); + // Interpellation topic is completely unrelated to the event — Policy Context box NOT shown, but spotlight still shown + mockClientInstance.fetchInterpellations.mockResolvedValue([{ + titel: 'Fråga om infrastruktur', + dok_id: 'H901ip7', + parti: 'S', + summary: 'Interpellation 2025/26:7\nav Test Person\ntill Infrastrukturminister Andreas Carlson\nOm vägnätets underhåll.', + }]); + + const result = await weekAheadModule.generateWeekAhead({ languages: ['en'] }); + expect(result.success).toBe(true); + expect(result.articles.length).toBeGreaterThan(0); + const article = result.articles[0]; + if (!article) throw new Error('Expected article to be generated'); + expect(article.html).toContain('Interpellation Spotlight'); + }); }); describe('Integration with Writer', () => {