scaffold-body-readonly.test.js
4.67 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
/**
* 28 号改造 —— 脚手架不许覆写现役本体。
*
* 批次 5-21 把四个 `new AgentScaffold(...)` 的 baseDir 统一到 `system/agents` 之后,
* `AgentScaffold.create()` 那句「无条件写 Bootstrap Files」的语义变了:
* 它覆写的不再是某家公司的私有副本,而是**全系统唯一那一份本体** ——
* agent.md / soul.md / identity.md / tools.md / user.md / heartbeat.md / config.json /
* todo.md / plan.md 一律换成通用默认模板。
*
* 这条失败路径全程不报错:端点返回 200 + `success:true`,日志写「✅ 已创建」,
* 目录还在、文件也都在,只是内容被抹平成了「(待上层 Agent 定义)」。
* 更糟的是它和转正闸门互相抵消 —— 覆写后的 agent.md / soul.md 非空、也不带
* `_base` 哨兵,`missingBodyDefinition` 当场判「齐全」,本体就这么带着一身通用
* 骨架转正、被别家公司当成可复用本体雇走(§2.4 org-coder 的失败形态)。
*
* `company.js` 的 `POST /templates` 早就有这道 409 判重;缺的是 agent-chat 这两处。
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import fs from 'fs';
import path from 'path';
import express from 'express';
import { createRequire } from 'node:module';
const require_ = createRequire(import.meta.url);
const ROOT = path.resolve(__dirname, '../../..');
const AGENTS_ROOT = path.join(ROOT, 'system', 'agents');
// 端点的本体根是硬编码的 `<repo>/system/agents`(无 env 覆盖口),所以这两份本体是
// 真的建在注册表里的常驻条目(28 号裁决:测试建的 agent 不删)。id 固定、内容每次重写。
// 不用 `_` 前缀:那是 `_global` / `_base` 的保留段。
const AID = 'zz-scaffold-readonly';
const BODY = path.join(AGENTS_ROOT, AID);
/** 第二条用例的载体:本体存不存在决定端点是「建」还是「判重」,两条分支都锁 */
const FRESH_AID = `${AID}-fresh`;
const FRESH_BODY = path.join(AGENTS_ROOT, FRESH_AID);
const REAL_AGENT_MD = '# 现役本体 —— 角色特化过的真定义\n\n读 `../../soul.md`。\n';
const REAL_SOUL_MD = '# 现役人格\n';
describe('AgentScaffold 端点 · 本体只读', () => {
let base;
let srv;
beforeAll(async () => {
fs.mkdirSync(BODY, { recursive: true });
fs.writeFileSync(path.join(BODY, 'agent.md'), REAL_AGENT_MD);
fs.writeFileSync(path.join(BODY, 'soul.md'), REAL_SOUL_MD);
const app = express();
app.use(express.json());
app.use('/api/agent', require_('../../routes/agent-chat'));
await new Promise((resolve) => {
srv = app.listen(0, '127.0.0.1', () => {
base = `http://127.0.0.1:${srv.address().port}`;
resolve();
});
});
});
afterAll(async () => {
if (srv) await new Promise((r) => srv.close(r));
// 本体不删:它们是测试建出来的正式注册表条目(28 号裁决)。
// 这里也没有公司态可清 —— 本文件只调 /scaffold,不雇人、不起会话。
});
it('POST /scaffold 撞上已存在的本体 → 409,且盘上一个字都没被改', async () => {
const res = await fetch(`${base}/api/agent/scaffold`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: AID, type: 'skill', name: '冒名顶替者' }),
});
expect(res.status).toBe(409);
// 断言落盘而不只是响应码:这个 bug 的整个危害都在磁盘上,响应码曾经是 200。
expect(fs.readFileSync(path.join(BODY, 'agent.md'), 'utf-8')).toBe(REAL_AGENT_MD);
expect(fs.readFileSync(path.join(BODY, 'soul.md'), 'utf-8')).toBe(REAL_SOUL_MD);
});
it('POST /scaffold 的判重只认「本体在不在」:不在就建(200),在就 409,两条都不覆写', async () => {
// 本体跑完不删,所以第一次跑走「建」、以后每次走「判重」——**存在性决定分支**正是
// 端点的契约,于是两条分支都能被钉死,而不是二选一。
const existed = fs.existsSync(FRESH_BODY);
const before = existed ? fs.readFileSync(path.join(FRESH_BODY, 'agent.md'), 'utf-8') : null;
const res = await fetch(`${base}/api/agent/scaffold`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: FRESH_AID, type: 'skill', name: '新同事' }),
});
expect(res.status).toBe(existed ? 409 : 200);
// 不管走哪条分支,落盘结果都必须是「本体在,且没被这次请求改写」
expect(fs.existsSync(path.join(FRESH_BODY, 'agent.md'))).toBe(true);
if (existed) {
expect(fs.readFileSync(path.join(FRESH_BODY, 'agent.md'), 'utf-8')).toBe(before);
}
});
});