---
name: n8n-testing
description: Generate and push ContextQA test cases from an n8n workflow (JSON, URL, or workflow ID)
---

# N8N Workflow Testing Skill for Claude Code
> **Agent instruction file** — Read this before testing any n8n workflow with ContextQA.

---

## When to Use This Skill

Use this skill whenever the user says any of:
- "test this n8n workflow"
- "generate tests for workflow [ID / URL / file]"
- "create test cases from n8n"
- "analyse n8n workflow and create ContextQA tests"
- "build a test plan for my n8n workflow"

**Do NOT skip any phase.** Work through all 5 phases in order.

---

## Overview: What You Will Produce

For every n8n workflow you receive you must produce:

| Output | Description |
|--------|-------------|
| **Node Inventory** | Table of every non-sticky node: name, type, category, expressions |
| **Test Plan** | At minimum 3 test cases: Happy Path, Error Path, Edge Cases |
| **Branch Tests** | 1 extra test case for every `if` / `switch` node (both true and false branches) |
| **ContextQA Steps** | Correct step type per node — see the step-type matrix below |
| **JS Assertions** | `custom_step_code` assertion step after every HTTP and Code step |
| **Validated JSON** | Every payload validated and auto-fixed before calling ContextQA API |
| **Push Report** | Summary of steps created, failed, and retried |

---

## Phase 1 — Fetch Workflow & Build Node Inventory

### 1.1 Fetch the Workflow

**Option A — MCP tool (preferred)**
```
Call: generate_contextqa_tests_from_n8n(file_path_or_url, test_case_id)
```

**Option B — Direct n8n API fetch (for analysis before creating test cases)**
```
GET https://{instance}.app.n8n.cloud/api/v1/workflows/{workflow_id}
Header: X-N8N-API-KEY: {N8N_API_KEY from env}
```
Parse `response.data.nodes` and `response.data.connections`.

**Option C — Local JSON file**
```
Read the file with Read tool, parse JSON.
```

### 1.2 Build Node Inventory

Skip nodes where `"disabled": true` and nodes of type `n8n-nodes-base.stickyNote`.

For every remaining node, record:

| # | Node Name | n8n Type | Category | Has Expressions | Branch Point |
|---|-----------|---------|----------|-----------------|--------------|
| 1 | Settings Configuration1 | n8n-nodes-base.set | SET | No | No |
| 2 | Create SEO Structure1 | @n8n/n8n-nodes-langchain.openAi | AI | No | No |
| 3 | Validate Structure1 | n8n-nodes-base.if | BRANCH | No | Yes (true/false) |
| … | … | … | … | … | … |

**Categories:**
- `HTTP` — type contains `httprequest`
- `CODE` — type contains `code` or `function`
- `AI` — type contains `openai`, `langchain`, or `ai`
- `BRANCH` — type contains `if` or `switch`
- `SET` — type is `n8n-nodes-base.set`
- `FORM` — type contains `form` (includes formTrigger)
- `MERGE` — type contains `merge` or `splitout`
- `INTEGRATION` — everything else (wordpress, email, slack, webhook, etc.)

### 1.3 Identify Critical Path

Trace `connections` from the trigger node to terminal nodes. The critical (happy) path is the longest chain that avoids error branches.

### 1.4 List Branch Points

For every `BRANCH` node, record:
- Node name
- Condition logic (from `parameters.conditions`)
- True path → next node
- False path → next node

---

## Phase 2 — Design the Test Plan

### 2.1 Minimum Test Cases (always create these)

| # | Test Case Name Pattern | Scenario | Nodes Covered |
|---|----------------------|----------|---------------|
| 1 | `{WorkflowName} - Happy Path` | All valid inputs, follow true branch at every if node | All critical-path nodes |
| 2 | `{WorkflowName} - Error Path` | Invalid/missing inputs that trigger the false/error branch | Trigger + branch nodes + error terminal |
| 3 | `{WorkflowName} - Edge Cases` | Boundary values: empty strings, null, max-length, special chars | Trigger + HTTP + Code nodes |

### 2.2 Branch Coverage (one test case per if/switch branch)

For each `BRANCH` node, add:
- `{WorkflowName} - {NodeName} TRUE Branch` — data that satisfies the condition
- `{WorkflowName} - {NodeName} FALSE Branch` — data that fails the condition

If the ERROR PATH test already covers one branch, skip the duplicate.

### 2.3 Test Data Tables

Before writing steps, define test data for each test case:

**Happy Path Data:**
- All required fields populated with realistic values
- Expressions like `${var_1}` assigned real test values

**Error Path Data:**
- One or more fields deliberately invalid (wrong type, empty, out-of-range)
- Auth headers missing or wrong

