import { expect, test } from "bun:test"; import { buildLlmDossier, createPseudonymizer, reversePseudonymsInText, reversePseudonymsInDossier, } from "./privacy"; import type { LlmDossier, SummaryData } from "./privacy"; const sample: SummaryData = { kontakt: { unternehmen: { name: "Muster GmbH", adresse: "Hauptstrasse 1", plz: "80331 Muenchen", telefon: "+49 89 123456", email: "kontakt@muster.de", web: "www.muster.de", }, ansprechpartner: { name: "Max Mustermann", telefon: "0170 1234567", email: "max.mustermann@muster.de", }, }, unternehmen: { branche: "Maschinenbau", gruendungsjahr: "1998", anzahl_mitarbeiter: "120", interne_notiz: "Nicht fuer LLM", }, fragen: { frage1: { label: "Frage 1", antwort: "Max Mustermann beschreibt die Entwicklung der Muster GmbH. Muster investiert weiter. Kontakt: max.mustermann@muster.de.", fragen: [ { id: "frage1_1", text: "Was macht das Unternehmen aus?", antwort: "Die Muster GmbH ist unter +49 89 123456 erreichbar.", }, ], }, }, kriterium: { robustheit_resilienz: { label: "Robustheit", antwort: "Die Muster GmbH hat ein Risikomanagement etabliert. Muster nutzt Fruehwarnprozesse.", }, }, }; test("builds a minimized dossier without contact data or unknown company fields", () => { const dossier = buildLlmDossier(sample); const serialized = JSON.stringify(dossier); expect(dossier.unternehmen).toEqual({ branche: "Maschinenbau", gruendungsjahr: "1998", anzahl_mitarbeiter: "120", }); expect(serialized).not.toContain("kontakt"); expect(serialized).not.toContain("interne_notiz"); expect(serialized).not.toContain("Hauptstrasse"); }); test("pseudonymizes known personal and contact values in LLM-bound strings", () => { const privacy = createPseudonymizer(sample); const prepared = privacy.pseudonymizeDossier(buildLlmDossier(sample)); const serialized = JSON.stringify(prepared.safe); expect(serialized).toContain("[PERSON_1]"); expect(serialized).toContain("[UNTERNEHMEN]"); expect(serialized).toContain("[EMAIL_2]"); expect(serialized).toContain("[TELEFON_1]"); expect(serialized).not.toContain("Max Mustermann"); expect(serialized).not.toContain("Muster GmbH"); expect(serialized).not.toContain("Muster investiert"); expect(serialized).not.toContain("Muster nutzt"); expect(serialized).not.toContain("max.mustermann@muster.de"); expect(prepared.audit.mode).toBe("minimized+pseudonymized"); expect(prepared.audit.companyName).toBe("Muster GmbH"); expect(prepared.audit.replacements.length).toBeGreaterThan(0); }); test("reversePseudonymsInText restores original values from known replacements", () => { const text = "[PERSON_1] von [UNTERNEHMEN] nutzt [EMAIL_2]."; const reversed = reversePseudonymsInText(text, [ { value: "Max Mustermann", type: "person", placeholder: "[PERSON_1]" }, { value: "Muster GmbH", type: "company", placeholder: "[UNTERNEHMEN]" }, { value: "max.mustermann@muster.de", type: "email", placeholder: "[EMAIL_2]" }, ]); expect(reversed).toBe("Max Mustermann von Muster GmbH nutzt max.mustermann@muster.de."); }); test("reversePseudonyms works via the Pseudonymizer API", () => { const privacy = createPseudonymizer(sample); const reversed = privacy.reversePseudonyms("[PERSON_1] arbeitet bei [UNTERNEHMEN]."); expect(reversed).toContain("Max Mustermann"); expect(reversed).toContain("Muster GmbH"); }); test("reversePseudonymsInText handles text without placeholders", () => { const text = "Kein Platzhalter hier."; const reversed = reversePseudonymsInText(text, [ { value: "Max Mustermann", type: "person", placeholder: "[PERSON_1]" }, ]); expect(reversed).toBe(text); }); test("reversePseudonymsInDossier reverses nested text fields including zusammenfassung", () => { const dossier: LlmDossier = { fragen: { frage1: { label: "[PERSON_1] von [UNTERNEHMEN] beantwortet", zusammenfassung: "[PERSON_1] arbeitet bei [UNTERNEHMEN] und nutzt [EMAIL_2].", fragen: [ { id: "f1", text: "Was macht [UNTERNEHMEN]?", antwort: "[PERSON_1] sagt [EMAIL_2]." }, ], }, }, kriterium: { robustheit: { label: "Robustheit von [PERSON_1]", zusammenfassung: "[UNTERNEHMEN] ist robust.", }, }, unternehmen: { referenzen: "Referenz von [PERSON_1] bei [UNTERNEHMEN].", }, }; const reversed = reversePseudonymsInDossier(dossier, [ { value: "Max Mustermann", type: "person", placeholder: "[PERSON_1]" }, { value: "Muster GmbH", type: "company", placeholder: "[UNTERNEHMEN]" }, { value: "max@muster.de", type: "email", placeholder: "[EMAIL_2]" }, ]); expect(reversed.fragen?.frage1?.zusammenfassung).toBe( "Max Mustermann arbeitet bei Muster GmbH und nutzt max@muster.de.", ); expect(reversed.fragen?.frage1?.fragen?.[0]?.antwort).toBe( "Max Mustermann sagt max@muster.de.", ); expect(reversed.fragen?.frage1?.label).toBe( "Max Mustermann von Muster GmbH beantwortet", ); expect(reversed.kriterium?.robustheit?.zusammenfassung).toBe( "Muster GmbH ist robust.", ); expect(reversed.unternehmen?.referenzen).toBe( "Referenz von Max Mustermann bei Muster GmbH.", ); }); test("reversePseudonymsInText handles edge case with numeric suffixes in text", () => { const text = "[PERSON_1] und [PERSON_1]."; const reversed = reversePseudonymsInText(text, [ { value: "Max Mustermann", type: "person", placeholder: "[PERSON_1]" }, ]); expect(reversed).toBe("Max Mustermann und Max Mustermann."); });