mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
Closes #671 (Complete fix for the plan mode system inside automaker) Related: #619, #627, #531, #660 ## Issues Fixed ### 1. Non-Claude Provider Support - Removed Claude model restriction from planning mode UI selectors - Added `detectSpecFallback()` function to detect specs without `[SPEC_GENERATED]` marker - All providers (OpenAI, Gemini, Cursor, etc.) can now use spec and full planning modes - Fallback detection looks for structural elements: tasks block, acceptance criteria, problem statement, implementation plan, etc. ### 2. Crash/Restart Recovery - Added `resetStuckFeatures()` to clean up transient states on auto-mode start - Features stuck in `in_progress` are reset to `ready` or `backlog` - Tasks stuck in `in_progress` are reset to `pending` - Plan generation stuck in `generating` is reset to `pending` - `loadPendingFeatures()` now includes recovery cases for interrupted executions - Persisted task status in `planSpec.tasks` array allows resuming from last completed task ### 3. Spec Todo List UI Updates - Added `ParsedTask` and `PlanSpec` types to `@automaker/types` for consistent typing - New `auto_mode_task_status` event emitted when task status changes - New `auto_mode_summary` event emitted when summary is extracted - Query invalidation triggers on task status updates for real-time UI refresh - Task markers (`[TASK_START]`, `[TASK_COMPLETE]`, `[PHASE_COMPLETE]`) are detected and persisted to planSpec.tasks for UI display ### 4. Summary Extraction - Added `extractSummary()` function to parse summaries from multiple formats: - `<summary>` tags (explicit) - `## Summary` sections (markdown) - `**Goal**:` sections (lite mode) - `**Problem**:` sections (spec/full modes) - `**Solution**:` sections (fallback) - Summary is saved to `feature.summary` field after execution - Summary is extracted from plan content during spec generation ### 5. Worktree Mode Support (#619) - Recovery logic properly handles branchName filtering - Features in worktrees maintain correct association during recovery ## Files Changed - libs/types/src/feature.ts - Added ParsedTask and PlanSpec interfaces - libs/types/src/index.ts - Export new types - apps/server/src/services/auto-mode-service.ts - Core fixes for all issues - apps/server/tests/unit/services/auto-mode-task-parsing.test.ts - New tests - apps/ui/src/store/app-store.ts - Import types from @automaker/types - apps/ui/src/hooks/use-auto-mode.ts - Handle new events - apps/ui/src/hooks/use-query-invalidation.ts - Invalidate on task updates - apps/ui/src/types/electron.d.ts - New event type definitions - apps/ui/src/components/views/board-view/dialogs/*.tsx - Enable planning for all models 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
571 lines
16 KiB
TypeScript
571 lines
16 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import type { ParsedTask } from '@automaker/types';
|
|
|
|
/**
|
|
* Test the task parsing logic by reimplementing the parsing functions
|
|
* These mirror the logic in auto-mode-service.ts parseTasksFromSpec and parseTaskLine
|
|
*/
|
|
|
|
function parseTaskLine(line: string, currentPhase?: string): ParsedTask | null {
|
|
// Match pattern: - [ ] T###: Description | File: path
|
|
const taskMatch = line.match(/- \[ \] (T\d{3}):\s*([^|]+)(?:\|\s*File:\s*(.+))?$/);
|
|
if (!taskMatch) {
|
|
// Try simpler pattern without file
|
|
const simpleMatch = line.match(/- \[ \] (T\d{3}):\s*(.+)$/);
|
|
if (simpleMatch) {
|
|
return {
|
|
id: simpleMatch[1],
|
|
description: simpleMatch[2].trim(),
|
|
phase: currentPhase,
|
|
status: 'pending',
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id: taskMatch[1],
|
|
description: taskMatch[2].trim(),
|
|
filePath: taskMatch[3]?.trim(),
|
|
phase: currentPhase,
|
|
status: 'pending',
|
|
};
|
|
}
|
|
|
|
function parseTasksFromSpec(specContent: string): ParsedTask[] {
|
|
const tasks: ParsedTask[] = [];
|
|
|
|
// Extract content within ```tasks ... ``` block
|
|
const tasksBlockMatch = specContent.match(/```tasks\s*([\s\S]*?)```/);
|
|
if (!tasksBlockMatch) {
|
|
// Try fallback: look for task lines anywhere in content
|
|
const taskLines = specContent.match(/- \[ \] T\d{3}:.*$/gm);
|
|
if (!taskLines) {
|
|
return tasks;
|
|
}
|
|
// Parse fallback task lines
|
|
let currentPhase: string | undefined;
|
|
for (const line of taskLines) {
|
|
const parsed = parseTaskLine(line, currentPhase);
|
|
if (parsed) {
|
|
tasks.push(parsed);
|
|
}
|
|
}
|
|
return tasks;
|
|
}
|
|
|
|
const tasksContent = tasksBlockMatch[1];
|
|
const lines = tasksContent.split('\n');
|
|
|
|
let currentPhase: string | undefined;
|
|
|
|
for (const line of lines) {
|
|
const trimmedLine = line.trim();
|
|
|
|
// Check for phase header (e.g., "## Phase 1: Foundation")
|
|
const phaseMatch = trimmedLine.match(/^##\s*(.+)$/);
|
|
if (phaseMatch) {
|
|
currentPhase = phaseMatch[1].trim();
|
|
continue;
|
|
}
|
|
|
|
// Check for task line
|
|
if (trimmedLine.startsWith('- [ ]')) {
|
|
const parsed = parseTaskLine(trimmedLine, currentPhase);
|
|
if (parsed) {
|
|
tasks.push(parsed);
|
|
}
|
|
}
|
|
}
|
|
|
|
return tasks;
|
|
}
|
|
|
|
describe('Task Parsing', () => {
|
|
describe('parseTaskLine', () => {
|
|
it('should parse task with file path', () => {
|
|
const line = '- [ ] T001: Create user model | File: src/models/user.ts';
|
|
const result = parseTaskLine(line);
|
|
expect(result).toEqual({
|
|
id: 'T001',
|
|
description: 'Create user model',
|
|
filePath: 'src/models/user.ts',
|
|
phase: undefined,
|
|
status: 'pending',
|
|
});
|
|
});
|
|
|
|
it('should parse task without file path', () => {
|
|
const line = '- [ ] T002: Setup database connection';
|
|
const result = parseTaskLine(line);
|
|
expect(result).toEqual({
|
|
id: 'T002',
|
|
description: 'Setup database connection',
|
|
phase: undefined,
|
|
status: 'pending',
|
|
});
|
|
});
|
|
|
|
it('should include phase when provided', () => {
|
|
const line = '- [ ] T003: Write tests | File: tests/user.test.ts';
|
|
const result = parseTaskLine(line, 'Phase 1: Foundation');
|
|
expect(result?.phase).toBe('Phase 1: Foundation');
|
|
});
|
|
|
|
it('should return null for invalid line', () => {
|
|
expect(parseTaskLine('- [ ] Invalid format')).toBeNull();
|
|
expect(parseTaskLine('Not a task line')).toBeNull();
|
|
expect(parseTaskLine('')).toBeNull();
|
|
});
|
|
|
|
it('should handle multi-word descriptions', () => {
|
|
const line = '- [ ] T004: Implement user authentication with JWT tokens | File: src/auth.ts';
|
|
const result = parseTaskLine(line);
|
|
expect(result?.description).toBe('Implement user authentication with JWT tokens');
|
|
});
|
|
|
|
it('should trim whitespace from description and file path', () => {
|
|
const line = '- [ ] T005: Create API endpoint | File: src/routes/api.ts ';
|
|
const result = parseTaskLine(line);
|
|
expect(result?.description).toBe('Create API endpoint');
|
|
expect(result?.filePath).toBe('src/routes/api.ts');
|
|
});
|
|
});
|
|
|
|
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].id).toBe('T001');
|
|
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 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');
|
|
});
|
|
});
|
|
|
|
describe('spec content generation patterns', () => {
|
|
it('should match the expected lite mode output format', () => {
|
|
const liteModeOutput = `
|
|
1. **Goal**: Implement user registration
|
|
2. **Approach**: Create form component, add validation, connect to API
|
|
3. **Files to Touch**: src/components/Register.tsx, src/api/auth.ts
|
|
4. **Tasks**:
|
|
1. Create registration form
|
|
2. Add form validation
|
|
3. Connect to backend API
|
|
5. **Risks**: Form state management complexity
|
|
|
|
[PLAN_GENERATED] Planning outline complete.
|
|
`;
|
|
expect(liteModeOutput).toContain('[PLAN_GENERATED]');
|
|
expect(liteModeOutput).toContain('Goal');
|
|
expect(liteModeOutput).toContain('Approach');
|
|
});
|
|
|
|
it('should match the expected spec mode output format', () => {
|
|
const specModeOutput = `
|
|
1. **Problem**: Users cannot register for accounts
|
|
|
|
2. **Solution**: Implement registration form with email/password validation
|
|
|
|
3. **Acceptance Criteria**:
|
|
- GIVEN a new user, WHEN they fill in valid details, THEN account is created
|
|
|
|
4. **Files to Modify**:
|
|
| File | Purpose | Action |
|
|
|------|---------|--------|
|
|
| src/Register.tsx | Registration form | create |
|
|
|
|
5. **Implementation Tasks**:
|
|
\`\`\`tasks
|
|
- [ ] T001: Create registration component | File: src/Register.tsx
|
|
- [ ] T002: Add form validation | File: src/Register.tsx
|
|
\`\`\`
|
|
|
|
6. **Verification**: Manual testing of registration flow
|
|
|
|
[SPEC_GENERATED] Please review the specification above.
|
|
`;
|
|
expect(specModeOutput).toContain('[SPEC_GENERATED]');
|
|
expect(specModeOutput).toContain('```tasks');
|
|
expect(specModeOutput).toContain('T001');
|
|
});
|
|
|
|
it('should match the expected full mode output format', () => {
|
|
const fullModeOutput = `
|
|
1. **Problem Statement**: Users need ability to create accounts
|
|
|
|
2. **User Story**: As a new user, I want to register, so that I can access the app
|
|
|
|
3. **Acceptance Criteria**:
|
|
- **Happy Path**: GIVEN valid email, WHEN registering, THEN account created
|
|
- **Edge Cases**: GIVEN existing email, WHEN registering, THEN error shown
|
|
|
|
4. **Technical Context**:
|
|
| Aspect | Value |
|
|
|--------|-------|
|
|
| Affected Files | src/Register.tsx |
|
|
|
|
5. **Non-Goals**: Social login, password recovery
|
|
|
|
6. **Implementation Tasks**:
|
|
\`\`\`tasks
|
|
## Phase 1: Foundation
|
|
- [ ] T001: Setup component structure | File: src/Register.tsx
|
|
|
|
## Phase 2: Core Implementation
|
|
- [ ] T002: Add form logic | File: src/Register.tsx
|
|
|
|
## Phase 3: Integration & Testing
|
|
- [ ] T003: Connect to API | File: src/api/auth.ts
|
|
\`\`\`
|
|
|
|
[SPEC_GENERATED] Please review the comprehensive specification above.
|
|
`;
|
|
expect(fullModeOutput).toContain('Phase 1');
|
|
expect(fullModeOutput).toContain('Phase 2');
|
|
expect(fullModeOutput).toContain('Phase 3');
|
|
expect(fullModeOutput).toContain('[SPEC_GENERATED]');
|
|
});
|
|
});
|
|
|
|
describe('detectSpecFallback - non-Claude model support', () => {
|
|
/**
|
|
* Reimplementation of detectSpecFallback for testing
|
|
* This mirrors the logic in auto-mode-service.ts for detecting specs
|
|
* when the [SPEC_GENERATED] marker is missing (common with non-Claude models)
|
|
*/
|
|
function detectSpecFallback(text: string): boolean {
|
|
// Check for key structural elements of a spec
|
|
const hasTasksBlock = /```tasks[\s\S]*```/.test(text);
|
|
const hasTaskLines = /- \[ \] T\d{3}:/.test(text);
|
|
|
|
// Check for common spec sections (case-insensitive)
|
|
const hasAcceptanceCriteria = /acceptance criteria/i.test(text);
|
|
const hasTechnicalContext = /technical context/i.test(text);
|
|
const hasProblemStatement = /problem statement/i.test(text);
|
|
const hasUserStory = /user story/i.test(text);
|
|
// Additional patterns for different model outputs
|
|
const hasGoal = /\*\*Goal\*\*:/i.test(text);
|
|
const hasSolution = /\*\*Solution\*\*:/i.test(text);
|
|
const hasImplementation = /implementation\s*(plan|steps|approach)/i.test(text);
|
|
const hasOverview = /##\s*(overview|summary)/i.test(text);
|
|
|
|
// Spec is detected if we have task structure AND at least some spec content
|
|
const hasTaskStructure = hasTasksBlock || hasTaskLines;
|
|
const hasSpecContent =
|
|
hasAcceptanceCriteria ||
|
|
hasTechnicalContext ||
|
|
hasProblemStatement ||
|
|
hasUserStory ||
|
|
hasGoal ||
|
|
hasSolution ||
|
|
hasImplementation ||
|
|
hasOverview;
|
|
|
|
return hasTaskStructure && hasSpecContent;
|
|
}
|
|
|
|
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 style)', () => {
|
|
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
|
|
- [ ] T002: Update CSS variables
|
|
\`\`\`
|
|
`;
|
|
expect(detectSpecFallback(content)).toBe(true);
|
|
});
|
|
|
|
it('should detect spec with Summary section', () => {
|
|
const content = `
|
|
## Summary
|
|
Adding a new dashboard component.
|
|
|
|
- [ ] T001: Create dashboard layout
|
|
- [ ] T002: Add widgets
|
|
`;
|
|
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
|
|
- [ ] T002: Phase 2 implementation
|
|
`;
|
|
expect(detectSpecFallback(content)).toBe(true);
|
|
});
|
|
|
|
it('should detect spec with implementation steps', () => {
|
|
const content = `
|
|
## Implementation Steps
|
|
Follow these steps:
|
|
|
|
- [ ] T001: Step one
|
|
- [ ] T002: Step two
|
|
`;
|
|
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', () => {
|
|
const content = 'Just some random text without any structure';
|
|
expect(detectSpecFallback(content)).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);
|
|
|
|
const content2 = `
|
|
## acceptance criteria
|
|
Lower case section header
|
|
|
|
- [ ] T001: Task
|
|
`;
|
|
expect(detectSpecFallback(content2)).toBe(true);
|
|
});
|
|
|
|
it('should detect OpenAI-style output without explicit marker', () => {
|
|
// Non-Claude models may format specs differently but still have the key elements
|
|
const openAIStyleOutput = `
|
|
# Feature Specification: User Authentication
|
|
|
|
**Goal**: Allow users to securely log into the application
|
|
|
|
**Solution**: Implement JWT-based authentication with refresh tokens
|
|
|
|
## Acceptance Criteria
|
|
1. Users can log in with email and password
|
|
2. Invalid credentials show error message
|
|
3. Sessions persist across page refreshes
|
|
|
|
## Implementation Tasks
|
|
\`\`\`tasks
|
|
- [ ] T001: Create auth service | File: src/services/auth.ts
|
|
- [ ] T002: Build login component | File: src/components/Login.tsx
|
|
- [ ] T003: Add protected routes | File: src/App.tsx
|
|
\`\`\`
|
|
`;
|
|
expect(detectSpecFallback(openAIStyleOutput)).toBe(true);
|
|
});
|
|
|
|
it('should detect Gemini-style output without explicit marker', () => {
|
|
const geminiStyleOutput = `
|
|
## Overview
|
|
|
|
This specification describes the implementation of a user profile page.
|
|
|
|
## Technical Context
|
|
- Framework: React
|
|
- State: Redux
|
|
|
|
## Tasks
|
|
|
|
- [ ] T001: Create ProfilePage component
|
|
- [ ] T002: Add profile API endpoint
|
|
- [ ] T003: Style the profile page
|
|
`;
|
|
expect(detectSpecFallback(geminiStyleOutput)).toBe(true);
|
|
});
|
|
});
|
|
});
|