Setting the file. One moment.
Run Wire · N8n:testmax N8n AI · n8n-io/n8n · Skills Docs
ContentsBack to the top of the page export function runsInProcess
— line 203
This file
Number 40.19
Position 19 of 24
Type JavaScript
Size 16 KB
Lines 313 scripts/ run_wire.mjs
JavaScript · 313 lines · 16 KB
14 import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs' ;
15 import { join, resolve } from 'node:path' ;
16
17 import { LOGIC_TYPES, TRIGGER_KITS, graph, isAiNode, isTrigger, pinPlan, typeMatches } from './sdk.mjs' ;
18 import { MODEL_CREDENTIALS, fillSetup, isWrite, loadEngine, modelKey, nodeCredentialTypes, runWorkflow, startNode } from './wire.mjs' ;
19
20 const ITEMS_PER_NODE = 3 ;
21 const BODY_CHARS = 1500 ;
22 // This process holds the settings and has no sandbox. The eval hook answers only the requests that
23 // go through the n8n request helpers. Database, SSH, FTP and SMTP clients, files and a shell bypass
24 // it, so only the node types below run here.
25 // These nodes run code from the builder. The pinned-data runs run them in the n8n task runner.
26 const CODE_NODE_TYPES = new Set ([
27 'n8n-nodes-base.code' ,
28 'n8n-nodes-base.function' ,
29 'n8n-nodes-base.functionItem' ,
30 'n8n-nodes-base.aiTransform' ,
31 '@n8n/n8n-nodes-langchain.code' ,
32 '@n8n/n8n-nodes-langchain.toolCode' ,
33 ]);
34 // Telegram is here because its credential has no authenticate block.
35 const IN_PROCESS_TYPES = new Set ([
36 ... [ ... LOGIC_TYPES ]. filter (( type ) => ! CODE_NODE_TYPES . has (type)),
37 'n8n-nodes-base.httpRequest' ,
38 'n8n-nodes-base.wait' ,
39 'n8n-nodes-base.telegram' ,
40 ]);
41 // Each model node has a key in MODEL_CREDENTIALS. The other nodes call only the model or the helpers.
42 const IN_PROCESS_AI_TYPES = new Set (
43 [
44 'agent' , 'chainLlm' , 'chainSummarization' , 'informationExtractor' , 'textClassifier' , 'sentimentAnalysis' ,
45 'openAi' , 'anthropic' , 'googleGemini' ,
46 'lmChatOpenAi' , 'lmChatAnthropic' , 'lmChatGoogleGemini' , 'lmChatMistralCloud' , 'lmChatGroq' ,
47 'outputParserStructured' , 'outputParserAutofixing' , 'outputParserItemList' ,
48 'memoryBufferWindow' , 'toolCalculator' , 'toolThink' , 'toolHttpRequest' ,
49 ]. map (( type ) => `@n8n/n8n-nodes-langchain.${ type }` ),
50 );
51 const readJson = ( file ) => ( existsSync (file) ? JSON . parse ( readFileSync (file, 'utf8' )) : null );
52
53 /** Read a path such as `messages[0].from` from a value. */
54 export function readPath ( value , path ) {
55 const keys = Array. isArray (path) ? path : String (path). match ( / [ ^ .[ \] ] + / g ) ?? [];
56 return keys. reduce (( v , k ) => (v == null ? undefined : v[ / ^ \d +$ / . test (k) ? Number (k) : k]), value);
57 }
58
59 const requestText = ( r ) => JSON . stringify ({ url: r.url, qs: r.qs ?? undefined , body: r.body ?? undefined });
60
61 function nodeSummary ( runData , types ) {
62 const nodes = {};
63 for ( const [ name , runs ] of Object. entries (runData ?? {})) {
64 const run = runs. at ( - 1 ) ?? {};
65 const outputs = (run.data?.main ?? []). map (( o ) => o ?? []);
66 const error = run.error;
67 nodes[name] = {
68 type: types[name],
69 status: run.executionStatus,
70 error: error ? error.message ?? error.description ?? String (error) : null ,
71 runs: runs. length ,
72 outputCounts: outputs. map (( o ) => o. length ),
73 items: outputs. flat (). slice ( 0 , ITEMS_PER_NODE ). map (( i ) => i.json),
74 };
75 }
76 return nodes;
77 }
78
79 const trim = ( value ) => {
80 const text = value === undefined ? undefined : JSON . stringify (value);
81 return text && text. length > BODY_CHARS ? `${ text . slice ( 0 , BODY_CHARS ) }…` : value;
82 };
83 const trimRequest = ( r ) => ({ ... r, body: trim (r.body), response: trim (r.response) });
84
85 /**
86 * Resolve a marker of an expectation:
87 * @trigger.<path> a value of the first trigger item, such as @trigger.messages[0].from
88 * @response.<type>.<path> a value of the response to the last write request of a node type,
89 * such as @response.notion.id (the id of the page that Notion created)
90 * Other strings stay as they are.
91 */
92 function resolveMarker ( value , triggerItem , requests ) {
93 if ( typeof value !== 'string' ) return value;
94 if (value. startsWith ( '@trigger.' )) return readPath (triggerItem, value. slice ( '@trigger.' . length ));
95 const response = value. match ( / ^ @response \. ( [ ^ .] + ) \. ( . + ) $ / );
96 if (response) {
97 const last = requests. filter (( r ) => isWrite (r) && typeMatches (r.nodeType, response[ 1 ])). at ( - 1 );
98 return last ? readPath (last.response, response[ 2 ]) : undefined ;
99 }
100 return value;
101 }
102
103 /** Check the expectations of a scenario against the nodes that ran and the requests that they sent. */
104 export function evaluate ( scenario , nodes , requests , trigger , kit ) {
105 const checks = [];
106 const expect = scenario.expect ?? {};
107 const ran = Object. values (nodes). filter (( n ) => n.status === 'success' && ! n.error);
108 for ( const pattern of expect.reach ?? []) {
109 checks. push ({ kind: 'reach' , type: pattern, ok: ran. some (( n ) => typeMatches (n.type ?? '' , pattern)) });
110 }
111 for ( const pattern of expect.skip ?? []) {
112 checks. push ({ kind: 'skip' , type: pattern, ok: ! Object. values (nodes). some (( n ) => typeMatches (n.type ?? '' , pattern)) });
113 }
114 for ( const want of expect.requests ?? []) {
115 // An array in contains means any one of its markers.
116 const contains = (want.contains ?? []). map (( c ) => (Array. isArray (c) ? c : [c]). map (( m ) => ({ marker: m, value: resolveMarker (m, trigger[ 0 ], requests) })));
117 const sent = requests. filter (( r ) => typeMatches (r.nodeType, want.type) && ( ! want.method || r.method === want.method. toUpperCase ()));
118 const has = ( r , any ) => any. some (( c ) => c.value !== undefined && c.value !== null && requestText (r). includes ( String (c.value)));
119 const matching = sent. filter (( r ) => contains. every (( any ) => has (r, any)));
120 const ok = want.count === undefined ? matching. length > 0 : matching. length === want.count;
121 const show = ( c ) => (c.marker === c.value ? `"${ c . value }"` : `${ c . marker } ("${ c . value ?? 'not found'}")` );
122 const details = sent. length
123 ? sent. map (( r ) => {
124 const missing = contains. filter (( any ) => ! has (r, any)). map (( any ) => any. map (show). join ( ' or ' ));
125 if (missing. length ) return `${ r . node }: ${ r . method } ${ r . url } does not carry ${ missing . join ( ', ' ) }` ;
126 const body = r.body === undefined ? '' : JSON . stringify (r.body);
127 return `${ r . node }: ${ r . method } ${ r . url }${ body ? ` with \` ${ body . length > 300 ? `${ body . slice ( 0 , 300 ) }…` : body } \` ` : ''}` ;
128 })
129 : [ `No ${ want . method ?? ''} request from a ${ want . type } node.` . replace ( ' ' , ' ' )];
130 checks. push ({ kind: 'request' , type: want.type, contains: contains. map (( any ) => any. map (( c ) => c.marker). join ( ' or ' )), count: want.count, ok, details });
131 }
132 if (expect.noWrites) {
133 const writes = requests. filter (isWrite);
134 checks. push ({ kind: 'noWrites' , ok: writes. length === 0 , details: writes. map (( r ) => `${ r . node }: ${ r . method } ${ r . url }` ) });
135 }
136 if (scenario.derive === 'each-item' && kit) {
137 const ids = trigger. map (( item ) => String ( readPath (item, kit.identity)));
138 const carriers = requests. filter (( r ) => isWrite (r) && ids. some (( id ) => requestText (r). includes (id)));
139 const byNode = new Map ();
140 for ( const r of carriers) byNode. set (r.node, [ ... (byNode. get (r.node) ?? []), r]);
141 const details = [];
142 let ok = byNode.size > 0 ;
143 for ( const [ node , list ] of byNode) {
144 const reached = ids. filter (( id ) => list. some (( r ) => requestText (r). includes (id)));
145 const missed = ids. filter (( id ) => ! reached. includes (id));
146 if (missed. length ) ok = false ;
147 details. push ( `${ node }: ${ list . length } request${ list . length === 1 ? '' : 's'}, for ${ reached . join ( ', ' ) || 'nobody'}${ missed . length ? `, nothing for ${ missed . join ( ', ' ) }` : ''}` );
148 }
149 if ( ! byNode.size) details. push ( `No outgoing request names a sender (${ ids . join ( ', ' ) }).` );
150 checks. push ({ kind: 'eachItem' , ids, ok, details });
151 }
152 return checks;
153 }
154
155 /** The scenarios of one workflow: contract, edge and AI, with their pin data and case rows. */
156 function buildScenarios ( workflow , cfg ) {
157 const g = graph (workflow);
158 const triggerNode = g.triggers. find (( t ) => isTrigger (t.type));
159 const kit = triggerNode ? TRIGGER_KITS [triggerNode.type] : undefined ;
160 const contract = cfg.scenarios?. length ? cfg.scenarios : null ;
161 const edges = [ ... (kit?.edges ?? []), ... (cfg.edge ?? [])];
162 // The triggerNode of pinPlan stays: a name from scenarios.json can be missing in this workflow.
163 const merge = ( s , entry , kind ) => ({ ... s, ... entry, triggerNode: s.triggerNode, kind });
164 const list = [
165 ... (contract ? pinPlan (workflow, cfg, contract).scenarios. map (( s , i ) => merge (s, contract[i], 'contract' )) : pinPlan (workflow, cfg).scenarios. map (( s ) => merge (s, {}, 'contract' ))),
166 ... pinPlan (workflow, cfg, edges).scenarios. map (( s , i ) => merge (s, edges[i], 'edge' )),
167 ... pinPlan (workflow, cfg, cfg.questions ?? []).scenarios. map (( s , i ) => merge (s, cfg.questions[i], 'ai' )),
168 ];
169 const keep = ( name ) => {
170 const node = g.byName. get (name);
171 return node && ( isTrigger (node.type) || isAiNode (g, node));
172 };
173 return {
174 g,
175 scenarios: list. map (( s ) => {
176 const pinData = Object. fromEntries (
177 Object. entries (s.pinData). filter (([ name ]) => keep (name) && ! (s.kind === 'ai' && isAiNode (g, g.byName. get (name)))),
178 );
179 return { ... s, pinData };
180 }),
181 };
182 }
183
184 /** Model sub-nodes and vendor model nodes, and whether a key exists for each, for the real-AI scenarios. */
185 function modelReadiness ( g ) {
186 const subnodes = g.edges. filter (( e ) => e.type === 'ai_languageModel' ). map (( e ) => g.byName. get (e.from));
187 const vendors = g.nodes. filter (( n ) => isAiNode (g, n) && ! g. aiInputs (n.name, 'ai_languageModel' ). length );
188 return [ ...new Set ([ ... subnodes, ... vendors])]. map (( node ) => {
189 const types = nodeCredentialTypes (node). filter (( t ) => MODEL_CREDENTIALS [t]);
190 const type = types. find (( t ) => modelKey (t)) ?? types[ 0 ];
191 const p = node.parameters ?? {};
192 return { node: node.name, credentialType: type ?? null , model: p.modelName ?? p.model?.value ?? p.model ?? p.modelId?.value ?? null , ready: Boolean (type && modelKey (type)) };
193 });
194 }
195
196 const isHttpCredential = ( name , { credentialType , parentTypes }) =>
197 credentialType (name).authenticate !== undefined || [name, ... parentTypes (name)]. some (( p ) => p === 'oAuth1Api' || p === 'oAuth2Api' );
198
199 /**
200 * True for a node on the allow-list: the types above, declarative nodes, and nodes whose credentials
201 * all authenticate HTTP requests. A tool variant (slackTool) counts as its base node.
202 */
203 export function runsInProcess ( node ) {
204 const base = node.type. startsWith ( 'n8n-nodes-base.' ) && node.type. endsWith ( 'Tool' ) ? node.type. slice ( 0 , - 'Tool' . length ) : node.type;
205 if ( IN_PROCESS_TYPES . has (base) || IN_PROCESS_AI_TYPES . has (base)) return true ;
206 if ( ! base. startsWith ( 'n8n-nodes-base.' )) return false ;
207 const engine = loadEngine ();
208 try {
209 const { description } = engine.nodeTypes. getByNameAndVersion (base, node.typeVersion);
210 if (description.requestDefaults !== undefined ) return true ;
211 const credentials = (description.credentials ?? []). map (( c ) => c.name);
212 return credentials. length > 0 && credentials. every (( name ) => isHttpCredential (name, engine));
213 } catch {
214 return false ;
215 }
216 }
217
218 /**
219 * The names and types of the nodes that run from the start trigger and are not on the allow-list.
220 * Pinned nodes do not run. A disabled node passes its input on, so the walk goes through it.
221 */
222 function outsideAllowList ( workflow , g , start , pinData ) {
223 const seen = new Set ([start]);
224 const running = new Set ();
225 // A Set visits the entries that the loop adds.
226 for ( const name of seen) {
227 for ( const targets of workflow.connections?.[name]?.main ?? []) {
228 for ( const { node } of targets ?? []) {
229 if (seen. has (node)) continue ;
230 seen. add (node);
231 if (g.byName. has (node) && ! Object. hasOwn (pinData, node)) running. add (node);
232 }
233 }
234 }
235 for ( const name of running) {
236 for ( const e of g.edges) if (e.to === name && e.type. startsWith ( 'ai_' )) running. add (e.from);
237 }
238 return [ ... running]. map (( name ) => g.byName. get (name)). filter (( n ) => ! runsInProcess (n)). map (( n ) => `${ n . name } (${ n . type })` );
239 }
240
241 async function runBuilder ( dir , cfg ) {
242 const workflow = readJson ( join (dir, 'workflow.json' ));
243 const { g , scenarios } = buildScenarios (workflow, cfg);
244 const canvas = readJson ( join (dir, 'canvas-issues.json' ));
245 const { workflow : filledWorkflow , filled } = fillSetup (workflow, canvas);
246 const types = Object. fromEntries ((workflow.nodes ?? []). map (( n ) => [n.name, n.type]));
247 const models = modelReadiness (g);
248 const out = { filled, models, scenarios: [] };
249 for ( const scenario of scenarios) {
250 const entry = { name: scenario.name, kind: scenario.kind, why: scenario.why, expect: scenario.expect ?? {}, derive: scenario.derive, pinned: Object. keys (scenario.pinData) };
251 if (scenario.kind === 'ai' && ! models. length ) {
252 out.scenarios. push ({ ... entry, status: 'skipped' , error: 'The workflow has no model sub-node.' });
253 continue ;
254 }
255 const start = startNode (workflow, scenario.pinData, scenario.triggerNode);
256 const outside = start ? outsideAllowList (workflow, g, start.name, scenario.pinData) : [];
257 if (outside. length ) {
258 out.scenarios. push ({ ... entry, status: 'skipped' , error: `Nodes outside the in-process allow-list: ${ outside . join ( ', ' ) }.` });
259 continue ;
260 }
261 if (scenario.kind === 'ai' && models. some (( m ) => ! m.ready)) {
262 const missing = models. filter (( m ) => ! m.ready). map (( m ) => `${ m . node } (${ m . credentialType ?? 'unknown credential'}: set ${ MODEL_CREDENTIALS [ m . credentialType ]?. keys [ 0 ] ?? 'a supported model'})` );
263 out.scenarios. push ({ ... entry, status: 'skipped' , error: `No model key for ${ missing . join ( ', ' ) }.` });
264 continue ;
265 }
266 const rows = ( node ) => Object. entries (scenario.byType ?? {}). find (([ pattern ]) => typeMatches (node.type, pattern))?.[ 1 ];
267 const trigger = start ? scenario.pinData[start.name]. map (( i ) => i.json) : [];
268 const kit = start ? TRIGGER_KITS [start.type] : undefined ;
269 const result = await runWorkflow (filledWorkflow, { pinData: scenario.pinData, triggerNode: scenario.triggerNode, rows, realModels: scenario.kind === 'ai' });
270 const nodes = nodeSummary (result.runData, types);
271 const errors = Object. entries (nodes). filter (([, n ]) => n.error). map (([ name , n ]) => `${ name }: ${ n . error }` );
272 Object. assign (entry, {
273 status: result.status,
274 error: result.error,
275 ms: result.ms,
276 nodes,
277 requests: result.requests. map (trimRequest),
278 blocked: result.blocked,
279 checks: evaluate (scenario, nodes, result.requests, trigger, kit),
280 });
281 if (scenario.kind === 'ai' ) {
282 entry.aiOutputs = Object. fromEntries (Object. entries (nodes). filter (([ name ]) => isAiNode (g, g.byName. get (name))). map (([ name , n ]) => [name, n.items]));
283 }
284 console. log ( `[wire] ${ dir . split ( '/' ). pop () }: ${ scenario . kind } "${ scenario . name }" -> ${ result . status }${ errors . length ? ` (${ errors [ 0 ] })` : result . error ? ` (${ result . error })` : ''}` );
285 out.scenarios. push (entry);
286 }
287 return out;
288 }
289
290 if (process.argv[ 1 ] && resolve (process.argv[ 1 ]) === new URL ( import . meta .url).pathname) {
291 const runDir = resolve (process.argv[ 2 ] ?? '.' );
292 const loaded = loadEngine ();
293 const cfg = readJson ( join (runDir, 'scenarios.json' )) ?? {};
294 for ( const entry of readdirSync (runDir, { withFileTypes: true })) {
295 const dir = join (runDir, entry.name);
296 if ( ! entry. isDirectory () || ! existsSync ( join (dir, 'workflow.json' ))) continue ;
297 let out;
298 if (loaded.error) out = { skipped: loaded.error, scenarios: [] };
299 else {
300 try {
301 out = await runBuilder (dir, cfg);
302 } catch (error) {
303 out = { skipped: `${ error ?. name ?? 'Error'}: ${ error ?. message ?? error }` , scenarios: [] };
304 }
305 }
306 // A failed real model request can echo its URL, with the key, in the error message.
307 const keys = Object. keys ( MODEL_CREDENTIALS ). map (modelKey). filter (Boolean);
308 writeFileSync ( join (dir, 'wire.json' ), keys. reduce (( text , key ) => text. replaceAll (key, '[redacted]' ), JSON . stringify (out, null , 1 )));
309 if (out.skipped) console. log ( `[wire] ${ entry . name }: skipped, ${ out . skipped }` );
310 }
311 // Model SDKs can keep sockets open.
312 process. exit ( 0 );
313 }