UNPKG

devin-workflow

Version:

~Devin AI workflow automation

623 lines (504 loc) 22.3 kB
import { WorkflowParser } from '../src/workflow-parser.js'; import { describe, it, expect } from '@jest/globals'; describe('WorkflowParser.parseStep', () => { let parser; beforeEach(() => { parser = new WorkflowParser(); }); it('parses a step with all fields', () => { const stepContent = `## Step 2: Implementation\n- Playbook: code-review\n- Prompt: Review the code\n- Handoff: Provide feedback\n- RelyPreviousStep: yes`; const step = parser.parseStep(stepContent, 2); expect(step.step_number).toBe(2); expect(step.title).toBe('Implementation'); expect(step.playbook).toBe('playbook-code-review'); expect(step.prompt).toMatch(/^Review the code/); expect(step.handoff).toMatch(/^Provide feedback/); expect(step.rely_previous_step).toBe(true); }); it('parses a step with minimal fields', () => { const stepContent = `## Step 1: Planning\n- Prompt: Plan the project`; const step = parser.parseStep(stepContent, 1); expect(step.step_number).toBe(1); expect(step.title).toBe('Planning'); expect(step.playbook).toBeNull(); expect(step.prompt).toMatch(/^Plan the project/); expect(step.handoff).toBeNull(); expect(step.rely_previous_step).toBe(false); }); it('throws if prompt is missing', () => { const stepContent = `## Step 1: Planning\n- Playbook: code-review`; expect(() => parser.parseStep(stepContent, 1)).toThrow(/Prompt is required/); }); it('normalizes playbook id', () => { const stepContent = `## Step 1: Planning\n- Playbook: playbook-custom\n- Prompt: Do something`; const step = parser.parseStep(stepContent, 1); expect(step.playbook).toBe('playbook-custom'); }); it('parses rely_previous_step as false', () => { const stepContent = `## Step 2: QA\n- Prompt: Test\n- RelyPreviousStep: no`; const step = parser.parseStep(stepContent, 2); expect(step.rely_previous_step).toBe(false); }); it('parses handoff as null if empty', () => { const stepContent = `## Step 2: QA\n- Prompt: Test\n- Handoff: `; const step = parser.parseStep(stepContent, 2); expect(step.handoff).toBeNull(); }); it('parses handoff with a long summary', () => { const stepContent = `## Step 1: Read ADO Ticket Data\n- Playbook: 2bad2151af5f4380af0acab90f1cd328\n- Prompt: ...\n- Handoff: Provide ticket ID, work item type, and confirm successful data extraction with attachment count and linked items summary`; const step = parser.parseStep(stepContent, 1); expect(step.handoff).toBe('Provide ticket ID, work item type, and confirm successful data extraction with attachment count and linked items summary'); }); it('parses handoff with structured content summary', () => { const stepContent = `## Step 2: Analyze Core Content\n- Playbook: <none>\n- RelyPreviousStep: yes\n- Prompt: ...\n- Handoff: Deliver structured content analysis with categorized requirements and attachment summaries`; const step = parser.parseStep(stepContent, 2); expect(step.handoff).toBe('Deliver structured content analysis with categorized requirements and attachment summaries'); }); it('parses handoff as null if <none>', () => { const stepContent = `## Step 3: Something\n- Prompt: ...\n- Handoff: <none>`; const step = parser.parseStep(stepContent, 3); expect(step.handoff).toBeNull(); }); it('parses handoff as null if only spaces', () => { const stepContent = `## Step 4: Something\n- Prompt: ...\n- Handoff: `; const step = parser.parseStep(stepContent, 4); expect(step.handoff).toBeNull(); }); it('parses handoff as null if value is null', () => { const stepContent = `## Step 5: Something\n- Prompt: ...\n- Handoff: null`; const step = parser.parseStep(stepContent, 5); expect(step.handoff).toBeNull(); }); it('parses handoff as string if value is 0', () => { const stepContent = `## Step 6: Something\n- Prompt: ...\n- Handoff: 0`; const step = parser.parseStep(stepContent, 6); expect(step.handoff).toBe('0'); }); it('parses a step with repo field', () => { const stepContent = `## Step 1: Setup\n- Repo: dev.azure.com/access-devops/Access%20Vincere/_git/staffingboss-reactapp\n- Prompt: Clone and setup the repository`; const step = parser.parseStep(stepContent, 1); expect(step.step_number).toBe(1); expect(step.title).toBe('Setup'); expect(step.repo).toBe('dev.azure.com/access-devops/Access%20Vincere/_git/staffingboss-reactapp'); expect(step.prompt).toBe('Clone and setup the repository'); expect(step.playbook).toBeNull(); expect(step.handoff).toBeNull(); }); it('parses a step with all fields including repo', () => { const stepContent = `## Step 2: Implementation\n- Playbook: code-review\n- Repo: github.com/example/project\n- Prompt: Review the code\n- Handoff: Provide feedback\n- RelyPreviousStep: yes`; const step = parser.parseStep(stepContent, 2); expect(step.step_number).toBe(2); expect(step.title).toBe('Implementation'); expect(step.playbook).toBe('playbook-code-review'); expect(step.repo).toBe('github.com/example/project'); expect(step.prompt).toMatch(/^Review the code/); expect(step.handoff).toMatch(/^Provide feedback/); expect(step.rely_previous_step).toBe(true); }); it('parses repo as null if empty', () => { const stepContent = `## Step 3: Test\n- Prompt: Run tests\n- Repo: `; const step = parser.parseStep(stepContent, 3); expect(step.repo).toBeNull(); }); it('parses repo as null if empty string', () => { const stepContent = `## Step 1: Planning\n- Prompt: Plan the project\n- Repo: `; const step = parser.parseStep(stepContent, 1); expect(step.repo).toBeNull(); }); it('parses repo as null for various empty values', () => { const testCases = [ '- Prompt: Test the functionality\n- Repo: ', '- Prompt: Test the functionality\n- Repo: ', '- Prompt: Test the functionality\n- Repo:\n', '- Prompt: Test the functionality\n- Repo: \n', '- Prompt: Test the functionality\n- Repo: \n' ]; testCases.forEach(content => { const stepContent = `## Step 1: Testing\n${content}`; const step = parser.parseStep(stepContent, 1); expect(step.repo).toBeNull(); }); }); it('parses repo as "none" if value is none (for inheritance logic)', () => { const stepContent = `## Step 4: Deploy\n- Repo: none\n- Prompt: Deploy to production`; const step = parser.parseStep(stepContent, 4); expect(step.repo).toBe('none'); }); it('parses repo as "none" if value is None (case insensitive)', () => { const stepContent = `## Step 4: Deploy\n- Repo: None\n- Prompt: Deploy to production`; const step = parser.parseStep(stepContent, 4); expect(step.repo).toBe('none'); }); }); describe('WorkflowParser validateWorkflow + format', () => { let parser; beforeEach(() => { parser = new WorkflowParser(); }); describe('validateWorkflow', () => { it('should validate a correct workflow with no errors', () => { const steps = [ { step_number: 1, prompt: 'Analyze requirements', playbook: 'playbook-analysis', handoff: 'Requirements document', rely_previous_step: false }, { step_number: 2, prompt: 'Design solution', playbook: 'playbook-design', handoff: 'Design document', rely_previous_step: true } ]; const result = parser.validateWorkflow(steps); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); expect(result.warnings).toHaveLength(0); }); it('should return error for missing prompt', () => { const steps = [ { step_number: 1, prompt: null, playbook: 'playbook-analysis', handoff: 'Requirements document', rely_previous_step: false } ]; const result = parser.validateWorkflow(steps); expect(result.valid).toBe(false); expect(result.errors).toContain('Step 1: Missing required prompt'); }); it('should return error for first step relying on previous', () => { const steps = [ { step_number: 1, prompt: 'Analyze requirements', playbook: 'playbook-analysis', handoff: 'Requirements document', rely_previous_step: true } ]; const result = parser.validateWorkflow(steps); expect(result.valid).toBe(false); expect(result.errors).toContain('Step 1: Cannot rely on previous step as it\'s the first step'); }); it('should return error for invalid playbook format', () => { const steps = [ { step_number: 1, prompt: 'Analyze requirements', playbook: 'invalid-format', handoff: 'Requirements document', rely_previous_step: false } ]; const result = parser.validateWorkflow(steps); expect(result.valid).toBe(false); expect(result.errors).toContain('Step 1: Playbook ID should start with \'playbook-\''); }); it('should return warning for empty handoff instruction', () => { const steps = [ { step_number: 1, prompt: 'Analyze requirements', playbook: 'playbook-analysis', handoff: ' ', rely_previous_step: false } ]; const result = parser.validateWorkflow(steps); expect(result.valid).toBe(true); expect(result.warnings).toContain('Step 1: Empty handoff instruction'); }); it('should return warning for step relying on previous without handoff', () => { const steps = [ { step_number: 1, prompt: 'Analyze requirements', playbook: 'playbook-analysis', handoff: 'Requirements document', rely_previous_step: false }, { step_number: 2, prompt: 'Design solution', playbook: 'playbook-design', handoff: null, rely_previous_step: true } ]; const result = parser.validateWorkflow(steps); expect(result.valid).toBe(true); expect(result.warnings).toContain('Step 2: Relies on previous step but has no handoff instruction'); }); it('should handle multiple errors and warnings', () => { const steps = [ { step_number: 1, prompt: null, playbook: 'invalid-format', handoff: ' ', rely_previous_step: true }, { step_number: 2, prompt: 'Design solution', playbook: 'playbook-design', handoff: null, rely_previous_step: true } ]; const result = parser.validateWorkflow(steps); expect(result.valid).toBe(false); expect(result.errors).toHaveLength(3); expect(result.errors).toContain('Step 1: Missing required prompt'); expect(result.errors).toContain('Step 1: Cannot rely on previous step as it\'s the first step'); expect(result.errors).toContain('Step 1: Playbook ID should start with \'playbook-\''); expect(result.warnings).toHaveLength(2); expect(result.warnings).toContain('Step 1: Empty handoff instruction'); expect(result.warnings).toContain('Step 2: Relies on previous step but has no handoff instruction'); }); it('should accept valid playbook with correct prefix', () => { const steps = [ { step_number: 1, prompt: 'Analyze requirements', playbook: 'playbook-analysis', handoff: 'Requirements document', rely_previous_step: false } ]; const result = parser.validateWorkflow(steps); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); it('should handle steps without playbook', () => { const steps = [ { step_number: 1, prompt: 'Analyze requirements', playbook: null, handoff: 'Requirements document', rely_previous_step: false } ]; const result = parser.validateWorkflow(steps); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); it('should handle empty steps array', () => { const steps = []; const result = parser.validateWorkflow(steps); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); expect(result.warnings).toHaveLength(0); }); }); describe('splitIntoSteps', () => { it('should split markdown into correct step sections with all fields', () => { const markdown = `## Overview This is a test workflow. ## Step 1: Requirements Analysis - Playbook: playbook-analysis - Prompt: Analyze the requirements - Handoff: Requirements document ## Step 2: Design Phase - Playbook: playbook-design - Prompt: Create the design - Handoff: Design document`; const sections = parser.splitIntoSteps(markdown); expect(sections).toHaveLength(2); expect(sections[0]).toContain('## Step 1: Requirements Analysis'); expect(sections[0]).toContain('- Playbook: playbook-analysis'); expect(sections[0]).toContain('- Prompt: Analyze the requirements'); expect(sections[0]).toContain('- Handoff: Requirements document'); expect(sections[1]).toContain('## Step 2: Design Phase'); expect(sections[1]).toContain('- Playbook: playbook-design'); expect(sections[1]).toContain('- Prompt: Create the design'); expect(sections[1]).toContain('- Handoff: Design document'); }); it('should handle steps with missing playbook', () => { const markdown = `## Step 1: Requirements Analysis - Prompt: Analyze the requirements - Handoff: Requirements document ## Step 2: Design Phase - Prompt: Create the design - Handoff: Design document`; const sections = parser.splitIntoSteps(markdown); expect(sections).toHaveLength(2); expect(sections[0]).toContain('## Step 1: Requirements Analysis'); expect(sections[0]).not.toContain('- Playbook:'); expect(sections[0]).toContain('- Prompt: Analyze the requirements'); expect(sections[0]).toContain('- Handoff: Requirements document'); }); it('should handle steps with missing handoff', () => { const markdown = `## Step 1: Requirements Analysis - Playbook: playbook-analysis - Prompt: Analyze the requirements ## Step 2: Design Phase - Playbook: playbook-design - Prompt: Create the design`; const sections = parser.splitIntoSteps(markdown); expect(sections).toHaveLength(2); expect(sections[0]).toContain('## Step 1: Requirements Analysis'); expect(sections[0]).toContain('- Playbook: playbook-analysis'); expect(sections[0]).toContain('- Prompt: Analyze the requirements'); expect(sections[0]).not.toContain('- Handoff:'); }); it('should handle steps with only prompt (minimal structure)', () => { const markdown = `## Step 1: Requirements Analysis - Prompt: Analyze the requirements ## Step 2: Design Phase - Prompt: Create the design`; const sections = parser.splitIntoSteps(markdown); expect(sections).toHaveLength(2); expect(sections[0]).toContain('## Step 1: Requirements Analysis'); expect(sections[0]).toContain('- Prompt: Analyze the requirements'); expect(sections[0]).not.toContain('- Playbook:'); expect(sections[0]).not.toContain('- Handoff:'); expect(sections[1]).toContain('## Step 2: Design Phase'); expect(sections[1]).toContain('- Prompt: Create the design'); }); it('should handle mixed scenarios (some steps with playbook/handoff, others without)', () => { const markdown = `## Step 1: Requirements Analysis - Prompt: Analyze the requirements - Handoff: Requirements document ## Step 2: Design Phase - Playbook: playbook-design - Prompt: Create the design ## Step 3: Implementation - Prompt: Implement the solution`; const sections = parser.splitIntoSteps(markdown); expect(sections).toHaveLength(3); // Step 1: has handoff but no playbook expect(sections[0]).toContain('## Step 1: Requirements Analysis'); expect(sections[0]).not.toContain('- Playbook:'); expect(sections[0]).toContain('- Handoff: Requirements document'); // Step 2: has playbook but no handoff expect(sections[1]).toContain('## Step 2: Design Phase'); expect(sections[1]).toContain('- Playbook: playbook-design'); expect(sections[1]).not.toContain('- Handoff:'); // Step 3: minimal (only prompt) expect(sections[2]).toContain('## Step 3: Implementation'); expect(sections[2]).not.toContain('- Playbook:'); expect(sections[2]).not.toContain('- Handoff:'); }); it('should handle legacy format (## Step N ##)', () => { const markdown = `## Step 1 ## - Playbook: playbook-analysis - Prompt: Analyze the requirements ## Step 2 ## - Prompt: Create the design`; const sections = parser.splitIntoSteps(markdown); expect(sections).toHaveLength(2); expect(sections[0]).toContain('## Step 1 ##'); expect(sections[0]).toContain('- Playbook: playbook-analysis'); expect(sections[1]).toContain('## Step 2 ##'); expect(sections[1]).not.toContain('- Playbook:'); }); it('should handle step titles with and without space after colon', () => { const markdown = `## Step 1: Requirements Analysis - Prompt: Analyze the requirements ## Step 2:Design Phase - Prompt: Create the design`; const sections = parser.splitIntoSteps(markdown); expect(sections).toHaveLength(2); expect(sections[0]).toContain('## Step 1: Requirements Analysis'); expect(sections[1]).toContain('## Step 2:Design Phase'); }); it('should return empty array for markdown without step sections', () => { const markdown = `## Overview This is just an overview without any steps. ## Conclusion This is the end.`; const sections = parser.splitIntoSteps(markdown); expect(sections).toHaveLength(0); }); it('should handle single step correctly', () => { const markdown = `## Step 1: Single Step - Prompt: Do everything - Handoff: Complete solution`; const sections = parser.splitIntoSteps(markdown); expect(sections).toHaveLength(1); expect(sections[0]).toContain('## Step 1: Single Step'); expect(sections[0]).toContain('- Prompt: Do everything'); expect(sections[0]).toContain('- Handoff: Complete solution'); }); it('should handle steps with repo field in splitIntoSteps', () => { const markdown = `## Step 1: Setup Repository - Repo: github.com/example/project - Prompt: Clone and setup the repository - Handoff: Repository ready for development ## Step 2: Code Review - Playbook: playbook-review - Repo: dev.azure.com/access-devops/Access%20Vincere/_git/staffingboss-reactapp - Prompt: Review the code changes - Handoff: Review completed`; const sections = parser.splitIntoSteps(markdown); expect(sections).toHaveLength(2); expect(sections[0]).toContain('## Step 1: Setup Repository'); expect(sections[0]).toContain('- Repo: github.com/example/project'); expect(sections[0]).toContain('- Prompt: Clone and setup the repository'); expect(sections[0]).toContain('- Handoff: Repository ready for development'); expect(sections[1]).toContain('## Step 2: Code Review'); expect(sections[1]).toContain('- Playbook: playbook-review'); expect(sections[1]).toContain('- Repo: dev.azure.com/access-devops/Access%20Vincere/_git/staffingboss-reactapp'); expect(sections[1]).toContain('- Prompt: Review the code changes'); expect(sections[1]).toContain('- Handoff: Review completed'); }); it('should handle mixed scenarios with some steps having repo field', () => { const markdown = `## Step 1: Requirements Analysis - Prompt: Analyze the requirements - Handoff: Requirements document ## Step 2: Code Setup - Repo: github.com/example/project - Playbook: playbook-setup - Prompt: Setup the codebase ## Step 3: Implementation - Prompt: Implement the solution`; const sections = parser.splitIntoSteps(markdown); expect(sections).toHaveLength(3); // Step 1: no repo field expect(sections[0]).toContain('## Step 1: Requirements Analysis'); expect(sections[0]).not.toContain('- Repo:'); expect(sections[0]).toContain('- Handoff: Requirements document'); // Step 2: has repo field expect(sections[1]).toContain('## Step 2: Code Setup'); expect(sections[1]).toContain('- Repo: github.com/example/project'); expect(sections[1]).toContain('- Playbook: playbook-setup'); // Step 3: no repo field expect(sections[2]).toContain('## Step 3: Implementation'); expect(sections[2]).not.toContain('- Repo:'); }); it('should handle repo inheritance scenarios in splitIntoSteps', () => { const markdown = `## Step 1: Setup Repository - Repo: my-project - Prompt: Setup the main repository ## Step 2: Process Code - Prompt: Process the code (should inherit repo) ## Step 3: Deploy Without Repo - Repo: none - Prompt: Deploy without repository context ## Step 4: Continue Processing - Prompt: Continue processing (no inheritance after none)`; const sections = parser.splitIntoSteps(markdown); expect(sections).toHaveLength(4); // Step 1: defines repo expect(sections[0]).toContain('## Step 1: Setup Repository'); expect(sections[0]).toContain('- Repo: my-project'); // Step 2: no repo defined (will inherit) expect(sections[1]).toContain('## Step 2: Process Code'); expect(sections[1]).not.toContain('- Repo:'); // Step 3: explicitly sets repo to none expect(sections[2]).toContain('## Step 3: Deploy Without Repo'); expect(sections[2]).toContain('- Repo: none'); // Step 4: no repo defined (won't inherit due to step 3) expect(sections[3]).toContain('## Step 4: Continue Processing'); expect(sections[3]).not.toContain('- Repo:'); }); }); });