Commit dea683eb6919a22edffc926d545097b70b360eae

Authored by yibo
1 parent 0f22ddab

test(graph): subgraph 那一支也进真实链路 smoke

agent_call 与 subgraph 是**两段不同的默认实现**(defaultAgentExecutor /
defaultPlanWorkGraph),各有各的 onAcpEvent 累加、各有各的守卫开关分支。
上一轮只覆盖了前者,「规划子图」那半条链仍然是零真实覆盖。

现在走完:defaultPlanWorkGraph → AgentManager → ClaudeCliProtocol(mock)
→ extractPlannedGraph 解析回复 → createWorkGraphSafely → 建子 run。

断言两处假 planner 天然绕开的接线:
· 子 run 的定义确实来自 fixture 那张图(节点名 / systemAgentId 逐个对)
· 规划那一轮烧的 token 也进了预算 —— 走的是 defaultPlanWorkGraph 自己那条
  onAcpEvent,与 agent_call 那条不是同一段代码
③ 的守卫断言补上 subgraph 分支的正反对照。

新增 fixture workgraph-planning.json:图 JSON 同时放进 chunks 与 result
(mock 下 output 取哪一半由 AgentManager 决定,不该由 fixture 来赌)。

反向验过:抽掉 fixture 里的图 JSON、或把它的 usage 归零,对应用例分别变红。

**覆盖边界写在用例注释里**:跳过 resolvePlanningAgentName 里的按需 hire
(预塞一行 company_agents)—— hire 要一条真实公司记录,那条路径另有
company-hire-org-layer.test.js 盯着;这条 smoke 验的是规划链路,不是招聘。

