Skip to content

Commit 96567d8

Browse files
os-zhuangclaude
andcommitted
test(runtime): cover MCP run_action flow dispatch via automation runner
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 85d2d21 commit 96567d8

1 file changed

Lines changed: 59 additions & 0 deletions

File tree

packages/runtime/src/http-dispatcher.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2120,4 +2120,63 @@ describe('HttpDispatcher — MCP action bridge (list_actions / run_action)', ()
21202120
const { bridge } = makeBridge({ userId: 'u1', systemPermissions: [] });
21212121
await expect(bridge.runAction('defer_task', { recordId: 't1' })).rejects.toThrow(/cannot be invoked/i);
21222122
});
2123+
2124+
// ── flow dispatch (type:'flow' → automation flow runner) ──
2125+
const flowAction = {
2126+
name: 'escalate_ticket',
2127+
label: 'Escalate',
2128+
objectName: 'todo_task',
2129+
type: 'flow',
2130+
target: 'escalation_flow',
2131+
locations: ['record_header'],
2132+
};
2133+
const makeFlowBridge = (execCtx: any, automation: any) => {
2134+
const flowObject = { ...{ name: 'todo_task', label: 'Task', fields: {} }, actions: [flowAction] };
2135+
const ql: any = {
2136+
executeAction: vi.fn(),
2137+
registry: { getObject: () => flowObject },
2138+
find: vi.fn(async () => []),
2139+
insert: vi.fn(),
2140+
update: vi.fn(),
2141+
delete: vi.fn(),
2142+
};
2143+
const metadata: any = {
2144+
listObjects: vi.fn(async () => [flowObject]),
2145+
getObject: vi.fn(async () => flowObject),
2146+
};
2147+
const kernel: any = {
2148+
context: {
2149+
getService: (n: string) =>
2150+
n === 'objectql' || n === 'data' ? ql : n === 'metadata' ? metadata : n === 'automation' ? automation : null,
2151+
},
2152+
};
2153+
const dispatcher = new HttpDispatcher(kernel);
2154+
const ctx: any = { request: {}, environmentId: 'platform', executionContext: execCtx };
2155+
return { bridge: (dispatcher as any).buildMcpBridge(ctx), ql };
2156+
};
2157+
2158+
it('list_actions includes a flow action only when an automation service is present', async () => {
2159+
const withAuto = makeFlowBridge({ userId: 'u1', systemPermissions: [] }, { execute: vi.fn() });
2160+
expect((await withAuto.bridge.listActions()).map((a: any) => a.name)).toContain('escalate_ticket');
2161+
const noAuto = makeFlowBridge({ userId: 'u1', systemPermissions: [] }, null);
2162+
expect((await noAuto.bridge.listActions()).map((a: any) => a.name)).not.toContain('escalate_ticket');
2163+
});
2164+
2165+
it('run_action dispatches a flow action through the automation flow runner', async () => {
2166+
const execute = vi.fn(async () => ({ success: true, output: { escalated: true } }));
2167+
const { bridge, ql } = makeFlowBridge({ userId: 'u1', systemPermissions: [] }, { execute });
2168+
const res = await bridge.runAction('escalate_ticket', { recordId: 't1', params: { reason: 'sla' } });
2169+
expect(res.ok).toBe(true);
2170+
expect(ql.executeAction).not.toHaveBeenCalled(); // flow path, not executeAction
2171+
expect(execute).toHaveBeenCalledWith(
2172+
'escalation_flow',
2173+
expect.objectContaining({ triggerData: expect.objectContaining({ action: 'escalate_ticket', params: { reason: 'sla' } }) }),
2174+
);
2175+
});
2176+
2177+
it('run_action surfaces a flow failure as an error', async () => {
2178+
const execute = vi.fn(async () => ({ success: false, error: 'boom' }));
2179+
const { bridge } = makeFlowBridge({ userId: 'u1', systemPermissions: [] }, { execute });
2180+
await expect(bridge.runAction('escalate_ticket', {})).rejects.toThrow(/boom/i);
2181+
});
21232182
});

0 commit comments

Comments
 (0)