dream-routes.test.js
4.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/**
* dream 的 REST 入口(29-DREAM,见 docs/done/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 }]);
});
});