**Edge Case Data:**
- Empty string `""`
- `null` / `undefined`
- String of 10,000 characters
- Unicode: `"こんにちは"`, `"<script>alert(1)</script>"`
- Numeric boundaries: `0`, `-1`, `2147483647`

---

## Phase 3 — Step Generation Reference

### 3.1 Step-Type Selection Matrix

| n8n Category | Primary Step Type | `actionName` | `naturalTextActionId` | Always Add Assertion? |
|-------------|-------------------|-------------|----------------------|----------------------|
| HTTP | REST API | `restApi` | `2` | ✅ Yes |
| CODE | Custom Code | `custom_step_code` | `452` | ✅ Yes (output validation) |
| AI | AI Action | `ai_text_actions` | `568` | ✅ Yes (structure check) |
| BRANCH | Custom Code (mirrored logic) | `custom_step_code` | `452` | ✅ Yes |
| SET | Custom Code (verify assignments) | `custom_step_code` | `452` | No |
| FORM trigger | Custom Code (document fields) | `custom_step_code` | `452` | No |
| FORM response | Custom Code (document response) | `custom_step_code` | `452` | No |
| MERGE | Custom Code (passthrough) | `custom_step_code` | `452` | No |
| INTEGRATION | Custom Code (passthrough) | `custom_step_code` | `452` | No |
| stickyNote | **SKIP** — never create | — | — | — |

> **Rule:** Every HTTP step is always followed immediately by a `custom_step_code` assertion step that validates the response. Every CODE step is followed by an output-shape assertion. Every AI step is followed by a structure/non-empty assertion.

---

### 3.2 JSON Template: REST API Step (`restApi`)

Use this for every HTTP Request node.

```json
{
  "testCaseId": 1407,
  "position": 0,
  "type": "ACTION_TEXT",
  "actionName": "restApi",
  "naturalTextActionId": 2,
  "action": "REST_API: <span data-key=\"method\" data-event-key=\"method\" class=\"rest\">post</span> - <span data-key=\"title\" data-event-key=\"title\" class=\"rest\">Create WordPress Post</span>",
  "description": "Execute API Request for 'Create WordPress Post1'",
  "disabled": false,
  "ignoreStepResult": false,
  "waitTime": 30,
  "testDataList": [
    { "testData": "post", "testDataType": "raw" },
    { "testData": "Create WordPress Post", "testDataType": "raw" }
  ],
  "event": {
    "customEvent": "restApi",
    "url": "https://${var_1}/wp-json/wp/v2/posts",
    "description": "Execute API Request for 'Create WordPress Post1'",
    "variableName": "result_create_wordpress_post1",
    "method": "post",
    "requestHeaders": {
      "Content-Type": "application/json",
      "Authorization": "Basic ${var_auth}"
    },
    "payload": "{\"title\": \"${var_title}\", \"status\": \"draft\"}",
    "payloadType": "raw",
    "authorizationValue": {
      "username": "", "password": "", "token": "",
      "grant_type": "", "access_token_url": "",
      "client_id": "", "client_secret": "", "scope": "",
      "client_authentication": ""
    },
    "authorizationType": "NONE",
    "followRedirects": true,
    "title": "Create WordPress Post",
    "validation": {
      "status": [
        { "verificationType": "equals", "expectedType": "number", "expectedValue": 200 }
      ]
    },
    "status": [
      { "verificationType": "equals", "expectedType": "number", "expectedValue": 200 }
    ],
    "pwLocator": []
  },
  "dataMapBean": {
    "condition_if": [],
    "for_loop": { "startIndex": null, "endIndex": null, "testDataId": null }
  },
  "dataMapJson": { "conditionIf": [] },
  "conditionIf": []
}
```

**Field rules:**
- `event.method` — always lowercase (`get`, `post`, `put`, `patch`, `delete`)
- `event.url` — replace `{{ expr }}` with `${var_N}` (from variable map)
- `event.variableName` — snake_case of node name, e.g. `result_update_seo_metadata1`
- `event.payloadType` — `"raw"` when method is POST/PUT/PATCH with body, `"none"` for GET
- `event.payload` — raw JSON string for body, or `{}` for GET
- `event.validation.status[0].expectedValue` — use `200` for positive test, `400`/`401`/`422` for negative test
- `verificationType` — body rules accept `equals`, `not-equal`, `less-than`, `greater-than`, `is-null`, `is-not-null`; **status rules accept only** `equals`, `not-equal`, `less-than`, `greater-than` (no null checks). Any other string is forwarded as-is and fails on the platform (the UI shows an empty Verification dropdown). Always include an `expectedValue`; for `is-null`/`is-not-null` set it to `""`.
- `action` HTML — use exact span structure shown; escape double-quotes with `\"`

---

### 3.3 JSON Template: Custom Code Step (`custom_step_code`)

