mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
fix(core): Pass nodeTypesProvider to validate workflows fully at instance AI (#29333)
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import type { Workspace } from '@mastra/core/workspace';
|
||||
import { validateWorkflow } from '@n8n/workflow-sdk';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { INodeTypes } from 'n8n-workflow';
|
||||
|
||||
import type { InstanceAiContext } from '../../../types';
|
||||
import { isTriggerNodeType, type SubmitWorkflowAttempt } from '../submit-workflow.tool';
|
||||
@@ -7,10 +10,21 @@ jest.mock('@mastra/core/tools', () => ({
|
||||
createTool: jest.fn((config: Record<string, unknown>) => config),
|
||||
}));
|
||||
|
||||
jest.mock('@n8n/workflow-sdk', () => ({
|
||||
validateWorkflow: jest.fn(() => ({ errors: [], warnings: [] })),
|
||||
layoutWorkflowJSON: jest.fn((wf: unknown) => wf),
|
||||
}));
|
||||
|
||||
// `require` (rather than `import`) is needed because `submit-workflow.tool`
|
||||
// transitively pulls in @mastra/core (ESM-only); the require call here runs
|
||||
// AFTER the `jest.mock('@mastra/core/tools', …)` above, so the mock is in
|
||||
// place before the module is evaluated.
|
||||
const { createSubmitWorkflowTool } =
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/consistent-type-imports
|
||||
require('../submit-workflow.tool') as typeof import('../submit-workflow.tool');
|
||||
|
||||
const mockedValidateWorkflow = jest.mocked(validateWorkflow);
|
||||
|
||||
type Executable = {
|
||||
execute: (input: Record<string, unknown>) => Promise<{
|
||||
success: boolean;
|
||||
@@ -20,10 +34,12 @@ type Executable = {
|
||||
|
||||
function makeContext(
|
||||
permissions: InstanceAiContext['permissions'] = {} as InstanceAiContext['permissions'],
|
||||
overrides: Partial<InstanceAiContext> = {},
|
||||
): InstanceAiContext {
|
||||
return {
|
||||
permissions,
|
||||
workflowService: {} as InstanceAiContext['workflowService'],
|
||||
...overrides,
|
||||
} as unknown as InstanceAiContext;
|
||||
}
|
||||
|
||||
@@ -46,6 +62,86 @@ function makeWorkspace(): Workspace {
|
||||
} as unknown as Workspace;
|
||||
}
|
||||
|
||||
/** Workspace stub that simulates a successful sandbox build by emitting
|
||||
* parseable build.mjs output for the build command. */
|
||||
function makeBuildSuccessWorkspace(
|
||||
workflowJson: object = {
|
||||
id: 'wf-1',
|
||||
name: 'Test',
|
||||
nodes: [],
|
||||
connections: {},
|
||||
},
|
||||
): Workspace {
|
||||
return {
|
||||
sandbox: {
|
||||
executeCommand: async (command: string) => {
|
||||
await Promise.resolve();
|
||||
if (command === 'echo $HOME') {
|
||||
return { exitCode: 0, stdout: '/home/test\n', stderr: '' };
|
||||
}
|
||||
if (command.startsWith('node --import tsx build.mjs')) {
|
||||
return {
|
||||
exitCode: 0,
|
||||
stdout: JSON.stringify({
|
||||
success: true,
|
||||
workflow: workflowJson,
|
||||
warnings: [],
|
||||
}),
|
||||
stderr: '',
|
||||
};
|
||||
}
|
||||
return { exitCode: 0, stdout: '', stderr: '' };
|
||||
},
|
||||
},
|
||||
} as unknown as Workspace;
|
||||
}
|
||||
|
||||
describe('createSubmitWorkflowTool — schema validation wiring', () => {
|
||||
beforeEach(() => {
|
||||
mockedValidateWorkflow.mockReset();
|
||||
mockedValidateWorkflow.mockReturnValue({
|
||||
// One blocking error so we early-return before workflowService.create/update is called.
|
||||
errors: [{ code: 'INVALID_PARAM', message: 'forced for test', nodeName: 'X' }],
|
||||
warnings: [],
|
||||
} as never);
|
||||
});
|
||||
|
||||
it('forwards context.nodeTypesProvider into validateWorkflow', async () => {
|
||||
const nodeTypesProvider = mock<INodeTypes>();
|
||||
const context = makeContext({} as InstanceAiContext['permissions'], {
|
||||
nodeTypesProvider,
|
||||
});
|
||||
|
||||
const tool = createSubmitWorkflowTool(
|
||||
context,
|
||||
makeBuildSuccessWorkspace(),
|
||||
new Map(),
|
||||
) as unknown as Executable;
|
||||
|
||||
await tool.execute({ filePath: 'src/workflow.ts', name: 'Test' });
|
||||
|
||||
expect(mockedValidateWorkflow).toHaveBeenCalledWith(expect.any(Object), {
|
||||
nodeTypesProvider,
|
||||
strictMode: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('passes undefined nodeTypesProvider when context has none, strictMode still on', async () => {
|
||||
const tool = createSubmitWorkflowTool(
|
||||
makeContext(),
|
||||
makeBuildSuccessWorkspace(),
|
||||
new Map(),
|
||||
) as unknown as Executable;
|
||||
|
||||
await tool.execute({ filePath: 'src/workflow.ts', name: 'Test' });
|
||||
|
||||
expect(mockedValidateWorkflow).toHaveBeenCalledWith(expect.any(Object), {
|
||||
nodeTypesProvider: undefined,
|
||||
strictMode: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isTriggerNodeType', () => {
|
||||
it.each([
|
||||
'n8n-nodes-base.webhook',
|
||||
|
||||
@@ -123,7 +123,9 @@ export function createBuildWorkflowTool(context: InstanceAiContext) {
|
||||
// Parse TypeScript to WorkflowJSON with two-stage validation
|
||||
let result;
|
||||
try {
|
||||
result = parseAndValidate(finalCode);
|
||||
result = parseAndValidate(finalCode, {
|
||||
nodeTypesProvider: context.nodeTypesProvider,
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
|
||||
@@ -308,8 +308,13 @@ export function createSubmitWorkflowTool(
|
||||
nodeName: w.nodeName,
|
||||
}));
|
||||
|
||||
// Server-side schema validation (Zod checks against node type definitions)
|
||||
const schemaValidation = validateWorkflow(buildOutput.workflow);
|
||||
// Server-side schema validation (Zod checks against node type definitions).
|
||||
// strictMode is hardcoded on at AI-builder call sites — we want every
|
||||
// catchable bug surfaced as a blocking error so the agent can self-correct.
|
||||
const schemaValidation = validateWorkflow(buildOutput.workflow, {
|
||||
nodeTypesProvider: context.nodeTypesProvider,
|
||||
strictMode: true,
|
||||
});
|
||||
for (const issue of [...schemaValidation.errors, ...schemaValidation.warnings]) {
|
||||
allWarnings.push({
|
||||
code: issue.code,
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
McpToolCallResult,
|
||||
} from '@n8n/api-types';
|
||||
import type { WorkflowJSON } from '@n8n/workflow-sdk';
|
||||
import type { INodeTypes } from 'n8n-workflow';
|
||||
|
||||
// Service interfaces — dependency inversion so the package stays decoupled from n8n internals.
|
||||
// The backend module provides concrete implementations via InstanceAiAdapterService.
|
||||
@@ -595,6 +596,11 @@ export interface InstanceAiContext {
|
||||
currentUserAttachments?: InstanceAiAttachment[];
|
||||
/** Optional logger for diagnostics from domain tools. */
|
||||
logger?: Logger;
|
||||
/** Synchronous node-types provider used by host-side schema validation
|
||||
* (`validateWorkflow` from `@n8n/workflow-sdk`). Plumbed from the CLI
|
||||
* adapter; absent in pure-package contexts where no NodeTypes instance
|
||||
* is reachable. */
|
||||
nodeTypesProvider?: INodeTypes;
|
||||
}
|
||||
|
||||
// ── Task storage ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -8,6 +8,8 @@ jest.mock('../extract-code', () => ({
|
||||
}));
|
||||
|
||||
import { parseWorkflowCodeToBuilder, validateWorkflow } from '@n8n/workflow-sdk';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { INodeTypes } from 'n8n-workflow';
|
||||
|
||||
import { stripImportStatements } from '../extract-code';
|
||||
import { parseAndValidate, partitionWarnings } from '../parse-validate';
|
||||
@@ -119,6 +121,33 @@ describe('parseAndValidate', () => {
|
||||
'Failed to parse workflow code: Unknown error',
|
||||
);
|
||||
});
|
||||
|
||||
it('forwards nodeTypesProvider to both validators with strictMode on for schema validation', () => {
|
||||
const builder = makeBuilder();
|
||||
mockedParseWorkflowCodeToBuilder.mockReturnValue(builder as never);
|
||||
const nodeTypesProvider = mock<INodeTypes>();
|
||||
|
||||
parseAndValidate('code', { nodeTypesProvider });
|
||||
|
||||
expect(builder.validate).toHaveBeenCalledWith({ nodeTypesProvider });
|
||||
expect(mockedValidateWorkflow).toHaveBeenCalledWith(expect.any(Object), {
|
||||
nodeTypesProvider,
|
||||
strictMode: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('passes undefined provider but keeps strictMode on when no options are supplied', () => {
|
||||
const builder = makeBuilder();
|
||||
mockedParseWorkflowCodeToBuilder.mockReturnValue(builder as never);
|
||||
|
||||
parseAndValidate('code');
|
||||
|
||||
expect(builder.validate).toHaveBeenCalledWith({ nodeTypesProvider: undefined });
|
||||
expect(mockedValidateWorkflow).toHaveBeenCalledWith(expect.any(Object), {
|
||||
nodeTypesProvider: undefined,
|
||||
strictMode: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('partitionWarnings', () => {
|
||||
|
||||
@@ -7,10 +7,19 @@
|
||||
*/
|
||||
|
||||
import { parseWorkflowCodeToBuilder, validateWorkflow } from '@n8n/workflow-sdk';
|
||||
import type { INodeTypes } from 'n8n-workflow';
|
||||
|
||||
import { stripImportStatements } from './extract-code';
|
||||
import type { ParseAndValidateResult, ValidationWarning } from './types';
|
||||
|
||||
export interface ParseAndValidateOptions {
|
||||
/** Synchronous node-types provider used by both graph and schema validators.
|
||||
* Without it, AI-aware checks (`MISSING_REQUIRED_INPUT`,
|
||||
* `UNSUPPORTED_SUBNODE_INPUT`, `SUBNODE_PARAMETER_MISMATCH`,
|
||||
* `INVALID_INPUT_INDEX`) are silently skipped. */
|
||||
nodeTypesProvider?: INodeTypes;
|
||||
}
|
||||
|
||||
/** Validation issue from graph or JSON validation */
|
||||
interface ValidationIssue {
|
||||
code: string;
|
||||
@@ -46,10 +55,15 @@ function collectValidationIssues(
|
||||
* @returns ParseAndValidateResult with workflow JSON and any warnings/errors
|
||||
* @throws Error if parsing fails
|
||||
*/
|
||||
export function parseAndValidate(code: string): ParseAndValidateResult {
|
||||
export function parseAndValidate(
|
||||
code: string,
|
||||
options: ParseAndValidateOptions = {},
|
||||
): ParseAndValidateResult {
|
||||
// Strip import statements before parsing — SDK functions are available as globals
|
||||
const codeToParse = stripImportStatements(code);
|
||||
|
||||
const { nodeTypesProvider } = options;
|
||||
|
||||
try {
|
||||
// Parse the TypeScript code to WorkflowBuilder
|
||||
const builder = parseWorkflowCodeToBuilder(codeToParse);
|
||||
@@ -60,14 +74,16 @@ export function parseAndValidate(code: string): ParseAndValidateResult {
|
||||
const allWarnings: ValidationWarning[] = [];
|
||||
|
||||
// Stage 1: Structural validation via graph validators
|
||||
const graphValidation = builder.validate();
|
||||
const graphValidation = builder.validate({ nodeTypesProvider });
|
||||
collectValidationIssues(graphValidation.errors, allWarnings);
|
||||
collectValidationIssues(graphValidation.warnings, allWarnings);
|
||||
|
||||
const json = builder.toJSON();
|
||||
|
||||
// Stage 2: Schema validation via Zod schemas from schemaBaseDirs
|
||||
const schemaValidation = validateWorkflow(json);
|
||||
// Stage 2: Schema validation via Zod schemas from schemaBaseDirs.
|
||||
// strictMode is hardcoded on at AI-builder call sites — we want every
|
||||
// catchable bug surfaced as a blocking error so the agent can self-correct.
|
||||
const schemaValidation = validateWorkflow(json, { nodeTypesProvider, strictMode: true });
|
||||
collectValidationIssues(schemaValidation.errors, allWarnings);
|
||||
collectValidationIssues(schemaValidation.warnings, allWarnings);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@n8n/workflow-sdk",
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.1",
|
||||
"description": "TypeScript SDK for programmatically creating n8n workflows",
|
||||
"exports": {
|
||||
".": {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import get from 'lodash/get';
|
||||
import type { INodeTypes, IConnections as N8nIConnections, IDisplayOptions } from 'n8n-workflow';
|
||||
import { mapConnectionsByDestination } from 'n8n-workflow';
|
||||
|
||||
@@ -662,6 +663,16 @@ function validateSubnodeParameters(
|
||||
);
|
||||
|
||||
if (!matches) {
|
||||
// `displayOptions` on `builderHint.inputs[type]` can describe
|
||||
// either subnode-relative params (e.g. ai_vectorStore wants
|
||||
// vector-store mode='retrieve-as-tool') or parent-relative
|
||||
// params (e.g. ai_memory wants chatTrigger mode='hostedChat').
|
||||
// If every mismatched param is absent from the subnode, those
|
||||
// params don't belong to the subnode at all — blaming it is a
|
||||
// false-positive misdirect. Defer to validateParentSupportsInputs.
|
||||
const subnodeOwnsAnyParam = mismatches.some((m) => m.actual !== undefined);
|
||||
if (!subnodeOwnsAnyParam) continue;
|
||||
|
||||
const sdkFn = AI_CONNECTION_TO_SDK_FUNCTION[connectionType] || connectionType;
|
||||
|
||||
// Build error message with actual parameter names from displayOptions
|
||||
@@ -699,7 +710,10 @@ function buildConditionSummary(
|
||||
const parts: string[] = [];
|
||||
for (const [paramName, expectedValues] of Object.entries(displayOptions.show)) {
|
||||
if (!expectedValues) continue;
|
||||
const actual = parentParams[paramName];
|
||||
// Use lodash get so nested paths (e.g. 'options.loadPreviousSession')
|
||||
// resolve correctly — direct property access would read the literal
|
||||
// dotted key and report 'undefined' even when the nested value is set.
|
||||
const actual = get(parentParams, paramName);
|
||||
const expectedStr = (expectedValues as unknown[]).map((v) => `'${String(v)}'`).join(' or ');
|
||||
parts.push(`${paramName} should be ${expectedStr} (currently '${String(actual)}')`);
|
||||
}
|
||||
@@ -707,6 +721,28 @@ function buildConditionSummary(
|
||||
return parts.length > 0 ? `Required: ${parts.join(', ')}.` : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a description of which parameters TRIGGERED a requirement.
|
||||
* Used by `MISSING_REQUIRED_INPUT` where the displayOptions conditions are
|
||||
* already satisfied (that's why the requirement applies) — the agent needs
|
||||
* to know which params caused it so it can choose between satisfying the
|
||||
* requirement or backing out by changing those params.
|
||||
*/
|
||||
function buildTriggeringConditionSummary(
|
||||
displayOptions: IDisplayOptions,
|
||||
parentParams: Record<string, unknown>,
|
||||
): string {
|
||||
if (!displayOptions.show) return '';
|
||||
|
||||
const parts: string[] = [];
|
||||
for (const [paramName, _expectedValues] of Object.entries(displayOptions.show)) {
|
||||
const actual = get(parentParams, paramName);
|
||||
parts.push(`${paramName}='${String(actual)}'`);
|
||||
}
|
||||
|
||||
return parts.join(', ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that parent nodes actually support their connected AI input types
|
||||
* based on the parent's own parameters and builderHint.inputs displayOptions.
|
||||
@@ -775,7 +811,7 @@ function validateParentSupportsInputs(
|
||||
warnings.push(
|
||||
new ValidationWarning(
|
||||
'UNSUPPORTED_SUBNODE_INPUT',
|
||||
`'${subnodeName}' is connected to '${parentNode.name}' as ${subnodeField}, but '${parentNode.name}' does not support ${subnodeField} in its current configuration. ${conditionDetails}`,
|
||||
`'${parentNode.name}' has a ${subnodeField} subnode ('${subnodeName}') connected, but its current configuration does not accept one. ${conditionDetails} These parameters must be set on '${parentNode.name}' itself, NOT on the ${subnodeField} subnode. Alternatively, remove the ${subnodeField} connection if this capability isn't needed.`,
|
||||
parentNode.name,
|
||||
undefined,
|
||||
undefined,
|
||||
@@ -844,17 +880,20 @@ function validateRequiredInputsConnected(
|
||||
if (hasConnection) continue;
|
||||
|
||||
const subnodeField = AI_CONNECTION_TO_SUBNODE_FIELD[connectionType] || connectionType;
|
||||
const conditionDetails = inputConfig.displayOptions
|
||||
? ` ${buildConditionSummary(
|
||||
const triggerDetails = inputConfig.displayOptions
|
||||
? ` (triggered by ${buildTriggeringConditionSummary(
|
||||
inputConfig.displayOptions,
|
||||
(parentNode.parameters ?? {}) as Record<string, unknown>,
|
||||
)}`
|
||||
)})`
|
||||
: '';
|
||||
const alternative = inputConfig.displayOptions
|
||||
? ` Either connect a ${subnodeField} subnode, or change those parameters to remove the requirement.`
|
||||
: '';
|
||||
|
||||
errors.push(
|
||||
new ValidationError(
|
||||
'MISSING_REQUIRED_INPUT',
|
||||
`'${parentNode.name}' requires a ${subnodeField} subnode connected to its ${connectionType} input, but none is connected.${conditionDetails}`,
|
||||
`'${parentNode.name}' requires a ${subnodeField} subnode connected to its ${connectionType} input${triggerDetails}, but none is connected.${alternative}`,
|
||||
parentNode.name,
|
||||
undefined,
|
||||
'major',
|
||||
|
||||
@@ -2218,6 +2218,87 @@ describe('Validation', () => {
|
||||
const warnings = result.warnings.filter((w) => w.code === 'UNSUPPORTED_SUBNODE_INPUT');
|
||||
expect(warnings).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not also fire SUBNODE_PARAMETER_MISMATCH for parent-relative displayOptions (INS-136)', () => {
|
||||
// Scenario: chat trigger that has loadPreviousSession: memory but no
|
||||
// `mode` set, so the parent fails its own displayOptions. The memory
|
||||
// subnode is connected - but `mode` and `options.loadPreviousSession`
|
||||
// are parent params, not subnode params. Only UNSUPPORTED_SUBNODE_INPUT
|
||||
// should fire (parent-relative); SUBNODE_PARAMETER_MISMATCH is a false
|
||||
// positive when the subnode doesn't even own those params.
|
||||
const provider = {
|
||||
getByNameAndVersion: (type: string) => {
|
||||
if (type === '@n8n/n8n-nodes-langchain.chatTrigger') {
|
||||
return {
|
||||
description: {
|
||||
inputs: ['main'],
|
||||
builderHint: {
|
||||
inputs: {
|
||||
ai_memory: {
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
mode: ['hostedChat', 'webhook'],
|
||||
'options.loadPreviousSession': ['memory'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return { description: { inputs: ['main'] } };
|
||||
},
|
||||
getByName: (type: string) => provider.getByNameAndVersion(type),
|
||||
getKnownTypes: () => ({}),
|
||||
};
|
||||
|
||||
const workflowJson = {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'ct-1',
|
||||
name: 'Chat Trigger',
|
||||
type: '@n8n/n8n-nodes-langchain.chatTrigger',
|
||||
typeVersion: 1.4,
|
||||
position: [0, 0] as [number, number],
|
||||
// Parent's own `mode` and `options.loadPreviousSession` not set
|
||||
// -> parent fails displayOptions
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'mem-1',
|
||||
name: 'Session Memory',
|
||||
type: '@n8n/n8n-nodes-langchain.memoryBufferWindow',
|
||||
typeVersion: 1,
|
||||
position: [0, 200] as [number, number],
|
||||
parameters: {}, // memory subnode has no `mode` or `options.loadPreviousSession`
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'Session Memory': {
|
||||
ai_memory: [[{ node: 'Chat Trigger', type: 'ai_memory', index: 0 }]],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateWorkflow(workflowJson, {
|
||||
nodeTypesProvider: provider as never,
|
||||
allowDisconnectedNodes: true,
|
||||
});
|
||||
|
||||
const mismatch = result.warnings.filter((w) => w.code === 'SUBNODE_PARAMETER_MISMATCH');
|
||||
const unsupported = result.warnings.filter((w) => w.code === 'UNSUPPORTED_SUBNODE_INPUT');
|
||||
|
||||
expect(mismatch).toHaveLength(0);
|
||||
expect(unsupported).toHaveLength(1);
|
||||
expect(unsupported[0].nodeName).toBe('Chat Trigger');
|
||||
// Message should direct the LLM to set params on the parent
|
||||
expect(unsupported[0].message).toContain("must be set on 'Chat Trigger' itself");
|
||||
expect(unsupported[0].message).toContain('NOT on the memory subnode');
|
||||
});
|
||||
});
|
||||
|
||||
describe('MISSING_REQUIRED_INPUT validation', () => {
|
||||
@@ -2291,7 +2372,12 @@ describe('Validation', () => {
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].nodeName).toBe('Chat Trigger');
|
||||
expect(errors[0].message).toContain('ai_memory');
|
||||
expect(errors[0].message).toContain('loadPreviousSession');
|
||||
// The triggering condition reports the actual nested value via lodash get,
|
||||
// not the literal dotted-key lookup that would resolve to 'undefined'.
|
||||
expect(errors[0].message).toContain("options.loadPreviousSession='memory'");
|
||||
expect(errors[0].message).not.toContain("options.loadPreviousSession='undefined'");
|
||||
// Message offers the alternative path (change the params, don't connect).
|
||||
expect(errors[0].message).toContain('change those parameters to remove the requirement');
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
+3
@@ -32,6 +32,7 @@ import type { CredentialsFinderService } from '@/credentials/credentials-finder.
|
||||
import type { ActiveExecutions } from '@/active-executions';
|
||||
import type { WorkflowRunner } from '@/workflow-runner';
|
||||
import type { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import type { NodeTypes } from '@/node-types';
|
||||
import type { DataTableService } from '@/modules/data-table/data-table.service';
|
||||
import type { DataTableRepository } from '@/modules/data-table/data-table.repository';
|
||||
import type { DynamicNodeParametersService } from '@/services/dynamic-node-parameters.service';
|
||||
@@ -76,6 +77,7 @@ const credentialsFinderService = mock<CredentialsFinderService>();
|
||||
const activeExecutions = mock<ActiveExecutions>();
|
||||
const workflowRunner = mock<WorkflowRunner>();
|
||||
const loadNodesAndCredentials = mock<LoadNodesAndCredentials>();
|
||||
const nodeTypes = mock<NodeTypes>();
|
||||
const dataTableService = mock<DataTableService>();
|
||||
const dataTableRepository = mock<DataTableRepository>();
|
||||
const dynamicNodeParametersService = mock<DynamicNodeParametersService>();
|
||||
@@ -107,6 +109,7 @@ const service = new InstanceAiAdapterService(
|
||||
activeExecutions,
|
||||
workflowRunner,
|
||||
loadNodesAndCredentials,
|
||||
nodeTypes,
|
||||
mock<InstanceSettings>({ staticCacheDir: '/tmp/test-cache' }),
|
||||
dataTableService,
|
||||
dataTableRepository,
|
||||
|
||||
+19
-15
@@ -726,29 +726,30 @@ function createNodeAdapterForTests(nodes: Array<Record<string, unknown>>) {
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[10],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[11],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[12],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[13],
|
||||
{ staticCacheDir: '/tmp' } as unknown as ConstructorParameters<
|
||||
typeof InstanceAiAdapterService
|
||||
>[13],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[14],
|
||||
>[14],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[15],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[16],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[17],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[18],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[19],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[20],
|
||||
{
|
||||
getPreferences: jest.fn().mockReturnValue({ branchReadOnly: false }),
|
||||
} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[20],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[21],
|
||||
} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[21],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[22],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[23],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[24],
|
||||
{ isLicensed: jest.fn().mockReturnValue(false) } as unknown as ConstructorParameters<
|
||||
typeof InstanceAiAdapterService
|
||||
>[24],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[25],
|
||||
>[25],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[26],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[27],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[28],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[29],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[30],
|
||||
);
|
||||
|
||||
(
|
||||
@@ -862,22 +863,23 @@ function createDataTableAdapterForTests(overrides?: {
|
||||
collectTypes: jest.fn().mockResolvedValue({ nodes: [], credentials: [] }),
|
||||
} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[12],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[13],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[14],
|
||||
mockDataTableService as unknown as DataTableService,
|
||||
mockDataTableRepository as unknown as DataTableRepository,
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[16],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[17],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[18],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[19],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[20],
|
||||
mockSourceControlPreferencesService as unknown as SourceControlPreferencesService,
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[21],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[22],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[23],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[24],
|
||||
{ isLicensed: jest.fn().mockReturnValue(false) } as unknown as License,
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[25],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[26],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[27],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[28],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[29],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[30],
|
||||
);
|
||||
|
||||
const adapter = service.createContext(mockUser).dataTableService;
|
||||
@@ -1133,14 +1135,15 @@ function createWorkflowAdapterForTests(overrides?: {
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[17],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[18],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[19],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[20],
|
||||
{
|
||||
getPreferences: jest
|
||||
.fn()
|
||||
.mockReturnValue({ branchReadOnly: overrides?.branchReadOnly ?? false }),
|
||||
} as unknown as SourceControlPreferencesService,
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[21],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[22],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[23],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[24],
|
||||
{
|
||||
isLicensed: jest.fn().mockImplementation((feat: string) => {
|
||||
if (feat === 'feat:namedVersions') return overrides?.namedVersionsLicensed ?? false;
|
||||
@@ -1149,10 +1152,10 @@ function createWorkflowAdapterForTests(overrides?: {
|
||||
}),
|
||||
isSharingEnabled: jest.fn().mockReturnValue(false),
|
||||
} as unknown as License,
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[25],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[26],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[27],
|
||||
{ track: jest.fn() } as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[28],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[28],
|
||||
{ track: jest.fn() } as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[29],
|
||||
mockAiBuilderTemporaryWorkflowRepository as unknown as AiBuilderTemporaryWorkflowRepository,
|
||||
);
|
||||
|
||||
@@ -1469,18 +1472,19 @@ function createExecutionAdapterForTests(overrides?: { sharingEnabled?: boolean }
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[17],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[18],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[19],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[20],
|
||||
{
|
||||
getPreferences: jest.fn().mockReturnValue({ branchReadOnly: false }),
|
||||
} as unknown as SourceControlPreferencesService,
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[21],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[22],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[23],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[24],
|
||||
mockLicense as unknown as License,
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[25],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[26],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[27],
|
||||
mockRoleService as unknown as RoleService,
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[28],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[29],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[30],
|
||||
);
|
||||
|
||||
const adapter = service.createContext(mockUser).executionService;
|
||||
|
||||
@@ -105,6 +105,7 @@ import { EventService } from '@/events/event.service';
|
||||
import { ExecutionPersistence } from '@/executions/execution-persistence';
|
||||
import { License } from '@/license';
|
||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import { NodeTypes } from '@/node-types';
|
||||
import { DataTableRepository } from '@/modules/data-table/data-table.repository';
|
||||
import { DataTableService } from '@/modules/data-table/data-table.service';
|
||||
import { SourceControlPreferencesService } from '@/modules/source-control.ee/source-control-preferences.service.ee';
|
||||
@@ -170,6 +171,7 @@ export class InstanceAiAdapterService {
|
||||
private readonly activeExecutions: ActiveExecutions,
|
||||
private readonly workflowRunner: WorkflowRunner,
|
||||
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
|
||||
private readonly nodeTypes: NodeTypes,
|
||||
private readonly instanceSettings: InstanceSettings,
|
||||
private readonly dataTableService: DataTableService,
|
||||
private readonly dataTableRepository: DataTableRepository,
|
||||
@@ -212,6 +214,7 @@ export class InstanceAiAdapterService {
|
||||
workspaceService: this.createWorkspaceAdapter(user),
|
||||
licenseHints: this.buildLicenseHints(),
|
||||
logger: this.logger,
|
||||
nodeTypesProvider: this.nodeTypes,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user