first commit
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
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
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
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' } } } }]],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user