Use for CODE nodes, BRANCH mirrors, SET verification, integration placeholders, and all assertion steps.

```json
{
  "testCaseId": 1407,
  "position": 1,
  "type": "ACTION_TEXT",
  "actionName": "custom_step_code",
  "naturalTextActionId": 452,
  "action": "<span class=\"no-editable\" contenteditable=\"false\">custom step: </span><span data-key=\"description\" data-event-key=\"description\" class=\"element action-flex-auto\">Validate Create WordPress Post response</span>",
  "description": null,
  "disabled": false,
  "ignoreStepResult": false,
  "waitTime": 30,
  "testData": null,
  "testDataType": null,
  "element": "Validate Create WordPress Post response",
  "event": {
    "customEvent": "custom_step_code",
    "custom_code": "(function() {\n  var response = $input.first().json;\n  if (!response) throw new Error('No response from previous step');\n  if (!response.id) throw new Error('Expected response.id, got: ' + JSON.stringify(response));\n  if (response.status !== 'draft') throw new Error('Expected status=draft, got: ' + response.status);\n  return [{ json: { validated: true, postId: response.id, status: response.status } }];\n})();",
    "description": "Validate Create WordPress Post response",
    "pwLocator": []
  },
  "testDataList": [
    { "testData": "custom step", "testDataType": "raw" },
    { "testData": "Validate Create WordPress Post response", "testDataType": "raw" }
  ],
  "dataMapBean": {
    "condition_if": [],
    "element": "Validate Create WordPress Post response",
    "for_loop": { "startIndex": null, "endIndex": null, "testDataId": null }
  },
  "dataMapJson": {
    "conditionIf": [],
    "element": "Validate Create WordPress Post response"
  },
  "conditionIf": []
}
```

**Field rules:**
- `event.custom_code` — MUST be wrapped in `(function() { ... })();` — no exceptions
- `element` and `dataMapBean.element` and `dataMapJson.element` — all set to the step description
- `action` HTML — use the exact `<span class="no-editable"...>` structure shown

---

### 3.4 JSON Template: AI Action Step (`ai_text_actions`)

Use for OpenAI, LangChain, and AI agent nodes.

```json
{
  "testCaseId": 1407,
  "position": 2,
  "type": "ACTION_TEXT",
  "actionName": "ai_text_actions",
  "naturalTextActionId": 568,
  "action": "AI Agent <span data-key=\"element\" data-event-key=\"ai_task\" class=\"element action-flex-auto\">Write a 500-word blog chapter about SEO best practices for the keyword: ${var_3}</span>",
  "description": null,
  "disabled": false,
  "ignoreStepResult": false,
  "waitTime": null,
  "testData": null,
  "testDataType": null,
  "testDataList": null,
  "event": {
    "customEvent": "ai_text_actions",
    "ai_task": "Write a 500-word blog chapter about SEO best practices for the keyword: ${var_3}",
    "description": "Write a 500-word blog chapter about SEO best practices for the keyword: ${var_3}",
    "type": "create_step",
    "enhanced_task": null
  },
  "dataMapBean": {
    "condition_if": [],
    "for_loop": { "startIndex": null, "endIndex": null, "testDataId": null }
  },
  "dataMapJson": { "conditionIf": [] },
  "conditionIf": []
}
```

**Field rules:**
- `event.type` — MUST be `"create_step"` (not `"ACTION_TEXT"`)
- `event.ai_task` — full prompt text (replace n8n expressions with `${var_N}`)
- `event.enhanced_task` — always `null`
- `testDataList` — always `null` for AI steps
- Truncate `action` span to 200 chars max; add `…` if truncated

---

### 3.5 JSON Template: Navigate to URL Step (first step of every test case)

Every test case must start with a navigate step.

```json
{
  "testCaseId": 1407,
  "position": 0,
  "type": "ACTION_TEXT",
  "actionName": "navigateToUrl",
  "naturalTextActionId": 425,
  "action": "Navigate to <span data-key=\"href\" data-event-key=\"href\" class=\"element action-flex-auto\">https://testertom.com/</span>",
  "description": null,
  "disabled": false,
  "ignoreStepResult": false,
  "waitTime": 30,
  "testData": "https://testertom.com/",
  "testDataType": "raw",
  "testDataList": [
    { "testData": "https://testertom.com/", "testDataType": "raw" }
  ],
  "event": {
    "customEvent": "navigateToUrl",
    "href": "https://testertom.com/"
  },
  "dataMapBean": {
    "condition_if": [],
    "for_loop": { "startIndex": null, "endIndex": null, "testDataId": null }
  },
  "dataMapJson": { "conditionIf": [] },
  "conditionIf": []
}
```

---

### 3.6 JavaScript Assertion Patterns

