feat: added unit tests for timeUtil and xmlUtil.

This commit is contained in:
Xiaohan-Tian
2025-09-06 18:53:36 -07:00
parent 2bf5328af7
commit 2dcc76368e
3 changed files with 844 additions and 0 deletions
+197
View File
@@ -0,0 +1,197 @@
// Test fixtures for XML utility testing
export const longTextWithAddNotes = `<thinking>
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.
</thinking>
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):
<add_notes>
<notes>
<note>
<pitch>C4</pitch>
<start_beat>0</start_beat>
<length>4</length>
</note>
<note>
<pitch>E4</pitch>
<start_beat>0</start_beat>
<length>4</length>
</note>
<note>
<pitch>G4</pitch>
<start_beat>0</start_beat>
<length>4</length>
</note>
<note>
<pitch>G3</pitch>
<start_beat>4</start_beat>
<length>4</length>
</note>
<note>
<pitch>B3</pitch>
<start_beat>4</start_beat>
<length>4</length>
</note>
<note>
<pitch>D4</pitch>
<start_beat>4</start_beat>
<length>4</length>
</note>
<note>
<pitch>A3</pitch>
<start_beat>8</start_beat>
<length>4</length>
</note>
<note>
<pitch>C4</pitch>
<start_beat>8</start_beat>
<length>4</length>
</note>
<note>
<pitch>E4</pitch>
<start_beat>8</start_beat>
<length>4</length>
</note>
<note>
<pitch>G3</pitch>
<start_beat>12</start_beat>
<length>4</length>
</note>
<note>
<pitch>B3</pitch>
<start_beat>12</start_beat>
<length>4</length>
</note>
<note>
<pitch>D4</pitch>
<start_beat>12</start_beat>
<length>4</length>
</note>
</notes>
</add_notes>`;
export const longTextWithReadMusic = `<thinking>
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.
<read_music>
<start_beat>0</start_beat>
<length>32</length>
</read_music>
Let me start by reading the current music to understand the melody I'm harmonizing.
</thinking>
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.
<read_music>
<start_beat>0</start_beat>
<length>32</length>
</read_music>`;
export const multipleXmlBlocks = `Here's how to add multiple musical elements:
First, let's add some notes:
<add_notes>
<notes>
<note>
<pitch>C4</pitch>
<start_beat>0</start_beat>
<length>2</length>
</note>
</notes>
</add_notes>
Then we can read the current music:
<read_music>
<start_beat>0</start_beat>
<length>8</length>
</read_music>
Finally, let's modify the tempo:
<modify_tempo>
<bpm>120</bpm>
</modify_tempo>
That's how you work with multiple XML commands!`;
export const nestedXmlContent = `<container>
<inner_element>
<deep_nested>
<value>Test Content</value>
</deep_nested>
</inner_element>
<another_element>
<data>More content here</data>
</another_element>
</container>`;
export const xmlWithAttributes = `Here's an XML block with attributes:
<add_notes region_id="main" track="melody">
<notes>
<note id="1" velocity="127">
<pitch>C4</pitch>
<start_beat>0</start_beat>
<length>1</length>
</note>
</notes>
</add_notes>
That was an example with attributes.`;
export const malformedXml = `This contains some malformed XML:
<unclosed_tag>
<properly_closed>content</properly_closed>
<another_unclosed>
<mismatched_tag>content</wrong_tag>
But this should work:
<valid_tag>
<content>This is valid</content>
</valid_tag>`;
export const noXmlContent = `This is just plain text content without any XML blocks.
It contains some angle brackets like <this> and </that> but no complete XML elements.
Also some <incomplete tags and random characters.`;
export const emptyAndWhitespaceXml = `
<empty_tag></empty_tag>
<whitespace_tag>
</whitespace_tag>
<mixed_content>
Some text with spaces
</mixed_content>
`;
+316
View File
@@ -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')
})
})
})
+331
View File
@@ -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 <test>content</test> and more text.`
const result = extractXMLFromString(input)
expect(result).toHaveLength(1)
expect(result[0]).toBe('<test>content</test>')
})
it('should extract multiple XML blocks', () => {
const input = `<first>content1</first> some text <second>content2</second>`
const result = extractXMLFromString(input)
expect(result).toHaveLength(2)
expect(result[0]).toBe('<first>content1</first>')
expect(result[1]).toBe('<second>content2</second>')
})
it('should extract XML blocks with nested elements', () => {
const input = `<outer><inner>nested content</inner></outer>`
const result = extractXMLFromString(input)
expect(result).toHaveLength(1)
expect(result[0]).toBe('<outer><inner>nested content</inner></outer>')
})
it('should extract XML blocks with attributes', () => {
const input = `<tag attr="value" id="123">content</tag>`
const result = extractXMLFromString(input)
expect(result).toHaveLength(1)
expect(result[0]).toBe('<tag attr="value" id="123">content</tag>')
})
it('should handle multiline XML blocks', () => {
const input = `<multiline>
<line1>content1</line1>
<line2>content2</line2>
</multiline>`
const result = extractXMLFromString(input)
expect(result).toHaveLength(1)
expect(result[0]).toContain('<multiline>')
expect(result[0]).toContain('<line1>content1</line1>')
expect(result[0]).toContain('<line2>content2</line2>')
expect(result[0]).toContain('</multiline>')
})
it('should extract XML from complex nested content fixture', () => {
const result = extractXMLFromString(nestedXmlContent)
expect(result).toHaveLength(1)
expect(result[0]).toContain('<container>')
expect(result[0]).toContain('<inner_element>')
expect(result[0]).toContain('<deep_nested>')
expect(result[0]).toContain('<value>Test Content</value>')
expect(result[0]).toContain('</container>')
})
it('should extract multiple XML blocks from fixture', () => {
const result = extractXMLFromString(multipleXmlBlocks)
expect(result).toHaveLength(3)
expect(result[0]).toContain('<add_notes>')
expect(result[0]).toContain('</add_notes>')
expect(result[1]).toContain('<read_music>')
expect(result[1]).toContain('</read_music>')
expect(result[2]).toContain('<modify_tempo>')
expect(result[2]).toContain('</modify_tempo>')
})
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('<add_notes>'))
expect(addNotesBlock).toBeDefined()
expect(addNotesBlock).toContain('<notes>')
expect(addNotesBlock).toContain('<note>')
expect(addNotesBlock).toContain('<pitch>C4</pitch>')
expect(addNotesBlock).toContain('<start_beat>0</start_beat>')
expect(addNotesBlock).toContain('<length>4</length>')
expect(addNotesBlock).toContain('</add_notes>')
// Should contain all 12 notes
const noteMatches = addNotesBlock!.match(/<note>/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('<read_music>')
expect(result[0]).toContain('<start_beat>0</start_beat>')
expect(result[0]).toContain('<length>32</length>')
expect(result[0]).toContain('</read_music>')
// Second XML block (at the end)
expect(result[1]).toContain('<read_music>')
expect(result[1]).toContain('<start_beat>0</start_beat>')
expect(result[1]).toContain('<length>32</length>')
expect(result[1]).toContain('</read_music>')
})
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('<valid_tag>'))
expect(validBlock).toBeDefined()
expect(validBlock).toContain('<content>This is valid</content>')
})
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('<empty_tag></empty_tag>')
expect(result[1]).toContain('<whitespace_tag>')
expect(result[1]).toContain('</whitespace_tag>')
expect(result[2]).toContain('<mixed_content>')
expect(result[2]).toContain('Some text with spaces')
expect(result[2]).toContain('</mixed_content>')
})
it('should handle XML with underscores and hyphens in tag names', () => {
const input = `<tag_name>content</tag_name> and <tag-name>content</tag-name>`
const result = extractXMLFromString(input)
expect(result).toHaveLength(2)
expect(result[0]).toBe('<tag_name>content</tag_name>')
expect(result[1]).toBe('<tag-name>content</tag-name>')
})
it('should handle self-closing tags (not currently supported)', () => {
const input = `<self-closing /> and <normal>content</normal>`
const result = extractXMLFromString(input)
// Current implementation doesn't support self-closing tags
expect(result).toHaveLength(1)
expect(result[0]).toBe('<normal>content</normal>')
})
it('should trim whitespace around extracted XML', () => {
const input = ` <tag>content</tag> `
const result = extractXMLFromString(input)
expect(result).toHaveLength(1)
expect(result[0]).toBe('<tag>content</tag>')
})
})
describe('wrapXmlBlocksInContent', () => {
it('should wrap single XML block in fenced code block', () => {
const input = `Here is <test>content</test> in text.`
const result = wrapXmlBlocksInContent(input)
expect(result).toBe('Here is ```xml\n<test>content</test>\n``` in text.')
})
it('should wrap multiple XML blocks', () => {
const input = `<first>content1</first> text <second>content2</second>`
const result = wrapXmlBlocksInContent(input)
expect(result).toContain('```xml\n<first>content1</first>\n```')
expect(result).toContain('```xml\n<second>content2</second>\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<add_notes>')
expect(result).toContain('</add_notes>\n```')
expect(result).toContain('```xml\n<read_music>')
expect(result).toContain('</read_music>\n```')
expect(result).toContain('```xml\n<modify_tempo>')
expect(result).toContain('</modify_tempo>\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<add_notes>')
expect(result).toContain('</add_notes>\n```')
// Should preserve the thinking content and other text
expect(result).toContain('<thinking>')
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<read_music>[\s\S]*?<\/read_music>\n```/g)
expect(fencedBlocks).toHaveLength(2)
// Should preserve the thinking content and other text
expect(result).toContain('<thinking>')
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<container>')
expect(result).toContain('<inner_element>')
expect(result).toContain('<deep_nested>')
expect(result).toContain('<value>Test Content</value>')
expect(result).toContain('</container>\n```')
})
it('should preserve XML block integrity when wrapping', () => {
const input = `Text before\n<complex>\n <nested>value</nested>\n <another>content</another>\n</complex>\nText after`
const result = wrapXmlBlocksInContent(input)
expect(result).toBe(`Text before\n\`\`\`xml\n<complex>\n <nested>value</nested>\n <another>content</another>\n</complex>\n\`\`\`\nText after`)
})
it('should handle duplicate XML blocks correctly', () => {
const input = `<same>content</same> and then <same>content</same> again`
const result = wrapXmlBlocksInContent(input)
// Both instances should be wrapped
const wrappedBlocks = result.match(/```xml\n<same>content<\/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 = `<large>${largeContent}</large>`
const extracted = extractXMLFromString(input)
expect(extracted).toHaveLength(1)
expect(extracted[0]).toContain(largeContent)
const wrapped = wrapXmlBlocksInContent(input)
expect(wrapped).toContain('```xml\n<large>')
expect(wrapped).toContain('</large>\n```')
})
it('should handle XML with special characters', () => {
const input = `<tag>Content with &lt; &gt; &amp; &quot; &apos;</tag>`
const extracted = extractXMLFromString(input)
expect(extracted).toHaveLength(1)
expect(extracted[0]).toContain('&lt; &gt; &amp; &quot; &apos;')
const wrapped = wrapXmlBlocksInContent(input)
expect(wrapped).toContain('```xml\n<tag>Content with &lt; &gt; &amp; &quot; &apos;</tag>\n```')
})
it('should handle XML with CDATA sections', () => {
const input = `<tag><![CDATA[Some content with <special> chars]]></tag>`
const extracted = extractXMLFromString(input)
expect(extracted).toHaveLength(1)
expect(extracted[0]).toContain('<![CDATA[')
expect(extracted[0]).toContain(']]>')
})
it('should handle mixed content with partial XML-like text', () => {
const input = `This < is not XML and neither > is this <incomplete and <valid>content</valid> is valid`
const extracted = extractXMLFromString(input)
expect(extracted).toHaveLength(1)
expect(extracted[0]).toBe('<valid>content</valid>')
})
})
})