mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-01 15:47:41 +08:00
feat(ai-builder): Format zod erros on agent config - better toast msg (#34867)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
AgentJsonConfigSchema,
|
||||
findVectorStoreToolNameCollisions,
|
||||
formatAgentConfigZodError,
|
||||
} from '../agent-json-config.schema';
|
||||
|
||||
const minimalConfig = {
|
||||
@@ -542,3 +543,32 @@ describe('AgentJsonConfigSchema — model/credential coupling', () => {
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatAgentConfigZodError', () => {
|
||||
it('formats an invalid MCP server name as path: message without a Zod JSON dump', () => {
|
||||
const result = AgentJsonConfigSchema.safeParse({
|
||||
...minimalConfig,
|
||||
mcpServers: [
|
||||
{
|
||||
name: 'has spaces',
|
||||
url: 'https://example.com/mcp',
|
||||
transport: 'streamableHttp',
|
||||
authentication: 'none',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
if (result.success) return;
|
||||
|
||||
const formatted = formatAgentConfigZodError(result.error);
|
||||
expect(formatted).toContain('mcpServers.0.name');
|
||||
expect(formatted).toContain(
|
||||
'MCP server name can only contain letters, numbers, hyphens, and underscores',
|
||||
);
|
||||
expect(formatted).not.toContain('"validation": "regex"');
|
||||
expect(result.error.issues[0]?.message).toBe(
|
||||
'MCP server name can only contain letters, numbers, hyphens, and underscores',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,7 +90,9 @@ const WebSearchConfigSchema = z.object({
|
||||
credential: z.string().optional(),
|
||||
});
|
||||
|
||||
const HexColorSchema = z.string().regex(/^#[0-9A-Fa-f]{6}$/);
|
||||
const HexColorSchema = z
|
||||
.string()
|
||||
.regex(/^#[0-9A-Fa-f]{6}$/, 'Color must be a 6-digit hex value (e.g. #FF5500)');
|
||||
|
||||
export const DEFAULT_AGENT_PERSONALISATION = {
|
||||
icon: 'bot',
|
||||
@@ -196,7 +198,10 @@ const AgentJsonSkillConfigSchema = z.object({
|
||||
id: z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(/^[A-Za-z0-9_-]+$/),
|
||||
.regex(
|
||||
/^[A-Za-z0-9_-]+$/,
|
||||
'Skill id can only contain letters, numbers, hyphens, and underscores',
|
||||
),
|
||||
});
|
||||
|
||||
const AgentJsonTaskConfigSchema = z.object({
|
||||
@@ -204,7 +209,10 @@ const AgentJsonTaskConfigSchema = z.object({
|
||||
id: z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(/^[A-Za-z0-9_-]+$/),
|
||||
.regex(
|
||||
/^[A-Za-z0-9_-]+$/,
|
||||
'Task id can only contain letters, numbers, hyphens, and underscores',
|
||||
),
|
||||
enabled: z.boolean(),
|
||||
});
|
||||
|
||||
@@ -228,6 +236,10 @@ export const McpServerConfigSchema = z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(64)
|
||||
.regex(
|
||||
/^[a-zA-Z0-9_-]+$/,
|
||||
'MCP server name can only contain letters, numbers, hyphens, and underscores',
|
||||
)
|
||||
.refine((name) => name.trim().length > 0, 'MCP server name cannot be blank')
|
||||
.describe('Unique display name. The SDK normalizes it when building model-facing tool names'),
|
||||
description: z.string().max(512).optional().describe('Human-readable server description'),
|
||||
@@ -323,7 +335,10 @@ const VectorStoreBaseShape = {
|
||||
.string()
|
||||
.min(1)
|
||||
.max(64)
|
||||
.regex(VECTOR_STORE_NAME_REGEX)
|
||||
.regex(
|
||||
VECTOR_STORE_NAME_REGEX,
|
||||
'Vector store name can only contain letters, numbers, hyphens, and underscores',
|
||||
)
|
||||
.describe('Unique connection name, also used as the SDK tool-name suffix: search_<name>'),
|
||||
credential: CredentialIdSchema,
|
||||
useWhen: z.string().trim().min(1).max(VECTOR_STORE_USE_WHEN_MAX_LENGTH),
|
||||
@@ -365,7 +380,13 @@ export const AgentVectorStoreConfigSchema = z.discriminatedUnion('provider', [
|
||||
|
||||
const CustomToolJsonConfigSchema = z.object({
|
||||
type: z.literal('custom'),
|
||||
id: z.string().min(1).regex(CUSTOM_TOOL_ID_REGEX),
|
||||
id: z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(
|
||||
CUSTOM_TOOL_ID_REGEX,
|
||||
'Custom tool id can only contain letters, numbers, and underscores',
|
||||
),
|
||||
requireApproval: z.boolean().optional(),
|
||||
});
|
||||
|
||||
@@ -574,3 +595,9 @@ export function formatZodErrors(error: ZodError): ConfigValidationError[] {
|
||||
received: 'received' in issue ? String(issue.received) : undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
export function formatAgentConfigZodError(error: ZodError): string {
|
||||
return formatZodErrors(error)
|
||||
.map((issue) => `${issue.path}: ${issue.message}`)
|
||||
.join('; ');
|
||||
}
|
||||
|
||||
@@ -162,6 +162,30 @@ describe('AgentConfigService', () => {
|
||||
}),
|
||||
).resolves.toMatchObject({ valid: true });
|
||||
});
|
||||
|
||||
it('returns a human-readable Zod error for an invalid MCP server name', async () => {
|
||||
const { service } = makeService();
|
||||
|
||||
const result = await service.validateConfig({
|
||||
...baseConfig,
|
||||
mcpServers: [
|
||||
{
|
||||
name: 'has spaces',
|
||||
url: 'https://example.com/mcp',
|
||||
transport: 'streamableHttp',
|
||||
authentication: 'none',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
if (result.valid) return;
|
||||
expect(result.error).toContain('mcpServers.0.name');
|
||||
expect(result.error).toContain(
|
||||
'MCP server name can only contain letters, numbers, hyphens, and underscores',
|
||||
);
|
||||
expect(result.error).not.toContain('"validation": "regex"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateConfig', () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { reconcileNativeWebSearch } from '@n8n/ai-utilities/agent-config';
|
||||
import {
|
||||
AgentJsonConfigSchema,
|
||||
findVectorStoreToolNameCollisions,
|
||||
formatAgentConfigZodError,
|
||||
sanitizeAgentJsonConfig,
|
||||
type AgentJsonConfig,
|
||||
type AgentJsonToolConfig,
|
||||
@@ -66,7 +67,7 @@ export class AgentConfigService {
|
||||
|
||||
const parsed = AgentJsonConfigSchema.safeParse(sanitizeAgentJsonConfig(raw));
|
||||
if (!parsed.success) {
|
||||
return { valid: false, error: parsed.error.message };
|
||||
return { valid: false, error: formatAgentConfigZodError(parsed.error) };
|
||||
}
|
||||
|
||||
const config = parsed.data;
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Agent as RuntimeAgent, BuiltAgent, BuiltTool, CredentialProvider }
|
||||
import type { AgentJsonConfig, AgentSkill } from '@n8n/api-types';
|
||||
import {
|
||||
AGENT_WORKFLOW_TRIGGER_TYPE,
|
||||
formatZodErrors,
|
||||
formatAgentConfigZodError,
|
||||
RunnableInlineAgentConfigSchema,
|
||||
sanitizeAgentJsonConfig,
|
||||
sanitizeAgentSkillBodies,
|
||||
@@ -556,10 +556,9 @@ export class AgentWorkflowExecutionService {
|
||||
...(payload.skills !== undefined ? { skills: sanitizeAgentSkillBodies(payload.skills) } : {}),
|
||||
});
|
||||
if (!parsed.success) {
|
||||
const details = formatZodErrors(parsed.error)
|
||||
.map((issue) => `${issue.path}: ${issue.message}`)
|
||||
.join('; ');
|
||||
throw new UserError(`Invalid inline agent configuration: ${details}`);
|
||||
throw new UserError(
|
||||
`Invalid inline agent configuration: ${formatAgentConfigZodError(parsed.error)}`,
|
||||
);
|
||||
}
|
||||
const config = parsed.data.config;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user