// @vitest-environment node /** * Das lokale Fehlerprotokoll. * * **Der Anlass.** Sämtliche Fehler des Hauptprozesses landeten bis 0.22.0 * ausschließlich in `console.error` – und die sieht im gepackten Programm * niemand, weil es ohne Konsole startet. Der Rückmeldeweg dieser Anwendung * ist eine E-Mail; wer „es stürzt manchmal ab“ melden wollte, hatte nichts in * der Hand, und wer die Meldung bearbeiten sollte, bekam nichts Greifbares. * * **Was hier vor allem geprüft wird**, ist nicht das Schreiben, sondern das * Weglassen: Eine Datei, die zwar hilft, aber nebenbei den Anmeldenamen * mitschreibt, wäre ein schlechterer Tausch als gar keine. Die Anwendung * verspricht an drei Stellen, dass nichts Personenbezogenes das Gerät * verlässt – diese Datei ist dazu gedacht, von Hand verschickt zu werden. */ import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { entpersoniert, fehlerZeile, MAX_BYTES, protokollEinrichten, protokollieren, protokollPfad, protokollVergessen, PROTOKOLL_DATEI, } from '../src/main/protokoll'; let verzeichnis = ''; function inhalt(): string { const pfad = join(verzeichnis, PROTOKOLL_DATEI); return existsSync(pfad) ? readFileSync(pfad, 'utf8') : ''; } beforeEach(() => { verzeichnis = mkdtempSync(join(tmpdir(), 'wsk-protokoll-')); protokollEinrichten(verzeichnis); }); afterEach(() => { protokollVergessen(); rmSync(verzeichnis, { recursive: true, force: true }); }); describe('Personenbezug entfernen', () => { it('ersetzt das Heimatverzeichnis, egal mit welchem Trennzeichen', () => { /* Node liefert Windows-Pfade mit Rückstrich, viele Meldungen aus Chromium und SQLite mit Schrägstrich – beide enthalten den Anmeldenamen. */ const heim = 'C:\\Users\\Erika'; expect(entpersoniert('EACCES: C:\\Users\\Erika\\AppData\\lernstand.db', heim)).toBe( 'EACCES: \\AppData\\lernstand.db', ); expect(entpersoniert('open C:/Users/Erika/AppData/lernstand.db', heim)).toBe( 'open /AppData/lernstand.db', ); }); it('macht aus jeder Meldung genau eine Zeile', () => { /* Sonst ließe sich eine Fehlermeldung so wählen, dass sie im Protokoll wie mehrere Einträge mit eigenen Zeitstempeln aussieht. */ expect(entpersoniert('erste Zeile\nzweite Zeile\r\ndritte', 'C:\\Users\\Erika')).toBe( 'erste Zeile zweite Zeile dritte', ); }); it('kürzt eine überlange Meldung', () => { const lang = entpersoniert('x'.repeat(10_000), 'C:\\Users\\Erika'); expect(lang.length).toBeLessThanOrEqual(2000); }); it('kommt ohne bekanntes Heimatverzeichnis aus', () => { expect(entpersoniert('irgendein Fehler', '')).toBe('irgendein Fehler'); }); }); describe('Schreiben', () => { it('schreibt Zeitpunkt, Bereich und Meldung', () => { protokollieren('fenster', 'Renderer beendet (crashed)', new Date('2026-08-29T18:30:00.000Z')); expect(inhalt()).toBe('2026-08-29T18:30:00.000Z [fenster] Renderer beendet (crashed)\n'); }); it('hängt weitere Einträge an, statt zu überschreiben', () => { protokollieren('a', 'erster', new Date('2026-08-29T18:30:00.000Z')); protokollieren('b', 'zweiter', new Date('2026-08-29T18:31:00.000Z')); expect(inhalt().trimEnd().split('\n')).toHaveLength(2); }); it('nimmt den Anmeldenamen auch beim Schreiben heraus', () => { /* Die Reinigung sitzt an genau einer Stelle – hier wird gemessen, dass der Schreibweg sie wirklich benutzt und nicht daran vorbeiführt. */ protokollieren( 'fehler', `EACCES: open ${join(process.env['USERPROFILE'] ?? process.env['HOME'] ?? '', 'geheim.db')}`, ); expect(inhalt()).toContain(''); }); it('schweigt, solange kein Ablageort feststeht', () => { /* Ohne Einrichten darf nichts entstehen: Sonst legte ein früh auftretender Fehler eine Datei an einer beliebigen Stelle an. */ protokollVergessen(); protokollieren('fehler', 'darf nirgends landen'); expect(existsSync(join(verzeichnis, PROTOKOLL_DATEI))).toBe(false); }); it('bringt die Anwendung nicht zu Fall, wenn nicht geschrieben werden kann', () => { /* Ein Protokoll, das den Start verhindert, wäre schlimmer als keines. Hier steht ein Verzeichnis an der Stelle der Datei. */ protokollVergessen(); const eng = join(verzeichnis, 'eng'); writeFileSync(join(verzeichnis, 'sperre'), 'x'); protokollEinrichten(join(verzeichnis, 'sperre')); expect(() => { protokollieren('fehler', 'irgendetwas'); }).not.toThrow(); expect(existsSync(eng)).toBe(false); }); it('nennt seinen Ablageort für den Systemzustand', () => { expect(protokollPfad()).toBe(join(verzeichnis, PROTOKOLL_DATEI)); protokollVergessen(); expect(protokollPfad()).toBeNull(); }); }); describe('Obergrenze', () => { it('schneidet die ältere Hälfte weg, statt endlos zu wachsen', () => { const pfad = join(verzeichnis, PROTOKOLL_DATEI); /* Eine überfüllte Datei mit erkennbarem Anfang und Ende. */ const zeile = `${'a'.repeat(200)}\n`; writeFileSync( pfad, `ALTESTE_ZEILE\n${zeile.repeat(Math.ceil(MAX_BYTES / 200))}JUENGSTE_ZEILE\n`, ); expect(statSync(pfad).size).toBeGreaterThan(MAX_BYTES); protokollieren('fehler', 'neuer Eintrag'); const danach = inhalt(); expect(statSync(pfad).size).toBeLessThan(MAX_BYTES); expect(danach).not.toContain('ALTESTE_ZEILE'); expect(danach).toContain('JUENGSTE_ZEILE'); expect(danach).toContain('neuer Eintrag'); expect(danach).toContain('[gekürzt]'); }); }); describe('Fehler zu einer Zeile machen', () => { it('nennt Meldung und die erste Stelle der Aufrufliste', () => { const zeile = fehlerZeile(new Error('Etwas ging schief')); expect(zeile).toContain('Etwas ging schief'); }); it('kommt auch mit einem geworfenen Nicht-Fehler zurecht', () => { expect(fehlerZeile('nur ein Text')).toBe('nur ein Text'); expect(fehlerZeile(undefined)).toBe('undefined'); }); });