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>
This commit is contained in:
2025-09-22 02:04:19 +02:00
parent b03160437e
commit 5155a548eb
13 changed files with 1360 additions and 176 deletions

183
Makefile
View File

@@ -230,22 +230,7 @@ CURRENT_ISSUE_FILE := $(WORKSPACE_DIR)/current_issue.json
# List all gitea issues
list-issues:
@echo "📋 MarkiTect Issues from Gitea Repository"
@echo "========================================"
@echo ""
@if ! command -v curl >/dev/null 2>&1; then \
echo "❌ curl not found - required for API access"; \
exit 1; \
fi
@if ! command -v jq >/dev/null 2>&1; then \
echo "⚠️ jq not found - using basic formatting"; \
echo " Install jq for better formatting: sudo apt install jq"; \
curl -s "$(ISSUES_API)" | head -20; \
else \
curl -s "$(ISSUES_API)" | jq -r '.[] | "[\(.state | ascii_upcase)] #\(.number): \(.title)\n Created: \(.created_at[:10]) | Updated: \(.updated_at[:10])\n \(.body[:80])...\n"' | head -40; \
fi
@echo ""
@echo "💡 Tip: Use 'make show-issue NUM=X' to see full details"
@python3 tddai_cli.py list-issues
# Show detailed view of a specific issue
show-issue:
@@ -253,39 +238,11 @@ show-issue:
echo "❌ Please specify issue number: make show-issue NUM=5"; \
exit 1; \
fi
@if ! command -v curl >/dev/null 2>&1; then \
echo "❌ curl not found - required for API access"; \
exit 1; \
fi
@echo "🔍 Issue #$(NUM) Details"
@echo "======================="
@echo ""
@if ! command -v jq >/dev/null 2>&1; then \
echo "⚠️ jq not found - using basic formatting"; \
curl -s "$(ISSUES_API)/$(NUM)"; \
else \
curl -s "$(ISSUES_API)/$(NUM)" | jq -r 'if . == null or .message then "❌ Issue #$(NUM) not found or API error" else "**Title:** " + .title + "\n**Status:** " + (.state | ascii_upcase) + "\n**Number:** #" + (.number | tostring) + "\n**Created:** " + (.created_at[:10]) + " by " + (.user.full_name // .user.login) + "\n**Updated:** " + (.updated_at[:10]) + "\n**URL:** " + .html_url + "\n\n**Description:**\n" + .body end' 2>/dev/null || echo "❌ Issue #$(NUM) not found or API error"; \
fi
@echo ""
@echo "💡 Tip: Use 'make list-issues' to see all issues"
@python3 tddai_cli.py show-issue $(NUM)
# List only open issues (active backlog)
list-open-issues:
@echo "📋 Open MarkiTect Issues (Active Backlog)"
@echo "========================================"
@echo ""
@if ! command -v curl >/dev/null 2>&1; then \
echo "❌ curl not found - required for API access"; \
exit 1; \
fi
@if ! command -v jq >/dev/null 2>&1; then \
echo "⚠️ jq not found - using basic formatting"; \
curl -s "$(ISSUES_API)?state=open" | head -20; \
else \
curl -s "$(ISSUES_API)?state=open" | jq -r '.[] | "[OPEN] #\(.number): \(.title)\n Created: \(.created_at[:10]) | Updated: \(.updated_at[:10])\n \(.body[:80])...\n"' | head -40; \
fi
@echo ""
@echo "💡 Tip: Use 'make show-issue NUM=X' for full details or 'make list-issues' for all issues"
@python3 tddai_cli.py list-open-issues
# Generate test skeleton from gitea issue (requires Claude Code)
test-from-issue:
@@ -316,142 +273,16 @@ start-issue:
echo "❌ Please specify issue number: make start-issue NUM=1"; \
exit 1; \
fi
@echo "🔍 Starting work on issue #$(NUM)..."
@if [ -f "$(CURRENT_ISSUE_FILE)" ]; then \
CURRENT=$$(cat "$(CURRENT_ISSUE_FILE)" | jq -r '.number // "unknown"'); \
echo "⚠️ Already working on issue #$$CURRENT"; \
echo " Run 'make finish-issue' first or 'make workspace-status' to see details"; \
exit 1; \
fi
@if ! command -v curl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then \
echo "❌ curl and jq required for workspace management"; \
exit 1; \
fi
@echo "📋 Fetching issue #$(NUM) details..."
@ISSUE_DATA=$$(curl -s "$(ISSUES_API)/$(NUM)" 2>/dev/null); \
if echo "$$ISSUE_DATA" | jq -e '.title' >/dev/null 2>&1; then \
mkdir -p "$(WORKSPACE_DIR)/issue_$(NUM)/tests"; \
echo "$$ISSUE_DATA" | jq '{number: .number, title: .title, body: .body, state: .state, created_at: .created_at, html_url: .html_url}' > "$(CURRENT_ISSUE_FILE)"; \
echo "$$ISSUE_DATA" | jq -r '"# Issue #" + (.number | tostring) + ": " + .title + "\n\n## Description\n" + .body + "\n\n## Requirements Breakdown\n\n- [ ] TODO: Break down requirements into testable scenarios\n- [ ] TODO: Identify edge cases\n- [ ] TODO: Define acceptance criteria\n\n## Test Plan\n\n- [ ] TODO: List specific test scenarios to implement\n"' > "$(WORKSPACE_DIR)/issue_$(NUM)/requirements.md"; \
echo "# Test Plan for Issue #$(NUM)" > "$(WORKSPACE_DIR)/issue_$(NUM)/test_plan.md"; \
echo "" >> "$(WORKSPACE_DIR)/issue_$(NUM)/test_plan.md"; \
echo "## Test Scenarios" >> "$(WORKSPACE_DIR)/issue_$(NUM)/test_plan.md"; \
echo "" >> "$(WORKSPACE_DIR)/issue_$(NUM)/test_plan.md"; \
echo "- [ ] TODO: Add specific test scenarios" >> "$(WORKSPACE_DIR)/issue_$(NUM)/test_plan.md"; \
echo "✅ Workspace created for issue #$(NUM)"; \
echo "📁 Workspace: $(WORKSPACE_DIR)/issue_$(NUM)/"; \
echo "📋 Requirements: $(WORKSPACE_DIR)/issue_$(NUM)/requirements.md"; \
echo "🧪 Test plan: $(WORKSPACE_DIR)/issue_$(NUM)/test_plan.md"; \
echo ""; \
echo "💡 Next steps:"; \
echo " 1. Review requirements.md and break down the issue"; \
echo " 2. Plan test scenarios in test_plan.md"; \
echo " 3. Use 'make add-test' to generate tests"; \
echo " 4. Use 'make finish-issue' when complete"; \
else \
echo "❌ Issue #$(NUM) not found or API error"; \
echo " Use 'make list-open-issues' to see available issues"; \
fi
@python3 tddai_cli.py start-issue $(NUM)
# Add test to current issue workspace
add-test:
@if [ ! -f "$(CURRENT_ISSUE_FILE)" ]; then \
echo "❌ No active issue workspace"; \
echo " Run 'make start-issue NUM=X' first"; \
exit 1; \
fi
@if ! command -v claude >/dev/null 2>&1; then \
echo "❌ Claude Code not found - required for test generation"; \
exit 1; \
fi
@CURRENT_ISSUE=$$(cat "$(CURRENT_ISSUE_FILE)" | jq -r '.number'); \
ISSUE_TITLE=$$(cat "$(CURRENT_ISSUE_FILE)" | jq -r '.title'); \
ISSUE_BODY=$$(cat "$(CURRENT_ISSUE_FILE)" | jq -r '.body'); \
echo "🧪 Adding test to issue #$$CURRENT_ISSUE workspace"; \
echo ""; \
echo "📋 Issue: $$ISSUE_TITLE"; \
echo "📁 Workspace: $(WORKSPACE_DIR)/issue_$$CURRENT_ISSUE/"; \
echo ""; \
echo "🤖 Please ask Claude Code to generate a test:"; \
echo ""; \
echo " Command: 'Generate a test for the current workspace issue'"; \
echo ""; \
echo "📝 Test Requirements:"; \
echo " - Save test in: $(WORKSPACE_DIR)/issue_$$CURRENT_ISSUE/tests/"; \
echo " - Name format: test_issue_$$CURRENT_ISSUE_<scenario>.py"; \
echo " - Include docstring referencing issue #$$CURRENT_ISSUE"; \
echo " - Follow TDD principles (test should fail initially)"; \
echo " - Review requirements.md and test_plan.md for context"; \
echo ""; \
echo "📋 Issue Details:"; \
echo " Title: $$ISSUE_TITLE"; \
echo " Description: $$ISSUE_BODY"; \
echo ""; \
echo "💡 After generation: Use 'make workspace-status' to see all tests"
@python3 tddai_cli.py add-test
# Show current workspace status
workspace-status:
@if [ ! -f "$(CURRENT_ISSUE_FILE)" ]; then \
echo "📋 No active issue workspace"; \
echo " Use 'make start-issue NUM=X' to begin working on an issue"; \
exit 0; \
fi
@CURRENT_ISSUE=$$(cat "$(CURRENT_ISSUE_FILE)" | jq -r '.number'); \
ISSUE_TITLE=$$(cat "$(CURRENT_ISSUE_FILE)" | jq -r '.title'); \
ISSUE_STATE=$$(cat "$(CURRENT_ISSUE_FILE)" | jq -r '.state'); \
echo "📋 Active Issue Workspace"; \
echo "========================"; \
echo ""; \
echo "🎯 Issue #$$CURRENT_ISSUE: $$ISSUE_TITLE"; \
echo "📊 Status: $$ISSUE_STATE"; \
echo "📁 Workspace: $(WORKSPACE_DIR)/issue_$$CURRENT_ISSUE/"; \
echo ""; \
if [ -d "$(WORKSPACE_DIR)/issue_$$CURRENT_ISSUE/tests" ]; then \
TEST_COUNT=$$(find "$(WORKSPACE_DIR)/issue_$$CURRENT_ISSUE/tests" -name "*.py" | wc -l); \
echo "🧪 Generated Tests ($$TEST_COUNT):"; \
if [ $$TEST_COUNT -gt 0 ]; then \
find "$(WORKSPACE_DIR)/issue_$$CURRENT_ISSUE/tests" -name "*.py" -exec basename {} \; | sed 's/^/ - /'; \
else \
echo " - No tests generated yet"; \
fi; \
echo ""; \
fi; \
echo "📋 Workspace Files:"; \
echo " - requirements.md (review and break down issue)"; \
echo " - test_plan.md (plan test scenarios)"; \
echo " - tests/ (generated test files)"; \
echo ""; \
echo "💡 Commands:"; \
echo " - make add-test (generate another test)"; \
echo " - make finish-issue (complete and move tests to main)"
@python3 tddai_cli.py workspace-status
# Complete issue work (move tests to main and cleanup)
finish-issue:
@if [ ! -f "$(CURRENT_ISSUE_FILE)" ]; then \
echo "❌ No active issue workspace"; \
echo " Nothing to finish"; \
exit 1; \
fi
@CURRENT_ISSUE=$$(cat "$(CURRENT_ISSUE_FILE)" | jq -r '.number'); \
ISSUE_TITLE=$$(cat "$(CURRENT_ISSUE_FILE)" | jq -r '.title'); \
echo "🏁 Finishing work on issue #$$CURRENT_ISSUE"; \
echo ""; \
if [ -d "$(WORKSPACE_DIR)/issue_$$CURRENT_ISSUE/tests" ]; then \
TEST_COUNT=$$(find "$(WORKSPACE_DIR)/issue_$$CURRENT_ISSUE/tests" -name "*.py" | wc -l); \
if [ $$TEST_COUNT -gt 0 ]; then \
echo "📦 Moving $$TEST_COUNT test(s) to tests/ directory..."; \
cp $(WORKSPACE_DIR)/issue_$$CURRENT_ISSUE/tests/*.py tests/ 2>/dev/null || echo " No .py files to move"; \
echo "✅ Tests moved to main tests/ directory"; \
else \
echo "⚠️ No tests found in workspace"; \
fi; \
fi; \
echo "🧹 Cleaning up workspace..."; \
rm -rf "$(WORKSPACE_DIR)/issue_$$CURRENT_ISSUE"; \
rm -f "$(CURRENT_ISSUE_FILE)"; \
echo "✅ Issue #$$CURRENT_ISSUE workspace cleaned up"; \
echo ""; \
echo "💡 Next steps:"; \
echo " - Run 'make test' to verify tests fail (red state)"; \
echo " - Implement code to make tests pass (green state)"; \
echo " - Start next issue with 'make start-issue NUM=X'"
@python3 tddai_cli.py finish-issue

26
tddai/__init__.py Normal file
View File

@@ -0,0 +1,26 @@
"""
tddai - Test-Driven Development with AI Support
A Python library for managing issue-driven TDD workflows with AI assistance.
Provides workspace management, test generation, and issue integration.
"""
from .workspace import WorkspaceManager, Workspace, WorkspaceStatus
from .issue_fetcher import IssueFetcher, Issue
from .test_generator import TestGenerator
from .exceptions import TddaiError, WorkspaceError, IssueError, ConfigurationError, TestGenerationError
__version__ = "0.1.0"
__all__ = [
"WorkspaceManager",
"Workspace",
"WorkspaceStatus",
"IssueFetcher",
"Issue",
"TestGenerator",
"TddaiError",
"WorkspaceError",
"IssueError",
"ConfigurationError",
"TestGenerationError",
]

92
tddai/config.py Normal file
View File

@@ -0,0 +1,92 @@
"""
Configuration management for tddai.
"""
import os
from pathlib import Path
from typing import Optional
from dataclasses import dataclass
from .exceptions import ConfigurationError
@dataclass
class TddaiConfig:
"""Configuration settings for tddai."""
# Workspace settings
workspace_dir: Path = Path(".markitect_workspace")
current_issue_file: str = "current_issue.json"
# Git repository settings
gitea_url: str = "http://92.205.130.254:32166"
repo_owner: str = "coulomb"
repo_name: str = "markitect_project"
# Test settings
tests_dir: Path = Path("tests")
test_file_pattern: str = "test_issue_{issue_num}_{scenario}.py"
# AI settings
claude_code_command: str = "claude"
@property
def issues_api_url(self) -> str:
"""Get the full issues API URL."""
return f"{self.gitea_url}/api/v1/repos/{self.repo_owner}/{self.repo_name}/issues"
@property
def current_issue_path(self) -> Path:
"""Get the path to current issue file."""
return self.workspace_dir / self.current_issue_file
@classmethod
def from_environment(cls) -> "TddaiConfig":
"""Create config from environment variables."""
config = cls()
# Override with environment variables if present
if gitea_url := os.getenv("TDDAI_GITEA_URL"):
config.gitea_url = gitea_url
if repo_owner := os.getenv("TDDAI_REPO_OWNER"):
config.repo_owner = repo_owner
if repo_name := os.getenv("TDDAI_REPO_NAME"):
config.repo_name = repo_name
if workspace_dir := os.getenv("TDDAI_WORKSPACE_DIR"):
config.workspace_dir = Path(workspace_dir)
return config
def validate(self) -> None:
"""Validate configuration settings."""
if not self.gitea_url:
raise ConfigurationError("gitea_url cannot be empty")
if not self.repo_owner:
raise ConfigurationError("repo_owner cannot be empty")
if not self.repo_name:
raise ConfigurationError("repo_name cannot be empty")
# Global config instance
_config: Optional[TddaiConfig] = None
def get_config() -> TddaiConfig:
"""Get the global configuration instance."""
global _config
if _config is None:
_config = TddaiConfig.from_environment()
_config.validate()
return _config
def set_config(config: TddaiConfig) -> None:
"""Set the global configuration instance."""
global _config
config.validate()
_config = config

28
tddai/exceptions.py Normal file
View File

@@ -0,0 +1,28 @@
"""
Custom exceptions for tddai library.
"""
class TddaiError(Exception):
"""Base exception for all tddai errors."""
pass
class WorkspaceError(TddaiError):
"""Raised when workspace operations fail."""
pass
class IssueError(TddaiError):
"""Raised when issue operations fail."""
pass
class ConfigurationError(TddaiError):
"""Raised when configuration is invalid."""
pass
class TestGenerationError(TddaiError):
"""Raised when test generation fails."""
pass

136
tddai/issue_fetcher.py Normal file
View File

@@ -0,0 +1,136 @@
"""
Issue fetching from Gitea API.
"""
import json
import subprocess
from dataclasses import dataclass
from datetime import datetime
from typing import List, Optional, Dict, Any
from .config import get_config
from .exceptions import IssueError
@dataclass
class Issue:
"""Represents a Gitea issue."""
number: int
title: str
body: str
state: str
created_at: datetime
updated_at: datetime
html_url: str
assignee: Optional[str] = None
labels: List[str] = None
def __post_init__(self):
if self.labels is None:
self.labels = []
class IssueFetcher:
"""Fetches issues from Gitea API."""
def __init__(self, config=None):
self.config = config or get_config()
def fetch_issue(self, issue_number: int) -> Issue:
"""Fetch a specific issue by number."""
try:
result = subprocess.run(
['curl', '-s', f"{self.config.issues_api_url}/{issue_number}"],
capture_output=True,
text=True,
check=True
)
if result.returncode != 0:
raise IssueError(f"Failed to fetch issue #{issue_number}: {result.stderr}")
issue_data = json.loads(result.stdout)
if 'message' in issue_data:
raise IssueError(f"Issue #{issue_number} not found: {issue_data['message']}")
return self._parse_issue(issue_data)
except subprocess.CalledProcessError as e:
raise IssueError(f"Failed to fetch issue #{issue_number}: {e}")
except json.JSONDecodeError as e:
raise IssueError(f"Failed to parse issue data: {e}")
def fetch_issues(self, state: str = "all") -> List[Issue]:
"""Fetch all issues with optional state filter."""
try:
url = self.config.issues_api_url
if state != "all":
url += f"?state={state}"
result = subprocess.run(
['curl', '-s', url],
capture_output=True,
text=True,
check=True
)
if result.returncode != 0:
raise IssueError(f"Failed to fetch issues: {result.stderr}")
issues_data = json.loads(result.stdout)
if isinstance(issues_data, dict) and 'message' in issues_data:
raise IssueError(f"Failed to fetch issues: {issues_data['message']}")
if not isinstance(issues_data, list):
raise IssueError("Invalid response format: expected list of issues")
return [self._parse_issue(issue_data) for issue_data in issues_data]
except subprocess.CalledProcessError as e:
raise IssueError(f"Failed to fetch issues: {e}")
except json.JSONDecodeError as e:
raise IssueError(f"Failed to parse issues data: {e}")
def fetch_open_issues(self) -> List[Issue]:
"""Fetch only open issues."""
return self.fetch_issues(state="open")
def _parse_issue(self, issue_data: Dict[str, Any]) -> Issue:
"""Parse issue data from API response."""
try:
labels = [label['name'] for label in issue_data.get('labels', [])]
assignee = None
if issue_data.get('assignee'):
assignee = issue_data['assignee'].get('login')
return Issue(
number=issue_data['number'],
title=issue_data['title'],
body=issue_data.get('body', ''),
state=issue_data['state'],
created_at=datetime.fromisoformat(issue_data['created_at'].replace('Z', '+00:00')),
updated_at=datetime.fromisoformat(issue_data['updated_at'].replace('Z', '+00:00')),
html_url=issue_data['html_url'],
assignee=assignee,
labels=labels
)
except (KeyError, ValueError) as e:
raise IssueError(f"Failed to parse issue data: {e}")
def get_issue_data_dict(self, issue_number: int) -> Dict[str, Any]:
"""Get issue data as dictionary for workspace creation."""
issue = self.fetch_issue(issue_number)
return {
'number': issue.number,
'title': issue.title,
'body': issue.body,
'state': issue.state,
'created_at': issue.created_at.isoformat(),
'updated_at': issue.updated_at.isoformat(),
'html_url': issue.html_url,
'assignee': {'login': issue.assignee} if issue.assignee else None,
'labels': [{'name': label} for label in issue.labels]
}

163
tddai/test_generator.py Normal file
View File

@@ -0,0 +1,163 @@
"""
Test generation with AI assistance.
"""
import subprocess
import tempfile
from pathlib import Path
from typing import Optional
from .config import get_config
from .workspace import WorkspaceManager
from .exceptions import TestGenerationError
class TestGenerator:
"""Generates tests using AI assistance."""
def __init__(self, config=None):
self.config = config or get_config()
self.workspace_manager = WorkspaceManager(config)
def generate_test(self, scenario_name: str, test_description: str) -> Path:
"""Generate a test file for the current workspace issue."""
workspace = self.workspace_manager.get_current_workspace()
if not workspace:
raise TestGenerationError("No active workspace found")
# Create test file name
test_filename = self.config.test_file_pattern.format(
issue_num=workspace.issue_number,
scenario=scenario_name.lower().replace(' ', '_').replace('-', '_')
)
test_file_path = workspace.tests_dir / test_filename
# Generate test prompt
prompt = self._create_test_prompt(workspace, scenario_name, test_description)
# Use Claude Code to generate the test
try:
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
f.write(prompt)
prompt_file = Path(f.name)
result = subprocess.run(
[self.config.claude_code_command, '--file', str(prompt_file)],
cwd=workspace.workspace_dir,
capture_output=True,
text=True
)
prompt_file.unlink() # Clean up temp file
if result.returncode != 0:
raise TestGenerationError(f"Claude Code failed: {result.stderr}")
# Extract Python code from Claude's response
test_content = self._extract_test_code(result.stdout)
# Write test file
test_file_path.write_text(test_content)
# Update test plan
self._update_test_plan(workspace, scenario_name, test_filename)
return test_file_path
except subprocess.CalledProcessError as e:
raise TestGenerationError(f"Failed to generate test: {e}")
except Exception as e:
raise TestGenerationError(f"Test generation error: {e}")
def _create_test_prompt(self, workspace, scenario_name: str, test_description: str) -> str:
"""Create prompt for Claude Code to generate test."""
return f"""# Test Generation Request
## Context
- Issue #{workspace.issue_number}: {workspace.issue_title}
- Scenario: {scenario_name}
## Issue Description
{workspace.issue_body}
## Test Requirements
{test_description}
## Instructions
Please generate a comprehensive Python test file that:
1. Tests the behavior described in the scenario
2. Follows pytest conventions
3. Includes proper docstrings and comments
4. Tests both positive and negative cases
5. Uses meaningful test method names
6. Includes appropriate assertions
The test should focus on behavior verification rather than implementation details.
## Expected Output
Please provide only the Python test code without any additional explanation.
The code should be ready to save as `{self.config.test_file_pattern.format(issue_num=workspace.issue_number, scenario=scenario_name.lower().replace(' ', '_'))}`
"""
def _extract_test_code(self, claude_response: str) -> str:
"""Extract Python test code from Claude's response."""
lines = claude_response.split('\n')
code_lines = []
in_code_block = False
for line in lines:
if line.strip().startswith('```python'):
in_code_block = True
continue
elif line.strip() == '```' and in_code_block:
break
elif in_code_block:
code_lines.append(line)
if not code_lines:
# If no code block found, assume entire response is code
return claude_response.strip()
return '\n'.join(code_lines)
def _update_test_plan(self, workspace, scenario_name: str, test_filename: str) -> None:
"""Update the test plan with the new test."""
test_plan_content = workspace.test_plan_file.read_text()
# Add test to the generated tests section
new_entry = f"- [x] {scenario_name} (`{test_filename}`)"
if "### Generated Tests" in test_plan_content:
# Add to existing generated tests section
lines = test_plan_content.split('\n')
for i, line in enumerate(lines):
if line.strip() == "Tests generated for this workspace will be listed here as they are created.":
lines[i] = new_entry
break
elif line.startswith("- [") and "Generated Tests" in lines[max(0, i-5):i]:
lines.insert(i, new_entry)
break
else:
# Add at the end of generated tests section
for i, line in enumerate(lines):
if "### Generated Tests" in line:
# Find next section or end
j = i + 1
while j < len(lines) and not lines[j].startswith('##'):
j += 1
lines.insert(j, new_entry)
break
workspace.test_plan_file.write_text('\n'.join(lines))
def list_generated_tests(self) -> list:
"""List all generated tests for the current workspace."""
workspace = self.workspace_manager.get_current_workspace()
if not workspace:
return []
if not workspace.tests_dir.exists():
return []
return list(workspace.tests_dir.glob("*.py"))

219
tddai/workspace.py Normal file
View File

@@ -0,0 +1,219 @@
"""
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)

331
tddai_cli.py Normal file
View File

@@ -0,0 +1,331 @@
#!/usr/bin/env python3
"""
CLI interface for tddai library.
"""
import sys
import argparse
from pathlib import Path
# Add current directory to path so we can import tddai
sys.path.insert(0, str(Path(__file__).parent))
from tddai import (
WorkspaceManager, IssueFetcher, TestGenerator,
WorkspaceStatus, TddaiError
)
def workspace_status():
"""Show current workspace status."""
try:
manager = WorkspaceManager()
status = manager.get_status()
if status == WorkspaceStatus.CLEAN:
print("📋 No active issue workspace")
print(" Use 'make start-issue NUM=X' to begin working on an issue")
return
if status == WorkspaceStatus.DIRTY:
print("⚠️ Workspace directory exists but no current issue file")
print(" Run 'make finish-issue' to clean up or 'make start-issue' to create new workspace")
return
workspace = manager.get_current_workspace()
if not workspace:
print("❌ Failed to load workspace")
return
print("📋 Active Issue Workspace")
print("========================")
print()
print(f"🎯 Issue #{workspace.issue_number}: {workspace.issue_title}")
print(f"📊 Status: {workspace.issue_state}")
print(f"📁 Workspace: {workspace.workspace_dir}/issue_{workspace.issue_number}/")
print()
if workspace.tests_dir.exists():
test_files = list(workspace.tests_dir.glob("*.py"))
print(f"🧪 Generated Tests ({len(test_files)}):")
if test_files:
for test_file in test_files:
print(f" - {test_file.name}")
else:
print(" - No tests generated yet")
print()
print("📋 Workspace Files:")
print(" - requirements.md (review and break down issue)")
print(" - test_plan.md (plan test scenarios)")
print(" - tests/ (generated test files)")
print()
print("💡 Commands:")
print(" - make add-test (generate another test)")
print(" - make finish-issue (complete and move tests to main)")
except TddaiError as e:
print(f"❌ Error: {e}")
sys.exit(1)
def start_issue(issue_number: int):
"""Start working on an issue."""
try:
manager = WorkspaceManager()
fetcher = IssueFetcher()
# Check if workspace already active
status = manager.get_status()
if status == WorkspaceStatus.ACTIVE:
current = manager.get_current_workspace()
print(f"⚠️ Already working on issue #{current.issue_number}")
print(" Run 'make finish-issue' first or 'make workspace-status' to see details")
sys.exit(1)
print(f"🔍 Starting work on issue #{issue_number}...")
print(f"📋 Fetching issue #{issue_number} details...")
# Fetch issue data
issue_data = fetcher.get_issue_data_dict(issue_number)
# Create workspace
workspace = manager.create_workspace(issue_data)
print(f"✅ Workspace created for issue #{issue_number}")
print(f"📁 Workspace: {workspace.workspace_dir}/issue_{issue_number}/")
print(f"📋 Requirements: {workspace.requirements_file}")
print(f"🧪 Test plan: {workspace.test_plan_file}")
print()
print("💡 Next steps:")
print(" 1. Review requirements.md and break down the issue")
print(" 2. Plan test scenarios in test_plan.md")
print(" 3. Use 'make add-test' to generate tests")
print(" 4. Use 'make finish-issue' when complete")
except TddaiError as e:
print(f"❌ Error: {e}")
sys.exit(1)
def finish_issue():
"""Finish current issue workspace."""
try:
manager = WorkspaceManager()
workspace = manager.get_current_workspace()
if not workspace:
print("❌ No active issue workspace")
print(" Nothing to finish")
sys.exit(1)
print(f"🏁 Finishing work on issue #{workspace.issue_number}")
print()
# Check for tests
if workspace.tests_dir.exists():
test_files = list(workspace.tests_dir.glob("*.py"))
if test_files:
print(f"📦 Moving {len(test_files)} test(s) to tests/ directory...")
print("✅ Tests moved to main tests/ directory")
else:
print("⚠️ No tests found in workspace")
# Finish workspace (moves tests and cleans up)
manager.finish_workspace()
print("🧹 Cleaning up workspace...")
print(f"✅ Issue #{workspace.issue_number} workspace cleaned up")
print()
print("💡 Next steps:")
print(" - Run 'make test' to verify tests fail (red state)")
print(" - Implement code to make tests pass (green state)")
print(" - Start next issue with 'make start-issue NUM=X'")
except TddaiError as e:
print(f"❌ Error: {e}")
sys.exit(1)
def add_test_guidance():
"""Show guidance for adding tests."""
try:
manager = WorkspaceManager()
workspace = manager.get_current_workspace()
if not workspace:
print("❌ No active issue workspace")
print(" Run 'make start-issue NUM=X' first")
sys.exit(1)
print(f"🧪 Adding test to issue #{workspace.issue_number} workspace")
print()
print(f"📋 Issue: {workspace.issue_title}")
print(f"📁 Workspace: {workspace.workspace_dir}/issue_{workspace.issue_number}/")
print()
print("🤖 Please ask Claude Code to generate a test:")
print()
print(" Command: 'Generate a test for the current workspace issue'")
print()
print("📝 Test Requirements:")
print(f" - Save test in: {workspace.tests_dir}/")
print(f" - Name format: test_issue_{workspace.issue_number}_<scenario>.py")
print(f" - Include docstring referencing issue #{workspace.issue_number}")
print(" - Follow TDD principles (test should fail initially)")
print(" - Review requirements.md and test_plan.md for context")
print()
print("📋 Issue Details:")
print(f" Title: {workspace.issue_title}")
print(f" Description: {workspace.issue_body}")
print()
print("💡 After generation: Use 'make workspace-status' to see all tests")
except TddaiError as e:
print(f"❌ Error: {e}")
sys.exit(1)
def list_issues():
"""List all issues."""
try:
fetcher = IssueFetcher()
print("📋 MarkiTect Issues")
print("==================")
print()
issues = fetcher.fetch_issues()
if not issues:
print("No issues found")
return
for issue in issues:
status_icon = "🟢" if issue.state == "open" else "🔴"
print(f"{status_icon} #{issue.number}: {issue.title}")
print(f" Status: {issue.state.upper()} | Created: {issue.created_at.strftime('%Y-%m-%d')}")
# Truncate body for list view
body_preview = issue.body[:80] + "..." if len(issue.body) > 80 else issue.body
if body_preview:
print(f" {body_preview}")
print()
print("💡 Tip: Use 'make show-issue NUM=X' for full details")
except TddaiError as e:
print(f"❌ Error: {e}")
sys.exit(1)
def list_open_issues():
"""List only open issues."""
try:
fetcher = IssueFetcher()
print("📋 Open MarkiTect Issues (Active Backlog)")
print("========================================")
print()
issues = fetcher.fetch_open_issues()
if not issues:
print("No open issues found")
return
for issue in issues:
print(f"[OPEN] #{issue.number}: {issue.title}")
print(f" Created: {issue.created_at.strftime('%Y-%m-%d')} | Updated: {issue.updated_at.strftime('%Y-%m-%d')}")
# Truncate body for list view
body_preview = issue.body[:80] + "..." if len(issue.body) > 80 else issue.body
if body_preview:
print(f" {body_preview}")
print()
print("💡 Tip: Use 'make show-issue NUM=X' for full details or 'make list-issues' for all issues")
except TddaiError as e:
print(f"❌ Error: {e}")
sys.exit(1)
def show_issue(issue_number: int):
"""Show detailed issue information."""
try:
fetcher = IssueFetcher()
print(f"🔍 Issue #{issue_number} Details")
print("=======================")
print()
issue = fetcher.fetch_issue(issue_number)
print(f"**Title:** {issue.title}")
print(f"**Status:** {issue.state.upper()}")
print(f"**Number:** #{issue.number}")
print(f"**Created:** {issue.created_at.strftime('%Y-%m-%d %H:%M')}")
print(f"**Updated:** {issue.updated_at.strftime('%Y-%m-%d %H:%M')}")
print(f"**URL:** {issue.html_url}")
if issue.assignee:
print(f"**Assignee:** {issue.assignee}")
if issue.labels:
print(f"**Labels:** {', '.join(issue.labels)}")
print()
print("**Description:**")
print(issue.body)
print()
print("💡 Tip: Use 'make list-issues' to see all issues")
except TddaiError as e:
print(f"❌ Error: {e}")
sys.exit(1)
def main():
"""Main CLI entry point."""
parser = argparse.ArgumentParser(description="tddai CLI tool")
subparsers = parser.add_subparsers(dest='command', help='Available commands')
# Workspace commands
subparsers.add_parser('workspace-status', help='Show workspace status')
start_parser = subparsers.add_parser('start-issue', help='Start working on issue')
start_parser.add_argument('issue_number', type=int, help='Issue number')
subparsers.add_parser('finish-issue', help='Finish current issue')
subparsers.add_parser('add-test', help='Show guidance for adding tests')
# Issue commands
subparsers.add_parser('list-issues', help='List all issues')
subparsers.add_parser('list-open-issues', help='List open issues')
show_parser = subparsers.add_parser('show-issue', help='Show issue details')
show_parser.add_argument('issue_number', type=int, help='Issue number')
args = parser.parse_args()
if not args.command:
parser.print_help()
return
try:
if args.command == 'workspace-status':
workspace_status()
elif args.command == 'start-issue':
start_issue(args.issue_number)
elif args.command == 'finish-issue':
finish_issue()
elif args.command == 'add-test':
add_test_guidance()
elif args.command == 'list-issues':
list_issues()
elif args.command == 'list-open-issues':
list_open_issues()
elif args.command == 'show-issue':
show_issue(args.issue_number)
except KeyboardInterrupt:
print("\n⚠️ Operation cancelled")
sys.exit(1)
if __name__ == '__main__':
main()

1
tests/test_example.py Normal file
View File

@@ -0,0 +1 @@
# Test content

View File

@@ -0,0 +1 @@
# Complete test content

View File

@@ -0,0 +1,6 @@
"""Test for issue #11."""
import pytest
def test_feature():
"""Test the feature implementation."""
assert True # Replace with actual test

View File

@@ -0,0 +1,194 @@
"""
Test TDD workflow integration for workspace infrastructure.
This test validates issue #11: Setup TDD workspace infrastructure
- Tests complete workflow from start to finish
- Validates integration between workspace and main codebase
- Tests cleanup and finalization processes
"""
import pytest
import subprocess
import tempfile
import shutil
from pathlib import Path
from unittest.mock import patch
from tddai import WorkspaceManager, IssueFetcher
from tddai.config import TddaiConfig
class TestWorkflowIntegration:
"""Test suite for complete TDD workflow integration."""
@pytest.fixture
def temp_workspace(self):
"""Create a temporary workspace for testing."""
temp_dir = Path(tempfile.mkdtemp())
config = TddaiConfig(workspace_dir=temp_dir / ".markitect_workspace")
yield config
shutil.rmtree(temp_dir)
def test_make_workspace_status_command(self):
"""Test that make workspace-status command works correctly."""
result = subprocess.run(['make', 'workspace-status'],
capture_output=True, text=True)
assert result.returncode == 0, "workspace-status command should succeed"
# Should show clean workspace when no active workspace
assert ("No active issue workspace" in result.stdout or
"Workspace directory exists but no current issue file" in result.stdout)
def test_make_add_test_command_without_workspace(self):
"""Test that make add-test provides proper error when no workspace."""
result = subprocess.run(['make', 'add-test'],
capture_output=True, text=True)
assert result.returncode != 0, "add-test command should fail when no workspace"
assert "No active issue workspace" in result.stdout
def test_cli_integration_basic(self):
"""Test that CLI script can be imported and basic functions exist."""
import tddai_cli
# Test that main functions exist
assert hasattr(tddai_cli, 'workspace_status')
assert hasattr(tddai_cli, 'start_issue')
assert hasattr(tddai_cli, 'finish_issue')
assert hasattr(tddai_cli, 'main')
def test_workspace_to_main_integration(self, temp_workspace):
"""Test moving tests from workspace to main tests directory."""
manager = WorkspaceManager(temp_workspace)
mock_issue_data = {
'number': 11,
'title': 'Test Issue',
'body': 'Test Description',
'state': 'open',
'created_at': '2025-01-01T00:00:00Z',
'html_url': 'http://example.com/issues/11',
'assignee': None,
'labels': []
}
# Create workspace
workspace = manager.create_workspace(mock_issue_data)
# Add a test file to workspace
test_file = workspace.tests_dir / "test_issue_11_feature.py"
test_content = '''"""Test for issue #11."""
import pytest
def test_feature():
"""Test the feature implementation."""
assert True # Replace with actual test
'''
test_file.write_text(test_content)
# Finish workspace (should move tests)
finished_workspace = manager.finish_workspace()
# Verify test was moved to main tests directory
main_test_file = temp_workspace.tests_dir / "test_issue_11_feature.py"
assert main_test_file.exists()
assert main_test_file.read_text() == test_content
# Verify workspace is cleaned up
assert not temp_workspace.workspace_dir.exists()
def test_workspace_cleanup_process(self, temp_workspace):
"""Test that workspace cleanup removes temporary files."""
manager = WorkspaceManager(temp_workspace)
mock_issue_data = {
'number': 11,
'title': 'Test Issue',
'body': 'Test Description',
'state': 'open'
}
# Create workspace
workspace = manager.create_workspace(mock_issue_data)
# Verify workspace exists
assert workspace.workspace_dir.exists()
assert workspace.issue_dir.exists()
# Clean up
manager.cleanup_workspace()
# Verify cleanup
assert not workspace.workspace_dir.exists()
def test_gitignore_excludes_workspace(self):
"""Test that workspace files are properly excluded from git."""
gitignore_path = Path(".gitignore")
assert gitignore_path.exists(), "Gitignore file should exist"
with open(gitignore_path, 'r') as f:
gitignore_content = f.read()
assert ".markitect_workspace/" in gitignore_content, \
"Workspace should be excluded from git"
@patch('tddai.issue_fetcher.subprocess.run')
def test_issue_fetcher_integration(self, mock_run, temp_workspace):
"""Test that IssueFetcher properly integrates with API."""
# Mock successful curl response
mock_run.return_value.returncode = 0
mock_run.return_value.stdout = """{
"number": 11,
"title": "Setup TDD workspace infrastructure",
"body": "Create workspace management system",
"state": "open",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z",
"html_url": "http://example.com/issues/11",
"assignee": null,
"labels": []
}"""
fetcher = IssueFetcher(temp_workspace)
issue = fetcher.fetch_issue(11)
assert issue.number == 11
assert issue.title == "Setup TDD workspace infrastructure"
assert issue.state == "open"
def test_complete_workflow_cycle(self, temp_workspace):
"""Test complete workflow from start to finish."""
manager = WorkspaceManager(temp_workspace)
mock_issue_data = {
'number': 11,
'title': 'Setup TDD workspace infrastructure',
'body': 'Create workspace management system for TDD workflow',
'state': 'open',
'created_at': '2025-01-01T00:00:00Z',
'html_url': 'http://example.com/issues/11',
'assignee': None,
'labels': []
}
# 1. Start with clean workspace
assert manager.get_status().name == "CLEAN"
# 2. Create workspace
workspace = manager.create_workspace(mock_issue_data)
assert manager.get_status().name == "ACTIVE"
assert workspace.issue_number == 11
# 3. Add test files
test_file = workspace.tests_dir / "test_issue_11_complete.py"
test_file.write_text("# Complete test content")
# 4. Finish workspace
finished = manager.finish_workspace()
assert finished.issue_number == 11
assert manager.get_status().name == "CLEAN"
# 5. Verify test moved to main
main_test = temp_workspace.tests_dir / "test_issue_11_complete.py"
assert main_test.exists()
assert main_test.read_text() == "# Complete test content"

View File

@@ -0,0 +1,156 @@
"""
Test workspace creation functionality for TDD infrastructure.
This test validates issue #11: Setup TDD workspace infrastructure
- Tests workspace creation from issue numbers
- Validates workspace structure and files
- Ensures proper error handling
"""
import pytest
import tempfile
import shutil
from pathlib import Path
from unittest.mock import Mock, patch
from tddai import WorkspaceManager, IssueFetcher, WorkspaceStatus, WorkspaceError, IssueError
from tddai.config import TddaiConfig
class TestWorkspaceCreation:
"""Test suite for workspace creation functionality."""
@pytest.fixture
def temp_workspace(self):
"""Create a temporary workspace for testing."""
temp_dir = Path(tempfile.mkdtemp())
config = TddaiConfig(workspace_dir=temp_dir / ".markitect_workspace")
yield config
shutil.rmtree(temp_dir)
@pytest.fixture
def mock_issue_data(self):
"""Mock issue data for testing."""
return {
'number': 11,
'title': 'Setup TDD workspace infrastructure',
'body': 'Create workspace management system for TDD workflow',
'state': 'open',
'created_at': '2025-01-01T00:00:00Z',
'html_url': 'http://example.com/issues/11',
'assignee': None,
'labels': []
}
def test_workspace_manager_initialization(self, temp_workspace):
"""Test that WorkspaceManager can be initialized."""
manager = WorkspaceManager(temp_workspace)
assert manager.config == temp_workspace
def test_workspace_status_clean_initially(self, temp_workspace):
"""Test that workspace status is clean when no workspace exists."""
manager = WorkspaceManager(temp_workspace)
status = manager.get_status()
assert status == WorkspaceStatus.CLEAN
def test_workspace_creation_from_issue_data(self, temp_workspace, mock_issue_data):
"""Test that workspace can be created from issue data."""
manager = WorkspaceManager(temp_workspace)
workspace = manager.create_workspace(mock_issue_data)
assert workspace.issue_number == 11
assert workspace.issue_title == 'Setup TDD workspace infrastructure'
assert workspace.workspace_dir == temp_workspace.workspace_dir
# Verify workspace status changes to active
status = manager.get_status()
assert status == WorkspaceStatus.ACTIVE
def test_workspace_directory_structure_created(self, temp_workspace, mock_issue_data):
"""Test that workspace creates proper directory structure."""
manager = WorkspaceManager(temp_workspace)
workspace = manager.create_workspace(mock_issue_data)
assert workspace.workspace_dir.exists()
assert workspace.issue_dir.exists()
assert workspace.tests_dir.exists()
def test_workspace_metadata_files_created(self, temp_workspace, mock_issue_data):
"""Test that workspace creates required metadata files."""
manager = WorkspaceManager(temp_workspace)
workspace = manager.create_workspace(mock_issue_data)
assert workspace.requirements_file.exists()
assert workspace.test_plan_file.exists()
assert temp_workspace.current_issue_path.exists()
def test_current_issue_metadata_content(self, temp_workspace, mock_issue_data):
"""Test that current issue metadata is properly stored."""
manager = WorkspaceManager(temp_workspace)
manager.create_workspace(mock_issue_data)
current_workspace = manager.get_current_workspace()
assert current_workspace.issue_number == 11
assert current_workspace.issue_title == 'Setup TDD workspace infrastructure'
assert current_workspace.issue_state == 'open'
def test_workspace_prevents_multiple_active_issues(self, temp_workspace, mock_issue_data):
"""Test that only one workspace can be active at a time."""
manager = WorkspaceManager(temp_workspace)
manager.create_workspace(mock_issue_data)
# Try to create another workspace
second_issue_data = mock_issue_data.copy()
second_issue_data['number'] = 12
second_issue_data['title'] = 'Different issue'
with pytest.raises(WorkspaceError, match="Workspace already active"):
manager.create_workspace(second_issue_data)
@patch('tddai.issue_fetcher.subprocess.run')
def test_issue_fetcher_handles_invalid_issue(self, mock_run, temp_workspace):
"""Test error handling for invalid issue numbers."""
# Mock curl response for non-existent issue
mock_run.return_value.returncode = 0
mock_run.return_value.stdout = '{"message": "404 Not Found"}'
fetcher = IssueFetcher(temp_workspace)
with pytest.raises(IssueError, match="not found"):
fetcher.fetch_issue(999)
def test_workspace_cleanup(self, temp_workspace, mock_issue_data):
"""Test that workspace can be cleaned up properly."""
manager = WorkspaceManager(temp_workspace)
manager.create_workspace(mock_issue_data)
# Verify workspace exists
assert manager.get_status() == WorkspaceStatus.ACTIVE
# Clean up
manager.cleanup_workspace()
# Verify workspace is clean
assert manager.get_status() == WorkspaceStatus.CLEAN
assert not temp_workspace.workspace_dir.exists()
def test_workspace_finish_moves_tests(self, temp_workspace, mock_issue_data):
"""Test that finishing workspace moves tests to main directory."""
manager = WorkspaceManager(temp_workspace)
workspace = manager.create_workspace(mock_issue_data)
# Create a test file in workspace
test_file = workspace.tests_dir / "test_example.py"
test_file.write_text("# Test content")
# Finish workspace
finished_workspace = manager.finish_workspace()
assert finished_workspace.issue_number == 11
assert manager.get_status() == WorkspaceStatus.CLEAN
# Verify test was moved
main_test_file = temp_workspace.tests_dir / "test_example.py"
assert main_test_file.exists()
assert main_test_file.read_text() == "# Test content"