Files
Arcrun/cypher-executor/tests/executor.test.ts
T
uncle6me-web 0860e84d22 feat(t135): 庫目錄可自主移除+標示還在不在同步
leo:「需要加移除按鈕。因為別人裝錯我沒辦法幫他弄,需要可以自主」
+「你應該要顯示這個庫沒有本地對應的 folder,那就不容易刪錯」
(兩態非三態——leo 二修:「分兩種沒意義」,那是內部狀態不是用戶分類)。
- DELETE /portal/admin/libraries/:id(登記簿)與 by-name/:name(auto 庫需輸入庫名確認)
- kbdb 加 deprecate-by-library(auto 庫移除=標 deprecated,資料保留可還原)
- daemon/libraries 存 active 清單 → 卡片標 🟢同步中/灰目前沒有在同步
- 不自動刪(daemon 可能沒開機);daemon 從未回報時整列不標
vitest 24 passed(1 紅=console HTML 搬遷陳舊測試,非本案)。
(實作=子 CC;驗證+commit=總管。含 t116/t117 先前未 commit 的 graph-executor/wasi-shim 修正)
2026-07-29 14:45:44 +08:00

253 lines
8.6 KiB
TypeScript
Raw 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.
// Cypher Executor 端到端測試
import { SELF } from 'cloudflare:test';
import { describe, it, expect } from 'vitest';
import { GraphExecutor } from '../src/graph-executor';
import type { ComponentRunner, ExecutionGraph } from '../src/types';
describe('GET /', () => {
it('回傳服務狀態', async () => {
const res = await SELF.fetch('http://localhost/');
const data = await res.json() as Record<string, unknown>;
expect(res.status).toBe(200);
expect(data.service).toBe('arcrun-cypher-executor');
});
});
describe('POST /validate', () => {
it('驗證合法的圖定義', async () => {
const res = await SELF.fetch('http://localhost/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: 'test-graph',
name: '測試圖',
nodes: [
{ id: 'n1', type: 'Input' },
{ id: 'n2', type: 'Output' },
],
edges: [
{ from: 'n1', to: 'n2', type: 'PIPE' },
],
}),
});
const data = await res.json() as { valid: boolean };
expect(res.status).toBe(200);
expect(data.valid).toBe(true);
});
it('偵測無效邊', async () => {
const res = await SELF.fetch('http://localhost/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: 'bad-graph',
name: '壞圖',
nodes: [{ id: 'n1', type: 'Input' }],
edges: [{ from: 'n1', to: 'n999', type: 'PIPE' }],
}),
});
expect(res.status).toBe(400);
});
});
describe('POST /execute', () => {
it('PIPE 鏈: Input → passthrough → Output', async () => {
const res = await SELF.fetch('http://localhost/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
graph: {
id: 'g1',
name: 'PIPE 測試',
nodes: [
{ id: 'input', type: 'Input', data: { message: 'hello' } },
{ id: 'pass', type: 'Component', componentId: 'comp_passthrough' },
{ id: 'output', type: 'Output' },
],
edges: [
{ from: 'input', to: 'pass', type: 'PIPE' },
{ from: 'pass', to: 'output', type: 'PIPE' },
],
},
context: {},
}),
});
const data = await res.json() as { success: boolean; data: { message: string }; trace: unknown[] };
expect(res.status).toBe(200);
expect(data.success).toBe(true);
expect(data.data.message).toBe('hello');
expect(data.trace.length).toBeGreaterThanOrEqual(3);
});
it('Component 執行: uppercase 轉換', async () => {
const res = await SELF.fetch('http://localhost/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
graph: {
id: 'g2',
name: 'uppercase 測試',
nodes: [
{ id: 'input', type: 'Input', data: { text: 'hello world' } },
{ id: 'upper', type: 'Component', componentId: 'comp_uppercase' },
{ id: 'output', type: 'Output' },
],
edges: [
{ from: 'input', to: 'upper', type: 'PIPE' },
{ from: 'upper', to: 'output', type: 'PIPE' },
],
},
context: {},
}),
});
const data = await res.json() as { success: boolean; data: { text: string } };
expect(data.success).toBe(true);
expect(data.data.text).toBe('HELLO WORLD');
});
it('PIPE 鏈: 多層 counter 累加', async () => {
const res = await SELF.fetch('http://localhost/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
graph: {
id: 'g3',
name: 'counter 測試',
nodes: [
{ id: 'input', type: 'Input', data: { count: 0 } },
{ id: 'c1', type: 'Component', componentId: 'comp_counter' },
{ id: 'c2', type: 'Component', componentId: 'comp_counter' },
{ id: 'c3', type: 'Component', componentId: 'comp_counter' },
{ id: 'output', type: 'Output' },
],
edges: [
{ from: 'input', to: 'c1', type: 'PIPE' },
{ from: 'c1', to: 'c2', type: 'PIPE' },
{ from: 'c2', to: 'c3', type: 'PIPE' },
{ from: 'c3', to: 'output', type: 'PIPE' },
],
},
context: {},
}),
});
const data = await res.json() as { success: boolean; data: { count: number } };
expect(data.success).toBe(true);
expect(data.data.count).toBe(3);
});
it('IF 條件分支', async () => {
const res = await SELF.fetch('http://localhost/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
graph: {
id: 'g4',
name: 'IF 測試',
nodes: [
{ id: 'input', type: 'Input', data: { valid: true, text: 'go' } },
{ id: 'upper', type: 'Component', componentId: 'comp_uppercase' },
{ id: 'output', type: 'Output' },
],
edges: [
{ from: 'input', to: 'upper', type: 'IF', condition: 'result.valid === true' },
{ from: 'upper', to: 'output', type: 'PIPE' },
],
},
context: {},
}),
});
const data = await res.json() as { success: boolean; data: { text: string } };
expect(data.success).toBe(true);
expect(data.data.text).toBe('GO');
});
it('不存在的零件回傳失敗', async () => {
const res = await SELF.fetch('http://localhost/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
graph: {
id: 'g5',
name: '失敗測試',
nodes: [
{ id: 'input', type: 'Input', data: {} },
{ id: 'bad', type: 'Component', componentId: 'comp_not_exist' },
],
edges: [
{ from: 'input', to: 'bad', type: 'PIPE' },
],
},
context: {},
}),
});
const data = await res.json() as { success: boolean; error: string };
expect(data.success).toBe(false);
expect(data.error).toContain('不存在');
});
it('缺少必填欄位回傳 400', async () => {
const res = await SELF.fetch('http://localhost/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ graph: { id: 'x' } }),
});
expect(res.status).toBe(400);
});
});
// t117: FOREACH 全部項目失敗 → 錯誤訊息含 status codeGraphExecutor 單元測試)
describe('t117: FOREACH 全項失敗 → ExecutionError 含 status code', () => {
it('FOREACH 所有項目 success:false(含 status 401)→ executor.execute() 拋出含 "401" 的錯誤', async () => {
// mock loader:任何零件都回 {success:false, status:401, error:"HTTP 401"}
const failLoader = async (_: string): Promise<ComponentRunner> =>
async () => ({ success: false, status: 401, error: 'HTTP 401', data: { body: 'Unauthorized' } });
const executor = new GraphExecutor(failLoader);
const graph: ExecutionGraph = {
id: 'foreach-fail-t117',
name: 'FOREACH 全失敗',
nodes: [
{ id: 'input', type: 'Input', data: { items: ['a', 'b'] } },
{ id: 'writer', type: 'Component', componentId: 'http_request' },
],
edges: [
{ from: 'input', to: 'writer', type: 'FOREACH', iterator: 'item' },
],
};
// t117 核心驗證:全部失敗 → throw(不再靜默)
await expect(executor.execute(graph, {})).rejects.toThrow(/401/);
});
it('FOREACH 部分項目成功 → 不拋出(只有全部失敗才報錯)', async () => {
let callCount = 0;
// 第一次呼叫失敗,第二次成功(部分失敗不觸發 t117 all-fail 路徑)
const mixedLoader = async (_: string): Promise<ComponentRunner> =>
async () => {
callCount++;
if (callCount === 1) return { success: false, status: 401, error: 'HTTP 401' };
return { success: true, data: { ok: true } };
};
const executor = new GraphExecutor(mixedLoader);
const graph: ExecutionGraph = {
id: 'foreach-mixed-t117',
name: 'FOREACH 部分失敗',
nodes: [
{ id: 'input', type: 'Input', data: { items: ['a', 'b'] } },
{ id: 'writer', type: 'Component', componentId: 'http_request' },
],
edges: [
{ from: 'input', to: 'writer', type: 'FOREACH', iterator: 'item' },
],
};
// 部分失敗 → 不拋出,正常回傳 results 陣列
const result = await executor.execute(graph, {});
expect(result).toBeDefined();
});
});