/** * Tests for src/utils/scriptDetector.ts * * Run with: * npx tsx --test tests/utils/scriptDetector.test.ts * * Or in package.json scripts: * "test:quality": "tsx --test tests/utils/scriptDetector.test.ts" */ import { describe, it } from 'node:test'; import { strict as assert } from 'node:assert'; import { getScript, isArabicScriptLang, evaluateChunk, evaluateDocument, detectArabicVariant, lengthCheck, patternCheck, } from '../../src/utils/scriptDetector.ts'; describe('getScript', () => { it('maps cyrillic languages', () => { assert.equal(getScript('ru'), 'cyrillic'); assert.equal(getScript('uk'), 'cyrillic'); assert.equal(getScript('be'), 'cyrillic'); }); it('maps latin languages', () => { assert.equal(getScript('en'), 'latin'); assert.equal(getScript('fr'), 'latin'); assert.equal(getScript('de'), 'latin'); assert.equal(getScript('vi'), 'latin'); }); it('maps CJK languages', () => { assert.equal(getScript('zh'), 'cjk'); assert.equal(getScript('zh-cn'), 'cjk'); }); it('maps Japanese to hiragana_katakana', () => { assert.equal(getScript('ja'), 'hiragana_katakana'); }); it('maps Korean to hangul', () => { assert.equal(getScript('ko'), 'hangul'); }); it('maps Yiddish to hebrew (not arabic)', () => { assert.equal(getScript('yi'), 'hebrew'); }); it('maps Maldivian to thaana (not arabic)', () => { assert.equal(getScript('dv'), 'thaana'); }); it('falls back to latin for unknown', () => { assert.equal(getScript('xx'), 'latin'); assert.equal(getScript(''), 'latin'); assert.equal(getScript(null), 'latin'); }); it('is case-insensitive', () => { assert.equal(getScript('FR'), 'latin'); assert.equal(getScript('ZH-CN'), 'cjk'); }); }); describe('isArabicScriptLang', () => { it('returns true for Arabic-script langs', () => { assert.equal(isArabicScriptLang('ar'), true); assert.equal(isArabicScriptLang('fa'), true); assert.equal(isArabicScriptLang('ur'), true); assert.equal(isArabicScriptLang('ckb'), true); }); it('returns false for non-Arabic langs', () => { assert.equal(isArabicScriptLang('fr'), false); assert.equal(isArabicScriptLang('he'), false); assert.equal(isArabicScriptLang('en'), false); }); }); // ---------- Happy path ---------- describe('evaluateChunk — correct script', () => { it('russian', () => { const r = evaluateChunk('Hello world', 'Привет мир', 'ru'); assert.equal(r.passed, true); assert.ok(!r.issues.includes('wrong_script')); }); it('chinese', () => { const r = evaluateChunk('Hello', '你好世界', 'zh'); assert.equal(r.passed, true); }); it('arabic', () => { const r = evaluateChunk('Hello', 'مرحبا بالعالم', 'ar'); assert.equal(r.passed, true); }); it('persian (with unique chars)', () => { const r = evaluateChunk('Hello', 'سلام چطوری؟ من پژوهشگر هستم', 'fa'); assert.equal(r.passed, true); }); it('french', () => { const r = evaluateChunk('Hello', 'Bonjour le monde', 'fr'); assert.equal(r.passed, true); }); it('hebrew', () => { const r = evaluateChunk('Hello', 'שלום עולם', 'he'); assert.equal(r.passed, true); }); it('korean', () => { const r = evaluateChunk('Hello', '안녕하세요 세계', 'ko'); assert.equal(r.passed, true); }); it('japanese', () => { const r = evaluateChunk('Hello', 'こんにちは世界', 'ja'); assert.equal(r.passed, true); }); it('hindi', () => { const r = evaluateChunk('Hello', 'नमस्ते दुनिया', 'hi'); assert.equal(r.passed, true); }); it('thai', () => { const r = evaluateChunk('Hello', 'สวัสดีชาวโลก', 'th'); assert.equal(r.passed, true); }); it('greek', () => { const r = evaluateChunk('Hello', 'Γεια σας κόσμε', 'el'); assert.equal(r.passed, true); }); }); // ---------- Wrong script detection ---------- describe('evaluateChunk — wrong script', () => { it('french text for japanese target', () => { const r = evaluateChunk('Hello', 'Bonjour le monde', 'ja'); assert.equal(r.passed, false); assert.ok(r.issues.includes('wrong_script')); }); it('arabic text for french target (language confusion)', () => { const r = evaluateChunk('Hello', 'مرحبا بالعالم', 'fr'); assert.equal(r.passed, false); assert.ok(r.issues.includes('wrong_script')); }); it('russian text for english target', () => { const r = evaluateChunk('Hello', 'Привет мир', 'en'); assert.equal(r.passed, false); assert.ok(r.issues.includes('wrong_script')); }); it('chinese text for korean target', () => { const r = evaluateChunk('Hello', '你好世界', 'ko'); assert.equal(r.passed, false); assert.ok(r.issues.includes('wrong_script')); }); }); // ---------- Arabic variant discrimination ---------- describe('evaluateChunk — Arabic variant discrimination', () => { it('persian text for arabic target fails', () => { const r = evaluateChunk('Hello', 'سلام چطوری؟', 'ar'); assert.equal(r.passed, false); assert.ok(r.issues.includes('wrong_arabic_variant')); }); it('urdu text for persian target fails', () => { const r = evaluateChunk('Hello', 'السلام ٹڈ', 'fa'); assert.equal(r.passed, false); assert.ok(r.issues.includes('wrong_arabic_variant')); }); it('pashto text for arabic target fails', () => { const r = evaluateChunk('Hello', 'السلام ټډړ', 'ar'); assert.equal(r.passed, false); assert.ok(r.issues.includes('wrong_arabic_variant')); }); }); // ---------- Edge cases ---------- describe('evaluateChunk — edge cases', () => { it('empty translation passes (skipped)', () => { const r = evaluateChunk('Hello', '', 'fr'); assert.equal(r.passed, true); assert.ok(r.issues.includes('empty_translation')); }); it('whitespace-only translation passes', () => { const r = evaluateChunk('Hello', ' \n ', 'fr'); assert.equal(r.passed, true); }); it('null translation passes', () => { const r = evaluateChunk('Hello', null, 'fr'); assert.equal(r.passed, true); }); it('undefined translation passes', () => { const r = evaluateChunk('Hello', undefined, 'fr'); assert.equal(r.passed, true); }); it('numbers only pass for fr target', () => { const r = evaluateChunk('Price: 100', '100', 'fr'); assert.equal(r.passed, true); }); it('unknown target lang falls back to latin', () => { const r = evaluateChunk('Hello', 'Bonjour', 'xx'); assert.equal(r.passed, true); }); }); // ---------- Length ---------- describe('evaluateChunk — length', () => { it('huge translation flagged via repetition (source too short)', () => { const r = evaluateChunk('Short', 'x'.repeat(1000), 'fr'); assert.ok(r.issues.includes('repetition_hallucination')); }); it('huge translation with long source flagged as length', () => { const src = 'A'.repeat(200); const r = evaluateChunk(src, 'x'.repeat(1000), 'fr'); assert.ok(r.issues.includes('length_outlier')); }); it('tiny translation flagged', () => { const r = evaluateChunk('A'.repeat(100), 'ok', 'fr'); assert.ok(r.issues.includes('truncation_suspect')); }); it('numeric source skips length check', () => { const r = evaluateChunk('Price: 100', '100', 'fr'); assert.ok(!r.issues.includes('truncation_suspect')); assert.ok(!r.issues.includes('length_outlier')); }); }); // ---------- Pattern leak / repetition ---------- describe('evaluateChunk — pattern leak', () => { it('english prompt leak', () => { const r = evaluateChunk('Hello', 'Translation: Bonjour le monde', 'fr'); assert.ok(r.issues.includes('prompt_leak')); }); it('french prompt leak', () => { const r = evaluateChunk('Hello', 'Voici la traduction : Bonjour', 'fr'); assert.ok(r.issues.includes('prompt_leak')); }); it('chinese prompt leak', () => { const r = evaluateChunk('Hello', '翻译:你好', 'zh'); assert.ok(r.issues.includes('prompt_leak')); }); it('token repetition detected', () => { const r = evaluateChunk('Hello', 'the the the the the the the', 'fr'); assert.ok(r.issues.includes('repetition_hallucination')); }); it('char repetition detected', () => { const r = evaluateChunk('Hello', 'x'.repeat(30), 'fr'); assert.ok(r.issues.includes('repetition_hallucination')); }); it('normal text has no leak', () => { const r = evaluateChunk('Hello', 'Bonjour le monde', 'fr'); assert.ok(!r.issues.includes('prompt_leak')); assert.ok(!r.issues.includes('repetition_hallucination')); }); }); // ---------- Document-level ---------- describe('evaluateDocument', () => { it('all good', () => { const result = evaluateDocument( ['Hello', 'World', 'Good morning'], ['Bonjour', 'Monde', 'Bonjour'], 'fr', ); assert.equal(result.passed, true); assert.equal(result.chunkCount, 3); assert.equal(result.failedChunkCount, 0); }); it('some failures', () => { const result = evaluateDocument( ['Hello', 'World', 'Good morning'], ['Bonjour', 'مرحبا', 'Bonjour'], 'fr', ); assert.equal(result.passed, false); assert.equal(result.failedChunkCount, 1); assert.ok('wrong_script' in result.issues); assert.equal(result.samples.length, 1); }); it('empty lists', () => { const result = evaluateDocument([], [], 'fr'); assert.equal(result.passed, true); assert.equal(result.chunkCount, 0); }); it('sample size caps samples', () => { const result = evaluateDocument( Array(10).fill('hi'), Array(10).fill('مرحبا'), 'fr', 3, ); assert.equal(result.failedChunkCount, 10); assert.equal(result.samples.length, 3); }); }); // ---------- detectArabicVariant direct ---------- describe('detectArabicVariant', () => { it('persian for persian passes', () => { const r = detectArabicVariant('سلام چطوری', 'fa'); assert.equal(r.verdict, 'pass'); }); it('persian for arabic fails', () => { const r = detectArabicVariant('سلام چطوری', 'ar'); assert.equal(r.verdict, 'fail'); assert.ok(r.detectedVariants.includes('fa')); }); it('urdu for persian fails', () => { const r = detectArabicVariant('السلام ٹڈ', 'fa'); assert.equal(r.verdict, 'fail'); assert.ok(r.detectedVariants.includes('ur')); }); it('non-arabic text skipped', () => { const r = detectArabicVariant('Bonjour le monde', 'fa'); assert.equal(r.verdict, 'skip'); }); }); // ---------- lengthCheck direct ---------- describe('lengthCheck', () => { it('normal length returns ratio', () => { const r = lengthCheck( 'Hello world this is a longer source string', 'Bonjour le monde ceci est une traduction francaise', ); assert.equal(r.issue, null); assert.ok(r.ratio !== null); }); it('huge translation', () => { const r = lengthCheck('A'.repeat(200), 'x'.repeat(1000)); assert.equal(r.issue, 'length_outlier'); }); it('tiny translation', () => { const r = lengthCheck('A'.repeat(100), 'ok'); assert.equal(r.issue, 'truncation_suspect'); }); it('empty translation flagged', () => { const r = lengthCheck('Hello world this is a test', ''); assert.equal(r.issue, 'truncation_suspect'); }); }); // ---------- patternCheck direct ---------- describe('patternCheck', () => { it('no leak on normal text', () => { const r = patternCheck('Bonjour le monde'); assert.equal(r.issue, null); }); it('english leak', () => { const r = patternCheck('Translation: Bonjour'); assert.equal(r.issue, 'prompt_leak'); }); it('french leak', () => { const r = patternCheck('Voici la traduction : Bonjour'); assert.equal(r.issue, 'prompt_leak'); }); it('chinese leak', () => { const r = patternCheck('翻译:你好'); assert.equal(r.issue, 'prompt_leak'); }); it('token repetition', () => { const r = patternCheck('the the the the the the the'); assert.equal(r.issue, 'repetition_hallucination'); }); it('char repetition', () => { const r = patternCheck('x'.repeat(30)); assert.equal(r.issue, 'repetition_hallucination'); }); });