步驟5 抽驗補證:三型分支用「真零件實跑輸出」端到端驗過(不再是推論)

總管 08-01 抽驗要求(正確):ON_CASE/ON_CATCH grep=0,我用 ON_BRANCH 通用承接,
但既有測試是用 Input 節點**手餵分支形狀**——那只證明「引擎依標籤選邊」,
**沒證明「真零件吐的標籤對得上」**。leo 特別點名 switch/try_catch,故補實測。

真零件實跑(wasmtime 跑 .component-builds/*.wasm,原文抄進測試當 given):
  if_control  active   → {"data":{"branch":"true","result":true},"success":true}
  if_control  inactive → {"data":{"branch":"false","result":false},"success":true}
  switch      case1    → {"data":{"branch":"branch_active"},"success":true}
  switch      case3    → {"data":{"branch":"branch_pending"},"success":true}
  switch      無匹配   → {"data":{"branch":"branch_default"},"success":true}
  try_catch   成功     → {"data":{"branch":"try","result":{"value":42}},"success":true}
  try_catch   失敗     → {"data":{"branch":"catch","error":"boom"},"success":true}
⇒ 三顆的標籤與 readBranch 讀的 data.branch **完全對得上**,非「理論上支援」。

新增 tests/branch-real-components.test.ts(8 項綠):把上列真輸出送進引擎,
驗 if 兩路/switch 多路(含**第 3 條 case** 證明第 N 路走對)+default/
try_catch ok 與 catch 兩路,每項都驗「該走的走、不該走的沒走」。

新增 tests/branch-hint-response.test.ts(7 項綠):驗逐顆查三顆的回應
自帶 branch_field/branches/edge_types/usage/example,並把 AI 實際看到的內容印出來
(總管要求「貼回應」)。另驗不分岔零件(http_request/code)無此欄=不加噪音。

全套 227 passed(前 212 +15),失敗數維持既有 9 筆未變;tsc --noEmit 綠。

SDD: workflow-discovery 3.11|CP: arcrun-usable 步驟 5

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-07-31 16:43:25 +08:00
parent c9feb15a37
commit e688d805ea
2 changed files with 215 additions and 0 deletions
@@ -0,0 +1,68 @@
/**
* 逐顆查詢的回應要自我說明分支用法(SDD workflow-discovery 3.11;總管 08-01 抽驗第 3 點)
*
* 判準(leo/總管一致):**AI 只看那一顆的回應,就知道怎麼接下一步**——
* 不必回頭讀 skill、不必猜。看得到分支說明才算數。
*
* 取證背景(08-01 prod):逐顆查 if_control 只回
* status/componentId/type/source/input_schema{condition,input}/success_rate/stability
* ⇒ **沒有任何欄位說明分支怎麼接** ⇒ 走 n8n 式逐顆查的 AI 只好寫 code。
*
* 本檔直接驗 `branchHintFor()`(回應裡那個欄位的來源),並把 AI 實際會看到的內容印出來。
*/
import { describe, it, expect } from 'vitest';
import { branchHintFor } from '../src/lib/branch-hints';
describe('三顆分支零件的查詢回應自帶用法(AI 看一眼就知道怎麼接)', () => {
for (const id of ['if_control', 'switch', 'try_catch']) {
it(`${id}:回應含 branch_fieldbranchesedge_typesusageexample`, () => {
const hint = branchHintFor(id);
expect(hint).toBeDefined();
// 這一顆會輸出哪個欄位當分支標籤
expect(hint!.branch_field).toBe('data.branch');
// 接下游要用哪些邊型
expect(hint!.edge_types.length).toBeGreaterThan(0);
// 一行說明 + 可照抄範例(缺任一個,AI 都得自己猜)
expect(hint!.usage.length).toBeGreaterThan(0);
expect(hint!.example.length).toBeGreaterThan(0);
// eslint-disable-next-line no-console
console.log(
`\n──────── 逐顆查 ${id} 時,AI 會看到的 branch_hint ────────\n` +
JSON.stringify(hint, null, 2),
);
});
}
it('if_control 明說 ON_TRUEON_FALSE 兩條邊', () => {
const h = branchHintFor('if_control')!;
expect(h.edge_types).toContain('ON_TRUE');
expect(h.edge_types).toContain('ON_FALSE');
expect(h.branches).toEqual(['true', 'false']);
// 明說「不需要自己寫 code 判斷」——這句是防腹語術的關鍵
expect(h.usage).toContain('不需要自己寫 code');
});
it('switch 明說用 ON_BRANCH 並在邊上標 case 名,且 default 不需特別邊型', () => {
const h = branchHintFor('switch')!;
expect(h.edge_types).toContain('ON_BRANCH');
expect(h.usage).toContain('ON_BRANCH');
expect(h.usage).toContain('default_branch');
// branches 是動態的(由 cases 決定),要誠實說明而非給死清單
expect(typeof h.branches).toBe('string');
});
it('try_catch 明說 trycatch 兩條標籤,錯誤處理不必寫 code', () => {
const h = branchHintFor('try_catch')!;
expect(h.branches).toEqual(['try', 'catch']);
expect(h.edge_types).toContain('ON_BRANCH');
expect(h.usage).toContain('不需要寫 code');
});
it('不分岔的零件沒有 branch_hint(不加噪音)', () => {
expect(branchHintFor('http_request')).toBeUndefined();
expect(branchHintFor('code')).toBeUndefined();
expect(branchHintFor(undefined)).toBeUndefined();
});
});
@@ -0,0 +1,147 @@
/**
* 三型分支零件「真的接上引擎」的實測(SDD workflow-discovery 3.11
*
* 為什麼要另立這一支(總管 08-01 抽驗要求,正確的要求):
* conditional-edges.test.ts 是用 Input 節點**手餵分支形狀**測引擎走邊邏輯,
* 那證明的是「引擎依標籤選邊」,**沒有證明「真零件吐出來的標籤真的對得上」**。
* leo 特別點名 switchtry_catch,且 `ON_CASE``ON_CATCH` grep=0
* ⇒ 必須排除「機制通用所以理論上支援」這種推論。
*
* 本檔的 given 全部是**真 WASM 零件的實跑輸出**wasmtime 執行 .component-builds/*.wasm
* 抓回來的原文,非杜撰),再送進引擎驗證走對邊。
*
* 真零件實跑指令(可復驗):
* cd .component-builds
* echo '{"condition":"status == active","input":{"status":"active"}}' | wasmtime if_control/component.wasm
* echo '{"value":"pending","cases":[...],"default_branch":"branch_default"}' | wasmtime switch/component.wasm
* echo '{"result":null,"error":"boom"}' | wasmtime try_catch/component.wasm
*/
import { SELF } from 'cloudflare:test';
import { describe, it, expect } from 'vitest';
async function run(graph: unknown) {
const res = await SELF.fetch('http://localhost/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ graph, context: {} }),
});
const body = (await res.json()) as {
success: boolean;
trace?: Array<{ nodeId: string }>;
};
return { body, visited: (body.trace ?? []).map(t => t.nodeId) };
}
/** 真零件輸出 → 當作上游節點的 output 餵進圖 */
function graphWith(realOutput: unknown, edges: Array<Record<string, unknown>>, extraNodes: string[]) {
return {
id: 'real-branch',
name: '真零件輸出走邊',
nodes: [
{ id: 'ctrl', type: 'Input', data: realOutput },
...extraNodes.map(id => ({
id, type: 'Component', componentId: 'comp_uppercase', data: { text: id },
})),
],
edges,
};
}
describe('if_control 真輸出 → 引擎走對邊', () => {
// 真跑:echo '{"condition":"status == active","input":{"status":"active"}}' | wasmtime if_control/component.wasm
const REAL_TRUE = { data: { branch: 'true', result: true }, success: true };
// 真跑:input.status = "inactive"
const REAL_FALSE = { data: { branch: 'false', result: false }, success: true };
const edges = [
{ from: 'ctrl', to: 'yes', type: 'ON_TRUE' },
{ from: 'ctrl', to: 'no', type: 'ON_FALSE' },
];
it('條件成立(真輸出 branch="true")→ 走 ON_TRUE', async () => {
const { body, visited } = await run(graphWith(REAL_TRUE, edges, ['yes', 'no']));
expect(body.success).toBe(true);
expect(visited).toContain('yes');
expect(visited).not.toContain('no');
});
it('條件不成立(真輸出 branch="false")→ 走 ON_FALSE', async () => {
const { visited } = await run(graphWith(REAL_FALSE, edges, ['yes', 'no']));
expect(visited).toContain('no');
expect(visited).not.toContain('yes');
});
});
describe('switch 真輸出 → 引擎走對邊(多路+default,leo:「switch 更嚴重」)', () => {
// 真跑(三個 case + default_branch):
// value="active" → {"data":{"branch":"branch_active"},"success":true}
// value="pending" → {"data":{"branch":"branch_pending"},"success":true}
// value="zzz" → {"data":{"branch":"branch_default"},"success":true}
const REAL_CASE1 = { data: { branch: 'branch_active' }, success: true };
const REAL_CASE3 = { data: { branch: 'branch_pending' }, success: true };
const REAL_DEFAULT = { data: { branch: 'branch_default' }, success: true };
const targets = ['p_active', 'p_inactive', 'p_pending', 'p_default'];
const edges = [
{ from: 'ctrl', to: 'p_active', type: 'ON_BRANCH', branch: 'branch_active' },
{ from: 'ctrl', to: 'p_inactive', type: 'ON_BRANCH', branch: 'branch_inactive' },
{ from: 'ctrl', to: 'p_pending', type: 'ON_BRANCH', branch: 'branch_pending' },
{ from: 'ctrl', to: 'p_default', type: 'ON_BRANCH', branch: 'branch_default' },
];
it('第 1 條 case(真輸出 branch_active)→ 只走 p_active', async () => {
const { body, visited } = await run(graphWith(REAL_CASE1, edges, targets));
expect(body.success).toBe(true);
expect(visited).toContain('p_active');
expect(visited).not.toContain('p_inactive');
expect(visited).not.toContain('p_pending');
expect(visited).not.toContain('p_default');
});
it('第 3 條 case(真輸出 branch_pending)→ 只走 p_pending(證明第 N 條路走得對)', async () => {
const { visited } = await run(graphWith(REAL_CASE3, edges, targets));
expect(visited).toContain('p_pending');
expect(visited).not.toContain('p_active');
expect(visited).not.toContain('p_inactive');
expect(visited).not.toContain('p_default');
});
it('無匹配(真輸出 branch_default)→ 只走 p_default', async () => {
const { visited } = await run(graphWith(REAL_DEFAULT, edges, targets));
expect(visited).toContain('p_default');
expect(visited).not.toContain('p_active');
expect(visited).not.toContain('p_pending');
});
});
describe('try_catch 真輸出 → 引擎走對邊(okcatch 兩路都驗)', () => {
// 真跑:echo '{"result":{"value":42},"error":""}' | wasmtime try_catch/component.wasm
const REAL_TRY = { data: { branch: 'try', result: { value: 42 } }, success: true };
// 真跑:echo '{"result":null,"error":"boom"}' | wasmtime try_catch/component.wasm
const REAL_CATCH = { data: { branch: 'catch', error: 'boom' }, success: true };
const edges = [
{ from: 'ctrl', to: 'normal', type: 'ON_BRANCH', branch: 'try' },
{ from: 'ctrl', to: 'rescue', type: 'ON_BRANCH', branch: 'catch' },
];
it('成功(真輸出 branch="try")→ 走 normal,不走 rescue', async () => {
const { body, visited } = await run(graphWith(REAL_TRY, edges, ['normal', 'rescue']));
expect(body.success).toBe(true);
expect(visited).toContain('normal');
expect(visited).not.toContain('rescue');
});
it('失敗(真輸出 branch="catch")→ 走 rescue,不走 normal', async () => {
const { visited } = await run(graphWith(REAL_CATCH, edges, ['normal', 'rescue']));
expect(visited).toContain('rescue');
expect(visited).not.toContain('normal');
});
it('try_catch 的 catch 路承接了「上游失敗」——不必寫 code try 一遍', async () => {
// 這是 leo 點名 try_catch 的原因:schema 用文字寫「走 catch 分支」但機器層沒有那條路。
// 現在有了:catch 標籤 → ON_BRANCH branch="catch" → 補救節點。
const { visited } = await run(graphWith(REAL_CATCH, edges, ['normal', 'rescue']));
expect(visited).toContain('rescue');
});
});