Files
Arcrun/cypher-executor/tests/executor.test.ts
Claude 2707fca32b feat(arcrun): implement arcrun MVP — open-source AI workflow engine
Phase 1-5 complete per .agents/specs/u6u-core-mvp/:

**Phase 1 — Cherry-pick & cleanup**
- Create arcrun/ from cypher-executor, credentials, builtins, registry
- Remove 9 InkStone Service Bindings (KBDB, REGISTRY, CLINIC_*, AICEO, MINI_ME)
- Rewrite component-loader: 3-layer (builtin → WASM_BUCKET R2 → error)
- Remove autoPublishMissing.ts, proxy.ts (AICEO), execution-logger.ts (KBDB)
- Clean all KV namespace IDs and InkStone internal URLs from config files

**Phase 2 — contract.yaml completeness**
- Add credentials_required to gmail, google_sheets, telegram, line_notify
- Add config_example to all 21 components with annotated field descriptions

**Phase 3 — Credential injection**
- Add credential-injector.ts: AES-GCM decrypt from CREDENTIALS_KV
- Integrate into GraphExecutor before WASM execution
- Structured errors with repair instructions when credential missing

**Phase 4 — CLI (acr)**
- cli/package.json: arcrun package, bin: acr, deps: commander/js-yaml/chalk/ora
- 8 commands: init, creds push, push, run, validate, parts, list, logs
- Standard mode: writes directly to user's CF KV via CF REST API
- acr init: interactive setup with arcrun.dev API Key registration

**Phase 5 — Open source release prep**
- README.md: 5-minute quickstart, component table, workflow YAML syntax
- CONTRIBUTING.md: TinyGo dev env, component scaffolding, submission flow
- Security audit: no InkStone internal URLs/IDs in committed files
- .gitignore: exclude credentials.yaml, .wrangler, *.wasm

https://claude.ai/code/session_01BnCdSLVH8tUed9VrrPavgT
2026-04-16 04:06:25 +00:00

195 lines
6.4 KiB
TypeScript

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