mirror of
https://github.com/Nikhil-Doye/workflow-builder.git
synced 2026-07-22 02:01:56 +02:00
Add Jest configuration and testing utilities for improved test coverage
- Introduced a Jest configuration file to set up the testing environment with jsdom and TypeScript support. - Added setupTests.ts to mock localStorage and console methods, enhancing test reliability and reducing noise. - Implemented comprehensive test cases for llmProcessor and copilotService, ensuring robust functionality and error handling. - Created unit tests for variable substitution utility, validating correct behavior for various scenarios. - Enhanced database connection manager tests to cover connection handling and credential validation. - Established a workflow store test suite to validate workflow management and node/edge operations.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
module.exports = {
|
||||
testEnvironment: "jsdom",
|
||||
setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
|
||||
moduleNameMapping: {
|
||||
"^@/(.*)$": "<rootDir>/src/$1",
|
||||
},
|
||||
collectCoverageFrom: [
|
||||
"src/**/*.{ts,tsx}",
|
||||
"!src/**/*.d.ts",
|
||||
"!src/index.tsx",
|
||||
"!src/setupTests.ts",
|
||||
"!src/test-*.ts", // Exclude manual test files
|
||||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 70,
|
||||
functions: 70,
|
||||
lines: 70,
|
||||
statements: 70,
|
||||
},
|
||||
},
|
||||
testMatch: [
|
||||
"<rootDir>/src/**/__tests__/**/*.{ts,tsx}",
|
||||
"<rootDir>/src/**/*.{test,spec}.{ts,tsx}",
|
||||
],
|
||||
transform: {
|
||||
"^.+\\.(ts|tsx)$": "ts-jest",
|
||||
},
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json"],
|
||||
testPathIgnorePatterns: ["/node_modules/", "/build/"],
|
||||
collectCoverage: true,
|
||||
coverageDirectory: "coverage",
|
||||
coverageReporters: ["text", "lcov", "html"],
|
||||
};
|
||||
Generated
+1621
-170
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,8 @@
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"test:coverage": "react-scripts test --coverage --watchAll=false",
|
||||
"test:ci": "react-scripts test --coverage --watchAll=false --ci",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
@@ -54,5 +56,11 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"jest-environment-jsdom": "^30.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
import llmProcessor from '../../services/processors/llmProcessor';
|
||||
import { ExecutionContext, ExecutionPlan } from '../../services/executionEngine';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('../../services/openaiService', () => ({
|
||||
callOpenAI: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../utils/variableSubstitution', () => ({
|
||||
substituteVariables: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('LLM Processor', () => {
|
||||
let mockCallOpenAI: jest.MockedFunction<any>;
|
||||
let mockSubstituteVariables: jest.MockedFunction<any>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockCallOpenAI = require('../../services/openaiService').callOpenAI;
|
||||
mockSubstituteVariables = require('../../utils/variableSubstitution').substituteVariables;
|
||||
});
|
||||
|
||||
describe('Basic Processing', () => {
|
||||
it('should process text with basic prompt', async () => {
|
||||
const context: ExecutionContext = {
|
||||
nodeId: 'node-1',
|
||||
nodeType: 'llmTask',
|
||||
config: {
|
||||
prompt: 'Analyze this text: {{input}}',
|
||||
model: 'gpt-3.5-turbo',
|
||||
temperature: 0.7,
|
||||
},
|
||||
inputs: new Map([['input', 'Hello world']]),
|
||||
outputs: new Map(),
|
||||
status: 'pending',
|
||||
retryCount: 0,
|
||||
maxRetries: 3,
|
||||
};
|
||||
|
||||
const plan: ExecutionPlan = {
|
||||
id: 'plan-1',
|
||||
workflowId: 'workflow-1',
|
||||
nodes: [context],
|
||||
edges: [],
|
||||
executionMode: 'sequential',
|
||||
parallelGroups: [],
|
||||
conditions: new Map(),
|
||||
status: 'running',
|
||||
results: new Map(),
|
||||
errors: new Map(),
|
||||
};
|
||||
|
||||
mockCallOpenAI.mockResolvedValue({
|
||||
content: 'Analysis: The text "Hello world" is a common greeting.',
|
||||
usage: { total_tokens: 50 },
|
||||
});
|
||||
|
||||
mockSubstituteVariables.mockReturnValue('Analyze this text: Hello world');
|
||||
|
||||
const result = await llmProcessor(context, plan);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toBe('Analysis: The text "Hello world" is a common greeting.');
|
||||
expect(result.model).toBe('gpt-3.5-turbo');
|
||||
expect(mockCallOpenAI).toHaveBeenCalledWith(
|
||||
'Analyze this text: Hello world',
|
||||
{
|
||||
model: 'gpt-3.5-turbo',
|
||||
temperature: 0.7,
|
||||
maxTokens: 1000,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should use default model when not specified', async () => {
|
||||
const context: ExecutionContext = {
|
||||
nodeId: 'node-1',
|
||||
nodeType: 'llmTask',
|
||||
config: {
|
||||
prompt: 'Simple prompt',
|
||||
},
|
||||
inputs: new Map(),
|
||||
outputs: new Map(),
|
||||
status: 'pending',
|
||||
retryCount: 0,
|
||||
maxRetries: 3,
|
||||
};
|
||||
|
||||
const plan: ExecutionPlan = {
|
||||
id: 'plan-1',
|
||||
workflowId: 'workflow-1',
|
||||
nodes: [context],
|
||||
edges: [],
|
||||
executionMode: 'sequential',
|
||||
parallelGroups: [],
|
||||
conditions: new Map(),
|
||||
status: 'running',
|
||||
results: new Map(),
|
||||
errors: new Map(),
|
||||
};
|
||||
|
||||
mockCallOpenAI.mockResolvedValue({
|
||||
content: 'Response',
|
||||
usage: { total_tokens: 10 },
|
||||
});
|
||||
|
||||
mockSubstituteVariables.mockReturnValue('Simple prompt');
|
||||
|
||||
await llmProcessor(context, plan);
|
||||
|
||||
expect(mockCallOpenAI).toHaveBeenCalledWith(
|
||||
'Simple prompt',
|
||||
{
|
||||
model: 'gpt-3.5-turbo',
|
||||
temperature: 0.7,
|
||||
maxTokens: 1000,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should fall back to input data when no prompt provided', async () => {
|
||||
const context: ExecutionContext = {
|
||||
nodeId: 'node-1',
|
||||
nodeType: 'llmTask',
|
||||
config: {},
|
||||
inputs: new Map([['input', 'Fallback input data']]),
|
||||
outputs: new Map(),
|
||||
status: 'pending',
|
||||
retryCount: 0,
|
||||
maxRetries: 3,
|
||||
};
|
||||
|
||||
const plan: ExecutionPlan = {
|
||||
id: 'plan-1',
|
||||
workflowId: 'workflow-1',
|
||||
nodes: [context],
|
||||
edges: [],
|
||||
executionMode: 'sequential',
|
||||
parallelGroups: [],
|
||||
conditions: new Map(),
|
||||
status: 'running',
|
||||
results: new Map(),
|
||||
errors: new Map(),
|
||||
};
|
||||
|
||||
mockCallOpenAI.mockResolvedValue({
|
||||
content: 'Processed: Fallback input data',
|
||||
usage: { total_tokens: 20 },
|
||||
});
|
||||
|
||||
mockSubstituteVariables.mockReturnValue('Fallback input data');
|
||||
|
||||
const result = await llmProcessor(context, plan);
|
||||
|
||||
expect(result.input).toBe('Fallback input data');
|
||||
expect(result.output).toBe('Processed: Fallback input data');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Variable Substitution', () => {
|
||||
it('should apply variable substitution to prompts', async () => {
|
||||
const context: ExecutionContext = {
|
||||
nodeId: 'node-1',
|
||||
nodeType: 'llmTask',
|
||||
config: {
|
||||
prompt: 'Process {{node-0.output}} and {{node-1.data}}',
|
||||
},
|
||||
inputs: new Map(),
|
||||
outputs: new Map(),
|
||||
status: 'pending',
|
||||
retryCount: 0,
|
||||
maxRetries: 3,
|
||||
};
|
||||
|
||||
const plan: ExecutionPlan = {
|
||||
id: 'plan-1',
|
||||
workflowId: 'workflow-1',
|
||||
nodes: [context],
|
||||
edges: [],
|
||||
executionMode: 'sequential',
|
||||
parallelGroups: [],
|
||||
conditions: new Map(),
|
||||
status: 'running',
|
||||
results: new Map(),
|
||||
errors: new Map(),
|
||||
};
|
||||
|
||||
// Mock node label to ID mapping
|
||||
(plan as any).nodeLabelToId = new Map([
|
||||
['node-0', 'uuid-1'],
|
||||
['node-1', 'uuid-2'],
|
||||
]);
|
||||
|
||||
mockCallOpenAI.mockResolvedValue({
|
||||
content: 'Processed with variables',
|
||||
usage: { total_tokens: 30 },
|
||||
});
|
||||
|
||||
mockSubstituteVariables.mockReturnValue('Process data1 and data2');
|
||||
|
||||
await llmProcessor(context, plan);
|
||||
|
||||
expect(mockSubstituteVariables).toHaveBeenCalledWith(
|
||||
'Process {{node-0.output}} and {{node-1.data}}',
|
||||
expect.any(Map),
|
||||
expect.any(Map)
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip substitution when prompt has no variables', async () => {
|
||||
const context: ExecutionContext = {
|
||||
nodeId: 'node-1',
|
||||
nodeType: 'llmTask',
|
||||
config: {
|
||||
prompt: 'Simple prompt without variables',
|
||||
},
|
||||
inputs: new Map(),
|
||||
outputs: new Map(),
|
||||
status: 'pending',
|
||||
retryCount: 0,
|
||||
maxRetries: 3,
|
||||
};
|
||||
|
||||
const plan: ExecutionPlan = {
|
||||
id: 'plan-1',
|
||||
workflowId: 'workflow-1',
|
||||
nodes: [context],
|
||||
edges: [],
|
||||
executionMode: 'sequential',
|
||||
parallelGroups: [],
|
||||
conditions: new Map(),
|
||||
status: 'running',
|
||||
results: new Map(),
|
||||
errors: new Map(),
|
||||
};
|
||||
|
||||
mockCallOpenAI.mockResolvedValue({
|
||||
content: 'Response',
|
||||
usage: { total_tokens: 10 },
|
||||
});
|
||||
|
||||
mockSubstituteVariables.mockReturnValue('Simple prompt without variables');
|
||||
|
||||
await llmProcessor(context, plan);
|
||||
|
||||
expect(mockSubstituteVariables).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle OpenAI API errors', async () => {
|
||||
const context: ExecutionContext = {
|
||||
nodeId: 'node-1',
|
||||
nodeType: 'llmTask',
|
||||
config: {
|
||||
prompt: 'Test prompt',
|
||||
},
|
||||
inputs: new Map(),
|
||||
outputs: new Map(),
|
||||
status: 'pending',
|
||||
retryCount: 0,
|
||||
maxRetries: 3,
|
||||
};
|
||||
|
||||
const plan: ExecutionPlan = {
|
||||
id: 'plan-1',
|
||||
workflowId: 'workflow-1',
|
||||
nodes: [context],
|
||||
edges: [],
|
||||
executionMode: 'sequential',
|
||||
parallelGroups: [],
|
||||
conditions: new Map(),
|
||||
status: 'running',
|
||||
results: new Map(),
|
||||
errors: new Map(),
|
||||
};
|
||||
|
||||
mockCallOpenAI.mockRejectedValue(new Error('API rate limit exceeded'));
|
||||
mockSubstituteVariables.mockReturnValue('Test prompt');
|
||||
|
||||
await expect(llmProcessor(context, plan)).rejects.toThrow(
|
||||
'LLM processing failed: API rate limit exceeded'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle variable substitution errors', async () => {
|
||||
const context: ExecutionContext = {
|
||||
nodeId: 'node-1',
|
||||
nodeType: 'llmTask',
|
||||
config: {
|
||||
prompt: 'Process {{invalid.variable}}',
|
||||
},
|
||||
inputs: new Map(),
|
||||
outputs: new Map(),
|
||||
status: 'pending',
|
||||
retryCount: 0,
|
||||
maxRetries: 3,
|
||||
};
|
||||
|
||||
const plan: ExecutionPlan = {
|
||||
id: 'plan-1',
|
||||
workflowId: 'workflow-1',
|
||||
nodes: [context],
|
||||
edges: [],
|
||||
executionMode: 'sequential',
|
||||
parallelGroups: [],
|
||||
conditions: new Map(),
|
||||
status: 'running',
|
||||
results: new Map(),
|
||||
errors: new Map(),
|
||||
};
|
||||
|
||||
mockSubstituteVariables.mockImplementation(() => {
|
||||
throw new Error('Variable substitution failed');
|
||||
});
|
||||
|
||||
await expect(llmProcessor(context, plan)).rejects.toThrow(
|
||||
'LLM processing failed: Variable substitution failed'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty response from OpenAI', async () => {
|
||||
const context: ExecutionContext = {
|
||||
nodeId: 'node-1',
|
||||
nodeType: 'llmTask',
|
||||
config: {
|
||||
prompt: 'Test prompt',
|
||||
},
|
||||
inputs: new Map(),
|
||||
outputs: new Map(),
|
||||
status: 'pending',
|
||||
retryCount: 0,
|
||||
maxRetries: 3,
|
||||
};
|
||||
|
||||
const plan: ExecutionPlan = {
|
||||
id: 'plan-1',
|
||||
workflowId: 'workflow-1',
|
||||
nodes: [context],
|
||||
edges: [],
|
||||
executionMode: 'sequential',
|
||||
parallelGroups: [],
|
||||
conditions: new Map(),
|
||||
status: 'running',
|
||||
results: new Map(),
|
||||
errors: new Map(),
|
||||
};
|
||||
|
||||
mockCallOpenAI.mockResolvedValue({
|
||||
content: '',
|
||||
usage: { total_tokens: 0 },
|
||||
});
|
||||
|
||||
mockSubstituteVariables.mockReturnValue('Test prompt');
|
||||
|
||||
const result = await llmProcessor(context, plan);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Configuration Options', () => {
|
||||
it('should use custom temperature and max tokens', async () => {
|
||||
const context: ExecutionContext = {
|
||||
nodeId: 'node-1',
|
||||
nodeType: 'llmTask',
|
||||
config: {
|
||||
prompt: 'Test prompt',
|
||||
model: 'gpt-4',
|
||||
temperature: 0.9,
|
||||
maxTokens: 2000,
|
||||
},
|
||||
inputs: new Map(),
|
||||
outputs: new Map(),
|
||||
status: 'pending',
|
||||
retryCount: 0,
|
||||
maxRetries: 3,
|
||||
};
|
||||
|
||||
const plan: ExecutionPlan = {
|
||||
id: 'plan-1',
|
||||
workflowId: 'workflow-1',
|
||||
nodes: [context],
|
||||
edges: [],
|
||||
executionMode: 'sequential',
|
||||
parallelGroups: [],
|
||||
conditions: new Map(),
|
||||
status: 'running',
|
||||
results: new Map(),
|
||||
errors: new Map(),
|
||||
};
|
||||
|
||||
mockCallOpenAI.mockResolvedValue({
|
||||
content: 'Response',
|
||||
usage: { total_tokens: 100 },
|
||||
});
|
||||
|
||||
mockSubstituteVariables.mockReturnValue('Test prompt');
|
||||
|
||||
await llmProcessor(context, plan);
|
||||
|
||||
expect(mockCallOpenAI).toHaveBeenCalledWith(
|
||||
'Test prompt',
|
||||
{
|
||||
model: 'gpt-4',
|
||||
temperature: 0.9,
|
||||
maxTokens: 2000,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,331 @@
|
||||
import { copilotService } from "../../services/copilotService";
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock("../../services/openaiService", () => ({
|
||||
callOpenAI: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../services/agents/AgentManager", () => ({
|
||||
agentManager: {
|
||||
generateWorkflowFromDescription: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../utils/workflowGenerator", () => ({
|
||||
generateWorkflowStructure: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../utils/workflowValidator", () => ({
|
||||
validateWorkflowStructure: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("CopilotService", () => {
|
||||
let mockCallOpenAI: jest.MockedFunction<any>;
|
||||
let mockAgentManager: any;
|
||||
let mockGenerateWorkflowStructure: jest.MockedFunction<any>;
|
||||
let mockValidateWorkflowStructure: jest.MockedFunction<any>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockCallOpenAI = require("../../services/openaiService").callOpenAI;
|
||||
mockAgentManager =
|
||||
require("../../services/agents/AgentManager").agentManager;
|
||||
mockGenerateWorkflowStructure =
|
||||
require("../../utils/workflowGenerator").generateWorkflowStructure;
|
||||
mockValidateWorkflowStructure =
|
||||
require("../../utils/workflowValidator").validateWorkflowStructure;
|
||||
});
|
||||
|
||||
describe("parseUserIntent", () => {
|
||||
it("should parse user intent successfully", async () => {
|
||||
const mockResponse = {
|
||||
content: JSON.stringify({
|
||||
intent: "WEB_SCRAPING",
|
||||
entities: {
|
||||
urls: ["https://example.com"],
|
||||
dataTypes: ["html"],
|
||||
},
|
||||
workflowStructure: {
|
||||
nodes: [
|
||||
{ type: "dataInput", label: "URL Input" },
|
||||
{ type: "webScraping", label: "Web Scraper" },
|
||||
],
|
||||
edges: [{ source: "node-0", target: "node-1" }],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
mockCallOpenAI.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await copilotService.parseUserIntent(
|
||||
"Scrape data from https://example.com"
|
||||
);
|
||||
|
||||
expect(result.intent).toBe("WEB_SCRAPING");
|
||||
expect(result.entities.urls).toContain("https://example.com");
|
||||
expect(result.workflowStructure).toBeDefined();
|
||||
expect(mockCallOpenAI).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Scrape data from https://example.com"),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle parsing errors gracefully", async () => {
|
||||
mockCallOpenAI.mockRejectedValue(new Error("API Error"));
|
||||
|
||||
await expect(
|
||||
copilotService.parseUserIntent("Invalid input")
|
||||
).rejects.toThrow("API Error");
|
||||
});
|
||||
|
||||
it("should use cache for repeated requests", async () => {
|
||||
const mockResponse = {
|
||||
content: JSON.stringify({
|
||||
intent: "DATA_PROCESSING",
|
||||
entities: {},
|
||||
workflowStructure: null,
|
||||
}),
|
||||
};
|
||||
|
||||
mockCallOpenAI.mockResolvedValue(mockResponse);
|
||||
|
||||
// First call
|
||||
await copilotService.parseUserIntent("Process some data");
|
||||
expect(mockCallOpenAI).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Second call should use cache
|
||||
await copilotService.parseUserIntent("Process some data");
|
||||
expect(mockCallOpenAI).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateWorkflowSuggestions", () => {
|
||||
it("should generate workflow suggestions", async () => {
|
||||
const mockResponse = {
|
||||
content: JSON.stringify({
|
||||
suggestions: [
|
||||
"Add a data validation step",
|
||||
"Include error handling",
|
||||
"Add data transformation",
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
mockCallOpenAI.mockResolvedValue(mockResponse);
|
||||
|
||||
const suggestions = await copilotService.generateWorkflowSuggestions(
|
||||
"web scraping workflow",
|
||||
{ nodes: [], edges: [] }
|
||||
);
|
||||
|
||||
expect(suggestions).toHaveLength(3);
|
||||
expect(suggestions[0]).toBe("Add a data validation step");
|
||||
expect(mockCallOpenAI).toHaveBeenCalledWith(
|
||||
expect.stringContaining("web scraping workflow"),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle empty suggestions", async () => {
|
||||
const mockResponse = {
|
||||
content: JSON.stringify({
|
||||
suggestions: [],
|
||||
}),
|
||||
};
|
||||
|
||||
mockCallOpenAI.mockResolvedValue(mockResponse);
|
||||
|
||||
const suggestions = await copilotService.generateWorkflowSuggestions(
|
||||
"simple workflow",
|
||||
{ nodes: [], edges: [] }
|
||||
);
|
||||
|
||||
expect(suggestions).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateWorkflowFromDescription", () => {
|
||||
it("should generate workflow from description", async () => {
|
||||
const mockWorkflowStructure = {
|
||||
nodes: [
|
||||
{ type: "dataInput", label: "Input" },
|
||||
{ type: "llmTask", label: "Processor" },
|
||||
],
|
||||
edges: [{ source: "node-0", target: "node-1" }],
|
||||
};
|
||||
|
||||
mockAgentManager.generateWorkflowFromDescription.mockResolvedValue({
|
||||
intent: "AI_ANALYSIS",
|
||||
entities: {},
|
||||
workflowStructure: mockWorkflowStructure,
|
||||
});
|
||||
|
||||
const result = await copilotService.generateWorkflowFromDescription(
|
||||
"Analyze text data with AI"
|
||||
);
|
||||
|
||||
expect(result.intent).toBe("AI_ANALYSIS");
|
||||
expect(result.workflowStructure).toEqual(mockWorkflowStructure);
|
||||
expect(
|
||||
mockAgentManager.generateWorkflowFromDescription
|
||||
).toHaveBeenCalledWith("Analyze text data with AI");
|
||||
});
|
||||
|
||||
it("should handle generation errors", async () => {
|
||||
mockAgentManager.generateWorkflowFromDescription.mockRejectedValue(
|
||||
new Error("Generation failed")
|
||||
);
|
||||
|
||||
await expect(
|
||||
copilotService.generateWorkflowFromDescription("Invalid request")
|
||||
).rejects.toThrow("Generation failed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateWorkflow", () => {
|
||||
it("should validate workflow structure", async () => {
|
||||
const mockWorkflow = {
|
||||
nodes: [
|
||||
{ type: "dataInput", label: "Input" },
|
||||
{ type: "llmTask", label: "Processor" },
|
||||
],
|
||||
edges: [{ source: "node-0", target: "node-1" }],
|
||||
};
|
||||
|
||||
mockValidateWorkflowStructure.mockReturnValue({
|
||||
isValid: true,
|
||||
issues: [],
|
||||
suggestions: [],
|
||||
});
|
||||
|
||||
const result = await copilotService.validateWorkflow(mockWorkflow);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.issues).toHaveLength(0);
|
||||
expect(mockValidateWorkflowStructure).toHaveBeenCalledWith(mockWorkflow);
|
||||
});
|
||||
|
||||
it("should identify validation issues", async () => {
|
||||
const mockWorkflow = {
|
||||
nodes: [
|
||||
{ type: "dataInput", label: "Input" },
|
||||
// Missing processor node
|
||||
],
|
||||
edges: [],
|
||||
};
|
||||
|
||||
mockValidateWorkflowStructure.mockReturnValue({
|
||||
isValid: false,
|
||||
issues: ["No processing nodes found"],
|
||||
suggestions: ["Add a processing node"],
|
||||
});
|
||||
|
||||
const result = await copilotService.validateWorkflow(mockWorkflow);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.issues).toContain("No processing nodes found");
|
||||
expect(result.suggestions).toContain("Add a processing node");
|
||||
});
|
||||
});
|
||||
|
||||
describe("optimizeWorkflow", () => {
|
||||
it("should optimize workflow performance", async () => {
|
||||
const mockResponse = {
|
||||
content: JSON.stringify({
|
||||
optimizations: [
|
||||
"Add parallel processing for independent nodes",
|
||||
"Implement caching for repeated operations",
|
||||
"Optimize data flow between nodes",
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
mockCallOpenAI.mockResolvedValue(mockResponse);
|
||||
|
||||
const optimizations = await copilotService.optimizeWorkflow({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
});
|
||||
|
||||
expect(optimizations).toHaveLength(3);
|
||||
expect(optimizations[0]).toBe(
|
||||
"Add parallel processing for independent nodes"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCopilotSuggestions", () => {
|
||||
it("should get contextual suggestions", async () => {
|
||||
const mockResponse = {
|
||||
content: JSON.stringify({
|
||||
suggestions: [
|
||||
"Consider adding error handling",
|
||||
"Add data validation step",
|
||||
"Include progress indicators",
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
mockCallOpenAI.mockResolvedValue(mockResponse);
|
||||
|
||||
const suggestions = await copilotService.getCopilotSuggestions(
|
||||
"web scraping workflow"
|
||||
);
|
||||
|
||||
expect(suggestions).toHaveLength(3);
|
||||
expect(suggestions[0]).toBe("Consider adding error handling");
|
||||
});
|
||||
|
||||
it("should handle context-specific suggestions", async () => {
|
||||
const mockResponse = {
|
||||
content: JSON.stringify({
|
||||
suggestions: [
|
||||
"Add database connection for data storage",
|
||||
"Include data transformation steps",
|
||||
"Add output formatting options",
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
mockCallOpenAI.mockResolvedValue(mockResponse);
|
||||
|
||||
const suggestions = await copilotService.getCopilotSuggestions(
|
||||
"data processing workflow with database"
|
||||
);
|
||||
|
||||
expect(suggestions).toHaveLength(3);
|
||||
expect(suggestions[0]).toBe("Add database connection for data storage");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("should handle malformed JSON responses", async () => {
|
||||
mockCallOpenAI.mockResolvedValue({
|
||||
content: "Invalid JSON response",
|
||||
});
|
||||
|
||||
await expect(
|
||||
copilotService.parseUserIntent("Test input")
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should handle network timeouts", async () => {
|
||||
mockCallOpenAI.mockRejectedValue(new Error("Request timeout"));
|
||||
|
||||
await expect(
|
||||
copilotService.parseUserIntent("Test input")
|
||||
).rejects.toThrow("Request timeout");
|
||||
});
|
||||
|
||||
it("should handle empty responses", async () => {
|
||||
mockCallOpenAI.mockResolvedValue({
|
||||
content: "",
|
||||
});
|
||||
|
||||
await expect(
|
||||
copilotService.parseUserIntent("Test input")
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,379 @@
|
||||
import { databaseConnectionManager } from '../../services/databaseConnectionManager';
|
||||
|
||||
// Mock localStorage
|
||||
const mockLocalStorage = {
|
||||
getItem: jest.fn(),
|
||||
setItem: jest.fn(),
|
||||
removeItem: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
};
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: mockLocalStorage,
|
||||
});
|
||||
|
||||
// Mock connector classes
|
||||
jest.mock('../../connectors/database/MongoConnector', () => ({
|
||||
MongoConnector: jest.fn().mockImplementation(() => ({
|
||||
testConnection: jest.fn().mockResolvedValue(true),
|
||||
connect: jest.fn().mockResolvedValue(true),
|
||||
disconnect: jest.fn().mockResolvedValue(undefined),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('../../connectors/database/MySQLConnector', () => ({
|
||||
MySQLConnector: jest.fn().mockImplementation(() => ({
|
||||
testConnection: jest.fn().mockResolvedValue(true),
|
||||
connect: jest.fn().mockResolvedValue(true),
|
||||
disconnect: jest.fn().mockResolvedValue(undefined),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('../../connectors/database/PostgresConnector', () => ({
|
||||
PostgresConnector: jest.fn().mockImplementation(() => ({
|
||||
testConnection: jest.fn().mockResolvedValue(true),
|
||||
connect: jest.fn().mockResolvedValue(true),
|
||||
disconnect: jest.fn().mockResolvedValue(undefined),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('DatabaseConnectionManager', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockLocalStorage.getItem.mockReturnValue(null);
|
||||
});
|
||||
|
||||
describe('Connection Management', () => {
|
||||
it('should add a MongoDB connection', async () => {
|
||||
const connectionData = {
|
||||
name: 'Test MongoDB',
|
||||
type: 'mongodb' as const,
|
||||
host: 'localhost',
|
||||
port: 27017,
|
||||
database: 'testdb',
|
||||
connectionString: 'mongodb://localhost:27017/testdb',
|
||||
};
|
||||
|
||||
const connectionId = await databaseConnectionManager.addConnection(connectionData);
|
||||
|
||||
expect(connectionId).toBeDefined();
|
||||
expect(connectionId).toMatch(/^conn_/);
|
||||
|
||||
const connection = databaseConnectionManager.getConnection(connectionId);
|
||||
expect(connection).toBeDefined();
|
||||
expect(connection!.name).toBe('Test MongoDB');
|
||||
expect(connection!.type).toBe('mongodb');
|
||||
});
|
||||
|
||||
it('should add a MySQL connection', async () => {
|
||||
const connectionData = {
|
||||
name: 'Test MySQL',
|
||||
type: 'mysql' as const,
|
||||
host: 'localhost',
|
||||
port: 3306,
|
||||
database: 'testdb',
|
||||
username: 'root',
|
||||
password: 'password',
|
||||
};
|
||||
|
||||
const connectionId = await databaseConnectionManager.addConnection(connectionData);
|
||||
|
||||
expect(connectionId).toBeDefined();
|
||||
|
||||
const connection = databaseConnectionManager.getConnection(connectionId);
|
||||
expect(connection).toBeDefined();
|
||||
expect(connection!.name).toBe('Test MySQL');
|
||||
expect(connection!.type).toBe('mysql');
|
||||
});
|
||||
|
||||
it('should add a PostgreSQL connection', async () => {
|
||||
const connectionData = {
|
||||
name: 'Test PostgreSQL',
|
||||
type: 'postgresql' as const,
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'testdb',
|
||||
username: 'postgres',
|
||||
password: 'password',
|
||||
};
|
||||
|
||||
const connectionId = await databaseConnectionManager.addConnection(connectionData);
|
||||
|
||||
expect(connectionId).toBeDefined();
|
||||
|
||||
const connection = databaseConnectionManager.getConnection(connectionId);
|
||||
expect(connection).toBeDefined();
|
||||
expect(connection!.name).toBe('Test PostgreSQL');
|
||||
expect(connection!.type).toBe('postgresql');
|
||||
});
|
||||
|
||||
it('should update a connection', async () => {
|
||||
const connectionData = {
|
||||
name: 'Test Connection',
|
||||
type: 'mysql' as const,
|
||||
host: 'localhost',
|
||||
port: 3306,
|
||||
database: 'testdb',
|
||||
username: 'root',
|
||||
password: 'password',
|
||||
};
|
||||
|
||||
const connectionId = await databaseConnectionManager.addConnection(connectionData);
|
||||
|
||||
const updated = await databaseConnectionManager.updateConnection(connectionId, {
|
||||
name: 'Updated Connection',
|
||||
port: 3307,
|
||||
});
|
||||
|
||||
expect(updated).toBe(true);
|
||||
|
||||
const connection = databaseConnectionManager.getConnection(connectionId);
|
||||
expect(connection!.name).toBe('Updated Connection');
|
||||
expect(connection!.port).toBe(3307);
|
||||
});
|
||||
|
||||
it('should delete a connection', async () => {
|
||||
const connectionData = {
|
||||
name: 'Test Connection',
|
||||
type: 'mysql' as const,
|
||||
host: 'localhost',
|
||||
port: 3306,
|
||||
database: 'testdb',
|
||||
username: 'root',
|
||||
password: 'password',
|
||||
};
|
||||
|
||||
const connectionId = await databaseConnectionManager.addConnection(connectionData);
|
||||
const deleted = await databaseConnectionManager.deleteConnection(connectionId);
|
||||
|
||||
expect(deleted).toBe(true);
|
||||
expect(databaseConnectionManager.getConnection(connectionId)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Credential Validation', () => {
|
||||
it('should validate MongoDB credentials', async () => {
|
||||
const invalidConnection = {
|
||||
name: 'Invalid MongoDB',
|
||||
type: 'mongodb' as const,
|
||||
host: 'localhost',
|
||||
port: 27017,
|
||||
database: 'testdb',
|
||||
// Missing connectionString
|
||||
};
|
||||
|
||||
await expect(databaseConnectionManager.addConnection(invalidConnection)).rejects.toThrow(
|
||||
'MongoDB requires a connection string'
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate MySQL credentials', async () => {
|
||||
const invalidConnection = {
|
||||
name: 'Invalid MySQL',
|
||||
type: 'mysql' as const,
|
||||
host: 'localhost',
|
||||
port: 3306,
|
||||
database: 'testdb',
|
||||
// Missing username and password
|
||||
};
|
||||
|
||||
await expect(databaseConnectionManager.addConnection(invalidConnection)).rejects.toThrow(
|
||||
'MySQL connection validation failed'
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate PostgreSQL credentials', async () => {
|
||||
const invalidConnection = {
|
||||
name: 'Invalid PostgreSQL',
|
||||
type: 'postgresql' as const,
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'testdb',
|
||||
// Missing username and password
|
||||
};
|
||||
|
||||
await expect(databaseConnectionManager.addConnection(invalidConnection)).rejects.toThrow(
|
||||
'PostgreSQL connection validation failed'
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate port ranges', async () => {
|
||||
const invalidConnection = {
|
||||
name: 'Invalid Port',
|
||||
type: 'mysql' as const,
|
||||
host: 'localhost',
|
||||
port: 99999, // Invalid port
|
||||
database: 'testdb',
|
||||
username: 'root',
|
||||
password: 'password',
|
||||
};
|
||||
|
||||
await expect(databaseConnectionManager.addConnection(invalidConnection)).rejects.toThrow(
|
||||
'Valid port number (1-65535) is required'
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate MongoDB connection string format', async () => {
|
||||
const invalidConnection = {
|
||||
name: 'Invalid MongoDB',
|
||||
type: 'mongodb' as const,
|
||||
host: 'localhost',
|
||||
port: 27017,
|
||||
database: 'testdb',
|
||||
connectionString: 'invalid-connection-string',
|
||||
};
|
||||
|
||||
await expect(databaseConnectionManager.addConnection(invalidConnection)).rejects.toThrow(
|
||||
'Invalid MongoDB connection string format'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Connection Testing', () => {
|
||||
it('should test a connection successfully', async () => {
|
||||
const connectionData = {
|
||||
name: 'Test Connection',
|
||||
type: 'mysql' as const,
|
||||
host: 'localhost',
|
||||
port: 3306,
|
||||
database: 'testdb',
|
||||
username: 'root',
|
||||
password: 'password',
|
||||
};
|
||||
|
||||
const connectionId = await databaseConnectionManager.addConnection(connectionData);
|
||||
const result = await databaseConnectionManager.testConnection(connectionId);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('Connection successful');
|
||||
expect(result.executionTime).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should handle connection test failure', async () => {
|
||||
// Mock connector to return false for testConnection
|
||||
const { MongoConnector } = require('../../connectors/database/MongoConnector');
|
||||
MongoConnector.mockImplementation(() => ({
|
||||
testConnection: jest.fn().mockResolvedValue(false),
|
||||
connect: jest.fn().mockResolvedValue(false),
|
||||
disconnect: jest.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const connectionData = {
|
||||
name: 'Test Connection',
|
||||
type: 'mongodb' as const,
|
||||
host: 'localhost',
|
||||
port: 27017,
|
||||
database: 'testdb',
|
||||
connectionString: 'mongodb://localhost:27017/testdb',
|
||||
};
|
||||
|
||||
const connectionId = await databaseConnectionManager.addConnection(connectionData);
|
||||
const result = await databaseConnectionManager.testConnection(connectionId);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe('Connection test failed');
|
||||
});
|
||||
|
||||
it('should handle connection test error', async () => {
|
||||
// Mock connector to throw an error
|
||||
const { MongoConnector } = require('../../connectors/database/MongoConnector');
|
||||
MongoConnector.mockImplementation(() => ({
|
||||
testConnection: jest.fn().mockRejectedValue(new Error('Connection failed')),
|
||||
connect: jest.fn().mockRejectedValue(new Error('Connection failed')),
|
||||
disconnect: jest.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const connectionData = {
|
||||
name: 'Test Connection',
|
||||
type: 'mongodb' as const,
|
||||
host: 'localhost',
|
||||
port: 27017,
|
||||
database: 'testdb',
|
||||
connectionString: 'mongodb://localhost:27017/testdb',
|
||||
};
|
||||
|
||||
const connectionId = await databaseConnectionManager.addConnection(connectionData);
|
||||
const result = await databaseConnectionManager.testConnection(connectionId);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe('Connection failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Connection Operations', () => {
|
||||
it('should connect to database', async () => {
|
||||
const connectionData = {
|
||||
name: 'Test Connection',
|
||||
type: 'mysql' as const,
|
||||
host: 'localhost',
|
||||
port: 3306,
|
||||
database: 'testdb',
|
||||
username: 'root',
|
||||
password: 'password',
|
||||
};
|
||||
|
||||
const connectionId = await databaseConnectionManager.addConnection(connectionData);
|
||||
const connected = await databaseConnectionManager.connect(connectionId);
|
||||
|
||||
expect(connected).toBe(true);
|
||||
|
||||
const connection = databaseConnectionManager.getConnection(connectionId);
|
||||
expect(connection!.status).toBe('connected');
|
||||
});
|
||||
|
||||
it('should disconnect from database', async () => {
|
||||
const connectionData = {
|
||||
name: 'Test Connection',
|
||||
type: 'mysql' as const,
|
||||
host: 'localhost',
|
||||
port: 3306,
|
||||
database: 'testdb',
|
||||
username: 'root',
|
||||
password: 'password',
|
||||
};
|
||||
|
||||
const connectionId = await databaseConnectionManager.addConnection(connectionData);
|
||||
await databaseConnectionManager.connect(connectionId);
|
||||
|
||||
const disconnected = await databaseConnectionManager.disconnect(connectionId);
|
||||
|
||||
expect(disconnected).toBe(true);
|
||||
|
||||
const connection = databaseConnectionManager.getConnection(connectionId);
|
||||
expect(connection!.status).toBe('disconnected');
|
||||
});
|
||||
|
||||
it('should get connections by type', async () => {
|
||||
const mysqlConnection = {
|
||||
name: 'MySQL Connection',
|
||||
type: 'mysql' as const,
|
||||
host: 'localhost',
|
||||
port: 3306,
|
||||
database: 'testdb',
|
||||
username: 'root',
|
||||
password: 'password',
|
||||
};
|
||||
|
||||
const postgresConnection = {
|
||||
name: 'PostgreSQL Connection',
|
||||
type: 'postgresql' as const,
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'testdb',
|
||||
username: 'postgres',
|
||||
password: 'password',
|
||||
};
|
||||
|
||||
await databaseConnectionManager.addConnection(mysqlConnection);
|
||||
await databaseConnectionManager.addConnection(postgresConnection);
|
||||
|
||||
const mysqlConnections = databaseConnectionManager.getConnectionsByType('mysql');
|
||||
const postgresConnections = databaseConnectionManager.getConnectionsByType('postgresql');
|
||||
|
||||
expect(mysqlConnections).toHaveLength(1);
|
||||
expect(postgresConnections).toHaveLength(1);
|
||||
expect(mysqlConnections[0].name).toBe('MySQL Connection');
|
||||
expect(postgresConnections[0].name).toBe('PostgreSQL Connection');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,312 @@
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import { useWorkflowStore } from "../../store/workflowStore";
|
||||
import { Workflow, WorkflowNode, WorkflowEdge } from "../../types";
|
||||
|
||||
// Mock localStorage
|
||||
const mockLocalStorage = {
|
||||
getItem: jest.fn(),
|
||||
setItem: jest.fn(),
|
||||
removeItem: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
};
|
||||
|
||||
Object.defineProperty(window, "localStorage", {
|
||||
value: mockLocalStorage,
|
||||
});
|
||||
|
||||
describe("WorkflowStore", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockLocalStorage.getItem.mockReturnValue(null);
|
||||
|
||||
// Reset the store state before each test
|
||||
const { result } = renderHook(() => useWorkflowStore());
|
||||
act(() => {
|
||||
result.current.clearAllWorkflows();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Workflow Management", () => {
|
||||
it("should create a new workflow", () => {
|
||||
const { result } = renderHook(() => useWorkflowStore());
|
||||
|
||||
act(() => {
|
||||
result.current.createWorkflow("Test Workflow");
|
||||
});
|
||||
|
||||
expect(result.current.workflows).toHaveLength(1);
|
||||
expect(result.current.workflows[0].name).toBe("Test Workflow");
|
||||
expect(result.current.currentWorkflow).toBe(result.current.workflows[0]);
|
||||
});
|
||||
|
||||
it("should load workflows from localStorage", () => {
|
||||
const mockWorkflows = [
|
||||
{
|
||||
id: "workflow-1",
|
||||
name: "Saved Workflow",
|
||||
nodes: [],
|
||||
edges: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
mockLocalStorage.getItem.mockReturnValue(JSON.stringify(mockWorkflows));
|
||||
|
||||
const { result } = renderHook(() => useWorkflowStore());
|
||||
|
||||
expect(result.current.workflows).toHaveLength(1);
|
||||
expect(result.current.workflows[0].name).toBe("Saved Workflow");
|
||||
});
|
||||
|
||||
it("should update workflow", () => {
|
||||
const { result } = renderHook(() => useWorkflowStore());
|
||||
|
||||
act(() => {
|
||||
result.current.createWorkflow("Original Name");
|
||||
});
|
||||
|
||||
const workflowId = result.current.currentWorkflow!.id;
|
||||
|
||||
act(() => {
|
||||
result.current.updateWorkflow(workflowId, { name: "Updated Name" });
|
||||
});
|
||||
|
||||
expect(result.current.currentWorkflow!.name).toBe("Updated Name");
|
||||
expect(mockLocalStorage.setItem).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should delete workflow", () => {
|
||||
const { result } = renderHook(() => useWorkflowStore());
|
||||
|
||||
act(() => {
|
||||
result.current.createWorkflow("Workflow 1");
|
||||
result.current.createWorkflow("Workflow 2");
|
||||
});
|
||||
|
||||
const workflowId = result.current.workflows[0].id;
|
||||
|
||||
act(() => {
|
||||
result.current.deleteWorkflow(workflowId);
|
||||
});
|
||||
|
||||
expect(result.current.workflows).toHaveLength(1);
|
||||
expect(result.current.workflows[0].name).toBe("Workflow 2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Node Management", () => {
|
||||
it("should add a node", () => {
|
||||
const { result } = renderHook(() => useWorkflowStore());
|
||||
|
||||
act(() => {
|
||||
result.current.createWorkflow("Test Workflow");
|
||||
result.current.addNode("dataInput", { x: 100, y: 100 });
|
||||
});
|
||||
|
||||
expect(result.current.currentWorkflow!.nodes).toHaveLength(1);
|
||||
expect(result.current.currentWorkflow!.nodes[0].type).toBe("dataInput");
|
||||
expect(result.current.currentWorkflow!.nodes[0].data.id).toBe(
|
||||
result.current.currentWorkflow!.nodes[0].id
|
||||
);
|
||||
});
|
||||
|
||||
it("should update a node", () => {
|
||||
const { result } = renderHook(() => useWorkflowStore());
|
||||
|
||||
act(() => {
|
||||
result.current.createWorkflow("Test Workflow");
|
||||
result.current.addNode("dataInput", { x: 100, y: 100 });
|
||||
});
|
||||
|
||||
const nodeId = result.current.currentWorkflow!.nodes[0].id;
|
||||
|
||||
act(() => {
|
||||
result.current.updateNode(nodeId, { label: "Updated Label" });
|
||||
});
|
||||
|
||||
expect(result.current.currentWorkflow!.nodes[0].data.label).toBe(
|
||||
"Updated Label"
|
||||
);
|
||||
});
|
||||
|
||||
it("should delete a node", () => {
|
||||
const { result } = renderHook(() => useWorkflowStore());
|
||||
|
||||
act(() => {
|
||||
result.current.createWorkflow("Test Workflow");
|
||||
result.current.addNode("dataInput", { x: 100, y: 100 });
|
||||
result.current.addNode("llmTask", { x: 200, y: 200 });
|
||||
});
|
||||
|
||||
const nodeId = result.current.currentWorkflow!.nodes[0].id;
|
||||
|
||||
act(() => {
|
||||
result.current.deleteNode(nodeId);
|
||||
});
|
||||
|
||||
expect(result.current.currentWorkflow!.nodes).toHaveLength(1);
|
||||
expect(result.current.currentWorkflow!.nodes[0].type).toBe("llmTask");
|
||||
});
|
||||
|
||||
it("should validate unique node labels", () => {
|
||||
const { result } = renderHook(() => useWorkflowStore());
|
||||
|
||||
act(() => {
|
||||
result.current.createWorkflow("Test Workflow");
|
||||
result.current.addNode("dataInput", { x: 100, y: 100 });
|
||||
result.current.addNode("llmTask", { x: 200, y: 200 });
|
||||
});
|
||||
|
||||
const firstNodeId = result.current.currentWorkflow!.nodes[0].id;
|
||||
const secondNodeId = result.current.currentWorkflow!.nodes[1].id;
|
||||
|
||||
act(() => {
|
||||
result.current.updateNode(firstNodeId, { label: "Test Label" });
|
||||
result.current.updateNode(secondNodeId, { label: "Test Label" });
|
||||
});
|
||||
|
||||
// Should warn about duplicate labels
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Node label "Test Label" already exists')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge Management", () => {
|
||||
it("should add an edge", () => {
|
||||
const { result } = renderHook(() => useWorkflowStore());
|
||||
|
||||
act(() => {
|
||||
result.current.createWorkflow("Test Workflow");
|
||||
result.current.addNode("dataInput", { x: 100, y: 100 });
|
||||
result.current.addNode("llmTask", { x: 200, y: 200 });
|
||||
});
|
||||
|
||||
const sourceId = result.current.currentWorkflow!.nodes[0].id;
|
||||
const targetId = result.current.currentWorkflow!.nodes[1].id;
|
||||
|
||||
act(() => {
|
||||
result.current.addEdge(sourceId, targetId);
|
||||
});
|
||||
|
||||
expect(result.current.currentWorkflow!.edges).toHaveLength(1);
|
||||
expect(result.current.currentWorkflow!.edges[0].source).toBe(sourceId);
|
||||
expect(result.current.currentWorkflow!.edges[0].target).toBe(targetId);
|
||||
});
|
||||
|
||||
it("should delete an edge", () => {
|
||||
const { result } = renderHook(() => useWorkflowStore());
|
||||
|
||||
act(() => {
|
||||
result.current.createWorkflow("Test Workflow");
|
||||
result.current.addNode("dataInput", { x: 100, y: 100 });
|
||||
result.current.addNode("llmTask", { x: 200, y: 200 });
|
||||
});
|
||||
|
||||
const sourceId = result.current.currentWorkflow!.nodes[0].id;
|
||||
const targetId = result.current.currentWorkflow!.nodes[1].id;
|
||||
|
||||
act(() => {
|
||||
result.current.addEdge(sourceId, targetId);
|
||||
});
|
||||
|
||||
const edgeId = result.current.currentWorkflow!.edges[0].id;
|
||||
|
||||
act(() => {
|
||||
result.current.deleteEdge(edgeId);
|
||||
});
|
||||
|
||||
expect(result.current.currentWorkflow!.edges).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ID Migration", () => {
|
||||
it("should migrate deterministic IDs to UUIDs", () => {
|
||||
const mockWorkflow = {
|
||||
id: "workflow-1",
|
||||
name: "Test Workflow",
|
||||
nodes: [
|
||||
{ id: "node-0", data: { id: "node-0", label: "Input Node" } },
|
||||
{ id: "node-1", data: { id: "node-1", label: "Process Node" } },
|
||||
],
|
||||
edges: [{ id: "edge-0", source: "node-0", target: "node-1" }],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
mockLocalStorage.getItem.mockReturnValue(JSON.stringify([mockWorkflow]));
|
||||
|
||||
const { result } = renderHook(() => useWorkflowStore());
|
||||
|
||||
const workflow = result.current.workflows[0];
|
||||
|
||||
// Check that deterministic IDs were migrated to UUIDs
|
||||
expect(workflow.nodes[0].id).not.toMatch(/^node-\d+$/);
|
||||
expect(workflow.nodes[1].id).not.toMatch(/^node-\d+$/);
|
||||
expect(workflow.nodes[0].data.id).toBe(workflow.nodes[0].id);
|
||||
expect(workflow.nodes[1].data.id).toBe(workflow.nodes[1].id);
|
||||
|
||||
// Check that edges reference the new UUIDs
|
||||
expect(workflow.edges[0].source).toBe(workflow.nodes[0].id);
|
||||
expect(workflow.edges[0].target).toBe(workflow.nodes[1].id);
|
||||
});
|
||||
|
||||
it("should not migrate already migrated workflows", () => {
|
||||
const mockWorkflow = {
|
||||
id: "workflow-1",
|
||||
name: "Test Workflow",
|
||||
nodes: [
|
||||
{ id: "uuid-1", data: { id: "uuid-1", label: "Input Node" } },
|
||||
{ id: "uuid-2", data: { id: "uuid-2", label: "Process Node" } },
|
||||
],
|
||||
edges: [{ id: "edge-uuid", source: "uuid-1", target: "uuid-2" }],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
mockLocalStorage.getItem.mockReturnValue(JSON.stringify([mockWorkflow]));
|
||||
|
||||
const { result } = renderHook(() => useWorkflowStore());
|
||||
|
||||
const workflow = result.current.workflows[0];
|
||||
|
||||
// Should remain unchanged
|
||||
expect(workflow.nodes[0].id).toBe("uuid-1");
|
||||
expect(workflow.nodes[1].id).toBe("uuid-2");
|
||||
expect(workflow.edges[0].source).toBe("uuid-1");
|
||||
expect(workflow.edges[0].target).toBe("uuid-2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Execution", () => {
|
||||
it("should execute workflow", async () => {
|
||||
const { result } = renderHook(() => useWorkflowStore());
|
||||
|
||||
act(() => {
|
||||
result.current.createWorkflow("Test Workflow");
|
||||
result.current.addNode("dataInput", { x: 100, y: 100 });
|
||||
});
|
||||
|
||||
// Mock the execution engine
|
||||
const mockExecuteWorkflow = jest.fn().mockResolvedValue({
|
||||
id: "exec-1",
|
||||
status: "completed",
|
||||
results: new Map(),
|
||||
});
|
||||
|
||||
// Replace the execution engine with mock
|
||||
jest.doMock("../../services/executionEngine", () => ({
|
||||
executionEngine: {
|
||||
executeWorkflow: mockExecuteWorkflow,
|
||||
},
|
||||
}));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.executeWorkflow("test input");
|
||||
});
|
||||
|
||||
expect(mockExecuteWorkflow).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { substituteVariables, NodeOutput } from '../../utils/variableSubstitution';
|
||||
|
||||
describe('Variable Substitution - Simple Tests', () => {
|
||||
it('should substitute basic variables', () => {
|
||||
const template = 'Hello {{node1.output}}';
|
||||
const nodeOutputs = new Map<string, NodeOutput>([
|
||||
['node1', { nodeId: 'node1', output: 'World', data: 'World' }],
|
||||
]);
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe('Hello World');
|
||||
});
|
||||
|
||||
it('should handle missing nodes gracefully', () => {
|
||||
const template = 'Hello {{missingNode.output}}';
|
||||
const nodeOutputs = new Map<string, NodeOutput>();
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe('Hello {{missingNode.output}}');
|
||||
});
|
||||
|
||||
it('should handle empty template', () => {
|
||||
const template = '';
|
||||
const nodeOutputs = new Map<string, NodeOutput>();
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle null template', () => {
|
||||
const template = null as any;
|
||||
const nodeOutputs = new Map<string, NodeOutput>();
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,295 @@
|
||||
import {
|
||||
substituteVariables,
|
||||
NodeOutput,
|
||||
} from "../../utils/variableSubstitution";
|
||||
|
||||
describe("Variable Substitution", () => {
|
||||
describe("Basic Substitution", () => {
|
||||
it("should substitute simple node output", () => {
|
||||
const template = "Hello {{node1.output}}";
|
||||
const nodeOutputs = new Map<string, NodeOutput>([
|
||||
["node1", { nodeId: "node1", output: "World", data: "World" }],
|
||||
]);
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe("Hello World");
|
||||
});
|
||||
|
||||
it("should substitute multiple variables", () => {
|
||||
const template = "{{node1.output}} and {{node2.output}}";
|
||||
const nodeOutputs = new Map<string, NodeOutput>([
|
||||
["node1", { nodeId: "node1", output: "Hello", data: "Hello" }],
|
||||
["node2", { nodeId: "node2", output: "World", data: "World" }],
|
||||
]);
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe("Hello and World");
|
||||
});
|
||||
|
||||
it("should handle empty template", () => {
|
||||
const template = "";
|
||||
const nodeOutputs = new Map<string, NodeOutput>();
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it("should handle template without variables", () => {
|
||||
const template = "No variables here";
|
||||
const nodeOutputs = new Map<string, NodeOutput>();
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe("No variables here");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Property Access", () => {
|
||||
it("should access output property", () => {
|
||||
const template = "{{node1.output}}";
|
||||
const nodeOutputs = new Map<string, NodeOutput>([
|
||||
[
|
||||
"node1",
|
||||
{ nodeId: "node1", output: "Test Output", data: "Test Data" },
|
||||
],
|
||||
]);
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe("Test Output");
|
||||
});
|
||||
|
||||
it("should access data property", () => {
|
||||
const template = "{{node1.data}}";
|
||||
const nodeOutputs = new Map<string, NodeOutput>([
|
||||
[
|
||||
"node1",
|
||||
{ nodeId: "node1", output: "Test Output", data: "Test Data" },
|
||||
],
|
||||
]);
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe("Test Data");
|
||||
});
|
||||
|
||||
it("should access error property", () => {
|
||||
const template = "{{node1.error}}";
|
||||
const nodeOutputs = new Map<string, NodeOutput>([
|
||||
[
|
||||
"node1",
|
||||
{ nodeId: "node1", output: "Test Output", error: "Test Error" },
|
||||
],
|
||||
]);
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe("Test Error");
|
||||
});
|
||||
|
||||
it("should access status property", () => {
|
||||
const template = "{{node1.status}}";
|
||||
const nodeOutputs = new Map<string, NodeOutput>([
|
||||
[
|
||||
"node1",
|
||||
{ nodeId: "node1", output: "Test Output", status: "completed" },
|
||||
],
|
||||
]);
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe("completed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Node Label to ID Mapping", () => {
|
||||
it("should resolve node labels to IDs", () => {
|
||||
const template = "{{inputNode.output}}";
|
||||
const nodeOutputs = new Map<string, NodeOutput>([
|
||||
[
|
||||
"node-1",
|
||||
{ nodeId: "node-1", output: "Input Data", data: "Input Data" },
|
||||
],
|
||||
]);
|
||||
const nodeLabelToId = new Map<string, string>([["inputNode", "node-1"]]);
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs, nodeLabelToId);
|
||||
|
||||
expect(result).toBe("Input Data");
|
||||
});
|
||||
|
||||
it("should fall back to direct ID when label not found", () => {
|
||||
const template = "{{node-1.output}}";
|
||||
const nodeOutputs = new Map<string, NodeOutput>([
|
||||
[
|
||||
"node-1",
|
||||
{
|
||||
nodeId: "node-1",
|
||||
output: "Direct ID Data",
|
||||
data: "Direct ID Data",
|
||||
},
|
||||
],
|
||||
]);
|
||||
const nodeLabelToId = new Map<string, string>();
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs, nodeLabelToId);
|
||||
|
||||
expect(result).toBe("Direct ID Data");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Missing Node Handling", () => {
|
||||
it("should return original template when node not found", () => {
|
||||
const template = "{{missingNode.output}}";
|
||||
const nodeOutputs = new Map<string, NodeOutput>();
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe("{{missingNode.output}}");
|
||||
});
|
||||
|
||||
it("should warn when node not found", () => {
|
||||
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
||||
const template = "{{missingNode.output}}";
|
||||
const nodeOutputs = new Map<string, NodeOutput>();
|
||||
|
||||
substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"Node missingNode (missingNode) not found for variable {{missingNode.output}}"
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should show available nodes in warning", () => {
|
||||
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
||||
const template = "{{missingNode.output}}";
|
||||
const nodeOutputs = new Map<string, NodeOutput>([
|
||||
["node1", { nodeId: "node1", output: "Data1", data: "Data1" }],
|
||||
["node2", { nodeId: "node2", output: "Data2", data: "Data2" }],
|
||||
]);
|
||||
|
||||
substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith("Available nodes: node1, node2");
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Complex Templates", () => {
|
||||
it("should handle mixed content and variables", () => {
|
||||
const template = "Process {{node1.output}} and save to {{node2.data}}";
|
||||
const nodeOutputs = new Map<string, NodeOutput>([
|
||||
["node1", { nodeId: "node1", output: "input.txt", data: "input.txt" }],
|
||||
[
|
||||
"node2",
|
||||
{ nodeId: "node2", output: "output.txt", data: "output.txt" },
|
||||
],
|
||||
]);
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe("Process input.txt and save to output.txt");
|
||||
});
|
||||
|
||||
it("should handle multiple occurrences of same variable", () => {
|
||||
const template = "{{node1.output}} and {{node1.output}} again";
|
||||
const nodeOutputs = new Map<string, NodeOutput>([
|
||||
["node1", { nodeId: "node1", output: "Hello", data: "Hello" }],
|
||||
]);
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe("Hello and Hello again");
|
||||
});
|
||||
|
||||
it("should handle nested object properties", () => {
|
||||
const template = "{{node1.result.data}}";
|
||||
const nodeOutputs = new Map<string, NodeOutput>([
|
||||
[
|
||||
"node1",
|
||||
{
|
||||
nodeId: "node1",
|
||||
output: { result: { data: "Nested Data" } },
|
||||
data: { result: { data: "Nested Data" } },
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe("Nested Data");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Type Conversion", () => {
|
||||
it("should convert numbers to strings", () => {
|
||||
const template = "Count: {{node1.output}}";
|
||||
const nodeOutputs = new Map<string, NodeOutput>([
|
||||
["node1", { nodeId: "node1", output: 42, data: 42 }],
|
||||
]);
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe("Count: 42");
|
||||
});
|
||||
|
||||
it("should convert booleans to strings", () => {
|
||||
const template = "Status: {{node1.output}}";
|
||||
const nodeOutputs = new Map<string, NodeOutput>([
|
||||
["node1", { nodeId: "node1", output: true, data: true }],
|
||||
]);
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe("Status: true");
|
||||
});
|
||||
|
||||
it("should handle null and undefined values", () => {
|
||||
const template = "Value: {{node1.output}}";
|
||||
const nodeOutputs = new Map<string, NodeOutput>([
|
||||
["node1", { nodeId: "node1", output: null, data: null }],
|
||||
]);
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe("Value: ");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("should handle malformed variable syntax", () => {
|
||||
const template = "{{node1.output";
|
||||
const nodeOutputs = new Map<string, NodeOutput>([
|
||||
["node1", { nodeId: "node1", output: "Data", data: "Data" }],
|
||||
]);
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe("{{node1.output");
|
||||
});
|
||||
|
||||
it("should handle empty variable names", () => {
|
||||
const template = "{{}}";
|
||||
const nodeOutputs = new Map<string, NodeOutput>();
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe("{{}}");
|
||||
});
|
||||
|
||||
it("should handle non-string templates", () => {
|
||||
const template = null as any;
|
||||
const nodeOutputs = new Map<string, NodeOutput>();
|
||||
|
||||
const result = substituteVariables(template, nodeOutputs);
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import "@testing-library/jest-dom";
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = {
|
||||
getItem: jest.fn(),
|
||||
setItem: jest.fn(),
|
||||
removeItem: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
};
|
||||
Object.defineProperty(window, "localStorage", {
|
||||
value: localStorageMock,
|
||||
});
|
||||
|
||||
// Mock console methods to reduce noise in tests
|
||||
global.console = {
|
||||
...console,
|
||||
// Uncomment to ignore a specific log level
|
||||
// log: jest.fn(),
|
||||
// debug: jest.fn(),
|
||||
// info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
};
|
||||
|
||||
// Mock ResizeObserver
|
||||
global.ResizeObserver = jest.fn().mockImplementation(() => ({
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock IntersectionObserver
|
||||
global.IntersectionObserver = jest.fn().mockImplementation(() => ({
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock crypto for UUID generation
|
||||
Object.defineProperty(global, "crypto", {
|
||||
value: {
|
||||
randomUUID: () => "mock-uuid-" + Math.random().toString(36).substr(2, 9),
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user