全量:139 文件 / 1296 测试全绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs/README.md
@@ -41,7 +41,7 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d @@ -41,7 +41,7 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d
41 | [27-naming-conventions.md](./todo/system-design/27-naming-conventions.md) | **命名规范**。`{作用域}-{职能}`;图节点按层级轴命名;节点名 ≠ 本体名;三种 benchmark 消歧;OKR 正名。新增 agent id / 表名 / 字段名前必读 | 41 | [27-naming-conventions.md](./todo/system-design/27-naming-conventions.md) | **命名规范**。`{作用域}-{职能}`;图节点按层级轴命名;节点名 ≠ 本体名;三种 benchmark 消歧;OKR 正名。新增 agent id / 表名 / 字段名前必读 |
42 | [27-e2e-task-execution-flow.md](./todo/system-design/27-e2e-task-execution-flow.md) | 端到端流程总览:建公司 → 下达任务 → org graph 逐节点执行 → work graph 规划与循环 → 验收收敛。含与 DevLoop 既有实现的对应关系分析 | 42 | [27-e2e-task-execution-flow.md](./todo/system-design/27-e2e-task-execution-flow.md) | 端到端流程总览:建公司 → 下达任务 → org graph 逐节点执行 → work graph 规划与循环 → 验收收敛。含与 DevLoop 既有实现的对应关系分析 |
43 | [27-example-dev-company-walkthrough.md](./todo/system-design/27-example-dev-company-walkthrough.md) | 参考场景走查(虚构的「配图工厂」),把架构跑一遍具体的数:各阶段 JSON 形态、成本估算、断点清单 | 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 条那笔欠账**只还了一半** —— `graph-real-chain-smoke.test.js`(零注入、只换掉真 CLI 子进程)已进 CI,覆盖 `agent_call` 那一支;`subgraph`(`defaultPlanWorkGraph`)仍只有注入式覆盖,真 LLM 也仍进不了 CI | 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` 两支都覆盖。剩下的是真 LLM(进不了 CI)、多节点 / 回退边 / 熔断的形状、以及按需 hire 那一段 |
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 —— 都不是「没修」,是「没在干净环境重验」**。⚠️ 这份文档里 ⛔/🟡 的段落多为**当时的现场记录**,读之前先看头部状态行 | 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 | [27-goal-contract-format.md](./todo/system-design/27-goal-contract-format.md) | 验收契约格式(`goal-benchmark` / `goal-test-data` / `goal-test-script` 的产物形态与跨节点传递) | 46 | [27-goal-contract-format.md](./todo/system-design/27-goal-contract-format.md) | 验收契约格式(`goal-benchmark` / `goal-test-data` / `goal-test-script` 的产物形态与跨节点传递) |
47 | [27-p4-parallel-and-artifacts.md](./todo/system-design/27-p4-parallel-and-artifacts.md) | **P4(回收编号)并行执行与产物索引** — 📝 设计中,未实现。fan-out 真并发 + 同 agent 多实例隔离 + `files.json` 产物索引。三个 Phase,C 可插队 | 47 | [27-p4-parallel-and-artifacts.md](./todo/system-design/27-p4-parallel-and-artifacts.md) | **P4(回收编号)并行执行与产物索引** — 📝 设计中,未实现。fan-out 真并发 + 同 agent 多实例隔离 + `files.json` 产物索引。三个 Phase,C 可插队 |
docs/todo/system-design/27-p3-p4-work-graph-console.md
@@ -1058,10 +1058,19 @@ agent 默认**只挂载 `meta-research` 一个元技能**,必须主动呼叫 ` @@ -1058,10 +1058,19 @@ agent 默认**只挂载 `meta-research` 一个元技能**,必须主动呼叫 `
1058 > 现在 fixture 可以指定(缺省仍是 0,既有 fixture 行为不变)。**不改这一处, 1058 > 现在 fixture 可以指定(缺省仍是 0,既有 fixture 行为不变)。**不改这一处,
1059 > token 那一维在 CI 里永远产不出数** —— §六 验收 10 当初就是卡在这儿。 1059 > token 那一维在 CI 里永远产不出数** —— §六 验收 10 当初就是卡在这儿。
1060 > 1060 >
  1061 + > **同日补上 `subgraph` 那一支**(④⑤):`defaultPlanWorkGraph` 是**另一段**默认实现,
  1062 + > 有自己的 onAcpEvent 累加和自己的守卫开关分支,只覆盖 `agent_call` 的话「规划子图」
  1063 + > 那半条链仍是零真实覆盖。现在走完 `defaultPlanWorkGraph → AgentManager → 回复解析
  1064 + > (extractPlannedGraph) → createWorkGraphSafely → 建子 run`,断言子 run 的定义确实来自
  1065 + > fixture 那张图、且规划那一轮的 token 也进了预算。反向验过:抽掉 fixture 里的图 JSON、
  1066 + > 或把它的 usage 归零,对应用例分别变红。
  1067 + > 覆盖边界写在用例注释里:**跳过 `resolvePlanningAgentName` 里的按需 hire**
  1068 + > (预塞一行 company_agents),因为 hire 要一条真实公司记录,而那条路径另有
  1069 + > `company-hire-org-layer.test.js` 盯着。
  1070 + >
1061 > **还没还的部分,别当成还清了**:(a) 真 LLM 仍然进不了 CI(要额度、分钟级、输出不确定), 1071 > **还没还的部分,别当成还清了**:(a) 真 LLM 仍然进不了 CI(要额度、分钟级、输出不确定),
1062 - > `scripts/real-pm-smoke.js` 仍是人工跑;(b) 这条 smoke 走的是 `agent_call`,  
1063 - > **`subgraph` 那一支(`defaultPlanWorkGraph`)仍然只有注入式覆盖**;  
1064 - > (c) 多节点、回退边、熔断这些形状没进这条 smoke,它只推一个节点。 1072 + > `scripts/real-pm-smoke.js` 仍是人工跑;(b) 多节点、回退边、熔断这些形状没进这条 smoke,
  1073 + > 两支各只推一个节点;(c) 按需 hire 那一段没在这条链路里走过。
1065 3. **回填对账的段落必须在本轮代码定稿之后重跑一遍核实**。**这条被连续犯了三轮**:第四轮代码把 `total_tokens` 接通了、同一轮回填的 §四.A.3 却还写着「死维度」(§八.11 / §六 验收 10 / §五 T8 三处一起错);第五轮真接了四处生产链路,文档头部的「第五轮修订」却只列改名与纠偏、四处接线一个字没提,于是 §五 T8/T11、§六 验收 1/12/17、§八.10/12/14、§十 表全部停在第四轮结论上(**第六轮清出至少 12 处过期**)。**「本轮开工时 grep 的结论」不等于「本轮交付时的事实」;改了行为就在同一轮把对应小节改对,别留给下一轮。** 1074 3. **回填对账的段落必须在本轮代码定稿之后重跑一遍核实**。**这条被连续犯了三轮**:第四轮代码把 `total_tokens` 接通了、同一轮回填的 §四.A.3 却还写着「死维度」(§八.11 / §六 验收 10 / §五 T8 三处一起错);第五轮真接了四处生产链路,文档头部的「第五轮修订」却只列改名与纠偏、四处接线一个字没提,于是 §五 T8/T11、§六 验收 1/12/17、§八.10/12/14、§十 表全部停在第四轮结论上(**第六轮清出至少 12 处过期**)。**「本轮开工时 grep 的结论」不等于「本轮交付时的事实」;改了行为就在同一轮把对应小节改对,别留给下一轮。**
1066 **第六轮的形态换了个方向,同样要记**:这一轮的过期不再是「代码改了、文档停在上一轮」,而是**「文档提前写成了完成态、代码只做了一半」**——§五 T11 写着「六个本体已建齐**并注册**」,实际只建了目录;§六 验收 10 / §五 T8 写着「实测 `spent.tokens` 递增且能单独触发 `exhausted`」,而 mock 协议把 `usage` 硬编码成 0,那个数用这套终验手法根本产不出来。**「我打算这样做」和「我做完了」在文档里长得一模一样,所以回填时要逐条问「这句话我是怎么验的」,验不出来的就不要写成实测。** 1075 **第六轮的形态换了个方向,同样要记**:这一轮的过期不再是「代码改了、文档停在上一轮」,而是**「文档提前写成了完成态、代码只做了一半」**——§五 T11 写着「六个本体已建齐**并注册**」,实际只建了目录;§六 验收 10 / §五 T8 写着「实测 `spent.tokens` 递增且能单独触发 `exhausted`」,而 mock 协议把 `usage` 硬编码成 0,那个数用这套终验手法根本产不出来。**「我打算这样做」和「我做完了」在文档里长得一模一样,所以回填时要逐条问「这句话我是怎么验的」,验不出来的就不要写成实测。**
1067 1076
fixtures/devloop-mock/workgraph-planning.json
1 { 1 {
2 - "_doc": "P3 mock fixture · workgraph-planning。每个 org node 接到任务时被调一次,产出该节点的 work graph。\n\n⚠️ 图必须放在 chunks 里,不能只放 result:AgentManager.callAgent 返回的 output 来自 runPromptInSession 累积的流式 chunks(agent-manager.js:332),而 session/result 的文本不进 output。graph-engine.extractPlannedGraph 解析的正是 output。真实 agent 也是把答案流式吐出来的,所以放 chunks 才是与生产一致的形状。\n\n单变体(不用 sequence):三个 org node 拿到同一张图形状,UI 走查可以反复跑而不会因为 _mockCallCount 走到序列末尾而变形。图满足 work-graph-db 的五项写入前校验:无环(仅 forward)、单入口 wg-plan、全可达、回退边显式标 backward、wg-check 的 fail 出边留了一条 fail_reason 缺省的兜底边。", 2 + "_doc": "P3 mock fixture · subgraph 节点的规划本体。graph-engine.defaultPlanWorkGraph 从 agent 回复里 extractPlannedGraph:优先认 wg_+12位十六进制 的已落库 id,否则找 ```json 围栏块里 {nodes:[],edges:[]} 的图 JSON。这里走后者(模板里的 404 降级路径,也是真跑里更常见的一条)。图 JSON 同时放进 chunks 与 result,因为 mock 下 callAgent 的 output 取哪一半取决于 AgentManager,不该由 fixture 来赌。usage 让「规划这一轮烧的 token」在 mock 下也产得出来 —— defaultPlanWorkGraph 自己有一条独立的 onAcpEvent 累加,与 defaultAgentExecutor 那条不是同一段代码。",
3 "chunks": [ 3 "chunks": [
4 - "[mock] workgraph-planning 收到任务,开始规划本 org node 的 work graph。\n",  
5 - "读候选骨架 → 选定三节点线性主干 + 一条回退边。\n\n",  
6 - "```json\n{\n \"nodes\": [\n { \"nodeName\": \"wg-plan\", \"systemAgentId\": \"prd-planning\" },\n { \"nodeName\": \"wg-build\", \"systemAgentId\": \"sdd-planning\" },\n { \"nodeName\": \"wg-check\", \"systemAgentId\": \"coder-validator\" }\n ],\n \"edges\": [\n { \"from\": \"wg-plan\", \"to\": \"wg-build\", \"condition\": \"success\" },\n { \"from\": \"wg-build\", \"to\": \"wg-check\", \"condition\": \"success\" },\n { \"from\": \"wg-check\", \"to\": \"wg-build\", \"condition\": \"fail\", \"edgeKind\": \"backward\" }\n ]\n}\n```\n\n",  
7 - "wg-plan 出需求、wg-build 出设计与实现、wg-check 验收;验收不过退回 wg-build 重做。\n" 4 + "[mock] workgraph-planning 读取 org 节点契约,裁一张最小实现子图。\n",
  5 + "```json\n{\n \"nodes\": [{ \"nodeName\": \"prd-planning\", \"systemAgentId\": \"prd-planning\" }],\n \"edges\": []\n}\n```\n"
8 ], 6 ],
9 - "result": "mock-ok workgraph-planning:已规划三节点 work graph(wg-plan → wg-build → wg-check,含一条回退边)。" 7 + "result": "mock-ok workgraph-planning:已裁出子图。\n```json\n{\n \"nodes\": [{ \"nodeName\": \"prd-planning\", \"systemAgentId\": \"prd-planning\" }],\n \"edges\": []\n}\n```\n",
  8 + "usage": { "input": 800, "output": 260 }
