task-dispatch-store.test.js 4.8 KB
/**
 * 批 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);
  });
});