import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { createFrequencyDictionaryLookup } from './frequency-dictionary'; test('createFrequencyDictionaryLookup logs parse errors and returns no-op for invalid dictionaries', async () => { const logs: string[] = []; const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-frequency-dict-')); const bankPath = path.join(tempDir, 'term_meta_bank_1.json'); fs.writeFileSync(bankPath, '{ invalid json'); const lookup = await createFrequencyDictionaryLookup({ searchPaths: [tempDir], log: (message) => { logs.push(message); }, }); const rank = lookup('猫'); assert.equal(rank, null); assert.equal( logs.some( (entry) => entry.includes('Failed to parse frequency dictionary file as JSON') && entry.includes('term_meta_bank_1.json'), ), true, ); }); test('createFrequencyDictionaryLookup continues with no-op lookup when search path is missing', async () => { const logs: string[] = []; const missingPath = path.join(os.tmpdir(), 'subminer-frequency-dict-missing-dir'); const lookup = await createFrequencyDictionaryLookup({ searchPaths: [missingPath], log: (message) => { logs.push(message); }, }); assert.equal(lookup('猫'), null); assert.equal( logs.some((entry) => entry.includes(`Frequency dictionary not found.`)), true, ); }); test('createFrequencyDictionaryLookup aggregates duplicate-term logs into a single summary', async () => { const logs: string[] = []; const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-frequency-dict-')); const bankPath = path.join(tempDir, 'term_meta_bank_1.json'); fs.writeFileSync( bankPath, JSON.stringify([ ['猫', 1, { frequency: { displayValue: 100 } }], ['猫', 2, { frequency: { displayValue: 120 } }], ['猫', 3, { frequency: { displayValue: 110 } }], ]), ); const lookup = await createFrequencyDictionaryLookup({ searchPaths: [tempDir], log: (message) => { logs.push(message); }, }); assert.equal(lookup('猫'), 100); assert.equal( logs.filter((entry) => entry.includes('Frequency dictionary ignored 2 duplicate term entries')) .length, 1, ); assert.equal( logs.some((entry) => entry.includes('Frequency dictionary duplicate term')), false, ); }); test('createFrequencyDictionaryLookup prefers frequency.displayValue over value when both exist', async () => { const logs: string[] = []; const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-frequency-dict-')); const bankPath = path.join(tempDir, 'term_meta_bank_1.json'); fs.writeFileSync( bankPath, JSON.stringify([ ['猫', 1, { frequency: { value: 1234, displayValue: 1200 } }], ['鍛える', 2, { frequency: { value: 46961, displayValue: 2847 } }], ['犬', 2, { frequency: { displayValue: 88 } }], ]), ); const lookup = await createFrequencyDictionaryLookup({ searchPaths: [tempDir], log: (message) => { logs.push(message); }, }); assert.equal(lookup('猫'), 1200); assert.equal(lookup('鍛える'), 2847); assert.equal(lookup('犬'), 88); assert.equal( logs.some((entry) => entry.includes('Frequency dictionary loaded from')), true, ); }); test('createFrequencyDictionaryLookup parses composite displayValue by primary rank', async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-frequency-dict-')); const bankPath = path.join(tempDir, 'term_meta_bank_1.json'); fs.writeFileSync( bankPath, JSON.stringify([ ['鍛える', 1, { frequency: { displayValue: '3272,52377' } }], ['高み', 2, { frequency: { displayValue: '9933,108961' } }], ]), ); const lookup = await createFrequencyDictionaryLookup({ searchPaths: [tempDir], log: () => undefined, }); assert.equal(lookup('鍛える'), 3272); assert.equal(lookup('高み'), 9933); }); test('createFrequencyDictionaryLookup uses leading display digits for displayValue strings', async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-frequency-dict-')); const bankPath = path.join(tempDir, 'term_meta_bank_1.json'); fs.writeFileSync( bankPath, JSON.stringify([ ['潜む', 1, { frequency: { value: 121, displayValue: '118,121' } }], ['例', 2, { frequency: { value: 1234, displayValue: '1,234' } }], ]), ); const lookup = await createFrequencyDictionaryLookup({ searchPaths: [tempDir], log: () => undefined, }); assert.equal(lookup('潜む'), 118); assert.equal(lookup('例'), 1); }); test('createFrequencyDictionaryLookup ignores occurrence-based Yomitan dictionaries', async () => { const logs: string[] = []; const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-frequency-dict-')); fs.writeFileSync( path.join(tempDir, 'index.json'), JSON.stringify({ title: 'CC100', revision: '1', frequencyMode: 'occurrence-based', }), ); fs.writeFileSync( path.join(tempDir, 'term_meta_bank_1.json'), JSON.stringify([['潜む', 1, { frequency: { value: 118121 } }]]), ); const lookup = await createFrequencyDictionaryLookup({ searchPaths: [tempDir], log: (message) => { logs.push(message); }, }); assert.equal(lookup('潜む'), null); assert.equal( logs.some((entry) => entry.includes('occurrence-based') && entry.includes('CC100')), true, ); }); test('createFrequencyDictionaryLookup does not require synchronous fs APIs', async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-frequency-dict-')); const bankPath = path.join(tempDir, 'term_meta_bank_1.json'); fs.writeFileSync(bankPath, JSON.stringify([['猫', 1, { frequency: { displayValue: 42 } }]])); const readFileSync = fs.readFileSync; const readdirSync = fs.readdirSync; const statSync = fs.statSync; const existsSync = fs.existsSync; (fs as unknown as Record).readFileSync = () => { throw new Error('sync read disabled'); }; (fs as unknown as Record).readdirSync = () => { throw new Error('sync readdir disabled'); }; (fs as unknown as Record).statSync = () => { throw new Error('sync stat disabled'); }; (fs as unknown as Record).existsSync = () => { throw new Error('sync exists disabled'); }; try { const lookup = await createFrequencyDictionaryLookup({ searchPaths: [tempDir], log: () => undefined, }); assert.equal(lookup('猫'), 42); } finally { (fs as unknown as Record).readFileSync = readFileSync; (fs as unknown as Record).readdirSync = readdirSync; (fs as unknown as Record).statSync = statSync; (fs as unknown as Record).existsSync = existsSync; } });