/** * Der Meldetext zu einer Frage. * * Zwei Dinge stehen hier auf dem Spiel: dass die Meldung **eindeutig** ist – * die amtliche Nummer allein ist es nicht – und dass **nichts** über die * meldende Person hineingerät. Der Text verlässt später das Gerät. */ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; import { KONTAKT } from '../src/shared/kontakt'; import type { Katalog } from '../src/shared/katalog'; import { meldetextBauen } from '../src/shared/meldetext'; const FRAGE = { id: 'I.1-01', amtliche_nummer: '1.01', seite: 6 }; const KATALOG = { herausgeber: 'Bundesverwaltungsamt', stand: '2024-12-16', quelldatei_sha256: '7e5e41faee0a9198b6ba3e2fe3b46ef313448b06753516579596f98212b1d108', }; const PROGRAMM = { fassung: '0.12.0', baukennung: '0218791', baustand: '2026-08-22' }; /** Der echte Katalog – für die Aussagen, die nur an ihm zu prüfen sind. */ function echterKatalog(): Katalog { const pfad = join(__dirname, '..', '..', 'content', 'katalog', 'katalog.json'); return JSON.parse(readFileSync(pfad, 'utf-8')) as Katalog; } describe('Der Meldetext', () => { it('nennt Frage, Katalogstand und Programmfassung', () => { const text = meldetextBauen(FRAGE, KATALOG, PROGRAMM); expect(text).toContain('1.01'); expect(text).toContain('I.1-01'); expect(text).toContain('Seite 6'); expect(text).toContain('Bundesverwaltungsamt'); expect(text).toContain('16.12.2024'); expect(text).toContain('0.12.0'); expect(text).toContain('0218791'); expect(text).toContain(KONTAKT.epost); }); it('kürzt die Prüfsumme und sagt, dass sie gekürzt ist', () => { /* Die volle Summe hat 64 Zeichen und spränge die Zeile. Gekürzt genügt sie, um zwei Katalogfassungen auseinanderzuhalten – aber das muss dastehen, sonst hält der Empfänger sie für die ganze. */ const text = meldetextBauen(FRAGE, KATALOG, PROGRAMM); expect(text).toContain('7e5e41faee0a'); expect(text).toContain('(gekürzt)'); expect(text).not.toContain(KATALOG.quelldatei_sha256); }); it('kommt ohne Baukennung aus', () => { /* Ohne Git zur Bauzeit gibt es keine. Dann steht dort nichts statt eines nichtssagenden „unbekannt" – dasselbe Verhalten wie im Systemzustand. */ const text = meldetextBauen(FRAGE, KATALOG, { ...PROGRAMM, baukennung: '', baustand: '' }); expect(text).toContain('Fassung 0.12.0'); expect(text).not.toContain('Bau '); }); it('führt zu jeder der 575 Fragen ihre eindeutige Kennung', () => { /* Der eigentliche Prüfstein. Nachgezählt am echten Katalog teilen 227 der 575 Fragen ihre amtliche Nummer mit mindestens einer anderen – „01" gibt es als II-01, III-01 und IV-01. Wer nur die Nummer meldet, meldet dreideutig. */ const katalog = echterKatalog(); const mehrdeutig = katalog.fragen.filter( (frage) => katalog.fragen.filter((andere) => andere.amtliche_nummer === frage.amtliche_nummer).length > 1, ); expect(mehrdeutig.length).toBeGreaterThan(200); for (const frage of katalog.fragen) { const text = meldetextBauen(frage, katalog.meta, PROGRAMM); expect(text).toContain(frage.id); } }); it('steht Zeichen für Zeichen fest – nichts kann versehentlich mitreisen', () => { /* Der stärkste Wächter, den es hier geben kann, und die eigentliche Sperre gegen den Fehler, der beim nächsten Umbau naheliegt: schnell noch den Profilnamen oder die gegebene Antwort mit hineinnehmen, weil es bei der Bearbeitung hülfe. Ein Wortfilter fängt das nicht – er kennt nur die Wörter, an die jemand vorher gedacht hat. Der vollständige Vergleich fängt jede Ergänzung, auch die, die niemand vorhergesehen hat. Ändert sich der Wortlaut absichtlich, ist diese Zeile mitzuändern; das ist der Sinn. */ expect(meldetextBauen(FRAGE, KATALOG, PROGRAMM)).toBe( [ 'Fehlermeldung zur Waffensachkunde-Lernsoftware', `Senden an: ${KONTAKT.epost}`, '', 'Frage: 1.01 (Kennung I.1-01), Seite 6 der Vorlage', 'Fragenkatalog: Bundesverwaltungsamt, Stand 16.12.2024, Prüfsumme 7e5e41faee0a (gekürzt)', 'Programm: Fassung 0.12.0, Bau 0218791 vom 22.08.2026', '', 'Diese Angaben hat die Lernsoftware zusammengestellt. Sie enthalten nichts über die', 'meldende Person und nichts über ihren Lernstand.', '', 'Was nicht stimmt (bitte hier beschreiben):', '', ].join('\n'), ); }); it('nennt keinen Pfad und keinen Rechnernamen', () => { /* Die Angaben kommen aus vier Skalaren, nicht aus `AnwendungsInfo` – dort hängt `datenbank.meldung` mit dran, die im Fehlerfall einen Dateipfad enthalten kann. Diese Prüfung hält fest, dass die Trennung wirkt. */ const text = meldetextBauen(FRAGE, KATALOG, PROGRAMM).toLowerCase(); for (const verboten of ['appdata', 'users', '.db', 'sqlite']) { expect(text).not.toContain(verboten); } }); it('trennt Zeilen einheitlich und richtet nichts mit Leerzeichen aus', () => { /* Der Text soll in einer Nur-Text-Mail genauso aussehen wie in einem Forum. Ausgerichtete Spalten halten nur in Festbreitenschrift. */ const text = meldetextBauen(FRAGE, KATALOG, PROGRAMM); expect(text).not.toContain('\r'); expect(text).not.toMatch(/\S {2,}\S/u); }); });