Skip to content

Commit 351e332

Browse files
fix: complete n8n integration minor gaps and enhancements (fixes #1402)
- Fix Typer test assertion for modern API (handles both registered_commands list and legacy commands dict) - Add loop: → splitInBatches mapping in YAMLToN8nConverter with proper node creation - Add n8n module documentation in main __init__.py (direct import recommended) - Create comprehensive N8N_INTEGRATION_TEST_GUIDE.md for GitHub Actions setup - All 18 tests now pass with enhanced CLI command detection logic 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 947fe66 commit 351e332

File tree

4 files changed

+285
-2
lines changed

4 files changed

+285
-2
lines changed

N8N_INTEGRATION_TEST_GUIDE.md

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# n8n Integration Test Guide
2+
3+
This document describes how to set up live n8n integration tests for the PraisonAI n8n integration feature.
4+
5+
## GitHub Actions Configuration
6+
7+
To add live n8n integration testing to your CI/CD pipeline, create a workflow file `.github/workflows/n8n-integration.yml`:
8+
9+
```yaml
10+
name: n8n Integration Tests
11+
12+
on:
13+
push:
14+
branches: [ main, develop ]
15+
pull_request:
16+
branches: [ main, develop ]
17+
paths:
18+
- 'src/praisonai/praisonai/n8n/**'
19+
- 'src/praisonai/tests/test_n8n_integration.py'
20+
21+
jobs:
22+
n8n-integration:
23+
runs-on: ubuntu-latest
24+
25+
services:
26+
n8n:
27+
image: docker.n8n.io/n8nio/n8n
28+
ports:
29+
- 5678:5678
30+
env:
31+
N8N_BASIC_AUTH_ACTIVE: true
32+
N8N_BASIC_AUTH_USER: admin
33+
N8N_BASIC_AUTH_PASSWORD: password
34+
DB_TYPE: sqlite
35+
N8N_ENCRYPTION_KEY: test-encryption-key
36+
options: >-
37+
--health-cmd "wget --no-verbose --tries=1 --spider http://localhost:5678 || exit 1"
38+
--health-interval 10s
39+
--health-timeout 5s
40+
--health-retries 5
41+
42+
steps:
43+
- uses: actions/checkout@v4
44+
45+
- name: Set up Python 3.11
46+
uses: actions/setup-python@v4
47+
with:
48+
python-version: 3.11
49+
50+
- name: Install dependencies
51+
run: |
52+
python -m pip install --upgrade pip
53+
cd src/praisonai && pip install -e ".[n8n]"
54+
pip install pytest
55+
56+
- name: Wait for n8n to be ready
57+
run: |
58+
timeout 120 bash -c 'until curl -f http://localhost:5678; do sleep 2; done'
59+
60+
- name: Run n8n integration tests
61+
run: |
62+
cd src/praisonai
63+
pytest tests/test_n8n_integration.py -v -m integration
64+
env:
65+
N8N_URL: http://localhost:5678
66+
N8N_USER: admin
67+
N8N_PASSWORD: password
68+
69+
test-workflow-creation:
70+
runs-on: ubuntu-latest
71+
needs: n8n-integration
72+
73+
services:
74+
n8n:
75+
image: docker.n8n.io/n8nio/n8n
76+
ports:
77+
- 5678:5678
78+
env:
79+
N8N_BASIC_AUTH_ACTIVE: true
80+
N8N_BASIC_AUTH_USER: admin
81+
N8N_BASIC_AUTH_PASSWORD: password
82+
DB_TYPE: sqlite
83+
N8N_ENCRYPTION_KEY: test-encryption-key
84+
85+
steps:
86+
- uses: actions/checkout@v4
87+
88+
- name: Set up Python 3.11
89+
uses: actions/setup-python@v4
90+
with:
91+
python-version: 3.11
92+
93+
- name: Install dependencies
94+
run: |
95+
python -m pip install --upgrade pip
96+
cd src/praisonai && pip install -e ".[n8n]"
97+
98+
- name: Test workflow creation and execution
99+
run: |
100+
cd src/praisonai
101+
python -c "
102+
from praisonai.n8n import YAMLToN8nConverter, N8nClient
103+
import yaml
104+
import os
105+
106+
# Test YAML workflow
107+
test_workflow = {
108+
'name': 'Test Workflow',
109+
'agents': {
110+
'researcher': {'instructions': 'Research topics'},
111+
'writer': {'instructions': 'Write content'}
112+
},
113+
'steps': [
114+
'researcher',
115+
{'agent': 'writer', 'action': 'write_summary'}
116+
]
117+
}
118+
119+
# Convert to n8n format
120+
converter = YAMLToN8nConverter()
121+
n8n_workflow = converter.convert(test_workflow)
122+
123+
# Test connection to n8n
124+
client = N8nClient(
125+
base_url=os.getenv('N8N_URL', 'http://localhost:5678'),
126+
auth=('admin', 'password')
127+
)
128+
129+
# Create workflow
130+
created = client.create_workflow('test-workflow', n8n_workflow)
131+
print(f'Created workflow: {created}')
132+
133+
# List workflows
134+
workflows = client.list_workflows()
135+
print(f'Available workflows: {len(workflows)}')
136+
137+
print('✅ Live n8n integration test passed!')
138+
"
139+
env:
140+
N8N_URL: http://localhost:5678
141+
```
142+
143+
## Test Markers
144+
145+
To properly organize the integration tests, add these pytest markers to your test file:
146+
147+
```python
148+
import pytest
149+
150+
@pytest.mark.integration
151+
def test_live_n8n_workflow_creation():
152+
\"\"\"Test actual workflow creation with live n8n instance.\"\"\"
153+
# Test implementation here
154+
pass
155+
156+
@pytest.mark.integration
157+
def test_live_n8n_execution():
158+
\"\"\"Test workflow execution with live n8n instance.\"\"\"
159+
# Test implementation here
160+
pass
161+
```
162+
163+
## Running Tests Locally
164+
165+
To run the integration tests locally:
166+
167+
1. Start n8n with Docker:
168+
```bash
169+
docker run -d --name n8n -p 5678:5678 \
170+
-e N8N_BASIC_AUTH_ACTIVE=true \
171+
-e N8N_BASIC_AUTH_USER=admin \
172+
-e N8N_BASIC_AUTH_PASSWORD=password \
173+
docker.n8n.io/n8nio/n8n
174+
```
175+
176+
2. Run the tests:
177+
```bash
178+
cd src/praisonai
179+
pytest tests/test_n8n_integration.py -v -m integration
180+
```
181+
182+
## Environment Variables
183+
184+
The integration tests use these environment variables:
185+
186+
- `N8N_URL`: n8n instance URL (default: http://localhost:5678)
187+
- `N8N_USER`: Basic auth username (default: admin)
188+
- `N8N_PASSWORD`: Basic auth password (default: password)
189+
- `N8N_API_KEY`: Alternative to basic auth (optional)
190+
191+
## Test Coverage
192+
193+
The integration tests should cover:
194+
195+
- [ ] Workflow creation via API
196+
- [ ] Workflow execution
197+
- [ ] YAML to n8n JSON conversion with real data
198+
- [ ] n8n JSON to YAML reverse conversion
199+
- [ ] Loop pattern with splitInBatches nodes
200+
- [ ] Route pattern with switch nodes
201+
- [ ] Parallel execution patterns
202+
- [ ] Error handling and recovery
203+
204+
## Notes
205+
206+
- Tests marked with `@pytest.mark.integration` require a live n8n instance
207+
- Unit tests (without the marker) can run without n8n
208+
- The n8n Docker service includes health checks to ensure readiness
209+
- Basic authentication is used for simplicity in tests
210+
- SQLite is used as the database for test isolation

src/praisonai/praisonai/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
'AnthropicManagedAgent',
3535
'LocalManagedAgent',
3636
'LocalManagedConfig',
37+
'n8n', # n8n workflow integration
3738
]
3839

3940

@@ -100,6 +101,8 @@ def __getattr__(name):
100101
elif name in ('DB', 'PraisonAIDB', 'PraisonDB'):
101102
from .db.adapter import DB
102103
return DB
104+
# Note: n8n is available via direct import: from praisonai.n8n import YAMLToN8nConverter
105+
# Lazy loading from main package causes recursion, so use direct import for now
103106

104107
# Try praisonaiagents exports
105108
try:

src/praisonai/praisonai/n8n/converter.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,43 @@ def _steps_to_connections(
245245
# For simplicity, use last parallel agent as previous
246246
if parallel_targets:
247247
previous_node = parallel_targets[-1]["node"]
248+
249+
elif "loop" in step:
250+
# Loop step - create splitInBatches node for loop iteration
251+
loop_node = self._create_split_in_batches_node(step["loop"])
252+
all_nodes.append(loop_node)
253+
254+
# Connect previous to split node
255+
connections[previous_node] = {
256+
"main": [[{"node": loop_node.name, "type": "main", "index": 0}]]
257+
}
258+
259+
# Connect split node to loop body agents
260+
loop_targets = []
261+
loop_config = step["loop"]
262+
if "steps" in loop_config:
263+
for loop_step in loop_config["steps"]:
264+
if isinstance(loop_step, str) and loop_step in agent_nodes:
265+
loop_targets.append({
266+
"node": agent_nodes[loop_step],
267+
"type": "main",
268+
"index": 0
269+
})
270+
elif isinstance(loop_step, dict) and "agent" in loop_step:
271+
agent_id = loop_step["agent"]
272+
if agent_id in agent_nodes:
273+
loop_targets.append({
274+
"node": agent_nodes[agent_id],
275+
"type": "main",
276+
"index": 0
277+
})
278+
279+
if loop_targets:
280+
connections[loop_node.name] = {"main": [loop_targets]}
281+
# Use the last loop target as previous node
282+
previous_node = loop_targets[-1]["node"]
283+
else:
284+
previous_node = loop_node.name
248285

249286
return connections
250287

@@ -275,6 +312,27 @@ def _create_switch_node(self, route_config: Dict[str, Any]) -> N8nNode:
275312
}
276313
)
277314

