/** * 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'); // 端点的本体根是硬编码的 `/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); } }); });