Files
Arcrun/cypher-executor/tests/query-trigger.test.ts
Leo 313aeb13bf feat(cypher-executor): 同步查詢 trigger + graph_neighbors 查詢面 workflow 示範
Part 1(框架,cypher-executor webhooks-named.ts):加同步查詢 trigger。
現有 named webhook /trigger 回 {success,data,trace,duration_ms} 信封、只有 POST;
查詢面(console/MCP 打 graph neighbors/traverse)要 GET + 直接拿最終節點輸出當 response。
新端點同步 await 執行 workflow graph → 回 result.data(最終節點輸出)本身當 body(非 202):
  - GET  /q/:ns/:name                    (namespace 走 path,input 走 query string)
  - GET  /webhooks/named/:name/query     (X-Arcrun-API-Key header,input 走 query string)
  - POST /webhooks/named/:name/query     (header,input 走 body)
  - POST /webhooks/named/:ns/:name/query (namespace 走 path,input 走 body)
認證沿用 X-Arcrun-API-Key。誠實(mindset §7):節點失敗回 error+trace(500,非假綠);
paused 工作流無法同步回答 → 409 明講;輸出 5 MiB 硬上限(超過 413);duration 走 header 不污染 body。

Part 2(A 類 workflow.yaml):registry/examples/graph-neighbors/。
http_request 打 base custom domain kbdb.finally.click(避 CF 1042)撈 triplet records
→ code 零件記憶體 BFS(對照 kbdb-graph-plugin graph-traverse.ts)→ 同步回鄰居。
把 graph plugin 內建 GET /graph/neighbors 泛化成查詢面 workflow 的示範。

測試:cypher-executor/tests/query-trigger.test.ts(7 測,全綠)——同步回輸出(非 202)、
GET/POST × header/path 四端點、節點失敗回錯+trace、缺 key 401、不存在 404。
用內建 comp_uppercase(純記憶體)證明 Part 1 同步 trigger 機制本身可用。

待驗:graph_neighbors 需 code 零件部署 leo21c 後才能 live 端到端(另線處理);
triplet template id 上線前對一次(workflow 已參數化未寫死)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015d5jDbuqT5Htwv3Q88XXKk
2026-07-07 08:16:17 +00:00

