// tests/unit/main/native-module.test.ts // // 守卫:当前测试运行器下 better-sqlite3 原生模块必须真的能加载并读写。 // // 为什么要有这条(2026-10-19 实地复现): // better-sqlite3 不是 N-API,一个 .node 只对应一个 ABI —— Electron 30.5 用 // ABI 123,系统 Node 24 用 ABI 137,两者共用同一个 // node_modules/better-sqlite3/build/Release/better_sqlite3.node。 // 旧 pretest 钩子为了跑测试会**破坏性重建**这份模块(把 app 那份覆盖成 // Node 版),Windows 上文件被运行中的 electron 进程锁住 → EBUSY → // 整条 `npm run test:run` 中止,哪怕测试本身全绿。 // // 现在 scripts/run-tests.cjs 改成自适应:能用系统 Node 加载就用系统 Node, // 否则用 Electron 自带 Node 跑(ELECTRON_RUN_AS_NODE=1),测试与 app 共用 // 同一份原生模块,不再有互相覆盖。 // // 这条测试锁住「跑测试的那条路线下原生模块可用」这个前提 —— // 将来谁再把 pretest 换回 `npm rebuild better-sqlite3`,或让 CI 换回 // 裸 `npx vitest run`,这条会以「模块加载失败 + 该用哪条命令」的形式立刻 // 暴露,而不是变成一堆莫名其妙的 SQLITE_CANTOPEN。 // // 兼容性:两条路线下本用例都应通过 —— // - 系统 Node 路线:模块是 Node ABI 版(或 napi 通用版),直接可加载 // - Electron 路线:模块是 Electron ABI 版,Electron 自带 Node 可加载 import { describe, expect, it } from 'vitest'; interface TestDb { exec(sql: string): void; close(): void; prepare(sql: string): { run(...args: unknown[]): { changes: number }; get(...args: unknown[]): unknown; }; pragma(sql: string): unknown; } type DatabaseCtor = new (path: string) => TestDb; const ABI_HINT = [ '[native-module] 当前测试运行器加载不了 better-sqlite3 原生模块。', ' better-sqlite3 不是 N-API,一个 .node 只对应一个 ABI(Electron / 系统 Node 各一份),', ' 两份共用 node_modules/better-sqlite3/build/Release/better_sqlite3.node。', ' 请用 `npm run test:run` —— scripts/run-tests.cjs 会自动挑能加载的那条路线', ' (系统 Node 直跑,或 Electron 自带 Node 跑 ELECTRON_RUN_AS_NODE=1);', ' 裸 `npx vitest run` 会用系统 Node,而此刻那份 .node 可能是 Electron ABI 的。', ].join('\n'); /** * 打开内存库。注意:**原生 binding 是构造时才加载的**(require 只是拿到 * JS 包装),所以 import 成功不代表 ABI 匹配 —— 必须在 new Database() 外面 * 兜 try/catch,否则报的是难懂的 "NODE_MODULE_VERSION 123 vs 137"。 */ async function openMemoryDb(): Promise { let ctor: DatabaseCtor | undefined; try { const mod = (await import('better-sqlite3')) as unknown as { default: DatabaseCtor }; ctor = mod.default; return new ctor(':memory:'); } catch (err) { expect.fail( `${ABI_HINT}\n 原始错误:${err instanceof Error ? err.message : String(err)}`, ); throw err; // eslint-disable-line no-unreachable -- expect.fail 已抛,仅为满足类型 } } describe('better-sqlite3 原生模块', () => { it('能打开内存库、建表、写入并读回', async () => { const db = await openMemoryDb(); try { db.exec('create table t (a integer primary key, b text not null)'); const ins = db.prepare('insert into t (b) values (?)'); const res = ins.run('hello'); expect(res.changes).toBe(1); const row = db.prepare('select a, b from t where a = ?').get(1) as { a: number; b: string; }; expect(row).toEqual({ a: 1, b: 'hello' }); } finally { db.close(); } }); it('pragma 可用(主进程依赖 journal_mode / foreign_keys 的行为)', async () => { const db = await openMemoryDb(); try { // journal_mode 在内存库上恒为 memory,这里只断言 pragma 调用本身可用 expect(() => db.pragma('journal_mode')).not.toThrow(); expect(() => db.pragma('foreign_keys = ON')).not.toThrow(); const fk = db.pragma('foreign_keys') as Array<{ foreign_keys: number }>; expect(fk[0]?.foreign_keys).toBe(1); } finally { db.close(); } }); });