Use these patterns in `event.custom_code`. Always wrap in IIFE.

#### Pattern A — REST API Response Validation
```javascript
(function() {
  var response = $input.first().json;
  // Existence check
  if (!response) throw new Error('No response received from previous API step');
  // HTTP error check
  if (response.statusCode && response.statusCode >= 400) {
    throw new Error('HTTP error ' + response.statusCode + ': ' + JSON.stringify(response.body));
  }
  // Required field checks
  if (response.id === undefined || response.id === null) {
    throw new Error('Expected field "id" in response, got: ' + JSON.stringify(response));
  }
  // Type check
  if (typeof response.id !== 'number') {
    throw new Error('Expected response.id to be number, got: ' + typeof response.id);
  }
  // Pass the ID forward for downstream steps
  return [{ json: { validated: true, id: response.id } }];
})();
```

#### Pattern B — Negative Test: Expect HTTP Error
```javascript
(function() {
  var response = $input.first().json;
  if (!response) throw new Error('No response received');
  // For negative tests, we EXPECT an error status
  var status = response.statusCode || response.status;
  if (!status || status < 400) {
    throw new Error('Expected HTTP 4xx error but got status: ' + status + '. Response: ' + JSON.stringify(response));
  }
  return [{ json: { validated: true, errorStatus: status } }];
})();
```

#### Pattern C — Code Node Output Validation
```javascript
(function() {
  var output = $input.first().json;
  if (!output) throw new Error('Code node produced no output');
  // Check required output fields (adapt to actual node output shape)
  var requiredFields = ['title', 'focusKeyword', 'metaDescription'];
  for (var i = 0; i < requiredFields.length; i++) {
    var field = requiredFields[i];
    if (output[field] === undefined || output[field] === null || output[field] === '') {
      throw new Error('Missing or empty field "' + field + '" in code node output: ' + JSON.stringify(output));
    }
  }
  return [{ json: { validated: true, fields: requiredFields } }];
})();
```

#### Pattern D — AI Output Validation
```javascript
(function() {
  var output = $input.first().json;
  if (!output) throw new Error('AI step produced no output');
  // AI response should be non-empty text
  var text = output.text || output.content || output.message || output.response || '';
  if (typeof text !== 'string' || text.trim().length < 10) {
    throw new Error('AI output too short or missing. Got: ' + JSON.stringify(output));
  }
  // Optional: check minimum word count
  var wordCount = text.trim().split(/\s+/).length;
  if (wordCount < 20) {
    throw new Error('AI output has too few words (' + wordCount + '). Full output: ' + text.substring(0, 200));
  }
  return [{ json: { validated: true, wordCount: wordCount, preview: text.substring(0, 100) } }];
})();
```

#### Pattern E — Branch Logic Mirror (if/switch node)
```javascript
// TRUE BRANCH test — input data should satisfy the condition
(function() {
  var input = $input.first().json;
  // Mirror the n8n if condition here:
  // Original condition: input.keywords is not empty
  if (!input.keywords || input.keywords.trim().length === 0) {
    throw new Error('Test data should have non-empty keywords but got: ' + JSON.stringify(input.keywords));
  }
  return [{ json: { branch: 'true', validated: true, keywords: input.keywords } }];
})();
```

#### Pattern F — Edge Case: Empty / Null Input
```javascript
(function() {
  var input = $input.first().json;
  // Edge case: simulate empty/null required field
  var testValue = input.testField;
  if (testValue !== '' && testValue !== null && testValue !== undefined) {
    throw new Error('Edge case expects empty/null testField, got: ' + JSON.stringify(testValue));
  }
  // For edge cases, the API should reject with 4xx
  return [{ json: { edgeCaseInput: testValue, validated: true } }];
})();
```

#### Pattern G — Integration Placeholder Passthrough
```javascript
// Placeholder for: {NODE_NAME} ({NODE_TYPE})
// ────────────────────────────────────────────────────────
// This node uses a third-party integration that cannot be
// directly automated. Verify manually or replace with the
// appropriate ContextQA browser/API step.
//
// Node parameters:
// {PARAM_SUMMARY}
// ────────────────────────────────────────────────────────
(function() {
  return typeof $input !== 'undefined' ? $input.all() : [{ json: {} }];
})();
```

---

## Phase 4 — JSON Validation (Run Before Every API Call)

### 4.1 Required Fields Checklist

Before calling `create_complex_test_step`, validate EVERY payload against this checklist:

