Files
alighasami 3d5eaf9445
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
first commit
2026-03-17 16:22:57 +03:30

357 lines
7.9 KiB
TypeScript

import { mock } from 'jest-mock-extended';
import type { INode, ISupplyDataFunctions } from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow';
import { z } from 'zod';
import { N8nStructuredOutputParser } from './N8nStructuredOutputParser';
describe('N8nStructuredOutputParser', () => {
let mockContext: jest.Mocked<ISupplyDataFunctions>;
beforeEach(() => {
mockContext = mock<ISupplyDataFunctions>();
mockContext.addInputData.mockReturnValue({ index: 0 });
mockContext.addOutputData.mockReturnValue(undefined);
mockContext.getNode.mockReturnValue({
name: 'Test Node',
type: 'n8n-nodes-langchain.outputParserStructured',
typeVersion: 1.3,
position: [0, 0],
} as INode);
});
// Bug AI-1852
describe('Backticks in JSON string values', () => {
it('should parse JSON containing markdown code blocks in string values', async () => {
const schema = z.object({
output: z.object({
message: z.string(),
status: z.string(),
}),
});
const parser = new N8nStructuredOutputParser(mockContext, schema);
// This is the exact problematic output from the bug report
// Valid JSON wrapped in code fence, but contains backticks INSIDE the message field
const problematicOutput = `\`\`\`json
{
"output": {
"message": "## Example\\n\`\`\`bash\\n--set globals.enable=false\\n\`\`\`\\n",
"status": "completed"
}
}
\`\`\``;
const result = await parser.parse(problematicOutput);
expect(result).toEqual({
output: {
message: '## Example\n```bash\n--set globals.enable=false\n```\n',
status: 'completed',
},
});
});
it('should parse JSON containing multiple code blocks in string values', async () => {
const schema = z.object({
output: z.object({
content: z.string(),
}),
});
const parser = new N8nStructuredOutputParser(mockContext, schema);
const multipleCodeBlocksOutput = `\`\`\`json
{
"output": {
"content": "First example:\\n\`\`\`javascript\\nconst x = 1;\\n\`\`\`\\n\\nSecond example:\\n\`\`\`python\\nprint('hello')\\n\`\`\`"
}
}
\`\`\``;
const result = await parser.parse(multipleCodeBlocksOutput);
expect(result).toEqual({
output: {
content:
"First example:\n```javascript\nconst x = 1;\n```\n\nSecond example:\n```python\nprint('hello')\n```",
},
});
});
it('should parse nested markdown in complex JSON structures', async () => {
const schema = z.object({
output: z.object({
steps: z.array(
z.object({
title: z.string(),
description: z.string(),
code: z.string(),
}),
),
}),
});
const parser = new N8nStructuredOutputParser(mockContext, schema);
const complexOutput = `\`\`\`json
{
"output": {
"steps": [
{
"title": "Step 1",
"description": "Run this command:\\n\`\`\`bash\\nnpm install\\n\`\`\`",
"code": "npm install"
}
]
}
}
\`\`\``;
const result = await parser.parse(complexOutput);
expect(result).toEqual({
output: {
steps: [
{
title: 'Step 1',
description: 'Run this command:\n```bash\nnpm install\n```',
code: 'npm install',
},
],
},
});
});
});
describe('Valid JSON parsing', () => {
it('should parse valid JSON without code fence', async () => {
const schema = z.object({
output: z.object({
message: z.string(),
status: z.string(),
}),
});
const parser = new N8nStructuredOutputParser(mockContext, schema);
const validOutput = `{
"output": {
"message": "Simple message",
"status": "completed"
}
}`;
const result = await parser.parse(validOutput);
expect(result).toEqual({
output: {
message: 'Simple message',
status: 'completed',
},
});
});
it('should parse valid JSON wrapped in code fence without internal backticks', async () => {
const schema = z.object({
output: z.object({
message: z.string(),
status: z.string(),
}),
});
const parser = new N8nStructuredOutputParser(mockContext, schema);
const validOutput = `\`\`\`json
{
"output": {
"message": "Simple message",
"status": "completed"
}
}
\`\`\``;
const result = await parser.parse(validOutput);
expect(result).toEqual({
output: {
message: 'Simple message',
status: 'completed',
},
});
});
it('should parse JSON with escaped quotes and newlines', async () => {
const schema = z.object({
output: z.object({
message: z.string(),
}),
});
const parser = new N8nStructuredOutputParser(mockContext, schema);
const validOutput = `{
"output": {
"message": "Line 1\\nLine 2\\n\\"quoted\\""
}
}`;
const result = await parser.parse(validOutput);
expect(result).toEqual({
output: {
message: 'Line 1\nLine 2\n"quoted"',
},
});
});
it('should handle code fence with json language marker', async () => {
const schema = z.object({
output: z.object({
data: z.string(),
}),
});
const parser = new N8nStructuredOutputParser(mockContext, schema);
const validOutput = `\`\`\`json
{
"output": {
"data": "test"
}
}
\`\`\``;
const result = await parser.parse(validOutput);
expect(result).toEqual({
output: {
data: 'test',
},
});
});
it('should handle code fence without language marker', async () => {
const schema = z.object({
output: z.object({
data: z.string(),
}),
});
const parser = new N8nStructuredOutputParser(mockContext, schema);
const validOutput = `\`\`\`
{
"output": {
"data": "test"
}
}
\`\`\``;
const result = await parser.parse(validOutput);
expect(result).toEqual({
output: {
data: 'test',
},
});
});
});
describe('Error handling', () => {
it('should handle invalid JSON gracefully', async () => {
const schema = z.object({
output: z.object({
message: z.string(),
}),
});
const parser = new N8nStructuredOutputParser(mockContext, schema);
const invalidOutput = 'not valid json';
await expect(parser.parse(invalidOutput)).rejects.toThrow(
"Model output doesn't fit required format",
);
});
it('should handle empty output', async () => {
const schema = z.object({
output: z.object({
message: z.string(),
}),
});
const parser = new N8nStructuredOutputParser(mockContext, schema);
const emptyOutput = '{}';
await expect(parser.parse(emptyOutput)).rejects.toThrow(
"Model output doesn't fit required format",
);
});
it('should handle schema mismatch', async () => {
const schema = z.object({
output: z.object({
message: z.string(),
requiredField: z.string(),
}),
});
const parser = new N8nStructuredOutputParser(mockContext, schema);
const mismatchOutput = `{
"output": {
"message": "Test"
}
}`;
await expect(parser.parse(mismatchOutput)).rejects.toThrow(
"Model output doesn't fit required format",
);
});
});
describe('Context integration', () => {
it('should call addInputData with correct parameters', async () => {
const schema = z.object({
output: z.object({
message: z.string(),
}),
});
const parser = new N8nStructuredOutputParser(mockContext, schema);
const validOutput = '{"output": {"message": "test"}}';
await parser.parse(validOutput);
expect(mockContext.addInputData).toHaveBeenCalledWith(NodeConnectionTypes.AiOutputParser, [
[{ json: { action: 'parse', text: validOutput } }],
]);
});
it('should call addOutputData with parsed result', async () => {
const schema = z.object({
output: z.object({
message: z.string(),
}),
});
const parser = new N8nStructuredOutputParser(mockContext, schema);
const validOutput = '{"output": {"message": "test"}}';
await parser.parse(validOutput);
expect(mockContext.addOutputData).toHaveBeenCalledWith(
NodeConnectionTypes.AiOutputParser,
0,
[[{ json: { action: 'parse', response: { output: { message: 'test' } } } }]],
);
});
});
});