Fix Docker Swarm discovery network name mismatch
- Changed NetworkName from 'chorus_default' to 'chorus_net' - This matches the actual network 'CHORUS_chorus_net' (service prefix added automatically) - Fixes discovered_count:0 issue - now successfully discovering all 25 agents - Updated IMPLEMENTATION-SUMMARY with deployment status Result: All 25 CHORUS agents now discovered successfully via Docker Swarm API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
440
tests/test_council_artifacts.py
Executable file
440
tests/test_council_artifacts.py
Executable file
@@ -0,0 +1,440 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test Suite for Council-Generated Project Artifacts
|
||||
|
||||
This test verifies the complete flow:
|
||||
1. Project creation triggers council formation
|
||||
2. Council roles are claimed by CHORUS agents
|
||||
3. Council produces artifacts
|
||||
4. Artifacts are retrievable via API
|
||||
|
||||
Usage:
|
||||
python test_council_artifacts.py
|
||||
python test_council_artifacts.py --verbose
|
||||
python test_council_artifacts.py --wait-time 60
|
||||
"""
|
||||
|
||||
import requests
|
||||
import time
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Color:
|
||||
"""ANSI color codes for terminal output"""
|
||||
HEADER = '\033[95m'
|
||||
OKBLUE = '\033[94m'
|
||||
OKCYAN = '\033[96m'
|
||||
OKGREEN = '\033[92m'
|
||||
WARNING = '\033[93m'
|
||||
FAIL = '\033[91m'
|
||||
ENDC = '\033[0m'
|
||||
BOLD = '\033[1m'
|
||||
UNDERLINE = '\033[4m'
|
||||
|
||||
|
||||
class TestStatus(Enum):
|
||||
"""Test execution status"""
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
PASSED = "passed"
|
||||
FAILED = "failed"
|
||||
SKIPPED = "skipped"
|
||||
|
||||
|
||||
class CouncilArtifactTester:
|
||||
"""Test harness for council artifact generation"""
|
||||
|
||||
def __init__(self, whoosh_url: str = "http://localhost:8800", verbose: bool = False):
|
||||
self.whoosh_url = whoosh_url
|
||||
self.verbose = verbose
|
||||
self.auth_token = "dev-token"
|
||||
self.test_results = []
|
||||
self.created_project_id = None
|
||||
|
||||
def log(self, message: str, level: str = "INFO"):
|
||||
"""Log a message with color coding"""
|
||||
colors = {
|
||||
"INFO": Color.OKBLUE,
|
||||
"SUCCESS": Color.OKGREEN,
|
||||
"WARNING": Color.WARNING,
|
||||
"ERROR": Color.FAIL,
|
||||
"HEADER": Color.HEADER
|
||||
}
|
||||
color = colors.get(level, "")
|
||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||
print(f"{color}[{timestamp}] {level}: {message}{Color.ENDC}")
|
||||
|
||||
def verbose_log(self, message: str):
|
||||
"""Log only if verbose mode is enabled"""
|
||||
if self.verbose:
|
||||
self.log(message, "INFO")
|
||||
|
||||
def record_test(self, name: str, status: TestStatus, details: str = ""):
|
||||
"""Record test result"""
|
||||
self.test_results.append({
|
||||
"name": name,
|
||||
"status": status.value,
|
||||
"details": details,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
def make_request(self, method: str, endpoint: str, data: Optional[Dict] = None) -> Optional[Dict]:
|
||||
"""Make HTTP request to WHOOSH API"""
|
||||
url = f"{self.whoosh_url}{endpoint}"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.auth_token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
try:
|
||||
if method == "GET":
|
||||
response = requests.get(url, headers=headers, timeout=30)
|
||||
elif method == "POST":
|
||||
response = requests.post(url, headers=headers, json=data, timeout=30)
|
||||
elif method == "DELETE":
|
||||
response = requests.delete(url, headers=headers, timeout=30)
|
||||
else:
|
||||
raise ValueError(f"Unsupported HTTP method: {method}")
|
||||
|
||||
self.verbose_log(f"{method} {endpoint} -> {response.status_code}")
|
||||
|
||||
if response.status_code in [200, 201, 202]:
|
||||
return response.json()
|
||||
else:
|
||||
self.log(f"Request failed: {response.status_code} - {response.text}", "ERROR")
|
||||
return None
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.log(f"Request exception: {e}", "ERROR")
|
||||
return None
|
||||
|
||||
def test_1_whoosh_health(self) -> bool:
|
||||
"""Test 1: Verify WHOOSH is accessible"""
|
||||
self.log("TEST 1: Checking WHOOSH health...", "HEADER")
|
||||
|
||||
try:
|
||||
# WHOOSH doesn't have a dedicated health endpoint, use projects list
|
||||
headers = {"Authorization": f"Bearer {self.auth_token}"}
|
||||
response = requests.get(f"{self.whoosh_url}/api/v1/projects", headers=headers, timeout=5)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
project_count = len(data.get("projects", []))
|
||||
self.log(f"✓ WHOOSH is healthy and accessible ({project_count} existing projects)", "SUCCESS")
|
||||
self.record_test("WHOOSH Health Check", TestStatus.PASSED, f"{project_count} projects")
|
||||
return True
|
||||
else:
|
||||
self.log(f"✗ WHOOSH health check failed: {response.status_code}", "ERROR")
|
||||
self.record_test("WHOOSH Health Check", TestStatus.FAILED, f"Status: {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.log(f"✗ Cannot reach WHOOSH: {e}", "ERROR")
|
||||
self.record_test("WHOOSH Health Check", TestStatus.FAILED, str(e))
|
||||
return False
|
||||
|
||||
def test_2_create_project(self) -> bool:
|
||||
"""Test 2: Create a test project"""
|
||||
self.log("TEST 2: Creating test project...", "HEADER")
|
||||
|
||||
# Use an existing GITEA repository for testing
|
||||
# Generate unique name by appending timestamp
|
||||
import random
|
||||
test_suffix = random.randint(1000, 9999)
|
||||
test_repo = f"https://gitea.chorus.services/tony/TEST"
|
||||
|
||||
self.verbose_log(f"Using repository: {test_repo}")
|
||||
|
||||
project_data = {
|
||||
"repository_url": test_repo
|
||||
}
|
||||
|
||||
result = self.make_request("POST", "/api/v1/projects", project_data)
|
||||
|
||||
if result and "id" in result:
|
||||
self.created_project_id = result["id"]
|
||||
self.log(f"✓ Project created successfully: {self.created_project_id}", "SUCCESS")
|
||||
self.log(f" Name: {result.get('name', 'N/A')}", "INFO")
|
||||
self.log(f" Status: {result.get('status', 'unknown')}", "INFO")
|
||||
self.verbose_log(f" Project details: {json.dumps(result, indent=2)}")
|
||||
self.record_test("Create Project", TestStatus.PASSED, f"Project ID: {self.created_project_id}")
|
||||
return True
|
||||
else:
|
||||
self.log("✗ Failed to create project", "ERROR")
|
||||
self.record_test("Create Project", TestStatus.FAILED)
|
||||
return False
|
||||
|
||||
def test_3_verify_council_formation(self) -> bool:
|
||||
"""Test 3: Verify council was formed for the project"""
|
||||
self.log("TEST 3: Verifying council formation...", "HEADER")
|
||||
|
||||
if not self.created_project_id:
|
||||
self.log("✗ No project ID available", "ERROR")
|
||||
self.record_test("Council Formation", TestStatus.SKIPPED, "No project created")
|
||||
return False
|
||||
|
||||
result = self.make_request("GET", f"/api/v1/projects/{self.created_project_id}")
|
||||
|
||||
if result:
|
||||
council_id = result.get("id") # Council ID is same as project ID
|
||||
status = result.get("status", "unknown")
|
||||
|
||||
self.log(f"✓ Council found: {council_id}", "SUCCESS")
|
||||
self.log(f" Status: {status}", "INFO")
|
||||
self.log(f" Name: {result.get('name', 'N/A')}", "INFO")
|
||||
|
||||
self.record_test("Council Formation", TestStatus.PASSED, f"Council: {council_id}, Status: {status}")
|
||||
return True
|
||||
else:
|
||||
self.log("✗ Council not found", "ERROR")
|
||||
self.record_test("Council Formation", TestStatus.FAILED)
|
||||
return False
|
||||
|
||||
def test_4_wait_for_role_claims(self, max_wait_seconds: int = 30) -> bool:
|
||||
"""Test 4: Wait for CHORUS agents to claim roles"""
|
||||
self.log(f"TEST 4: Waiting for agent role claims (max {max_wait_seconds}s)...", "HEADER")
|
||||
|
||||
if not self.created_project_id:
|
||||
self.log("✗ No project ID available", "ERROR")
|
||||
self.record_test("Role Claims", TestStatus.SKIPPED, "No project created")
|
||||
return False
|
||||
|
||||
start_time = time.time()
|
||||
claimed_roles = 0
|
||||
|
||||
while time.time() - start_time < max_wait_seconds:
|
||||
# Check council status
|
||||
result = self.make_request("GET", f"/api/v1/projects/{self.created_project_id}")
|
||||
|
||||
if result:
|
||||
# TODO: Add endpoint to get council agents/claims
|
||||
# For now, check if status changed to 'active'
|
||||
status = result.get("status", "unknown")
|
||||
|
||||
if status == "active":
|
||||
self.log(f"✓ Council activated! All roles claimed", "SUCCESS")
|
||||
self.record_test("Role Claims", TestStatus.PASSED, "Council activated")
|
||||
return True
|
||||
|
||||
self.verbose_log(f" Council status: {status}, waiting...")
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
self.log(f"⚠ Timeout waiting for role claims ({elapsed:.1f}s)", "WARNING")
|
||||
self.log(f" Council may still be forming - this is normal for new deployments", "INFO")
|
||||
self.record_test("Role Claims", TestStatus.FAILED, f"Timeout after {elapsed:.1f}s")
|
||||
return False
|
||||
|
||||
def test_5_fetch_artifacts(self) -> bool:
|
||||
"""Test 5: Fetch artifacts produced by the council"""
|
||||
self.log("TEST 5: Fetching council artifacts...", "HEADER")
|
||||
|
||||
if not self.created_project_id:
|
||||
self.log("✗ No project ID available", "ERROR")
|
||||
self.record_test("Fetch Artifacts", TestStatus.SKIPPED, "No project created")
|
||||
return False
|
||||
|
||||
result = self.make_request("GET", f"/api/v1/councils/{self.created_project_id}/artifacts")
|
||||
|
||||
if result:
|
||||
artifacts = result.get("artifacts") or [] # Handle null artifacts
|
||||
|
||||
if len(artifacts) > 0:
|
||||
self.log(f"✓ Found {len(artifacts)} artifact(s)", "SUCCESS")
|
||||
|
||||
for i, artifact in enumerate(artifacts, 1):
|
||||
self.log(f"\n Artifact {i}:", "INFO")
|
||||
self.log(f" ID: {artifact.get('id')}", "INFO")
|
||||
self.log(f" Type: {artifact.get('artifact_type')}", "INFO")
|
||||
self.log(f" Name: {artifact.get('artifact_name')}", "INFO")
|
||||
self.log(f" Status: {artifact.get('status')}", "INFO")
|
||||
self.log(f" Produced by: {artifact.get('produced_by', 'N/A')}", "INFO")
|
||||
self.log(f" Produced at: {artifact.get('produced_at')}", "INFO")
|
||||
|
||||
if self.verbose and artifact.get('content'):
|
||||
content_preview = artifact['content'][:200]
|
||||
self.verbose_log(f" Content preview: {content_preview}...")
|
||||
|
||||
self.record_test("Fetch Artifacts", TestStatus.PASSED, f"Found {len(artifacts)} artifacts")
|
||||
return True
|
||||
else:
|
||||
self.log("⚠ No artifacts found yet", "WARNING")
|
||||
self.log(" This is normal - councils need time to produce artifacts", "INFO")
|
||||
self.record_test("Fetch Artifacts", TestStatus.FAILED, "No artifacts produced yet")
|
||||
return False
|
||||
else:
|
||||
self.log("✗ Failed to fetch artifacts", "ERROR")
|
||||
self.record_test("Fetch Artifacts", TestStatus.FAILED, "API request failed")
|
||||
return False
|
||||
|
||||
def test_6_verify_artifact_content(self) -> bool:
|
||||
"""Test 6: Verify artifact content is valid"""
|
||||
self.log("TEST 6: Verifying artifact content...", "HEADER")
|
||||
|
||||
if not self.created_project_id:
|
||||
self.log("✗ No project ID available", "ERROR")
|
||||
self.record_test("Artifact Content Validation", TestStatus.SKIPPED, "No project created")
|
||||
return False
|
||||
|
||||
result = self.make_request("GET", f"/api/v1/councils/{self.created_project_id}/artifacts")
|
||||
|
||||
if result:
|
||||
artifacts = result.get("artifacts") or [] # Handle null artifacts
|
||||
|
||||
if len(artifacts) == 0:
|
||||
self.log("⚠ No artifacts to validate", "WARNING")
|
||||
self.record_test("Artifact Content Validation", TestStatus.SKIPPED, "No artifacts")
|
||||
return False
|
||||
|
||||
valid_count = 0
|
||||
for artifact in artifacts:
|
||||
has_content = bool(artifact.get('content') or artifact.get('content_json'))
|
||||
has_metadata = all([
|
||||
artifact.get('artifact_type'),
|
||||
artifact.get('artifact_name'),
|
||||
artifact.get('status')
|
||||
])
|
||||
|
||||
if has_content and has_metadata:
|
||||
valid_count += 1
|
||||
self.verbose_log(f" ✓ Artifact {artifact.get('id')} is valid")
|
||||
else:
|
||||
self.log(f" ✗ Artifact {artifact.get('id')} is incomplete", "WARNING")
|
||||
|
||||
if valid_count == len(artifacts):
|
||||
self.log(f"✓ All {valid_count} artifact(s) are valid", "SUCCESS")
|
||||
self.record_test("Artifact Content Validation", TestStatus.PASSED, f"{valid_count}/{len(artifacts)} valid")
|
||||
return True
|
||||
else:
|
||||
self.log(f"⚠ Only {valid_count}/{len(artifacts)} artifact(s) are valid", "WARNING")
|
||||
self.record_test("Artifact Content Validation", TestStatus.FAILED, f"{valid_count}/{len(artifacts)} valid")
|
||||
return False
|
||||
else:
|
||||
self.log("✗ Failed to fetch artifacts for validation", "ERROR")
|
||||
self.record_test("Artifact Content Validation", TestStatus.FAILED, "API request failed")
|
||||
return False
|
||||
|
||||
def test_7_cleanup(self) -> bool:
|
||||
"""Test 7: Cleanup - delete test project"""
|
||||
self.log("TEST 7: Cleaning up test project...", "HEADER")
|
||||
|
||||
if not self.created_project_id:
|
||||
self.log("⚠ No project to clean up", "WARNING")
|
||||
self.record_test("Cleanup", TestStatus.SKIPPED, "No project created")
|
||||
return True
|
||||
|
||||
result = self.make_request("DELETE", f"/api/v1/projects/{self.created_project_id}")
|
||||
|
||||
if result:
|
||||
self.log(f"✓ Project deleted successfully: {self.created_project_id}", "SUCCESS")
|
||||
self.record_test("Cleanup", TestStatus.PASSED)
|
||||
return True
|
||||
else:
|
||||
self.log(f"⚠ Failed to delete project - manual cleanup may be needed", "WARNING")
|
||||
self.record_test("Cleanup", TestStatus.FAILED)
|
||||
return False
|
||||
|
||||
def run_all_tests(self, skip_cleanup: bool = False, wait_time: int = 30):
|
||||
"""Run all tests in sequence"""
|
||||
self.log("\n" + "="*70, "HEADER")
|
||||
self.log("COUNCIL ARTIFACT GENERATION TEST SUITE", "HEADER")
|
||||
self.log("="*70 + "\n", "HEADER")
|
||||
|
||||
tests = [
|
||||
("WHOOSH Health Check", self.test_1_whoosh_health, []),
|
||||
("Create Test Project", self.test_2_create_project, []),
|
||||
("Verify Council Formation", self.test_3_verify_council_formation, []),
|
||||
("Wait for Role Claims", self.test_4_wait_for_role_claims, [wait_time]),
|
||||
("Fetch Artifacts", self.test_5_fetch_artifacts, []),
|
||||
("Validate Artifact Content", self.test_6_verify_artifact_content, []),
|
||||
]
|
||||
|
||||
if not skip_cleanup:
|
||||
tests.append(("Cleanup Test Data", self.test_7_cleanup, []))
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
skipped = 0
|
||||
|
||||
for name, test_func, args in tests:
|
||||
try:
|
||||
result = test_func(*args)
|
||||
if result:
|
||||
passed += 1
|
||||
else:
|
||||
# Check if it was skipped
|
||||
last_result = self.test_results[-1] if self.test_results else None
|
||||
if last_result and last_result["status"] == "skipped":
|
||||
skipped += 1
|
||||
else:
|
||||
failed += 1
|
||||
except Exception as e:
|
||||
self.log(f"✗ Test exception: {e}", "ERROR")
|
||||
self.record_test(name, TestStatus.FAILED, str(e))
|
||||
failed += 1
|
||||
|
||||
print() # Blank line between tests
|
||||
|
||||
# Print summary
|
||||
self.print_summary(passed, failed, skipped)
|
||||
|
||||
def print_summary(self, passed: int, failed: int, skipped: int):
|
||||
"""Print test summary"""
|
||||
total = passed + failed + skipped
|
||||
|
||||
self.log("="*70, "HEADER")
|
||||
self.log("TEST SUMMARY", "HEADER")
|
||||
self.log("="*70, "HEADER")
|
||||
|
||||
self.log(f"\nTotal Tests: {total}", "INFO")
|
||||
self.log(f" Passed: {passed} {Color.OKGREEN}{'✓' * passed}{Color.ENDC}", "SUCCESS")
|
||||
if failed > 0:
|
||||
self.log(f" Failed: {failed} {Color.FAIL}{'✗' * failed}{Color.ENDC}", "ERROR")
|
||||
if skipped > 0:
|
||||
self.log(f" Skipped: {skipped} {Color.WARNING}{'○' * skipped}{Color.ENDC}", "WARNING")
|
||||
|
||||
success_rate = (passed / total * 100) if total > 0 else 0
|
||||
self.log(f"\nSuccess Rate: {success_rate:.1f}%", "INFO")
|
||||
|
||||
if self.created_project_id:
|
||||
self.log(f"\nTest Project ID: {self.created_project_id}", "INFO")
|
||||
|
||||
# Detailed results
|
||||
if self.verbose:
|
||||
self.log("\nDetailed Results:", "HEADER")
|
||||
for result in self.test_results:
|
||||
status_color = {
|
||||
"passed": Color.OKGREEN,
|
||||
"failed": Color.FAIL,
|
||||
"skipped": Color.WARNING
|
||||
}.get(result["status"], "")
|
||||
|
||||
self.log(f" {result['name']}: {status_color}{result['status'].upper()}{Color.ENDC}", "INFO")
|
||||
if result.get("details"):
|
||||
self.log(f" {result['details']}", "INFO")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
parser = argparse.ArgumentParser(description="Test council artifact generation")
|
||||
parser.add_argument("--whoosh-url", default="http://localhost:8800",
|
||||
help="WHOOSH base URL (default: http://localhost:8800)")
|
||||
parser.add_argument("--verbose", "-v", action="store_true",
|
||||
help="Enable verbose output")
|
||||
parser.add_argument("--skip-cleanup", action="store_true",
|
||||
help="Skip cleanup step (leave test project)")
|
||||
parser.add_argument("--wait-time", type=int, default=30,
|
||||
help="Seconds to wait for role claims (default: 30)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
tester = CouncilArtifactTester(whoosh_url=args.whoosh_url, verbose=args.verbose)
|
||||
tester.run_all_tests(skip_cleanup=args.skip_cleanup, wait_time=args.wait_time)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user