const { describe, it } = require('node:test'); const assert = require('node:assert'); const { checkCitation, extractEntities, computeCoverage, detectFabricatedCitations } = require('../src/citation-checker'); describe('citation_checker', () => { describe('extractEntities', () => { it('extracts EUR amounts with € symbol', () => { const entities = extractEntities('Free shipping for orders over €200.'); const amountEntities = entities.filter(e => e.startsWith('amount:')); assert.ok(amountEntities.some(e => e === 'amount:200'), `Expected amount:200, got ${amountEntities}`); }); it('extracts EUR amounts with EUR prefix', () => { const entities = extractEntities('The minimum order is EUR 30.'); const amountEntities = entities.filter(e => e.startsWith('amount:')); assert.ok(amountEntities.some(e => e === 'amount:30'), `Expected amount:30, got ${amountEntities}`); }); it('extracts EUR amounts with suffix', () => { const entities = extractEntities('Total cost: 150€'); const amountEntities = entities.filter(e => e.startsWith('amount:')); assert.ok(amountEntities.some(e => e === 'amount:150'), `Expected amount:150, got ${amountEntities}`); }); it('extracts EUR amounts with "euros" suffix', () => { const entities = extractEntities('Shipping costs 5 euros.'); const amountEntities = entities.filter(e => e.startsWith('amount:')); assert.ok(amountEntities.some(e => e === 'amount:5'), `Expected amount:5, got ${amountEntities}`); }); it('extracts percentages', () => { const entities = extractEntities('We offer a 10% discount for members.'); const pctEntities = entities.filter(e => e.startsWith('pct:')); assert.ok(pctEntities.some(e => e === 'pct:10'), `Expected pct:10, got ${pctEntities}`); }); it('extracts time limits in days', () => { const entities = extractEntities('You have 30 days to return items.'); const timeEntities = entities.filter(e => e.startsWith('time:')); assert.ok(timeEntities.length > 0, `Expected time entities, got ${entities}`); }); it('extracts time limits in months', () => { const entities = extractEntities('Warranty lasts 6 months.'); const timeEntities = entities.filter(e => e.startsWith('time:')); assert.ok(timeEntities.length > 0, `Expected time entities, got ${entities}`); }); it('extracts EU country names', () => { const entities = extractEntities('We ship to all EU countries including Netherlands and Germany.'); const countryEntities = entities.filter(e => e.startsWith('country:')); assert.ok(countryEntities.some(e => e === 'country:netherlands'), `Expected country:netherlands, got ${countryEntities}`); assert.ok(countryEntities.some(e => e === 'country:germany'), `Expected country:germany, got ${countryEntities}`); }); it('extracts English policy terms', () => { const entities = extractEntities('Our refund and return policy covers shipping.'); const policyEntities = entities.filter(e => e.startsWith('policy:')); assert.ok(policyEntities.some(e => e === 'policy:refund'), `Expected policy:refund, got ${policyEntities}`); assert.ok(policyEntities.some(e => e === 'policy:return'), `Expected policy:return, got ${policyEntities}`); assert.ok(policyEntities.some(e => e === 'policy:shipping'), `Expected policy:shipping, got ${policyEntities}`); }); it('extracts number+unit combos', () => { const entities = extractEntities('Minimum order is 5 pieces.'); const unitEntities = entities.filter(e => e.startsWith('unit:')); assert.ok(unitEntities.length > 0, `Expected unit entities, got ${entities}`); }); it('returns empty array for null/undefined text', () => { assert.deepStrictEqual(extractEntities(null), []); assert.deepStrictEqual(extractEntities(undefined), []); assert.deepStrictEqual(extractEntities(''), []); }); it('handles decimal amounts', () => { const entities = extractEntities('The fee is €4.95.'); const amountEntities = entities.filter(e => e.startsWith('amount:')); assert.ok(amountEntities.some(e => e === 'amount:4.95'), `Expected amount:4.95, got ${amountEntities}`); }); }); describe('computeCoverage', () => { it('finds covered entities in KB texts', () => { const replyEntities = ['amount:200', 'policy:shipping']; const kbHitTexts = ['Free shipping for orders over €200 within Netherlands.']; const { covered, uncovered } = computeCoverage(replyEntities, kbHitTexts); assert.ok(covered.includes('amount:200'), `Expected amount:200 in covered, got ${covered}`); assert.ok(covered.includes('policy:shipping'), `Expected policy:shipping in covered, got ${covered}`); assert.strictEqual(uncovered.length, 0); }); it('identifies uncovered entities', () => { const replyEntities = ['amount:500', 'policy:shipping', 'country:spain']; const kbHitTexts = ['Free shipping for orders over €200 within Netherlands.']; const { covered, uncovered } = computeCoverage(replyEntities, kbHitTexts); assert.ok(covered.includes('policy:shipping'), `Expected policy:shipping in covered`); assert.ok(uncovered.includes('amount:500'), `Expected amount:500 in uncovered`); assert.ok(uncovered.includes('country:spain'), `Expected country:spain in uncovered`); }); it('handles KB hit objects with question/answer', () => { const replyEntities = ['amount:200', 'policy:shipping']; const kbHitTexts = [{ question: 'Shipping policy?', answer: 'Free shipping for orders over €200.', category: 'shipping' }]; const { covered } = computeCoverage(replyEntities, kbHitTexts); assert.ok(covered.includes('amount:200')); }); it('returns empty for empty reply entities', () => { const { covered, uncovered } = computeCoverage([], ['Some KB text']); assert.deepStrictEqual(covered, []); assert.deepStrictEqual(uncovered, []); }); }); describe('checkCitation', () => { it('returns null for non-KB intent (product_search)', () => { const result = checkCitation('Some product reply', ['KB text'], 'product_search'); assert.strictEqual(result.citation_score, null); assert.strictEqual(result.hallucination_flag, null); }); it('returns null for non-KB intent (order_tracking)', () => { const result = checkCitation('Your order is shipped', [], 'order_tracking'); assert.strictEqual(result.citation_score, null); assert.strictEqual(result.hallucination_flag, null); }); it('returns null for non-KB intent (inventory_check)', () => { const result = checkCitation('5 items in stock', [], 'inventory_check'); assert.strictEqual(result.citation_score, null); assert.strictEqual(result.hallucination_flag, null); }); it('computes high citation_score for well-supported reply', () => { const reply = 'Free shipping for orders over €200 within the Netherlands. We offer a 30-day return policy.'; const kbHits = [{ question: 'Shipping?', answer: 'Free shipping for orders over €200 within Netherlands.', category: 'shipping' }]; const result = checkCitation(reply, kbHits, 'policy_faq'); assert.ok(result.citation_score >= 0.5, `Expected score >= 0.5, got ${result.citation_score}`); assert.strictEqual(result.hallucination_flag, false); }); it('flags hallucination for low coverage reply', () => { const reply = 'Free shipping for orders over €200 to ALL EU countries including Spain and Italy. 50% discount on membership.'; const kbHits = ['Free shipping for orders over €200 within Netherlands only. No special discounts.']; const result = checkCitation(reply, kbHits, 'policy_faq'); assert.strictEqual(result.hallucination_flag, true); assert.ok(result.citation_score < 0.5, `Expected score < 0.5, got ${result.citation_score}`); assert.strictEqual(result.severity, 'low_coverage'); }); it('returns citation_score=0 and hallucination_flag=true for no KB hits with policy entities', () => { const reply = 'Our refund policy allows returns within 30 days. Shipping costs €5.99.'; const result = checkCitation(reply, [], 'policy_faq'); assert.strictEqual(result.citation_score, 0); assert.strictEqual(result.hallucination_flag, true); assert.strictEqual(result.severity, 'no_kb_support'); }); it('returns null for no KB hits and no factual entities', () => { const reply = 'Hello! How can I help you today?'; const result = checkCitation(reply, [], 'general'); assert.strictEqual(result.citation_score, null); assert.strictEqual(result.hallucination_flag, null); }); it('returns null for null/empty reply text', () => { const result = checkCitation(null, ['KB text'], 'policy_faq'); assert.strictEqual(result.citation_score, null); assert.strictEqual(result.hallucination_flag, null); }); it('returns citation_score=1 when no entities are extracted from reply', () => { const reply = 'Yes, that is correct.'; const kbHits = ['Some KB answer about something.']; const result = checkCitation(reply, kbHits, 'policy_faq'); // No entities = nothing to hallucinate about assert.strictEqual(result.citation_score, 1); assert.strictEqual(result.hallucination_flag, false); }); it('handles general intent with KB hits', () => { const reply = 'You can get a refund within 30 days.'; const kbHits = ['Our refund policy: 30-day return window.']; const result = checkCitation(reply, kbHits, 'general'); assert.ok(result.citation_score >= 0.5); }); }); describe('detectFabricatedCitations', () => { it('detects cross-domain URLs not in KB sources', () => { const reply = 'See our policy at https://evil-competitor.com/fake-policy for details.'; const kbSources = ['https://yehwang.com/real-policy']; const fabricated = detectFabricatedCitations(reply, kbSources); assert.strictEqual(fabricated.length, 1); assert.ok(fabricated[0].includes('evil-competitor.com')); }); it('allows same-origin navigation links not in KB sources (站内导航不算编造)', () => { const reply = 'Browse items on https://dropshipping.yehwang.com'; const kbSources = ['https://dropshipping.yehwang.com/faq/returns']; const fabricated = detectFabricatedCitations(reply, kbSources); assert.strictEqual(fabricated.length, 0); }); it('returns empty when all reply URLs are in KB sources', () => { const reply = 'See https://yehwang.com/policy for details.'; const kbSources = ['https://yehwang.com/policy']; const fabricated = detectFabricatedCitations(reply, kbSources); assert.strictEqual(fabricated.length, 0); }); it('returns empty for reply with no URLs', () => { const reply = 'Free shipping for orders over €200.'; const fabricated = detectFabricatedCitations(reply, ['https://yehwang.com/policy']); assert.strictEqual(fabricated.length, 0); }); it('returns empty for null/undefined inputs', () => { assert.deepStrictEqual(detectFabricatedCitations(null, []), []); assert.deepStrictEqual(detectFabricatedCitations('', null), []); assert.deepStrictEqual(detectFabricatedCitations('', []), []); }); it('flags fabricated cross-domain citation as severe hallucination in checkCitation', () => { const reply = 'See our policy at https://other-site.com/fake-page for details about €200 free shipping.'; const kbHits = ['Free shipping over €200.']; const kbSources = ['https://yehwang.com/real-shipping-policy']; const result = checkCitation(reply, kbHits, 'policy_faq', kbSources); assert.strictEqual(result.hallucination_flag, true); assert.strictEqual(result.severity, 'fabricated_citation'); assert.strictEqual(result.citation_score, 0); }); }); });