import { ApiConfig, ExecutionStep, HttpMethod, Tool } from '@superglue/shared';
import { describe, expect, it } from 'vitest';
import { ToolExecutor } from './tool-executor.js';
describe('ToolExecutor - Failure Behavior', () => {
describe('Loop Steps with FAIL behavior (default)', () => {
it('should stop execution on first failure in loop', async () => {
const mockApiConfig: ApiConfig = {
id: 'test-api',
urlHost: 'https://api.example.com',
urlPath: '/test',
method: HttpMethod.GET,
instruction: 'Test endpoint that fails on second iteration',
};
const step: ExecutionStep = {
id: 'testStep',
apiConfig: mockApiConfig,
loopSelector: '(sourceData) => [1, 2, 3]', // Will try 3 iterations
failureBehavior: 'FAIL', // Explicit default
};
const tool: Tool = {
id: 'test-tool',
steps: [step],
finalTransform: '(sourceData) => sourceData',
integrationIds: [],
};
const executor = new ToolExecutor({
tool,
metadata: { traceId: 'test-trace', orgId: '' },
integrations: [],
});
// Mock the strategy registry to fail on second iteration
let callCount = 0;
(executor as any).strategyRegistry = {
routeAndExecute: async () => {
callCount++;
if (callCount === 2) {
return { success: false, error: 'Simulated failure on iteration 2' };
}
return { success: true, strategyExecutionData: { result: `iteration ${callCount}` } };
},
};
const result = await executor.execute({
payload: {},
credentials: {},
options: {},
});
expect(result.success).toBe(false);
expect(result.error).toContain('Simulated failure');
expect(callCount).toBe(2); // Should only reach 2nd iteration before failing
});
});
describe('Loop Steps with CONTINUE behavior', () => {
it('should continue execution through all iterations despite failures', async () => {
const mockApiConfig: ApiConfig = {
id: 'test-api',
urlHost: 'https://api.example.com',
urlPath: '/test',
method: HttpMethod.GET,
instruction: 'Test endpoint that fails on some iterations',
};
const step: ExecutionStep = {
id: 'testStep',
apiConfig: mockApiConfig,
loopSelector: '(sourceData) => [1, 2, 3, 4, 5]',
failureBehavior: 'CONTINUE',
};
const tool: Tool = {
id: 'test-tool',
steps: [step],
finalTransform: '(sourceData) => sourceData',
integrationIds: [],
};
const executor = new ToolExecutor({
tool,
metadata: { traceId: 'test-trace', orgId: '' },
integrations: [],
});
// Mock the strategy registry to fail on iterations 2 and 4
let callCount = 0;
(executor as any).strategyRegistry = {
routeAndExecute: async () => {
callCount++;
if (callCount === 2 || callCount === 4) {
return { success: false, error: `Simulated failure on iteration ${callCount}` };
}
return { success: true, strategyExecutionData: { result: `success on iteration ${callCount}` } };
},
};
const result = await executor.execute({
payload: {},
credentials: {},
options: {},
});
// Workflow should succeed overall
expect(result.success).toBe(true);
// Step should succeed
const stepResult = result.stepResults[0];
expect(stepResult.success).toBe(true);
// All 5 iterations should be executed
expect(callCount).toBe(5);
// Check result structure - data should be array of objects with currentItem, data, and success
expect(Array.isArray(stepResult.data)).toBe(true);
expect(stepResult.data).toHaveLength(5);
// Verify successful iterations
expect(stepResult.data[0].success).toBe(true);
expect(stepResult.data[0].data).toEqual({ result: 'success on iteration 1' });
expect(stepResult.data[2].success).toBe(true);
expect(stepResult.data[2].data).toEqual({ result: 'success on iteration 3' });
expect(stepResult.data[4].success).toBe(true);
expect(stepResult.data[4].data).toEqual({ result: 'success on iteration 5' });
// Verify failed iterations
expect(stepResult.data[1].success).toBe(false);
expect(stepResult.data[1].data).toBe(null);
expect(stepResult.data[1].error).toContain('Simulated failure on iteration 2');
expect(stepResult.data[3].success).toBe(false);
expect(stepResult.data[3].data).toBe(null);
expect(stepResult.data[3].error).toContain('Simulated failure on iteration 4');
});
it('should include currentItem in both successful and failed iterations', async () => {
const mockApiConfig: ApiConfig = {
id: 'test-api',
urlHost: 'https://api.example.com',
urlPath: '/test',
method: HttpMethod.GET,
instruction: 'Test endpoint',
};
const step: ExecutionStep = {
id: 'testStep',
apiConfig: mockApiConfig,
loopSelector: '(sourceData) => [{id: 1, name: "Alice"}, {id: 2, name: "Bob"}, {id: 3, name: "Charlie"}]',
failureBehavior: 'CONTINUE',
};
const tool: Tool = {
id: 'test-tool',
steps: [step],
finalTransform: '(sourceData) => sourceData',
integrationIds: [],
};
const executor = new ToolExecutor({
tool,
metadata: { traceId: 'test-trace', orgId: '' },
integrations: [],
});
let callCount = 0;
(executor as any).strategyRegistry = {
routeAndExecute: async () => {
callCount++;
if (callCount === 2) {
return { success: false, error: 'Bob failed' };
}
return { success: true, strategyExecutionData: { processed: true } };
},
};
const result = await executor.execute({
payload: {},
credentials: {},
options: {},
});
expect(result.success).toBe(true);
const stepResult = result.stepResults[0];
// Check currentItem is preserved for all iterations
expect(stepResult.data[0].currentItem).toEqual({id: 1, name: "Alice"});
expect(stepResult.data[0].success).toBe(true);
expect(stepResult.data[1].currentItem).toEqual({id: 2, name: "Bob"});
expect(stepResult.data[1].success).toBe(false);
expect(stepResult.data[1].error).toContain('Bob failed');
expect(stepResult.data[2].currentItem).toEqual({id: 3, name: "Charlie"});
expect(stepResult.data[2].success).toBe(true);
});
});
describe('Direct Steps with CONTINUE behavior', () => {
it('should mark step as successful even when execution fails', async () => {
const mockApiConfig: ApiConfig = {
id: 'test-api',
urlHost: 'https://api.example.com',
urlPath: '/test',
method: HttpMethod.GET,
instruction: 'Test endpoint that will fail',
};
const step: ExecutionStep = {
id: 'testStep',
apiConfig: mockApiConfig,
loopSelector: '(sourceData) => ({})', // Returns object for direct execution
failureBehavior: 'CONTINUE',
};
const tool: Tool = {
id: 'test-tool',
steps: [step],
finalTransform: '(sourceData) => sourceData',
integrationIds: [],
};
const executor = new ToolExecutor({
tool,
metadata: { traceId: 'test-trace', orgId: '' },
integrations: [],
});
(executor as any).strategyRegistry = {
routeAndExecute: async () => {
return { success: false, error: 'Direct execution failed' };
},
};
const result = await executor.execute({
payload: {},
credentials: {},
options: {},
});
// Workflow should succeed overall
expect(result.success).toBe(true);
// Step should succeed
const stepResult = result.stepResults[0];
expect(stepResult.success).toBe(true);
// But data should indicate the failure
expect(stepResult.data.success).toBe(false);
expect(stepResult.data.data).toBe(null);
expect(stepResult.data.error).toContain('Direct execution failed');
});
});
describe('Multi-step workflows with mixed failure behaviors', () => {
it('should continue workflow when first step has CONTINUE behavior and fails', async () => {
const mockApiConfig1: ApiConfig = {
id: 'test-api-1',
urlHost: 'https://api.example.com',
urlPath: '/test1',
method: HttpMethod.GET,
instruction: 'First step that may fail',
};
const mockApiConfig2: ApiConfig = {
id: 'test-api-2',
urlHost: 'https://api.example.com',
urlPath: '/test2',
method: HttpMethod.GET,
instruction: 'Second step',
};
const step1: ExecutionStep = {
id: 'step1',
apiConfig: mockApiConfig1,
loopSelector: '(sourceData) => ({})',
failureBehavior: 'CONTINUE',
};
const step2: ExecutionStep = {
id: 'step2',
apiConfig: mockApiConfig2,
loopSelector: '(sourceData) => ({})',
failureBehavior: 'FAIL', // Default behavior
};
const tool: Tool = {
id: 'test-tool',
steps: [step1, step2],
finalTransform: '(sourceData) => sourceData',
integrationIds: [],
};
const executor = new ToolExecutor({
tool,
metadata: { traceId: 'test-trace', orgId: '' },
integrations: [],
});
let stepExecuted = 0;
(executor as any).strategyRegistry = {
routeAndExecute: async () => {
stepExecuted++;
if (stepExecuted === 1) {
return { success: false, error: 'Step 1 failed' };
}
return { success: true, strategyExecutionData: { result: 'step 2 success' } };
},
};
const result = await executor.execute({
payload: {},
credentials: {},
options: {},
});
// Workflow should succeed
expect(result.success).toBe(true);
// Both steps should have executed
expect(stepExecuted).toBe(2);
expect(result.stepResults).toHaveLength(2);
// Step 1 should succeed at step level but show failure in data
expect(result.stepResults[0].success).toBe(true);
expect(result.stepResults[0].data.success).toBe(false);
// Step 2 should succeed normally
expect(result.stepResults[1].success).toBe(true);
expect(result.stepResults[1].data.success).toBe(true);
});
it('should stop workflow when step with FAIL behavior fails', async () => {
const mockApiConfig1: ApiConfig = {
id: 'test-api-1',
urlHost: 'https://api.example.com',
urlPath: '/test1',
method: HttpMethod.GET,
instruction: 'First step',
};
const mockApiConfig2: ApiConfig = {
id: 'test-api-2',
urlHost: 'https://api.example.com',
urlPath: '/test2',
method: HttpMethod.GET,
instruction: 'Second step that will fail',
};
const step1: ExecutionStep = {
id: 'step1',
apiConfig: mockApiConfig1,
loopSelector: '(sourceData) => ({})',
failureBehavior: 'CONTINUE',
};
const step2: ExecutionStep = {
id: 'step2',
apiConfig: mockApiConfig2,
loopSelector: '(sourceData) => ({})',
failureBehavior: 'FAIL',
};
const tool: Tool = {
id: 'test-tool',
steps: [step1, step2],
finalTransform: '(sourceData) => sourceData',
integrationIds: [],
};
const executor = new ToolExecutor({
tool,
metadata: { traceId: 'test-trace', orgId: '' },
integrations: [],
});
let stepExecuted = 0;
(executor as any).strategyRegistry = {
routeAndExecute: async () => {
stepExecuted++;
if (stepExecuted === 2) {
return { success: false, error: 'Step 2 failed' };
}
return { success: true, strategyExecutionData: { result: 'success' } };
},
};
const result = await executor.execute({
payload: {},
credentials: {},
options: {},
});
// Workflow should fail
expect(result.success).toBe(false);
expect(result.error).toContain('Step 2 failed');
// Both steps should have executed
expect(stepExecuted).toBe(2);
expect(result.stepResults).toHaveLength(2);
});
});
describe('Empty loops with CONTINUE behavior', () => {
it('should handle empty loop arrays gracefully', async () => {
const mockApiConfig: ApiConfig = {
id: 'test-api',
urlHost: 'https://api.example.com',
urlPath: '/test',
method: HttpMethod.GET,
instruction: 'Test endpoint',
};
const step: ExecutionStep = {
id: 'testStep',
apiConfig: mockApiConfig,
loopSelector: '(sourceData) => []', // Empty array
failureBehavior: 'CONTINUE',
};
const tool: Tool = {
id: 'test-tool',
steps: [step],
finalTransform: '(sourceData) => sourceData',
integrationIds: [],
};
const executor = new ToolExecutor({
tool,
metadata: { traceId: 'test-trace', orgId: '' },
integrations: [],
});
let callCount = 0;
(executor as any).strategyRegistry = {
routeAndExecute: async () => {
callCount++;
return { success: true, strategyExecutionData: { result: 'success' } };
},
};
const result = await executor.execute({
payload: {},
credentials: {},
options: {},
});
expect(result.success).toBe(true);
expect(callCount).toBe(0); // No iterations should execute
const stepResult = result.stepResults[0];
expect(stepResult.success).toBe(true);
expect(stepResult.data).toEqual([]);
});
});
});