@path58/p58-n8n 0.2.16 → 0.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -22145,6 +22145,7 @@ __export(n8n_api_adapter_exports, {
|
|
|
22145
22145
|
deleteCredential: () => deleteCredential,
|
|
22146
22146
|
deleteExecution: () => deleteExecution,
|
|
22147
22147
|
deleteWorkflow: () => deleteWorkflow,
|
|
22148
|
+
executeWorkflowViaRest: () => executeWorkflowViaRest,
|
|
22148
22149
|
extractNodeDebugData: () => extractNodeDebugData,
|
|
22149
22150
|
getCredentialWithData: () => getCredentialWithData,
|
|
22150
22151
|
getExecution: () => getExecution,
|
|
@@ -22332,6 +22333,34 @@ async function activateWorkflow(config2, workflowId) {
|
|
|
22332
22333
|
throw buildN8nError(error);
|
|
22333
22334
|
}
|
|
22334
22335
|
}
|
|
22336
|
+
async function executeWorkflowViaRest(config2, workflowId, workflowData) {
|
|
22337
|
+
const email = process.env.N8N_USER_EMAIL;
|
|
22338
|
+
const password = process.env.N8N_USER_PASSWORD;
|
|
22339
|
+
if (!email || !password) {
|
|
22340
|
+
throw new Error("N8N_USER_EMAIL and N8N_USER_PASSWORD are required for REST workflow execution");
|
|
22341
|
+
}
|
|
22342
|
+
const restBaseUrl = deriveInstanceBaseUrl(config2.baseUrl);
|
|
22343
|
+
const cookie = await getRestSessionCookieWith(restBaseUrl, email, password);
|
|
22344
|
+
const cfHeaders = {};
|
|
22345
|
+
if (process.env.CF_ACCESS_CLIENT_ID && process.env.CF_ACCESS_CLIENT_SECRET) {
|
|
22346
|
+
cfHeaders["CF-Access-Client-Id"] = process.env.CF_ACCESS_CLIENT_ID;
|
|
22347
|
+
cfHeaders["CF-Access-Client-Secret"] = process.env.CF_ACCESS_CLIENT_SECRET;
|
|
22348
|
+
}
|
|
22349
|
+
const requestBody = workflowData && typeof workflowData === "object" && !Array.isArray(workflowData) ? { workflowData: { ...workflowData, id: workflowId } } : workflowData ? { workflowData } : {};
|
|
22350
|
+
try {
|
|
22351
|
+
const res = await axios_default.post(`${restBaseUrl}/rest/workflows/${workflowId}/run`, requestBody, {
|
|
22352
|
+
timeout: config2.timeoutMs ?? 3e4,
|
|
22353
|
+
headers: {
|
|
22354
|
+
Cookie: cookie,
|
|
22355
|
+
"Content-Type": "application/json",
|
|
22356
|
+
...cfHeaders
|
|
22357
|
+
}
|
|
22358
|
+
});
|
|
22359
|
+
return res.data?.data ?? {};
|
|
22360
|
+
} catch (error) {
|
|
22361
|
+
throw buildN8nError(error);
|
|
22362
|
+
}
|
|
22363
|
+
}
|
|
22335
22364
|
async function deactivateWorkflow(config2, workflowId) {
|
|
22336
22365
|
const client = createN8nHttpClient(config2);
|
|
22337
22366
|
try {
|
|
@@ -41626,6 +41655,11 @@ async function buildNodeInfoResponse(nodeType, includeSections) {
|
|
|
41626
41655
|
response.best_practices = node.best_practices ?? null;
|
|
41627
41656
|
response.common_errors = node.common_errors ?? null;
|
|
41628
41657
|
response.node_role = node.node_role ?? null;
|
|
41658
|
+
} else {
|
|
41659
|
+
const configHints = node.best_practices?.configuration;
|
|
41660
|
+
if (Array.isArray(configHints) && configHints.length > 0) {
|
|
41661
|
+
response.best_practices = { configuration: configHints };
|
|
41662
|
+
}
|
|
41629
41663
|
}
|
|
41630
41664
|
if (nodeType === "n8n-nodes-base.code") {
|
|
41631
41665
|
response.coding_reference = CODE_NODE_REFERENCE;
|
|
@@ -41740,14 +41774,15 @@ Returns:
|
|
|
41740
41774
|
- properties: Node-level configuration properties (optional)
|
|
41741
41775
|
- config_examples: Real-world configuration examples from validated workflows (optional)
|
|
41742
41776
|
- version_info: Available type versions for this node (optional)
|
|
41743
|
-
- enrichment: best_practices (structured JSON), common_errors (structured JSON), node_role (string) \u2014
|
|
41777
|
+
- enrichment: best_practices (structured JSON), common_errors (structured JSON), node_role (string) \u2014 best_practices.configuration is auto-included when non-empty; full enrichment is opt-in
|
|
41744
41778
|
- agent_card: summary, primary_use_case, node_class, discovery_paths, search_triggers \u2014 opt-in only
|
|
41745
41779
|
- ai_metadata: AI/LangChain node specialization \u2014 ai_type, supported_models, vendor, input/output types, chain_patterns, capabilities \u2014 opt-in only, returns null for non-AI nodes
|
|
41746
41780
|
- docs: documentation facts with source references (fact_type, fact text, confidence, page_title, source_url) \u2014 opt-in only, returns empty array for nodes without doc coverage
|
|
41747
41781
|
|
|
41748
41782
|
Use the 'include' parameter to request only specific sections for efficiency.
|
|
41749
41783
|
If 'include' is omitted, default sections are returned (operations, credentials, properties, config_examples, version_info).
|
|
41750
|
-
|
|
41784
|
+
best_practices.configuration is automatically included when non-empty \u2014 no opt-in needed for configuration hints.
|
|
41785
|
+
The 'enrichment' (full), 'agent_card', 'ai_metadata', and 'docs' sections are opt-in \u2014 request them explicitly when you need node intelligence context.
|
|
41751
41786
|
|
|
41752
41787
|
Examples:
|
|
41753
41788
|
- { "node_type": "n8n-nodes-base.httpRequest" } \u2192 Default sections (no enrichment)
|
|
@@ -41967,13 +42002,12 @@ Returns workflow_id and URL on success.`,
|
|
|
41967
42002
|
|
|
41968
42003
|
// dist/mcp/tools/handlers/execute-workflow.js
|
|
41969
42004
|
init_n8n_api_adapter();
|
|
41970
|
-
var
|
|
42005
|
+
var import_node_crypto = require("node:crypto");
|
|
41971
42006
|
init_timeout();
|
|
41972
42007
|
var import_logging34 = __toESM(require_logging(), 1);
|
|
41973
42008
|
|
|
41974
42009
|
// dist/mcp/tools/handlers/test-workflow.js
|
|
41975
42010
|
init_n8n_api_adapter();
|
|
41976
|
-
var import_node_crypto = require("node:crypto");
|
|
41977
42011
|
init_timeout();
|
|
41978
42012
|
var import_logging33 = __toESM(require_logging(), 1);
|
|
41979
42013
|
init_adapters2();
|
|
@@ -41999,6 +42033,7 @@ var CREDENTIAL_HINTS = {
|
|
|
41999
42033
|
var WEBHOOK_NOT_READY_HINT = "Webhook endpoint initialization delay \u2014 the workflow is deployed. Call test_workflow directly to retry the test.";
|
|
42000
42034
|
var WEBHOOK_CONFLICT_HINT = "Webhook URL conflict \u2014 another workflow is using the same webhook path. Call list_workflows to find the conflicting workflow, then either delete_workflow the old one or use partial_update_workflow to change this webhook path. Do NOT use Bash.";
|
|
42001
42035
|
var SCHEMA_FORMAT_HINT = "Input format error in operations payload. Check that array fields are arrays and string fields are strings. Review partial_update_workflow schema before retrying.";
|
|
42036
|
+
var MISSING_CREDENTIAL_TYPE_HINT = "Required credential type not available in n8n. Call list_credentials to check what credential types exist. If the required type is missing, call get_node_info on the failing node to check alternative auth modes (e.g. Service Account instead of OAuth2). If no compatible credential type exists, call build_workflow to rebuild using a webhook trigger or a node that uses an available credential type. Do NOT loop on partial_update_workflow \u2014 switch approach instead.";
|
|
42002
42037
|
function toMessage(error) {
|
|
42003
42038
|
if (!error)
|
|
42004
42039
|
return "";
|
|
@@ -42053,6 +42088,9 @@ function isExecutionTimeout(msg) {
|
|
|
42053
42088
|
function isUnknownNode(msg) {
|
|
42054
42089
|
return msg.includes("unknown node") || msg.includes("node type") && msg.includes("not found") || msg.includes("node type missing") || msg.includes("import error") || msg.includes("not a known node") || msg.includes("node not found");
|
|
42055
42090
|
}
|
|
42091
|
+
function isMissingCredentialType(msg) {
|
|
42092
|
+
return msg.includes("missing required credential") || msg.includes("no credentials found for");
|
|
42093
|
+
}
|
|
42056
42094
|
function isValidationError(msg) {
|
|
42057
42095
|
return msg.includes("validation") || msg.includes("schema mismatch") || msg.includes("missing required") || msg.includes("missing field") || msg.includes("invalid parameter") || msg.includes("required property") || msg.includes("configuration issue") || // "N nodes have configuration issues" (n8n publish error)
|
|
42058
42096
|
msg.includes("missing or invalid required");
|
|
@@ -42085,6 +42123,8 @@ function classifyFailure(error, context = {}) {
|
|
|
42085
42123
|
return classify("config_error", context, context.nodeType);
|
|
42086
42124
|
if (isSchemaError(msg))
|
|
42087
42125
|
return classify("partial_fix_possible", context, void 0, SCHEMA_FORMAT_HINT);
|
|
42126
|
+
if (isMissingCredentialType(msg))
|
|
42127
|
+
return classify("credential_error", context, void 0, MISSING_CREDENTIAL_TYPE_HINT);
|
|
42088
42128
|
if (isValidationError(msg))
|
|
42089
42129
|
return classify("partial_fix_possible", context);
|
|
42090
42130
|
if (isMultipleFailures(msg))
|
|
@@ -42291,15 +42331,6 @@ async function fetchWebhookExecution(apiConfig, workflowId) {
|
|
|
42291
42331
|
return EMPTY_EXECUTION_DATA;
|
|
42292
42332
|
}
|
|
42293
42333
|
}
|
|
42294
|
-
function annotateCloneDebug(debug) {
|
|
42295
|
-
const hasErrors = Object.values(debug).some((v) => v?.status === "error" || v?.status === "failed");
|
|
42296
|
-
if (!hasErrors)
|
|
42297
|
-
return debug;
|
|
42298
|
-
return {
|
|
42299
|
-
...debug,
|
|
42300
|
-
_clone_test_note: "IMPORTANT: Node errors in this debug data are CLONE ARTIFACTS. The clone uses shared credentials which may not work identically. The overall test PASSED \u2014 do NOT fix these node-level errors."
|
|
42301
|
-
};
|
|
42302
|
-
}
|
|
42303
42334
|
function extractExecutedNodeNames(execution) {
|
|
42304
42335
|
const runData = execution?.data?.resultData?.runData;
|
|
42305
42336
|
if (!runData || typeof runData !== "object")
|
|
@@ -42454,10 +42485,6 @@ async function executeWebhookTrigger(workflow, payload, timeoutMs, apiConfig) {
|
|
|
42454
42485
|
}
|
|
42455
42486
|
}
|
|
42456
42487
|
}
|
|
42457
|
-
var CLONE_TRIGGER_KEYWORDS = ["trigger", "webhook", "cron", "schedule", "polling"];
|
|
42458
|
-
var WEBHOOK_INITIAL_WAIT_MS = 3e3;
|
|
42459
|
-
var WEBHOOK_MAX_ATTEMPTS = 5;
|
|
42460
|
-
var WEBHOOK_RETRY_DELAYS_MS = [1e3, 2e3, 4e3, 8e3];
|
|
42461
42488
|
var RATE_LIMIT_MAX_RETRIES = 3;
|
|
42462
42489
|
var RATE_LIMIT_RETRY_DELAYS_MS = [2e3, 4e3, 8e3];
|
|
42463
42490
|
function isRateLimitError(error) {
|
|
@@ -42477,214 +42504,66 @@ function buildRateLimitedResult(triggerType, webhookPath) {
|
|
|
42477
42504
|
next_step: "Credential is temporarily throttled. Do not debug with Bash. Wait 30s and retry test_workflow."
|
|
42478
42505
|
};
|
|
42479
42506
|
}
|
|
42480
|
-
async function
|
|
42481
|
-
for (let retry = 0; retry < RATE_LIMIT_MAX_RETRIES; retry++) {
|
|
42482
|
-
const delay = RATE_LIMIT_RETRY_DELAYS_MS[retry];
|
|
42483
|
-
import_logging33.logger.info("test_workflow: clone 429 retry", { attempt: retry + 1, delay, webhookPath });
|
|
42484
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
42485
|
-
try {
|
|
42486
|
-
await postWebhook(buildN8nHostUrl(), webhookPath, testPayload, 15e3);
|
|
42487
|
-
return "success";
|
|
42488
|
-
} catch (retryErr) {
|
|
42489
|
-
if (!isRateLimitError(retryErr))
|
|
42490
|
-
return "success";
|
|
42491
|
-
}
|
|
42492
|
-
}
|
|
42493
|
-
return "exhausted";
|
|
42494
|
-
}
|
|
42495
|
-
function extractCredentialIds(nodes) {
|
|
42496
|
-
const ids = /* @__PURE__ */ new Set();
|
|
42497
|
-
for (const node of nodes) {
|
|
42498
|
-
if (!node.credentials || typeof node.credentials !== "object")
|
|
42499
|
-
continue;
|
|
42500
|
-
for (const cred of Object.values(node.credentials)) {
|
|
42501
|
-
const id = cred?.id;
|
|
42502
|
-
if (typeof id === "string" && id)
|
|
42503
|
-
ids.add(id);
|
|
42504
|
-
}
|
|
42505
|
-
}
|
|
42506
|
-
return [...ids];
|
|
42507
|
-
}
|
|
42508
|
-
function extractCredentialNames(nodes) {
|
|
42509
|
-
const names = [];
|
|
42510
|
-
for (const node of nodes) {
|
|
42511
|
-
if (!node.credentials || typeof node.credentials !== "object")
|
|
42512
|
-
continue;
|
|
42513
|
-
for (const [credType, cred] of Object.entries(node.credentials)) {
|
|
42514
|
-
const name = cred?.name;
|
|
42515
|
-
names.push(typeof name === "string" && name ? `${name} (${credType})` : credType);
|
|
42516
|
-
}
|
|
42517
|
-
}
|
|
42518
|
-
return [...new Set(names)];
|
|
42519
|
-
}
|
|
42520
|
-
async function testViaClone(workflow, triggerType, testPayload, timeoutMs, apiConfig) {
|
|
42521
|
-
const hasR2W = await hasRespondToWebhookNode(workflow);
|
|
42522
|
-
if (hasR2W) {
|
|
42523
|
-
return {
|
|
42524
|
-
execution_id: null,
|
|
42525
|
-
status: "error",
|
|
42526
|
-
trigger_type: triggerType,
|
|
42527
|
-
result_summary: "Cannot test via clone: workflow contains a Respond to Webhook node. Test this workflow using test_workflow with a direct webhook POST.",
|
|
42528
|
-
next_step: "This workflow has a Respond to Webhook node. Call test_workflow directly \u2014 it handles the response mode correctly.",
|
|
42529
|
-
failure_type: "config_error",
|
|
42530
|
-
recovery_hint: "Node configuration error. Review the workflow structure \u2014 Respond to Webhook nodes require direct webhook testing."
|
|
42531
|
-
};
|
|
42532
|
-
}
|
|
42533
|
-
const nodes = workflow.nodes ?? [];
|
|
42534
|
-
const triggerNode = nodes.find((n) => {
|
|
42535
|
-
const type = String(n?.type ?? "").toLowerCase();
|
|
42536
|
-
return CLONE_TRIGGER_KEYWORDS.some((kw) => type.includes(kw));
|
|
42537
|
-
});
|
|
42538
|
-
const triggerName = triggerNode?.name ?? "Manual Trigger";
|
|
42539
|
-
const triggerPosition = triggerNode?.position ?? [250, 300];
|
|
42540
|
-
const webhookPath = `test-clone-${(0, import_node_crypto.randomUUID)().slice(0, 8)}`;
|
|
42541
|
-
const cloneNodes = nodes.map((node) => {
|
|
42542
|
-
if (node.name === triggerName) {
|
|
42543
|
-
return {
|
|
42544
|
-
parameters: { path: webhookPath, httpMethod: "POST", options: {} },
|
|
42545
|
-
id: node.id ?? "webhook-clone",
|
|
42546
|
-
name: "Webhook",
|
|
42547
|
-
type: "n8n-nodes-base.webhook",
|
|
42548
|
-
typeVersion: 2,
|
|
42549
|
-
position: triggerPosition,
|
|
42550
|
-
webhookId: (0, import_node_crypto.randomUUID)()
|
|
42551
|
-
};
|
|
42552
|
-
}
|
|
42553
|
-
return node;
|
|
42554
|
-
});
|
|
42555
|
-
const connections = { ...workflow.connections ?? {} };
|
|
42556
|
-
if (triggerName !== "Webhook" && connections[triggerName]) {
|
|
42557
|
-
connections["Webhook"] = connections[triggerName];
|
|
42558
|
-
delete connections[triggerName];
|
|
42559
|
-
}
|
|
42560
|
-
const cloneStartMs = Date.now();
|
|
42507
|
+
async function testViaDirectExecution(workflow, triggerType, _testPayload, timeoutMs, apiConfig) {
|
|
42561
42508
|
const { calculateAdaptiveTimeout: calculateAdaptiveTimeout2 } = await Promise.resolve().then(() => (init_timeout(), timeout_exports));
|
|
42562
42509
|
const adaptiveTimeout = calculateAdaptiveTimeout2(workflow);
|
|
42563
42510
|
const effectiveTimeout = Math.max(timeoutMs, adaptiveTimeout);
|
|
42564
42511
|
if (effectiveTimeout > timeoutMs) {
|
|
42565
|
-
import_logging33.logger.info("test_workflow: adaptive timeout (
|
|
42512
|
+
import_logging33.logger.info("test_workflow: adaptive timeout (direct)", {
|
|
42566
42513
|
workflowId: workflow.id,
|
|
42567
42514
|
requested: timeoutMs,
|
|
42568
42515
|
adaptive: adaptiveTimeout,
|
|
42569
42516
|
effective: effectiveTimeout
|
|
42570
42517
|
});
|
|
42571
42518
|
}
|
|
42572
|
-
let cloneId = null;
|
|
42573
42519
|
try {
|
|
42574
|
-
const
|
|
42575
|
-
|
|
42576
|
-
|
|
42577
|
-
|
|
42578
|
-
});
|
|
42579
|
-
cloneId = createResult.id;
|
|
42580
|
-
const credentialIds = extractCredentialIds(nodes);
|
|
42581
|
-
await shareCredentialsWithWorkflow(apiConfig, credentialIds, cloneId);
|
|
42582
|
-
const activateResult = await activateWorkflow(apiConfig, cloneId);
|
|
42583
|
-
if (!activateResult.active) {
|
|
42584
|
-
const credNames = extractCredentialNames(nodes);
|
|
42585
|
-
const credDetail = credNames.length > 0 ? ` Required credentials: ${credNames.join(", ")}.` : "";
|
|
42520
|
+
const persistedWorkflow = await getWorkflow(apiConfig, workflow.id);
|
|
42521
|
+
const runResult = await executeWorkflowViaRest(apiConfig, workflow.id, persistedWorkflow);
|
|
42522
|
+
const executionId = runResult.executionId ?? runResult.id ?? null;
|
|
42523
|
+
if (!executionId) {
|
|
42586
42524
|
return {
|
|
42587
42525
|
execution_id: null,
|
|
42588
42526
|
status: "error",
|
|
42589
42527
|
trigger_type: triggerType,
|
|
42590
|
-
result_summary:
|
|
42591
|
-
next_step:
|
|
42592
|
-
failure_type: "
|
|
42593
|
-
recovery_hint: "
|
|
42594
|
-
};
|
|
42595
|
-
}
|
|
42596
|
-
await new Promise((r) => setTimeout(r, WEBHOOK_INITIAL_WAIT_MS));
|
|
42597
|
-
let webhookRegistrationFailed = false;
|
|
42598
|
-
let rateLimitExhausted = false;
|
|
42599
|
-
for (let attempt = 0; attempt < WEBHOOK_MAX_ATTEMPTS; attempt++) {
|
|
42600
|
-
try {
|
|
42601
|
-
if (attempt > 0)
|
|
42602
|
-
await new Promise((r) => setTimeout(r, WEBHOOK_RETRY_DELAYS_MS[attempt - 1]));
|
|
42603
|
-
await postWebhook(buildN8nHostUrl(), webhookPath, testPayload, 15e3);
|
|
42604
|
-
break;
|
|
42605
|
-
} catch (webhookErr) {
|
|
42606
|
-
const errMsg = webhookErr instanceof Error ? webhookErr.message : String(webhookErr);
|
|
42607
|
-
const is404 = errMsg.includes("404") || errMsg.includes("not found");
|
|
42608
|
-
const isTimeout = errMsg.includes("timeout");
|
|
42609
|
-
if (isTimeout)
|
|
42610
|
-
break;
|
|
42611
|
-
if (is404 && attempt < WEBHOOK_MAX_ATTEMPTS - 1) {
|
|
42612
|
-
import_logging33.logger.debug("test_workflow: webhook 404, retrying activation race", { attempt, webhookPath });
|
|
42613
|
-
continue;
|
|
42614
|
-
}
|
|
42615
|
-
if (is404) {
|
|
42616
|
-
webhookRegistrationFailed = true;
|
|
42617
|
-
break;
|
|
42618
|
-
}
|
|
42619
|
-
if (isRateLimitError(webhookErr)) {
|
|
42620
|
-
const rlResult = await retryCloneWebhookOn429(webhookPath, testPayload);
|
|
42621
|
-
if (rlResult === "exhausted") {
|
|
42622
|
-
rateLimitExhausted = true;
|
|
42623
|
-
break;
|
|
42624
|
-
}
|
|
42625
|
-
if (rlResult === "success")
|
|
42626
|
-
break;
|
|
42627
|
-
}
|
|
42628
|
-
throw webhookErr;
|
|
42629
|
-
}
|
|
42630
|
-
}
|
|
42631
|
-
if (rateLimitExhausted) {
|
|
42632
|
-
return buildRateLimitedResult(triggerType, webhookPath);
|
|
42633
|
-
}
|
|
42634
|
-
if (webhookRegistrationFailed) {
|
|
42635
|
-
return {
|
|
42636
|
-
execution_id: null,
|
|
42637
|
-
status: "error",
|
|
42638
|
-
trigger_type: triggerType,
|
|
42639
|
-
result_summary: `Clone webhook "POST ${webhookPath}" failed to register after 5 retries (~18s). n8n may be under heavy load from concurrent processes.`,
|
|
42640
|
-
next_step: "Clone webhook failed to register after retries. Do NOT use Bash to retry manually. Report workflow as built-but-untested and stop.",
|
|
42641
|
-
failure_type: "config_error",
|
|
42642
|
-
recovery_hint: "Unclassified error. Review error message. Consider get_node_info for the failing node."
|
|
42528
|
+
result_summary: "Direct REST execution did not return an execution ID.",
|
|
42529
|
+
next_step: "Retry test_workflow once. If the workflow still does not return an execution ID, stop and inspect the n8n instance.",
|
|
42530
|
+
failure_type: "execution_timeout",
|
|
42531
|
+
recovery_hint: "Internal REST execution did not return an execution ID. Retry test_workflow once; avoid rebuilding unless the workflow structure is clearly wrong."
|
|
42643
42532
|
};
|
|
42644
42533
|
}
|
|
42645
|
-
|
|
42646
|
-
|
|
42647
|
-
const safetyDeadline = Date.now() + SAFETY_CAP_MS;
|
|
42648
|
-
import_logging33.logger.info("test_workflow: polling for clone execution (state-driven)", {
|
|
42534
|
+
const safetyDeadline = Date.now() + effectiveTimeout;
|
|
42535
|
+
import_logging33.logger.info("test_workflow: polling for direct execution via REST run", {
|
|
42649
42536
|
workflowId: workflow.id,
|
|
42650
|
-
|
|
42651
|
-
|
|
42537
|
+
executionId,
|
|
42538
|
+
safetyCapMs: effectiveTimeout
|
|
42652
42539
|
});
|
|
42653
|
-
let
|
|
42654
|
-
let latestExecId = null;
|
|
42655
|
-
let cloneExecFailed = false;
|
|
42540
|
+
let executionFailed = false;
|
|
42656
42541
|
let nodeDebug;
|
|
42657
42542
|
while (Date.now() < safetyDeadline) {
|
|
42658
|
-
const
|
|
42659
|
-
|
|
42660
|
-
|
|
42661
|
-
|
|
42662
|
-
|
|
42663
|
-
if (fullExec.finished || fullExec.status === "error" || fullExec.status === "success") {
|
|
42664
|
-
latestExecId = latest.id;
|
|
42665
|
-
cloneExecFailed = fullExec.status === "error";
|
|
42666
|
-
nodeDebug = extractNodeDebugData(fullExec);
|
|
42667
|
-
break;
|
|
42668
|
-
}
|
|
42543
|
+
const fullExec = await getExecution(apiConfig, executionId, true);
|
|
42544
|
+
if (fullExec.finished || fullExec.status === "error" || fullExec.status === "success") {
|
|
42545
|
+
executionFailed = fullExec.status === "error";
|
|
42546
|
+
nodeDebug = extractNodeDebugData(fullExec);
|
|
42547
|
+
break;
|
|
42669
42548
|
}
|
|
42670
42549
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
42671
42550
|
}
|
|
42672
|
-
if (
|
|
42551
|
+
if (!nodeDebug && Date.now() >= safetyDeadline) {
|
|
42673
42552
|
try {
|
|
42674
|
-
await deleteExecution(apiConfig,
|
|
42675
|
-
import_logging33.logger.warn("test_workflow: cancelled execution at safety cap
|
|
42553
|
+
await deleteExecution(apiConfig, executionId);
|
|
42554
|
+
import_logging33.logger.warn("test_workflow: cancelled direct execution at safety cap", {
|
|
42676
42555
|
workflowId: workflow.id,
|
|
42677
|
-
executionId
|
|
42556
|
+
executionId
|
|
42678
42557
|
});
|
|
42679
42558
|
} catch (cancelErr) {
|
|
42680
|
-
import_logging33.logger.warn("test_workflow: failed to cancel execution", {
|
|
42559
|
+
import_logging33.logger.warn("test_workflow: failed to cancel direct execution", {
|
|
42681
42560
|
workflowId: workflow.id,
|
|
42682
|
-
executionId
|
|
42561
|
+
executionId,
|
|
42683
42562
|
error: cancelErr
|
|
42684
42563
|
});
|
|
42685
42564
|
}
|
|
42686
42565
|
}
|
|
42687
|
-
if (
|
|
42566
|
+
if (executionFailed && nodeDebug) {
|
|
42688
42567
|
const failedEntry = Object.entries(nodeDebug).find(([_, v]) => v?.status === "error");
|
|
42689
42568
|
if (failedEntry) {
|
|
42690
42569
|
const [nodeName, nodeData] = failedEntry;
|
|
@@ -42692,75 +42571,57 @@ async function testViaClone(workflow, triggerType, testPayload, timeoutMs, apiCo
|
|
|
42692
42571
|
const nodeErrMsg = errInfo?.message ?? "Unknown error";
|
|
42693
42572
|
const taxonomy = classifyFailure(errInfo ?? nodeErrMsg, { nodeName, httpCode: errInfo?.httpCode });
|
|
42694
42573
|
return {
|
|
42695
|
-
execution_id:
|
|
42574
|
+
execution_id: executionId,
|
|
42696
42575
|
status: "execution_error",
|
|
42697
42576
|
trigger_type: triggerType,
|
|
42698
42577
|
failed_node: nodeName,
|
|
42699
42578
|
node_error: nodeErrMsg,
|
|
42700
42579
|
node_error_description: errInfo?.description,
|
|
42701
|
-
result_summary: `
|
|
42702
|
-
next_step: `Fix the "${nodeName}" node configuration in
|
|
42580
|
+
result_summary: `Workflow execution FAILED at node "${nodeName}": ${nodeErrMsg}`,
|
|
42581
|
+
next_step: `Fix the "${nodeName}" node configuration in workflow ${workflow.id}, then retry test_workflow.`,
|
|
42703
42582
|
...taxonomy
|
|
42704
42583
|
};
|
|
42705
42584
|
}
|
|
42706
42585
|
}
|
|
42707
|
-
if (
|
|
42708
|
-
|
|
42709
|
-
|
|
42710
|
-
|
|
42711
|
-
|
|
42712
|
-
|
|
42586
|
+
if (!nodeDebug) {
|
|
42587
|
+
return {
|
|
42588
|
+
execution_id: executionId,
|
|
42589
|
+
status: "error",
|
|
42590
|
+
trigger_type: triggerType,
|
|
42591
|
+
result_summary: `Workflow execution did not complete within timeout for workflow ${workflow.id}.`,
|
|
42592
|
+
next_step: "Retry test_workflow once. If the workflow still times out, inspect the instance or run manually in n8n.",
|
|
42593
|
+
failure_type: "execution_timeout",
|
|
42594
|
+
recovery_hint: "Execution timed out before reaching a terminal state. Retry test_workflow once; avoid rebuilding unless the workflow structure is clearly wrong."
|
|
42595
|
+
};
|
|
42713
42596
|
}
|
|
42714
|
-
const annotatedDebug = latestExecId && nodeDebug ? annotateCloneDebug(nodeDebug) : nodeDebug;
|
|
42715
42597
|
return {
|
|
42716
|
-
execution_id:
|
|
42717
|
-
status:
|
|
42598
|
+
execution_id: executionId,
|
|
42599
|
+
status: "success",
|
|
42718
42600
|
trigger_type: triggerType,
|
|
42719
|
-
result_summary:
|
|
42720
|
-
next_step:
|
|
42721
|
-
raw_result:
|
|
42722
|
-
...!latestExecId ? { failure_type: "execution_timeout", recovery_hint: "Execution timed out. Use get_execution_result to check status. Workflow may be too slow or waiting for external input." } : {}
|
|
42601
|
+
result_summary: `Workflow tested successfully via direct REST execution on workflow ${workflow.id}.`,
|
|
42602
|
+
next_step: "Test passed. The workflow is working correctly. No further changes needed.",
|
|
42603
|
+
raw_result: nodeDebug ? { node_debug: nodeDebug } : void 0
|
|
42723
42604
|
};
|
|
42724
42605
|
} catch (err) {
|
|
42725
42606
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
42607
|
+
import_logging33.logger.warn("test_workflow: direct execution failed", {
|
|
42608
|
+
workflowId: workflow.id,
|
|
42609
|
+
error: errMsg
|
|
42610
|
+
});
|
|
42726
42611
|
return {
|
|
42727
42612
|
execution_id: null,
|
|
42728
42613
|
status: "error",
|
|
42729
42614
|
trigger_type: triggerType,
|
|
42730
|
-
result_summary: `
|
|
42615
|
+
result_summary: `Direct workflow execution failed: ${errMsg}.`,
|
|
42731
42616
|
...classifyFailure(err, {})
|
|
42732
42617
|
};
|
|
42733
|
-
} finally {
|
|
42734
|
-
if (cloneId) {
|
|
42735
|
-
try {
|
|
42736
|
-
await deleteWorkflow(apiConfig, cloneId);
|
|
42737
|
-
} catch {
|
|
42738
|
-
}
|
|
42739
|
-
}
|
|
42740
42618
|
}
|
|
42741
42619
|
}
|
|
42742
|
-
function buildGuidanceResult(triggerType) {
|
|
42743
|
-
const messages = {
|
|
42744
|
-
manual: "Activate the workflow, then trigger it manually in the n8n UI.",
|
|
42745
|
-
schedule: "Clone-based testing is disabled. Use execute_workflow to test schedule workflows on demand via clone-and-execute.",
|
|
42746
|
-
polling: "Clone-based testing is disabled. Use execute_workflow to test polling workflows on demand via clone-and-execute.",
|
|
42747
|
-
webhook: ""
|
|
42748
|
-
};
|
|
42749
|
-
return {
|
|
42750
|
-
execution_id: null,
|
|
42751
|
-
status: "guidance",
|
|
42752
|
-
trigger_type: triggerType,
|
|
42753
|
-
result_summary: messages[triggerType]
|
|
42754
|
-
};
|
|
42755
|
-
}
|
|
42756
42620
|
async function routeTestExecution(workflow, triggerType, testPayload, timeoutMs, apiConfig) {
|
|
42757
42621
|
if (triggerType === "webhook") {
|
|
42758
42622
|
return executeWebhookTrigger(workflow, testPayload, timeoutMs, apiConfig);
|
|
42759
42623
|
}
|
|
42760
|
-
|
|
42761
|
-
return buildGuidanceResult(triggerType);
|
|
42762
|
-
}
|
|
42763
|
-
return testViaClone(workflow, triggerType, testPayload, timeoutMs, apiConfig);
|
|
42624
|
+
return testViaDirectExecution(workflow, triggerType, testPayload, timeoutMs, apiConfig);
|
|
42764
42625
|
}
|
|
42765
42626
|
var activeTests = /* @__PURE__ */ new Set();
|
|
42766
42627
|
function buildConcurrentTestError(workflowId) {
|
|
@@ -42837,7 +42698,7 @@ and deactivates afterward. No need to construct URLs or use curl.
|
|
|
42837
42698
|
|
|
42838
42699
|
Example: test_workflow({ workflow_id: "abc123", test_payload: { name: "Test", email: "test@example.com" } })
|
|
42839
42700
|
|
|
42840
|
-
**Schedule/manual/polling:**
|
|
42701
|
+
**Schedule/manual/polling:** The tool executes the saved workflow directly via n8n's internal REST run path and polls the real execution. This tests the actual workflow with minimal overhead.
|
|
42841
42702
|
|
|
42842
42703
|
Returns: { execution_id, status, trigger_type, result_summary, http_status, webhook_response, execution_time_ms, nodes_executed, failed_node, node_error, node_error_description }
|
|
42843
42704
|
When status is "execution_error": the webhook POST succeeded (HTTP 2xx) but the workflow execution failed. failed_node contains the node name, node_error the error message. Fix the node and retry.
|
|
@@ -42878,7 +42739,7 @@ Follow the recovery_hint instead of using Bash commands.`,
|
|
|
42878
42739
|
|
|
42879
42740
|
// dist/mcp/tools/handlers/execute-workflow.js
|
|
42880
42741
|
var DEFAULT_POLL_INTERVAL_MS = 500;
|
|
42881
|
-
var
|
|
42742
|
+
var CLONE_TRIGGER_KEYWORDS = ["trigger", "webhook", "cron", "schedule", "polling"];
|
|
42882
42743
|
function buildApiConfig3(timeoutMs) {
|
|
42883
42744
|
return {
|
|
42884
42745
|
baseUrl: process.env.N8N_API_URL ?? process.env.N8N_API_BASE_URL ?? "http://localhost:5678/api/v1",
|
|
@@ -42912,7 +42773,7 @@ function findTriggerNodeType(workflow) {
|
|
|
42912
42773
|
const nodes = workflow.nodes ?? [];
|
|
42913
42774
|
const trigger = nodes.find((n) => {
|
|
42914
42775
|
const type = String(n?.type ?? "").toLowerCase();
|
|
42915
|
-
return
|
|
42776
|
+
return CLONE_TRIGGER_KEYWORDS.some((kw) => type.includes(kw));
|
|
42916
42777
|
});
|
|
42917
42778
|
return trigger ? String(trigger.type ?? "") : null;
|
|
42918
42779
|
}
|
|
@@ -43053,11 +42914,11 @@ function buildCloneWorkflow(original) {
|
|
|
43053
42914
|
const nodes = original.nodes ?? [];
|
|
43054
42915
|
const triggerNode = nodes.find((n) => {
|
|
43055
42916
|
const type = String(n?.type ?? "").toLowerCase();
|
|
43056
|
-
return
|
|
42917
|
+
return CLONE_TRIGGER_KEYWORDS.some((kw) => type.includes(kw));
|
|
43057
42918
|
});
|
|
43058
42919
|
const triggerName = triggerNode?.name ?? "Manual Trigger";
|
|
43059
42920
|
const triggerPosition = triggerNode?.position ?? [250, 300];
|
|
43060
|
-
const webhookPath = `clone-exec-${(0,
|
|
42921
|
+
const webhookPath = `clone-exec-${(0, import_node_crypto.randomUUID)().slice(0, 8)}`;
|
|
43061
42922
|
const cloneNodes = nodes.map((node) => {
|
|
43062
42923
|
if (node.name === triggerName) {
|
|
43063
42924
|
return {
|
|
@@ -43071,7 +42932,7 @@ function buildCloneWorkflow(original) {
|
|
|
43071
42932
|
type: "n8n-nodes-base.webhook",
|
|
43072
42933
|
typeVersion: 2,
|
|
43073
42934
|
position: triggerPosition,
|
|
43074
|
-
webhookId: (0,
|
|
42935
|
+
webhookId: (0, import_node_crypto.randomUUID)()
|
|
43075
42936
|
};
|
|
43076
42937
|
}
|
|
43077
42938
|
return node;
|
|
@@ -50624,7 +50485,6 @@ function substituteDeprecatedAiNodes(nodes, connections) {
|
|
|
50624
50485
|
|
|
50625
50486
|
// dist/mcp/tools/handlers/build-workflow.js
|
|
50626
50487
|
init_n8n_api_adapter();
|
|
50627
|
-
var import_node_crypto3 = require("node:crypto");
|
|
50628
50488
|
init_timeout();
|
|
50629
50489
|
var import_logging67 = __toESM(require_logging(), 1);
|
|
50630
50490
|
|
|
@@ -51815,10 +51675,20 @@ var SKIP_NODE_TYPE_PATTERNS = [
|
|
|
51815
51675
|
function isSkipNode(nodeType) {
|
|
51816
51676
|
return SKIP_NODE_TYPE_PATTERNS.some((p) => nodeType.toLowerCase() === p.toLowerCase());
|
|
51817
51677
|
}
|
|
51678
|
+
var CREDENTIAL_TYPE_ALIASES = {
|
|
51679
|
+
// googleCalendarOAuth2Api: removed — real OAuth2 credential now provisioned
|
|
51680
|
+
googleSheetsOAuth2Api: ["googleApi"],
|
|
51681
|
+
googleDriveOAuth2Api: ["googleApi"],
|
|
51682
|
+
googleDocsOAuth2Api: ["googleApi"],
|
|
51683
|
+
googleAnalyticsOAuth2Api: ["googleApi"],
|
|
51684
|
+
googleTasksOAuth2Api: ["googleApi"],
|
|
51685
|
+
googleGmailOAuth2Api: ["googleApi"]
|
|
51686
|
+
};
|
|
51818
51687
|
async function buildWirePlan(nodes, credentialCache) {
|
|
51819
51688
|
const bundle = await initPublicCatalogBundle();
|
|
51820
51689
|
const plans = [];
|
|
51821
51690
|
const ambiguous = [];
|
|
51691
|
+
const unwired = [];
|
|
51822
51692
|
for (const node of nodes) {
|
|
51823
51693
|
if (hasCredentials(node))
|
|
51824
51694
|
continue;
|
|
@@ -51831,7 +51701,22 @@ async function buildWirePlan(nodes, credentialCache) {
|
|
|
51831
51701
|
const credType = required[0].credential_type;
|
|
51832
51702
|
const candidates = credentialCache.filter((c) => c.type === credType || c.type.toLowerCase() === credType.toLowerCase());
|
|
51833
51703
|
if (candidates.length === 0) {
|
|
51834
|
-
|
|
51704
|
+
const aliases = CREDENTIAL_TYPE_ALIASES[credType] ?? [];
|
|
51705
|
+
let wiredViaAlias = false;
|
|
51706
|
+
for (const aliasType of aliases) {
|
|
51707
|
+
const aliasCandidates = credentialCache.filter((c) => c.type === aliasType);
|
|
51708
|
+
if (aliasCandidates.length > 0) {
|
|
51709
|
+
const picked2 = pickBestCredential(aliasCandidates, node.type);
|
|
51710
|
+
plans.push({ nodeName: node.name, credentialType: aliasType, credential: picked2 });
|
|
51711
|
+
import_logging66.logger.debug("credential-auto-wire: wired via alias", { node: node.name, credType, aliasType });
|
|
51712
|
+
wiredViaAlias = true;
|
|
51713
|
+
break;
|
|
51714
|
+
}
|
|
51715
|
+
}
|
|
51716
|
+
if (!wiredViaAlias) {
|
|
51717
|
+
import_logging66.logger.debug("credential-auto-wire: no match for type or aliases", { node: node.name, credType });
|
|
51718
|
+
unwired.push({ node: node.name, required_type: credType });
|
|
51719
|
+
}
|
|
51835
51720
|
continue;
|
|
51836
51721
|
}
|
|
51837
51722
|
if (candidates.length === 1) {
|
|
@@ -51851,7 +51736,7 @@ async function buildWirePlan(nodes, credentialCache) {
|
|
|
51851
51736
|
});
|
|
51852
51737
|
}
|
|
51853
51738
|
}
|
|
51854
|
-
return { plans, ambiguous };
|
|
51739
|
+
return { plans, ambiguous, unwired };
|
|
51855
51740
|
}
|
|
51856
51741
|
async function patchWorkflowCredentials(workflowId, config2, plans) {
|
|
51857
51742
|
const workflow = await getWorkflow(config2, workflowId);
|
|
@@ -51875,9 +51760,9 @@ async function autoWireCredentials2(workflowId, deployedNodes, config2) {
|
|
|
51875
51760
|
try {
|
|
51876
51761
|
const credentialCache = await getPreloadedCredentials();
|
|
51877
51762
|
if (credentialCache.length === 0) {
|
|
51878
|
-
return { wired: [], ambiguous: [] };
|
|
51763
|
+
return { wired: [], ambiguous: [], unwired: [] };
|
|
51879
51764
|
}
|
|
51880
|
-
const { plans, ambiguous } = await buildWirePlan(deployedNodes, credentialCache);
|
|
51765
|
+
const { plans, ambiguous, unwired } = await buildWirePlan(deployedNodes, credentialCache);
|
|
51881
51766
|
if (plans.length > 0) {
|
|
51882
51767
|
await patchWorkflowCredentials(workflowId, config2, plans);
|
|
51883
51768
|
import_logging66.logger.info("credential-auto-wire: wired credentials", {
|
|
@@ -51891,13 +51776,13 @@ async function autoWireCredentials2(workflowId, deployedNodes, config2) {
|
|
|
51891
51776
|
credential: p.credential.name,
|
|
51892
51777
|
type: p.credentialType
|
|
51893
51778
|
}));
|
|
51894
|
-
return { wired, ambiguous };
|
|
51779
|
+
return { wired, ambiguous, unwired };
|
|
51895
51780
|
} catch (err) {
|
|
51896
51781
|
import_logging66.logger.warn("credential-auto-wire: failed (non-fatal), continuing without wiring", {
|
|
51897
51782
|
workflowId,
|
|
51898
51783
|
error: err instanceof Error ? err.message : String(err)
|
|
51899
51784
|
});
|
|
51900
|
-
return { wired: [], ambiguous: [] };
|
|
51785
|
+
return { wired: [], ambiguous: [], unwired: [] };
|
|
51901
51786
|
}
|
|
51902
51787
|
}
|
|
51903
51788
|
|
|
@@ -51931,13 +51816,28 @@ function buildUnfixableIssues(validation) {
|
|
|
51931
51816
|
}
|
|
51932
51817
|
async function executeWebhookTest(path3, payload, timeoutMs, start) {
|
|
51933
51818
|
try {
|
|
51934
|
-
const out = await withTimeout(
|
|
51819
|
+
const out = await withTimeout(postWebhookDetailed(buildN8nHostUrl(), path3, payload ?? {}, timeoutMs), timeoutMs, "build_workflow:test");
|
|
51935
51820
|
return { success: true, output: out, duration_ms: Math.round(performance.now() - start), method: "webhook_inline" };
|
|
51936
51821
|
} catch (err) {
|
|
51937
51822
|
return { success: false, error: err instanceof Error ? err.message : String(err), duration_ms: Math.round(performance.now() - start), method: "webhook_inline" };
|
|
51938
51823
|
}
|
|
51939
51824
|
}
|
|
51940
|
-
|
|
51825
|
+
function buildCloneLifecycleSummary(startMs, telemetry) {
|
|
51826
|
+
const summary = {};
|
|
51827
|
+
if (telemetry.clone_created_at)
|
|
51828
|
+
summary.clone_create_ms = telemetry.clone_created_at - startMs;
|
|
51829
|
+
if (telemetry.clone_activated_at)
|
|
51830
|
+
summary.clone_activate_ms = telemetry.clone_activated_at - startMs;
|
|
51831
|
+
if (telemetry.clone_publish_ready_at)
|
|
51832
|
+
summary.clone_publish_ready_ms = telemetry.clone_publish_ready_at - startMs;
|
|
51833
|
+
if (telemetry.webhook_fired_at)
|
|
51834
|
+
summary.webhook_fire_ms = telemetry.webhook_fired_at - startMs;
|
|
51835
|
+
if (telemetry.first_poll_at)
|
|
51836
|
+
summary.first_poll_ms = telemetry.first_poll_at - startMs;
|
|
51837
|
+
if (telemetry.first_execution_seen_at)
|
|
51838
|
+
summary.first_execution_seen_ms = telemetry.first_execution_seen_at - startMs;
|
|
51839
|
+
return summary;
|
|
51840
|
+
}
|
|
51941
51841
|
function buildCloneApiConfig(timeoutMs) {
|
|
51942
51842
|
return {
|
|
51943
51843
|
baseUrl: process.env.N8N_TEST_API_URL ?? process.env.N8N_API_URL ?? process.env.N8N_API_BASE_URL ?? "http://localhost:5678/api/v1",
|
|
@@ -51945,116 +51845,68 @@ function buildCloneApiConfig(timeoutMs) {
|
|
|
51945
51845
|
timeoutMs
|
|
51946
51846
|
};
|
|
51947
51847
|
}
|
|
51948
|
-
function
|
|
51949
|
-
const triggerNode = nodes.find((n) => {
|
|
51950
|
-
const type = String(n?.type ?? "").toLowerCase();
|
|
51951
|
-
return BUILD_CLONE_TRIGGER_KEYWORDS.some((kw) => type.includes(kw));
|
|
51952
|
-
});
|
|
51953
|
-
const triggerName = triggerNode?.name ?? "Manual Trigger";
|
|
51954
|
-
const webhookPath = `build-test-${(0, import_node_crypto3.randomUUID)().slice(0, 8)}`;
|
|
51955
|
-
const cloneNodes = nodes.map((node) => node.name === triggerName ? { parameters: { path: webhookPath, httpMethod: "POST", options: {} }, id: node.id ?? "webhook-clone", name: "Webhook", type: "n8n-nodes-base.webhook", typeVersion: 2, position: node.position ?? [250, 300], webhookId: (0, import_node_crypto3.randomUUID)() } : node);
|
|
51956
|
-
const cloneConnections = { ...connections };
|
|
51957
|
-
if (triggerName !== "Webhook" && cloneConnections[triggerName]) {
|
|
51958
|
-
cloneConnections["Webhook"] = cloneConnections[triggerName];
|
|
51959
|
-
delete cloneConnections[triggerName];
|
|
51960
|
-
}
|
|
51961
|
-
return { cloneNodes, cloneConnections, webhookPath };
|
|
51962
|
-
}
|
|
51963
|
-
async function postCloneWebhook(webhookPath, payload) {
|
|
51964
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
51965
|
-
try {
|
|
51966
|
-
if (attempt > 0)
|
|
51967
|
-
await new Promise((r) => setTimeout(r, 1e3 * attempt));
|
|
51968
|
-
await postWebhook(buildN8nHostUrl(), webhookPath, payload, 15e3);
|
|
51969
|
-
return;
|
|
51970
|
-
} catch (err) {
|
|
51971
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
51972
|
-
if (msg.includes("timeout"))
|
|
51973
|
-
return;
|
|
51974
|
-
if (!msg.includes("404") || attempt === 2)
|
|
51975
|
-
throw err;
|
|
51976
|
-
}
|
|
51977
|
-
}
|
|
51978
|
-
}
|
|
51979
|
-
async function pollCloneExecution(config2, cloneId, budgetMs) {
|
|
51848
|
+
async function pollDirectExecution(config2, executionId, budgetMs, telemetry, start) {
|
|
51980
51849
|
const deadline = Date.now() + budgetMs;
|
|
51981
51850
|
while (Date.now() < deadline) {
|
|
51982
|
-
|
|
51983
|
-
|
|
51984
|
-
|
|
51985
|
-
|
|
51986
|
-
|
|
51987
|
-
|
|
51988
|
-
|
|
51989
|
-
|
|
51851
|
+
if (!telemetry.first_poll_at)
|
|
51852
|
+
telemetry.first_poll_at = Date.now();
|
|
51853
|
+
const full = await getExecution(config2, executionId, true);
|
|
51854
|
+
if (!telemetry.first_execution_seen_at) {
|
|
51855
|
+
telemetry.first_execution_seen_at = Date.now();
|
|
51856
|
+
import_logging67.logger.info("build_workflow: direct execution became visible", {
|
|
51857
|
+
executionId,
|
|
51858
|
+
...buildCloneLifecycleSummary(start, telemetry)
|
|
51859
|
+
});
|
|
51860
|
+
}
|
|
51861
|
+
if (full.finished || full.status === "error" || full.status === "crashed" || full.status === "success") {
|
|
51862
|
+
const hasError = full.status === "error" || full.status === "crashed" || full.error != null;
|
|
51863
|
+
return { execId: executionId, execStatus: hasError ? "error" : "success", nodeDebug: extractNodeDebugData(full) };
|
|
51990
51864
|
}
|
|
51991
51865
|
await new Promise((r) => setTimeout(r, 500));
|
|
51992
51866
|
}
|
|
51993
51867
|
return { execId: null, execStatus: "timeout" };
|
|
51994
51868
|
}
|
|
51995
|
-
async function
|
|
51996
|
-
const
|
|
51997
|
-
|
|
51998
|
-
|
|
51869
|
+
async function runDirectExecution(config2, workflowId, persistedWorkflow, effectiveTimeout, start) {
|
|
51870
|
+
const telemetry = { clone_created_at: Date.now() };
|
|
51871
|
+
const runResult = await executeWorkflowViaRest(config2, workflowId, persistedWorkflow);
|
|
51872
|
+
telemetry.webhook_fired_at = Date.now();
|
|
51873
|
+
const executionId = runResult.executionId ?? runResult.id ?? null;
|
|
51874
|
+
if (!executionId) {
|
|
51875
|
+
return {
|
|
51876
|
+
success: false,
|
|
51877
|
+
error: "Direct execution did not return an execution ID via REST run",
|
|
51878
|
+
duration_ms: Math.round(performance.now() - start),
|
|
51879
|
+
method: "direct_execute"
|
|
51880
|
+
};
|
|
51999
51881
|
}
|
|
52000
|
-
await new Promise((r) => setTimeout(r, 3e3));
|
|
52001
|
-
const payload = testPayload ?? { test: true };
|
|
52002
|
-
await postCloneWebhook(webhookPath, payload);
|
|
52003
|
-
await new Promise((r) => setTimeout(r, 1500));
|
|
52004
51882
|
const remaining = Math.max(effectiveTimeout - (performance.now() - start), 500);
|
|
52005
|
-
const { execId, execStatus, nodeDebug } = await
|
|
51883
|
+
const { execId, execStatus, nodeDebug } = await pollDirectExecution(config2, executionId, remaining, telemetry, start);
|
|
52006
51884
|
return {
|
|
52007
51885
|
success: execStatus === "success",
|
|
52008
|
-
output: execId ? `
|
|
51886
|
+
output: execId ? `Direct test ${execStatus}` : "Direct test timed out",
|
|
51887
|
+
error: execStatus === "timeout" ? "Direct execution timed out before reaching a terminal state" : void 0,
|
|
52009
51888
|
duration_ms: Math.round(performance.now() - start),
|
|
52010
51889
|
execution_id: execId ?? void 0,
|
|
52011
51890
|
node_debug: nodeDebug,
|
|
52012
|
-
method: "
|
|
51891
|
+
method: "direct_execute"
|
|
52013
51892
|
};
|
|
52014
51893
|
}
|
|
52015
|
-
async function
|
|
51894
|
+
async function executeDirectTriggerTest(workflowId, timeoutMs) {
|
|
52016
51895
|
const start = performance.now();
|
|
52017
51896
|
const config2 = buildCloneApiConfig(timeoutMs);
|
|
52018
51897
|
const workflow = await getWorkflow(config2, workflowId);
|
|
52019
|
-
const nodes = workflow.nodes ?? [];
|
|
52020
|
-
const hasR2W = await hasRespondToWebhookNode(workflow);
|
|
52021
|
-
if (hasR2W) {
|
|
52022
|
-
return {
|
|
52023
|
-
success: false,
|
|
52024
|
-
error: "Cannot test via clone: workflow contains a Respond to Webhook node. Use direct webhook POST (test_workflow) instead.",
|
|
52025
|
-
duration_ms: Math.round(performance.now() - start),
|
|
52026
|
-
method: "clone_and_execute"
|
|
52027
|
-
};
|
|
52028
|
-
}
|
|
52029
|
-
const connections = workflow.connections ?? {};
|
|
52030
|
-
const { cloneNodes, cloneConnections, webhookPath } = buildCloneSpec(nodes, connections, workflow.settings);
|
|
52031
51898
|
const { calculateAdaptiveTimeout: calculateAdaptiveTimeout2 } = await Promise.resolve().then(() => (init_timeout(), timeout_exports));
|
|
52032
51899
|
const adaptiveTimeout = calculateAdaptiveTimeout2(workflow);
|
|
52033
51900
|
const effectiveTimeout = Math.max(timeoutMs, adaptiveTimeout);
|
|
52034
|
-
let cloneId = null;
|
|
52035
51901
|
try {
|
|
52036
|
-
|
|
52037
|
-
nodes: cloneNodes,
|
|
52038
|
-
connections: cloneConnections,
|
|
52039
|
-
settings: workflow.settings ?? { executionOrder: "v1" }
|
|
52040
|
-
});
|
|
52041
|
-
cloneId = created.id;
|
|
52042
|
-
return await runCloneExecution(config2, cloneId, webhookPath, testPayload, effectiveTimeout, start);
|
|
51902
|
+
return await runDirectExecution(config2, workflowId, workflow, effectiveTimeout, start);
|
|
52043
51903
|
} catch (err) {
|
|
52044
|
-
return {
|
|
52045
|
-
|
|
52046
|
-
|
|
52047
|
-
|
|
52048
|
-
|
|
52049
|
-
|
|
52050
|
-
} catch {
|
|
52051
|
-
}
|
|
52052
|
-
try {
|
|
52053
|
-
await deleteWorkflow(config2, cloneId).catch(() => {
|
|
52054
|
-
});
|
|
52055
|
-
} catch {
|
|
52056
|
-
}
|
|
52057
|
-
}
|
|
51904
|
+
return {
|
|
51905
|
+
success: false,
|
|
51906
|
+
error: `Direct test failed: ${err.message}`,
|
|
51907
|
+
duration_ms: Math.round(performance.now() - start),
|
|
51908
|
+
method: "direct_execute"
|
|
51909
|
+
};
|
|
52058
51910
|
}
|
|
52059
51911
|
}
|
|
52060
51912
|
async function retryWebhookTest(path3, testPayload, timeoutMs, start) {
|
|
@@ -52093,12 +51945,12 @@ async function runTestStep(workflow, workflowId, testPayload, timeoutMs) {
|
|
|
52093
51945
|
import_logging67.logger.info("build_workflow: Respond to Webhook detected, retrying direct POST (no clone)", { workflowId });
|
|
52094
51946
|
return retryWebhookTest(path3, testPayload, timeoutMs, start);
|
|
52095
51947
|
}
|
|
52096
|
-
import_logging67.logger.info("build_workflow: webhook 404,
|
|
52097
|
-
return
|
|
51948
|
+
import_logging67.logger.info("build_workflow: webhook 404, retrying direct POST with backoff", { workflowId });
|
|
51949
|
+
return retryWebhookTest(path3, testPayload, timeoutMs, start);
|
|
52098
51950
|
}
|
|
52099
51951
|
return testResult;
|
|
52100
51952
|
}
|
|
52101
|
-
return
|
|
51953
|
+
return executeDirectTriggerTest(workflowId, timeoutMs);
|
|
52102
51954
|
}
|
|
52103
51955
|
async function deployWorkflow2(workflow, activate, timeoutMs) {
|
|
52104
51956
|
const config2 = buildApiConfig10(timeoutMs);
|
|
@@ -52563,13 +52415,10 @@ function buildRichResponseFields(result, cleanedArgs, exec) {
|
|
|
52563
52415
|
};
|
|
52564
52416
|
} else if (exec?.tested && !exec.success) {
|
|
52565
52417
|
parts.push(`Test: \u274C failed \u2014 ${exec.error ?? "unknown error"}`);
|
|
52566
|
-
const taxonomy = classifyFailure(exec.error ?? "", {});
|
|
52567
52418
|
result.test_result = {
|
|
52568
52419
|
status: "error",
|
|
52569
52420
|
execution_id: exec.execution_id,
|
|
52570
|
-
duration_ms: exec.duration_ms
|
|
52571
|
-
failure_type: taxonomy.failure_type,
|
|
52572
|
-
recovery_hint: taxonomy.recovery_hint
|
|
52421
|
+
duration_ms: exec.duration_ms
|
|
52573
52422
|
};
|
|
52574
52423
|
} else {
|
|
52575
52424
|
result.test_result = { status: "skipped" };
|
|
@@ -52900,11 +52749,9 @@ async function handleBuildWorkflowStandard(args, correlationId, startTime) {
|
|
|
52900
52749
|
result.next_step = `Workflow deployed and tested successfully (${exec.method}). Ready for production use.`;
|
|
52901
52750
|
} else if (exec?.tested && !exec.success) {
|
|
52902
52751
|
const taxonomy = classifyFailure(exec.error ?? "", {});
|
|
52903
|
-
const retryHint = hasWebhookTrigger ? `Call test_workflow with workflow_id "${wfId}" and test_payload: {\u2026} to retry with a custom webhook payload.` : `Call execute_workflow with workflow_id "${wfId}" to retry after fixing.`;
|
|
52752
|
+
const retryHint = exec.error?.includes("no execution became visible") || exec.error?.includes("did not return an execution ID") ? `Call test_workflow with workflow_id "${wfId}" to retry once the n8n instance is responsive.` : hasWebhookTrigger ? `Call test_workflow with workflow_id "${wfId}" and test_payload: {\u2026} to retry with a custom webhook payload.` : `Call execute_workflow with workflow_id "${wfId}" to retry after fixing.`;
|
|
52904
52753
|
const failureCtx = taxonomy.failure_type !== "config_error" ? `${taxonomy.recovery_hint} ` : "";
|
|
52905
52754
|
result.next_step = `Workflow deployed but test FAILED [${taxonomy.failure_type}]: ${exec.error ?? "unknown error"}. ${failureCtx}${retryHint}`;
|
|
52906
|
-
result.failure_type = taxonomy.failure_type;
|
|
52907
|
-
result.recovery_hint = taxonomy.recovery_hint;
|
|
52908
52755
|
} else if (hasWebhookTrigger) {
|
|
52909
52756
|
result.next_step = `If you have not yet tested this workflow, call test_workflow with workflow_id "${wfId}" and test_payload: { /* your webhook JSON */ }. If test_workflow already returned test_passed=true and you have NOT called partial_update_workflow since, the workflow is complete \u2014 call activate_workflow. Do NOT use Bash/curl \u2014 test_workflow handles activation, URL resolution, and response capture automatically.`;
|
|
52910
52757
|
} else {
|
|
@@ -52946,11 +52793,6 @@ CRITICAL RULES:
|
|
|
52946
52793
|
Pipeline: assemble \u2192 validate (L1-L6 + 33 fixers) \u2192 deploy to n8n.
|
|
52947
52794
|
Returns: workflow_id, workflow_url, validation changelog, optional test result.
|
|
52948
52795
|
|
|
52949
|
-
When test_result.status is "error", the response also includes:
|
|
52950
|
-
- failure_type: one of "config_error", "credential_error", "rate_limited", "execution_timeout", "partial_fix_possible", "rebuild_required"
|
|
52951
|
-
- recovery_hint: specific guidance on what to do next
|
|
52952
|
-
Follow the recovery_hint instead of using Bash commands.
|
|
52953
|
-
|
|
52954
52796
|
Examples:
|
|
52955
52797
|
- Fused: { description: "Send a Telegram message when a new Gmail arrives" }
|
|
52956
52798
|
- Schedule: { name: "Daily Slack", nodes: [{name: "Schedule", type: "n8n-nodes-base.scheduleTrigger", parameters: {rule: {interval: [{field: "cronExpression", expression: "0 8 * * *"}]}}}, {name: "Slack", type: "n8n-nodes-base.slack", operation: "postMessage", credential_id: "abc"}], connections: [{from: "Schedule", to: "Slack"}] }
|
|
@@ -54324,7 +54166,7 @@ connection problems.`,
|
|
|
54324
54166
|
var import_node_fs2 = require("node:fs");
|
|
54325
54167
|
var import_node_os2 = require("node:os");
|
|
54326
54168
|
var import_node_path2 = require("node:path");
|
|
54327
|
-
var
|
|
54169
|
+
var import_node_crypto2 = require("node:crypto");
|
|
54328
54170
|
var import_logging73 = __toESM(require_logging(), 1);
|
|
54329
54171
|
init_p58_config();
|
|
54330
54172
|
var API_BASE_URL2 = "https://p58-n8n.path58.com";
|
|
@@ -54352,7 +54194,7 @@ async function callValidateEndpoint(token) {
|
|
|
54352
54194
|
function writeConfigAtomic(cfg) {
|
|
54353
54195
|
const configDir = (0, import_node_path2.dirname)(ACTIVATE_CONFIG_FILE);
|
|
54354
54196
|
(0, import_node_fs2.mkdirSync)(configDir, { recursive: true });
|
|
54355
|
-
const tmpFile = (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), `p58-config-${(0,
|
|
54197
|
+
const tmpFile = (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), `p58-config-${(0, import_node_crypto2.randomUUID)()}.json`);
|
|
54356
54198
|
(0, import_node_fs2.writeFileSync)(tmpFile, JSON.stringify(cfg, null, 2), { mode: 384 });
|
|
54357
54199
|
(0, import_node_fs2.renameSync)(tmpFile, ACTIVATE_CONFIG_FILE);
|
|
54358
54200
|
}
|