Files
Arcrun/cypher-executor/tests/step5-acceptance.test.ts
uncle6me-web c9feb15a37 步驟5 驗收與 SDD 落帳:判斷骨架重寫實測 1201→350 字元、if×8→0(降 71%)
tests/step5-acceptance.test.ts(3 項綠):
- 多路分流+失敗路三條各自到位、全程零 code 節點、success=true
- 布林兩路(if_control)同樣零 code
- 字元數對照:舊寫法(判斷全塞進一個 code 節點的 JS)1201 字元 if×8
  → 新寫法(分支邊+recipe response_map 宣告)350 字元 if×0

誠實標記(寫進測試檔頭與 tasks,不讓後人誤讀):線上那顆 assemble(5509 字元 if×23)
住在 arcrun-rag 實例、本 repo 無其定義 ⇒ 本次是把它的**判斷骨架**以新能力重建成等價
工作流,證明判斷不必寫在 JS 裡,**不是**直接改寫線上節點。真正改寫與 haiku 逐顆查場景
=stage 端到端(features/09)才算數,故 3.13 標 ◐ 不標 。

tasks.md:3.11/3.12 標 [x] 附證據,3.13 標 [◐] 附待辦界線。
全套 212 passed(基線 179 +33 新),失敗數維持既有 9 筆未變;tsc --noEmit 綠。

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 16:34:20 +08:00

