/** * 批 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('标题把
还原成换行再取首行 —— 否则整条正文糊成一长条', () => { expect(mig.tasklogTitle('第一行
第二行')).toBe('第一行'); expect(mig.tasklogTitle('第一行
第二行')).toBe('第一行'); expect(mig.tasklogTitle('')).toBe('(无标题派单)'); }); it('dispatched 落 running —— 派出去了还没回话,不是终态', () => { expect(mig.TASKLOG_STATUS.dispatched).toBe('running'); expect(mig.TASKLOG_STATUS.done).toBe('completed'); }); });