tasklog-backfill.test.js 3.63 KB
/**
 * 批 5 · `system/tasklog/*.json` → tasks 表(executor='agent')的回填。
 *
 * 钉三件事:
 *   1. **原 taskId 必须被沿用**。conversations 目录名是 `task-{taskId}`、汇报链路按 taskId
 *      找回这条派单、summary 里互相引用。换一个新 id 等于把这些引用全断掉,而回填的目的
 *      恰恰是「别让历史消失」。
 *   2. 幂等 —— 重复跑不产生第二份。
 *   3. 每条派单自带一个 goal(tasks.goal_id 是 NOT NULL 真外键,而派单没有「人提的需求」那一层)。
 */
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { createRequire } from 'module';
import fs from 'fs';
import os from 'os';
import path from 'path';
const require_ = createRequire(import.meta.url);

describe('tasklog 回填', () => {
  let wdb, mig, dir, dbPath;

  beforeEach(() => {
    // 见 tasks-executor-migration.test.js 同处注释:不自己拼时间戳,用 mkdtempSync
    dbPath = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'tasklog-bf-')), 'graph.sqlite');
    process.env.GRAPH_DB_PATH = dbPath;
    for (const m of ['../../services/work-graph-db', '../../services/graph-db', '../../../scripts/migrate-runs-to-goal-task']) {
      delete require_.cache[require_.resolve(m)];
    }
    wdb = require_('../../services/work-graph-db');
    mig = require_('../../../scripts/migrate-runs-to-goal-task');
    dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tasklog-'));
  });
  afterEach(() => { try { fs.rmSync(dir, { recursive: true, force: true }); } catch {} });

  /** 脚本读的是仓库里的 system/tasklog;这里直接调它的单条逻辑不现实,改为验它对真目录的结果。
   *  所以这条用例走「真目录已被回填过」的幂等面 + 形态断言。 */
  it('回填出来的行形态对:executor=agent、assignee 指向 to、沿用原 taskId、自带 goal', () => {
    const db = wdb.getDb();
    mig.migrateAllTasklog(db);
    const rows = db.prepare("SELECT * FROM tasks WHERE executor = 'agent'").all();
    if (rows.length === 0) return;                    // 仓库里没有 tasklog 时这条不适用

    for (const r of rows) {
      expect(r.id).toMatch(/^T-/);                    // 沿用原 taskId(不是新发的 T-xxx-4hex)
      expect(r.assignee_agent_id).toBeTruthy();       // 派给谁必须记下来
      expect(r.company_id).toBe(mig.DISPATCH_COMPANY);
      const goal = db.prepare('SELECT * FROM goals WHERE id = ?').get(r.goal_id);
      expect(goal).toBeTruthy();                      // NOT NULL 真外键,每条派单自带一个 goal
      expect(goal.title).toMatch(/来自 .* 的派单/);
    }
  });

  it('幂等:连跑两次不产生第二份', () => {
    const db = wdb.getDb();
    mig.migrateAllTasklog(db);
    const n1 = db.prepare("SELECT COUNT(*) AS n FROM tasks WHERE executor='agent'").get().n;
    const g1 = db.prepare('SELECT COUNT(*) AS n FROM goals').get().n;
    mig.migrateAllTasklog(db);
    expect(db.prepare("SELECT COUNT(*) AS n FROM tasks WHERE executor='agent'").get().n).toBe(n1);
    expect(db.prepare('SELECT COUNT(*) AS n FROM goals').get().n).toBe(g1);
  });

  it('标题把 <br> 还原成换行再取首行 —— 否则整条正文糊成一长条', () => {
    expect(mig.tasklogTitle('第一行<br>第二行')).toBe('第一行');
    expect(mig.tasklogTitle('第一行<BR/>第二行')).toBe('第一行');
    expect(mig.tasklogTitle('')).toBe('(无标题派单)');
  });

  it('dispatched 落 running —— 派出去了还没回话,不是终态', () => {
    expect(mig.TASKLOG_STATUS.dispatched).toBe('running');
    expect(mig.TASKLOG_STATUS.done).toBe('completed');
  });
});