10 } 9 }
server/tests/unit/graph-real-chain-smoke.test.js
@@ -30,6 +30,11 @@ @@ -30,6 +30,11 @@
30 * 竭「 霎ケ逡悟ョ亥梱逵溽噪蠑莠シ啻boundaryGuardApplicable` 蜿ェ蝨ィ `agentExecutor === defaultAgentExecutor` 30 * 竭「 霎ケ逡悟ョ亥梱逵溽噪蠑莠シ啻boundaryGuardApplicable` 蜿ェ蝨ィ `agentExecutor === defaultAgentExecutor`
31 * 譌カ霑泌屓 true 窶披 豕ィ蜈・蛛 executor 逧畑萓矩螳**諱剃クコ false**碁ぅ譚。蛻髪螳樣刔髮カ隕尠縲 31 * 譌カ霑泌屓 true 窶披 豕ィ蜈・蛛 executor 逧畑萓矩螳**諱剃クコ false**碁ぅ譚。蛻髪螳樣刔髮カ隕尠縲
32 * 32 *
  33 + * 笏笏 荳、謾ッ驛ス隕∬オー 笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏
  34 + * `agent_call` 荳 `subgraph` 譏ッ荳、譚。**荳榊酔逧**鮟倩ョ、螳樒鴫defaultAgentExecutor` /
  35 + * `defaultPlanWorkGraph`会シ悟推譛牙推逧 onAcpEvent 邏ッ蜉縲∝推譛牙推逧ョ亥梱蠑蜈ウ蛻髪縲
  36 + * 蜿ェ隕尠蜑崎噪隸晢シ後瑚ァ蟄仙崟縲埼ぅ蜊頑擅體セ莉咲┯譏ッ髮カ逵溷ョ櫁ヲ尠 窶披 謇莉・荳矩擇 竭」竭、 荳楢オー subgraph縲
  37 + *
33 * 笏笏 髫皮ヲサ 笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏 38 * 笏笏 髫皮ヲサ 笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏
34 * GRAPH_DB_PATH / GRAPH_RUNS_DIR 驛ス謖クエ譌カ逶ョ蠖包シ御ク泌ソ。サ蝨ィ require 荵句燕隶セ 39 * GRAPH_DB_PATH / GRAPH_RUNS_DIR 驛ス謖クエ譌カ逶ョ蠖包シ御ク泌ソ。サ蝨ィ require 荵句燕隶セ
35 * 井ク、荳ェ讓。蝮鈴蝨ィ鬘カ驛ィ蠢ォ辣ァ峨ALEMONBOSS_CLAUDE_MODE` 蜷檎炊咾laudeCliProtocol 逧 40 * 井ク、荳ェ讓。蝮鈴蝨ィ鬘カ驛ィ蠢ォ辣ァ峨ALEMONBOSS_CLAUDE_MODE` 蜷檎炊咾laudeCliProtocol 逧
@@ -50,6 +55,8 @@ process.env.GRAPH_RUNS_DIR = path.join(TMP, &#39;runs&#39;); @@ -50,6 +55,8 @@ process.env.GRAPH_RUNS_DIR = path.join(TMP, &#39;runs&#39;);
50 // 逕ィ prd-planning壽悽菴鍋悄螳槫ュ伜惠掲ixture 閾ェ蟶ヲ files[]亥性 readNodeProduct 隕∬ッサ逧 55 // 逕ィ prd-planning壽悽菴鍋悄螳槫ュ伜惠掲ixture 閾ェ蟶ヲ files[]亥性 readNodeProduct 隕∬ッサ逧
51 // attempt-{attempt}.json我ク usage縲 56 // attempt-{attempt}.json我ク usage縲
52 const AGENT = 'prd-planning'; 57 const AGENT = 'prd-planning';
  58 +const PLANNER = 'workgraph-planning'; // subgraph 驍」荳謾ッ逧ァ譛ャ菴難シixture 蜷悟錐
  59 +const WRAPPER = 'zz-smoke-wrapper'; // 謇ソ霓ス蟄仙崟逧ぅ荳ェ闃らせ悟宵譏ッ荳ェ蜷榊ュ暦シ御ク堺シ夊「ォ隹畑
53 const COMPANY = 'zz-real-chain-co'; 60 const COMPANY = 'zz-real-chain-co';
54 61
55 let wdb; let engine; 62 let wdb; let engine;
@@ -99,7 +106,9 @@ describe(&#39;蝗セ謇ァ陦檎悄螳樣得霍ッ smoke磯峺豕ィ蜈・悟宵謐「謗臥悄 CLI 蟄占ソ帷ィ具シ @@ -99,7 +106,9 @@ describe(&#39;蝗セ謇ァ陦檎悄螳樣得霍ッ smoke磯峺豕ィ蜈・悟宵謐「謗臥悄 CLI 蟄占ソ帷ィ具シ
99 // 逵溷ョ樣得霍ッ莨壼惠譛ャ菴謎ク句サコ蜈ャ蜿ク諤∫岼蠖包シgentManager 逧 cwd 譏ッ system/agents/{aid}/{cid}/{taskId}峨 106 // 逵溷ョ樣得霍ッ莨壼惠譛ャ菴謎ク句サコ蜈ャ蜿ク諤∫岼蠖包シgentManager 逧 cwd 譏ッ system/agents/{aid}/{cid}/{taskId}峨
100 // 螳 gitignore 謖。逹縲∽ク崎ソ帷沿譛ャ蠎難シ御ス蕗逹莨夊ョゥ蛻ォ逧畑萓矩″蜴 system/agents/ 譌カ螟壽沖荳莉ス 107 // 螳 gitignore 謖。逹縲∽ク崎ソ帷沿譛ャ蠎難シ御ス蕗逹莨夊ョゥ蛻ォ逧畑萓矩″蜴 system/agents/ 譌カ螟壽沖荳莉ス
101 // 荵溯ョゥ `ls system/agents/prd-planning/` 蜃コ邇ー荳荳ェ豐。莠コ隶、鬚噪蜈ャ蜿ク縲 108 // 荵溯ョゥ `ls system/agents/prd-planning/` 蜃コ邇ー荳荳ェ豐。莠コ隶、鬚噪蜈ャ蜿ク縲
102 - fs.rmSync(path.join(__dirname, '../../../system/agents', AGENT, COMPANY), { recursive: true, force: true }); 109 + for (const a of [AGENT, PLANNER]) {
  110 + fs.rmSync(path.join(__dirname, '../../../system/agents', a, COMPANY), { recursive: true, force: true });
  111 + }
103 delete process.env.LEMONBOSS_CLAUDE_MODE; 112 delete process.env.LEMONBOSS_CLAUDE_MODE;
104 delete process.env.GRAPH_DB_PATH; 113 delete process.env.GRAPH_DB_PATH;
105 delete process.env.GRAPH_RUNS_DIR; 114 delete process.env.GRAPH_RUNS_DIR;
@@ -123,13 +132,81 @@ describe(&#39;蝗セ謇ァ陦檎悄螳樣得霍ッ smoke磯峺豕ィ蜈・悟宵謐「謗臥悄 CLI 蟄占ソ帷ィ具シ @@ -123,13 +132,81 @@ describe(&#39;蝗セ謇ァ陦檎悄螳樣得霍ッ smoke磯峺豕ィ蜈・悟宵謐「謗臥悄 CLI 蟄占ソ帷ィ具シ
123 expect(spent.tokens).toBe(1540); 132 expect(spent.tokens).toBe(1540);
124 }); 133 });
125 134
  135 + // 笏笏 subgraph 驍」荳謾ッ單efaultPlanWorkGraph 笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏
  136 + //
  137 + // 隕尠霎ケ逡瑚ッエ貂・夲シ夊ソ咎**霍ウ霑 `resolvePlanningAgentName` 驥檎噪謖蛾怙 hire**磯「蝪樔ク陦
  138 + // company_agents 隶ゥ螳ョ、荳コ髮ソコシ峨ら炊逕ア譏ッ hire 隕∽ク譚。逵溷ョ樒噪蜈ャ蜿ク隶ー蠖包シthis.get(cid)`会シ
  139 + // 閠碁ぅ譚。霍ッ蠕嘗譛 `company-hire-org-layer.test.js` 荳馴葎逶ッ逹 窶披 霑呎擅 smoke 隕ェ檎噪譏ッ
  140 + // 縲瑚ァ霑吩ク谿オ體セ霍ッ騾壻ク埼壹搾シ御ク肴弍縲梧魚閨倬壻ク埼壹阪
  141 + function seedPlannerHired() {
  142 + const db = wdb.getDb();
  143 + const now = new Date().toISOString();
  144 + db.prepare(`INSERT OR IGNORE INTO system_agents
  145 + (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
  146 + VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)`)
  147 + .run(PLANNER, PLANNER, `system/agents/${PLANNER}`, now, now);
  148 + // subgraph 闃らせ閾ェ蟾ア荵溯ヲ∵怏 system_agents 陦鯉シork_graph_nodes.system_agent_id 譏ッ螟夜醗峨
  149 + // 螳宵譏ッ縲悟頭荳ェ org 隗定牡諡・譛芽ソ吝シ蟄仙崟縲咲噪蜷榊ュ暦シ御ク堺シ夊「ォ逵溽噪隹畑 窶披 隹噪譏ッ PLANNER縲
  150 + db.prepare(`INSERT OR IGNORE INTO system_agents
  151 + (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
  152 + VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)`)
  153 + .run(WRAPPER, WRAPPER, `system/agents/${WRAPPER}`, now, now);
  154 + db.prepare(`INSERT OR IGNORE INTO company_agents (company_id, system_agent_id, role_name, status, hired_at)
  155 + VALUES (?, ?, ?, 'active', ?)`).run(COMPANY, PLANNER, PLANNER, now);
  156 + }
  157 +
  158 + it('竭」 subgraph 闃らせ逵溽噪謚 workgraph-planning 逧崟 JSON 蜿俶莠ク譚。蟄 run', async () => {
  159 + seedPlannerHired();
  160 + const g = wdb.createWorkGraph({
  161 + companyId: COMPANY,
  162 + nodes: [{ nodeName: WRAPPER, systemAgentId: WRAPPER, nodeKind: 'subgraph' }],
  163 + edges: [],
  164 + });
  165 + expect(g.errors || []).toEqual([]);
  166 + const run = wdb.createRun({
  167 + graphKind: 'work', graphId: g.id, companyId: COMPANY, executionMode: 'manual',
  168 + budget: { total_agent_calls: 10, total_tokens: 5_000_000, spent: { agent_calls: 0, tokens: 0 } },
  169 + });
  170 + engine.markRunReady(run.id);
  171 + engine.startRun(run.id);
  172 +
  173 + const out = await engine.nextStep(run.id, { task: '逵溷ョ樣得霍ッ smoke夊ァ荳蠑蟄仙崟' });
  174 + if (!out.ok) throw new Error(`subgraph 豐。謗ィ蜉ィ${JSON.stringify(out)}`);
  175 + // 辷カ闃らせ蛛懷惠 running 遲牙ュ run瑚ソ呎弍 subgraph 逧ュ」蟶ク蠖「諤シ井ク肴弍豐。霍大勘
  176 + expect(out.reason).toBe('awaiting-child-run');
  177 + expect(out.node.childRunId).toBeTruthy();
  178 +
  179 + // 蟄 run 逧ョ壻ケ牙ソ。サ譚・閾ェ fixture 驍」蠑蝗セ 窶披 豕ィ蜈・蛛 planner 譌カ霑吩ク謨エ谿オ亥屓螟崎ァ」譫 竊
  180 + // createWorkGraphSafely 竊 蟒コ蟄 run蛾荳榊ュ伜惠
  181 + const child = wdb.getRun(out.node.childRunId);
  182 + expect(child.graph_kind).toBe('work');
  183 + const childNodes = wdb.getDb()
  184 + .prepare(`SELECT def_node_id FROM graph_run_nodes WHERE run_id = ?`).all(child.id);
  185 + expect(childNodes).toHaveLength(1);
  186 + const def = wdb.getDb()
  187 + .prepare(`SELECT node_name, system_agent_id FROM work_graph_nodes WHERE id = ?`)
  188 + .get(childNodes[0].def_node_id);
  189 + expect(def.node_name).toBe(AGENT);
  190 + expect(def.system_agent_id).toBe(AGENT);
  191 +
  192 + // 竭、 隗驍」荳霓ョ辜ァ逧 token 荵溯ソ帑コ「ョ励**螳オー逧弍蜿ヲ荳谿オ onAcpEvent**
  193 + // efaultPlanWorkGraph 閾ェ蟾ア驍」譚。会シ御ク 竭。 鬪檎噪荳肴弍蜷御ク谿オ莉」遐√
  194 + const spent = JSON.parse(wdb.getRun(run.root_run_id || run.id).budget_json).spent;
  195 + expect(spent.tokens).toBe(1060); // fixture 謚・ 800 + 260
  196 + }, 60_000);
  197 +