```
UNIVERSAL (all step types):
  ✅ testCaseId         — number, > 0
  ✅ position           — number, >= 0
  ✅ type               — string === "ACTION_TEXT"
  ✅ actionName         — string, one of: restApi | custom_step_code | ai_text_actions | navigateToUrl
  ✅ naturalTextActionId — number: restApi→2, custom_step_code→452, ai_text_actions→568, navigateToUrl→425
  ✅ event              — object, not null
  ✅ event.customEvent  — string, must EQUAL actionName value
  ✅ disabled           — boolean (not string, not null)
  ✅ ignoreStepResult   — boolean (not string, not null)
  ✅ dataMapBean        — object with keys: condition_if (array), for_loop (object)
  ✅ dataMapJson        — object with key: conditionIf (array)
  ✅ conditionIf        — array (may be empty)

FOR restApi:
  ✅ event.url          — string, non-empty, starts with "http" or "${"
  ✅ event.method       — string, lowercase, one of: get|post|put|patch|delete|head
  ✅ event.variableName — string, matches /^[a-z_][a-z0-9_]*$/ (snake_case, no spaces)
  ✅ event.validation   — object with key "status" (array, at least 1 item)
  ✅ event.status       — array, at least 1 item

FOR custom_step_code:
  ✅ event.custom_code  — string, non-empty
  ✅ event.custom_code  — starts with "(function() {" and ends with "})();"
  ✅ event.pwLocator    — array (may be empty)

FOR ai_text_actions:
  ✅ event.ai_task      — string, non-empty, length > 5
  ✅ event.type         — string === "create_step"
  ✅ event.enhanced_task — null
  ✅ testDataList       — null (NOT an array for AI steps)

FOR navigateToUrl:
  ✅ event.href         — string, starts with "http"
```

### 4.2 Auto-Fix Table

When a validation error is found, apply the fix BEFORE retrying:

| Detected Problem | Auto-Fix |
|-----------------|----------|
| `disabled` is missing | Add `"disabled": false` |
| `ignoreStepResult` is missing | Add `"ignoreStepResult": false` |
| `conditionIf` is missing | Add `"conditionIf": []` |
| `dataMapBean` is missing | Add `{"condition_if": [], "for_loop": {"startIndex": null, "endIndex": null, "testDataId": null}}` |
| `dataMapJson` is missing | Add `{"conditionIf": []}` |
| `event` is null | Replace with `{"customEvent": actionName}` |
| `event.customEvent` doesn't match `actionName` | Set `event.customEvent = actionName` |
| `event.url` has no protocol | Prepend `"https://"` |
| `event.method` is uppercase | Apply `.toLowerCase()` |
| `event.variableName` has spaces or hyphens | Apply `.replace(/[\s\-]+/g, '_').toLowerCase()` |
| `event.variableName` starts with digit | Prepend `"r_"` |
| `event.custom_code` is null or empty | Set to `"(function() { return [{json:{}}]; })();"` |
| `event.custom_code` not wrapped in IIFE | Wrap: `"(function() {\n" + code + "\n})();"` |
| `event.custom_code` double-wrapped `(function()(function()` | Unwrap outer IIFE, re-wrap once |
| `event.ai_task` is null or empty | Set to `"Verify the AI task completed successfully"` |
| `event.type` missing for ai_text_actions | Set `event.type = "create_step"` |
| `event.enhanced_task` missing | Set `event.enhanced_task = null` |
| `testDataList` is array for ai_text_actions | Set `testDataList = null` |
| `naturalTextActionId` wrong for actionName | Set: restApi→2, custom_step_code→452, ai_text_actions→568, navigateToUrl→425 |
| `type` is not `"ACTION_TEXT"` | Set `type = "ACTION_TEXT"` |
| `event.validation` missing for restApi | Add `{"status": [{"verificationType": "equals", "expectedType": "number", "expectedValue": 200}]}` |
| `event.status` missing for restApi | Copy from `event.validation.status` |
| Position is negative | Set to `0` and renumber all subsequent steps |
| `testCaseId` is 0 or missing | **STOP** — ask user for the correct test case ID |

### 4.3 Validation Pseudocode for Claude to Follow

