Commit 0f22ddab132fdb466d29e6cf7ba22dcc410272e2
1 parent
54ee14eb
test(graph): 真实链路 smoke 进 CI;顺带修一段把人引向已修缺陷的过期注释
## 一、W7「零边图」不是待办 —— 它早修了,是注释在骗人 `company-service.js` 的判据注释仍写着「判据用**有没有边**」,并留着一段 「已知残留:单 org node 的最小图没有边,判据③不生效……真跑到那一步再看」。 **那个残留在 `58a2865`(08-17)就修掉了**:`orgGraphMembership` 现在的主判据是 `authored_by='planner'` 的节点存在(「orggraph-planning 提交过一张图」的直接证据, 零边也认),边数降为存量行的回落判据;守卫 `company-hire-org-layer.test.js` 的 `CO_ZEROEDGE` + `W7-fix` 两条盯着。 注释没跟着改,于是它把读的人**引向一个已经修好的缺陷**去重修一遍 —— 我就是这么被 带偏的:读到那段注释就没再往 `orgGraphMembership` 里跟,还据此往待办清单里写了一条。 这是「指路牌失效」那一族,和 CLAUDE.md 死链同型,只是方向相反(那边指向不存在的东西, 这边描述已不存在的洞)。 判据的措辞也改了:**「成型」的判据在 graph-service.orgGraphMembership(),不在这里**, 免得下一个人照注释理解。P3B 文档与 docs/README.md 的对应条目一并更正。 ## 二、还上「真实路径 smoke 进 CI」那笔欠账的一半(27-P3/P4 §十 硬规矩 2) 原话是「凡是靠注入点验的能力,都要有一条真实路径的 smoke 进 CI 才算验收完成 —— 这条欠账仍未还」。此前唯一走真实路径的 `scripts/real-pm-smoke.js` 要真 API 额度、 人工跑、而且验的是 DevLoop 不是图执行。 做法是把「真 LLM」和「真代码路径」拆开:**只把最外层的 CLI 子进程换成 fixture 回放**, setAgentExecutor / setPlanWorkGraph 一个都不调。链路完整走 advanceOne → defaultAgentExecutor → AgentManager → ClaudeCliProtocol → onAcpEvent → 落盘 → readNodeProduct → settleNode → spendBudget → 边界守卫。 三条断言各盯一处**假 executor 天然绕开**的接线: · ① mock/files 真的落到节点产物目录(假 executor 直接 return,这一环整个不存在) · ② token 用量真的从 session/result.usage 数进预算 —— **就是 §六 验收 10 卡了两轮的那一维** · ③ boundaryGuardApplicable 在这条路径上为真(注入假 executor 时它恒为假, 那条唯一有实际后果的分支实际零覆盖);带反向对照,注入后必须变假 反向验过:断掉①的落盘、断掉②的累加,对应用例分别变红。 为此改了一处 mock:session/result 的 usage 原来硬编码 {input:0,output:0}, 现在 fixture 可指定(缺省仍是 0,既有 fixture 行为一个字不变)。 **不改这一处,token 那一维在 CI 里永远产不出数** —— 验收 10 当初就是卡在这儿。 **没还清的部分写在文档里,别当成还清了**:真 LLM 仍进不了 CI; subgraph 那一支(defaultPlanWorkGraph)仍只有注入式覆盖; 多节点 / 回退边 / 熔断没进这条 smoke,它只推一个节点。 afterAll 清掉真实链路建出的公司态目录(gitignore 挡着不进库,但留着会让别的 用例遍历 system/agents/ 时多扫一份)。 全量:139 文件 / 1295 测试全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Showing
8 changed files
with
200 additions
and
19 deletions
docs/README.md
| ... | ... | @@ -41,8 +41,8 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d |
| 41 | 41 | | [27-naming-conventions.md](./todo/system-design/27-naming-conventions.md) | **命名规范**。`{作用域}-{职能}`;图节点按层级轴命名;节点名 ≠ 本体名;三种 benchmark 消歧;OKR 正名。新增 agent id / 表名 / 字段名前必读 | |
| 42 | 42 | | [27-e2e-task-execution-flow.md](./todo/system-design/27-e2e-task-execution-flow.md) | 端到端流程总览:建公司 → 下达任务 → org graph 逐节点执行 → work graph 规划与循环 → 验收收敛。含与 DevLoop 既有实现的对应关系分析 | |
| 43 | 43 | | [27-example-dev-company-walkthrough.md](./todo/system-design/27-example-dev-company-walkthrough.md) | 参考场景走查(虚构的「配图工厂」),把架构跑一遍具体的数:各阶段 JSON 形态、成本估算、断点清单 | |
| 44 | -| [27-p3-p4-work-graph-console.md](./todo/system-design/27-p3-p4-work-graph-console.md) | P3+P4(原 P4 回退+Loop-Validator 已并入)图执行引擎与执行控制台 — ✅ 已实现(`c3abf4a`)。**仍留 todo 的原因**:§十 末「四条硬规矩」第 2 条那笔欠账未还 —— 引擎用例一律注入假 executor / 假 planner,**没有一条真实路径的 smoke 进 CI** | | |
| 45 | -| [27-p3b-real-run-runway.md](./todo/system-design/27-p3b-real-run-runway.md) | P3 收尾之真跑跑道(从 mock 走到真 Claude CLI)— 📝 **仍在推进**。验收 7 ⛔ 真跑不成立(`readme-writer` 混进 org graph,未修);2/4/8/9 未在干净环境验。缺陷② 的根因(边界守卫误判并行会话)已于 2026-08-20 修掉,但**根因修了不等于验收点验了** | | |
| 44 | +| [27-p3-p4-work-graph-console.md](./todo/system-design/27-p3-p4-work-graph-console.md) | P3+P4(原 P4 回退+Loop-Validator 已并入)图执行引擎与执行控制台 — ✅ 已实现(`c3abf4a`)。**仍留 todo 的原因**:§十 末「四条硬规矩」第 2 条那笔欠账**只还了一半** —— `graph-real-chain-smoke.test.js`(零注入、只换掉真 CLI 子进程)已进 CI,覆盖 `agent_call` 那一支;`subgraph`(`defaultPlanWorkGraph`)仍只有注入式覆盖,真 LLM 也仍进不了 CI | | |
| 45 | +| [27-p3b-real-run-runway.md](./todo/system-design/27-p3b-real-run-runway.md) | P3 收尾之真跑跑道(从 mock 走到真 Claude CLI)— 📝 **仍在推进**。验收 7(work 层本体混进 org graph)✅ 已修;缺陷② 的根因(边界守卫误判并行会话)2026-08-20 修掉。**剩下的是 2/4/8/9 —— 都不是「没修」,是「没在干净环境重验」**。⚠️ 这份文档里 ⛔/🟡 的段落多为**当时的现场记录**,读之前先看头部状态行 | | |
| 46 | 46 | | [27-goal-contract-format.md](./todo/system-design/27-goal-contract-format.md) | 验收契约格式(`goal-benchmark` / `goal-test-data` / `goal-test-script` 的产物形态与跨节点传递) | |
| 47 | 47 | | [27-p4-parallel-and-artifacts.md](./todo/system-design/27-p4-parallel-and-artifacts.md) | **P4(回收编号)并行执行与产物索引** — 📝 设计中,未实现。fan-out 真并发 + 同 agent 多实例隔离 + `files.json` 产物索引。三个 Phase,C 可插队 | |
| 48 | 48 | | [28-followups.md](./todo/system-design/28-followups.md) | 28 号(Agent 注册表统一)的遗留清单 — 主文档已在 `done/`,这份装没做完的:dream 机制(只有口述、无设计)、任务态清理(源码树无限增长)、`source_kind`/`legacy_path` 删列、三项「已实施未验证」。§五 是**已立起来的守卫清单**(别重复造,也别删) | | ... | ... |
docs/todo/system-design/27-p3-p4-work-graph-console.md
| ... | ... | @@ -1040,7 +1040,28 @@ agent 默认**只挂载 `meta-research` 一个元技能**,必须主动呼叫 ` |
| 1040 | 1040 | |
| 1041 | 1041 | 1. **新增任何跨环节的字段/计数,四环各写一句话在 PR 描述里**(谁产生、经谁传递、落到哪张表/哪个文件、谁读回)。十三处里有九处只要问一句「谁产生它」就能当场发现;第 10、11、12 三处则是问「**谁读回它**」才发现的——**四环要问满,不能只问产生端**。而且「读回」不止是代码读,**还包括读给人/agent 看的那一层**:第 12 处的数据一直好好躺在 verdict JSON 里,只是 CLI 没打出来,消费它的 agent 就等于没有。 |
| 1042 | 1042 | **第 13 处又把这一条推进了一步**:产生端**是 agent 时,「谁产生它」的答案不是一段代码,而是一份提示词**——所以问完还要再问一句「**那份提示词现在到底怎么写的**」。第 13 处的引擎侧四环无懈可击,`describeArtifactContract` 也确实注入了写法,坏就坏在 agent 自己的 `agent.md` 里有一段**返工前写的、方向相反的**粒度说明,两份指令打架时 agent 照哪份写没人知道。 |
| 1043 | -2. **测试全绿 ≠ 能跑**。P3 的引擎用例(第六轮 R-17.1 返工后 19 条)至今仍一律注入假 executor / 假 planner,第 7、8、9、10 号缺陷和当时的「`planWorkGraph` 未接线」全部在覆盖范围之外——**夹具正好绕开了缺失的那一环**。第五轮补了一条不注入假实现的端到端(§六 验收 17)才把这个洞照出来,**但它是人工脚本、没进 CI**,随时会腐坏。**凡是靠注入点验的能力,都要有一条真实路径的 smoke 进 CI 才算验收完成**——这条欠账仍未还。 | |
| 1043 | +2. **测试全绿 ≠ 能跑**。P3 的引擎用例(第六轮 R-17.1 返工后 19 条)至今仍一律注入假 executor / 假 planner,第 7、8、9、10 号缺陷和当时的「`planWorkGraph` 未接线」全部在覆盖范围之外——**夹具正好绕开了缺失的那一环**。第五轮补了一条不注入假实现的端到端(§六 验收 17)才把这个洞照出来,**但它是人工脚本、没进 CI**,随时会腐坏。**凡是靠注入点验的能力,都要有一条真实路径的 smoke 进 CI 才算验收完成**。 | |
| 1044 | + | |
| 1045 | + > **2026-08-20:这条欠账已部分还上** —— `server/tests/unit/graph-real-chain-smoke.test.js` | |
| 1046 | + > 进了 `npm run test:unit`。做法是把「真 LLM」和「真代码路径」拆开:**只把最外层的 | |
| 1047 | + > Claude CLI 子进程换成 fixture 回放**(`LEMONBOSS_CLAUDE_MODE=mock`), | |
| 1048 | + > `setAgentExecutor` / `setPlanWorkGraph` 一个都不调,链路完整走 | |
| 1049 | + > `advanceOne → defaultAgentExecutor → AgentManager → ClaudeCliProtocol → onAcpEvent | |
| 1050 | + > → 落盘 → readNodeProduct → settleNode → spendBudget → 边界守卫`。 | |
| 1051 | + > | |
| 1052 | + > 三条断言各盯一处「假 executor 天然绕开」的接线:① `mock/files` 真的落到节点产物目录; | |
| 1053 | + > ② token 用量真的从 `session/result.usage` 数进预算(**就是本文档 §六 验收 10 卡了两轮的那一维**); | |
| 1054 | + > ③ `boundaryGuardApplicable` 在这条路径上为真(注入假 executor 时它恒为假,那条分支实际零覆盖)。 | |
| 1055 | + > 反向验过:断掉①的落盘、断掉②的累加,对应用例分别变红。 | |
| 1056 | + > | |
| 1057 | + > 为此改了一处 mock:`session/result.usage` 原来硬编码 `{input:0,output:0}`, | |
| 1058 | + > 现在 fixture 可以指定(缺省仍是 0,既有 fixture 行为不变)。**不改这一处, | |
| 1059 | + > token 那一维在 CI 里永远产不出数** —— §六 验收 10 当初就是卡在这儿。 | |
| 1060 | + > | |
| 1061 | + > **还没还的部分,别当成还清了**:(a) 真 LLM 仍然进不了 CI(要额度、分钟级、输出不确定), | |
| 1062 | + > `scripts/real-pm-smoke.js` 仍是人工跑;(b) 这条 smoke 走的是 `agent_call`, | |
| 1063 | + > **`subgraph` 那一支(`defaultPlanWorkGraph`)仍然只有注入式覆盖**; | |
| 1064 | + > (c) 多节点、回退边、熔断这些形状没进这条 smoke,它只推一个节点。 | |
| 1044 | 1065 | 3. **回填对账的段落必须在本轮代码定稿之后重跑一遍核实**。**这条被连续犯了三轮**:第四轮代码把 `total_tokens` 接通了、同一轮回填的 §四.A.3 却还写着「死维度」(§八.11 / §六 验收 10 / §五 T8 三处一起错);第五轮真接了四处生产链路,文档头部的「第五轮修订」却只列改名与纠偏、四处接线一个字没提,于是 §五 T8/T11、§六 验收 1/12/17、§八.10/12/14、§十 表全部停在第四轮结论上(**第六轮清出至少 12 处过期**)。**「本轮开工时 grep 的结论」不等于「本轮交付时的事实」;改了行为就在同一轮把对应小节改对,别留给下一轮。** |
| 1045 | 1066 | **第六轮的形态换了个方向,同样要记**:这一轮的过期不再是「代码改了、文档停在上一轮」,而是**「文档提前写成了完成态、代码只做了一半」**——§五 T11 写着「六个本体已建齐**并注册**」,实际只建了目录;§六 验收 10 / §五 T8 写着「实测 `spent.tokens` 递增且能单独触发 `exhausted`」,而 mock 协议把 `usage` 硬编码成 0,那个数用这套终验手法根本产不出来。**「我打算这样做」和「我做完了」在文档里长得一模一样,所以回填时要逐条问「这句话我是怎么验的」,验不出来的就不要写成实测。** |
| 1046 | 1067 | ... | ... |
docs/todo/system-design/27-p3b-real-run-runway.md
| ... | ... | @@ -6,7 +6,8 @@ |
| 6 | 6 | > **起因**: [27-REQ](./27-requirement.md) R-18(招聘归 `workgraph-planning`;停用 mock 改真 CLI) |
| 7 | 7 | > **前置**: P3 主体已实现并提交(`c3abf4a` 起七轮),测试 719/719 全绿 |
| 8 | 8 | > **状态**(2026-08-20 校准): 📝 **仍在推进**。第一批验收 1/3 已验、2 的后半与 4 未在干净环境重验; |
| 9 | -> 第二批验收 7 ⛔ 真跑不成立(§六bis 缺陷③ `readme-writer` 混进 org graph,**未修**); | |
| 9 | +> 第二批验收 7 ✅ **已修**(`58a2865`「W7 零边图」,守卫 `company-hire-org-layer.test.js` 的 | |
| 10 | +> `CO_ZEROEDGE` + `W7-fix`)—— 下面 §六bis 缺陷③ 那段是**当时的现场记录**,不是当前状态; | |
| 10 | 11 | > 第三批 8/9 真 CLI 未验。 |
| 11 | 12 | > §六bis 缺陷② 的**根因**(边界守卫误判并行会话的改动)已于 2026-08-20 修掉, |
| 12 | 13 | > 见 [27-P3C §17.2](../../done/system-design/27-p3c-goal-task-model.md) —— 但**根因修了不等于验收点验了**,那几条仍要重跑一遍。 |
| ... | ... | @@ -776,7 +777,12 @@ agent 读的是 `agent.md`,那一侧不改,模型就会继续以为「前三 |
| 776 | 777 | 5. 新建公司 → 界面点「重新规划组织」→ `orggraph-planning` 真跑 → org graph 出现,且它雇的三个 org 节点都在。 |
| 777 | 778 | 6. 下达任务 → `workgraph-planning` 真跑 → 它引用的本体被自己雇进公司 → 图提交成功(不再外键挂)。 |
| 778 | 779 | 7. 上条跑完后,控制台左栏 org graph **仍然只有 org 节点**,没有混进 `prd-planning` 这类 work 层本体。 |
| 779 | - ⛔ **真跑不成立**(2026-08-17 第三次真 CLI 终验,见 §六bis 缺陷③):三条判据都盖不住 | |
| 780 | + ✅ **已修**(`58a2865`「W7 零边图」,2026-08-17 当天)。判据加了主判据 | |
| 781 | + `authored_by='planner'` 的节点存在 —— 「orggraph-planning 提交过一张图」的直接证据, | |
| 782 | + 零边也认;边数降为存量行的回落判据。守卫 `company-hire-org-layer.test.js` 的 | |
| 783 | + `CO_ZEROEDGE`(零边图上 work 层本体不入图)+ `W7-fix`(零边图上图里点名的 org 节点 | |
| 784 | + 重复雇仍入图,P2 语义不被误伤)。 | |
| 785 | + **以下是修之前的现场记录,留档不是当前状态**:三条判据都盖不住 | |
| 780 | 786 | 「零条边的最小 org 图 + 一个既非 `meta-*`、又没在 work graph 骨架里点过名的本体」, |
| 781 | 787 | `readme-writer` 被真的写进了 `org_graph_nodes`,`GET /org-graph` 返回两个节点, |
| 782 | 788 | 下一次任务的左栏并排显示两张 org 卡片。 | ... | ... |
fixtures/devloop-mock/prd-planning.json
| 1 | 1 | { |
| 2 | - "_doc": "P3 mock fixture · work graph 里的 wg-plan 节点本体。files 经 mock/files 事件落到该节点的产物目录(graph-engine.defaultAgentExecutor 接收),attempt-{attempt}.json 是引擎 readNodeProduct 要读的那份。verdict=no_prior_failure 表示「本轮没有前序失败要归因」,节点判 success。", | |
| 2 | + "_doc": "P3 mock fixture · work graph 里的 wg-plan 节点本体。files 经 mock/files 事件落到该节点的产物目录(graph-engine.defaultAgentExecutor 接收),attempt-{attempt}.json 是引擎 readNodeProduct 要读的那份。verdict=no_prior_failure 表示「本轮没有前序失败要归因」,节点判 success。|usage 是给 graph-real-chain-smoke 用的:让「token 用量」这一维在 mock 下也产得出来(缺省 0 时那条链只有真 CLI 才验得了)。", | |
| 3 | 3 | "chunks": [ |
| 4 | 4 | "[mock] prd-planning 读取 task-goal 与共享产物。\n", |
| 5 | 5 | "输出需求要点 3 条,写 prd.md。\n" |
| ... | ... | @@ -14,5 +14,9 @@ |
| 14 | 14 | "path": "prd.md", |
| 15 | 15 | "content": "# PRD(mock)\n\n- 要点 1:提供一个可运行的最小接口\n- 要点 2:错误输入要有明确响应\n- 要点 3:验收以测试脚本为准\n" |
| 16 | 16 | } |
| 17 | - ] | |
| 17 | + ], | |
| 18 | + "usage": { | |
| 19 | + "input": 1200, | |
| 20 | + "output": 340 | |
| 21 | + } | |
| 18 | 22 | } | ... | ... |
packages/acp-core/protocol/ClaudeCliProtocol.js
| ... | ... | @@ -632,7 +632,11 @@ class ClaudeCliProtocol extends EventEmitter { |
| 632 | 632 | cost_usd: 0, |
| 633 | 633 | session_id: this.sessionId, |
| 634 | 634 | stop_reason: 'end_turn', |
| 635 | - usage: { input: 0, output: 0 }, | |
| 635 | + // usage 允许 fixture 指定。**这一维原来在 mock 下恒为 0**,于是 | |
| 636 | + // 「谁来数 tokens」那条链(ClaudeCliProtocol → onAcpEvent → spendBudget) | |
| 637 | + // 只有真起 CLI 才验得了 —— 27-P3/P4 §六 验收 10 就卡在这儿。 | |
| 638 | + // 缺省仍是 0,既有 fixture 行为一个字不变。 | |
| 639 | + usage: fixture.usage || { input: 0, output: 0 }, | |
| 636 | 640 | model: 'mock', |
| 637 | 641 | mock: true, |
| 638 | 642 | files_produced: Array.isArray(fixture.files) ? fixture.files.length : 0, | ... | ... |
server/services/company-service.js
| ... | ... | @@ -119,22 +119,30 @@ function buildAgentDefinitionBrief({ agentId, bodyPath, description, intro }) { |
| 119 | 119 | // (planner-validator / validator-triager / benchmark-validator / testdata-validator, |
| 120 | 120 | // 这些 type='validator' 的本体判据①盖不住,而 org-validator 只当 ownerOrgNode 出现、 |
| 121 | 121 | // 不是 nodes[] 里的 systemAgentId,所以不会被误伤)。 |
| 122 | -// ③ 公司已经有一张**成型的**组织工作流图(存在 active 的 org_graph_edges), | |
| 123 | -// 而这次雇的角色不在图上 → 不入图:图一旦成型,作者就是 orggraph-planning | |
| 124 | -// (POST /org-graph),hire() 不再往上追加节点。这条兜住 ①② 都不认识的、 | |
| 125 | -// 按 executor.json 的 {{implAgent}} / {{validatorAgent}} 动态选出来的本体。 | |
| 126 | -// 判据用「有没有边」而不是「有没有节点」,因为 P1/P2 的 hire 自己就会造节点, | |
| 127 | -// 拿节点当信号会把「同一家公司连雇两个人,第二个不入图」这条 P2 语义废掉 | |
| 128 | -// (server/tests/integration/graph-agent-p1-hire.test.js 就是这么用的)。 | |
| 122 | +// ③ 公司已经有一张**成型的**组织工作流图,而这次雇的角色不在图上 → 不入图: | |
| 123 | +// 图一旦成型,作者就是 orggraph-planning(POST /org-graph),hire() 不再往上追加节点。 | |
| 124 | +// 这条兜住 ①② 都不认识的、按 executor.json 的 {{implAgent}} / {{validatorAgent}} | |
| 125 | +// 动态选出来的本体。 | |
| 126 | +// | |
| 127 | +// **「成型」的判据在 graph-service.orgGraphMembership(),不在这里** —— 改判据去那儿改, | |
| 128 | +// 别照这段注释理解。它现在是两条: | |
| 129 | +// · 主判据 `authored_by='planner'` 的节点存在(`writeGraph` 是唯一写它的地方) | |
| 130 | +// —— 这是「orggraph-planning 提交过一张图」的**直接**证据; | |
| 131 | +// · 回落判据 active 的 `org_graph_edges` 存在 —— 只为该列之前的存量行(authored_by 为 NULL)。 | |
| 132 | +// | |
| 133 | +// ⚠️ **回落判据曾经是唯一判据,那是个真的洞**(P3B W7):单 org node 的最小图零条边, | |
| 134 | +// 三条判据全落空 → `readme-writer` 这类占位符现填的本体被真的写进 `org_graph_nodes`, | |
| 135 | +// 控制台左栏并排显示两张 org 卡片。修在 `58a2865`,守卫 | |
| 136 | +// `company-hire-org-layer.test.js` 的 `CO_ZEROEDGE` + `W7-fix` 两条。 | |
| 137 | +// **别拿节点数当信号**:P1/P2 的 hire 自己就会造节点,那会把「同一家公司连雇两个人, | |
| 138 | +// 第二个不入图」这条 P2 语义废掉(`graph-agent-p1-hire.test.js` 就是这么用的)—— | |
| 139 | +// `authored_by` 能分开「planner 画的」和「hire 顺手记的」,节点数不能。 | |
| 140 | +// | |
| 129 | 141 | // 顺带修掉一个老问题:org 节点 role_name='org-coder' 绑定本体 'coder' 时, |
| 130 | 142 | // 在成型图上再雇一次 'coder' 以前会多出一行同义节点,现在不会了。 |
| 131 | 143 | // |
| 132 | 144 | // 默认行为(都不命中)= 写。P1/P2 既有语义「雇一个角色 → org graph 出现这个节点」 |
| 133 | 145 | // 在「公司还没有成型的图」这个前提下原样保住(graph-agent-p1-acceptance.test.js 判据 1)。 |
| 134 | -// | |
| 135 | -// 已知残留:单 org node 的最小图没有边,判据③不生效,这时只有 ①② 认识的本体会被拦下; | |
| 136 | -// 一个既不是 meta-* 又没在骨架里点过名的本体(executor.json 的占位符现填)仍会入图。 | |
| 137 | -// 真跑到那一步再看,别为它提前加规则。 | |
| 138 | 146 | const NON_ORG_AGENT_TYPES = new Set(['meta-bootstrap', 'meta-planning']); |
| 139 | 147 | const WORK_GRAPH_TEMPLATES_DIR = path.resolve(__dirname, '../../system/work-graph-templates'); |
| 140 | 148 | ... | ... |
server/services/graph-engine.js
| ... | ... | @@ -3935,6 +3935,9 @@ module.exports = { |
| 3935 | 3935 | // executor 与注入点 |
| 3936 | 3936 | executeSubgraph, |
| 3937 | 3937 | defaultAgentExecutor, |
| 3938 | + // 导出只为让真实链路 smoke 能直接问一句「守卫在这条路径上开着没有」 | |
| 3939 | + // (graph-real-chain-smoke.test.js ③)。生产里它只被 advanceOne 调。 | |
| 3940 | + boundaryGuardApplicable, | |
| 3938 | 3941 | defaultPlanWorkGraph, |
| 3939 | 3942 | setAgentExecutor, |
| 3940 | 3943 | setPlanWorkGraph, | ... | ... |
server/tests/unit/graph-real-chain-smoke.test.js
0 → 100644
| 1 | +/** | |
| 2 | + * 图执行的**真实链路 smoke** —— 全程零注入,唯一被替换的是最外层的 Claude CLI 子进程。 | |
| 3 | + * | |
| 4 | + * ── 这条测试还的是哪笔债 ────────────────────────────────────────────── | |
| 5 | + * [27-P3/P4](../../../docs/todo/system-design/27-p3-p4-work-graph-console.md) §十 末 | |
| 6 | + * 「四条硬规矩」第 2 条: | |
| 7 | + * | |
| 8 | + * > **测试全绿 ≠ 能跑**。P3 的引擎用例至今仍一律注入假 executor / 假 planner, | |
| 9 | + * > 第 7、8、9、10 号缺陷和当时的「planWorkGraph 未接线」全部在覆盖范围之外—— | |
| 10 | + * > **夹具正好绕开了缺失的那一环**。……**凡是靠注入点验的能力,都要有一条真实路径的 | |
| 11 | + * > smoke 进 CI 才算验收完成**——这条欠账仍未还。 | |
| 12 | + * | |
| 13 | + * 在这之前,唯一走真实路径的是 `scripts/real-pm-smoke.js`(人工跑、要真 API 额度、 | |
| 14 | + * 而且验的是 DevLoop 不是图执行),**没进 CI,随时会腐坏**。 | |
| 15 | + * | |
| 16 | + * ── 「零注入」的边界画在哪 ──────────────────────────────────────────── | |
| 17 | + * 不调 `setAgentExecutor` / `setPlanWorkGraph`。链路完整走: | |
| 18 | + * advanceOne → defaultAgentExecutor → AgentManager → ClaudeCliProtocol | |
| 19 | + * → onAcpEvent(mock/files, session/result) → 落盘 → readNodeProduct | |
| 20 | + * → settleNode → spendBudget → 边界守卫 | |
| 21 | + * 只有最后那层真子进程被 `LEMONBOSS_CLAUDE_MODE=mock` 换成 fixture 回放 —— | |
| 22 | + * 真 LLM 进不了 CI(要额度、要分钟级时间、输出不确定),但**「真 LLM」和「真代码路径」 | |
| 23 | + * 是两件事**,这里换掉前者、保住后者。 | |
| 24 | + * | |
| 25 | + * ── 三条断言各盯一处「假 executor 天然绕开」的接线 ──────────────────── | |
| 26 | + * ① 产物落盘:`mock/files` → `defaultAgentExecutor` 的 onAcpEvent → 节点产物目录。 | |
| 27 | + * 假 executor 直接返回 `{status:'success'}`,这一环整个不存在。 | |
| 28 | + * ② token 计数:`session/result.usage` → onAcpEvent 累加 → `spendBudget`。 | |
| 29 | + * 这是 §六 验收 10 卡了两轮的那一维(「三个维度写在一起,只有它没有产出方」)。 | |
| 30 | + * ③ 边界守卫真的开了:`boundaryGuardApplicable` 只在 `agentExecutor === defaultAgentExecutor` | |
| 31 | + * 时返回 true —— 注入假 executor 的用例里它**恒为 false**,那条分支实际零覆盖。 | |
| 32 | + * | |
| 33 | + * ── 隔离 ────────────────────────────────────────────────────────────── | |
| 34 | + * GRAPH_DB_PATH / GRAPH_RUNS_DIR 都指临时目录,且必须在 require 之前设 | |
| 35 | + * (两个模块都在顶部快照)。`LEMONBOSS_CLAUDE_MODE` 同理:ClaudeCliProtocol 的 | |
| 36 | + * MOCK_MODE 是模块级 const。 | |
| 37 | + */ | |
| 38 | +process.env.LEMONBOSS_CLAUDE_MODE = 'mock'; | |
| 39 | + | |
| 40 | +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; | |
| 41 | +import fs from 'fs'; | |
| 42 | +import os from 'os'; | |
| 43 | +import path from 'path'; | |
| 44 | + | |
| 45 | +const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'lemonboss-real-chain-')); | |
| 46 | +process.env.GRAPH_DB_PATH = path.join(TMP, 'graph.sqlite'); | |
| 47 | +process.env.GRAPH_RUNS_DIR = path.join(TMP, 'runs'); | |
| 48 | + | |
| 49 | +// fixture 名 = 本体 id(图执行侧的 agent 名带公司前缀,mock 会先剥掉)。 | |
| 50 | +// 用 prd-planning:本体真实存在,fixture 自带 files[](含 readNodeProduct 要读的 | |
| 51 | +// attempt-{attempt}.json)与 usage。 | |
| 52 | +const AGENT = 'prd-planning'; | |
| 53 | +const COMPANY = 'zz-real-chain-co'; | |
| 54 | + | |
| 55 | +let wdb; let engine; | |
| 56 | + | |
| 57 | +describe('图执行真实链路 smoke(零注入,只换掉真 CLI 子进程)', () => { | |
| 58 | + let runId; let nodeRunId; let rootRunId; | |
| 59 | + | |
| 60 | + beforeAll(async () => { | |
| 61 | + for (const m of ['../../services/graph-db', '../../services/work-graph-db', | |
| 62 | + '../../services/graph-run-sse', '../../services/graph-engine']) { | |
| 63 | + delete require.cache[require.resolve(m)]; | |
| 64 | + } | |
| 65 | + wdb = require('../../services/work-graph-db'); | |
| 66 | + engine = require('../../services/graph-engine'); | |
| 67 | + | |
| 68 | + const db = wdb.getDb(); | |
| 69 | + const now = new Date().toISOString(); | |
| 70 | + db.prepare(`INSERT OR IGNORE INTO system_agents | |
| 71 | + (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at) | |
| 72 | + VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)`) | |
| 73 | + .run(AGENT, AGENT, `system/agents/${AGENT}`, now, now); | |
| 74 | + | |
| 75 | + const g = wdb.createWorkGraph({ companyId: COMPANY, nodes: [{ nodeName: AGENT, systemAgentId: AGENT }], edges: [] }); | |
| 76 | + expect(g.errors || []).toEqual([]); | |
| 77 | + const run = wdb.createRun({ | |
| 78 | + graphKind: 'work', graphId: g.id, companyId: COMPANY, executionMode: 'manual', | |
| 79 | + // 额度给足:这条 smoke 要验的是「有没有数」,不是「够不够花」 | |
| 80 | + budget: { total_agent_calls: 10, total_tokens: 5_000_000, spent: { agent_calls: 0, tokens: 0 } }, | |
| 81 | + }); | |
| 82 | + engine.markRunReady(run.id); | |
| 83 | + runId = run.id; | |
| 84 | + // createRun 的返回值不一定带 root_run_id,从库里读回来(这条 run 自己就是 root) | |
| 85 | + rootRunId = wdb.getRun(run.id).root_run_id; | |
| 86 | + expect(rootRunId).toBe(run.id); | |
| 87 | + | |
| 88 | + // ready → running(「下一步」只对 running/paused 可用) | |
| 89 | + engine.startRun(runId, { mode: 'manual' }); | |
| 90 | + | |
| 91 | + const out = await engine.nextStep(runId, { task: '真实链路 smoke:产出一份 PRD' }); | |
| 92 | + if (!out.ok) throw new Error(`nextStep 没推动:${JSON.stringify(out)}`); | |
| 93 | + nodeRunId = out.node.id; | |
| 94 | + // 节点必须真的跑完 —— 停在 running / fail 的话下面三条断言验的是残局 | |
| 95 | + expect(out.node.status).toBe('success'); | |
| 96 | + }, 60_000); | |
| 97 | + | |
| 98 | + afterAll(() => { | |
| 99 | + // 真实链路会在本体下建公司态目录(AgentManager 的 cwd 是 system/agents/{aid}/{cid}/{taskId})。 | |
| 100 | + // 它 gitignore 挡着、不进版本库,但留着会让别的用例遍历 system/agents/ 时多扫一份, | |
| 101 | + // 也让 `ls system/agents/prd-planning/` 出现一个没人认领的公司。 | |
| 102 | + fs.rmSync(path.join(__dirname, '../../../system/agents', AGENT, COMPANY), { recursive: true, force: true }); | |
| 103 | + delete process.env.LEMONBOSS_CLAUDE_MODE; | |
| 104 | + delete process.env.GRAPH_DB_PATH; | |
| 105 | + delete process.env.GRAPH_RUNS_DIR; | |
| 106 | + fs.rmSync(TMP, { recursive: true, force: true }); | |
| 107 | + }); | |
| 108 | + | |
| 109 | + it('① mock/files 真的落到了节点产物目录(假 executor 下这一环整个不存在)', () => { | |
| 110 | + const dir = path.join(process.env.GRAPH_RUNS_DIR, rootRunId, 'nodes', nodeRunId); | |
| 111 | + expect(fs.existsSync(path.join(dir, 'attempt-0.json'))).toBe(true); | |
| 112 | + expect(fs.readFileSync(path.join(dir, 'prd.md'), 'utf8')).toContain('PRD'); | |
| 113 | + // attempt-0.json 是引擎 readNodeProduct 要读的那份 —— 它读不到就判不出 verdict, | |
| 114 | + // 所以这条同时也证明了 `{attempt}` 占位符替换那一步没坏 | |
| 115 | + const product = JSON.parse(fs.readFileSync(path.join(dir, 'attempt-0.json'), 'utf8')); | |
| 116 | + expect(product.verdict).toBe('no_prior_failure'); | |
| 117 | + }); | |
| 118 | + | |
| 119 | + it('② token 用量真的从 session/result.usage 数进了预算(§六 验收 10 那一维)', () => { | |
| 120 | + const spent = JSON.parse(wdb.getRun(rootRunId).budget_json).spent; | |
| 121 | + expect(spent.agent_calls).toBe(1); | |
| 122 | + // fixture 报 1200 + 340;断言等值而不是 >0 —— 「大于 0」在累加写成 `=` 而非 `+=` 时也成立 | |
| 123 | + expect(spent.tokens).toBe(1540); | |
| 124 | + }); | |
| 125 | + | |
| 126 | + it('③ 边界守卫在这条路径上是**开着**的(注入假 executor 时它恒为假,那条分支零覆盖)', () => { | |
| 127 | + // 判据取自引擎自己的开关函数,而不是去数 timeline 事件: | |
| 128 | + // 守卫在宿主仓库干净时本来就不产生事件,拿「没有事件」当「没开」会反过来判错。 | |
| 129 | + expect(engine.boundaryGuardApplicable({ nodeKind: 'agent_call' })).toBe(true); | |
| 130 | + // 对照:注入之后必须变假 —— 少了这条,上面那句在函数写成 `return true` 时也全绿 | |
| 131 | + engine.setAgentExecutor(async () => ({ status: 'success' })); | |
| 132 | + expect(engine.boundaryGuardApplicable({ nodeKind: 'agent_call' })).toBe(false); | |
| 133 | + engine.setAgentExecutor(null); | |
| 134 | + }); | |
| 135 | +}); | ... | ... |