const test = require('node:test'); const assert = require('node:assert'); const fs = require('fs'); const os = require('os'); const path = require('path'); const { discover } = require('../library/local-import'); const created = []; function freshRoot(tag) { const root = fs.mkdtempSync(path.join(os.tmpdir(), `peoplelib-local-import-${tag}-`)); created.push(root); return root; } function write(root, relativePath, contents = '') { const target = path.join(root, relativePath); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, contents); return target; } test.after(() => { for (const root of created) { try { fs.rmSync(root, { recursive: true, force: true }); } catch (error) { /* ignore */ } } }); test('discovers a directly selected supported file with a canonical record', async () => { const root = freshRoot('direct'); const selected = write(root, 'A Book.PDF'); const canonical = fs.realpathSync(selected); assert.deepStrictEqual(await discover([selected]), [{ path: canonical, name: 'A Book.PDF', format: 'pdf', parentName: path.basename(root) }]); assert.ok(path.isAbsolute(canonical)); }); test('recursively discovers supported files and uses each immediate parent name', async () => { const root = freshRoot('recursive'); const first = write(root, 'root.epub'); const second = write(root, path.join('Shelf One', 'nested.MOBI')); const third = write(root, path.join('Shelf One', 'Deeper', 'last.fb2')); const result = await discover([root]); const byName = new Map(result.map((record) => [record.name, record])); assert.deepStrictEqual( new Set(result.map((record) => record.path)), new Set([first, second, third].map((value) => fs.realpathSync(value))) ); assert.strictEqual(byName.get('root.epub').parentName, path.basename(root)); assert.strictEqual(byName.get('nested.MOBI').parentName, 'Shelf One'); assert.strictEqual(byName.get('last.fb2').parentName, 'Deeper'); assert.deepStrictEqual( Object.fromEntries(result.map((record) => [record.name, record.format])), { 'last.fb2': 'fb2', 'nested.MOBI': 'mobi', 'root.epub': 'epub' } ); }); test('handles mixed file and directory inputs while skipping unsupported and non-files', async () => { const root = freshRoot('mixed'); const folder = path.join(root, 'folder'); const inFolder = write(root, path.join('folder', 'comic.cbz')); const azw = write(root, path.join('folder', 'legacy.azw')); const direct = write(root, 'notes.txt'); const markdown = write(root, path.join('folder', 'guide.MD')); write(root, path.join('folder', 'cover.jpg')); write(root, 'README.rtf'); fs.mkdirSync(path.join(root, 'empty')); const result = await discover([ path.join(root, 'missing.pdf'), path.join(root, 'README.rtf'), path.join(root, 'empty'), direct, folder ]); assert.deepStrictEqual( result.map((record) => record.path), [inFolder, azw, markdown, direct].map((value) => fs.realpathSync(value)).sort(), 'md 与 txt 都能进内置阅读器,必须和其他图书格式一样被本地导入发现' ); }); test('de-duplicates repeated selections', async () => { const root = freshRoot('duplicate'); const selected = write(root, 'duplicate.djvu'); const result = await discover([selected, root, selected]); assert.strictEqual(result.length, 1); assert.strictEqual(result[0].path, fs.realpathSync(selected)); }); test('does not follow symbolic links to files or directories when links are available', async (t) => { const root = freshRoot('symlink'); const outside = freshRoot('symlink-target'); const ordinary = write(root, 'ordinary.cbr'); const linkedFileTarget = write(outside, 'linked.pdf'); const linkedDirectoryTarget = path.join(outside, 'books'); const nestedTarget = write(outside, path.join('books', 'nested.azw3')); const fileLink = path.join(root, 'file-link.pdf'); const directoryLink = path.join(root, 'directory-link'); try { fs.symlinkSync(linkedFileTarget, fileLink, 'file'); fs.symlinkSync( linkedDirectoryTarget, directoryLink, process.platform === 'win32' ? 'junction' : 'dir' ); } catch (error) { t.skip(`symbolic links are unavailable: ${error.code || error.message}`); return; } const result = await discover([root, fileLink, directoryLink]); assert.deepStrictEqual(result.map((record) => record.path), [fs.realpathSync(ordinary)]); assert.ok(!result.some((record) => record.path === fs.realpathSync(nestedTarget))); }); test('returns a deterministic path-sorted order independent of selection order', async () => { const root = freshRoot('order'); write(root, 'zeta.txt'); write(root, 'Alpha.pdf'); write(root, path.join('middle', 'beta.epub')); const forward = await discover([path.join(root, 'zeta.txt'), path.join(root, 'middle'), root]); const reverse = await discover([root, path.join(root, 'middle'), path.join(root, 'zeta.txt')]); assert.deepStrictEqual(forward, reverse); assert.deepStrictEqual( forward.map((record) => record.path), forward.map((record) => record.path).slice().sort((left, right) => { const leftKey = process.platform === 'win32' ? left.toLowerCase() : left; const rightKey = process.platform === 'win32' ? right.toLowerCase() : right; return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : left < right ? -1 : left > right ? 1 : 0; }) ); }); test('throws a clear Chinese error when the supported-file maximum is exceeded', async () => { const root = freshRoot('maximum'); write(root, 'one.pdf'); write(root, 'two.epub'); write(root, 'three.mobi'); await assert.rejects( discover([root], { maxFiles: 2 }), /本地导入文件数量超过上限(最多 2 个)/ ); });