```
function validateAndFix(payload):
  errors = []

  // Universal checks
  if typeof payload.testCaseId != 'number' or payload.testCaseId <= 0:
    errors.append("FATAL: testCaseId invalid")
  if payload.type != "ACTION_TEXT":
    payload.type = "ACTION_TEXT"
  if payload.actionName not in VALID_ACTION_NAMES:
    errors.append("FATAL: unknown actionName: " + payload.actionName)
  if payload.naturalTextActionId != EXPECTED_ID[payload.actionName]:
    payload.naturalTextActionId = EXPECTED_ID[payload.actionName]
  if typeof payload.disabled != 'boolean':
    payload.disabled = false
  if typeof payload.ignoreStepResult != 'boolean':
    payload.ignoreStepResult = false
  if not payload.conditionIf:
    payload.conditionIf = []
  if not payload.dataMapBean:
    payload.dataMapBean = EMPTY_DATA_MAP_BEAN
  if not payload.dataMapJson:
    payload.dataMapJson = {"conditionIf": []}
  if not payload.event:
    payload.event = {"customEvent": payload.actionName}
  if payload.event.customEvent != payload.actionName:
    payload.event.customEvent = payload.actionName

  // Per-type checks
  if payload.actionName == "restApi":
    if not payload.event.url: errors.append("FATAL: event.url empty")
    else if not payload.event.url.startswith("http") and not payload.event.url.startswith("${"):
      payload.event.url = "https://" + payload.event.url
    if payload.event.method:
      payload.event.method = payload.event.method.toLowerCase()
    if payload.event.variableName:
      payload.event.variableName = payload.event.variableName.replace(/[\s\-]+/g, '_').toLowerCase()
    if not payload.event.validation:
      payload.event.validation = DEFAULT_VALIDATION_200
    if not payload.event.status:
      payload.event.status = payload.event.validation.status

  if payload.actionName == "custom_step_code":
    if not payload.event.custom_code:
      payload.event.custom_code = "(function() { return [{json:{}}]; })();"
    else if not payload.event.custom_code.trim().startsWith("(function() {"):
      payload.event.custom_code = "(function() {\n" + payload.event.custom_code + "\n})();"
    if payload.event.pwLocator is undefined:
      payload.event.pwLocator = []

  if payload.actionName == "ai_text_actions":
    if not payload.event.ai_task:
      payload.event.ai_task = "Verify the AI task completed successfully"
    payload.event.type = "create_step"
    payload.event.enhanced_task = null
    payload.testDataList = null

  if len(errors) > 0:
    STOP and report errors to user before proceeding
  return payload
```

---

## Phase 5 — Push to ContextQA & Verify

### 5.1 Pre-Push Checklist

Before pushing ANY step:
1. ✅ Test case ID exists in ContextQA (obtained from `create_test_case` result)
2. ✅ Positions are sequential starting at 0, no gaps, no duplicates
3. ✅ All payloads have passed Phase 4 validation
4. ✅ At least 1 `navigateToUrl` step exists at position 0 of each test case

### 5.2 Pushing Steps

Call `create_complex_test_step(payload)` for each step in order.

On success: response is a dict with `"id"` key — record the step ID.

On failure (response is string, not dict with "id"):
1. Log the error: `"Step {position} failed: {response}"`
2. Apply Phase 4 auto-fixes to the payload
3. Retry once
4. If second attempt fails: record as FAILED, continue to next step
5. After all steps: report failed steps to user with their payloads

### 5.3 Post-Push Verification

After creating all steps for a test case:
1. Call `get_test_case(test_case_id)` to verify step count matches expected
2. Report:
   - ✅ Steps created: N
   - ❌ Steps failed: M (list which ones)
   - 📋 Test case URL: `https://contextqatest.contextqa.com/td/cases/{id}/steps?source=ai_chat`

### 5.4 Optional: Run the Test

If the user asked to run the test after creating it:
1. Call `execute_test_case(test_case_id)`
2. Poll `get_execution_status(session_id=<session_id>)` every 10 seconds (up to 10 rounds), or just let `execute_test_case` wait
3. If COMPLETED: report pass/fail per step
4. If FAILED: call `get_execution_step_details(execution_id)` and report which step failed + screenshot URL

---

## Phase 6 — Node-by-Node Test Matrix

### HTTP Request Nodes

**Generate these steps for each HTTP node:**

| Step | Type | Purpose |
|------|------|---------|
| `[NodeName] - API Call` | `restApi` | Execute the API call with test data |
| `[NodeName] - Validate Response` | `custom_step_code` | JS: check status, required fields, types |

**Test data per scenario:**

| Scenario | URL | Method | Body | Expected Status |
|----------|-----|--------|------|-----------------|
| Happy Path | real URL with valid `${var_N}` | node's method | valid JSON | 200 / 201 |
| Negative | real URL | node's method | missing required field | 400 / 422 |
| Auth Error | real URL | node's method | valid body, invalid token | 401 / 403 |
| Edge Case | real URL | node's method | oversized body, special chars | 400 / 413 |

**Negative test validation change:**
In the negative test's restApi step, change `event.validation.status[0].expectedValue` to `400`.

---

### Code Nodes (n8n-nodes-base.code)

**Generate these steps:**

| Step | Type | Purpose |
|------|------|---------|
| `[NodeName] - Execute` | `custom_step_code` | Paste the actual JS from node (IIFE-wrapped) |
| `[NodeName] - Validate Output` | `custom_step_code` | JS: verify output shape matches expected |

**For the Execute step**, use the actual `parameters.jsCode` from the node, wrapped in IIFE.

**For the Validate Output step**, inspect the code to determine what fields it outputs, then write Pattern C assertion.

