From d119c366b715f6ce47a4949e34ebf6b0130fbeb6 Mon Sep 17 00:00:00 2001 From: Mobiletic Date: Wed, 1 Jul 2026 14:36:36 +0100 Subject: [PATCH] test: add 5 multi-turn conversation examples Scripted, deterministic conversations (fictional Swiss e-learning chats) via a mock provider, each asserting exact per-turn round-trips + stable entity ids: login support (attributes attach to PER_1), two learners (applyKnown reuse of a re-mentioned name), old/new IBAN kept distinct, parent+minor+doctor (3 people), and HR (manager+org reused, two employees). 46 tests total. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/anonymizer.test.ts | 166 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/test/anonymizer.test.ts b/test/anonymizer.test.ts index f5d0792..fa8718e 100644 --- a/test/anonymizer.test.ts +++ b/test/anonymizer.test.ts @@ -295,3 +295,169 @@ describe('multi-turn conversation (anonymizeTurn)', () => { expect(session.history).toHaveLength(4); }); }); + +/** + * Five multi-turn conversation examples (fictional Swiss e-learning chats), each + * driven by a deterministic scripted provider. Every turn must restore exactly, + * and entity ids must stay stable across the whole conversation. + */ +describe('conversation examples (multi-turn scenarios)', () => { + type Turn = { text: string; anon: string; mapping: Record }; + + async function runConversation(turns: Turn[]): Promise { + const anonymizeInConversation = vi.fn(); + for (const t of turns) { + anonymizeInConversation.mockResolvedValueOnce({ anon: t.anon, mapping: t.mapping, legend: {} }); + } + const a = new Anonymizer({ llm: failingLlm({ anonymizeInConversation }) }); + let session: AnonymizerSession = { mapping: {}, legend: {}, history: [] }; + for (const t of turns) { + const r = await a.anonymizeTurn(t.text, session); + session = r.session; + expect(a.deanonymize(r.anon, r.mapping)).toBe(t.text); // exact round-trip, every turn + } + expect(session.history).toHaveLength(turns.length); + return session; + } + + it('1 — login support: attributes attach to the same learner (PER_1) across turns', async () => { + const s = await runConversation([ + { + text: "Bonjour, je suis Élodie Vogt et je n'arrive plus à me connecter.", + anon: "Bonjour, je suis [PER_1.PRENOM:F] [PER_1.NOM:F] et je n'arrive plus à me connecter.", + mapping: { '[PER_1.PRENOM:F]': 'Élodie', '[PER_1.NOM:F]': 'Vogt' }, + }, + { + text: 'Mon e-mail de compte est elodie.vogt@gmx.ch.', + anon: 'Mon e-mail de compte est [PER_1.EMAIL:Perso].', + mapping: { '[PER_1.EMAIL:Perso]': 'elodie.vogt@gmx.ch' }, + }, + { + text: 'Merci de réinitialiser mon mot de passe pour la formation « Comptabilité ».', + anon: 'Merci de réinitialiser mon mot de passe pour la formation « Comptabilité ».', + mapping: {}, + }, + ]); + expect(s.mapping['[PER_1.PRENOM:F]']).toBe('Élodie'); + expect(s.mapping['[PER_1.EMAIL:Perso]']).toBe('elodie.vogt@gmx.ch'); + expect(Object.keys(s.mapping).some((k) => k.startsWith('[PER_2'))).toBe(false); // only one person + }); + + it('2 — two learners: a friend introduced then re-mentioned keeps PER_2 (applyKnown reuse)', async () => { + const s = await runConversation([ + { + text: "Salut, c'est Idris Haldimann. Je veux m'inscrire à la formation « Développement web ».", + anon: "Salut, c'est [PER_1.PRENOM:M] [PER_1.NOM:M]. Je veux m'inscrire à la formation « Développement web ».", + mapping: { '[PER_1.PRENOM:M]': 'Idris', '[PER_1.NOM:M]': 'Haldimann' }, + }, + { + text: 'Mon amie Léa Bianchi aimerait aussi participer.', + anon: 'Mon amie [PER_2.PRENOM:F] [PER_2.NOM:F] aimerait aussi participer.', + mapping: { '[PER_2.PRENOM:F]': 'Léa', '[PER_2.NOM:F]': 'Bianchi' }, + }, + { + // "Léa" is re-mentioned literally → applyKnown reuses [PER_2.PRENOM:F] before the model. + text: "Léa a déjà un compte sous l'e-mail lea.bianchi@bluewin.ch.", + anon: "[PER_2.PRENOM:F] a déjà un compte sous l'e-mail [PER_2.EMAIL:Perso].", + mapping: { '[PER_2.EMAIL:Perso]': 'lea.bianchi@bluewin.ch' }, + }, + ]); + expect(s.mapping['[PER_1.PRENOM:M]']).toBe('Idris'); + expect(s.mapping['[PER_2.PRENOM:F]']).toBe('Léa'); // stable across turns 2 & 3 + expect(s.mapping['[PER_2.EMAIL:Perso]']).toBe('lea.bianchi@bluewin.ch'); + }); + + it('3 — billing: old vs new IBAN kept distinct under one person', async () => { + const s = await runConversation([ + { + text: "Bonjour, ici Bruno Keller, j'ai un souci de facturation.", + anon: "Bonjour, ici [PER_1.PRENOM:M] [PER_1.NOM:M], j'ai un souci de facturation.", + mapping: { '[PER_1.PRENOM:M]': 'Bruno', '[PER_1.NOM:M]': 'Keller' }, + }, + { + text: "Mon ancien IBAN CH51 0483 5012 3456 7800 9 n'est plus valide.", + anon: "Mon ancien IBAN [PER_1.IBAN:Ancien] n'est plus valide.", + mapping: { '[PER_1.IBAN:Ancien]': 'CH51 0483 5012 3456 7800 9' }, + }, + { + text: 'Le nouveau est CH33 0900 0000 8765 4321 0, merci.', + anon: 'Le nouveau est [PER_1.IBAN:Nouveau], merci.', + mapping: { '[PER_1.IBAN:Nouveau]': 'CH33 0900 0000 8765 4321 0' }, + }, + ]); + expect(s.mapping['[PER_1.IBAN:Ancien]']).toBe('CH51 0483 5012 3456 7800 9'); + expect(s.mapping['[PER_1.IBAN:Nouveau]']).toBe('CH33 0900 0000 8765 4321 0'); + }); + + it('4 — parent + minor child + doctor: three distinct people across turns', async () => { + const s = await runConversation([ + { + text: "Bonjour, je souhaite inscrire mon fils Timéo Moret, né le 09.09.2012, à l'atelier « Robotique ».", + anon: "Bonjour, je souhaite inscrire mon fils [PER_1.PRENOM:M] [PER_1.NOM:M], né le [PER_1.DATE_NAISSANCE:2012], à l'atelier « Robotique ».", + mapping: { + '[PER_1.PRENOM:M]': 'Timéo', + '[PER_1.NOM:M]': 'Moret', + '[PER_1.DATE_NAISSANCE:2012]': '09.09.2012', + }, + }, + { + text: "C'est moi sa mère, Sandra Baumann, qui gère le dossier ; mon numéro est 079 222 33 44.", + anon: "C'est moi sa mère, [PER_2.PRENOM:F] [PER_2.NOM:F], qui gère le dossier ; mon numéro est [PER_2.TELEPHONE:Mobile].", + mapping: { + '[PER_2.PRENOM:F]': 'Sandra', + '[PER_2.NOM:F]': 'Baumann', + '[PER_2.TELEPHONE:Mobile]': '079 222 33 44', + }, + }, + { + text: 'Le certificat médical a été établi par le Dr Rui Almeida.', + anon: 'Le certificat médical a été établi par le Dr [PER_3.PRENOM:M] [PER_3.NOM:M].', + mapping: { '[PER_3.PRENOM:M]': 'Rui', '[PER_3.NOM:M]': 'Almeida' }, + }, + ]); + expect(s.mapping['[PER_1.NOM:M]']).toBe('Moret'); // child + expect(s.mapping['[PER_2.NOM:F]']).toBe('Baumann'); // mother + expect(s.mapping['[PER_3.NOM:M]']).toBe('Almeida'); // doctor + }); + + it('5 — HR: manager + org reused, two employees added across turns', async () => { + const s = await runConversation([ + { + text: 'Bonjour, je suis Patrick Nussbaum, RH chez Migros.', + anon: 'Bonjour, je suis [PER_1.PRENOM:M] [PER_1.NOM:M], RH chez [ORG_1.NOM:Entreprise].', + mapping: { + '[PER_1.PRENOM:M]': 'Patrick', + '[PER_1.NOM:M]': 'Nussbaum', + '[ORG_1.NOM:Entreprise]': 'Migros', + }, + }, + { + text: 'Je veux inscrire Chloé Aebischer (chloe.aebischer@migros.ch) à « Sécurité ».', + anon: 'Je veux inscrire [PER_2.PRENOM:F] [PER_2.NOM:F] ([PER_2.EMAIL:Pro]) à « Sécurité ».', + mapping: { + '[PER_2.PRENOM:F]': 'Chloé', + '[PER_2.NOM:F]': 'Aebischer', + '[PER_2.EMAIL:Pro]': 'chloe.aebischer@migros.ch', + }, + }, + { + text: 'Ainsi que Deniz Yilmaz, joignable au 076 555 88 99.', + anon: 'Ainsi que [PER_3.PRENOM:U] [PER_3.NOM:U], joignable au [PER_3.TELEPHONE:Mobile].', + mapping: { + '[PER_3.PRENOM:U]': 'Deniz', + '[PER_3.NOM:U]': 'Yilmaz', + '[PER_3.TELEPHONE:Mobile]': '076 555 88 99', + }, + }, + { + text: 'La facture va à notre IBAN CH70 0076 2011 6238 5295 7.', + anon: 'La facture va à notre IBAN [ORG_1.IBAN:Entreprise].', + mapping: { '[ORG_1.IBAN:Entreprise]': 'CH70 0076 2011 6238 5295 7' }, + }, + ]); + expect(s.mapping['[ORG_1.NOM:Entreprise]']).toBe('Migros'); // org stable across turns 1 & 4 + expect(s.mapping['[PER_2.PRENOM:F]']).toBe('Chloé'); + expect(s.mapping['[PER_3.NOM:U]']).toBe('Yilmaz'); + expect(s.mapping['[ORG_1.IBAN:Entreprise]']).toBe('CH70 0076 2011 6238 5295 7'); + }); +});