fix(core): Pass nodeTypesProvider to validate workflows fully at instance AI (#29333)

This commit is contained in:
Jaakko Husso
2026-04-29 14:25:20 +00:00
committed by GitHub
parent 84ac8110f8
commit 388cd79908
12 changed files with 319 additions and 30 deletions
@@ -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,
+6
View File
@@ -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 -1
View File
@@ -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);
});
@@ -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,
@@ -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,
};
}