From 54470be3a4d5a8d78a303cf30b42a3ce74c3ee43 Mon Sep 17 00:00:00 2001 From: yibo Date: Thu, 20 Aug 2026 18:19:14 +0800 Subject: [PATCH] test(p3b): 验收 4 进零注入链路;2/8 由真跑验掉,9 的准确状态写清楚 --- docs/README.md | 2 +- docs/todo/system-design/27-p3b-real-run-runway.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- fixtures/devloop-mock/benchmark-validator.json | 18 ++++++++++++++++++ server/tests/unit/graph-real-chain-smoke.test.js | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ server/tests/unit/template-verdicts.test.js | Bin 10239 -> 0 bytes 5 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 fixtures/devloop-mock/benchmark-validator.json diff --git a/docs/README.md b/docs/README.md index 258d93d..294705f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -42,7 +42,7 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d | [27-e2e-task-execution-flow.md](./todo/system-design/27-e2e-task-execution-flow.md) | 端到端流程总览:建公司 → 下达任务 → org graph 逐节点执行 → work graph 规划与循环 → 验收收敛。含与 DevLoop 既有实现的对应关系分析 | | [27-example-dev-company-walkthrough.md](./todo/system-design/27-example-dev-company-walkthrough.md) | 参考场景走查(虚构的「配图工厂」),把架构跑一遍具体的数:各阶段 JSON 形态、成本估算、断点清单 | | [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 那一段 | -| [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 —— 都不是「没修」,是「没在干净环境重验」**。⚠️ 这份文档里 ⛔/🟡 的段落多为**当时的现场记录**,读之前先看头部状态行 | +| [27-p3b-real-run-runway.md](./todo/system-design/27-p3b-real-run-runway.md) | P3 收尾之真跑跑道(从 mock 走到真 Claude CLI)— 📝 **只剩验收 9**。2026-08-20 第四次真跑(§六ter)验掉 2 与 8,验收 4 由零注入链路验掉,7 早已修好。验收 9 的准确状态是「产出端通了、消费端没等到那个形状」—— 需要一次 **org 层回退边指向 subgraph 节点**。⚠️ 这份文档里 ⛔/🟡 的段落多为**当时的现场记录**,读之前先看头部状态行 | | [27-goal-contract-format.md](./todo/system-design/27-goal-contract-format.md) | 验收契约格式(`goal-benchmark` / `goal-test-data` / `goal-test-script` 的产物形态与跨节点传递) | | [27-p4-parallel-and-artifacts.md](./todo/system-design/27-p4-parallel-and-artifacts.md) | **P4(回收编号)并行执行与产物索引** — 📝 设计中,未实现。fan-out 真并发 + 同 agent 多实例隔离 + `files.json` 产物索引。三个 Phase,C 可插队 | | [28-followups.md](./todo/system-design/28-followups.md) | 28 号(Agent 注册表统一)的遗留清单 — 主文档已在 `done/`,这份装没做完的:dream 机制(只有口述、无设计)、任务态清理(源码树无限增长)、`source_kind`/`legacy_path` 删列、三项「已实施未验证」。§五 是**已立起来的守卫清单**(别重复造,也别删) | diff --git a/docs/todo/system-design/27-p3b-real-run-runway.md b/docs/todo/system-design/27-p3b-real-run-runway.md index c290f40..fe60194 100644 --- a/docs/todo/system-design/27-p3b-real-run-runway.md +++ b/docs/todo/system-design/27-p3b-real-run-runway.md @@ -8,7 +8,8 @@ > **状态**(2026-08-20 校准): 📝 **仍在推进**。第一批验收 1/3 已验、2 的后半与 4 未在干净环境重验; > 第二批验收 7 ✅ **已修**(`58a2865`「W7 零边图」,守卫 `company-hire-org-layer.test.js` 的 > `CO_ZEROEDGE` + `W7-fix`)—— 下面 §六bis 缺陷③ 那段是**当时的现场记录**,不是当前状态; -> 第三批 8/9 真 CLI 未验。 +> 验收 2 ✅、验收 8 ✅ 由 2026-08-20 的真跑验掉,验收 4 ✅ 由零注入链路验掉, +> **验收 9 仍开**(产出端已通、消费端没等到那个形状)—— 逐条证据见 §六ter。 > §六bis 缺陷② 的**根因**(边界守卫误判并行会话的改动)已于 2026-08-20 修掉, > 见 [27-P3C §17.2](../../done/system-design/27-p3c-goal-task-model.md) —— 但**根因修了不等于验收点验了**,那几条仍要重跑一遍。 @@ -935,3 +936,65 @@ failingVerdict = verdict === null ? raw !== null : !PASSING_VERDICTS.has(verdict (「找不到 FAILING_VERDICTS——取值域防线失去依据,不要让它静默空跑」)。 真去反转的话,把该测试里 `engineSet('FAILING_VERDICTS')` 换成 `engineSet('PASSING_VERDICTS')` 并把 `VERDICT_DOMAIN` 的组成对应改一行即可;**不要**把断言删掉了事。 + +--- + +## 六ter、第四次真跑复验(2026-08-20) + +**跑法**:主仓库、真 Claude CLI、工作区干净(`git status` 空)。公司 `e2e-demo`, +把 2026-08-18 卡住的那条 run(`run_9168bd24f46c`,做 lemonai 官网)解开后逐节点 `/next` 推到收敛。 +19 次真 agent 调用、177,462 tokens,全树 `completed`,goal → `done`。 +过程记在 [27-P3C §17.5](../../done/system-design/27-p3c-goal-task-model.md)。 + +### 逐条结果 + +| 验收 | 结果 | 证据 | +|---|---|---| +| **2** 产出落在项目根内、宿主仓库 `git status` 无新增/修改 | ✅ **成立** | 19 次真 agent 调用之后 `git status --porcelain` **完全为空**;交付物(`index.html` / `styles.css` / `app.js` / `assets/img/` / `DELIVERY.md`)都在 `data/companies/e2e-demo/projects/p-bkhylu/` 内。这是 §六bis「后半被并行 agent 打破」那一条的干净环境重验 | +| **3** 越界被守卫拒绝并留痕 | ✅ 早已成立;误判那一半 2026-08-20 修掉 | 见 [27-P3C §17.2](../../done/system-design/27-p3c-goal-task-model.md) | +| **4** 非法契约 → validator 判失败且引擎按失败处理 | ✅ **成立**,但**不是真 CLI 验的** | `graph-real-chain-smoke.test.js` ⑥(零注入链路,只换掉 CLI 子进程):fixture 产出 `verdict: "attributed"` → 节点 `fail`、`fail_reason=input_underspecified`、沿 fail 边点亮下游、判词落在下游节点行上。反向验过:verdict 改成 `passed` 立刻红 | +| **7** work 层本体不混进 org graph | ✅ 已修(`58a2865`) | `CO_ZEROEDGE` / `W7-fix` | +| **8** validator-triager 的提示词里有「哪个节点失败、第几 attempt、失败摘要」 | ✅ **成立(真 CLI)** | 判据是**它引用了只可能来自提示词的内部 id**:triager 的判词原文写着「`org-coder-impl` 私有目录 `rn_96a8cd696dcc` 下只有 `_research.md`」—— `rn_96a8cd696dcc` 是 `graph_run_nodes.id`,agent 没有任何别的途径知道它,只能来自 `describeFailureContext` 注入的那一段 | +| **9** 一次真实的跨层回退,两半地址齐全、只重置被点名的节点 | 🟡 **产出端通了,消费端没等到那个形状** | 见下 | + +### 验收 9 的准确状态 + +真跑里 triager **确实产出了两半地址**: + +``` +node.triage | 归因 verdict=attributed fail_reason=impl_bug → {"orgNode":"org-coder","workNode":"org-coder-impl"} +``` + +这一条本身有分量:[27-P3/P4](./27-p3-p4-work-graph-console.md) §八.22 的**第 13 处**记的正是 +「引擎四环全通,但产出方 `validator-triager` 的 agent.md 里根本没有这个键」—— +**那半条现在通了**,真实 agent 真的写出了 `orgNode` + `workNode`。 + +但紧接着的一条: + +``` +node.triage | 归因点名了 org-coder-impl,但回退目标 org-coder-impl 不是 subgraph 节点,忽略跨层地址 +``` + +**这不是缺陷,是判据在正常工作**:这次回退发生在 **work 层内部** +(`org-coder-validator` → `org-coder-impl`),`edge.to` 本身就是 `agent_call` 节点, +已经是最细粒度,没有「跨层」可言 —— `resolveCrossLayerTarget` 第 ② 关就是拦这个的。 + +所以验收 9 缺的是**那个形状本身**:需要一次 **org 层的回退边、且目标是 subgraph 节点**。 +今天这条 run 的三个 org 节点重试后一次过,没有 org 层失败。 + +> **别把「机制没验」和「机制没有」混为一谈**:定点重置的逻辑侧有 +> `graph-cross-layer-address.test.js` 三例注入式覆盖(只写 `target_node` / 只写 +> `target_org_node` / 模板键名逐字对齐)。缺的只是「真跑里发生过一次」。 + +### 顺带发现的一条缺陷(未修) + +真跑的 timeline 里,**每个节点的产物索引写入都失败了**,且被降级成一条日志: + +``` +node.output | 产物索引写入失败 contract.json: UNIQUE constraint failed: index 'idx_graph_files_path' +node.output | 产物索引写入失败 sdd.md: UNIQUE constraint failed: index 'idx_graph_files_path' +``` + +形态是重试时同一条产物路径被再写一次撞唯一索引。后果不是崩溃 —— +**是 `files.json` 产物索引在有重试的 run 上静默不全**,而那正是 +[27-P4](./27-p4-parallel-and-artifacts.md) 要建的东西。归到 P4 一起处理。 diff --git a/fixtures/devloop-mock/benchmark-validator.json b/fixtures/devloop-mock/benchmark-validator.json new file mode 100644 index 0000000..8a5a4b6 --- /dev/null +++ b/fixtures/devloop-mock/benchmark-validator.json @@ -0,0 +1,18 @@ +{ + "_doc": "P3 mock fixture · 判失败的验收节点(27-P3B §六 验收 4)。attempt-{attempt}.json 里 verdict=attributed 落在 graph-engine.FAILING_VERDICTS 里 → readNodeProduct 置 failingVerdict → 节点判 fail。这份 fixture 的**全部意义**就是让「validator 说失败 → 引擎真的按失败处理」这条判据在 CI 里跑得出来:在这之前它只是「静态成立」(人读代码读通了链路),没有任何一次真跑或真链路证据。改 verdict 的取值前先看 graph-engine 的 FAILING_VERDICTS。 fail_reason 必须取自 graph-engine.ATTRIBUTION_REASONS 这个封闭域(impl_bug / input_underspecified / skill_defect / infra_flake)—— 自创的词会被**按设计降级成 null** 并留一条 timeline,不是崩溃,但节点行上就没有归因了。第一版这里写了 contract_invalid,实测就是这个下场。", + "chunks": [ + "[mock] benchmark-validator 读取被测产物与验收契约。\n", + "契约形状非法:acceptance.json 缺 test_script 段。\n" + ], + "result": "mock-fail benchmark-validator:契约非法,判失败。", + "files": [ + { + "path": "attempt-{attempt}.json", + "content": "{\n \"schema\": \"validator-triager/v1\",\n \"verdict\": \"attributed\",\n \"fail_reason\": \"input_underspecified\",\n \"remediation_note\": \"acceptance.json 缺 test_script 段,补齐后重跑\"\n}\n" + } + ], + "usage": { + "input": 400, + "output": 120 + } +} diff --git a/server/tests/unit/graph-real-chain-smoke.test.js b/server/tests/unit/graph-real-chain-smoke.test.js index 06426ee..5cdf447 100644 --- a/server/tests/unit/graph-real-chain-smoke.test.js +++ b/server/tests/unit/graph-real-chain-smoke.test.js @@ -195,6 +195,66 @@ describe('图执行真实链路 smoke(零注入,只换掉真 CLI 子进程ï¼ expect(spent.tokens).toBe(1060); // fixture 报 800 + 260 }, 60_000); + it('⑥ validator 判失败时引擎真的按失败处理(27-P3B §六 验收 4,此前只是「静态成立」)', async () => { + // 原验收标准:「造一份非法契约跑 benchmark-validator:判决为失败且引擎按失败处理 + // (**不是** success)」。P3B 当时的结论是 🟡「静态成立,未真跑」—— 人读代码把 + // fixture verdict → FAILING_VERDICTS → readNodeProduct → callAgentNode 判 fail + // 这条链读通了,但没有任何一次真跑或真链路证据。 + // + // 「读通了」和「跑通了」之间隔着的正是这条 smoke 要补的东西:产物要真的落盘、 + // 要真的被 readNodeProduct 读到、verdict 要真的被认出来。任何一环断了, + // 节点都会以 success 收场 —— 而**判错成 success 的代价是不合格产物直接放行, + // 且不留痕迹**(P3B §八 那段「两种错代价不对称」说的就是这个方向)。 + const VALIDATOR = 'benchmark-validator'; + // 另开一家公司:一家公司同时只允许一条活跃 root run(idx_graph_runs_one_active_root), + // 上面几条留下的 run 还在活跃集里,共用公司会撞 RunSlotBusyError —— 那是约束在正常工作, + // 不是这条用例要验的东西。 + const CO = `${COMPANY}-v4`; + const db = wdb.getDb(); + const now = new Date().toISOString(); + db.prepare(`INSERT OR IGNORE INTO system_agents + (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at) + VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)`) + .run(VALIDATOR, VALIDATOR, `system/agents/${VALIDATOR}`, now, now); + + const g = wdb.createWorkGraph({ + companyId: CO, + // 带一条 fail 兜底边:没有出边时失败节点收在 `blocked`(挂起等人工), + // 那也是「按失败处理」,但看不出**路由**有没有走通。加上边,判据才完整: + // 失败 → 节点 fail → 沿 fail 边点亮下游。 + nodes: [ + { nodeName: VALIDATOR, systemAgentId: VALIDATOR }, + { nodeName: AGENT, systemAgentId: AGENT }, + ], + edges: [{ from: VALIDATOR, to: AGENT, condition: 'fail' }], + }); + expect(g.errors || []).toEqual([]); + const run = wdb.createRun({ graphKind: 'work', graphId: g.id, companyId: CO, executionMode: 'manual' }); + engine.markRunReady(run.id); + engine.startRun(run.id); + + const out = await engine.nextStep(run.id, { task: '真实链路 smoke:验一份非法契约' }); + if (!out.ok) throw new Error(`validator 没推动:${JSON.stringify(out)}`); + // 这两行就是验收 4 的全部:**不是 success**,且归因落了库 + expect(out.node.status).toBe('fail'); + // fail_reason 取自封闭域 ATTRIBUTION_REASONS —— fixture 里写个自创的词会被 + // 按设计降级成 null(引擎不认识的归因一律不信),第一版就是这么红的 + expect(out.node.failReason).toBe('input_underspecified'); + // 路由也要走通:失败的下一步是沿 fail 边点亮下游,不是原地不动 + expect((out.activated || []).map((a) => a.name)).toContain(AGENT); + + // 判词也要带下去 —— 没有它,下一轮 agent 不知道要修什么(§八.14 那条四环全通的链路)。 + // 注意判词落在**被点亮的下游节点**那一行,不是失败节点自己那一行: + // 「判词的载体 = 点亮它的那次状态转换」,这样多条回退边并存时天然不串台。 + const downstream = wdb.getDb() + .prepare(`SELECT remediation_note FROM graph_run_nodes + WHERE run_id = ? AND def_node_id = (SELECT id FROM work_graph_nodes WHERE work_graph_id = ? AND node_name = ?)`) + .get(run.id, g.id, AGENT); + expect(downstream.remediation_note).toContain('test_script'); + + fs.rmSync(path.join(__dirname, '../../../system/agents', VALIDATOR, CO), { recursive: true, force: true }); + }, 60_000); + it('③ 边界守卫在这条路径上是**开着**的(注入假 executor 时它恒为假,那条分支零覆盖)', () => { // 判据取自引擎自己的开关函数,而不是去数 timeline 事件: // 守卫在宿主仓库干净时本来就不产生事件,拿「没有事件」当「没开」会反过来判错。 diff --git a/server/tests/unit/template-verdicts.test.js b/server/tests/unit/template-verdicts.test.js index 2884b81..6ff2d47 100644 Binary files a/server/tests/unit/template-verdicts.test.js and b/server/tests/unit/template-verdicts.test.js differ -- libgit2 0.21.4