Skip to content

Commit 11a3936

Browse files
feat(agentflow): enhance validation and feedback mechanisms (#5915)
* feat(agentflow): enhance validation and feedback mechanisms - Added constraint validation functions to ensure flow integrity, including checks for single start nodes, nested iterations, and human input restrictions within iterations. - Implemented a new ValidationFeedback component to display validation results and issues directly in the UI. - Updated the Agentflow component to integrate validation feedback and display snackbar notifications for constraint violations. - Enhanced flow validation logic to detect hanging edges and validate node inputs, improving overall flow integrity checks. - Introduced new test cases for validation functions to ensure robustness and reliability. * address gemini review feedback
1 parent 09f2a15 commit 11a3936

20 files changed

Lines changed: 1079 additions & 190 deletions

packages/agentflow/TESTS.md

Lines changed: 59 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,137 +1,112 @@
1-
# @flowiseai/agentflow Test Plan
1+
# @flowiseai/agentflow — Testing Guide
22

33
## Running Tests
44

55
```bash
6-
# Fast run (no coverage)
7-
pnpm test
6+
pnpm test # Fast run (no coverage)
7+
pnpm test:coverage # With coverage enforcement
8+
pnpm test:watch # Watch mode during development
9+
```
810

9-
# With coverage enforcement
10-
pnpm test:coverage
11+
## Test Strategy
1112

12-
# Watch mode during development
13-
pnpm test:watch
14-
```
13+
Tests are prioritized by impact. When modifying a file, add or update tests in the same PR.
1514

16-
## Test Coverage by Tier
15+
### Tier 1 — Core Logic (must test)
1716

18-
Add tests when actively working on these files. Each tier reflects impact and testability.
17+
Pure business logic in `core/`, `infrastructure/`, and critical hooks. These carry the highest risk — a bug here affects every user. Always test in the same PR when modifying.
1918

20-
### Tier 1 — Core Logic
19+
**What belongs here:** validation rules, node utilities, API clients, state management (reducers, context actions), flow data hooks (`useFlowHandlers`).
2120

22-
These modules carry the highest risk. Test in the same PR when modifying.
21+
### Tier 2 — Feature Hooks & Dialogs (test when changing)
2322

24-
<!-- prettier-ignore -->
25-
| File | Key exports to test | Status |
26-
| --- | --- | --- |
27-
| `src/core/validation/` | `validateFlow`, `validateNode` — empty flows, missing/multiple starts, disconnected nodes, cycles, required inputs; `isValidConnectionAgentflowV2` — self-connections, cycle detection | ✅ Done |
28-
| `src/core/utils/` | `getUniqueNodeId`, `getUniqueNodeLabel`, `initNode`, `generateExportFlowData` | ✅ Done |
29-
| `src/core/node-catalog/` | `filterNodesByComponents`, `isAgentflowNode`, `groupNodesByCategory` | ✅ Done |
30-
| `src/core/node-config/` | `getAgentflowIcon`, `getNodeColor` | ✅ Done |
31-
| `src/core/theme/tokens.ts` | All design tokens — node colors, light/dark variants, spacing scale, semantic colors, ReactFlow colors, shadows, border radius, gradients | ✅ Done |
32-
| `src/core/theme/cssVariables.ts` | `generateCSSVariables()` — valid CSS strings, all variables, correct light/dark values, proper formatting, consistency with tokens | ✅ Done |
33-
| `src/core/theme/createAgentflowTheme.ts` | `createAgentflowTheme()` — MUI theme creation, palette mode, colors from tokens, custom card palette, spacing, border radius, consistency | ✅ Done |
34-
| `src/infrastructure/api/client.ts` | `createApiClient` — headers, auth token, 401 interceptor | ✅ Done |
35-
| `src/infrastructure/api/chatflows.ts` | All CRUD + `generateAgentflow` + `getChatModels`, FlowData serialization | ✅ Done |
36-
| `src/infrastructure/api/nodes.ts` | `getAllNodes`, `getNodeByName`, `getNodeIconUrl` | ✅ Done |
37-
| `src/infrastructure/store/AgentflowContext.tsx` | `agentflowReducer` (all actions), `normalizeNodes`, `deleteNode()`, `duplicateNode()`, `openEditDialog()`, `closeEditDialog()`, `setNodes()`, `setEdges()`, `updateNodeData()`, `deleteEdge()`, state synchronization with local setters. E2E: composite workflow (add→connect→edit→save), load→modify→save roundtrip, multi-edge from single node, rapid connect/disconnect cycles, edge deletion | ✅ Done |
38-
| `src/infrastructure/store/ApiContext.tsx` | `ApiProvider` — client creation, memoization, `useApiContext` error boundary | ✅ Done |
39-
| `src/useAgentflow.ts` | `getFlow()`, `toJSON()`, `validate()`, `addNode()`, `clear()`, `fitView()`, `getReactFlowInstance()`, instance stability | ✅ Done |
40-
| `src/features/canvas/hooks/useFlowHandlers.ts` | `handleConnect`, `handleNodesChange`, `handleEdgesChange`, `handleAddNode` — synchronous `onFlowChange` callbacks, dirty tracking, viewport resolution, change filtering | ✅ Done |
23+
Feature-level hooks and dialog components that orchestrate UI behavior. Test when adding features or fixing bugs.
4124

42-
### Tier 2 — Feature Hooks & Dialogs
25+
**What belongs here:** search logic, drag-and-drop, node color calculations, dialog state machines, theme detection.
4326

44-
Test when adding features or fixing bugs in these areas.
27+
### Tier 3 — UI Components (test if logic exists)
4528

46-
<!-- prettier-ignore -->
47-
| File | Key exports to test | Status |
48-
| --- | --- | --- |
49-
| `src/features/node-palette/search.ts` | `fuzzyScore`, `searchNodes`, `debounce` | ✅ Done |
50-
| `src/features/canvas/hooks/useFlowNodes.ts` | `useFlowNodes()` — category filtering, component whitelist, error states | ✅ Done |
51-
| `src/features/canvas/hooks/useDragAndDrop.ts` | `useDragAndDrop()` — JSON parse error handling, node init on drop | ✅ Done |
52-
| `src/features/canvas/hooks/useNodeColors.ts` | `useNodeColors()` — color calculations for selected/hover/dark mode | ✅ Done |
53-
| `src/infrastructure/store/ConfigContext.tsx` | `ConfigProvider` — theme detection (light/dark/system), media query listener | ✅ Done |
54-
| `src/features/generator/GenerateFlowDialog.tsx` | Dialog state machine — API call flow, error handling, progress state | ✅ Done |
55-
| `src/features/node-editor/EditNodeDialog.tsx` | Label editing — keyboard handling (Enter/Escape), node data updates | ✅ Done |
56-
| `src/features/canvas/hooks/useOpenNodeEditor.ts` | `openNodeEditor()` — node/schema lookup, inputValues initialization, early returns, fallback to `node.data.inputs` when API schema unavailable, API schema priority over `data.inputs` | ✅ Done |
29+
Presentational components that are mostly JSX. Only add tests if the component contains meaningful business logic (e.g., an exported helper function). Pure styling components (`styled.ts`, `MainCard.tsx`, etc.) do not need tests.
5730

58-
### Tier 3 — UI Components
31+
## Writing Tests
5932

60-
Mostly JSX with minimal logic. Only add tests if business logic is introduced.
33+
### File Extension Convention
6134

62-
<!-- prettier-ignore -->
63-
| File | Key exports to test | Status |
64-
| --- | --- | --- |
65-
| `src/features/canvas/components/NodeOutputHandles.tsx` | `getMinimumNodeHeight()` — linear scaling, MIN_NODE_HEIGHT floor | ✅ Done |
66-
| `src/features/canvas/components/ConnectionLine.tsx` | Edge label visibility per node type, label content (condition index, humanInput proceed/reject), edge color from AGENTFLOW_ICONS | ✅ Done |
67-
| `src/Agentflow.tsx` | Integration test — dark mode, ThemeProvider, CSS variables, header rendering, keyboard shortcuts (Cmd+S / Ctrl+S save), generate flow dialog, imperative ref | ✅ Done |
35+
The Jest config uses file extensions to select the test environment:
6836

69-
Files that are pure styling or data constants (`styled.ts`, `nodeIcons.ts`, `MainCard.tsx`, etc.) do not need dedicated tests.
37+
| Extension | Environment | When to use |
38+
| ----------- | ----------------------- | -------------------------------------------------------------------------- |
39+
| `.test.ts` | **node** (no DOM) | Pure logic — utilities, reducers, data transformations |
40+
| `.test.tsx` | **jsdom** (browser DOM) | Anything that renders React — `renderHook` with providers, component tests |
7041

71-
## Test Utilities
42+
Source files use `.tsx` only when they contain JSX syntax. A hook like `useAgentflow.ts` has no JSX, so it stays `.ts` even though its test is `.test.tsx` (because the test uses `renderHook` with a JSX wrapper).
7243

73-
### Factory Functions (`src/__test_utils__/factories.ts`)
44+
### Factory Functions
7445

75-
Use factory functions to create test fixtures with sensible defaults:
46+
Use factory functions from `@test-utils/factories` to create test fixtures with sensible defaults:
7647

7748
```typescript
7849
import { makeFlowNode, makeFlowEdge, makeNodeData } from '@test-utils/factories'
7950

80-
// Create test nodes
8151
const node = makeFlowNode('node-1', {
8252
type: 'agentflowNode',
8353
data: { name: 'llmAgentflow', label: 'LLM' }
8454
})
8555

86-
// Create test edges
8756
const edge = makeFlowEdge('node-1', 'node-2')
8857

89-
// Create node data (for palette/search tests)
9058
const nodeData = makeNodeData({ name: 'llmAgentflow', label: 'LLM' })
9159
```
9260

93-
### Custom Jest Environment
94-
95-
**File**: `src/__test_utils__/jest-environment-jsdom.js`
61+
### Mocking Patterns
9662

97-
Prevents the `canvas` native module from being loaded during jsdom initialization. The canvas package requires native compilation which fails in many environments, but it's only an optional dependency of jsdom and not needed for React component tests.
63+
**Mocking a module with `jest.mock`:**
9864

99-
This custom environment intercepts `require('canvas')` at the module level and returns a mock before jsdom tries to load the native binary.
65+
```typescript
66+
import { isValidConnectionAgentflowV2 } from '@/core'
10067

101-
### Module Mocks
68+
jest.mock('@/core', () => ({
69+
isValidConnectionAgentflowV2: jest.fn(() => true),
70+
getUniqueNodeId: jest.fn(() => 'new-node-1')
71+
}))
10272

103-
**ReactFlow Mock** (`src/__mocks__/reactflow.tsx`): Provides mock implementations of ReactFlow components and hooks.
73+
// Override per-test:
74+
it('should reject invalid connection', () => {
75+
;(isValidConnectionAgentflowV2 as jest.Mock).mockReturnValueOnce(false)
76+
// ...
77+
})
78+
```
10479

105-
Key features:
80+
**Mocking context hooks:**
10681

107-
- Uses `forwardRef` for MUI `styled()` compatibility (prevents emotion errors)
108-
- Uses `useState` internally to maintain stable references (prevents infinite re-render loops)
109-
- Exports all commonly used ReactFlow components (`Controls`, `MiniMap`, `Background`, etc.)
110-
- Mocks hooks (`useNodesState`, `useEdgesState`, `useReactFlow`)
82+
```typescript
83+
const mockSetDirty = jest.fn()
84+
85+
jest.mock('@/infrastructure/store', () => ({
86+
useAgentflowContext: () => ({
87+
state: { reactFlowInstance: null },
88+
setDirty: mockSetDirty
89+
})
90+
}))
91+
```
11192

112-
**Axios Mock** (`src/__mocks__/axios.ts`): Prevents network errors by mocking all HTTP requests. Returns empty arrays/objects for all API calls to silence network warnings in tests.
93+
### Module Mocks
11394

114-
**CSS Mock** (`src/__mocks__/styleMock.js`): Empty object export for CSS imports.
95+
**ReactFlow** (`src/__mocks__/reactflow.tsx`): Mock implementations of ReactFlow components and hooks. Uses `forwardRef` for MUI `styled()` compatibility and `useState` internally for stable references.
11596

116-
## File Extension Convention
97+
**Axios** (`src/__mocks__/axios.ts`): Prevents network errors by mocking all HTTP methods. Returns empty arrays/objects by default.
11798

118-
The jest config uses file extensions to select the test environment:
99+
**CSS/SVG** (`src/__mocks__/styleMock.js`): Empty object export for CSS and SVG imports.
119100

120-
| Extension | Environment | When to use |
121-
| ----------- | ----------------------- | -------------------------------------------------------------------------- |
122-
| `.test.ts` | **node** (no DOM) | Pure logic — utilities, reducers, data transformations |
123-
| `.test.tsx` | **jsdom** (browser DOM) | Anything that renders React — `renderHook` with providers, component tests |
101+
### Custom Jest Environment
124102

125-
**Source files** follow a different rule: use `.tsx` only when the file contains JSX syntax. A React hook like `useAgentflow.ts` has no JSX, so it stays `.ts` even though its test file is `.test.tsx` (because the test uses `renderHook` with a JSX wrapper).
103+
`src/__test_utils__/jest-environment-jsdom.js` intercepts `require('canvas')` and returns a mock before jsdom tries to load the native binary. This prevents build failures in environments without native canvas compilation.
126104

127105
## Configuration
128106

129107
- **Jest config**: `jest.config.js` — two projects: `unit` (node env, `.test.ts`) and `components` (custom jsdom env, `.test.tsx`)
130-
- **Test environment**: Component tests use custom jsdom environment (`src/__test_utils__/jest-environment-jsdom.js`) to handle canvas loading
131-
- **Import aliases**: `@test-utils` maps to `src/__test_utils__` for convenient imports
132-
- **Coverage thresholds**: uniform 80% floor (`branches`, `functions`, `lines`, `statements`) — see `coverageThreshold` in `jest.config.js` for the full list
133-
- **Coverage exclusions**:
134-
- `src/__test_utils__/**` — test utilities
135-
- `src/__mocks__/**` — module mocks
108+
- **Import aliases**: `@test-utils` maps to `src/__test_utils__`, `@/` maps to `src/`
109+
- **Coverage thresholds**: 80% floor for `branches`, `functions`, `lines`, `statements` — see `coverageThreshold` in `jest.config.js` for per-path entries
110+
- **Coverage exclusions**: `src/__test_utils__/**`, `src/__mocks__/**`
136111
- **CI**: `pnpm test:coverage` runs in GitHub Actions between lint and build
137112
- **Reports**: `coverage/lcov-report/index.html` for detailed HTML report

packages/agentflow/jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const baseConfig = {
1313
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
1414
moduleNameMapper: {
1515
'^@test-utils/(.*)$': '<rootDir>/src/__test_utils__/$1',
16+
'\\.svg$': '<rootDir>/src/__mocks__/styleMock.js',
1617
'^@/(.*)$': '<rootDir>/src/$1',
1718
'\\.(css|less|scss|sass)$': '<rootDir>/src/__mocks__/styleMock.js'
1819
}

packages/agentflow/src/Agentflow.tsx

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
22
import ReactFlow, { Background, Controls, MiniMap, ReactFlowProvider, useEdgesState, useNodesState } from 'reactflow'
33

4+
import { Alert, Snackbar } from '@mui/material'
45
import { IconSparkles } from '@tabler/icons-react'
56

67
import { tokens } from './core/theme'
78
import type { AgentFlowInstance, AgentflowProps, FlowData, FlowDataCallback, FlowEdge, FlowNode } from './core/types'
9+
import { applyValidationErrorsToNodes, validateFlow } from './core/validation'
810
import {
911
AgentflowHeader,
1012
ConnectionLine,
@@ -15,6 +17,7 @@ import {
1517
useFlowHandlers,
1618
useFlowNodes
1719
} from './features/canvas'
20+
import { ValidationFeedback } from './features/canvas/components'
1821
import { GenerateFlowDialog } from './features/generator'
1922
import { EditNodeDialog } from './features/node-editor'
2023
import { AddNodesDrawer, StyledFab } from './features/node-palette'
@@ -80,6 +83,17 @@ function AgentflowCanvas({
8083
const [edges, setLocalEdges, onEdgesChange] = useEdgesState(initialFlow?.edges || [])
8184
const [showGenerateDialog, setShowGenerateDialog] = useState(false)
8285

86+
// Constraint violation snackbar state
87+
const [snackbar, setSnackbar] = useState<{ open: boolean; message: string }>({ open: false, message: '' })
88+
89+
const handleConstraintViolation = useCallback((message: string) => {
90+
setSnackbar({ open: true, message })
91+
}, [])
92+
93+
const handleSnackbarClose = useCallback(() => {
94+
setSnackbar({ open: false, message: '' })
95+
}, [])
96+
8397
// Load available nodes
8498
const { availableNodes } = useFlowNodes()
8599

@@ -106,14 +120,16 @@ function AgentflowCanvas({
106120
onNodesChange,
107121
onEdgesChange,
108122
onFlowChange,
109-
availableNodes
123+
availableNodes,
124+
onConstraintViolation: handleConstraintViolation
110125
})
111126

112127
// Drag and drop handlers
113128
const { handleDragOver, handleDrop } = useDragAndDrop({
114129
nodes: nodes as FlowNode[],
115130
setLocalNodes: setLocalNodes as React.Dispatch<React.SetStateAction<FlowNode[]>>,
116-
reactFlowWrapper: reactFlowWrapper as React.RefObject<HTMLDivElement>
131+
reactFlowWrapper: reactFlowWrapper as React.RefObject<HTMLDivElement>,
132+
onConstraintViolation: handleConstraintViolation
117133
})
118134

119135
// Handle generated flow from dialog
@@ -134,13 +150,25 @@ function AgentflowCanvas({
134150
[setLocalNodes, setLocalEdges, setDirty, onFlowGenerated]
135151
)
136152

137-
// Handle save
153+
// Handle save — run validation first and highlight problem nodes
138154
const handleSave = useCallback(() => {
139-
if (onSave) {
140-
onSave(agentflow.getFlow())
141-
setDirty(false)
155+
if (!onSave) return
156+
157+
const flowNodes = nodes as FlowNode[]
158+
const flowEdges = edges as FlowEdge[]
159+
const result = validateFlow(flowNodes, flowEdges, availableNodes)
160+
161+
// Update node border highlighting: set errors on failing nodes, clear errors on now-valid nodes
162+
setLocalNodes((prev) => applyValidationErrorsToNodes(prev as FlowNode[], result.errors) as FlowNode[])
163+
164+
if (!result.valid) {
165+
handleConstraintViolation('Flow has validation errors. Please fix them before saving.')
166+
return
142167
}
143-
}, [onSave, agentflow, setDirty])
168+
169+
onSave(agentflow.getFlow())
170+
setDirty(false)
171+
}, [onSave, agentflow, setDirty, nodes, edges, availableNodes, setLocalNodes, handleConstraintViolation])
144172

145173
// Keyboard shortcut: Cmd+S / Ctrl+S to save
146174
useEffect(() => {
@@ -210,6 +238,16 @@ function AgentflowCanvas({
210238
</StyledFab>
211239
)}
212240

241+
{/* Validation Feedback - positioned at top right */}
242+
{!readOnly && (
243+
<ValidationFeedback
244+
nodes={nodes as FlowNode[]}
245+
edges={edges as FlowEdge[]}
246+
availableNodes={availableNodes}
247+
setNodes={setLocalNodes as React.Dispatch<React.SetStateAction<FlowNode[]>>}
248+
/>
249+
)}
250+
213251
<ReactFlow
214252
nodes={nodes}
215253
edges={edges}
@@ -244,6 +282,18 @@ function AgentflowCanvas({
244282

245283
{/* Edit Node Dialog */}
246284
<EditNodeDialog show={state.editingNodeId !== null} dialogProps={state.editDialogProps || {}} onCancel={closeEditDialog} />
285+
286+
{/* Constraint Violation Snackbar */}
287+
<Snackbar
288+
open={snackbar.open}
289+
autoHideDuration={5000}
290+
onClose={handleSnackbarClose}
291+
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
292+
>
293+
<Alert onClose={handleSnackbarClose} severity='error' variant='filled' sx={{ width: '100%' }}>
294+
{snackbar.message}
295+
</Alert>
296+
</Snackbar>
247297
</div>
248298
)
249299
}

0 commit comments

Comments
 (0)