arcrun — AI workflow execution engine (clean history)
Self-hosted 開源:WASM 零件 + recipe + cypher-executor,跑在你自己的 Cloudflare。 此為重建的乾淨歷史起點(移除曾誤 commit 的 GCP SA 金鑰,舊歷史保留在 richblack/arcrun 與本地 backup 分支)。含: - acr init --self-hosted installer(建 KV/R2 + codeload 拉預編譯 wasm + wrangler deploy + seed recipe) - recipe push 把關(資料外流提醒 + 打通檢查) - 19 個正當零件預編譯 wasm(claude_api/km_writer/kbdb_upsert_block 排除:違反 DECISIONS §1) - CLI / cypher-executor / registry / 完整 SDD Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
// Cypher Executor 端到端測試
|
||||
import { SELF } from 'cloudflare:test';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user