| import { describe, it, expect } from 'vitest'; |
| import { |
| parseTasksFromSpec, |
| detectTaskStartMarker, |
| detectTaskCompleteMarker, |
| detectPhaseCompleteMarker, |
| detectSpecFallback, |
| extractSummary, |
| } from '../../../src/services/spec-parser.js'; |
|
|
| describe('SpecParser', () => { |
| describe('parseTasksFromSpec', () => { |
| it('should parse tasks from a tasks code block', () => { |
| const specContent = ` |
| ## Specification |
| |
| Some description here. |
| |
| \`\`\`tasks |
| - [ ] T001: Create user model | File: src/models/user.ts |
| - [ ] T002: Add API endpoint | File: src/routes/users.ts |
| - [ ] T003: Write unit tests | File: tests/user.test.ts |
| \`\`\` |
| |
| ## Notes |
| Some notes here. |
| `; |
| const tasks = parseTasksFromSpec(specContent); |
| expect(tasks).toHaveLength(3); |
| expect(tasks[0]).toEqual({ |
| id: 'T001', |
| description: 'Create user model', |
| filePath: 'src/models/user.ts', |
| phase: undefined, |
| status: 'pending', |
| }); |
| expect(tasks[1].id).toBe('T002'); |
| expect(tasks[2].id).toBe('T003'); |
| }); |
|
|
| it('should parse tasks with phases', () => { |
| const specContent = ` |
| \`\`\`tasks |
| ## Phase 1: Foundation |
| - [ ] T001: Initialize project | File: package.json |
| - [ ] T002: Configure TypeScript | File: tsconfig.json |
| |
| ## Phase 2: Implementation |
| - [ ] T003: Create main module | File: src/index.ts |
| - [ ] T004: Add utility functions | File: src/utils.ts |
| |
| ## Phase 3: Testing |
| - [ ] T005: Write tests | File: tests/index.test.ts |
| \`\`\` |
| `; |
| const tasks = parseTasksFromSpec(specContent); |
| expect(tasks).toHaveLength(5); |
| expect(tasks[0].phase).toBe('Phase 1: Foundation'); |
| expect(tasks[1].phase).toBe('Phase 1: Foundation'); |
| expect(tasks[2].phase).toBe('Phase 2: Implementation'); |
| expect(tasks[3].phase).toBe('Phase 2: Implementation'); |
| expect(tasks[4].phase).toBe('Phase 3: Testing'); |
| }); |
|
|
| it('should return empty array for content without tasks', () => { |
| const specContent = 'Just some text without any tasks'; |
| const tasks = parseTasksFromSpec(specContent); |
| expect(tasks).toEqual([]); |
| }); |
|
|
| it('should fallback to finding task lines outside code block', () => { |
| const specContent = ` |
| ## Implementation Plan |
| |
| - [ ] T001: First task | File: src/first.ts |
| - [ ] T002: Second task | File: src/second.ts |
| `; |
| const tasks = parseTasksFromSpec(specContent); |
| expect(tasks).toHaveLength(2); |
| expect(tasks[0].id).toBe('T001'); |
| expect(tasks[1].id).toBe('T002'); |
| }); |
|
|
| it('should handle empty tasks block', () => { |
| const specContent = ` |
| \`\`\`tasks |
| \`\`\` |
| `; |
| const tasks = parseTasksFromSpec(specContent); |
| expect(tasks).toEqual([]); |
| }); |
|
|
| it('should handle empty string input', () => { |
| const tasks = parseTasksFromSpec(''); |
| expect(tasks).toEqual([]); |
| }); |
|
|
| it('should handle task without file path', () => { |
| const specContent = ` |
| \`\`\`tasks |
| - [ ] T001: Task without file |
| \`\`\` |
| `; |
| const tasks = parseTasksFromSpec(specContent); |
| expect(tasks).toHaveLength(1); |
| expect(tasks[0]).toEqual({ |
| id: 'T001', |
| description: 'Task without file', |
| phase: undefined, |
| status: 'pending', |
| }); |
| }); |
|
|
| it('should handle mixed valid and invalid lines', () => { |
| const specContent = ` |
| \`\`\`tasks |
| - [ ] T001: Valid task | File: src/valid.ts |
| - Invalid line |
| Some other text |
| - [ ] T002: Another valid task |
| \`\`\` |
| `; |
| const tasks = parseTasksFromSpec(specContent); |
| expect(tasks).toHaveLength(2); |
| }); |
|
|
| it('should preserve task order', () => { |
| const specContent = ` |
| \`\`\`tasks |
| - [ ] T003: Third |
| - [ ] T001: First |
| - [ ] T002: Second |
| \`\`\` |
| `; |
| const tasks = parseTasksFromSpec(specContent); |
| expect(tasks[0].id).toBe('T003'); |
| expect(tasks[1].id).toBe('T001'); |
| expect(tasks[2].id).toBe('T002'); |
| }); |
|
|
| it('should handle task IDs with different numbers', () => { |
| const specContent = ` |
| \`\`\`tasks |
| - [ ] T001: First |
| - [ ] T010: Tenth |
| - [ ] T100: Hundredth |
| \`\`\` |
| `; |
| const tasks = parseTasksFromSpec(specContent); |
| expect(tasks).toHaveLength(3); |
| expect(tasks[0].id).toBe('T001'); |
| expect(tasks[1].id).toBe('T010'); |
| expect(tasks[2].id).toBe('T100'); |
| }); |
|
|
| it('should trim whitespace from description and file path', () => { |
| const specContent = ` |
| \`\`\`tasks |
| - [ ] T001: Create API endpoint | File: src/routes/api.ts |
| \`\`\` |
| `; |
| const tasks = parseTasksFromSpec(specContent); |
| expect(tasks[0].description).toBe('Create API endpoint'); |
| expect(tasks[0].filePath).toBe('src/routes/api.ts'); |
| }); |
| }); |
|
|
| describe('detectTaskStartMarker', () => { |
| it('should detect task start marker and return task ID', () => { |
| expect(detectTaskStartMarker('[TASK_START] T001')).toBe('T001'); |
| expect(detectTaskStartMarker('[TASK_START] T042')).toBe('T042'); |
| expect(detectTaskStartMarker('[TASK_START] T999')).toBe('T999'); |
| }); |
|
|
| it('should handle marker with description', () => { |
| expect(detectTaskStartMarker('[TASK_START] T001: Creating user model')).toBe('T001'); |
| }); |
|
|
| it('should return null when no marker present', () => { |
| expect(detectTaskStartMarker('No marker here')).toBeNull(); |
| expect(detectTaskStartMarker('')).toBeNull(); |
| }); |
|
|
| it('should find marker in accumulated text', () => { |
| const accumulated = ` |
| Some earlier output... |
| |
| Now starting the task: |
| [TASK_START] T003: Setting up database |
| |
| Let me begin by... |
| `; |
| expect(detectTaskStartMarker(accumulated)).toBe('T003'); |
| }); |
|
|
| it('should handle whitespace variations', () => { |
| expect(detectTaskStartMarker('[TASK_START] T001')).toBe('T001'); |
| expect(detectTaskStartMarker('[TASK_START]\tT001')).toBe('T001'); |
| }); |
|
|
| it('should not match invalid task IDs', () => { |
| expect(detectTaskStartMarker('[TASK_START] TASK1')).toBeNull(); |
| expect(detectTaskStartMarker('[TASK_START] T1')).toBeNull(); |
| expect(detectTaskStartMarker('[TASK_START] T12')).toBeNull(); |
| }); |
| }); |
|
|
| describe('detectTaskCompleteMarker', () => { |
| it('should detect task complete marker and return task ID', () => { |
| expect(detectTaskCompleteMarker('[TASK_COMPLETE] T001')).toEqual({ |
| id: 'T001', |
| summary: undefined, |
| }); |
| expect(detectTaskCompleteMarker('[TASK_COMPLETE] T042')).toEqual({ |
| id: 'T042', |
| summary: undefined, |
| }); |
| }); |
|
|
| it('should handle marker with summary', () => { |
| expect(detectTaskCompleteMarker('[TASK_COMPLETE] T001: User model created')).toEqual({ |
| id: 'T001', |
| summary: 'User model created', |
| }); |
| }); |
|
|
| it('should return null when no marker present', () => { |
| expect(detectTaskCompleteMarker('No marker here')).toBeNull(); |
| expect(detectTaskCompleteMarker('')).toBeNull(); |
| }); |
|
|
| it('should find marker in accumulated text', () => { |
| const accumulated = ` |
| Working on the task... |
| |
| Done with the implementation: |
| [TASK_COMPLETE] T003: Database setup complete |
| |
| Moving on to... |
| `; |
| expect(detectTaskCompleteMarker(accumulated)).toEqual({ |
| id: 'T003', |
| summary: 'Database setup complete', |
| }); |
| }); |
|
|
| it('should find marker in the middle of a stream with trailing text', () => { |
| const streamText = |
| 'The implementation is complete! [TASK_COMPLETE] T001: Added user model and tests. Now let me check the next task...'; |
| expect(detectTaskCompleteMarker(streamText)).toEqual({ |
| id: 'T001', |
| summary: 'Added user model and tests. Now let me check the next task...', |
| }); |
| }); |
|
|
| it('should find marker in the middle of a stream with multiple tasks and return the FIRST match', () => { |
| const streamText = |
| '[TASK_COMPLETE] T001: Task one done. Continuing... [TASK_COMPLETE] T002: Task two done. Moving on...'; |
| expect(detectTaskCompleteMarker(streamText)).toEqual({ |
| id: 'T001', |
| summary: 'Task one done. Continuing...', |
| }); |
| }); |
|
|
| it('should not confuse with TASK_START marker', () => { |
| expect(detectTaskCompleteMarker('[TASK_START] T001')).toBeNull(); |
| }); |
|
|
| it('should not match invalid task IDs', () => { |
| expect(detectTaskCompleteMarker('[TASK_COMPLETE] TASK1')).toBeNull(); |
| expect(detectTaskCompleteMarker('[TASK_COMPLETE] T1')).toBeNull(); |
| }); |
|
|
| it('should allow brackets in summary text', () => { |
| |
| expect( |
| detectTaskCompleteMarker('[TASK_COMPLETE] T001: Supports array[index] access syntax') |
| ).toEqual({ |
| id: 'T001', |
| summary: 'Supports array[index] access syntax', |
| }); |
| }); |
|
|
| it('should handle summary with multiple brackets', () => { |
| expect( |
| detectTaskCompleteMarker('[TASK_COMPLETE] T042: Fixed bug in data[0].items[key] mapping') |
| ).toEqual({ |
| id: 'T042', |
| summary: 'Fixed bug in data[0].items[key] mapping', |
| }); |
| }); |
|
|
| it('should stop at newline in summary', () => { |
| const result = detectTaskCompleteMarker( |
| '[TASK_COMPLETE] T001: First line\nSecond line without marker' |
| ); |
| expect(result).toEqual({ |
| id: 'T001', |
| summary: 'First line', |
| }); |
| }); |
|
|
| it('should stop at next TASK_START marker', () => { |
| expect( |
| detectTaskCompleteMarker('[TASK_COMPLETE] T001: Summary text[TASK_START] T002') |
| ).toEqual({ |
| id: 'T001', |
| summary: 'Summary text', |
| }); |
| }); |
| }); |
|
|
| describe('detectPhaseCompleteMarker', () => { |
| it('should detect phase complete marker and return phase number', () => { |
| expect(detectPhaseCompleteMarker('[PHASE_COMPLETE] Phase 1')).toBe(1); |
| expect(detectPhaseCompleteMarker('[PHASE_COMPLETE] Phase 2')).toBe(2); |
| expect(detectPhaseCompleteMarker('[PHASE_COMPLETE] Phase 10')).toBe(10); |
| }); |
|
|
| it('should handle marker with description', () => { |
| expect(detectPhaseCompleteMarker('[PHASE_COMPLETE] Phase 1 complete')).toBe(1); |
| expect(detectPhaseCompleteMarker('[PHASE_COMPLETE] Phase 2: Foundation done')).toBe(2); |
| }); |
|
|
| it('should return null when no marker present', () => { |
| expect(detectPhaseCompleteMarker('No marker here')).toBeNull(); |
| expect(detectPhaseCompleteMarker('')).toBeNull(); |
| }); |
|
|
| it('should be case-insensitive', () => { |
| expect(detectPhaseCompleteMarker('[PHASE_COMPLETE] phase 1')).toBe(1); |
| expect(detectPhaseCompleteMarker('[PHASE_COMPLETE] PHASE 2')).toBe(2); |
| }); |
|
|
| it('should find marker in accumulated text', () => { |
| const accumulated = ` |
| Finishing up the phase... |
| |
| All tasks complete: |
| [PHASE_COMPLETE] Phase 2 complete |
| |
| Starting Phase 3... |
| `; |
| expect(detectPhaseCompleteMarker(accumulated)).toBe(2); |
| }); |
|
|
| it('should not confuse with task markers', () => { |
| expect(detectPhaseCompleteMarker('[TASK_COMPLETE] T001')).toBeNull(); |
| }); |
| }); |
|
|
| describe('detectSpecFallback', () => { |
| it('should detect spec with tasks block and acceptance criteria', () => { |
| const content = ` |
| ## Acceptance Criteria |
| - GIVEN a user, WHEN they login, THEN they see the dashboard |
| |
| \`\`\`tasks |
| - [ ] T001: Create login form | File: src/Login.tsx |
| \`\`\` |
| `; |
| expect(detectSpecFallback(content)).toBe(true); |
| }); |
|
|
| it('should detect spec with task lines and problem statement', () => { |
| const content = ` |
| ## Problem Statement |
| Users cannot currently log in to the application. |
| |
| ## Implementation Plan |
| - [ ] T001: Add authentication endpoint |
| - [ ] T002: Create login UI |
| `; |
| expect(detectSpecFallback(content)).toBe(true); |
| }); |
|
|
| it('should detect spec with Goal section (lite planning mode)', () => { |
| const content = ` |
| **Goal**: Implement user authentication |
| |
| **Solution**: Use JWT tokens for session management |
| |
| - [ ] T001: Setup auth middleware |
| - [ ] T002: Create token service |
| `; |
| expect(detectSpecFallback(content)).toBe(true); |
| }); |
|
|
| it('should detect spec with User Story format', () => { |
| const content = ` |
| ## User Story |
| As a user, I want to reset my password, so that I can regain access. |
| |
| ## Technical Context |
| This will modify the auth module. |
| |
| \`\`\`tasks |
| - [ ] T001: Add reset endpoint |
| \`\`\` |
| `; |
| expect(detectSpecFallback(content)).toBe(true); |
| }); |
|
|
| it('should detect spec with Overview section', () => { |
| const content = ` |
| ## Overview |
| This feature adds dark mode support. |
| |
| \`\`\`tasks |
| - [ ] T001: Add theme toggle |
| \`\`\` |
| `; |
| expect(detectSpecFallback(content)).toBe(true); |
| }); |
|
|
| it('should detect spec with Summary section', () => { |
| const content = ` |
| ## Summary |
| Adding a new dashboard component. |
| |
| - [ ] T001: Create dashboard layout |
| `; |
| expect(detectSpecFallback(content)).toBe(true); |
| }); |
|
|
| it('should detect spec with implementation plan', () => { |
| const content = ` |
| ## Implementation Plan |
| We will add the feature in two phases. |
| |
| - [ ] T001: Phase 1 setup |
| `; |
| expect(detectSpecFallback(content)).toBe(true); |
| }); |
|
|
| it('should detect spec with implementation steps', () => { |
| const content = ` |
| ## Implementation Steps |
| Follow these steps: |
| |
| - [ ] T001: Step one |
| `; |
| expect(detectSpecFallback(content)).toBe(true); |
| }); |
|
|
| it('should detect spec with implementation approach', () => { |
| const content = ` |
| ## Implementation Approach |
| We will use a modular approach. |
| |
| - [ ] T001: Create modules |
| `; |
| expect(detectSpecFallback(content)).toBe(true); |
| }); |
|
|
| it('should NOT detect spec without task structure', () => { |
| const content = ` |
| ## Problem Statement |
| Users cannot log in. |
| |
| ## Acceptance Criteria |
| - GIVEN a user, WHEN they try to login, THEN it works |
| `; |
| expect(detectSpecFallback(content)).toBe(false); |
| }); |
|
|
| it('should NOT detect spec without spec content sections', () => { |
| const content = ` |
| Here are some tasks: |
| |
| - [ ] T001: Do something |
| - [ ] T002: Do another thing |
| `; |
| expect(detectSpecFallback(content)).toBe(false); |
| }); |
|
|
| it('should NOT detect random text as spec', () => { |
| expect(detectSpecFallback('Just some random text')).toBe(false); |
| expect(detectSpecFallback('')).toBe(false); |
| }); |
|
|
| it('should handle case-insensitive matching for spec sections', () => { |
| const content = ` |
| ## ACCEPTANCE CRITERIA |
| All caps section header |
| |
| - [ ] T001: Task |
| `; |
| expect(detectSpecFallback(content)).toBe(true); |
| }); |
| }); |
|
|
| describe('extractSummary', () => { |
| describe('explicit <summary> tags', () => { |
| it('should extract content from summary tags', () => { |
| const text = 'Some preamble <summary>This is the summary content</summary> more text'; |
| expect(extractSummary(text)).toBe('This is the summary content'); |
| }); |
|
|
| it('should use last match to avoid stale summaries', () => { |
| const text = ` |
| <summary>Old stale summary</summary> |
| |
| More agent output... |
| |
| <summary>Fresh new summary</summary> |
| `; |
| expect(extractSummary(text)).toBe('Fresh new summary'); |
| }); |
|
|
| it('should handle multiline summary content', () => { |
| const text = `<summary>First line |
| Second line |
| Third line</summary>`; |
| expect(extractSummary(text)).toBe('First line\nSecond line\nThird line'); |
| }); |
|
|
| it('should trim whitespace from summary', () => { |
| const text = '<summary> trimmed content </summary>'; |
| expect(extractSummary(text)).toBe('trimmed content'); |
| }); |
| }); |
|
|
| describe('## Summary section (markdown)', () => { |
| it('should extract from ## Summary section', () => { |
| const text = ` |
| ## Summary |
| |
| This is a summary paragraph. |
| |
| ## Other Section |
| More content. |
| `; |
| expect(extractSummary(text)).toBe('This is a summary paragraph.'); |
| }); |
|
|
| it('should truncate long summaries to 500 chars', () => { |
| const longContent = 'A'.repeat(600); |
| const text = ` |
| ## Summary |
| |
| ${longContent} |
| |
| ## Next Section |
| `; |
| const result = extractSummary(text); |
| expect(result).not.toBeNull(); |
| expect(result!.length).toBeLessThanOrEqual(503); |
| expect(result!.endsWith('...')).toBe(true); |
| }); |
|
|
| it('should use last match for ## Summary', () => { |
| const text = ` |
| ## Summary |
| |
| Old summary content. |
| |
| ## Summary |
| |
| New summary content. |
| `; |
| expect(extractSummary(text)).toBe('New summary content.'); |
| }); |
|
|
| it('should stop at next markdown header', () => { |
| const text = ` |
| ## Summary |
| |
| Summary content here. |
| |
| ## Implementation |
| Implementation details. |
| `; |
| expect(extractSummary(text)).toBe('Summary content here.'); |
| }); |
|
|
| it('should include ### subsections within the summary (not cut off at ### Root Cause)', () => { |
| const text = ` |
| ## Summary |
| |
| Overview of changes. |
| |
| ### Root Cause |
| The bug was caused by X. |
| |
| ### Fix Applied |
| Changed Y to Z. |
| |
| ## Other Section |
| More content. |
| `; |
| const result = extractSummary(text); |
| expect(result).not.toBeNull(); |
| expect(result).toContain('Overview of changes.'); |
| expect(result).toContain('### Root Cause'); |
| expect(result).toContain('The bug was caused by X.'); |
| expect(result).toContain('### Fix Applied'); |
| expect(result).toContain('Changed Y to Z.'); |
| expect(result).not.toContain('## Other Section'); |
| }); |
|
|
| it('should include ### subsections and stop at next ## header', () => { |
| const text = ` |
| ## Summary |
| |
| Brief intro. |
| |
| ### Changes |
| - File A modified |
| - File B added |
| |
| ### Notes |
| Important context. |
| |
| ## Implementation |
| Details here. |
| `; |
| const result = extractSummary(text); |
| expect(result).not.toBeNull(); |
| expect(result).toContain('Brief intro.'); |
| expect(result).toContain('### Changes'); |
| expect(result).toContain('### Notes'); |
| expect(result).not.toContain('## Implementation'); |
| }); |
| }); |
|
|
| describe('**Goal**: section (lite planning mode)', () => { |
| it('should extract from **Goal**: section', () => { |
| const text = '**Goal**: Implement user authentication\n**Approach**: Use JWT'; |
| expect(extractSummary(text)).toBe('Implement user authentication'); |
| }); |
|
|
| it('should use last match for **Goal**:', () => { |
| const text = ` |
| **Goal**: Old goal |
| |
| More output... |
| |
| **Goal**: New goal |
| `; |
| expect(extractSummary(text)).toBe('New goal'); |
| }); |
|
|
| it('should handle inline goal', () => { |
| const text = '1. **Goal**: Add login functionality'; |
| expect(extractSummary(text)).toBe('Add login functionality'); |
| }); |
| }); |
|
|
| describe('**Problem**: section (spec/full modes)', () => { |
| it('should extract from **Problem**: section', () => { |
| const text = ` |
| **Problem**: Users cannot log in to the application |
| |
| **Solution**: Add authentication |
| `; |
| expect(extractSummary(text)).toBe('Users cannot log in to the application'); |
| }); |
|
|
| it('should extract from **Problem Statement**: section', () => { |
| const text = ` |
| **Problem Statement**: Users need password reset functionality |
| |
| 1. Create reset endpoint |
| `; |
| expect(extractSummary(text)).toBe('Users need password reset functionality'); |
| }); |
|
|
| it('should truncate long problem descriptions', () => { |
| const longProblem = 'X'.repeat(600); |
| const text = `**Problem**: ${longProblem}`; |
| const result = extractSummary(text); |
| expect(result).not.toBeNull(); |
| expect(result!.length).toBeLessThanOrEqual(503); |
| }); |
| }); |
|
|
| describe('**Solution**: section (fallback)', () => { |
| it('should extract from **Solution**: section as fallback', () => { |
| const text = '**Solution**: Use JWT for authentication\n1. Install package'; |
| expect(extractSummary(text)).toBe('Use JWT for authentication'); |
| }); |
|
|
| it('should truncate solution to 300 chars', () => { |
| const longSolution = 'Y'.repeat(400); |
| const text = `**Solution**: ${longSolution}`; |
| const result = extractSummary(text); |
| expect(result).not.toBeNull(); |
| expect(result!.length).toBeLessThanOrEqual(303); |
| }); |
| }); |
|
|
| describe('priority order', () => { |
| it('should prefer <summary> over ## Summary', () => { |
| const text = ` |
| ## Summary |
| |
| Markdown summary |
| |
| <summary>Tagged summary</summary> |
| `; |
| expect(extractSummary(text)).toBe('Tagged summary'); |
| }); |
|
|
| it('should prefer ## Summary over **Goal**:', () => { |
| const text = ` |
| **Goal**: Goal content |
| |
| ## Summary |
| |
| Summary section content. |
| `; |
| expect(extractSummary(text)).toBe('Summary section content.'); |
| }); |
|
|
| it('should prefer **Goal**: over **Problem**:', () => { |
| const text = ` |
| **Problem**: Problem description |
| |
| **Goal**: Goal description |
| `; |
| expect(extractSummary(text)).toBe('Goal description'); |
| }); |
|
|
| it('should prefer **Problem**: over **Solution**:', () => { |
| const text = ` |
| **Solution**: Solution description |
| |
| **Problem**: Problem description |
| `; |
| expect(extractSummary(text)).toBe('Problem description'); |
| }); |
| }); |
|
|
| describe('edge cases', () => { |
| it('should return null for empty string', () => { |
| expect(extractSummary('')).toBeNull(); |
| }); |
|
|
| it('should return null when no summary pattern found', () => { |
| expect(extractSummary('Random text without any summary patterns')).toBeNull(); |
| }); |
|
|
| it('should include all paragraphs in ## Summary section', () => { |
| const text = ` |
| ## Summary |
| |
| First paragraph of summary. |
| |
| Second paragraph of summary. |
| |
| ## Other |
| `; |
| const result = extractSummary(text); |
| expect(result).toContain('First paragraph of summary.'); |
| expect(result).toContain('Second paragraph of summary.'); |
| }); |
| }); |
|
|
| describe('pipeline accumulated output (multiple <summary> tags)', () => { |
| it('should return only the LAST summary tag from accumulated pipeline output', () => { |
| |
| |
| |
| |
| const accumulatedOutput = ` |
| ## Step 1: Code Review |
| |
| Some review output... |
| |
| <summary> |
| ## Code Review Summary |
| - Found 3 issues |
| - Suggested 2 improvements |
| </summary> |
| |
| --- |
| |
| ## Follow-up Session |
| |
| ## Step 2: Testing |
| |
| Running tests... |
| |
| <summary> |
| ## Testing Summary |
| - All 15 tests pass |
| - Coverage at 92% |
| </summary> |
| `; |
| const result = extractSummary(accumulatedOutput); |
| |
| expect(result).toBe('## Testing Summary\n- All 15 tests pass\n- Coverage at 92%'); |
| expect(result).not.toContain('Code Review'); |
| }); |
|
|
| it('should return only the LAST summary from three pipeline steps', () => { |
| const accumulatedOutput = ` |
| <summary>Step 1: Implementation complete</summary> |
| |
| --- |
| |
| ## Follow-up Session |
| |
| <summary>Step 2: Code review findings</summary> |
| |
| --- |
| |
| ## Follow-up Session |
| |
| <summary>Step 3: All tests passing</summary> |
| `; |
| const result = extractSummary(accumulatedOutput); |
| expect(result).toBe('Step 3: All tests passing'); |
| expect(result).not.toContain('Step 1'); |
| expect(result).not.toContain('Step 2'); |
| }); |
|
|
| it('should handle accumulated output where only one step has a summary tag', () => { |
| const accumulatedOutput = ` |
| ## Step 1: Implementation |
| Some raw output without summary tags... |
| |
| --- |
| |
| ## Follow-up Session |
| |
| ## Step 2: Testing |
| |
| <summary> |
| ## Test Results |
| - All tests pass |
| </summary> |
| `; |
| const result = extractSummary(accumulatedOutput); |
| expect(result).toBe('## Test Results\n- All tests pass'); |
| }); |
| }); |
| }); |
| }); |
|
|