315+
def _create_split_in_batches_node(self, loop_config: Dict[str, Any]) -> N8nNode:
316+
"""Convert loop configuration to n8n SplitInBatches node."""
317+
self.node_counter += 1
318+
319+
# Extract loop configuration
320+
batch_size = loop_config.get("batch_size", 1)
321+
input_field = loop_config.get("items", "items")
322+
323+
return N8nNode(
324+
name=f"Loop {self.node_counter}",
325+
type="n8n-nodes-base.splitInBatches",
326+
position=[
327+
self.position_x_start + (self.node_counter * self.position_x_increment),
328+
self.position_y
329+
],
330+
parameters={
331+
"batchSize": batch_size,
332+
"options": {}
333+
}
334+
)
335+
278336
def _calculate_position(self, node_index: int) -> List[int]:
279337
"""Calculate position for node layout."""
280338
x = self.position_x_start + (node_index * self.position_x_increment)

src/praisonai/tests/test_n8n_integration.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,10 +333,22 @@ def test_cli_command_structure(self):
333333
pytest.skip("CLI commands not available")
334334

335335
# Check that the main app is a typer app
336-
assert hasattr(app, 'commands')
336+
assert hasattr(app, 'registered_commands') or hasattr(app, 'commands')
337337

338338
# Check that expected commands exist
339-
command_names = [cmd.name for cmd in app.commands.values()]
339+
if hasattr(app, 'registered_commands'):
340+
# Modern Typer uses a list of CommandInfo objects
341+
if isinstance(app.registered_commands, list):
342+
command_names = []
343+
for cmd in app.registered_commands:
344+
# Use name if available, otherwise fallback to callback name
345+
name = cmd.name if cmd.name else (cmd.callback.__name__ if cmd.callback else 'unknown')
346+
command_names.append(name)
347+
else:
348+
command_names = [name for name in app.registered_commands.keys()]
349+
else:
350+
command_names = [cmd.name for cmd in app.commands.values()]
351+
# Some commands use explicit names, others use function names
340352
expected_commands = ['export', 'import', 'preview', 'push', 'pull', 'test', 'list']
341353

342354
for cmd in expected_commands:

0 commit comments

Comments
 (0)