n8n
Version:
n8n Workflow Automation Tool
225 lines (224 loc) • 10.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.identifyNodesForPinData = identifyNodesForPinData;
exports.identifyNodesForHints = identifyNodesForHints;
exports.generateMockHints = generateMockHints;
const backend_common_1 = require("@n8n/backend-common");
const di_1 = require("@n8n/di");
const n8n_workflow_1 = require("n8n-workflow");
const instance_ai_1 = require("@n8n/instance-ai");
const node_config_1 = require("./node-config");
function findAiRootNodeNames(workflow) {
const roots = new Set();
for (const nodeConns of Object.values(workflow.connections)) {
for (const [connType, outputs] of Object.entries(nodeConns)) {
if (!connType.startsWith('ai_') || !Array.isArray(outputs))
continue;
for (const group of outputs) {
if (!Array.isArray(group))
continue;
for (const conn of group) {
if (typeof conn === 'object' && conn !== null && 'node' in conn) {
roots.add(conn.node);
}
}
}
}
}
return roots;
}
function findAiSubNodeNames(workflow) {
const subNodes = new Set();
for (const [sourceName, nodeConns] of Object.entries(workflow.connections)) {
for (const connType of Object.keys(nodeConns)) {
if (connType.startsWith('ai_')) {
subNodes.add(sourceName);
}
}
}
return subNodes;
}
const BYPASS_NODE_TYPES = new Set([
'n8n-nodes-base.redis',
'n8n-nodes-base.mongoDb',
'n8n-nodes-base.mySql',
'n8n-nodes-base.postgres',
'n8n-nodes-base.microsoftSql',
'n8n-nodes-base.snowflake',
'n8n-nodes-base.kafka',
'n8n-nodes-base.rabbitmq',
'n8n-nodes-base.mqtt',
'n8n-nodes-base.amqp',
'n8n-nodes-base.ftp',
'n8n-nodes-base.ssh',
'n8n-nodes-base.ldap',
'n8n-nodes-base.emailSend',
'n8n-nodes-base.rssFeedRead',
'n8n-nodes-base.git',
]);
function identifyNodesForPinData(workflow) {
const aiRootNodes = findAiRootNodeNames(workflow);
return workflow.nodes.filter((node) => {
if (node.disabled)
return false;
if (aiRootNodes.has(node.name))
return true;
if (BYPASS_NODE_TYPES.has(node.type))
return true;
return false;
});
}
function identifyNodesForHints(workflow) {
const aiSubNodes = findAiSubNodeNames(workflow);
const aiRootNodes = findAiRootNodeNames(workflow);
const pinnedNodeNames = new Set(identifyNodesForPinData(workflow).map((n) => n.name));
return workflow.nodes.filter((node) => {
if (node.disabled)
return false;
if (aiSubNodes.has(node.name))
return false;
if (aiRootNodes.has(node.name))
return false;
if (pinnedNodeNames.has(node.name))
return false;
return true;
});
}
const SYSTEM_PROMPT = `You are a test data planner for n8n workflow automation. Your job is to create a consistent data context, trigger output data, and per-node hints that will guide an API mock server to generate realistic, coherent responses across all nodes in a workflow.
RULES:
1. Create a "globalContext" that defines the shared world — user IDs, entity names, channel names, email addresses, and relationships that ALL nodes should reference consistently.
2. Create a "triggerContent" object that represents the exact output the workflow's trigger/start node would produce. This is used as pin data (the node's output), so it must match what downstream nodes reference:
- Look at the trigger node's type to determine the output structure
- For webhook triggers: include { headers: {}, query: {}, body: { ...fields } } since downstream nodes reference $json.body.fieldName
- For service-specific triggers (Gmail Trigger, Slack Trigger, etc.): match the service's real event/message output format
- For schedule triggers: include timestamp fields
- For manual triggers: include the fields that downstream nodes reference
- CRITICAL: triggerContent must NEVER be an empty object ({}). Even for scenarios that test empty payloads ("empty submission", "no data", "missing fields"), emit the trigger envelope with empty *nested* fields — an empty webhook is { headers: {}, query: {}, body: {} }, a schedule with no context is { timestamp: "..." }. The workflow cannot execute without trigger output.
- CRITICAL: check what downstream nodes reference (e.g., $json.body.email, $json.subject, $json.text) and ensure those paths exist in triggerContent
3. Create a "nodeHints" object with one entry per node. Each hint describes what data that specific node's API response should contain, referencing entities from the global context.
4. Hints should describe the DATA CONTENT, not the API response format. The mock server already knows the API schema.
5. Ensure data flows logically through the workflow. If node A fetches items that node B processes, the items in A's hint should match what B expects.
6. Use realistic but clearly fake values (e.g., "jane@example.com", "U_abc123").
7. **If a "Test Scenario" section is provided, it OVERRIDES your default data generation.** Use the exact names, emails, values, and conditions described in the scenario. If the scenario says "no name field", do NOT include a name. If it says "email is not-an-email", use that exact value. The scenario defines the test — follow it precisely.
8. Return ONLY valid JSON, no explanation or markdown fencing.`;
function buildUserPrompt(workflow, nodeNames, scenarioHints) {
const sections = [
'Generate a consistent data context and per-node mock hints for this workflow.',
];
if (scenarioHints) {
sections.push('', '## Test Scenario', '', scenarioHints);
}
sections.push('', '## Workflow Nodes', '');
for (const node of workflow.nodes) {
let line = `- ${node.name} (${node.type})`;
const config = (0, node_config_1.extractNodeConfig)(node);
if (config) {
line += ` ${config}`;
}
sections.push(line);
}
sections.push('', '## Connections', '');
for (const [sourceName, nodeConns] of Object.entries(workflow.connections)) {
for (const [connType, outputs] of Object.entries(nodeConns)) {
if (!Array.isArray(outputs))
continue;
for (const group of outputs) {
if (!Array.isArray(group))
continue;
for (const conn of group) {
if (typeof conn === 'object' && conn !== null && 'node' in conn) {
sections.push(` ${sourceName} -[${connType}]-> ${conn.node}`);
}
}
}
}
}
sections.push('', '## Expected Output', '', '```json', '{');
sections.push(' "globalContext": "Shared entities: ...",');
sections.push(' "triggerContent": { "...exact output the trigger node would produce..." },');
sections.push(' "nodeHints": {');
for (let i = 0; i < Math.min(nodeNames.length, 3); i++) {
const comma = i < Math.min(nodeNames.length, 3) - 1 ? ',' : '';
sections.push(` "${nodeNames[i]}": "What data to return..."${comma}`);
}
if (nodeNames.length > 3)
sections.push(' ...');
sections.push(' }', '}', '```');
return sections.join('\n');
}
const MAX_HINT_ATTEMPTS = 2;
async function generateMockHints(options) {
const { workflow, nodeNames, scenarioHints } = options;
const emptyResult = {
globalContext: '',
nodeHints: {},
triggerContent: {},
warnings: [],
bypassPinData: {},
};
if (nodeNames.length === 0)
return emptyResult;
const userPrompt = buildUserPrompt(workflow, nodeNames, scenarioHints);
const warnings = [];
for (let attempt = 1; attempt <= MAX_HINT_ATTEMPTS; attempt++) {
let reason = '';
try {
const agent = (0, instance_ai_1.createEvalAgent)('eval-hint-generator', {
instructions: SYSTEM_PROMPT,
});
const result = await agent.generate(userPrompt, {
providerOptions: { anthropic: { maxTokens: 4096 } },
});
const text = (0, instance_ai_1.extractText)(result)
.replace(/^```(?:json)?\s*\n?/i, '')
.replace(/\n?\s*```\s*$/i, '')
.trim();
const parsed = (0, n8n_workflow_1.jsonParse)(text);
let globalContext = '';
if (typeof parsed.globalContext === 'string') {
globalContext = parsed.globalContext;
}
else if (typeof parsed.globalContext === 'object' && parsed.globalContext !== null) {
globalContext = JSON.stringify(parsed.globalContext);
}
if (typeof parsed.nodeHints !== 'object' ||
parsed.nodeHints === null ||
Array.isArray(parsed.nodeHints)) {
reason = `invalid nodeHints structure (raw: ${text.slice(0, 200)})`;
}
else {
const triggerContent = typeof parsed.triggerContent === 'object' &&
parsed.triggerContent !== null &&
!Array.isArray(parsed.triggerContent)
? parsed.triggerContent
: {};
if (Object.keys(triggerContent).length === 0) {
reason = 'empty triggerContent';
}
else {
const nodeHints = {};
for (const [key, value] of Object.entries(parsed.nodeHints)) {
nodeHints[key] = typeof value === 'string' ? value : JSON.stringify(value);
}
return {
globalContext,
nodeHints,
triggerContent: triggerContent,
warnings,
bypassPinData: {},
};
}
}
}
catch (error) {
reason = error instanceof Error ? error.message : String(error);
}
warnings.push(`Phase 1 attempt ${attempt}/${MAX_HINT_ATTEMPTS}: ${reason}`);
if (attempt < MAX_HINT_ATTEMPTS) {
di_1.Container.get(backend_common_1.Logger).warn(`[EvalMock] Phase 1 attempt ${attempt}/${MAX_HINT_ATTEMPTS} unusable (${reason}) — retrying`);
}
}
di_1.Container.get(backend_common_1.Logger).error(`[EvalMock] Phase 1 exhausted ${MAX_HINT_ATTEMPTS} attempts — ${warnings.join('; ')}`);
return { ...emptyResult, warnings };
}
//# sourceMappingURL=workflow-analysis.js.map