claw-code/.guardrails/scripts/fixture_loader.py

334 lines
9.7 KiB
Python

#!/usr/bin/env python3
"""
Fixture loader utility for test data management.
Provides functions to load, copy, and create test fixtures for TeamManager testing.
"""
import json
import shutil
import tempfile
from pathlib import Path
from typing import Dict, Any, Optional, List
import sys
import os
# Add parent directory to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
FIXTURES_DIR = Path(".teams/fixtures")
class FixtureLoader:
"""Utility class for loading and managing test fixtures."""
AVAILABLE_FIXTURES = [
"minimal-project",
"full-project",
"phase1-complete",
"partial-assignments",
"empty-project",
]
def __init__(self, fixtures_dir: Path = None):
"""
Initialize the fixture loader.
Args:
fixtures_dir: Directory containing fixture files (default: .teams/fixtures)
"""
self.fixtures_dir = fixtures_dir or FIXTURES_DIR
def list_fixtures(self) -> List[str]:
"""List all available fixtures."""
return self.AVAILABLE_FIXTURES.copy()
def load_fixture(self, name: str) -> Dict[str, Any]:
"""
Load a fixture by name.
Args:
name: Fixture name (e.g., "minimal-project")
Returns:
Dict containing the fixture data
Raises:
FileNotFoundError: If fixture doesn't exist
ValueError: If name is invalid
"""
if not name or not isinstance(name, str):
raise ValueError("Fixture name must be a non-empty string")
# Sanitize name to prevent path traversal
safe_name = name.replace("..", "").replace("/", "").replace("\\", "")
fixture_path = self.fixtures_dir / f"{safe_name}.json"
if not fixture_path.exists():
available = ", ".join(self.AVAILABLE_FIXTURES)
raise FileNotFoundError(
f"Fixture '{name}' not found at {fixture_path}. "
f"Available fixtures: {available}"
)
with open(fixture_path, 'r', encoding='utf-8') as f:
return json.load(f)
def load_fixture_to_temp(self, name: str, project_name: Optional[str] = None) -> Path:
"""
Load a fixture to a temporary directory for test isolation.
Args:
name: Fixture name
project_name: Optional new project name (default: use fixture's project_name)
Returns:
Path to the temporary directory containing the fixture
"""
data = self.load_fixture(name)
# Use provided project name or fixture's name
if project_name:
data["project_name"] = project_name
# Create temp directory
temp_dir = Path(tempfile.mkdtemp(prefix=f"fixture_{name}_"))
config_path = temp_dir / f"{data['project_name']}.json"
# Write fixture to temp location
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
return temp_dir
def copy_fixture(self, name: str, dest_dir: Path, new_project_name: Optional[str] = None) -> Path:
"""
Copy a fixture to a destination directory.
Args:
name: Fixture name
dest_dir: Destination directory
new_project_name: Optional new project name
Returns:
Path to the copied fixture file
"""
data = self.load_fixture(name)
# Use new project name if provided
project_name = new_project_name or data["project_name"]
dest_path = dest_dir / f"{project_name}.json"
# Update project name in data
if new_project_name:
data["project_name"] = new_project_name
data["updated_at"] = self._get_timestamp()
# Ensure destination exists
dest_dir.mkdir(parents=True, exist_ok=True)
# Write fixture
with open(dest_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
return dest_path
def create_custom_fixture(self, project_name: str, teams_data: List[Dict],
dest_dir: Optional[Path] = None) -> Path:
"""
Create a custom fixture programmatically.
Args:
project_name: Name for the project
teams_data: List of team data dicts
dest_dir: Optional destination directory (default: fixtures_dir)
Returns:
Path to the created fixture file
"""
fixture_data = {
"project_name": project_name,
"updated_at": self._get_timestamp(),
"teams": teams_data
}
dest_dir = dest_dir or self.fixtures_dir
dest_path = dest_dir / f"{project_name}.json"
# Ensure destination exists
dest_dir.mkdir(parents=True, exist_ok=True)
with open(dest_path, 'w', encoding='utf-8') as f:
json.dump(fixture_data, f, indent=2)
return dest_path
def create_minimal_team(self, team_id: int, name: str, phase: str) -> Dict[str, Any]:
"""
Create a minimal team data structure for custom fixtures.
Args:
team_id: Team ID
name: Team name
phase: Phase name
Returns:
Team data dict
"""
return {
"id": team_id,
"name": name,
"phase": phase,
"description": f"Team {team_id} description",
"status": "not_started",
"started_at": None,
"completed_at": None,
"roles": [],
"exit_criteria": ["Exit criterion 1"]
}
def create_minimal_role(self, name: str, assigned_to: Optional[str] = None) -> Dict[str, Any]:
"""
Create a minimal role data structure.
Args:
name: Role name
assigned_to: Optional assignee name
Returns:
Role data dict
"""
return {
"name": name,
"responsibility": f"{name} responsibilities",
"deliverables": ["Deliverable 1"],
"assigned_to": assigned_to
}
def _get_timestamp(self) -> str:
"""Get current ISO timestamp."""
from datetime import datetime
return datetime.now().isoformat()
def validate_fixture(self, data: Dict[str, Any]) -> List[str]:
"""
Validate fixture data structure.
Args:
data: Fixture data dict
Returns:
List of validation errors (empty if valid)
"""
errors = []
if "project_name" not in data:
errors.append("Missing 'project_name' field")
if "teams" not in data:
errors.append("Missing 'teams' field")
elif not isinstance(data["teams"], list):
errors.append("'teams' must be a list")
else:
team_ids = set()
for i, team in enumerate(data["teams"]):
if "id" not in team:
errors.append(f"Team {i}: Missing 'id' field")
elif team["id"] in team_ids:
errors.append(f"Team {i}: Duplicate team ID {team['id']}")
else:
team_ids.add(team["id"])
if "name" not in team:
errors.append(f"Team {team.get('id', i)}: Missing 'name' field")
if "roles" not in team:
errors.append(f"Team {team.get('id', i)}: Missing 'roles' field")
return errors
def load_fixture(name: str) -> Dict[str, Any]:
"""
Convenience function to load a fixture.
Args:
name: Fixture name
Returns:
Fixture data dict
"""
loader = FixtureLoader()
return loader.load_fixture(name)
def load_fixture_to_temp(name: str, project_name: Optional[str] = None) -> Path:
"""
Convenience function to load a fixture to a temp directory.
Args:
name: Fixture name
project_name: Optional new project name
Returns:
Path to temp directory
"""
loader = FixtureLoader()
return loader.load_fixture_to_temp(name, project_name)
def copy_fixture(name: str, dest_dir: Path, new_project_name: Optional[str] = None) -> Path:
"""
Convenience function to copy a fixture.
Args:
name: Fixture name
dest_dir: Destination directory
new_project_name: Optional new project name
Returns:
Path to copied fixture
"""
loader = FixtureLoader()
return loader.copy_fixture(name, dest_dir, new_project_name)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Fixture loader utility")
parser.add_argument("command", choices=["list", "load", "validate"],
help="Command to execute")
parser.add_argument("--fixture", help="Fixture name")
parser.add_argument("--project-name", help="New project name (for copy)")
args = parser.parse_args()
loader = FixtureLoader()
if args.command == "list":
print("Available fixtures:")
for name in loader.list_fixtures():
print(f" - {name}")
elif args.command == "load":
if not args.fixture:
print("Error: --fixture is required")
sys.exit(1)
data = loader.load_fixture(args.fixture)
print(json.dumps(data, indent=2))
elif args.command == "validate":
if not args.fixture:
print("Error: --fixture is required")
sys.exit(1)
data = loader.load_fixture(args.fixture)
errors = loader.validate_fixture(data)
if errors:
print(f"Validation errors for '{args.fixture}':")
for error in errors:
print(f" - {error}")
sys.exit(1)
else:
print(f"Fixture '{args.fixture}' is valid")