basant307/AI_Governance_Project
045
1/**2 * @license3 * Copyright 2025 Qwen team4 * SPDX-License-Identifier: Apache-2.05 */6 7import { describe, it, expect, beforeEach, vi } from 'vitest';8import * as fs from 'node:fs';9import * as path from 'node:path';10 11// Mock fs module12vi.mock('node:fs', () => ({13 existsSync: vi.fn(),14 mkdirSync: vi.fn(),15 writeFileSync: vi.fn(),16 readFileSync: vi.fn(),17}));18 19// Mock i18n module20vi.mock('../i18n/index.js', () => ({21 detectSystemLanguage: vi.fn(),22 getLanguageNameFromLocale: vi.fn((locale: string) => {23 const map: Record<string, string> = {24 en: 'English',25 'zh-tw': 'Traditional Chinese',26 zh: 'Chinese',27 ru: 'Russian',28 de: 'German',29 ja: 'Japanese',30 ko: 'Korean',31 fr: 'French',32 es: 'Spanish',33 };34 return map[locale.toLowerCase()] || 'English';35 }),36}));37 38// Mock @qwen-code/qwen-code-core39vi.mock('@qwen-code/qwen-code-core', () => ({40 Storage: {41 getGlobalQwenDir: vi.fn(() => '/mock/home/.qwen'),42 },43}));44 45import * as i18n from '../i18n/index.js';46import {47 OUTPUT_LANGUAGE_AUTO,48 isAutoLanguage,49 normalizeOutputLanguage,50 resolveOutputLanguage,51 writeOutputLanguageFile,52 updateOutputLanguageFile,53 writeOutputLanguageAndRegisterPath,54 initializeLlmOutputLanguage,55} from './languageUtils.js';56 57describe('languageUtils', () => {58 beforeEach(() => {59 vi.clearAllMocks();60 });61 62 describe('OUTPUT_LANGUAGE_AUTO', () => {63 it('should be "auto"', () => {64 expect(OUTPUT_LANGUAGE_AUTO).toBe('auto');65 });66 });67 68 describe('isAutoLanguage', () => {69 it('should return true for "auto"', () => {70 expect(isAutoLanguage('auto')).toBe(true);71 });72 73 it('should return true for "AUTO" (case insensitive)', () => {74 expect(isAutoLanguage('AUTO')).toBe(true);75 });76 77 it('should return true for "Auto" (case insensitive)', () => {78 expect(isAutoLanguage('Auto')).toBe(true);79 });80 81 it('should return true for undefined', () => {82 expect(isAutoLanguage(undefined)).toBe(true);83 });84 85 it('should return true for null', () => {86 expect(isAutoLanguage(null)).toBe(true);87 });88 89 it('should return true for empty string', () => {90 expect(isAutoLanguage('')).toBe(true);91 });92 93 it('should return false for explicit language', () => {94 expect(isAutoLanguage('Chinese')).toBe(false);95 });96 97 it('should return false for locale code', () => {98 expect(isAutoLanguage('zh')).toBe(false);99 });100 });101 102 describe('normalizeOutputLanguage', () => {103 it('should convert "en" to "English"', () => {104 expect(normalizeOutputLanguage('en')).toBe('English');105 });106 107 it('should normalize "english" to "English"', () => {108 expect(normalizeOutputLanguage('english')).toBe('English');109 expect(normalizeOutputLanguage('English')).toBe('English');110 expect(normalizeOutputLanguage('en-US')).toBe('English');111 });112 113 it('should convert "zh" to "Chinese"', () => {114 expect(normalizeOutputLanguage('zh')).toBe('Chinese');115 });116 117 it('should convert "ru" to "Russian"', () => {118 expect(normalizeOutputLanguage('ru')).toBe('Russian');119 });120 121 it('should convert "de" to "German"', () => {122 expect(normalizeOutputLanguage('de')).toBe('German');123 });124 125 it('should convert "ja" to "Japanese"', () => {126 expect(normalizeOutputLanguage('ja')).toBe('Japanese');127 });128 129 it('should be case insensitive for locale codes', () => {130 expect(normalizeOutputLanguage('ZH')).toBe('Chinese');131 expect(normalizeOutputLanguage('Ru')).toBe('Russian');132 });133 134 it('should convert "zh-TW" (mixed case) to "Traditional Chinese"', () => {135 expect(normalizeOutputLanguage('zh-TW')).toBe('Traditional Chinese');136 expect(normalizeOutputLanguage('zh-tw')).toBe('Traditional Chinese');137 expect(normalizeOutputLanguage('ZH-TW')).toBe('Traditional Chinese');138 });139 140 it('should preserve explicit language names as-is', () => {141 expect(normalizeOutputLanguage('Japanese')).toBe('Japanese');142 expect(normalizeOutputLanguage('French')).toBe('French');143 expect(normalizeOutputLanguage('french')).toBe('French');144 });145 146 it('should preserve unknown language names as-is', () => {147 expect(normalizeOutputLanguage('CustomLanguage')).toBe('CustomLanguage');148 expect(normalizeOutputLanguage('日本語')).toBe('日本語');149 });150 });151 152 describe('resolveOutputLanguage', () => {153 it('should resolve "auto" to detected system language', () => {154 vi.mocked(i18n.detectSystemLanguage).mockReturnValue('zh');155 156 expect(resolveOutputLanguage('auto')).toBe('Chinese');157 expect(i18n.detectSystemLanguage).toHaveBeenCalled();158 });159 160 it('should resolve undefined to detected system language', () => {161 vi.mocked(i18n.detectSystemLanguage).mockReturnValue('ru');162 163 expect(resolveOutputLanguage(undefined)).toBe('Russian');164 });165 166 it('should resolve null to detected system language', () => {167 vi.mocked(i18n.detectSystemLanguage).mockReturnValue('de');168 169 expect(resolveOutputLanguage(null)).toBe('German');170 });171 172 it('should normalize explicit locale codes', () => {173 expect(resolveOutputLanguage('zh')).toBe('Chinese');174 expect(i18n.detectSystemLanguage).not.toHaveBeenCalled();175 });176 177 it('should normalize "english"', () => {178 expect(resolveOutputLanguage('english')).toBe('English');179 expect(resolveOutputLanguage('en-US')).toBe('English');180 expect(i18n.detectSystemLanguage).not.toHaveBeenCalled();181 });182 183 it('should preserve explicit language names', () => {184 expect(resolveOutputLanguage('Japanese')).toBe('Japanese');185 });186 });187 188 describe('writeOutputLanguageFile', () => {189 beforeEach(() => {190 vi.mocked(fs.mkdirSync).mockImplementation(() => undefined);191 vi.mocked(fs.writeFileSync).mockImplementation(() => undefined);192 });193 194 it('should create directory and write file', () => {195 writeOutputLanguageFile('Chinese');196 197 const globalDir = '/mock/home/.qwen';198 const expectedDir = path.join(globalDir);199 const expectedFilePath = path.join(globalDir, 'output-language.md');200 201 expect(fs.mkdirSync).toHaveBeenCalledWith(expectedDir, {202 recursive: true,203 });204 expect(fs.writeFileSync).toHaveBeenCalledWith(205 expectedFilePath,206 expect.any(String),207 'utf-8',208 );209 });210 211 it('should include language in file content', () => {212 writeOutputLanguageFile('Japanese');213 214 const writtenContent = vi.mocked(fs.writeFileSync).mock.calls[0][1];215 expect(writtenContent).toContain('Japanese');216 expect(writtenContent).toContain(217 '# Output language preference: Japanese',218 );219 });220 221 it('should include machine-readable marker', () => {222 writeOutputLanguageFile('Chinese');223 224 const writtenContent = vi.mocked(fs.writeFileSync).mock.calls[0][1];225 expect(writtenContent).toContain(226 '<!-- qwen-code:llm-output-language: Chinese -->',227 );228 });229 230 it('should sanitize language for marker (remove dangerous characters)', () => {231 writeOutputLanguageFile('Test--Language');232 233 const writtenContent = vi.mocked(fs.writeFileSync).mock.calls[0][1];234 // The marker should have -- removed, but the heading preserves original235 expect(writtenContent).toContain(236 '# Output language preference: Test--Language',237 );238 expect(writtenContent).toContain(239 '<!-- qwen-code:llm-output-language: TestLanguage -->',240 );241 });242 243 it('should use mandatory language rule instead of preference', () => {244 writeOutputLanguageFile('Chinese');245 246 const writtenContent = vi.mocked(fs.writeFileSync).mock247 .calls[0][1] as string;248 expect(writtenContent).toContain(249 'You MUST always respond in **Chinese**',250 );251 expect(writtenContent).toContain(252 'This is a mandatory requirement, not a preference.',253 );254 expect(writtenContent).not.toContain('Prefer responding');255 });256 257 it('should write to custom targetPath when provided', () => {258 writeOutputLanguageFile('Korean', '/proj/.qwen/output-language.md');259 260 expect(fs.mkdirSync).toHaveBeenCalledWith('/proj/.qwen', {261 recursive: true,262 });263 expect(fs.writeFileSync).toHaveBeenCalledWith(264 '/proj/.qwen/output-language.md',265 expect.stringContaining('Korean'),266 'utf-8',267 );268 });269 270 it('should include exception clause for explicit user language requests', () => {271 writeOutputLanguageFile('English');272 273 const writtenContent = vi.mocked(fs.writeFileSync).mock274 .calls[0][1] as string;275 expect(writtenContent).toContain('## Exception');276 expect(writtenContent).toContain(277 "switch to the user's requested language for the remainder of the conversation",278 );279 });280 281 it('should use the correct language name throughout the template', () => {282 writeOutputLanguageFile('Japanese');283 284 const writtenContent = vi.mocked(fs.writeFileSync).mock285 .calls[0][1] as string;286 expect(writtenContent).toContain(287 'You MUST always respond in **Japanese**',288 );289 expect(writtenContent).toContain('## Rule');290 expect(writtenContent).toContain('## Exception');291 });292 });293 294 describe('updateOutputLanguageFile', () => {295 beforeEach(() => {296 vi.mocked(fs.mkdirSync).mockImplementation(() => undefined);297 vi.mocked(fs.writeFileSync).mockImplementation(() => undefined);298 });299 300 it('should resolve "auto" and write resolved language', () => {301 vi.mocked(i18n.detectSystemLanguage).mockReturnValue('zh');302 303 updateOutputLanguageFile('auto');304 305 const writtenContent = vi.mocked(fs.writeFileSync).mock.calls[0][1];306 expect(writtenContent).toContain('Chinese');307 });308 309 it('should normalize locale codes and write full name', () => {310 updateOutputLanguageFile('ja');311 312 const writtenContent = vi.mocked(fs.writeFileSync).mock.calls[0][1];313 expect(writtenContent).toContain('Japanese');314 });315 316 it('should write explicit language names directly', () => {317 updateOutputLanguageFile('French');318 319 const writtenContent = vi.mocked(fs.writeFileSync).mock.calls[0][1];320 expect(writtenContent).toContain('French');321 });322 });323 324 describe('initializeLlmOutputLanguage', () => {325 beforeEach(() => {326 vi.mocked(fs.existsSync).mockReturnValue(false);327 vi.mocked(fs.mkdirSync).mockImplementation(() => undefined);328 vi.mocked(fs.writeFileSync).mockImplementation(() => undefined);329 vi.mocked(fs.readFileSync).mockReturnValue('');330 });331 332 it('should create file when it does not exist', () => {333 vi.mocked(fs.existsSync).mockReturnValue(false);334 vi.mocked(i18n.detectSystemLanguage).mockReturnValue('en');335 336 initializeLlmOutputLanguage();337 338 expect(fs.mkdirSync).toHaveBeenCalled();339 expect(fs.writeFileSync).toHaveBeenCalledWith(340 expect.stringContaining('output-language.md'),341 expect.stringContaining('English'),342 'utf-8',343 );344 });345 346 it('should NOT overwrite file when it already exists with valid content', () => {347 vi.mocked(fs.existsSync).mockReturnValue(true);348 vi.mocked(i18n.detectSystemLanguage).mockReturnValue('en');349 vi.mocked(fs.readFileSync).mockReturnValue(350 `# Output language preference: French351<!-- qwen-code:llm-output-language: French -->352`,353 );354 355 initializeLlmOutputLanguage();356 357 expect(fs.writeFileSync).not.toHaveBeenCalled();358 });359 360 it('should NOT overwrite file even when setting differs from existing content', () => {361 vi.mocked(fs.existsSync).mockReturnValue(true);362 vi.mocked(fs.readFileSync).mockReturnValue(363 `# Output language preference: French364<!-- qwen-code:llm-output-language: French -->365`,366 );367 368 initializeLlmOutputLanguage('Japanese');369 370 // Should NOT overwrite - user's existing file takes precedence371 expect(fs.writeFileSync).not.toHaveBeenCalled();372 });373 374 it('should resolve "auto" to detected system language', () => {375 vi.mocked(fs.existsSync).mockReturnValue(false);376 vi.mocked(i18n.detectSystemLanguage).mockReturnValue('zh');377 378 initializeLlmOutputLanguage('auto');379 380 expect(fs.writeFileSync).toHaveBeenCalledWith(381 expect.stringContaining('output-language.md'),382 expect.stringContaining('Chinese'),383 'utf-8',384 );385 });386 387 it('should detect Chinese locale and create Chinese rule file', () => {388 vi.mocked(fs.existsSync).mockReturnValue(false);389 vi.mocked(i18n.detectSystemLanguage).mockReturnValue('zh');390 391 initializeLlmOutputLanguage();392 393 expect(fs.writeFileSync).toHaveBeenCalledWith(394 expect.stringContaining('output-language.md'),395 expect.stringContaining('Chinese'),396 'utf-8',397 );398 });399 400 it('should detect Russian locale and create Russian rule file', () => {401 vi.mocked(fs.existsSync).mockReturnValue(false);402 vi.mocked(i18n.detectSystemLanguage).mockReturnValue('ru');403 404 initializeLlmOutputLanguage();405 406 expect(fs.writeFileSync).toHaveBeenCalledWith(407 expect.stringContaining('output-language.md'),408 expect.stringContaining('Russian'),409 'utf-8',410 );411 });412 413 it('should detect German locale and create German rule file', () => {414 vi.mocked(fs.existsSync).mockReturnValue(false);415 vi.mocked(i18n.detectSystemLanguage).mockReturnValue('de');416 417 initializeLlmOutputLanguage();418 419 expect(fs.writeFileSync).toHaveBeenCalledWith(420 expect.stringContaining('output-language.md'),421 expect.stringContaining('German'),422 'utf-8',423 );424 });425 426 it('should handle file read errors gracefully', () => {427 vi.mocked(fs.existsSync).mockReturnValue(true);428 vi.mocked(fs.readFileSync).mockImplementation(() => {429 throw new Error('Read error');430 });431 vi.mocked(i18n.detectSystemLanguage).mockReturnValue('en');432 433 // Should not throw, and should create new file434 expect(() => initializeLlmOutputLanguage()).not.toThrow();435 expect(fs.writeFileSync).toHaveBeenCalled();436 });437 438 it('should parse legacy heading format', () => {439 vi.mocked(fs.existsSync).mockReturnValue(true);440 vi.mocked(fs.readFileSync).mockReturnValue(441 '# CRITICAL: Chinese Output Language Rule - HIGHEST PRIORITY',442 );443 vi.mocked(i18n.detectSystemLanguage).mockReturnValue('zh');444 445 initializeLlmOutputLanguage();446 447 // Should not overwrite since file already has Chinese448 expect(fs.writeFileSync).not.toHaveBeenCalled();449 });450 });451 452 describe('output-language.md path resolution priority', () => {453 it('should prefer project-level path over global path', () => {454 const projectPath = '/project/.qwen/output-language.md';455 const globalPath = '/mock/home/.qwen/output-language.md';456 457 vi.mocked(fs.existsSync).mockImplementation((p) => {458 if (p.toString() === projectPath) return true;459 if (p.toString() === globalPath) return true;460 return false;461 });462 463 let resolvedPath: string | undefined;464 if (fs.existsSync(projectPath)) {465 resolvedPath = projectPath;466 } else if (fs.existsSync(globalPath)) {467 resolvedPath = globalPath;468 }469 470 expect(resolvedPath).toBe(projectPath);471 });472 473 it('should fall back to global path when project-level does not exist', () => {474 const projectPath = '/project/.qwen/output-language.md';475 const globalPath = '/mock/home/.qwen/output-language.md';476 477 vi.mocked(fs.existsSync).mockImplementation((p) => {478 if (p.toString() === projectPath) return false;479 if (p.toString() === globalPath) return true;480 return false;481 });482 483 let resolvedPath: string | undefined;484 if (fs.existsSync(projectPath)) {485 resolvedPath = projectPath;486 } else if (fs.existsSync(globalPath)) {487 resolvedPath = globalPath;488 }489 490 expect(resolvedPath).toBe(globalPath);491 });492 493 it('should return undefined when neither path exists', () => {494 const projectPath = '/project/.qwen/output-language.md';495 const globalPath = '/mock/home/.qwen/output-language.md';496 497 vi.mocked(fs.existsSync).mockReturnValue(false);498 499 let resolvedPath: string | undefined;500 if (fs.existsSync(projectPath)) {501 resolvedPath = projectPath;502 } else if (fs.existsSync(globalPath)) {503 resolvedPath = globalPath;504 }505 506 expect(resolvedPath).toBeUndefined();507 });508 });509 510 describe('writeOutputLanguageAndRegisterPath', () => {511 beforeEach(() => {512 vi.mocked(fs.mkdirSync).mockImplementation(() => undefined);513 vi.mocked(fs.writeFileSync).mockImplementation(() => undefined);514 });515 516 it('writes to config-bound path when available', () => {517 const config = {518 getOutputLanguageFilePath: vi519 .fn()520 .mockReturnValue('/proj/.qwen/output-language.md'),521 setOutputLanguageFilePath: vi.fn(),522 };523 524 writeOutputLanguageAndRegisterPath('Chinese', config);525 526 expect(fs.writeFileSync).toHaveBeenCalledWith(527 '/proj/.qwen/output-language.md',528 expect.stringContaining('Chinese'),529 'utf-8',530 );531 expect(config.setOutputLanguageFilePath).not.toHaveBeenCalled();532 });533 534 it('writes to global default and registers path when config path is undefined', () => {535 const config = {536 getOutputLanguageFilePath: vi.fn().mockReturnValue(undefined),537 setOutputLanguageFilePath: vi.fn(),538 };539 540 writeOutputLanguageAndRegisterPath('Japanese', config);541 542 expect(fs.writeFileSync).toHaveBeenCalledWith(543 expect.stringContaining('output-language.md'),544 expect.stringContaining('Japanese'),545 'utf-8',546 );547 expect(config.setOutputLanguageFilePath).toHaveBeenCalledWith(548 expect.stringContaining('output-language.md'),549 );550 });551 552 it('handles null config gracefully', () => {553 writeOutputLanguageAndRegisterPath('Korean', null);554 555 expect(fs.writeFileSync).toHaveBeenCalledWith(556 expect.stringContaining('output-language.md'),557 expect.stringContaining('Korean'),558 'utf-8',559 );560 });561 562 it('handles undefined config gracefully', () => {563 writeOutputLanguageAndRegisterPath('Russian', undefined);564 565 expect(fs.writeFileSync).toHaveBeenCalledWith(566 expect.stringContaining('output-language.md'),567 expect.stringContaining('Russian'),568 'utf-8',569 );570 });571 });572});573 