| import { describe, it, expect, vi, beforeEach } from 'vitest'; |
| import { |
| escapeXml, |
| unescapeXml, |
| extractXmlSection, |
| extractXmlElements, |
| extractImplementedFeatures, |
| extractImplementedFeatureNames, |
| featureToXml, |
| featuresToXml, |
| updateImplementedFeaturesSection, |
| addImplementedFeature, |
| removeImplementedFeature, |
| updateImplementedFeature, |
| hasImplementedFeature, |
| toSpecOutputFeatures, |
| fromSpecOutputFeatures, |
| type ImplementedFeature, |
| type XmlExtractorLogger, |
| } from '@/lib/xml-extractor.js'; |
|
|
| describe('xml-extractor.ts', () => { |
| |
| const createMockLogger = (): XmlExtractorLogger & { calls: string[] } => { |
| const calls: string[] = []; |
| return { |
| calls, |
| debug: vi.fn((msg: string) => calls.push(`debug: ${msg}`)), |
| warn: vi.fn((msg: string) => calls.push(`warn: ${msg}`)), |
| }; |
| }; |
|
|
| beforeEach(() => { |
| vi.clearAllMocks(); |
| }); |
|
|
| describe('escapeXml', () => { |
| it('should escape ampersand', () => { |
| expect(escapeXml('foo & bar')).toBe('foo & bar'); |
| }); |
|
|
| it('should escape less than', () => { |
| expect(escapeXml('a < b')).toBe('a < b'); |
| }); |
|
|
| it('should escape greater than', () => { |
| expect(escapeXml('a > b')).toBe('a > b'); |
| }); |
|
|
| it('should escape double quotes', () => { |
| expect(escapeXml('say "hello"')).toBe('say "hello"'); |
| }); |
|
|
| it('should escape single quotes', () => { |
| expect(escapeXml("it's" + ' fine')).toBe('it's fine'); |
| }); |
|
|
| it('should handle null', () => { |
| expect(escapeXml(null)).toBe(''); |
| }); |
|
|
| it('should handle undefined', () => { |
| expect(escapeXml(undefined)).toBe(''); |
| }); |
|
|
| it('should handle empty string', () => { |
| expect(escapeXml('')).toBe(''); |
| }); |
|
|
| it('should escape multiple special characters', () => { |
| expect(escapeXml('a < b & c > d "e" \'f\'')).toBe( |
| 'a < b & c > d "e" 'f'' |
| ); |
| }); |
| }); |
|
|
| describe('unescapeXml', () => { |
| it('should unescape ampersand', () => { |
| expect(unescapeXml('foo & bar')).toBe('foo & bar'); |
| }); |
|
|
| it('should unescape less than', () => { |
| expect(unescapeXml('a < b')).toBe('a < b'); |
| }); |
|
|
| it('should unescape greater than', () => { |
| expect(unescapeXml('a > b')).toBe('a > b'); |
| }); |
|
|
| it('should unescape double quotes', () => { |
| expect(unescapeXml('say "hello"')).toBe('say "hello"'); |
| }); |
|
|
| it('should unescape single quotes', () => { |
| expect(unescapeXml('it's fine')).toBe("it's fine"); |
| }); |
|
|
| it('should handle empty string', () => { |
| expect(unescapeXml('')).toBe(''); |
| }); |
|
|
| it('should roundtrip with escapeXml', () => { |
| const original = 'Test & <value> "quoted" \'apostrophe\''; |
| expect(unescapeXml(escapeXml(original))).toBe(original); |
| }); |
| }); |
|
|
| describe('extractXmlSection', () => { |
| it('should extract section content', () => { |
| const xml = '<root><section>content here</section></root>'; |
| expect(extractXmlSection(xml, 'section')).toBe('content here'); |
| }); |
|
|
| it('should extract multiline section content', () => { |
| const xml = `<root> |
| <section> |
| line 1 |
| line 2 |
| </section> |
| </root>`; |
| expect(extractXmlSection(xml, 'section')).toContain('line 1'); |
| expect(extractXmlSection(xml, 'section')).toContain('line 2'); |
| }); |
|
|
| it('should return null for non-existent section', () => { |
| const xml = '<root><other>content</other></root>'; |
| expect(extractXmlSection(xml, 'section')).toBeNull(); |
| }); |
|
|
| it('should be case-insensitive', () => { |
| const xml = '<root><Section>content</Section></root>'; |
| expect(extractXmlSection(xml, 'section')).toBe('content'); |
| }); |
|
|
| it('should handle empty section', () => { |
| const xml = '<root><section></section></root>'; |
| expect(extractXmlSection(xml, 'section')).toBe(''); |
| }); |
| }); |
|
|
| describe('extractXmlElements', () => { |
| it('should extract all element values', () => { |
| const xml = '<items><item>one</item><item>two</item><item>three</item></items>'; |
| expect(extractXmlElements(xml, 'item')).toEqual(['one', 'two', 'three']); |
| }); |
|
|
| it('should return empty array for non-existent elements', () => { |
| const xml = '<items><other>value</other></items>'; |
| expect(extractXmlElements(xml, 'item')).toEqual([]); |
| }); |
|
|
| it('should trim whitespace', () => { |
| const xml = '<items><item> spaced </item></items>'; |
| expect(extractXmlElements(xml, 'item')).toEqual(['spaced']); |
| }); |
|
|
| it('should unescape XML entities', () => { |
| const xml = '<items><item>foo & bar</item></items>'; |
| expect(extractXmlElements(xml, 'item')).toEqual(['foo & bar']); |
| }); |
|
|
| it('should handle empty elements', () => { |
| const xml = '<items><item></item><item>value</item></items>'; |
| expect(extractXmlElements(xml, 'item')).toEqual(['', 'value']); |
| }); |
| }); |
|
|
| describe('extractImplementedFeatures', () => { |
| const sampleSpec = `<?xml version="1.0" encoding="UTF-8"?> |
| <project_specification> |
| <project_name>Test Project</project_name> |
| <implemented_features> |
| <feature> |
| <name>Feature One</name> |
| <description>First feature description</description> |
| </feature> |
| <feature> |
| <name>Feature Two</name> |
| <description>Second feature description</description> |
| <file_locations> |
| <location>src/feature-two.ts</location> |
| <location>src/utils/helper.ts</location> |
| </file_locations> |
| </feature> |
| </implemented_features> |
| </project_specification>`; |
|
|
| it('should extract all features', () => { |
| const features = extractImplementedFeatures(sampleSpec); |
| expect(features).toHaveLength(2); |
| }); |
|
|
| it('should extract feature names', () => { |
| const features = extractImplementedFeatures(sampleSpec); |
| expect(features[0].name).toBe('Feature One'); |
| expect(features[1].name).toBe('Feature Two'); |
| }); |
|
|
| it('should extract feature descriptions', () => { |
| const features = extractImplementedFeatures(sampleSpec); |
| expect(features[0].description).toBe('First feature description'); |
| expect(features[1].description).toBe('Second feature description'); |
| }); |
|
|
| it('should extract file_locations when present', () => { |
| const features = extractImplementedFeatures(sampleSpec); |
| expect(features[0].file_locations).toBeUndefined(); |
| expect(features[1].file_locations).toEqual(['src/feature-two.ts', 'src/utils/helper.ts']); |
| }); |
|
|
| it('should return empty array for missing section', () => { |
| const xml = |
| '<project_specification><project_name>Test</project_name></project_specification>'; |
| expect(extractImplementedFeatures(xml)).toEqual([]); |
| }); |
|
|
| it('should return empty array for empty section', () => { |
| const xml = `<project_specification> |
| <implemented_features> |
| </implemented_features> |
| </project_specification>`; |
| expect(extractImplementedFeatures(xml)).toEqual([]); |
| }); |
|
|
| it('should handle escaped content', () => { |
| const xml = `<implemented_features> |
| <feature> |
| <name>Test & Feature</name> |
| <description>Uses <brackets></description> |
| </feature> |
| </implemented_features>`; |
| const features = extractImplementedFeatures(xml); |
| expect(features[0].name).toBe('Test & Feature'); |
| expect(features[0].description).toBe('Uses <brackets>'); |
| }); |
| }); |
|
|
| describe('extractImplementedFeatureNames', () => { |
| it('should return only feature names', () => { |
| const xml = `<implemented_features> |
| <feature> |
| <name>Feature A</name> |
| <description>Description A</description> |
| </feature> |
| <feature> |
| <name>Feature B</name> |
| <description>Description B</description> |
| </feature> |
| </implemented_features>`; |
| expect(extractImplementedFeatureNames(xml)).toEqual(['Feature A', 'Feature B']); |
| }); |
|
|
| it('should return empty array for no features', () => { |
| const xml = '<root></root>'; |
| expect(extractImplementedFeatureNames(xml)).toEqual([]); |
| }); |
| }); |
|
|
| describe('featureToXml', () => { |
| it('should generate XML for feature without file_locations', () => { |
| const feature: ImplementedFeature = { |
| name: 'My Feature', |
| description: 'Feature description', |
| }; |
| const xml = featureToXml(feature); |
| expect(xml).toContain('<name>My Feature</name>'); |
| expect(xml).toContain('<description>Feature description</description>'); |
| expect(xml).not.toContain('<file_locations>'); |
| }); |
|
|
| it('should generate XML for feature with file_locations', () => { |
| const feature: ImplementedFeature = { |
| name: 'My Feature', |
| description: 'Feature description', |
| file_locations: ['src/index.ts', 'src/utils.ts'], |
| }; |
| const xml = featureToXml(feature); |
| expect(xml).toContain('<file_locations>'); |
| expect(xml).toContain('<location>src/index.ts</location>'); |
| expect(xml).toContain('<location>src/utils.ts</location>'); |
| }); |
|
|
| it('should escape special characters', () => { |
| const feature: ImplementedFeature = { |
| name: 'Test & Feature', |
| description: 'Has <tags>', |
| }; |
| const xml = featureToXml(feature); |
| expect(xml).toContain('Test & Feature'); |
| expect(xml).toContain('Has <tags>'); |
| }); |
|
|
| it('should not include empty file_locations array', () => { |
| const feature: ImplementedFeature = { |
| name: 'Feature', |
| description: 'Desc', |
| file_locations: [], |
| }; |
| const xml = featureToXml(feature); |
| expect(xml).not.toContain('<file_locations>'); |
| }); |
| }); |
|
|
| describe('featuresToXml', () => { |
| it('should generate XML for multiple features', () => { |
| const features: ImplementedFeature[] = [ |
| { name: 'Feature 1', description: 'Desc 1' }, |
| { name: 'Feature 2', description: 'Desc 2' }, |
| ]; |
| const xml = featuresToXml(features); |
| expect(xml).toContain('<name>Feature 1</name>'); |
| expect(xml).toContain('<name>Feature 2</name>'); |
| }); |
|
|
| it('should handle empty array', () => { |
| expect(featuresToXml([])).toBe(''); |
| }); |
| }); |
|
|
| describe('updateImplementedFeaturesSection', () => { |
| const baseSpec = `<?xml version="1.0" encoding="UTF-8"?> |
| <project_specification> |
| <project_name>Test</project_name> |
| <core_capabilities> |
| <capability>Testing</capability> |
| </core_capabilities> |
| <implemented_features> |
| <feature> |
| <name>Old Feature</name> |
| <description>Old description</description> |
| </feature> |
| </implemented_features> |
| </project_specification>`; |
|
|
| it('should replace existing section', () => { |
| const newFeatures: ImplementedFeature[] = [ |
| { name: 'New Feature', description: 'New description' }, |
| ]; |
| const result = updateImplementedFeaturesSection(baseSpec, newFeatures); |
| expect(result).toContain('New Feature'); |
| expect(result).not.toContain('Old Feature'); |
| }); |
|
|
| it('should insert section after core_capabilities if missing', () => { |
| const specWithoutSection = `<?xml version="1.0" encoding="UTF-8"?> |
| <project_specification> |
| <project_name>Test</project_name> |
| <core_capabilities> |
| <capability>Testing</capability> |
| </core_capabilities> |
| </project_specification>`; |
| const newFeatures: ImplementedFeature[] = [ |
| { name: 'New Feature', description: 'New description' }, |
| ]; |
| const result = updateImplementedFeaturesSection(specWithoutSection, newFeatures); |
| expect(result).toContain('<implemented_features>'); |
| expect(result).toContain('New Feature'); |
| }); |
|
|
| it('should handle multiple features', () => { |
| const newFeatures: ImplementedFeature[] = [ |
| { name: 'Feature A', description: 'Desc A' }, |
| { name: 'Feature B', description: 'Desc B', file_locations: ['src/b.ts'] }, |
| ]; |
| const result = updateImplementedFeaturesSection(baseSpec, newFeatures); |
| expect(result).toContain('Feature A'); |
| expect(result).toContain('Feature B'); |
| expect(result).toContain('src/b.ts'); |
| }); |
| }); |
|
|
| describe('addImplementedFeature', () => { |
| const baseSpec = `<implemented_features> |
| <feature> |
| <name>Existing Feature</name> |
| <description>Existing description</description> |
| </feature> |
| </implemented_features>`; |
|
|
| it('should add new feature', () => { |
| const newFeature: ImplementedFeature = { |
| name: 'New Feature', |
| description: 'New description', |
| }; |
| const result = addImplementedFeature(baseSpec, newFeature); |
| expect(result).toContain('Existing Feature'); |
| expect(result).toContain('New Feature'); |
| }); |
|
|
| it('should not add duplicate feature', () => { |
| const duplicate: ImplementedFeature = { |
| name: 'Existing Feature', |
| description: 'Different description', |
| }; |
| const result = addImplementedFeature(baseSpec, duplicate); |
| |
| const matches = result.match(/Existing Feature/g); |
| expect(matches).toHaveLength(1); |
| }); |
|
|
| it('should be case-insensitive for duplicates', () => { |
| const duplicate: ImplementedFeature = { |
| name: 'EXISTING FEATURE', |
| description: 'Different description', |
| }; |
| const result = addImplementedFeature(baseSpec, duplicate); |
| expect(result).not.toContain('EXISTING FEATURE'); |
| }); |
| }); |
|
|
| describe('removeImplementedFeature', () => { |
| const baseSpec = `<implemented_features> |
| <feature> |
| <name>Feature A</name> |
| <description>Description A</description> |
| </feature> |
| <feature> |
| <name>Feature B</name> |
| <description>Description B</description> |
| </feature> |
| </implemented_features>`; |
|
|
| it('should remove feature by name', () => { |
| const result = removeImplementedFeature(baseSpec, 'Feature A'); |
| expect(result).not.toContain('Feature A'); |
| expect(result).toContain('Feature B'); |
| }); |
|
|
| it('should be case-insensitive', () => { |
| const result = removeImplementedFeature(baseSpec, 'feature a'); |
| expect(result).not.toContain('Feature A'); |
| expect(result).toContain('Feature B'); |
| }); |
|
|
| it('should return unchanged content if feature not found', () => { |
| const result = removeImplementedFeature(baseSpec, 'Nonexistent'); |
| expect(result).toContain('Feature A'); |
| expect(result).toContain('Feature B'); |
| }); |
| }); |
|
|
| describe('updateImplementedFeature', () => { |
| const baseSpec = `<implemented_features> |
| <feature> |
| <name>My Feature</name> |
| <description>Original description</description> |
| </feature> |
| </implemented_features>`; |
|
|
| it('should update feature description', () => { |
| const result = updateImplementedFeature(baseSpec, 'My Feature', { |
| description: 'Updated description', |
| }); |
| expect(result).toContain('Updated description'); |
| expect(result).not.toContain('Original description'); |
| }); |
|
|
| it('should add file_locations', () => { |
| const result = updateImplementedFeature(baseSpec, 'My Feature', { |
| file_locations: ['src/new.ts'], |
| }); |
| expect(result).toContain('<file_locations>'); |
| expect(result).toContain('src/new.ts'); |
| }); |
|
|
| it('should preserve feature name if not updated', () => { |
| const result = updateImplementedFeature(baseSpec, 'My Feature', { |
| description: 'New desc', |
| }); |
| expect(result).toContain('My Feature'); |
| }); |
|
|
| it('should be case-insensitive', () => { |
| const result = updateImplementedFeature(baseSpec, 'my feature', { |
| description: 'Updated', |
| }); |
| expect(result).toContain('Updated'); |
| }); |
|
|
| it('should return unchanged content if feature not found', () => { |
| const result = updateImplementedFeature(baseSpec, 'Nonexistent', { |
| description: 'New', |
| }); |
| expect(result).toContain('Original description'); |
| }); |
| }); |
|
|
| describe('hasImplementedFeature', () => { |
| const baseSpec = `<implemented_features> |
| <feature> |
| <name>Existing Feature</name> |
| <description>Description</description> |
| </feature> |
| </implemented_features>`; |
|
|
| it('should return true for existing feature', () => { |
| expect(hasImplementedFeature(baseSpec, 'Existing Feature')).toBe(true); |
| }); |
|
|
| it('should return false for non-existing feature', () => { |
| expect(hasImplementedFeature(baseSpec, 'Nonexistent')).toBe(false); |
| }); |
|
|
| it('should be case-insensitive', () => { |
| expect(hasImplementedFeature(baseSpec, 'existing feature')).toBe(true); |
| expect(hasImplementedFeature(baseSpec, 'EXISTING FEATURE')).toBe(true); |
| }); |
| }); |
|
|
| describe('toSpecOutputFeatures', () => { |
| it('should convert to SpecOutput format', () => { |
| const features: ImplementedFeature[] = [ |
| { name: 'Feature 1', description: 'Desc 1' }, |
| { name: 'Feature 2', description: 'Desc 2', file_locations: ['src/f2.ts'] }, |
| ]; |
| const result = toSpecOutputFeatures(features); |
| expect(result).toEqual([ |
| { name: 'Feature 1', description: 'Desc 1' }, |
| { name: 'Feature 2', description: 'Desc 2', file_locations: ['src/f2.ts'] }, |
| ]); |
| }); |
|
|
| it('should handle empty array', () => { |
| expect(toSpecOutputFeatures([])).toEqual([]); |
| }); |
| }); |
|
|
| describe('fromSpecOutputFeatures', () => { |
| it('should convert from SpecOutput format', () => { |
| const specFeatures = [ |
| { name: 'Feature 1', description: 'Desc 1' }, |
| { name: 'Feature 2', description: 'Desc 2', file_locations: ['src/f2.ts'] }, |
| ]; |
| const result = fromSpecOutputFeatures(specFeatures); |
| expect(result).toEqual([ |
| { name: 'Feature 1', description: 'Desc 1' }, |
| { name: 'Feature 2', description: 'Desc 2', file_locations: ['src/f2.ts'] }, |
| ]); |
| }); |
|
|
| it('should handle empty array', () => { |
| expect(fromSpecOutputFeatures([])).toEqual([]); |
| }); |
| }); |
|
|
| describe('roundtrip', () => { |
| it('should maintain data integrity through extract -> update cycle', () => { |
| const originalSpec = `<?xml version="1.0" encoding="UTF-8"?> |
| <project_specification> |
| <project_name>Test</project_name> |
| <core_capabilities> |
| <capability>Testing</capability> |
| </core_capabilities> |
| <implemented_features> |
| <feature> |
| <name>Test & Feature</name> |
| <description>Uses <special> chars</description> |
| <file_locations> |
| <location>src/test.ts</location> |
| </file_locations> |
| </feature> |
| </implemented_features> |
| </project_specification>`; |
|
|
| |
| const features = extractImplementedFeatures(originalSpec); |
| expect(features[0].name).toBe('Test & Feature'); |
| expect(features[0].description).toBe('Uses <special> chars'); |
|
|
| |
| const result = updateImplementedFeaturesSection(originalSpec, features); |
|
|
| |
| const reExtracted = extractImplementedFeatures(result); |
| expect(reExtracted[0].name).toBe('Test & Feature'); |
| expect(reExtracted[0].description).toBe('Uses <special> chars'); |
| expect(reExtracted[0].file_locations).toEqual(['src/test.ts']); |
| }); |
| }); |
|
|
| describe('custom logger', () => { |
| it('should use custom logger for extractXmlSection', () => { |
| const mockLogger = createMockLogger(); |
| const xml = '<root><section>content</section></root>'; |
| extractXmlSection(xml, 'section', { logger: mockLogger }); |
| expect(mockLogger.debug).toHaveBeenCalledWith('Extracted <section> section'); |
| }); |
|
|
| it('should log when section is not found', () => { |
| const mockLogger = createMockLogger(); |
| const xml = '<root><other>content</other></root>'; |
| extractXmlSection(xml, 'missing', { logger: mockLogger }); |
| expect(mockLogger.debug).toHaveBeenCalledWith('Section <missing> not found'); |
| }); |
|
|
| it('should use custom logger for extractXmlElements', () => { |
| const mockLogger = createMockLogger(); |
| const xml = '<items><item>one</item><item>two</item></items>'; |
| extractXmlElements(xml, 'item', { logger: mockLogger }); |
| expect(mockLogger.debug).toHaveBeenCalledWith('Extracted 2 <item> elements'); |
| }); |
|
|
| it('should use custom logger for extractImplementedFeatures', () => { |
| const mockLogger = createMockLogger(); |
| const xml = `<implemented_features> |
| <feature> |
| <name>Test</name> |
| <description>Desc</description> |
| </feature> |
| </implemented_features>`; |
| extractImplementedFeatures(xml, { logger: mockLogger }); |
| expect(mockLogger.debug).toHaveBeenCalledWith('Extracted 1 implemented features'); |
| }); |
|
|
| it('should log when no implemented_features section found', () => { |
| const mockLogger = createMockLogger(); |
| const xml = '<root><other>content</other></root>'; |
| extractImplementedFeatures(xml, { logger: mockLogger }); |
| expect(mockLogger.debug).toHaveBeenCalledWith('No implemented_features section found'); |
| }); |
|
|
| it('should use custom logger warn for missing insertion point', () => { |
| const mockLogger = createMockLogger(); |
| |
| const xml = '<other>content</other>'; |
| const features: ImplementedFeature[] = [{ name: 'Test', description: 'Desc' }]; |
| updateImplementedFeaturesSection(xml, features, { logger: mockLogger }); |
| expect(mockLogger.warn).toHaveBeenCalledWith( |
| 'Could not find appropriate insertion point for implemented_features' |
| ); |
| }); |
| }); |
|
|
| describe('edge cases', () => { |
| describe('escapeXml edge cases', () => { |
| it('should handle strings with only special characters', () => { |
| expect(escapeXml('<>&"\'')).toBe('<>&"''); |
| }); |
|
|
| it('should handle very long strings', () => { |
| const longString = 'a'.repeat(10000) + '&' + 'b'.repeat(10000); |
| const escaped = escapeXml(longString); |
| expect(escaped).toContain('&'); |
| expect(escaped.length).toBe(20005); |
| }); |
|
|
| it('should handle unicode characters without escaping', () => { |
| const unicode = '日本語 emoji: 🚀 symbols: ∞ ≠ ≤'; |
| expect(escapeXml(unicode)).toBe(unicode); |
| }); |
| }); |
|
|
| describe('unescapeXml edge cases', () => { |
| it('should handle strings with only entities', () => { |
| expect(unescapeXml('<>&"'')).toBe('<>&"\''); |
| }); |
|
|
| it('should not double-unescape', () => { |
| |
| expect(unescapeXml('&lt;')).toBe('<'); |
| }); |
|
|
| it('should handle partial/invalid entities gracefully', () => { |
| |
| expect(unescapeXml('&unknown;')).toBe('&unknown;'); |
| expect(unescapeXml('&')).toBe('&'); |
| }); |
| }); |
|
|
| describe('extractXmlSection edge cases', () => { |
| it('should handle nested tags with same name', () => { |
| |
| |
| const xml = '<outer><outer>inner</outer></outer>'; |
| |
| expect(extractXmlSection(xml, 'outer')).toBe('<outer>inner'); |
| }); |
|
|
| it('should handle self-closing tags (returns null)', () => { |
| const xml = '<root><section /></root>'; |
| |
| expect(extractXmlSection(xml, 'section')).toBeNull(); |
| }); |
|
|
| it('should handle tags with attributes', () => { |
| const xml = '<root><section id="1" class="test">content</section></root>'; |
| |
| expect(extractXmlSection(xml, 'section')).toBeNull(); |
| }); |
|
|
| it('should handle whitespace in tag content', () => { |
| const xml = '<section> \n\t </section>'; |
| expect(extractXmlSection(xml, 'section')).toBe(' \n\t '); |
| }); |
| }); |
|
|
| describe('extractXmlElements edge cases', () => { |
| it('should handle elements across multiple lines', () => { |
| const xml = `<items> |
| <item> |
| first |
| </item> |
| <item>second</item> |
| </items>`; |
| |
| const result = extractXmlElements(xml, 'item'); |
| expect(result).toHaveLength(2); |
| expect(result[0]).toBe('first'); |
| expect(result[1]).toBe('second'); |
| }); |
|
|
| it('should handle consecutive elements without whitespace', () => { |
| const xml = '<items><item>a</item><item>b</item><item>c</item></items>'; |
| expect(extractXmlElements(xml, 'item')).toEqual(['a', 'b', 'c']); |
| }); |
| }); |
|
|
| describe('extractImplementedFeatures edge cases', () => { |
| it('should skip features without names', () => { |
| const xml = `<implemented_features> |
| <feature> |
| <description>Orphan description</description> |
| </feature> |
| <feature> |
| <name>Valid Feature</name> |
| <description>Has name</description> |
| </feature> |
| </implemented_features>`; |
| const features = extractImplementedFeatures(xml); |
| expect(features).toHaveLength(1); |
| expect(features[0].name).toBe('Valid Feature'); |
| }); |
|
|
| it('should handle features with empty names', () => { |
| const xml = `<implemented_features> |
| <feature> |
| <name></name> |
| <description>Empty name</description> |
| </feature> |
| </implemented_features>`; |
| const features = extractImplementedFeatures(xml); |
| expect(features).toHaveLength(0); |
| }); |
|
|
| it('should handle features with whitespace-only names', () => { |
| const xml = `<implemented_features> |
| <feature> |
| <name> </name> |
| <description>Whitespace name</description> |
| </feature> |
| </implemented_features>`; |
| const features = extractImplementedFeatures(xml); |
| expect(features).toHaveLength(0); |
| }); |
|
|
| it('should handle empty file_locations section', () => { |
| const xml = `<implemented_features> |
| <feature> |
| <name>Test</name> |
| <description>Desc</description> |
| <file_locations> |
| </file_locations> |
| </feature> |
| </implemented_features>`; |
| const features = extractImplementedFeatures(xml); |
| expect(features[0].file_locations).toBeUndefined(); |
| }); |
| }); |
|
|
| describe('featureToXml edge cases', () => { |
| it('should handle custom indentation', () => { |
| const feature: ImplementedFeature = { |
| name: 'Test', |
| description: 'Desc', |
| }; |
| const xml = featureToXml(feature, '\t'); |
| expect(xml).toContain('\t\t<feature>'); |
| expect(xml).toContain('\t\t\t<name>Test</name>'); |
| }); |
|
|
| it('should handle empty description', () => { |
| const feature: ImplementedFeature = { |
| name: 'Test', |
| description: '', |
| }; |
| const xml = featureToXml(feature); |
| expect(xml).toContain('<description></description>'); |
| }); |
|
|
| it('should handle undefined file_locations', () => { |
| const feature: ImplementedFeature = { |
| name: 'Test', |
| description: 'Desc', |
| file_locations: undefined, |
| }; |
| const xml = featureToXml(feature); |
| expect(xml).not.toContain('file_locations'); |
| }); |
| }); |
|
|
| describe('updateImplementedFeaturesSection edge cases', () => { |
| it('should insert before </project_specification> as fallback', () => { |
| const specWithoutCoreCapabilities = `<?xml version="1.0" encoding="UTF-8"?> |
| <project_specification> |
| <project_name>Test</project_name> |
| </project_specification>`; |
| const newFeatures: ImplementedFeature[] = [ |
| { name: 'New Feature', description: 'New description' }, |
| ]; |
| const result = updateImplementedFeaturesSection(specWithoutCoreCapabilities, newFeatures); |
| expect(result).toContain('<implemented_features>'); |
| expect(result).toContain('New Feature'); |
| expect(result.indexOf('<implemented_features>')).toBeLessThan( |
| result.indexOf('</project_specification>') |
| ); |
| }); |
|
|
| it('should return unchanged content when no insertion point found', () => { |
| const invalidSpec = '<other>content</other>'; |
| const newFeatures: ImplementedFeature[] = [{ name: 'Feature', description: 'Desc' }]; |
| const result = updateImplementedFeaturesSection(invalidSpec, newFeatures); |
| expect(result).toBe(invalidSpec); |
| }); |
|
|
| it('should handle empty features array', () => { |
| const spec = `<implemented_features> |
| <feature> |
| <name>Old</name> |
| <description>Old desc</description> |
| </feature> |
| </implemented_features>`; |
| const result = updateImplementedFeaturesSection(spec, []); |
| expect(result).toContain('<implemented_features>'); |
| expect(result).not.toContain('Old'); |
| }); |
| }); |
|
|
| describe('addImplementedFeature edge cases', () => { |
| it('should create section when adding to spec without implemented_features', () => { |
| const specWithoutSection = `<project_specification> |
| <core_capabilities> |
| <capability>Testing</capability> |
| </core_capabilities> |
| </project_specification>`; |
| const newFeature: ImplementedFeature = { |
| name: 'First Feature', |
| description: 'First description', |
| }; |
| const result = addImplementedFeature(specWithoutSection, newFeature); |
| expect(result).toContain('<implemented_features>'); |
| expect(result).toContain('First Feature'); |
| }); |
|
|
| it('should handle feature with all fields populated', () => { |
| const spec = `<implemented_features></implemented_features>`; |
| const newFeature: ImplementedFeature = { |
| name: 'Complete Feature', |
| description: 'Full description', |
| file_locations: ['src/a.ts', 'src/b.ts', 'src/c.ts'], |
| }; |
| const result = addImplementedFeature(spec, newFeature); |
| expect(result).toContain('Complete Feature'); |
| expect(result).toContain('src/a.ts'); |
| expect(result).toContain('src/b.ts'); |
| expect(result).toContain('src/c.ts'); |
| }); |
| }); |
|
|
| describe('updateImplementedFeature edge cases', () => { |
| it('should allow updating feature name', () => { |
| const spec = `<implemented_features> |
| <feature> |
| <name>Old Name</name> |
| <description>Desc</description> |
| </feature> |
| </implemented_features>`; |
| const result = updateImplementedFeature(spec, 'Old Name', { |
| name: 'New Name', |
| }); |
| expect(result).toContain('New Name'); |
| expect(result).not.toContain('Old Name'); |
| }); |
|
|
| it('should allow clearing file_locations', () => { |
| const spec = `<implemented_features> |
| <feature> |
| <name>Test</name> |
| <description>Desc</description> |
| <file_locations> |
| <location>src/old.ts</location> |
| </file_locations> |
| </feature> |
| </implemented_features>`; |
| const result = updateImplementedFeature(spec, 'Test', { |
| file_locations: [], |
| }); |
| expect(result).not.toContain('file_locations'); |
| expect(result).not.toContain('src/old.ts'); |
| }); |
|
|
| it('should handle updating multiple fields at once', () => { |
| const spec = `<implemented_features> |
| <feature> |
| <name>Original</name> |
| <description>Original desc</description> |
| </feature> |
| </implemented_features>`; |
| const result = updateImplementedFeature(spec, 'Original', { |
| name: 'Updated', |
| description: 'Updated desc', |
| file_locations: ['new/path.ts'], |
| }); |
| expect(result).toContain('Updated'); |
| expect(result).toContain('Updated desc'); |
| expect(result).toContain('new/path.ts'); |
| }); |
| }); |
|
|
| describe('toSpecOutputFeatures and fromSpecOutputFeatures edge cases', () => { |
| it('should handle features with empty file_locations array', () => { |
| const features: ImplementedFeature[] = [ |
| { name: 'Test', description: 'Desc', file_locations: [] }, |
| ]; |
| const specOutput = toSpecOutputFeatures(features); |
| expect(specOutput[0].file_locations).toBeUndefined(); |
| }); |
|
|
| it('should handle round-trip conversion', () => { |
| const original: ImplementedFeature[] = [ |
| { name: 'Feature 1', description: 'Desc 1' }, |
| { name: 'Feature 2', description: 'Desc 2', file_locations: ['src/f.ts'] }, |
| ]; |
| const specOutput = toSpecOutputFeatures(original); |
| const restored = fromSpecOutputFeatures(specOutput); |
| expect(restored).toEqual(original); |
| }); |
| }); |
| }); |
|
|
| describe('integration scenarios', () => { |
| it('should handle a complete spec file workflow', () => { |
| |
| let spec = `<?xml version="1.0" encoding="UTF-8"?> |
| <project_specification> |
| <project_name>My App</project_name> |
| <core_capabilities> |
| <capability>User management</capability> |
| </core_capabilities> |
| </project_specification>`; |
|
|
| |
| spec = addImplementedFeature(spec, { |
| name: 'User Authentication', |
| description: 'Login and logout functionality', |
| file_locations: ['src/auth/login.ts', 'src/auth/logout.ts'], |
| }); |
| expect(hasImplementedFeature(spec, 'User Authentication')).toBe(true); |
|
|
| |
| spec = addImplementedFeature(spec, { |
| name: 'User Profile', |
| description: 'View and edit user profile', |
| }); |
| expect(extractImplementedFeatureNames(spec)).toEqual(['User Authentication', 'User Profile']); |
|
|
| |
| spec = updateImplementedFeature(spec, 'User Authentication', { |
| file_locations: ['src/auth/login.ts', 'src/auth/logout.ts', 'src/auth/session.ts'], |
| }); |
| const features = extractImplementedFeatures(spec); |
| expect(features[0].file_locations).toContain('src/auth/session.ts'); |
|
|
| |
| spec = removeImplementedFeature(spec, 'User Profile'); |
| expect(hasImplementedFeature(spec, 'User Profile')).toBe(false); |
| expect(hasImplementedFeature(spec, 'User Authentication')).toBe(true); |
| }); |
|
|
| it('should handle special characters throughout workflow', () => { |
| const spec = `<project_specification> |
| <core_capabilities></core_capabilities> |
| </project_specification>`; |
|
|
| const result = addImplementedFeature(spec, { |
| name: 'Search & Filter', |
| description: 'Supports <query> syntax with "quoted" terms', |
| file_locations: ["src/search/parser's.ts"], |
| }); |
|
|
| const features = extractImplementedFeatures(result); |
| expect(features[0].name).toBe('Search & Filter'); |
| expect(features[0].description).toBe('Supports <query> syntax with "quoted" terms'); |
| expect(features[0].file_locations?.[0]).toBe("src/search/parser's.ts"); |
| }); |
|
|
| it('should preserve other XML content when modifying features', () => { |
| const spec = `<?xml version="1.0" encoding="UTF-8"?> |
| <project_specification> |
| <project_name>Preserved Name</project_name> |
| <description>This should be preserved</description> |
| <core_capabilities> |
| <capability>Capability 1</capability> |
| <capability>Capability 2</capability> |
| </core_capabilities> |
| <implemented_features> |
| <feature> |
| <name>Old Feature</name> |
| <description>Will be replaced</description> |
| </feature> |
| </implemented_features> |
| <future_plans>Keep this too</future_plans> |
| </project_specification>`; |
|
|
| const result = updateImplementedFeaturesSection(spec, [ |
| { name: 'New Feature', description: 'New desc' }, |
| ]); |
|
|
| expect(result).toContain('Preserved Name'); |
| expect(result).toContain('This should be preserved'); |
| expect(result).toContain('Capability 1'); |
| expect(result).toContain('Capability 2'); |
| expect(result).toContain('Keep this too'); |
| expect(result).not.toContain('Old Feature'); |
| expect(result).toContain('New Feature'); |
| }); |
| }); |
| }); |
|
|