133 lines
5.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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.
/**
* 同步查詢 triggersync query)測試 — webhooks-named.ts 的 /query 與 /q/* 端點。
*
* 驗證重點(對應交辦 Part 1):
* 1. 同步回「最終節點輸出」本身(非 202、非 {success,data,...} 信封)— GET(query string) 與 POST(body) 皆是。
* 2. namespace 走 path 的公開形態(/q/:ns/:name、POST /webhooks/named/:ns/:name/query)。
* 3. 節點失敗 → 誠實回錯誤 + trace(500),非假綠。
* 4. 認證:缺 X-Arcrun-API-Key → 401workflow 不存在 → 404。
*
* 用內建零件(comp_uppercase / comp_passthrough,純記憶體、無外部 fetch)當「極簡 workflow」,
* 因此本測試就是「Part 1 同步 trigger 機制本身可用」的證據(確實同步回值而非 202)。
*/
import { describe, it, expect, beforeAll } from 'vitest';
import { env, SELF } from 'cloudflare:test';
const API_KEY = 'test-tenant-query';
function kvKey(name: string, apiKey = API_KEY): string {
return `${apiKey}:wf:${name}`;
}
// 極簡 workflow:單一 comp_uppercase 節點。caller inputtext)→ 大寫 → 當最終輸出回。
// 無 Input/Output 節點:最終節點輸出即 comp_uppercase 的回傳,乾淨可斷言。
const UPPER_WF = {
name: 'q_upper',
graph: {
id: 'q_upper',
name: 'sync query upper',
nodes: [{ id: 'upper', type: 'Component', componentId: 'comp_uppercase' }],
edges: [],
},
description: '同步查詢:把 text 轉大寫回傳(測試用)',
created_at: new Date().toISOString(),
};
// 會失敗的 workflow:引用不存在的零件 → 節點執行失敗。
const FAIL_WF = {
name: 'q_fail',
graph: {
id: 'q_fail',
name: 'sync query fail',
nodes: [{ id: 'boom', type: 'Component', componentId: 'comp_does_not_exist' }],
edges: [],
},
description: '同步查詢:故意引用不存在零件(測試失敗路徑)',
created_at: new Date().toISOString(),
};
beforeAll(async () => {
await env.WEBHOOKS.put(kvKey(UPPER_WF.name), JSON.stringify(UPPER_WF));
await env.WEBHOOKS.put(kvKey(FAIL_WF.name), JSON.stringify(FAIL_WF));
});
describe('同步查詢 trigger — 成功回最終節點輸出(非 202、非信封)', () => {
it('POST /webhooks/named/:name/queryheader 認證,body input)同步回輸出', async () => {
const res = await SELF.fetch('http://localhost/webhooks/named/q_upper/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Arcrun-API-Key': API_KEY },
body: JSON.stringify({ text: 'hello' }),
});
expect(res.status).toBe(200); // 關鍵:非 202
const data = await res.json() as Record<string, unknown>;
// 回的是「最終節點輸出本身」:頂層直接有 text=HELLO,而非包在 { data: ... } 信封裡
expect(data.text).toBe('HELLO');
expect(data).not.toHaveProperty('duration_ms'); // 信封欄位不該出現在 body
// duration 走 header,不污染輸出
expect(res.headers.get('X-Arcrun-Duration-Ms')).not.toBeNull();
});
it('GET /webhooks/named/:name/queryheader 認證,query string input)同步回輸出', async () => {
const res = await SELF.fetch('http://localhost/webhooks/named/q_upper/query?text=world', {
headers: { 'X-Arcrun-API-Key': API_KEY },
});
expect(res.status).toBe(200);
const data = await res.json() as Record<string, unknown>;
expect(data.text).toBe('WORLD');
});
it('GET /q/:ns/:namenamespace 走 pathquery string input)同步回輸出', async () => {
const res = await SELF.fetch(`http://localhost/q/${API_KEY}/q_upper?text=abc`);
expect(res.status).toBe(200);
const data = await res.json() as Record<string, unknown>;
expect(data.text).toBe('ABC');
});
it('POST /webhooks/named/:ns/:name/querynamespace 走 pathbody input)同步回輸出', async () => {
const res = await SELF.fetch(`http://localhost/webhooks/named/${API_KEY}/q_upper/query`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: 'path' }),
});
expect(res.status).toBe(200);
const data = await res.json() as Record<string, unknown>;
expect(data.text).toBe('PATH');
});
});
describe('同步查詢 trigger — 誠實錯誤(不假綠)', () => {
it('節點失敗 → 500 + error + trace(非把錯誤當輸出)', async () => {
const res = await SELF.fetch('http://localhost/webhooks/named/q_fail/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Arcrun-API-Key': API_KEY },
body: JSON.stringify({}),
});
expect(res.status).toBe(500);
const data = await res.json() as { success: boolean; error: string; trace: unknown };
expect(data.success).toBe(false);
expect(typeof data.error).toBe('string');
expect(data.error.length).toBeGreaterThan(0);
expect(data.trace).toBeDefined();
});
});
describe('同步查詢 trigger — 認證與存在性', () => {
it('缺 X-Arcrun-API-Keyheader 形態)→ 401', async () => {
const res = await SELF.fetch('http://localhost/webhooks/named/q_upper/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: 'x' }),
});
expect(res.status).toBe(401);
});
it('workflow 不存在 → 404', async () => {
const res = await SELF.fetch('http://localhost/webhooks/named/no_such_wf/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Arcrun-API-Key': API_KEY },
body: JSON.stringify({}),
});
expect(res.status).toBe(404);
});
});