/** * Unit Tests —— work 层三份本体(org-coder / org-planner / code-planner)的路径口径 * 以及**写出它们的那条流水线**(28 号改造二轮,blocker 4)。 * * 这三份本体是 §2.4 的「公司自己养出来的」那一类:没有仓库自带模板,内容由 agent-creator * (一个 LLM)照着仓库里已有的 agent.md 依样画葫芦写出来的。它们原先写的是旧布局的 * `../../workspace/org-planner/prd.md` —— cwd = `data/companies/{cid}/agents/{role}/` 时才成立。 * 28 号把 cwd 换成 `system/agents/{aid}/{cid}/{taskId}/` 之后同一串路径指到 * `system/agents/{aid}/workspace/`:读上游读到空目录、写产物写进 agent 私有目录 * (`indexNodeOutputs` 扫不到),**两头都不报错**。 * * 每条 `../` 引用算得对不对,由 `agent-md-refs.test.js` 对全部本体统一把关,这里不重复。 * 本文件只补它盖不到的三件事: * ① 旧布局那个具体形态不许复活(它恰好也是「上跳两级」,深度对、目标错); * ② 要 agent 去**跑**的仓库内命令(`bin/lemon.js`)必须带上到仓库根的前缀 —— * 不带的话 shell 在任务态目录里找 `bin/lemon.js`,四段式流转整条哑掉; * ③ 这三份本体不写死产物路径、改成去任务描述的三个段头里取绝对路径, * 那三个段头就成了硬依赖:引擎那边一改字面量,它们就去取一个不存在的段。 * * 最后一组(「装配来源」)才是防回归的那条:只把盘上文件改对,下一个 agent-creator * 写出来的照样是旧形态。派单文本必须自己交代运行期布局。 */ import { describe, it, expect } from 'vitest'; import fs from 'fs'; import path from 'path'; const { buildAgentDefinitionBrief } = require('../../services/company-service'); const { GRAPH_WRITE_BOUNDARY_HEADER } = require('../../src/core/prompt-builder'); const ROOT = path.resolve(__dirname, '../../..'); const WORK_LAYER_AGENTS = ['org-coder', 'org-planner', 'code-planner']; /** 到仓库根的上跳数:cwd 是 {aid}/{cid}/{taskId},往上 taskId→cid→aid→agents→system→根 */ const TO_REPO_ROOT = '../../../../../'; /** 旧布局形态:上跳两级(= 本体目录)再进 workspace —— 深度对,目标是个空目录 */ const LEGACY_WORKSPACE_RE = /\.\.\/\.\.\/workspace\//; const bodyText = (aid) => fs.readFileSync(path.join(ROOT, 'system', 'agents', aid, 'agent.md'), 'utf-8'); /** * 引擎拼进任务描述的三个段头。前两个直接从 graph-engine 源码里取字面量、 * 第三个取 prompt-builder 导出的常量 —— 这里再抄一份就成了第四份逐字同构的约定。 */ function engineSectionHeaders() { const engine = fs.readFileSync(path.join(ROOT, 'server', 'services', 'graph-engine.js'), 'utf-8'); const upstream = engine.match(/'(\[上游输入[^']*)'/); const artifactDir = engine.match(/'(\[节点产物目录\])'/); expect(upstream, 'graph-engine 里找不到「上游输入」段头').not.toBeNull(); expect(artifactDir, 'graph-engine 里找不到「节点产物目录」段头').not.toBeNull(); return [upstream[1], artifactDir[1], GRAPH_WRITE_BOUNDARY_HEADER]; } describe('work 层本体的路径口径(cwd 深度 3)', () => { it('三份本体都在,且不是空文件(改名/被清掉时别静默空跑)', () => { for (const aid of WORK_LAYER_AGENTS) { expect(bodyText(aid).length, `${aid}/agent.md`).toBeGreaterThan(500); } }); it('旧布局的 `../../workspace/…` 一处都不许剩', () => { const hits = WORK_LAYER_AGENTS.flatMap((aid) => bodyText(aid) .split('\n') .map((line, i) => ({ aid, i, line })) .filter((r) => LEGACY_WORKSPACE_RE.test(r.line)) .map((r) => `system/agents/${r.aid}/agent.md:${r.i + 1} ${r.line.trim()}`), ); expect(hits).toEqual([]); }); it('`bin/lemon.js` 每一次出现都带到仓库根的前缀(四段式流转全靠这条命令)', () => { const bad = []; for (const aid of WORK_LAYER_AGENTS) { bodyText(aid).split('\n').forEach((line, i) => { for (const m of line.matchAll(/(\S*)bin\/lemon\.js/g)) { if (m[1] !== TO_REPO_ROOT) bad.push(`system/agents/${aid}/agent.md:${i + 1} ${m[0]}`); } }); } expect(bad).toEqual([]); }); it('本体里引用的引擎段头与引擎那边逐字一致(漂了就是去取一个不存在的段)', () => { const headers = engineSectionHeaders(); // 三份本体都把「路径去任务描述里取」当成硬约束,所以至少各命中一个段头; // 命中了就必须逐字对得上——这里查的是「引用了但对不上」。 const drifted = []; for (const aid of WORK_LAYER_AGENTS) { const text = bodyText(aid); const cited = headers.filter((h) => text.includes(h)); if (cited.length === 0) drifted.push(`${aid}/agent.md 一个引擎段头都没引,产物落点无处可取`); } expect(drifted).toEqual([]); }); }); /** * 装配来源(第二步,也是「去掉修复就红」的那条)。 * * 这三份 agent.md 是 untracked 的运行期产物 —— 只改盘上文件,下一次 agent-creator * 被触发时照样按它看到的老样板重写一遍。唯一能持久的修法是把运行期布局写进**派单文本**, * 由 `buildAgentDefinitionBrief` 单点定义。 * * 曾经有第二处调用方(`POST /templates/:templateId/generate`,手工模板管理页的 * 「生成定义」按钮)——那条手工 UI 已随「system/templates/ 并入 system/agents/」一起下线 * (agent 本体现在完全走自动化招聘产生),路由已删除,`POST /:companyId/hire` 是唯一调用方。 */ describe('装配来源:agent-creator 的产出契约必须交代运行期布局', () => { const brief = buildAgentDefinitionBrief({ agentId: 'org-coder', bodyPath: '/abs/system/agents/org-coder', description: '实现节点', }); it('三个必填文件仍然逐字点名(本体填不满就停在 draft)', () => { for (const f of ['agent.md', 'soul.md', '.claude/skills/main_skill/SKILL.md']) { expect(brief).toContain(`/abs/system/agents/org-coder/${f}`); } }); it('派单里写明 cwd 是深度 3 的任务态目录,不是本体目录', () => { expect(brief).toContain('system/agents/org-coder/{公司ID}/{任务ID}/'); }); it('派单里给出本体 / 公司态 / 仓库根三档的相对深度', () => { // 本体是 `../../`(不是 28 §4.1 手写的 `../../../`,那一级把结尾斜杠数进去了) expect(brief).toMatch(/本体文件[^\n]*`\.\.\/\.\.\/`/); expect(brief).toContain(TO_REPO_ROOT); }); it('派单里点名禁止旧布局的 `../../workspace/…`(这正是三份本体跑偏的形态)', () => { expect(brief).toMatch(LEGACY_WORKSPACE_RE); expect(brief).toContain('禁止'); }); it('派单引用的是写入边界段头常量本身,不是又抄一份字面量', () => { expect(brief).toContain(GRAPH_WRITE_BOUNDARY_HEADER); }); it('唯一的派单点走这一份契约:路由里不再留内联清单', () => { const routes = fs.readFileSync(path.join(ROOT, 'server', 'routes', 'company.js'), 'utf-8'); expect(routes).not.toContain('(人格与判断准则)'); expect(routes.match(/buildAgentDefinitionBrief\(/g) || []).toHaveLength(1); }); });