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) <noreply@anthropic.com>
This commit is contained in:
@@ -295,3 +295,169 @@ describe('multi-turn conversation (anonymizeTurn)', () => {
|
|||||||
expect(session.history).toHaveLength(4);
|
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<string, string> };
|
||||||
|
|
||||||
|
async function runConversation(turns: Turn[]): Promise<AnonymizerSession> {
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user