feat: Add AI Error Debugging using OpenAI (#8805)
This commit is contained in:
84
packages/cli/test/unit/services/ai.service.test.ts
Normal file
84
packages/cli/test/unit/services/ai.service.test.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { INode, INodeType } from 'n8n-workflow';
|
||||
import { ApplicationError, NodeOperationError } from 'n8n-workflow';
|
||||
import { AIService } from '@/services/ai.service';
|
||||
import config from '@/config';
|
||||
import { createDebugErrorPrompt } from '@/services/ai/prompts/debugError';
|
||||
|
||||
jest.mock('@/config', () => {
|
||||
return {
|
||||
getEnv: jest.fn().mockReturnValue('openai'),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@/services/ai/providers/openai', () => {
|
||||
return {
|
||||
AIProviderOpenAI: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
prompt: jest.fn(),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('AIService', () => {
|
||||
describe('constructor', () => {
|
||||
test('should throw if prompting with unknown provider type', async () => {
|
||||
jest.mocked(config).getEnv.mockReturnValue('unknown');
|
||||
const aiService = new AIService();
|
||||
|
||||
await expect(async () => await aiService.prompt([])).rejects.toThrow(ApplicationError);
|
||||
});
|
||||
|
||||
test('should throw if prompting with known provider type without api key', async () => {
|
||||
jest
|
||||
.mocked(config)
|
||||
.getEnv.mockImplementation((value) => (value === 'ai.openAIApiKey' ? '' : 'openai'));
|
||||
const aiService = new AIService();
|
||||
|
||||
await expect(async () => await aiService.prompt([])).rejects.toThrow(ApplicationError);
|
||||
});
|
||||
|
||||
test('should not throw if prompting with known provider type', () => {
|
||||
jest.mocked(config).getEnv.mockReturnValue('openai');
|
||||
const aiService = new AIService();
|
||||
|
||||
expect(async () => await aiService.prompt([])).not.toThrow(ApplicationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('prompt', () => {
|
||||
test('should call model.prompt', async () => {
|
||||
const service = new AIService();
|
||||
|
||||
await service.prompt(['message']);
|
||||
|
||||
expect(service.model.prompt).toHaveBeenCalledWith(['message']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('debugError', () => {
|
||||
test('should call prompt with error and nodeType', async () => {
|
||||
const service = new AIService();
|
||||
const promptSpy = jest.spyOn(service, 'prompt').mockResolvedValue('prompt');
|
||||
|
||||
const nodeType = {
|
||||
description: {
|
||||
displayName: 'Node Type',
|
||||
name: 'nodeType',
|
||||
properties: [],
|
||||
},
|
||||
} as unknown as INodeType;
|
||||
const error = new NodeOperationError(
|
||||
{
|
||||
type: 'n8n-nodes-base.error',
|
||||
typeVersion: 1,
|
||||
} as INode,
|
||||
'Error',
|
||||
);
|
||||
|
||||
await service.debugError(error, nodeType);
|
||||
|
||||
expect(promptSpy).toHaveBeenCalledWith(createDebugErrorPrompt(error, nodeType));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
import type { INodeProperties, INodePropertyCollection, INodePropertyOptions } from 'n8n-workflow';
|
||||
import {
|
||||
summarizeNodeTypeProperties,
|
||||
summarizeOption,
|
||||
summarizeProperty,
|
||||
} from '@/services/ai/utils/summarizeNodeTypeProperties';
|
||||
|
||||
describe('summarizeOption', () => {
|
||||
it('should return summarized option with value', () => {
|
||||
const option: INodePropertyOptions = {
|
||||
name: 'testOption',
|
||||
value: 'testValue',
|
||||
};
|
||||
|
||||
const result = summarizeOption(option);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'testOption',
|
||||
value: 'testValue',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return summarized option with values', () => {
|
||||
const option: INodePropertyCollection = {
|
||||
name: 'testOption',
|
||||
displayName: 'testDisplayName',
|
||||
values: [
|
||||
{
|
||||
name: 'testName',
|
||||
default: '',
|
||||
displayName: 'testDisplayName',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = summarizeOption(option);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'testOption',
|
||||
values: [
|
||||
{
|
||||
name: 'testDisplayName',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return summarized property', () => {
|
||||
const option: INodeProperties = {
|
||||
name: 'testName',
|
||||
default: '',
|
||||
displayName: 'testDisplayName',
|
||||
type: 'string',
|
||||
};
|
||||
|
||||
const result = summarizeOption(option);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'testDisplayName',
|
||||
type: 'string',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('summarizeProperty', () => {
|
||||
it('should return summarized property with displayOptions', () => {
|
||||
const property: INodeProperties = {
|
||||
default: '',
|
||||
name: 'testName',
|
||||
displayName: 'testDisplayName',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
testOption: ['testValue'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = summarizeProperty(property);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'testDisplayName',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
testOption: ['testValue'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return summarized property with options', () => {
|
||||
const property: INodeProperties = {
|
||||
name: 'testName',
|
||||
displayName: 'testDisplayName',
|
||||
default: '',
|
||||
type: 'string',
|
||||
options: [
|
||||
{
|
||||
name: 'testOption',
|
||||
value: 'testValue',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = summarizeProperty(property);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'testDisplayName',
|
||||
type: 'string',
|
||||
options: [
|
||||
{
|
||||
name: 'testOption',
|
||||
value: 'testValue',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return summarized property without displayOptions and options', () => {
|
||||
const property: INodeProperties = {
|
||||
name: 'testName',
|
||||
default: '',
|
||||
displayName: 'testDisplayName',
|
||||
type: 'string',
|
||||
};
|
||||
|
||||
const result = summarizeProperty(property);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'testDisplayName',
|
||||
type: 'string',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('summarizeNodeTypeProperties', () => {
|
||||
it('should return summarized properties', () => {
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
name: 'testName1',
|
||||
default: '',
|
||||
displayName: 'testDisplayName1',
|
||||
type: 'string',
|
||||
options: [
|
||||
{
|
||||
name: 'testOption1',
|
||||
value: 'testValue1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'testName2',
|
||||
default: '',
|
||||
displayName: 'testDisplayName2',
|
||||
type: 'number',
|
||||
options: [
|
||||
{
|
||||
name: 'testOption2',
|
||||
value: 'testValue2',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = summarizeNodeTypeProperties(properties);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'testDisplayName1',
|
||||
type: 'string',
|
||||
options: [
|
||||
{
|
||||
name: 'testOption1',
|
||||
value: 'testValue1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'testDisplayName2',
|
||||
type: 'number',
|
||||
options: [
|
||||
{
|
||||
name: 'testOption2',
|
||||
value: 'testValue2',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user