247 lines
10 KiB
JavaScript
247 lines
10 KiB
JavaScript
'use strict';
|
||
const assert = require('node:assert');
|
||
const fs = require('node:fs');
|
||
const os = require('node:os');
|
||
const path = require('node:path');
|
||
|
||
const fav = require('../favorites.js');
|
||
|
||
// check 支持 async fn —— 之前是同步的,传入 async 函数时断言在 await 之后
|
||
// 失败会变成 unhandledRejection(summary 还先把这条计进了 passed)。
|
||
async function check(name, fn) {
|
||
try { await fn(); console.log(' ok -', name); passed++; }
|
||
catch (e) { console.error(' FAIL -', name, '\n', e.message); process.exitCode = 1; }
|
||
}
|
||
|
||
let passed = 0;
|
||
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'clip-fav-'));
|
||
const dbPath = path.join(tmpHome, 'favorites.db');
|
||
|
||
check('open 创建 db 文件并返回实例', () => {
|
||
const db = fav.open(dbPath);
|
||
assert.ok(db, 'db 应该非空');
|
||
assert.ok(fs.existsSync(dbPath), 'db 文件应存在');
|
||
fav.close(db);
|
||
});
|
||
|
||
check('建表:favorites 表存在且列正确', () => {
|
||
const db = fav.open(dbPath);
|
||
const cols = db.prepare("PRAGMA table_info(favorites)").all();
|
||
const names = cols.map(c => c.name);
|
||
assert.deepStrictEqual(names, ['id','type','text','image','preview','source_clip_id','created_at','favorited_at']);
|
||
fav.close(db);
|
||
});
|
||
|
||
check('索引 idx_fav_favorited_at 存在', () => {
|
||
const db = fav.open(dbPath);
|
||
const idx = db.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_fav_favorited_at'").get();
|
||
assert.ok(idx, '索引应存在');
|
||
fav.close(db);
|
||
});
|
||
|
||
check('source_clip_id 上有 UNIQUE 约束', () => {
|
||
const db = fav.open(dbPath);
|
||
const idx = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='favorites'").get();
|
||
assert.ok(/UNIQUE\s*\(\s*source_clip_id\s*\)/i.test(idx.sql), '建表 SQL 应包含 UNIQUE(source_clip_id)');
|
||
fav.close(db);
|
||
});
|
||
|
||
check('add 插入文本并能取回', () => {
|
||
const dbPath2 = path.join(tmpHome, 'add-text.db');
|
||
const db = fav.open(dbPath2);
|
||
const now = Date.now() / 1000;
|
||
const clip = { id: 101, type: 'text', text: 'hello world', preview: 'hello world', createdAt: now };
|
||
const row = fav.add(db, clip);
|
||
assert.ok(row.id > 0, '应返回新 id');
|
||
assert.strictEqual(row.type, 'text');
|
||
assert.strictEqual(row.text, 'hello world');
|
||
assert.strictEqual(row.sourceClipId, 101);
|
||
assert.strictEqual(row.preview, 'hello world');
|
||
fav.close(db);
|
||
});
|
||
|
||
check('add 重复 source_clip_id 抛错', () => {
|
||
const dbPath2 = path.join(tmpHome, 'add-dup.db');
|
||
const db = fav.open(dbPath2);
|
||
const now = Date.now() / 1000;
|
||
const clip = { id: 102, type: 'text', text: 'first', preview: 'first', createdAt: now };
|
||
fav.add(db, clip);
|
||
let threw = false;
|
||
try { fav.add(db, { ...clip, text: 'second' }); }
|
||
catch (e) { threw = /UNIQUE/i.test(e.message); }
|
||
assert.ok(threw, '重复 source_clip_id 应抛 UNIQUE 错误');
|
||
fav.close(db);
|
||
});
|
||
|
||
check('add 插入图片并保留 BLOB', () => {
|
||
const dbPath2 = path.join(tmpHome, 'add-img.db');
|
||
const db = fav.open(dbPath2);
|
||
const now = Date.now() / 1000;
|
||
const buf = Buffer.from([0x89, 0x50, 0x4E, 0x47]); // 假 PNG 头
|
||
const clip = { id: 103, type: 'image', image: buf, preview: '[图片]', createdAt: now };
|
||
const row = fav.add(db, clip);
|
||
assert.strictEqual(row.type, 'image');
|
||
// 列表/详情不再内联图片数据,只给一个 hasImage 标记;
|
||
// 真正的字节走 getImage() 按需取(否则列表要背几百 MB 的 base64)
|
||
assert.strictEqual(row.hasImage, true);
|
||
assert.strictEqual(row.imageBase64, undefined, '不应再内联 base64');
|
||
const b64 = fav.getImage(db, row.id);
|
||
assert.strictEqual(typeof b64, 'string');
|
||
assert.strictEqual(Buffer.compare(Buffer.from(b64, 'base64'), buf), 0);
|
||
fav.close(db);
|
||
});
|
||
|
||
check('getImage 对文本条目返回 null', () => {
|
||
const dbPath2 = path.join(tmpHome, 'getimg-text.db');
|
||
const db = fav.open(dbPath2);
|
||
const row = fav.add(db, { id: 201, type: 'text', text: 'x', preview: 'x', createdAt: Date.now()/1000 });
|
||
assert.strictEqual(fav.getImage(db, row.id), null);
|
||
fav.close(db);
|
||
});
|
||
|
||
check('count 返回条数且不加载 BLOB', () => {
|
||
const dbPath2 = path.join(tmpHome, 'count.db');
|
||
const db = fav.open(dbPath2);
|
||
assert.strictEqual(fav.count(db), 0);
|
||
fav.add(db, { id: 301, type: 'text', text: 'a', preview: 'a', createdAt: Date.now()/1000 });
|
||
fav.add(db, { id: 302, type: 'image', image: Buffer.from([1,2,3]), preview: '[图片]', createdAt: Date.now()/1000 });
|
||
assert.strictEqual(fav.count(db), 2);
|
||
fav.close(db);
|
||
});
|
||
|
||
check('list 不内联图片数据', () => {
|
||
const dbPath2 = path.join(tmpHome, 'list-noimg.db');
|
||
const db = fav.open(dbPath2);
|
||
fav.add(db, { id: 401, type: 'image', image: Buffer.from([9,9,9]), preview: '[图片]', createdAt: Date.now()/1000 });
|
||
const rows = fav.list(db, '', 10);
|
||
assert.strictEqual(rows.length, 1);
|
||
assert.strictEqual(rows[0].hasImage, true);
|
||
assert.strictEqual(rows[0].imageBase64, undefined);
|
||
fav.close(db);
|
||
});
|
||
|
||
check('isFavorited 返回 favId 或 null', () => {
|
||
const dbPath2 = path.join(tmpHome, 'isfav.db');
|
||
const db = fav.open(dbPath2);
|
||
assert.strictEqual(fav.isFavorited(db, 103), null, '未收藏');
|
||
fav.add(db, { id: 104, type: 'text', text: 'x', preview: 'x', createdAt: Date.now()/1000 });
|
||
const id = fav.isFavorited(db, 104);
|
||
assert.ok(typeof id === 'number' && id > 0, '已收藏应返回数字 favId');
|
||
fav.close(db);
|
||
});
|
||
|
||
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
||
|
||
(async () => {
|
||
check('remove 删除存在的 fav', () => {
|
||
const dbPath2 = path.join(tmpHome, 'remove.db');
|
||
const db = fav.open(dbPath2);
|
||
const row = fav.add(db, { id: 201, type: 'text', text: 'a', preview: 'a', createdAt: Date.now()/1000 });
|
||
assert.strictEqual(fav.remove(db, row.id), true);
|
||
assert.strictEqual(fav.get(db, row.id), null);
|
||
fav.close(db);
|
||
});
|
||
|
||
check('remove 不存在的 fav 返回 false', () => {
|
||
const dbPath2 = path.join(tmpHome, 'remove-missing.db');
|
||
const db = fav.open(dbPath2);
|
||
assert.strictEqual(fav.remove(db, 99999), false);
|
||
fav.close(db);
|
||
});
|
||
|
||
check('get 取单条', () => {
|
||
const dbPath2 = path.join(tmpHome, 'get.db');
|
||
const db = fav.open(dbPath2);
|
||
const row = fav.add(db, { id: 202, type: 'text', text: 'b', preview: 'b', createdAt: Date.now()/1000 });
|
||
const got = fav.get(db, row.id);
|
||
assert.strictEqual(got.text, 'b');
|
||
assert.strictEqual(got.id, row.id);
|
||
fav.close(db);
|
||
});
|
||
|
||
await check('list 默认按 favorited_at DESC', async () => {
|
||
const dbPath2 = path.join(tmpHome, 'list-order.db');
|
||
const db = fav.open(dbPath2);
|
||
fav.add(db, { id: 301, type: 'text', text: 'first', preview: 'first', createdAt: Date.now()/1000 });
|
||
await sleep(10);
|
||
fav.add(db, { id: 302, type: 'text', text: 'second', preview: 'second', createdAt: Date.now()/1000 });
|
||
await sleep(10);
|
||
fav.add(db, { id: 303, type: 'text', text: 'third', preview: 'third', createdAt: Date.now()/1000 });
|
||
const all = fav.list(db);
|
||
assert.strictEqual(all.length, 3);
|
||
assert.strictEqual(all[0].text, 'third');
|
||
assert.strictEqual(all[1].text, 'second');
|
||
assert.strictEqual(all[2].text, 'first');
|
||
fav.close(db);
|
||
});
|
||
|
||
check('list 支持 limit', () => {
|
||
const dbPath2 = path.join(tmpHome, 'list-limit.db');
|
||
const db = fav.open(dbPath2);
|
||
for (let i = 0; i < 5; i++) fav.add(db, { id: 400+i, type: 'text', text: 'x'+i, preview: 'x'+i, createdAt: Date.now()/1000 });
|
||
assert.strictEqual(fav.list(db, '', 2).length, 2);
|
||
fav.close(db);
|
||
});
|
||
|
||
// getRaw:主进程「复制收藏到系统剪贴板」的取数路径(favorites:copy)。
|
||
// 必须带 image BLOB 且字段形状与 clips 表一致(type / text / image),
|
||
// 才能直接喂 lib/clip-payload。
|
||
check('getRaw 取文本收藏的原始行', () => {
|
||
const dbPath2 = path.join(tmpHome, 'getraw-text.db');
|
||
const db = fav.open(dbPath2);
|
||
const row = fav.add(db, { id: 501, type: 'text', text: 'raw-text', preview: 'raw-text', createdAt: Date.now()/1000 });
|
||
const raw = fav.getRaw(db, row.id);
|
||
assert.ok(raw, '应取到原始行');
|
||
assert.strictEqual(raw.type, 'text');
|
||
assert.strictEqual(raw.text, 'raw-text');
|
||
assert.strictEqual(raw.image, null);
|
||
fav.close(db);
|
||
});
|
||
|
||
check('getRaw 取图片收藏时带回 BLOB', () => {
|
||
const dbPath2 = path.join(tmpHome, 'getraw-img.db');
|
||
const db = fav.open(dbPath2);
|
||
const buf = Buffer.from([0x89, 0x50, 0x4E, 0x47, 7, 7]);
|
||
const row = fav.add(db, { id: 502, type: 'image', image: buf, preview: '[图片]', createdAt: Date.now()/1000 });
|
||
const raw = fav.getRaw(db, row.id);
|
||
assert.ok(raw, '应取到原始行');
|
||
assert.strictEqual(raw.type, 'image');
|
||
assert.ok(Buffer.isBuffer(raw.image), 'image 列应是 Buffer');
|
||
assert.strictEqual(Buffer.compare(raw.image, buf), 0, 'BLOB 内容应原样取回');
|
||
fav.close(db);
|
||
});
|
||
|
||
check('getRaw 对不存在的 favId 返回 null', () => {
|
||
const dbPath2 = path.join(tmpHome, 'getraw-missing.db');
|
||
const db = fav.open(dbPath2);
|
||
assert.strictEqual(fav.getRaw(db, 99999), null);
|
||
fav.close(db);
|
||
});
|
||
|
||
// ---------- 搜索 ----------
|
||
// 与历史搜索同语义(lib/search-plan):多 token AND、% _ \ 转义。
|
||
check('list 搜索:LIKE 元字符按字面匹配(% _ 不是通配符)', () => {
|
||
const dbPath2 = path.join(tmpHome, 'search-esc.db');
|
||
const db = fav.open(dbPath2);
|
||
fav.add(db, { id: 601, type: 'text', text: '100% off_sale promo', preview: '', createdAt: Date.now()/1000 });
|
||
fav.add(db, { id: 602, type: 'text', text: '100 apples for sale', preview: '', createdAt: Date.now()/1000 });
|
||
fav.add(db, { id: 603, type: 'text', text: 'aXb wildcard victim', preview: '', createdAt: Date.now()/1000 });
|
||
assert.strictEqual(fav.list(db, '100%').length, 1, '% 不能当通配符匹配一切以 100 开头的内容');
|
||
assert.strictEqual(fav.list(db, 'off_sale').length, 1, '_ 不能匹配任意单字符');
|
||
assert.strictEqual(fav.list(db, 'a_b').length, 0, '没有字面含 a_b 的记录');
|
||
fav.close(db);
|
||
});
|
||
|
||
check('list 搜索:多 token 是 AND、顺序无关', () => {
|
||
const dbPath2 = path.join(tmpHome, 'search-and.db');
|
||
const db = fav.open(dbPath2);
|
||
fav.add(db, { id: 611, type: 'text', text: 'walk in the park', preview: '', createdAt: Date.now()/1000 });
|
||
fav.add(db, { id: 612, type: 'text', text: 'park bench only', preview: '', createdAt: Date.now()/1000 });
|
||
assert.strictEqual(fav.list(db, 'park walk').length, 1, '两个 token 都出现才命中');
|
||
assert.strictEqual(fav.list(db, 'walk park').length, 1, '顺序无关');
|
||
assert.strictEqual(fav.list(db, 'park missing').length, 0, '缺一个就不命中');
|
||
fav.close(db);
|
||
});
|
||
|
||
console.log(`\n${passed} checks passed`);
|
||
})(); |