work-layer-body-paths.test.js
7.34 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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
/**
* 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);
});
});