buster/packages/ai/tests/utils/streaming/escape-normalizer.test.ts

215 lines
9.3 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
hasDoubleEscaping,
normalizeEscapedText,
normalizeStreamingChunk,
normalizeStreamingText,
} from '../../../src/utils/streaming/escape-normalizer';
describe('escape-normalizer', () => {
describe('hasDoubleEscaping', () => {
it('should detect double-escaped newlines', () => {
expect(hasDoubleEscaping('Hello\\\\nWorld')).toBe(true);
expect(hasDoubleEscaping('Hello\\nWorld')).toBe(false);
expect(hasDoubleEscaping('Hello\nWorld')).toBe(false);
});
it('should detect double-escaped tabs', () => {
expect(hasDoubleEscaping('Hello\\\\tWorld')).toBe(true);
expect(hasDoubleEscaping('Hello\\tWorld')).toBe(false);
expect(hasDoubleEscaping('Hello\tWorld')).toBe(false);
});
it('should detect double-escaped quotes', () => {
expect(hasDoubleEscaping('Say \\\\"Hello\\\\"')).toBe(true);
expect(hasDoubleEscaping('Say \\"Hello\\"')).toBe(false);
expect(hasDoubleEscaping('Say "Hello"')).toBe(false);
});
it('should detect mixed double-escaping', () => {
expect(hasDoubleEscaping('Line 1\\\\nLine 2\\\\tTabbed')).toBe(true);
expect(hasDoubleEscaping('Line 1\\nLine 2\\tTabbed')).toBe(false);
});
});
describe('normalizeEscapedText', () => {
it('should normalize double-escaped newlines', () => {
expect(normalizeEscapedText('Hello\\\\nWorld')).toBe('Hello\nWorld');
expect(normalizeEscapedText('Line 1\\\\nLine 2\\\\nLine 3')).toBe('Line 1\nLine 2\nLine 3');
});
it('should normalize double-escaped tabs', () => {
expect(normalizeEscapedText('Name\\\\tValue')).toBe('Name\tValue');
expect(normalizeEscapedText('Col1\\\\tCol2\\\\tCol3')).toBe('Col1\tCol2\tCol3');
});
it('should normalize double-escaped quotes', () => {
expect(normalizeEscapedText('Say \\\\"Hello\\\\"')).toBe('Say "Hello"');
expect(normalizeEscapedText('\\\\"quoted text\\\\"')).toBe('"quoted text"');
});
it('should normalize double-escaped backslashes', () => {
// Test pure backslash normalization
expect(normalizeEscapedText('Path\\\\\\\\file')).toBe('Path\\\\file');
// Test that we normalize escape sequences properly
expect(normalizeEscapedText('Line1\\\\nLine2')).toBe('Line1\nLine2');
});
it('should handle complex mixed escaping', () => {
const input = 'Line 1\\\\n\\\\tIndented\\\\n\\\\"Quote\\\\"\\\\nEnd';
const expected = 'Line 1\n\tIndented\n"Quote"\nEnd';
expect(normalizeEscapedText(input)).toBe(expected);
});
it('should not modify properly escaped text', () => {
const properlyEscaped = 'Hello\nWorld\tTabbed "Quoted"';
expect(normalizeEscapedText(properlyEscaped)).toBe(properlyEscaped);
});
it('should handle the specific case from the bug report', () => {
const input =
'Let me check the customer data structure to understand how to get customer names for the visualization.\\\\n\\\\nLooking at the customer model relationships:\\\\n- `customer` references `person` (for individual customers) via `personid`\\\\n- `customer` references `store` (for business customers) via `storeid`\\\\n- `person` has `firstname`, `lastname` fields\\\\n- `store` has `name` field\\\\n\\\\nI should query the data to see what the customer records look like and how to properly get customer names. This will help me plan the correct SQL query for the visualization.';
const expected =
'Let me check the customer data structure to understand how to get customer names for the visualization.\n\nLooking at the customer model relationships:\n- `customer` references `person` (for individual customers) via `personid`\n- `customer` references `store` (for business customers) via `storeid`\n- `person` has `firstname`, `lastname` fields\n- `store` has `name` field\n\nI should query the data to see what the customer records look like and how to properly get customer names. This will help me plan the correct SQL query for the visualization.';
expect(normalizeEscapedText(input)).toBe(expected);
});
});
describe('normalizeStreamingText', () => {
it('should handle text context normalization', () => {
const input = 'Hello\\\\nWorld';
expect(normalizeStreamingText(input, 'text')).toBe('Hello\nWorld');
});
it('should preserve valid JSON in json context', () => {
const validJson = '{"message": "Hello\\nWorld"}';
expect(normalizeStreamingText(validJson, 'json')).toBe(validJson);
});
it('should normalize double-escaped JSON content', () => {
const doubleEscapedJson = '{"message": "Hello\\\\nWorld"}';
expect(normalizeStreamingText(doubleEscapedJson, 'json')).toBe('{"message": "Hello\nWorld"}');
});
it('should handle partial JSON with double escaping', () => {
const partialJson = '{"message": "Hello\\\\nWor';
expect(normalizeStreamingText(partialJson, 'json')).toBe('{"message": "Hello\nWor');
});
});
describe('normalizeStreamingChunk', () => {
it('should detect and fix double-escaping in chunks', () => {
const chunk = 'Hello\\\\nWorld';
const result = normalizeStreamingChunk(chunk);
expect(result.normalized).toBe('Hello\nWorld');
expect(result.hasChanges).toBe(true);
});
it('should not modify chunks without double-escaping', () => {
const chunk = 'Hello\nWorld';
const result = normalizeStreamingChunk(chunk);
expect(result.normalized).toBe(chunk);
expect(result.hasChanges).toBe(false);
});
it('should handle boundary double-escaping', () => {
const previousChunk = 'Text ending with \\\\';
const currentChunk = 'n continuing';
const result = normalizeStreamingChunk(currentChunk, previousChunk);
// In this case, the boundary check doesn't detect double escaping because
// it's split across chunks - this is an edge case that might need special handling
// For now, we accept that this edge case won't be caught
expect(result.hasChanges).toBe(false);
});
it('should handle complex streaming scenarios', () => {
// Simulate streaming chunks of the bug report example
const chunks = [
'{"thought": "Let me check the customer data structure',
' to understand how to get customer names for the visualization.\\\\n\\\\nLooking at',
' the customer model relationships:\\\\n- `customer` references',
' `person` (for individual customers) via `personid`\\\\n- `customer`',
' references `store` (for business customers) via `storeid`"}',
];
let accumulated = '';
for (const chunk of chunks) {
const result = normalizeStreamingChunk(chunk, accumulated);
accumulated += result.normalized;
}
// The accumulated result should have normalized newlines
expect(accumulated).toContain('\n\nLooking at');
expect(accumulated).toContain('\n- `customer` references');
expect(accumulated).not.toContain('\\\\n');
});
});
describe('Mock streaming chunks for testing', () => {
// These are realistic chunks that might come from the streaming API
const mockChunks = [
// Chunk 1: Start of a sequential thinking tool call
{
chunk:
'{"toolCallId":"call_123","toolName":"sequential-thinking","args":{"thought":"Let me analyze',
expectDoubleEscaping: false,
},
// Chunk 2: Middle with proper escaping
{
chunk: ' the data structure.\\nFirst, I need to',
expectDoubleEscaping: false,
},
// Chunk 3: Middle with double escaping (bug case)
{
chunk: ' understand the relationships.\\\\n\\\\nThe customer table',
expectDoubleEscaping: true,
},
// Chunk 4: List with double escaping
{
chunk: ' has:\\\\n- Foreign key to person\\\\n- Foreign key to store',
expectDoubleEscaping: true,
},
// Chunk 5: End of thought
{
chunk: '","nextThoughtNeeded":true,"thoughtNumber":1}}',
expectDoubleEscaping: false,
},
];
it('should process mock chunks correctly', () => {
let accumulated = '';
const results: Array<{ chunk: string; normalized: string; hasChanges: boolean }> = [];
for (const mock of mockChunks) {
const result = normalizeStreamingChunk(mock.chunk, accumulated);
results.push({
chunk: mock.chunk,
normalized: result.normalized,
hasChanges: result.hasChanges,
});
accumulated += result.normalized;
// Verify double escaping detection
expect(hasDoubleEscaping(mock.chunk)).toBe(mock.expectDoubleEscaping);
}
// The final accumulated JSON should be parseable
// But it has unescaped newlines, so we need to check the content differently
expect(accumulated).toContain('"toolName":"sequential-thinking"');
expect(accumulated).toContain('"thought":"Let me analyze');
// Check that double escaping was normalized
expect(accumulated).toContain(
'The customer table has:\n- Foreign key to person\n- Foreign key to store'
);
expect(accumulated).not.toContain('\\\\n');
// Verify we have the expected structure even if JSON.parse fails due to unescaped newlines
expect(accumulated).toContain('"nextThoughtNeeded":true');
expect(accumulated).toContain('"thoughtNumber":1');
});
});
});