// 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; 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); }); });