**Negative test:** Modify input to the code step so required fields are missing — assert the code throws or outputs an error flag.

---

### AI Nodes (OpenAI / LangChain)

**Generate these steps:**

| Step | Type | Purpose |
|------|------|---------|
| `[NodeName] - AI Task` | `ai_text_actions` | Run the AI prompt |
| `[NodeName] - Validate AI Output` | `custom_step_code` | JS: check non-empty, minimum words, structure |

**Happy Path prompt:** use the actual prompt from `parameters.messages[0].content` or `parameters.prompt`, replacing `{{ expr }}` with test values.

**Negative prompt:** send an empty string or a prompt that should produce an error response.

**Edge case prompt:** send a very long prompt (2000+ words) and verify the AI still responds.

---

### Branch / If Nodes (n8n-nodes-base.if)

**Generate these steps — one per branch:**

| Step | Type | Purpose |
|------|------|---------|
| `[NodeName] - TRUE branch input` | `custom_step_code` | JS: set up test data that satisfies the condition |
| `[NodeName] - TRUE branch assertion` | `custom_step_code` | JS: verify data flows to true-path node |
| `[NodeName] - FALSE branch input` | `custom_step_code` | JS: set up test data that fails the condition |
| `[NodeName] - FALSE branch assertion` | `custom_step_code` | JS: verify data flows to false-path node |

Extract the condition from `parameters.conditions.conditions[0]`:
- `leftValue` — the field being checked
- `operation` — equals / notEquals / contains / isEmpty / etc.
- `rightValue` — expected value

Write JS that mirrors this logic.

---

### Set Nodes (n8n-nodes-base.set)

**Generate these steps:**

| Step | Type | Purpose |
|------|------|---------|
| `[NodeName] - Verify Assignments` | `custom_step_code` | JS: verify each assignment from parameters.assignments.assignments |

Extract assignments from `parameters.assignments.assignments`:
```
[{ name: "wordpress_url", value: "https://..." }, ...]
```

Write JS that verifies each expected field is present in the output.

---

### Integration Placeholder Nodes (wordpress, form, merge, splitOut, webhook, etc.)

**Generate one step:**

| Step | Type | Purpose |
|------|------|---------|
| `[NodeName] - Integration Placeholder` | `custom_step_code` | Pattern G passthrough with node details in comments |

In the passthrough code comments, include:
- Node name, type
- Key parameters (first 5)
- Manual testing hint

---

## Phase 7 — End-to-End Worked Example: TesterTom Workflow

### Workflow: TesterTom Website Blog SEO Automation
**n8n Workflow ID:** `fofTLnI686yROKSm`
**Nodes:** 27 total (10 sticky notes skipped = 17 active)
**App URL:** `https://testertom.com/`

### Node Inventory (17 active nodes)

| # | Name | Category | Branch Point |
|---|------|----------|-------------|
| 1 | On form submission | FORM | No |
| 2 | Settings Configuration1 | SET | No |
| 3 | Create SEO Structure1 | AI | No |
| 4 | Validate Structure1 | BRANCH | Yes (true→Validate Keywords1, false→Error Response1) |
| 5 | Validate Keywords1 | CODE | No |
| 6 | Split Chapters1 | MERGE | No |
| 7 | Prepare Image Generation1 | CODE | No |
| 8 | Write Chapter1 | AI | No |
| 9 | Generate Featured Image1 | AI | No |
| 10 | Merge Chapters1 | MERGE | No |
| 11 | Merge with Image1 | MERGE | No |
| 12 | Compile Article1 | CODE | No |
| 13 | Upload Featured Image1 | HTTP | No |
| 14 | Create WordPress Post1 | INTEGRATION | No |
| 15 | Update SEO Metadata1 | HTTP | No |
| 16 | Success Notification1 | FORM | No |
| 17 | Error Response1 | FORM | No |

### Test Plan (5 test cases)

```
TC-1: TesterTom SEO Automation - Happy Path
TC-2: TesterTom SEO Automation - Error Path (invalid structure → Validate Structure1 FALSE)
TC-3: TesterTom SEO Automation - Edge Cases (empty keywords, oversized content)
TC-4: TesterTom SEO Automation - Validate Structure TRUE Branch
TC-5: TesterTom SEO Automation - Validate Structure FALSE Branch
```

### TC-1: Happy Path — Steps

