import { describe, expect, it } from 'vitest' import { buildMessage, groupByProject, windowFor } from '~/lib/weekly-report' import type { PostListItem } from '~/data/repositories/posts' function item(overrides: Partial = {}): PostListItem { return { id: 'c0ffee00-0000-4000-8000-000000000001', slug: 'regel-engine', title: 'Regel-Engine: Bedingung trifft Aktion', teaser: null, type: { id: 't1', key: 'feature', labelDe: 'Feature', labelEn: 'Feature', color: '#2b4a9b' }, audience: 'internal', publishAt: new Date('2026-07-30T09:00:00.000Z'), authorName: 'Paul', coverMediaId: null, number: 142, projectSlug: 'trakk', projectName: 'Trakk', projectCode: 'TRK', projectColor: '#2e7d5b', ...overrides, } } const origin = 'https://logbuch.nyo.de' describe('windowFor', () => { it('spannt sieben Tage bis zum Aufruf', () => { const { from, to } = windowFor(new Date('2026-08-03T06:00:00.000Z')) expect(to.toISOString()).toBe('2026-08-03T06:00:00.000Z') expect(from.toISOString()).toBe('2026-07-27T06:00:00.000Z') }) }) describe('groupByProject', () => { it('fasst Einträge je Projekt zusammen und behält die Reihenfolge', () => { const groups = groupByProject([ item(), item({ id: 'b', slug: 'zwei', projectSlug: 'orbit', projectName: 'Orbit', projectCode: 'ORB' }), item({ id: 'c', slug: 'drei', number: 143 }), ]) expect(groups.map(group => group.slug)).toEqual(['trakk', 'orbit']) expect(groups[0]!.items).toHaveLength(2) }) }) describe('buildMessage', () => { const base = { from: new Date('2026-07-27T06:00:00.000Z'), to: new Date('2026-08-03T06:00:00.000Z'), } it('bleibt still, wenn nichts veröffentlicht wurde', () => { expect(buildMessage({ ...base, total: 0, projects: [] }, origin)).toBeNull() }) it('nennt Zeitraum, Anzahl und jedes Projekt', () => { const projects = groupByProject([item(), item({ id: 'b', slug: 'zwei', projectSlug: 'orbit', projectName: 'Orbit', projectCode: 'ORB' })]) const message = buildMessage({ ...base, total: 2, projects }, origin) expect(message?.text).toBe('Logbuch, 27.07.2026 bis 03.08.2026: 2 Einträge') const dump = JSON.stringify(message?.blocks) expect(dump).toContain('27.07.2026 bis 03.08.2026') expect(dump).toContain('|Trakk>*') expect(dump).toContain('|Orbit>*') expect(dump).toContain('TRK-0142') expect(dump).toContain('https://logbuch.nyo.de/trakk/regel-engine') }) it('zählt einen einzelnen Eintrag im Singular', () => { const message = buildMessage({ ...base, total: 1, projects: groupByProject([item()]) }, origin) expect(message?.text).toContain('1 Eintrag') }) it('verzichtet auf Emojis', () => { const message = buildMessage({ ...base, total: 1, projects: groupByProject([item()]) }, origin) expect(JSON.stringify(message)).not.toMatch(/\p{Extended_Pictographic}/u) }) }) describe('Karten', () => { const base = { from: new Date('2026-07-27T06:00:00.000Z'), to: new Date('2026-08-03T06:00:00.000Z'), } it('setzt Anreißer und Bild an den Eintrag', () => { const cover = new Map([['c0ffee00-0000-4000-8000-000000000001', { postId: 'c0ffee00-0000-4000-8000-000000000001', mediaId: 'm1', alt: 'Regelmaske mit Bedingung und Aktion', caption: null, width: 1600, height: 900, variants: [{ width: 480, format: 'webp', path: 'trakk/m1/480.webp' }], }]]) const projects = groupByProject([item({ teaser: 'Tickets reagieren jetzt selbst.' })]) const message = buildMessage({ ...base, total: 1, projects }, origin, cover as never) const dump = JSON.stringify(message?.blocks) expect(dump).toContain('Tickets reagieren jetzt selbst.') expect(dump).toContain('https://logbuch.nyo.de/api/v1/media/file/trakk/m1/480.webp') expect(dump).toContain('Regelmaske mit Bedingung und Aktion') }) it('kürzt lange Anreißer', () => { const long = `${'Wort '.repeat(80)}Ende` const projects = groupByProject([item({ teaser: long })]) const dump = JSON.stringify(buildMessage({ ...base, total: 1, projects }, origin)?.blocks) expect(dump).toContain('...') expect(dump).not.toContain('Ende') }) it('zeigt höchstens drei Karten je Projekt, der Rest kommt als Liste', () => { const many = Array.from({ length: 5 }, (_, index) => item({ id: `c0ffee00-0000-4000-8000-00000000000${index}`, slug: `beitrag-${index}`, number: 140 + index, title: `Beitrag ${index}`, })) const message = buildMessage({ ...base, total: 5, projects: groupByProject(many) }, origin) const sections = (message?.blocks ?? []).filter(block => (block as { type: string }).type === 'section') expect(sections).toHaveLength(5) }) })