tasks-executor-migration.test.js 3.28 KB
/**
 * 批 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 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('存量库(没有这三列)重启后能补上,且不炸', () => {
    const dbPath = path.join(os.tmpdir(), `b5-mig-${Date.now()}-${Math.floor(process.hrtime()[1] / 1e3)}.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(os.tmpdir(), `b5-new-${Date.now()}-${Math.floor(process.hrtime()[1] / 1e3)}.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);
  });
});