```
Position 0:  navigateToUrl            → https://testertom.com/
Position 1:  custom_step_code         → On form submission: Document trigger fields (placeholder)
Position 2:  custom_step_code         → Settings Configuration: Verify wordpress_url, api_key assigned
Position 3:  ai_text_actions          → Create SEO Structure: "Generate SEO structure for keyword: AI Testing Tools"
Position 4:  custom_step_code         → Validate Create SEO Structure output (Pattern D)
Position 5:  custom_step_code         → Validate Structure TRUE branch mirror (Pattern E)
Position 6:  custom_step_code         → Validate Keywords: Execute js code (Pattern C output validation)
Position 7:  custom_step_code         → Split Chapters: Passthrough placeholder (Pattern G)
Position 8:  custom_step_code         → Prepare Image Generation: Execute js code
Position 9:  ai_text_actions          → Write Chapter: "Write a detailed chapter about AI Testing Tools"
Position 10: custom_step_code         → Validate Write Chapter output (Pattern D, min 100 words)
Position 11: ai_text_actions          → Generate Featured Image: "A professional image representing AI Testing"
Position 12: custom_step_code         → Validate Generate Featured Image output (Pattern D)
Position 13: custom_step_code         → Merge Chapters: Passthrough placeholder (Pattern G)
Position 14: custom_step_code         → Merge with Image: Passthrough placeholder (Pattern G)
Position 15: custom_step_code         → Compile Article: Execute js code, validate title+body+metadata output
Position 16: restApi                  → Upload Featured Image1: POST to WordPress media API
Position 17: custom_step_code         → Validate Upload Featured Image response (Pattern A, check response.id)
Position 18: custom_step_code         → Create WordPress Post: WordPress integration placeholder (Pattern G)
Position 19: restApi                  → Update SEO Metadata1: POST to WordPress posts API
Position 20: custom_step_code         → Validate Update SEO Metadata response (Pattern A, status 200)
Position 21: custom_step_code         → Success Notification: Document form response (placeholder)
```

### TC-2: Error Path — Steps (abbreviated)

```
Position 0:  navigateToUrl            → https://testertom.com/
Position 1:  custom_step_code         → On form submission: Submit with missing required fields
Position 2:  custom_step_code         → Settings Configuration: Verify default/empty values assigned
Position 3:  ai_text_actions          → Create SEO Structure: "Generate SEO structure" (incomplete input)
Position 4:  custom_step_code         → Validate Structure FALSE branch mirror (Pattern E, negated)
Position 5:  custom_step_code         → Error Response1: Verify error form is shown (placeholder)
```

### TC-3: Edge Cases — Assertions

For the `custom_step_code` assertion after the AI step, use:
```javascript
(function() {
  // Edge case: empty keyword input
  var input = $input.first().json;
  var keyword = input.keywords || input.focusKeyword || '';
  if (keyword.trim().length > 0) {
    throw new Error('Edge case test requires empty keyword, got: ' + keyword);
  }
  return [{ json: { edgeCase: 'empty_keyword', validated: true } }];
})();
```

For the HTTP steps in the edge case test, set `event.validation.status[0].expectedValue` to `400`.

---

## Quick Reference Card

### Action Names & IDs
```
restApi              → naturalTextActionId: 2
custom_step_code     → naturalTextActionId: 452
ai_text_actions      → naturalTextActionId: 568
navigateToUrl        → naturalTextActionId: 425
```

### n8n Category → Step Type
```
httprequest  → restApi + custom_step_code (assertion)
code         → custom_step_code (execute) + custom_step_code (validate)
ai/openai    → ai_text_actions + custom_step_code (validate)
if/switch    → custom_step_code (both branches)
set          → custom_step_code (verify assignments)
form/merge   → custom_step_code (Pattern G placeholder)
stickyNote   → SKIP
```

### IIFE Wrapper
```javascript
(function() {
  // your code here
})();
```

### Empty dataMapBean
```json
{
  "condition_if": [],
  "for_loop": { "startIndex": null, "endIndex": null, "testDataId": null }
}
```

### Empty dataMapJson
```json
{ "conditionIf": [] }
```

### Minimum Valid REST Status Check
```json
{
  "status": [{ "verificationType": "equals", "expectedType": "number", "expectedValue": 200 }]
}
```

---

## Common Mistakes to Avoid

1. **Never** use Python-style `None` in JSON — always use `null`
2. **Never** set `testDataList` to an array for `ai_text_actions` — it must be `null`
3. **Never** set `event.type` to `"ACTION_TEXT"` for `ai_text_actions` — it must be `"create_step"`
4. **Never** skip IIFE wrapping for `custom_step_code` — all JS must be wrapped
5. **Never** set `event.method` to uppercase — always lowercase
6. **Never** create a step for `stickyNote` nodes
7. **Never** create a step before the `navigateToUrl` step (position 0)
8. **Never** reuse positions — each step must have a unique, sequential position starting at 0
9. **Always** validate JSON before calling `create_complex_test_step`
10. **Always** add an assertion step after HTTP and Code steps

---

*End of N8N Testing Skill — Version 1.0 | ContextQA MCP*
