/** * dream 的 REST 入口(29-DREAM,见 docs/todo/system-design/29-dream-design.md)。 * * 路由本身没有业务逻辑 —— 全部委托给已经测过的 `dream-service.js`。 * 这份只验两件路由层独有的东西: * ① 六个端点确实接到了对应的 service 函数上(不是「实现在、零调用方」); * ② `POST /sweep` 的并发锁 —— 上一轮没跑完时第二次触发要拒(409), * 跑完之后 `GET /sweep/status` 要能看到结果。这段状态机全部写在 route 里, * `dream-service.test.js` 覆盖不到。 * * `dream-service` 的六个被测函数在这里整体替身(monkeypatch 掉导出对象上的属性, * 用完在 afterAll 里还原)—— 不走真文件系统,也不会误扫真仓库里的 76 个本体。 */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; const express = require('express'); const dream = require('../../services/dream-service'); const dreamRouter = require('../../routes/dream'); const AGENT = 'zz-route-agent'; const orig = {}; function patch(name, fn) { orig[name] = dream[name]; dream[name] = fn; } function restore(name) { dream[name] = orig[name]; } let server; let base; let resolveSweep; beforeAll(() => { patch('listDreamableAgents', () => [AGENT]); patch('readState', () => ({ lastRunAt: '2026-08-20T00:00:00.000Z', lastAccepted: 2, lastRejected: 1 })); patch('companiesOf', () => ['zz-route-co']); patch('definitionWritability', () => [{ file: 'agent.md', allowed: true, reason: '' }]); patch('auditDistilled', () => [{ title: 'X', sources: [{ source: 'zz-route-co:memory.md#1', text: '原文' }] }]); patch('dreamOnce', async (agentId, opts) => ({ agentId, skipped: null, dryRun: !!(opts && opts.dryRun), accepted: 1 })); patch('dreamSweep', () => new Promise((resolve) => { resolveSweep = resolve; })); const app = express(); app.use(express.json()); app.use('/api/dream', dreamRouter); server = app.listen(0); base = `http://127.0.0.1:${server.address().port}/api/dream`; }); afterAll(() => { ['listDreamableAgents', 'readState', 'companiesOf', 'definitionWritability', 'auditDistilled', 'dreamOnce', 'dreamSweep'] .forEach(restore); server.close(); }); describe('dream REST 入口', () => { it('GET /agents 用 listDreamableAgents + readState + companiesOf 拼总览', async () => { const res = await fetch(`${base}/agents`); expect(res.status).toBe(200); const body = await res.json(); expect(body.agents).toEqual([ { agentId: AGENT, companies: 1, lastRunAt: '2026-08-20T00:00:00.000Z', lastAccepted: 2, lastRejected: 1 }, ]); }); it('GET /agents/:id 把状态、定义文件可写性、经验审计一起交出去', async () => { const res = await fetch(`${base}/agents/${AGENT}`); const body = await res.json(); expect(body.agentId).toBe(AGENT); expect(body.companies).toEqual(['zz-route-co']); expect(body.definitions[0].file).toBe('agent.md'); expect(body.experience[0].sources[0].text).toBe('原文'); }); it('POST /agents/:id/run 把 body.dryRun 原样传给 dreamOnce', async () => { const res = await fetch(`${base}/agents/${AGENT}/run`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dryRun: true }), }); const body = await res.json(); expect(body.dryRun).toBe(true); }); it('sweep 并发锁:跑着的时候第二次触发要 409,跑完 status 要能看到结果', async () => { const start = await fetch(`${base}/sweep`, { method: 'POST' }); expect(start.status).toBe(202); const mid = await fetch(`${base}/sweep/status`); expect((await mid.json()).sweeping).toBe(true); const blocked = await fetch(`${base}/sweep`, { method: 'POST' }); expect(blocked.status).toBe(409); resolveSweep([{ agentId: AGENT, accepted: 3 }]); await new Promise((r) => setTimeout(r, 20)); // 让 .then() 回调跑完 const done = await fetch(`${base}/sweep/status`); const doneBody = await done.json(); expect(doneBody.sweeping).toBe(false); expect(doneBody.results).toEqual([{ agentId: AGENT, accepted: 3 }]); }); });