diff --git a/src/test/fixtures/xml-samples.ts b/src/test/fixtures/xml-samples.ts new file mode 100644 index 0000000..ee2c1ee --- /dev/null +++ b/src/test/fixtures/xml-samples.ts @@ -0,0 +1,197 @@ +// Test fixtures for XML utility testing + +export const longTextWithAddNotes = ` +Now I can see the melody! This is a beautiful classical-style melody in C major. Let me analyze it: + +First 4 bars (beats 0-16): +- Bar 1: E E F G (likely suggesting C major - Am - F - C progressions) +- Bar 2: G F E D (descending line, could work with G - F - C) +- Bar 3: C C D E (starting on tonic, building up) +- Bar 4: E3/2 D1/2 D2 (resolution to D, suggesting dominant function) + +Last 4 bars (beats 16-32): +- Bar 5: E E F G (similar to bar 1) +- Bar 6: G F E D (similar to bar 2) +- Bar 7: C C D E (similar to bar 3) +- Bar 8: D3/2 C1/2 C2 (final resolution to C) + +This looks like "Ode to Joy" or a similar classical melody. For the harmony, I should: + +1. First 4 bars (soft and light): Use simple triads in root position or gentle inversions +2. Last 4 bars (fuller and brighter): Add some extensions, maybe use inversions for smoother voice leading, and possibly add an additional voice + +Let me think about chord progressions: +- Bar 1 (E E F G): C major works well +- Bar 2 (G F E D): G major (dominant) +- Bar 3 (C C D E): F major or Am could work +- Bar 4 (E D D): G major resolving the phrase + +For the second half, I can use similar progressions but make them richer. + +I'll start with the first 4 bars, keeping them light with simple triads in a comfortable mid-range. + + +Perfect! I can see this is a beautiful classical-style melody. Let me analyze the harmonic structure and create a supportive pad harmony track. + +For the first four bars, I'll use gentle, simple triads to support the melody without overpowering it. For the last four bars, I'll add more fullness and brightness while maintaining harmonic support. + +Let me start by adding the harmony for the first four bars (beats 0-16): + + + + +C4 +0 +4 + + +E4 +0 +4 + + +G4 +0 +4 + + +G3 +4 +4 + + +B3 +4 +4 + + +D4 +4 +4 + + +A3 +8 +4 + + +C4 +8 +4 + + +E4 +8 +4 + + +G3 +12 +4 + + +B3 +12 +4 + + +D4 +12 +4 + + +`; + +export const longTextWithReadMusic = ` +I need to create a pad harmony track for an existing melody. First, I should read the music to see what melody I'm working with, then analyze its harmonic structure to create appropriate pad chords. The user wants the first four bars (beats 0-16) to be soft and light, and the last four bars (beats 16-32) to be fuller and brighter while remaining supportive. + +0 +32 + +Let me start by reading the current music to understand the melody I'm harmonizing. + + +I'll help you create a pad harmony track for your melody. Let me first read the existing music to understand the melodic content and structure. + + +0 +32 +`; + +export const multipleXmlBlocks = `Here's how to add multiple musical elements: + +First, let's add some notes: + + + +C4 +0 +2 + + + + +Then we can read the current music: + +0 +8 + + +Finally, let's modify the tempo: + +120 + + +That's how you work with multiple XML commands!`; + +export const nestedXmlContent = ` + + +Test Content + + + +More content here + +`; + +export const xmlWithAttributes = `Here's an XML block with attributes: + + + + +C4 +0 +1 + + + + +That was an example with attributes.`; + +export const malformedXml = `This contains some malformed XML: + +content + +content + +But this should work: + +This is valid +`; + +export const noXmlContent = `This is just plain text content without any XML blocks. +It contains some angle brackets like and but no complete XML elements. +Also some + + + + + + + Some text with spaces + +`; \ No newline at end of file diff --git a/src/util/timeUtil.test.ts b/src/util/timeUtil.test.ts new file mode 100644 index 0000000..f9064e8 --- /dev/null +++ b/src/util/timeUtil.test.ts @@ -0,0 +1,316 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { parseTimeSignature, getTimeSignatureErrorMessage, beatsToTimeString, formatLocalDateTime } from './timeUtil' +import { TIME_CONSTANTS } from '../constants/coreConstants' + +describe('timeUtil', () => { + describe('parseTimeSignature', () => { + it('should parse valid time signatures', () => { + expect(parseTimeSignature('4/4')).toEqual({ numerator: 4, denominator: 4 }) + expect(parseTimeSignature('3/4')).toEqual({ numerator: 3, denominator: 4 }) + expect(parseTimeSignature('6/8')).toEqual({ numerator: 6, denominator: 8 }) + expect(parseTimeSignature('12/8')).toEqual({ numerator: 12, denominator: 8 }) + expect(parseTimeSignature('2/4')).toEqual({ numerator: 2, denominator: 4 }) + }) + + it('should handle whitespace around input', () => { + expect(parseTimeSignature(' 4/4 ')).toEqual({ numerator: 4, denominator: 4 }) + expect(parseTimeSignature(' 3/4 ')).toEqual({ numerator: 3, denominator: 4 }) + expect(parseTimeSignature('\t6/8\n')).toEqual({ numerator: 6, denominator: 8 }) + }) + + it('should return null for invalid formats', () => { + expect(parseTimeSignature('4')).toBeNull() + expect(parseTimeSignature('4/4/4')).toBeNull() + expect(parseTimeSignature('4-4')).toBeNull() + expect(parseTimeSignature('4:4')).toBeNull() + expect(parseTimeSignature('')).toBeNull() + expect(parseTimeSignature('/')).toBeNull() + expect(parseTimeSignature('4/')).toBeNull() + expect(parseTimeSignature('/4')).toBeNull() + }) + + it('should return null for non-numeric values', () => { + expect(parseTimeSignature('a/4')).toBeNull() + expect(parseTimeSignature('4/b')).toBeNull() + expect(parseTimeSignature('x/y')).toBeNull() + // Note: parseInt('4.5') returns 4, so these will parse as integers + // Testing the actual behavior of parseInt + expect(parseTimeSignature('4.5/4')).toEqual({ numerator: 4, denominator: 4 }) + expect(parseTimeSignature('4/4.5')).toEqual({ numerator: 4, denominator: 4 }) + }) + + it('should return null for numerators not in available list', () => { + // Assuming TIME_CONSTANTS has specific available numerators + expect(parseTimeSignature('99/4')).toBeNull() + expect(parseTimeSignature('0/4')).toBeNull() + expect(parseTimeSignature('-1/4')).toBeNull() + }) + + it('should return null for denominators not in available list', () => { + // Assuming TIME_CONSTANTS has specific available denominators + expect(parseTimeSignature('4/99')).toBeNull() + expect(parseTimeSignature('4/0')).toBeNull() + expect(parseTimeSignature('4/-1')).toBeNull() + }) + + it('should validate against TIME_CONSTANTS available values', () => { + // Test that function actually uses TIME_CONSTANTS for validation + const validNumerator = TIME_CONSTANTS.AVAILABLE_TIME_SIGNATURE_NUMERATORS[0] + const validDenominator = TIME_CONSTANTS.AVAILABLE_TIME_SIGNATURE_DENOMINATORS[0] + const invalidNumerator = 999 // Assuming this is not in the available list + const invalidDenominator = 999 // Assuming this is not in the available list + + expect(parseTimeSignature(`${validNumerator}/${validDenominator}`)).not.toBeNull() + expect(parseTimeSignature(`${invalidNumerator}/${validDenominator}`)).toBeNull() + expect(parseTimeSignature(`${validNumerator}/${invalidDenominator}`)).toBeNull() + }) + }) + + describe('getTimeSignatureErrorMessage', () => { + it('should return a formatted error message with available options', () => { + const message = getTimeSignatureErrorMessage() + + expect(message).toContain('Invalid time signature format') + expect(message).toContain('numerator/denominator') + expect(message).toContain('Available numerators:') + expect(message).toContain('Available denominators:') + expect(message).toContain('Examples: 4/4, 3/4, 6/8, 12/8') + }) + + it('should include actual available values from TIME_CONSTANTS', () => { + const message = getTimeSignatureErrorMessage() + + // Check that it includes values from TIME_CONSTANTS + TIME_CONSTANTS.AVAILABLE_TIME_SIGNATURE_NUMERATORS.forEach(numerator => { + expect(message).toContain(numerator.toString()) + }) + + TIME_CONSTANTS.AVAILABLE_TIME_SIGNATURE_DENOMINATORS.forEach(denominator => { + expect(message).toContain(denominator.toString()) + }) + }) + + it('should be a consistent message format', () => { + const message1 = getTimeSignatureErrorMessage() + const message2 = getTimeSignatureErrorMessage() + + expect(message1).toBe(message2) + }) + }) + + describe('beatsToTimeString', () => { + it('should format beats to BBB:B | mm:ss:mmm format', () => { + // Test basic 4/4 time signature + expect(beatsToTimeString(0, 120, { numerator: 4, denominator: 4 })) + .toBe('001:1 | 00:00:000') + + expect(beatsToTimeString(4, 120, { numerator: 4, denominator: 4 })) + .toBe('002:1 | 00:02:000') + + expect(beatsToTimeString(8, 120, { numerator: 4, denominator: 4 })) + .toBe('003:1 | 00:04:000') + }) + + it('should handle different time signatures correctly', () => { + // 3/4 time signature - 3 beats per bar + expect(beatsToTimeString(0, 120, { numerator: 3, denominator: 4 })) + .toBe('001:1 | 00:00:000') + + expect(beatsToTimeString(3, 120, { numerator: 3, denominator: 4 })) + .toBe('002:1 | 00:01:500') + + expect(beatsToTimeString(6, 120, { numerator: 3, denominator: 4 })) + .toBe('003:1 | 00:03:000') + + // 6/8 time signature - 6 beats per bar + expect(beatsToTimeString(0, 120, { numerator: 6, denominator: 8 })) + .toBe('001:1 | 00:00:000') + + expect(beatsToTimeString(6, 120, { numerator: 6, denominator: 8 })) + .toBe('002:1 | 00:03:000') + }) + + it('should handle different BPM values correctly', () => { + // 60 BPM - 1 beat per second + expect(beatsToTimeString(1, 60, { numerator: 4, denominator: 4 })) + .toBe('001:2 | 00:01:000') + + expect(beatsToTimeString(4, 60, { numerator: 4, denominator: 4 })) + .toBe('002:1 | 00:04:000') + + // 240 BPM - 4 beats per second + expect(beatsToTimeString(1, 240, { numerator: 4, denominator: 4 })) + .toBe('001:2 | 00:00:250') + + expect(beatsToTimeString(4, 240, { numerator: 4, denominator: 4 })) + .toBe('002:1 | 00:01:000') + }) + + it('should handle fractional beats correctly', () => { + expect(beatsToTimeString(1.5, 120, { numerator: 4, denominator: 4 })) + .toBe('001:2 | 00:00:750') + + expect(beatsToTimeString(2.25, 120, { numerator: 4, denominator: 4 })) + .toBe('001:3 | 00:01:125') + + expect(beatsToTimeString(4.75, 120, { numerator: 4, denominator: 4 })) + .toBe('002:1 | 00:02:375') + }) + + it('should pad numbers correctly in output format', () => { + // Test bar padding (BBB format) + expect(beatsToTimeString(0, 120, { numerator: 4, denominator: 4 })) + .toMatch(/^001:/) + + expect(beatsToTimeString(40, 120, { numerator: 4, denominator: 4 })) + .toMatch(/^011:/) // Bar 11 + + expect(beatsToTimeString(396, 120, { numerator: 4, denominator: 4 })) + .toMatch(/^100:/) // Bar 100 + + // Test time padding (mm:ss:mmm format) + expect(beatsToTimeString(1, 60, { numerator: 4, denominator: 4 })) + .toMatch(/\| 00:01:000$/) + + expect(beatsToTimeString(75, 60, { numerator: 4, denominator: 4 })) + .toMatch(/\| 01:15:000$/) // 1 minute 15 seconds + }) + + it('should handle large beat values', () => { + const result = beatsToTimeString(1000, 120, { numerator: 4, denominator: 4 }) + expect(result).toMatch(/^251:1 \| \d{2}:\d{2}:\d{3}$/) + }) + + it('should handle edge case of zero BPM gracefully', () => { + // This might cause division by zero, should handle gracefully + expect(() => beatsToTimeString(1, 0, { numerator: 4, denominator: 4 })) + .not.toThrow() + }) + + it('should handle negative beats', () => { + // Edge case - negative beats may produce negative values in time format + const result = beatsToTimeString(-1, 120, { numerator: 4, denominator: 4 }) + expect(result).toBeDefined() + expect(typeof result).toBe('string') + // The function may produce negative time values for negative beats + expect(result).toMatch(/^\d{3}:\d \| -?\d+:-?\d+:-?\d+$/) + }) + }) + + describe('formatLocalDateTime', () => { + let mockDate: Date + + beforeEach(() => { + // Use a fixed date for consistent testing + mockDate = new Date('2025-08-21T19:57:11.123Z') + }) + + it('should format date with correct structure', () => { + const result = formatLocalDateTime(mockDate) + + // Should contain date parts + expect(result).toMatch(/\d{4}/) // Year + expect(result).toMatch(/\d{2}/) // Month/day/hour/minute/second + expect(result).toContain(':') // Time separator + expect(result).toMatch(/GMT[+-]\d+|UTC|[A-Z]{3,4}/) // Timezone + }) + + it('should use 24-hour format', () => { + const morningDate = new Date('2025-08-21T09:30:00Z') + const eveningDate = new Date('2025-08-21T21:30:00Z') + + const morningResult = formatLocalDateTime(morningDate) + const eveningResult = formatLocalDateTime(eveningDate) + + // Should not contain AM/PM indicators + expect(morningResult).not.toMatch(/AM|PM/i) + expect(eveningResult).not.toMatch(/AM|PM/i) + }) + + it('should include timezone information', () => { + const result = formatLocalDateTime(mockDate) + + // Should contain some timezone indicator + expect(result).toMatch(/GMT[+-]\d+|UTC|[A-Z]{3,4}|\+\d{4}|-\d{4}/) + }) + + it('should handle different dates consistently', () => { + const dates = [ + new Date('2025-01-01T00:00:00Z'), + new Date('2025-06-15T12:30:45Z'), + new Date('2025-12-31T23:59:59Z') + ] + + dates.forEach(date => { + const result = formatLocalDateTime(date) + expect(result).toBeDefined() + expect(typeof result).toBe('string') + expect(result.length).toBeGreaterThan(10) + }) + }) + + it('should handle edge dates', () => { + const edgeDates = [ + new Date('1970-01-01T00:00:00Z'), // Unix epoch + new Date('2038-01-19T03:14:07Z'), // Near 32-bit timestamp limit + new Date('2100-12-31T23:59:59Z') // Future date + ] + + edgeDates.forEach(date => { + expect(() => formatLocalDateTime(date)).not.toThrow() + const result = formatLocalDateTime(date) + expect(typeof result).toBe('string') + expect(result.length).toBeGreaterThan(0) + }) + }) + + it('should be consistent for the same date', () => { + const result1 = formatLocalDateTime(mockDate) + const result2 = formatLocalDateTime(mockDate) + + expect(result1).toBe(result2) + }) + + it('should handle leap year dates', () => { + const leapYearDate = new Date('2024-02-29T12:00:00Z') // Leap year + + expect(() => formatLocalDateTime(leapYearDate)).not.toThrow() + const result = formatLocalDateTime(leapYearDate) + expect(result).toContain('2024') + expect(result).toContain('02') + expect(result).toContain('29') + }) + }) + + describe('integration tests', () => { + it('should work together for typical DAW workflow', () => { + // Parse time signature + const timeSignature = parseTimeSignature('4/4') + expect(timeSignature).not.toBeNull() + + // Use parsed time signature in time formatting + const timeString = beatsToTimeString(16, 120, timeSignature!) + expect(timeString).toBe('005:1 | 00:08:000') + + // Format current time + const now = new Date() + const formattedTime = formatLocalDateTime(now) + expect(formattedTime).toBeDefined() + }) + + it('should handle error cases gracefully in workflow', () => { + // Invalid time signature should not break workflow + const invalidTimeSignature = parseTimeSignature('invalid') + expect(invalidTimeSignature).toBeNull() + + // Get error message for user feedback + const errorMessage = getTimeSignatureErrorMessage() + expect(errorMessage).toContain('Invalid time signature') + + // Fallback to default time signature + const fallbackTimeSignature = { numerator: 4, denominator: 4 } + const timeString = beatsToTimeString(8, 120, fallbackTimeSignature) + expect(timeString).toBe('003:1 | 00:04:000') + }) + }) +}) \ No newline at end of file diff --git a/src/util/xmlUtil.test.ts b/src/util/xmlUtil.test.ts new file mode 100644 index 0000000..8e2ec1c --- /dev/null +++ b/src/util/xmlUtil.test.ts @@ -0,0 +1,331 @@ +import { describe, it, expect } from 'vitest' +import { extractXMLFromString, wrapXmlBlocksInContent } from './xmlUtil' +import { + longTextWithAddNotes, + longTextWithReadMusic, + multipleXmlBlocks, + nestedXmlContent, + xmlWithAttributes, + malformedXml, + noXmlContent, + emptyAndWhitespaceXml +} from '../test/fixtures/xml-samples' + +describe('xmlUtil', () => { + describe('extractXMLFromString', () => { + it('should extract simple XML blocks', () => { + const input = `Here is some text with content and more text.` + const result = extractXMLFromString(input) + + expect(result).toHaveLength(1) + expect(result[0]).toBe('content') + }) + + it('should extract multiple XML blocks', () => { + const input = `content1 some text content2` + const result = extractXMLFromString(input) + + expect(result).toHaveLength(2) + expect(result[0]).toBe('content1') + expect(result[1]).toBe('content2') + }) + + it('should extract XML blocks with nested elements', () => { + const input = `nested content` + const result = extractXMLFromString(input) + + expect(result).toHaveLength(1) + expect(result[0]).toBe('nested content') + }) + + it('should extract XML blocks with attributes', () => { + const input = `content` + const result = extractXMLFromString(input) + + expect(result).toHaveLength(1) + expect(result[0]).toBe('content') + }) + + it('should handle multiline XML blocks', () => { + const input = ` + content1 + content2 +` + const result = extractXMLFromString(input) + + expect(result).toHaveLength(1) + expect(result[0]).toContain('') + expect(result[0]).toContain('content1') + expect(result[0]).toContain('content2') + expect(result[0]).toContain('') + }) + + it('should extract XML from complex nested content fixture', () => { + const result = extractXMLFromString(nestedXmlContent) + + expect(result).toHaveLength(1) + expect(result[0]).toContain('') + expect(result[0]).toContain('') + expect(result[0]).toContain('') + expect(result[0]).toContain('Test Content') + expect(result[0]).toContain('') + }) + + it('should extract multiple XML blocks from fixture', () => { + const result = extractXMLFromString(multipleXmlBlocks) + + expect(result).toHaveLength(3) + expect(result[0]).toContain('') + expect(result[0]).toContain('') + expect(result[1]).toContain('') + expect(result[1]).toContain('') + expect(result[2]).toContain('') + expect(result[2]).toContain('') + }) + + it('should extract XML with attributes from fixture', () => { + const result = extractXMLFromString(xmlWithAttributes) + + expect(result).toHaveLength(1) + expect(result[0]).toContain('region_id="main"') + expect(result[0]).toContain('track="melody"') + expect(result[0]).toContain('id="1"') + expect(result[0]).toContain('velocity="127"') + }) + + it('should handle the long text with add_notes fixture (Case 1)', () => { + const result = extractXMLFromString(longTextWithAddNotes) + + expect(result).toHaveLength(2) // Contains thinking tag and add_notes block + + // Find the add_notes block + const addNotesBlock = result.find(block => block.includes('')) + expect(addNotesBlock).toBeDefined() + expect(addNotesBlock).toContain('') + expect(addNotesBlock).toContain('') + expect(addNotesBlock).toContain('C4') + expect(addNotesBlock).toContain('0') + expect(addNotesBlock).toContain('4') + expect(addNotesBlock).toContain('') + + // Should contain all 12 notes + const noteMatches = addNotesBlock!.match(//g) + expect(noteMatches).toHaveLength(12) + }) + + it('should handle the long text with read_music fixture (Case 2)', () => { + const result = extractXMLFromString(longTextWithReadMusic) + + expect(result).toHaveLength(2) + + // First XML block (inside thinking tag) + expect(result[0]).toContain('') + expect(result[0]).toContain('0') + expect(result[0]).toContain('32') + expect(result[0]).toContain('') + + // Second XML block (at the end) + expect(result[1]).toContain('') + expect(result[1]).toContain('0') + expect(result[1]).toContain('32') + expect(result[1]).toContain('') + }) + + it('should handle malformed XML gracefully', () => { + const result = extractXMLFromString(malformedXml) + + // Should extract valid XML blocks (ignores malformed ones) + expect(result.length).toBeGreaterThanOrEqual(1) + + // Find the definitely valid block + const validBlock = result.find(block => block.includes('')) + expect(validBlock).toBeDefined() + expect(validBlock).toContain('This is valid') + }) + + it('should return empty array for content with no XML', () => { + const result = extractXMLFromString(noXmlContent) + + expect(result).toHaveLength(0) + expect(result).toEqual([]) + }) + + it('should handle empty and whitespace XML', () => { + const result = extractXMLFromString(emptyAndWhitespaceXml) + + expect(result).toHaveLength(3) + expect(result[0]).toBe('') + expect(result[1]).toContain('') + expect(result[1]).toContain('') + expect(result[2]).toContain('') + expect(result[2]).toContain('Some text with spaces') + expect(result[2]).toContain('') + }) + + it('should handle XML with underscores and hyphens in tag names', () => { + const input = `content and content` + const result = extractXMLFromString(input) + + expect(result).toHaveLength(2) + expect(result[0]).toBe('content') + expect(result[1]).toBe('content') + }) + + it('should handle self-closing tags (not currently supported)', () => { + const input = ` and content` + const result = extractXMLFromString(input) + + // Current implementation doesn't support self-closing tags + expect(result).toHaveLength(1) + expect(result[0]).toBe('content') + }) + + it('should trim whitespace around extracted XML', () => { + const input = ` content ` + const result = extractXMLFromString(input) + + expect(result).toHaveLength(1) + expect(result[0]).toBe('content') + }) + }) + + describe('wrapXmlBlocksInContent', () => { + it('should wrap single XML block in fenced code block', () => { + const input = `Here is content in text.` + const result = wrapXmlBlocksInContent(input) + + expect(result).toBe('Here is ```xml\ncontent\n``` in text.') + }) + + it('should wrap multiple XML blocks', () => { + const input = `content1 text content2` + const result = wrapXmlBlocksInContent(input) + + expect(result).toContain('```xml\ncontent1\n```') + expect(result).toContain('```xml\ncontent2\n```') + }) + + it('should return original content when no XML blocks present', () => { + const input = noXmlContent + const result = wrapXmlBlocksInContent(input) + + expect(result).toBe(input) + }) + + it('should handle empty input', () => { + expect(wrapXmlBlocksInContent('')).toBe('') + expect(wrapXmlBlocksInContent(null as any)).toBeNull() + expect(wrapXmlBlocksInContent(undefined as any)).toBeUndefined() + }) + + it('should wrap XML blocks from multipleXmlBlocks fixture', () => { + const result = wrapXmlBlocksInContent(multipleXmlBlocks) + + expect(result).toContain('```xml\n') + expect(result).toContain('\n```') + expect(result).toContain('```xml\n') + expect(result).toContain('\n```') + expect(result).toContain('```xml\n') + expect(result).toContain('\n```') + + // Should preserve the surrounding text + expect(result).toContain('Here\'s how to add multiple musical elements:') + expect(result).toContain('First, let\'s add some notes:') + expect(result).toContain('Then we can read the current music:') + }) + + it('should wrap the long add_notes XML block (Case 1)', () => { + const result = wrapXmlBlocksInContent(longTextWithAddNotes) + + expect(result).toContain('```xml\n') + expect(result).toContain('\n```') + + // Should preserve the thinking content and other text + expect(result).toContain('') + expect(result).toContain('Perfect! I can see this is a beautiful') + expect(result).toContain('Let me start by adding the harmony') + }) + + it('should wrap the long read_music XML blocks (Case 2)', () => { + const result = wrapXmlBlocksInContent(longTextWithReadMusic) + + // Should contain two wrapped XML blocks + const fencedBlocks = result.match(/```xml\n[\s\S]*?<\/read_music>\n```/g) + expect(fencedBlocks).toHaveLength(2) + + // Should preserve the thinking content and other text + expect(result).toContain('') + expect(result).toContain('I\'ll help you create a pad harmony track') + expect(result).toContain('Let me first read the existing music') + }) + + it('should handle nested XML correctly', () => { + const result = wrapXmlBlocksInContent(nestedXmlContent) + + expect(result).toContain('```xml\n') + expect(result).toContain('') + expect(result).toContain('') + expect(result).toContain('Test Content') + expect(result).toContain('\n```') + }) + + it('should preserve XML block integrity when wrapping', () => { + const input = `Text before\n\n value\n content\n\nText after` + const result = wrapXmlBlocksInContent(input) + + expect(result).toBe(`Text before\n\`\`\`xml\n\n value\n content\n\n\`\`\`\nText after`) + }) + + it('should handle duplicate XML blocks correctly', () => { + const input = `content and then content again` + const result = wrapXmlBlocksInContent(input) + + // Both instances should be wrapped + const wrappedBlocks = result.match(/```xml\ncontent<\/same>\n```/g) + expect(wrappedBlocks).toHaveLength(2) + }) + }) + + describe('edge cases and error handling', () => { + it('should handle very large XML blocks', () => { + const largeContent = 'x'.repeat(10000) + const input = `${largeContent}` + + const extracted = extractXMLFromString(input) + expect(extracted).toHaveLength(1) + expect(extracted[0]).toContain(largeContent) + + const wrapped = wrapXmlBlocksInContent(input) + expect(wrapped).toContain('```xml\n') + expect(wrapped).toContain('\n```') + }) + + it('should handle XML with special characters', () => { + const input = `Content with < > & " '` + + const extracted = extractXMLFromString(input) + expect(extracted).toHaveLength(1) + expect(extracted[0]).toContain('< > & " '') + + const wrapped = wrapXmlBlocksInContent(input) + expect(wrapped).toContain('```xml\nContent with < > & " '\n```') + }) + + it('should handle XML with CDATA sections', () => { + const input = ` chars]]>` + + const extracted = extractXMLFromString(input) + expect(extracted).toHaveLength(1) + expect(extracted[0]).toContain('') + }) + + it('should handle mixed content with partial XML-like text', () => { + const input = `This < is not XML and neither > is this content is valid` + + const extracted = extractXMLFromString(input) + expect(extracted).toHaveLength(1) + expect(extracted[0]).toBe('content') + }) + }) +}) \ No newline at end of file