fix(core): Keep $fromAI placeholders intact on fields with expressions disabled (#31681)

Co-authored-by: Alexander Gekov <40495748+alexander-gekov@users.noreply.github.com>
This commit is contained in:
Rohit Gahlawat 2026-06-19 15:26:24 +05:30 committed by GitHub
parent 227050b965
commit 43eec00e6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 67 additions and 1 deletions

View File

@ -9,6 +9,7 @@ import { v4 as uuid } from 'uuid';
import { EXECUTE_WORKFLOW_NODE_TYPE, WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE } from './constants';
import { UnexpectedError, UserError } from './errors';
import { isExpression } from './expressions/expression-helpers';
import { isFromAIOnlyExpression } from './from-ai-parse-utils';
import { NodeConnectionTypes } from './interfaces';
import type {
FieldType,
@ -823,7 +824,8 @@ export function getNodeParameters(
// Strip expression prefix if noDataExpression is true
if (nodeProperties.noDataExpression && nodeParameters[nodeProperties.name] !== undefined) {
const value = nodeParameters[nodeProperties.name];
if (isExpression(value)) {
// A lone $fromAI() placeholder must keep its "=" or the AI tool call never resolves it (#30531)
if (isExpression(value) && !isFromAIOnlyExpression(value)) {
nodeParameters[nodeProperties.name] = value.slice(1);
nodeParametersFull[nodeProperties.name] = nodeParameters[nodeProperties.name];
}

View File

@ -6729,6 +6729,70 @@ describe('NodeHelpers', () => {
// When undefined, the default value (empty string) is used
expect(result?.resource).toBe('');
});
describe('$fromAI placeholder carve-out', () => {
const resolveQuery = (query: string) => {
const properties: INodeProperties[] = [
{
name: 'query',
displayName: 'Query',
type: 'string',
default: '',
noDataExpression: true,
},
];
const nodeValues: Record<string, string> = { query };
const node: INode = {
id: 'test-123',
name: 'Test',
type: 'n8n-nodes-base.test',
typeVersion: 1,
position: [0, 0],
parameters: nodeValues,
credentials: {},
};
const description: INodeTypeDescription = {
displayName: 'Test',
name: 'Test',
group: [],
version: 1,
description: 'Test',
defaults: {},
inputs: [],
outputs: [],
properties,
};
return getNodeParameters(properties, nodeValues, true, false, node, description)?.query;
};
it('keeps a lone $fromAI() placeholder intact', () => {
const query = "=$fromAI('sqlQuery', 'The SQL query to execute', 'string')";
expect(resolveQuery(query)).toBe(query);
});
it('keeps a $fromAI() placeholder wrapped in {{ }} intact', () => {
const query = "={{ $fromAI('sqlQuery') }}";
expect(resolveQuery(query)).toBe(query);
});
it('still strips a plain expression that is not a $fromAI() call', () => {
expect(resolveQuery('=$env.SECRET')).toBe('$env.SECRET');
});
it('still strips $fromAI() concatenated with another expression', () => {
expect(resolveQuery("=$fromAI('x') + $env.SECRET")).toBe("$fromAI('x') + $env.SECRET");
});
it('still strips $fromAI() with an interpolated value in its argument', () => {
// Split the `${` token so the lint rule doesn't read it as interpolation.
const interpolated = '`$' + '{$env.SECRET}`';
expect(resolveQuery(`=$fromAI(${interpolated})`)).toBe(`$fromAI(${interpolated})`);
});
});
});
describe('getNodeParameters filter defaults', () => {