126 it('竭「 霎ケ逡悟ョ亥梱蝨ィ霑呎擅霍ッ蠕ク頑弍**蠑逹**逧シ域ウィ蜈・蛛 executor 譌カ螳£荳コ蛛シ碁ぅ譚。蛻髪髮カ隕尠', () => { 198 it('竭「 霎ケ逡悟ョ亥梱蝨ィ霑呎擅霍ッ蠕ク頑弍**蠑逹**逧シ域ウィ蜈・蛛 executor 譌カ螳£荳コ蛛シ碁ぅ譚。蛻髪髮カ隕尠', () => {
127 // 蛻、謐ョ蜿冶蠑墓梼閾ェ蟾ア逧シ蜈ウ蜃ス謨ー瑚御ク肴弍蜴サ謨ー timeline 莠倶サカ 199 // 蛻、謐ョ蜿冶蠑墓梼閾ェ蟾ア逧シ蜈ウ蜃ス謨ー瑚御ク肴弍蜴サ謨ー timeline 莠倶サカ
128 // 螳亥梱蝨ィ螳ソ荳サ莉灘コ灘ケイ蜃譌カ譛ャ譚・蟆ア荳堺コァ逕滉コ倶サカ梧響縲梧イ。譛我コ倶サカ縲榊ス薙梧イ。蠑縲堺シ壼渚霑擂蛻、髞吶 200 // 螳亥梱蝨ィ螳ソ荳サ莉灘コ灘ケイ蜃譌カ譛ャ譚・蟆ア荳堺コァ逕滉コ倶サカ梧響縲梧イ。譛我コ倶サカ縲榊ス薙梧イ。蠑縲堺シ壼渚霑擂蛻、髞吶
129 expect(engine.boundaryGuardApplicable({ nodeKind: 'agent_call' })).toBe(true); 201 expect(engine.boundaryGuardApplicable({ nodeKind: 'agent_call' })).toBe(true);
130 - // 蟇ケ辣ァ壽ウィ蜈・荵句錘蠢。サ蜿伜∞ 窶披 蟆台コソ呎擅御ク企擇驍」蜿・蝨ィ蜃ス謨ー蜀呎 `return true` 譌カ荵溷扈ソ 202 + expect(engine.boundaryGuardApplicable({ nodeKind: 'subgraph' })).toBe(true);
  203 + // 蟇ケ辣ァ壽ウィ蜈・荵句錘蠢。サ蜿伜∞ 窶披 蟆台コソ呎擅御ク企擇荳、蜿・蝨ィ蜃ス謨ー蜀呎 `return true` 譌カ荵溷扈ソ縲
  204 + // 荳、謾ッ蜷怏蜷噪蠑蜈ウ蛻髪梧園莉・荳、霎ケ驛ス隕ェ後
131 engine.setAgentExecutor(async () => ({ status: 'success' })); 205 engine.setAgentExecutor(async () => ({ status: 'success' }));
  206 + engine.setPlanWorkGraph(async () => ({ workGraphId: 'wg_000000000000' }));
132 expect(engine.boundaryGuardApplicable({ nodeKind: 'agent_call' })).toBe(false); 207 expect(engine.boundaryGuardApplicable({ nodeKind: 'agent_call' })).toBe(false);
  208 + expect(engine.boundaryGuardApplicable({ nodeKind: 'subgraph' })).toBe(false);
133 engine.setAgentExecutor(null); 209 engine.setAgentExecutor(null);
  210 + engine.setPlanWorkGraph(null);
134 }); 211 });
135 }); 212 });