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

563 lines
19 KiB
Python

#!/usr/bin/env python3
"""
End-to-End (E2E) Tests for Team Manager
Tests complete workflows including:
- Full lifecycle: init → assign → list → status → delete
- Phase gate transitions
- Error scenarios
- Multi-phase workflow
- RBAC testing
- Batch operations
"""
import sys
import os
import unittest
import tempfile
import json
import shutil
from pathlib import Path
# Add parent directory to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from scripts.mocks import (
MockTeamManager,
MockLogger,
MockFileSystem,
create_admin_context,
create_team_lead_context,
create_viewer_context,
)
class TestE2EFullWorkflow(unittest.TestCase):
"""E2E: Complete project lifecycle workflow."""
def setUp(self):
"""Set up test fixtures."""
self.admin_ctx = create_admin_context("admin-user")
self.logger = MockLogger("test")
self.fs = MockFileSystem()
self.manager = MockTeamManager(
"e2e-workflow-test",
user_context=self.admin_ctx,
logger=self.logger,
fs=self.fs
)
def tearDown(self):
"""Clean up."""
self.manager.reset()
def test_complete_project_lifecycle(self):
"""
E2E-001: Full workflow: init → assign → start → complete → export → delete
"""
# Step 1: Initialize project
print("\n[E2E-001] Step 1: Initialize project")
result = self.manager.initialize_project()
self.assertTrue(result)
self.assertEqual(len(self.manager.teams), 12)
self.assertTrue(self.logger.has_event("project_initialized"))
# Step 2: Assign roles to Team 1
print("[E2E-001] Step 2: Assign roles to Team 1")
team1 = self.manager.teams[1]
for i, role in enumerate(team1.roles):
result = self.manager.assign_role(1, role.name, f"Person {i+1}")
self.assertTrue(result)
assigned_count = sum(1 for r in team1.roles if r.assigned_to)
self.assertEqual(assigned_count, 4)
# Step 3: Start Team 1
print("[E2E-001] Step 3: Start Team 1")
result = self.manager.start_team(1)
self.assertTrue(result)
self.assertEqual(team1.status, "active")
self.assertIsNotNone(team1.started_at)
# Step 4: Complete Team 1
print("[E2E-001] Step 4: Complete Team 1")
result = self.manager.complete_team(1)
self.assertTrue(result)
self.assertEqual(team1.status, "completed")
self.assertIsNotNone(team1.completed_at)
# Step 5: Check phase status
print("[E2E-001] Step 5: Check Phase 1 status")
phase_status = self.manager.get_phase_status("Phase 1: Strategy, Governance & Planning")
self.assertEqual(phase_status["completed"], 1)
self.assertGreater(phase_status["progress_pct"], 0)
# Step 6: List all teams
print("[E2E-001] Step 6: List teams")
team_list = self.manager.list_teams()
self.assertEqual(len(team_list), 12)
# Step 7: Delete the project
print("[E2E-001] Step 7: Delete project")
result = self.manager.delete_project(confirmed=True)
self.assertTrue(result["success"])
print("[E2E-001] ✓ Complete lifecycle test passed")
class TestE2EPhaseGates(unittest.TestCase):
"""E2E: Phase gate transitions."""
def setUp(self):
self.admin_ctx = create_admin_context()
self.manager = MockTeamManager(
"e2e-phase-test",
user_context=self.admin_ctx,
logger=MockLogger("test"),
fs=MockFileSystem()
)
self.manager.initialize_project()
def test_phase1_completion(self):
"""
E2E-002: Complete all Phase 1 teams and verify progress
"""
print("\n[E2E-002] Testing Phase 1 completion")
# Get Phase 1 teams
phase1_teams = [t for t in self.manager.teams.values()
if t.phase == "Phase 1: Strategy, Governance & Planning"]
self.assertEqual(len(phase1_teams), 3) # Teams 1, 2, 3
# Initially 0% complete
status = self.manager.get_phase_status("Phase 1: Strategy, Governance & Planning")
self.assertEqual(status["progress_pct"], 0.0)
# Complete all Phase 1 teams
for team in phase1_teams:
self.manager.start_team(team.id)
self.manager.complete_team(team.id)
# Now 100% complete
status = self.manager.get_phase_status("Phase 1: Strategy, Governance & Planning")
self.assertEqual(status["progress_pct"], 100.0)
self.assertEqual(status["completed"], 3)
print("[E2E-002] ✓ Phase 1 completion test passed")
def test_phase_progression(self):
"""
E2E-003: Test progression through multiple phases
"""
print("\n[E2E-003] Testing phase progression")
phases = [
"Phase 1: Strategy, Governance & Planning",
"Phase 2: Platform & Foundation",
"Phase 3: The Build Squads",
"Phase 4: Validation & Hardening",
"Phase 5: Delivery & Sustainment",
]
for phase in phases:
phase_teams = [t for t in self.manager.teams.values() if t.phase == phase]
for team in phase_teams:
self.manager.start_team(team.id)
self.manager.complete_team(team.id)
status = self.manager.get_phase_status(phase)
self.assertEqual(status["progress_pct"], 100.0)
print(f"{phase}: 100%")
print("[E2E-003] ✓ Phase progression test passed")
class TestE2EErrorScenarios(unittest.TestCase):
"""E2E: Error scenarios."""
def setUp(self):
self.admin_ctx = create_admin_context()
self.manager = MockTeamManager(
"e2e-error-test",
user_context=self.admin_ctx,
logger=MockLogger("test"),
fs=MockFileSystem()
)
self.manager.initialize_project()
def test_assign_to_invalid_team(self):
"""
E2E-004: Assign to invalid team returns False
"""
print("\n[E2E-004] Testing invalid team assignment")
result = self.manager.assign_role(99, "Some Role", "John Doe")
self.assertFalse(result)
print("[E2E-004] ✓ Invalid team assignment handled correctly")
def test_assign_invalid_role(self):
"""
E2E-005: Assign invalid role returns False
"""
print("\n[E2E-005] Testing invalid role assignment")
result = self.manager.assign_role(1, "Nonexistent Role", "John Doe")
self.assertFalse(result)
print("[E2E-005] ✓ Invalid role assignment handled correctly")
def test_delete_without_confirmation(self):
"""
E2E-006: Delete without confirmation requires confirmation
"""
print("\n[E2E-006] Testing delete without confirmation")
result = self.manager.delete_team(1, confirmed=False)
self.assertFalse(result["success"])
self.assertTrue(result["requires_confirmation"])
print("[E2E-006] ✓ Delete confirmation required correctly")
def test_delete_nonexistent_project(self):
"""
E2E-007: Delete non-existent project fails gracefully
"""
print("\n[E2E-007] Testing delete of non-existent project")
empty_manager = MockTeamManager(
"nonexistent-project",
user_context=self.admin_ctx,
logger=MockLogger("test"),
fs=MockFileSystem()
)
result = empty_manager.delete_project(confirmed=True)
self.assertFalse(result["success"])
print("[E2E-007] ✓ Non-existent project deletion handled correctly")
def test_unauthorized_access(self):
"""
E2E-008: Unauthorized access raises PermissionDenied
"""
print("\n[E2E-008] Testing unauthorized access")
from scripts.team_manager import PermissionDenied
viewer_ctx = create_viewer_context()
manager = MockTeamManager(
"e2e-auth-test",
user_context=viewer_ctx,
logger=MockLogger("test"),
fs=MockFileSystem()
)
with self.assertRaises(PermissionDenied):
manager.initialize_project()
print("[E2E-008] ✓ Unauthorized access handled correctly")
class TestE2ERBAC(unittest.TestCase):
"""E2E: Role-based access control."""
def setUp(self):
self.fs = MockFileSystem()
def test_admin_can_modify_any_team(self):
"""
E2E-009: Admin can modify any team
"""
print("\n[E2E-009] Testing admin permissions")
admin_ctx = create_admin_context()
manager = MockTeamManager("rbac-test", user_context=admin_ctx, logger=MockLogger("test"), fs=self.fs)
manager.initialize_project()
# Admin should be able to modify any team
for team_id in range(1, 13):
result = manager.assign_role(team_id, manager.teams[team_id].roles[0].name, f"Admin Assignee {team_id}")
self.assertTrue(result, f"Admin should be able to modify team {team_id}")
print("[E2E-009] ✓ Admin permissions verified")
def test_team_lead_can_only_modify_their_team(self):
"""
E2E-010: Team lead can only modify their assigned team
"""
print("\n[E2E-010] Testing team lead permissions")
admin_ctx = create_admin_context()
manager = MockTeamManager("rbac-test", user_context=admin_ctx, logger=MockLogger("test"), fs=self.fs)
manager.initialize_project()
# Create team lead context for team 5
team_lead_ctx = create_team_lead_context(team_id=5)
lead_manager = MockTeamManager(
"rbac-test",
user_context=team_lead_ctx,
logger=MockLogger("test"),
fs=self.fs
)
load_result = lead_manager.load()
self.assertTrue(load_result, "Failed to load project for team lead")
self.assertIn(5, lead_manager.teams, "Team 5 should exist after loading")
from scripts.team_manager import PermissionDenied
# Should succeed for team 5
result = lead_manager.assign_role(5, lead_manager.teams[5].roles[0].name, "Lead Assignee")
self.assertTrue(result)
print("[E2E-010] ✓ Team lead permissions verified")
def test_viewer_cannot_modify(self):
"""
E2E-011: Viewer cannot modify anything
"""
print("\n[E2E-011] Testing viewer restrictions")
viewer_ctx = create_viewer_context()
manager = MockTeamManager(
"rbac-test",
user_context=viewer_ctx,
logger=MockLogger("test"),
fs=MockFileSystem()
)
from scripts.team_manager import PermissionDenied
with self.assertRaises(PermissionDenied):
manager.initialize_project()
print("[E2E-011] ✓ Viewer restrictions verified")
class TestE2EBatchOperations(unittest.TestCase):
"""E2E: Batch operations."""
def setUp(self):
self.admin_ctx = create_admin_context()
self.fs = MockFileSystem()
self.manager = MockTeamManager(
"e2e-batch-test",
user_context=self.admin_ctx,
logger=MockLogger("test"),
fs=self.fs
)
self.manager.initialize_project()
self.temp_dir = tempfile.mkdtemp()
def tearDown(self):
"""Clean up temp files."""
import shutil
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_csv_export_import_roundtrip(self):
"""
E2E-012: CSV export/import roundtrip preserves data
"""
print("\n[E2E-012] Testing CSV roundtrip")
# First assign some roles
self.manager.assign_role(1, "Business Relationship Manager", "Alice")
self.manager.assign_role(1, "Lead Product Manager", "Bob")
# Export to CSV using real file system
csv_path = Path(self.temp_dir) / "test_export.csv"
result = self.manager.export_csv_file(csv_path)
self.assertTrue(result["success"])
# Verify CSV content exists on real file system
self.assertTrue(csv_path.exists())
# Verify content
with open(csv_path, 'r') as f:
content = f.read()
self.assertIn("Alice", content)
self.assertIn("Business Relationship Manager", content)
print("[E2E-012] ✓ CSV roundtrip test passed")
def test_json_export_import_roundtrip(self):
"""
E2E-013: JSON export/import roundtrip preserves data
"""
print("\n[E2E-013] Testing JSON roundtrip")
# Assign and complete Team 1
self.manager.assign_role(1, "Business Relationship Manager", "Alice")
self.manager.start_team(1)
self.manager.complete_team(1)
# Export to JSON using real file system
json_path = Path(self.temp_dir) / "test_export.json"
result = self.manager.export_json_file(json_path)
self.assertTrue(result["success"])
self.assertEqual(result["team_count"], 12)
# Verify JSON file exists
self.assertTrue(json_path.exists())
# Verify JSON content
with open(json_path, 'r') as f:
data = json.load(f)
self.assertEqual(data["project_name"], "e2e-batch-test")
self.assertEqual(len(data["teams"]), 12)
print("[E2E-013] ✓ JSON roundtrip test passed")
class TestE2EAgentMapping(unittest.TestCase):
"""E2E: Agent type to team mapping."""
def setUp(self):
self.admin_ctx = create_admin_context()
self.manager = MockTeamManager(
"e2e-agent-test",
user_context=self.admin_ctx,
logger=MockLogger("test"),
fs=MockFileSystem()
)
self.manager.initialize_project()
def test_agent_type_mapping(self):
"""
E2E-014: Agent types map to correct teams
"""
print("\n[E2E-014] Testing agent type mappings")
test_cases = [
("planner", 2, "Enterprise Architecture"),
("coder", 7, "Core Feature Squad"),
("reviewer", 10, "Quality Engineering (SDET)"),
("security", 9, "Cybersecurity (AppSec)"),
("tester", 10, "Quality Engineering (SDET)"),
("ops", 11, "Site Reliability Engineering (SRE)"),
]
for agent_type, expected_id, expected_name in test_cases:
team = self.manager.get_agent_team(agent_type)
self.assertIsNotNone(team, f"Agent type '{agent_type}' should map to a team")
self.assertEqual(team.id, expected_id)
self.assertEqual(team.name, expected_name)
print(f"'{agent_type}' → Team {expected_id}: {expected_name}")
print("[E2E-014] ✓ Agent type mapping verified")
def test_invalid_agent_type(self):
"""
E2E-015: Invalid agent type returns None
"""
print("\n[E2E-015] Testing invalid agent type")
team = self.manager.get_agent_team("invalid_agent_type")
self.assertIsNone(team)
print("[E2E-015] ✓ Invalid agent type handled correctly")
class TestE2ETeamValidation(unittest.TestCase):
"""E2E: Team size validation."""
def setUp(self):
self.admin_ctx = create_admin_context()
self.manager = MockTeamManager(
"e2e-validation-test",
user_context=self.admin_ctx,
logger=MockLogger("test"),
fs=MockFileSystem()
)
self.manager.initialize_project()
def test_empty_team_validation_fails(self):
"""
E2E-016: Empty team fails size validation
"""
print("\n[E2E-016] Testing empty team validation")
results = self.manager.validate_team_size(1)
self.assertFalse(results["valid"])
self.assertEqual(len(results["violations"]), 1)
self.assertEqual(results["violations"][0]["issue"], "undersized")
print("[E2E-016] ✓ Empty team validation works")
def test_properly_sized_team_passes(self):
"""
E2E-017: Properly staffed team passes validation
"""
print("\n[E2E-017] Testing properly sized team")
team = self.manager.teams[1]
for i, role in enumerate(team.roles[:4]): # Assign 4 people
self.manager.assign_role(1, role.name, f"Person {i+1}")
results = self.manager.validate_team_size(1)
self.assertTrue(results["valid"])
self.assertEqual(len(results["violations"]), 0)
print("[E2E-017] ✓ Properly sized team passes validation")
class TestE2EPersistence(unittest.TestCase):
"""E2E: Save/load persistence."""
def setUp(self):
self.admin_ctx = create_admin_context()
self.fs = MockFileSystem()
def test_save_and_load(self):
"""
E2E-018: Save and load preserves all data
"""
print("\n[E2E-018] Testing save/load persistence")
# Create and populate manager
manager1 = MockTeamManager(
"persistence-test",
user_context=self.admin_ctx,
logger=MockLogger("test"),
fs=self.fs
)
manager1.initialize_project()
manager1.assign_role(1, "Business Relationship Manager", "Test User")
manager1.start_team(1)
manager1.save()
# Load into new manager
manager2 = MockTeamManager(
"persistence-test",
user_context=self.admin_ctx,
logger=MockLogger("test2"),
fs=self.fs
)
result = manager2.load()
self.assertTrue(result)
# Verify data preserved
self.assertEqual(manager2.teams[1].status, "active")
role = next(r for r in manager2.teams[1].roles if r.name == "Business Relationship Manager")
self.assertEqual(role.assigned_to, "Test User")
print("[E2E-018] ✓ Save/load persistence verified")
def run_e2e_tests():
"""Run all E2E tests and return results."""
print("=" * 70)
print("Running E2E Tests for Team Manager")
print("=" * 70)
loader = unittest.TestLoader()
suite = unittest.TestSuite()
# Add all test classes
suite.addTests(loader.loadTestsFromTestCase(TestE2EFullWorkflow))
suite.addTests(loader.loadTestsFromTestCase(TestE2EPhaseGates))
suite.addTests(loader.loadTestsFromTestCase(TestE2EErrorScenarios))
suite.addTests(loader.loadTestsFromTestCase(TestE2ERBAC))
suite.addTests(loader.loadTestsFromTestCase(TestE2EBatchOperations))
suite.addTests(loader.loadTestsFromTestCase(TestE2EAgentMapping))
suite.addTests(loader.loadTestsFromTestCase(TestE2ETeamValidation))
suite.addTests(loader.loadTestsFromTestCase(TestE2EPersistence))
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
print("\n" + "=" * 70)
print(f"Tests run: {result.testsRun}")
print(f"Failures: {len(result.failures)}")
print(f"Errors: {len(result.errors)}")
print(f"Skipped: {len(result.skipped)}")
print("=" * 70)
return result.wasSuccessful()
if __name__ == "__main__":
success = run_e2e_tests()
sys.exit(0 if success else 1)