Files
Clipboard/test/migrate-idempotent.test.js
2026-09-12 13:59:12 +08:00

129 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* 回归测试v1 -> v2 迁移
*
* 曾经的 bugmigrate() 用 STEPS[v - 1] 取步骤,但 STEPS[0] 就是 v1->v2 的步骤,
* 所以 v=2 时取到的是 STEPS[1] === undefined整个 FTS5 迁移从来没跑过。
* 表现clips_fts 永远不存在,搜索一直静默退回 LIKE 全表扫描。
*/
const assert = require('node:assert');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const Database = require('better-sqlite3');
const { migrate, STEPS } = require('../lib/migrate.js');
let passed = 0;
function check(name, fn) {
try { fn(); console.log(' ok -', name); passed++; }
catch (e) { console.error(' FAIL -', name, e.message); process.exitCode = 1; }
}
function freshDb() {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'clip-mig2-'));
const db = new Database(path.join(tmp, 'h.db'));
db.exec(`CREATE TABLE clips (
id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT, text TEXT,
top_at REAL, created_at REAL NOT NULL DEFAULT 0)`);
return db;
}
check('STEPS[0] is the v1->v2 step (index maps version-2, not version-1)', () => {
assert.strictEqual(typeof STEPS[0], 'function');
assert.match(STEPS[0].name, /v1_to_v2/);
});
// 探测这个 SQLite 构建是否带 FTS5没有的话下面的断言换成"优雅降级"
let fts5 = true;
{
const db = freshDb();
try { db.exec(`CREATE VIRTUAL TABLE t USING fts5(x)`); } catch (_) { fts5 = false; }
db.close();
}
check('migrate actually reaches v2 and reports it', () => {
const db = freshDb();
const reached = migrate(db, 1, 2);
if (fts5) {
assert.strictEqual(reached, 2, '迁移应到达 v2');
assert.strictEqual(db.pragma('user_version', { simple: true }), 2);
} else {
assert.strictEqual(reached, 1, '没有 FTS5 时应停在 v1而不是谎报成功');
}
db.close();
});
if (fts5) {
check('clips_fts exists and is searchable after migrate', () => {
const db = freshDb();
db.prepare('INSERT INTO clips(type,text) VALUES(?,?)').run('text', 'hello world');
migrate(db, 1, 2);
const rows = db.prepare(`SELECT rowid FROM clips_fts WHERE clips_fts MATCH 'hello'`).all();
assert.strictEqual(rows.length, 1, '已有数据应被回填进 FTS 索引');
db.close();
});
check('migrate is idempotent across restarts (no duplicate index entries)', () => {
const db = freshDb();
db.prepare('INSERT INTO clips(type,text) VALUES(?,?)').run('text', 'hello world');
for (let launch = 0; launch < 3; launch++) migrate(db, 1, 2);
const n = db.prepare(`SELECT count(*) c FROM clips_fts WHERE clips_fts MATCH 'hello'`).get().c;
assert.strictEqual(n, 1, '重复启动不应重复回填');
db.exec(`INSERT INTO clips_fts(clips_fts) VALUES('integrity-check')`);
db.close();
});
check('user_version short-circuits the backfill on subsequent runs', () => {
const db = freshDb();
migrate(db, 1, 2);
// 直接往 clips 插一行但绕开触发器是做不到的,所以改为验证:
// 第二次 migrate 不再执行回填(否则这行会被索引两次)
db.prepare('INSERT INTO clips(type,text) VALUES(?,?)').run('text', 'unique token zzz');
migrate(db, 1, 2);
const n = db.prepare(`SELECT count(*) c FROM clips_fts WHERE clips_fts MATCH 'zzz'`).get().c;
assert.strictEqual(n, 1);
db.close();
});
check('AFTER UPDATE trigger skips FTS churn when only top_at changes', () => {
const db = freshDb();
db.prepare('INSERT INTO clips(type,text) VALUES(?,?)').run('text', 'hello world');
migrate(db, 1, 2);
db.prepare('UPDATE clips SET top_at = ? WHERE id = 1').run(123);
assert.strictEqual(
db.prepare(`SELECT count(*) c FROM clips_fts WHERE clips_fts MATCH 'hello'`).get().c, 1,
'置顶不应影响可搜索性');
db.exec(`INSERT INTO clips_fts(clips_fts) VALUES('integrity-check')`);
db.close();
});
check('AFTER UPDATE trigger still re-indexes when text changes', () => {
const db = freshDb();
db.prepare('INSERT INTO clips(type,text) VALUES(?,?)').run('text', 'hello world');
migrate(db, 1, 2);
db.prepare('UPDATE clips SET text = ? WHERE id = 1').run('goodbye moon');
assert.strictEqual(db.prepare(`SELECT count(*) c FROM clips_fts WHERE clips_fts MATCH 'hello'`).get().c, 0);
assert.strictEqual(db.prepare(`SELECT count(*) c FROM clips_fts WHERE clips_fts MATCH 'goodbye'`).get().c, 1);
db.close();
});
check('DELETE keeps the FTS index consistent', () => {
const db = freshDb();
db.prepare('INSERT INTO clips(type,text) VALUES(?,?)').run('text', 'hello world');
migrate(db, 1, 2);
db.prepare('DELETE FROM clips WHERE id = 1').run();
assert.strictEqual(db.prepare(`SELECT count(*) c FROM clips_fts WHERE clips_fts MATCH 'hello'`).get().c, 0);
db.exec(`INSERT INTO clips_fts(clips_fts) VALUES('integrity-check')`);
db.close();
});
} else {
console.log(' SKIP - FTS5 unavailable in this SQLite build');
}
check('migrate on a broken db degrades instead of throwing', () => {
const bad = { pragma() { throw new Error('nope'); }, exec() { throw new Error('nope'); } };
assert.strictEqual(migrate(bad, 1, 2), 1);
});
console.log(`\n${passed} checks passed`);