/** * 批 5 第 3 步:`/task/dispatch` 的存储从 system/tasklog/*.json 换成 tasks 表。 * * **这一步唯一不能坏的是契约**:`/task/dispatch` 与 `/task/report` 的 URL、请求体、响应体 * 印在 `packages/agent-core/base/common/tools-base.md` 里,是所有 agent 的工具说明 —— * 改一个字等于同时改所有 agent 的提示词。所以这份用例盯的是「形状没变」,不是「实现对不对」。 */ import { describe, it, expect, beforeAll, afterAll } 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('派单存储换成 tasks 表', () => { let wdb, app, srv, base; beforeAll(async () => { process.env.GRAPH_DB_PATH = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'dispatch-store-')), 'graph.sqlite'); for (const m of ['../../services/work-graph-db', '../../services/graph-db', '../../routes/agent-chat']) { delete require_.cache[require_.resolve(m)]; } wdb = require_('../../services/work-graph-db'); const express = require_('express'); app = express(); app.use(express.json()); app.use('/api/agent', require_('../../routes/agent-chat')); await new Promise((r) => { srv = app.listen(0, '127.0.0.1', r); }); base = `http://127.0.0.1:${srv.address().port}`; }); afterAll(() => { try { srv?.close(); } catch {} }); const post = async (p, body) => { const r = await fetch(base + p, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); return { status: r.status, body: await r.json().catch(() => ({})) }; }; /** 用真实存在的两个本体(端点会校验目录存在) */ const FROM = 'ceo'; const TO = 'qa-judge'; it('派单落进 tasks 表,且响应体是老形态(agent 提示词里印着它)', async () => { const r = await post('/api/agent/task/dispatch', { from: FROM, to: TO, task: '守卫用例:形态检查', priority: 'P1', goalId: 'O-900', nodeId: 'O-900.1.1', autoExecute: false, }); expect(r.status).toBe(200); // 响应体的字段一个都不能少 —— 少了就是改了所有 agent 的工具说明 for (const k of ['success', 'taskId', 'from', 'to', 'task', 'priority', 'autoExecute', 'message']) { expect(r.body).toHaveProperty(k); } expect(r.body.success).toBe(true); const row = wdb.getDb().prepare('SELECT * FROM tasks WHERE id = ?').get(r.body.taskId); expect(row).toBeTruthy(); expect(row.executor).toBe('agent'); // 调度器据此跳过它(图任务才排队) expect(row.assignee_agent_id).toBe(TO); expect(row.created_by).toBe(FROM); expect(row.status).toBe('running'); // meta 的 dispatched ↔ 表的 running // 每条派单自带一个 goal:goal_id 是 NOT NULL 真外键,派单没有「人提的需求」那一层 const goal = wdb.getDb().prepare('SELECT * FROM goals WHERE id = ?').get(row.goal_id); expect(goal.title).toContain(FROM); expect(goal.okr_node_id).toBe('O-900.1.1'); // objectiveId 由它的首段推导,不单独存 }); it('去重仍然拦得住,且 409 的响应体形态没变', async () => { const body = { from: FROM, to: TO, task: '守卫用例:去重', autoExecute: false }; const first = await post('/api/agent/task/dispatch', body); expect(first.status).toBe(200); const dup = await post('/api/agent/task/dispatch', body); expect(dup.status).toBe(409); for (const k of ['success', 'error', 'existingTaskId', 'existingTask', 'message']) { expect(dup.body).toHaveProperty(k); } expect(dup.body.existingTaskId).toBe(first.body.taskId); }); it('汇报回路:状态 / summary / 完成时间都落到表上', async () => { const d = await post('/api/agent/task/dispatch', { from: FROM, to: TO, task: '守卫用例:汇报回路', autoExecute: false, }); const rep = await post('/api/agent/task/report', { taskId: d.body.taskId, from: TO, to: FROM, status: 'done', summary: '干完了', }); expect(rep.status).toBe(200); const row = wdb.getDb().prepare('SELECT * FROM tasks WHERE id = ?').get(d.body.taskId); expect(row.status).toBe('completed'); // meta 的 done ↔ 表的 completed expect(row.summary).toBe('干完了'); expect(row.finished_at).toBeTruthy(); }); it('不再往 system/tasklog/ 写新文件', async () => { const dir = path.resolve(process.cwd(), 'system', 'tasklog'); const before = fs.existsSync(dir) ? fs.readdirSync(dir).length : 0; await post('/api/agent/task/dispatch', { from: FROM, to: TO, task: '守卫用例:不写文件', autoExecute: false }); const after = fs.existsSync(dir) ? fs.readdirSync(dir).length : 0; expect(after).toBe(before); }); });