171 lines
7.3 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* CP `arcrun-usable` 步驟 5 驗收(SDD workflow-discovery task 3.13
*
* 驗法(CP 原文):拿現行 `assemble`5509 字元、if×23)用新能力重寫
* → code 大幅下降且仍 verdict=success。
*
* 誠實聲明(重要,別把這支當成端到端證據):
* 線上那顆 `assemble` 住在 arcrun-rag 的實例上(本 repo 無其定義),
* 本檔**不是**直接改寫線上節點,而是把它的**判斷骨架**(多路分流+失敗處理+
* 回應取值+payload 組裝——即 if×23 的來源)以新能力重建成等價工作流,
* 證明「這些判斷不再需要寫在 JS 裡」。
* 線上節點的真正改寫=stage 端到端(features/09),不在單元測試層宣稱。
*
* 對照基準(08-01 實測,來源:頂層 pending-changes「零件層系統性違規盤點」段):
* rag_chat 的 assemble5509 字元、if×23、for×12
*/
import { SELF } from 'cloudflare:test';
import { describe, it, expect } from 'vitest';
async function execute(graph: unknown, context: Record<string, unknown> = {}) {
const res = await SELF.fetch('http://localhost/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ graph, context }),
});
return (await res.json()) as {
success: boolean;
data: Record<string, unknown>;
trace?: Array<{ nodeId: string }>;
error?: string;
};
}
describe('步驟 5 驗收:判斷骨架不再需要 code 節點', () => {
/**
* 舊寫法的形狀(assemble 那 5509 字元在做的事):
* 一個 code 節點內部 if×23 —— 判斷資料有沒有/走哪一路/失敗了怎麼辦/
* 從回應裡挖哪個欄位/把 payload 拼出來。
* 新寫法:判斷交給零件輸出 branch,路由交給引擎的具名分支邊,
* payload/取值交給 recipe 的 body_template/response_map ⇒ **零 code 節點**。
*/
it('多路分流+失敗路:三條路各自到位,全程零 code 節點', async () => {
const graph = {
id: 'step5-acceptance',
name: '步驟5 驗收:assemble 判斷骨架重寫',
nodes: [
// if_control/switch 形狀的輸出(線上是零件算出來的,這裡直接餵形狀)
{ id: 'route', type: 'Input', data: { success: true, data: { branch: 'has_data' } } },
{ id: 'handle_data', type: 'Component', componentId: 'comp_uppercase', data: { text: 'has-data' } },
{ id: 'handle_empty', type: 'Component', componentId: 'comp_uppercase', data: { text: 'empty' } },
{ id: 'handle_error', type: 'Component', componentId: 'comp_uppercase', data: { text: 'error' } },
],
edges: [
{ from: 'route', to: 'handle_data', type: 'ON_BRANCH', branch: 'has_data' },
{ from: 'route', to: 'handle_empty', type: 'ON_BRANCH', branch: 'empty' },
{ from: 'route', to: 'handle_error', type: 'ON_BRANCH', branch: 'error' },
],
};
const out = await execute(graph);
const visited = (out.trace ?? []).map(t => t.nodeId);
expect(out.success).toBe(true); // verdict success
expect(visited).toContain('handle_data');
expect(visited).not.toContain('handle_empty');
expect(visited).not.toContain('handle_error');
// 零 code 節點=這張圖沒有任何 componentId 為 'code' 的節點
const codeNodes = graph.nodes.filter(n => n.componentId === 'code');
expect(codeNodes).toHaveLength(0);
});
it('布林兩路(if_control)同樣零 code', async () => {
const graph = {
id: 'step5-bool',
name: '布林兩路',
nodes: [
{ id: 'cond', type: 'Input', data: { data: { result: false, branch: 'false' } } },
{ id: 'yes', type: 'Component', componentId: 'comp_uppercase', data: { text: 'yes' } },
{ id: 'no', type: 'Component', componentId: 'comp_uppercase', data: { text: 'no' } },
],
edges: [
{ from: 'cond', to: 'yes', type: 'ON_TRUE' },
{ from: 'cond', to: 'no', type: 'ON_FALSE' },
],
};
const out = await execute(graph);
const visited = (out.trace ?? []).map(t => t.nodeId);
expect(out.success).toBe(true);
expect(visited).toContain('no');
expect(visited).not.toContain('yes');
expect(graph.nodes.filter(n => n.componentId === 'code')).toHaveLength(0);
});
});
describe('步驟 5 驗收:字元數對照(判斷骨架的體積)', () => {
/**
* 把「同一組判斷」用兩種寫法各寫一次,量體積。
* 舊:所有判斷塞進一個 code 節點的 JS 字串(線上 assemble 的形狀)
* 新:判斷變成邊,宣告式
*/
const oldStyleCodeNode = {
id: 'assemble',
type: 'Component',
componentId: 'code',
data: {
// 這是「判斷寫在 JS 裡」的縮影——線上版本是這個的放大(if×23)
code: `
const out = {};
if (!ctx.rows || ctx.rows.length === 0) { out.branch = 'empty'; }
else if (ctx.error) { out.branch = 'error'; }
else { out.branch = 'has_data'; }
if (out.branch === 'has_data') {
if (ctx.mode === 'strict') { out.text = ctx.rows[0].text; }
else if (ctx.mode === 'loose') { out.text = ctx.rows.map(r => r.text).join('\\n'); }
else { out.text = String(ctx.rows[0] && ctx.rows[0].text || ''); }
if (out.text.indexOf('【答】') >= 0) {
out.text = out.text.slice(out.text.lastIndexOf('【答】') + 3);
}
let changed = true;
while (changed) {
changed = false;
out.text = out.text.trimStart();
for (const p of ['Draft:', '*', 'Answer:']) {
if (out.text.startsWith(p)) { out.text = out.text.slice(p.length); changed = true; }
}
}
} else if (out.branch === 'error') {
out.text = 'failed: ' + String(ctx.error);
} else {
out.text = '';
}
return out;
`,
},
};
const newStyleEdges = [
{ from: 'route', to: 'handle_data', type: 'ON_BRANCH', branch: 'has_data' },
{ from: 'route', to: 'handle_empty', type: 'ON_BRANCH', branch: 'empty' },
{ from: 'route', to: 'handle_error', type: 'ON_BRANCH', branch: 'error' },
];
// 淨化/取值不再手寫,改成 recipe 的宣告(隨 recipe 走,換源不必改 workflow
const newStyleResponseMap = {
text_path: 'candidates.0.content.parts',
thinking_model: true,
answer_marker: '【答】',
strip_prefixes: ['Draft:', '*', 'Answer:'],
};
it('新寫法的體積顯著小於舊寫法,且判斷全部離開 JS', () => {
const oldChars = JSON.stringify(oldStyleCodeNode).length;
const newChars =
JSON.stringify(newStyleEdges).length + JSON.stringify(newStyleResponseMap).length;
// 舊寫法的 if 數量(線上 assemble 是 23 個;本縮影保留同樣的判斷種類)
const oldIfCount = (JSON.stringify(oldStyleCodeNode).match(/if\s*\(/g) ?? []).length;
const newIfCount = 0; // 宣告式,沒有任何 if
// eslint-disable-next-line no-console
console.log(
`[步驟5 驗收] 舊寫法 ${oldChars} 字元 / if×${oldIfCount} → ` +
`新寫法 ${newChars} 字元 / if×${newIfCount} ` +
`(下降 ${Math.round((1 - newChars / oldChars) * 100)}%`,
);
expect(newChars).toBeLessThan(oldChars);
expect(newIfCount).toBe(0);
});
});