mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
fix(core): Allow .trim() in workflow-sdk parsing and coerce stringified patches (#29111)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d2d7dfc276
commit
b44e9d1207
|
|
@ -0,0 +1,33 @@
|
|||
import { buildWorkflowInputSchema } from '../build-workflow.tool';
|
||||
|
||||
describe('buildWorkflowInputSchema.patches coercion', () => {
|
||||
const patch = { old_str: 'foo', new_str: 'bar' };
|
||||
|
||||
it('accepts a native array of patches', () => {
|
||||
const parsed = buildWorkflowInputSchema.parse({ patches: [patch] });
|
||||
expect(parsed.patches).toEqual([patch]);
|
||||
});
|
||||
|
||||
it('accepts a JSON-stringified array of patches', () => {
|
||||
const parsed = buildWorkflowInputSchema.parse({ patches: JSON.stringify([patch]) });
|
||||
expect(parsed.patches).toEqual([patch]);
|
||||
});
|
||||
|
||||
it('rejects a non-JSON string with a helpful array-expected error', () => {
|
||||
const result = buildWorkflowInputSchema.safeParse({ patches: 'not-json' });
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues[0].path).toEqual(['patches']);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects a stringified object (not an array)', () => {
|
||||
const result = buildWorkflowInputSchema.safeParse({ patches: JSON.stringify(patch) });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('leaves patches undefined when not provided', () => {
|
||||
const parsed = buildWorkflowInputSchema.parse({});
|
||||
expect(parsed.patches).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -15,13 +15,25 @@ const patchSchema = z.object({
|
|||
new_str: z.string().describe('Replacement string'),
|
||||
});
|
||||
|
||||
// Coerce JSON-stringified arrays into arrays. The model sometimes sends `patches`
|
||||
// as a JSON string because the payload contains escaped code. Leave non-strings
|
||||
// untouched so Zod can validate them normally.
|
||||
function coercePatches(value: unknown): unknown {
|
||||
if (typeof value !== 'string') return value;
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export const buildWorkflowInputSchema = z.object({
|
||||
code: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Full TypeScript workflow code using @n8n/workflow-sdk. Required for new workflows.'),
|
||||
patches: z
|
||||
.array(patchSchema)
|
||||
.preprocess(coercePatches, z.array(patchSchema))
|
||||
.optional()
|
||||
.describe(
|
||||
'Array of {old_str, new_str} replacements to apply to existing workflow code. ' +
|
||||
|
|
|
|||
|
|
@ -932,6 +932,32 @@ describe('AST Interpreter', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('String.trim', () => {
|
||||
let sdkFunctions: SDKFunctions;
|
||||
|
||||
beforeEach(() => {
|
||||
sdkFunctions = createMockSDKFunctions();
|
||||
});
|
||||
|
||||
it('should allow " abc ".trim()', () => {
|
||||
const code = 'export default " abc ".trim();';
|
||||
const result = interpretSDKCode(code, sdkFunctions);
|
||||
expect(result).toBe('abc');
|
||||
});
|
||||
|
||||
it('should allow trim on a template literal', () => {
|
||||
const code = 'export default `\n hello\n`.trim();';
|
||||
const result = interpretSDKCode(code, sdkFunctions);
|
||||
expect(result).toBe('hello');
|
||||
});
|
||||
|
||||
it('should allow trim on a variable holding a string', () => {
|
||||
const code = 'const padded = " x "; export default padded.trim();';
|
||||
const result = interpretSDKCode(code, sdkFunctions);
|
||||
expect(result).toBe('x');
|
||||
});
|
||||
});
|
||||
|
||||
describe('expr(placeholder(...)) error', () => {
|
||||
it('should throw clear error when expr receives a PlaceholderValue', () => {
|
||||
const funcs: SDKFunctions = {
|
||||
|
|
|
|||
|
|
@ -377,6 +377,7 @@ export function getSafeJSONMethod(
|
|||
*/
|
||||
const SAFE_STRING_METHODS: Record<string, (str: string, ...args: unknown[]) => unknown> = {
|
||||
repeat: (str: string, count: unknown) => str.repeat(count as number),
|
||||
trim: (str: string) => str.trim(),
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user