Files
markitect-main/tddai/workspace.py
Bernd Worsch 5155a548eb feat: implement tddai Python library for TDD workspace management
- Create comprehensive tddai package with workspace, issue fetcher, and test generator modules
- Add Python CLI interface (tddai_cli.py) to replace complex Makefile shell logic
- Update Makefile targets to use Python CLI for better maintainability
- Implement proper behavior-based tests instead of file existence checks
- Add workspace lifecycle management (create, active, finish, cleanup)
- Add issue fetching from Gitea API with error handling
- Add comprehensive test coverage with 19 passing tests
- Support environment variable configuration for different deployments

This addresses issue #11: Setup TDD workspace infrastructure
All tests pass and the system achieves green state before commit.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-22 02:04:19 +02:00

219 lines
6.8 KiB
Python

"""
Workspace management for tddai.
"""
import json
import shutil
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Optional, Dict, Any
from .config import get_config
from .exceptions import WorkspaceError
class WorkspaceStatus(Enum):
"""Status of workspace."""
CLEAN = "clean"
ACTIVE = "active"
DIRTY = "dirty"
@dataclass
class Workspace:
"""Represents a TDD workspace for an issue."""
issue_number: int
issue_title: str
issue_body: str
issue_state: str
created_at: datetime
workspace_dir: Path
@property
def issue_dir(self) -> Path:
"""Get the issue-specific directory."""
return self.workspace_dir / f"issue_{self.issue_number}"
@property
def tests_dir(self) -> Path:
"""Get the tests directory for this issue."""
return self.issue_dir / "tests"
@property
def requirements_file(self) -> Path:
"""Get the requirements file path."""
return self.issue_dir / "requirements.md"
@property
def test_plan_file(self) -> Path:
"""Get the test plan file path."""
return self.issue_dir / "test_plan.md"
class WorkspaceManager:
"""Manages TDD workspaces for issues."""
def __init__(self, config=None):
self.config = config or get_config()
def get_status(self) -> WorkspaceStatus:
"""Get current workspace status."""
if not self.config.workspace_dir.exists():
return WorkspaceStatus.CLEAN
if not self.config.current_issue_path.exists():
return WorkspaceStatus.DIRTY
return WorkspaceStatus.ACTIVE
def get_current_workspace(self) -> Optional[Workspace]:
"""Get the currently active workspace."""
if not self.config.current_issue_path.exists():
return None
try:
with open(self.config.current_issue_path, 'r') as f:
issue_data = json.load(f)
return Workspace(
issue_number=issue_data['number'],
issue_title=issue_data['title'],
issue_body=issue_data['body'],
issue_state=issue_data['state'],
created_at=datetime.fromisoformat(issue_data['created_at']),
workspace_dir=self.config.workspace_dir
)
except (json.JSONDecodeError, KeyError, ValueError) as e:
raise WorkspaceError(f"Failed to load current workspace: {e}")
def create_workspace(self, issue_data: Dict[str, Any]) -> Workspace:
"""Create a new workspace for an issue."""
status = self.get_status()
if status == WorkspaceStatus.ACTIVE:
current = self.get_current_workspace()
raise WorkspaceError(
f"Workspace already active for issue #{current.issue_number}. "
"Finish current workspace before starting a new one."
)
# Clean up any dirty workspace
if status == WorkspaceStatus.DIRTY:
self.cleanup_workspace()
# Create workspace structure
workspace = Workspace(
issue_number=issue_data['number'],
issue_title=issue_data['title'],
issue_body=issue_data['body'],
issue_state=issue_data['state'],
created_at=datetime.now(),
workspace_dir=self.config.workspace_dir
)
# Create directories
workspace.workspace_dir.mkdir(exist_ok=True)
workspace.issue_dir.mkdir(exist_ok=True)
workspace.tests_dir.mkdir(exist_ok=True)
# Create metadata files
self._create_requirements_file(workspace, issue_data)
self._create_test_plan_file(workspace, issue_data)
self._save_current_issue(workspace, issue_data)
return workspace
def cleanup_workspace(self) -> None:
"""Clean up the current workspace."""
if self.config.workspace_dir.exists():
shutil.rmtree(self.config.workspace_dir)
def finish_workspace(self) -> Optional[Workspace]:
"""Finish the current workspace and integrate tests."""
workspace = self.get_current_workspace()
if not workspace:
return None
# Move tests to main tests directory
main_tests_dir = self.config.tests_dir
main_tests_dir.mkdir(exist_ok=True)
if workspace.tests_dir.exists():
for test_file in workspace.tests_dir.glob("*.py"):
dest_file = main_tests_dir / test_file.name
shutil.copy2(test_file, dest_file)
# Clean up workspace
self.cleanup_workspace()
return workspace
def _create_requirements_file(self, workspace: Workspace, issue_data: Dict[str, Any]) -> None:
"""Create requirements.md file for the issue."""
content = f"""# Requirements for Issue #{workspace.issue_number}
## Title
{workspace.issue_title}
## Description
{workspace.issue_body}
## Acceptance Criteria
- [ ] Implementation meets the requirements described above
- [ ] All tests pass
- [ ] Code follows project conventions
- [ ] Documentation is updated if needed
## Notes
Created: {workspace.created_at.strftime('%Y-%m-%d %H:%M:%S')}
"""
workspace.requirements_file.write_text(content)
def _create_test_plan_file(self, workspace: Workspace, issue_data: Dict[str, Any]) -> None:
"""Create test_plan.md file for the issue."""
content = f"""# Test Plan for Issue #{workspace.issue_number}
## Overview
This test plan outlines the testing strategy for implementing: {workspace.issue_title}
## Test Categories
### Unit Tests
- [ ] Core functionality tests
- [ ] Edge case handling
- [ ] Error condition tests
### Integration Tests
- [ ] Component integration
- [ ] API integration
- [ ] End-to-end scenarios
### Generated Tests
Tests generated for this workspace will be listed here as they are created.
## Test Execution
Run tests with: `pytest tests/test_issue_{workspace.issue_number}_*.py`
## Notes
- Follow TDD red-green-refactor cycle
- Each test should be focused and specific
- Tests should be readable and maintainable
"""
workspace.test_plan_file.write_text(content)
def _save_current_issue(self, workspace: Workspace, issue_data: Dict[str, Any]) -> None:
"""Save current issue metadata."""
current_issue_data = {
'number': workspace.issue_number,
'title': workspace.issue_title,
'body': workspace.issue_body,
'state': workspace.issue_state,
'created_at': workspace.created_at.isoformat(),
'url': issue_data.get('html_url', ''),
'assignee': issue_data.get('assignee', {}).get('login', '') if issue_data.get('assignee') else ''
}
with open(self.config.current_issue_path, 'w') as f:
json.dump(current_issue_data, f, indent=2)