/** * 批 5 · `tasks` 表吸收三列(executor / assignee_agent_id / parent_task_id)的**迁移路径**。 * * 为什么单独钉这一条:加列时我把 `CREATE INDEX ... ON tasks(assignee_agent_id)` 写进了建表块, * 而建表块整体先于迁移块执行 —— 存量库上 `CREATE TABLE IF NOT EXISTS tasks` 是 no-op, * 表还是加列前的形态,那一句索引直接抛「no such column」,`getDb()` 起不来。 * 后果不是少个功能,是**所有存量库一启动就炸**,而新建库(单测默认走的那条路)完全正常 —— * 只测新建库的话这个洞看不见。 */ import { describe, it, expect } from 'vitest'; import { createRequire } from 'module'; import fs from 'fs'; import os from 'os'; import path from 'path'; const require_ = createRequire(import.meta.url); const NEW_COLS = ['executor', 'assignee_agent_id', 'parent_task_id']; /** 用独立 DB 文件跑一遍 getDb(),返回该库的 work-graph-db 模块实例 */ function freshDb(dbPath) { process.env.GRAPH_DB_PATH = dbPath; for (const m of ['../../services/work-graph-db', '../../services/graph-db']) { delete require_.cache[require_.resolve(m)]; } const wdb = require_('../../services/work-graph-db'); wdb.getDb(); return wdb; } describe('tasks 表的批 5 迁移', () => { it('存量库(没有这三列)重启后能补上,且不炸', () => { // 用 mkdtempSync 拿唯一目录,而不是自己拿 Date.now() 拼文件名 —— // 后者会撞 test-id-stability 那条守卫(测试里不许用时间戳拼 id),而且 mkdtempSync // 本来就是仓库既有写法(graph-engine.test.js 的 runsDir 就是它)。 const dbPath = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'b5-mig-')), 'graph.sqlite'); const wdb = freshDb(dbPath); // 退回加列之前的形态:索引先删(它引用了要删的列),再删三列 const Database = require_('better-sqlite3'); const raw = new Database(dbPath); raw.exec('DROP INDEX IF EXISTS idx_tasks_assignee'); for (const c of NEW_COLS) raw.exec(`ALTER TABLE tasks DROP COLUMN ${c}`); const before = raw.pragma('table_info(tasks)').map((c) => c.name); raw.close(); for (const c of NEW_COLS) expect(before).not.toContain(c); // 重开进程 = 服务重启:迁移必须把三列和索引都补回来,且不抛 const wdb2 = freshDb(dbPath); const cols = wdb2.getDb().pragma('table_info(tasks)'); const names = cols.map((c) => c.name); for (const c of NEW_COLS) expect(names).toContain(c); // 旧行的落点:executor 缺省 'graph'(加列前建的全是图任务) expect(cols.find((c) => c.name === 'executor').dflt_value).toBe("'graph'"); // 索引必须建在 ALTER 之后 —— 顺序反了就是上面注释里那个「存量库一启动就炸」 const idx = wdb2.getDb() .prepare("SELECT 1 AS ok FROM sqlite_master WHERE type='index' AND name='idx_tasks_assignee'") .get(); expect(idx?.ok).toBe(1); expect(wdb).toBeTruthy(); // 前一个实例只是用来建库,这里避免 lint 抱怨未使用 }); it('新建库直接就是目标形态(三列 + 索引齐全)', () => { const dbPath = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'b5-new-')), 'graph.sqlite'); const wdb = freshDb(dbPath); const names = wdb.getDb().pragma('table_info(tasks)').map((c) => c.name); for (const c of NEW_COLS) expect(names).toContain(c); expect(wdb.getDb().prepare("SELECT 1 AS ok FROM sqlite_master WHERE type='index' AND name='idx_tasks_assignee'").get()?.ok).toBe(1); }); });