111 lines
3.5 KiB
Python
111 lines
3.5 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
Test deterministic cloning by creating multiple sessions and comparing data hashes.
|
||
|
|
"""
|
||
|
|
import asyncio
|
||
|
|
import hashlib
|
||
|
|
import json
|
||
|
|
from typing import List, Dict
|
||
|
|
import httpx
|
||
|
|
|
||
|
|
DEMO_API_URL = "http://localhost:8018"
|
||
|
|
INTERNAL_API_KEY = "test-internal-key"
|
||
|
|
|
||
|
|
async def create_demo_session(tier: str = "professional") -> dict:
|
||
|
|
"""Create a demo session"""
|
||
|
|
async with httpx.AsyncClient() as client:
|
||
|
|
response = await client.post(
|
||
|
|
f"{DEMO_API_URL}/api/demo/sessions",
|
||
|
|
json={"demo_account_type": tier}
|
||
|
|
)
|
||
|
|
return response.json()
|
||
|
|
|
||
|
|
async def get_all_data_from_service(
|
||
|
|
service_url: str,
|
||
|
|
tenant_id: str
|
||
|
|
) -> dict:
|
||
|
|
"""Fetch all data for a tenant from a service"""
|
||
|
|
async with httpx.AsyncClient() as client:
|
||
|
|
response = await client.get(
|
||
|
|
f"{service_url}/internal/demo/export/{tenant_id}",
|
||
|
|
headers={"X-Internal-API-Key": INTERNAL_API_KEY}
|
||
|
|
)
|
||
|
|
return response.json()
|
||
|
|
|
||
|
|
def calculate_data_hash(data: dict) -> str:
|
||
|
|
"""
|
||
|
|
Calculate SHA-256 hash of data, excluding audit timestamps.
|
||
|
|
"""
|
||
|
|
# Remove non-deterministic fields
|
||
|
|
clean_data = remove_audit_fields(data)
|
||
|
|
|
||
|
|
# Sort keys for consistency
|
||
|
|
json_str = json.dumps(clean_data, sort_keys=True)
|
||
|
|
|
||
|
|
return hashlib.sha256(json_str.encode()).hexdigest()
|
||
|
|
|
||
|
|
def remove_audit_fields(data: dict) -> dict:
|
||
|
|
"""Remove created_at, updated_at fields recursively"""
|
||
|
|
if isinstance(data, dict):
|
||
|
|
return {
|
||
|
|
k: remove_audit_fields(v)
|
||
|
|
for k, v in data.items()
|
||
|
|
if k not in ["created_at", "updated_at", "id"] # IDs are UUIDs
|
||
|
|
}
|
||
|
|
elif isinstance(data, list):
|
||
|
|
return [remove_audit_fields(item) for item in data]
|
||
|
|
else:
|
||
|
|
return data
|
||
|
|
|
||
|
|
async def test_determinism(tier: str = "professional", iterations: int = 10):
|
||
|
|
"""
|
||
|
|
Test that cloning is deterministic across multiple sessions.
|
||
|
|
"""
|
||
|
|
print(f"Testing determinism for {tier} tier ({iterations} iterations)...")
|
||
|
|
|
||
|
|
services = [
|
||
|
|
("inventory", "http://inventory-service:8002"),
|
||
|
|
("production", "http://production-service:8003"),
|
||
|
|
("recipes", "http://recipes-service:8004"),
|
||
|
|
]
|
||
|
|
|
||
|
|
hashes_by_service = {svc[0]: [] for svc in services}
|
||
|
|
|
||
|
|
for i in range(iterations):
|
||
|
|
# Create session
|
||
|
|
session = await create_demo_session(tier)
|
||
|
|
tenant_id = session["virtual_tenant_id"]
|
||
|
|
|
||
|
|
# Get data from each service
|
||
|
|
for service_name, service_url in services:
|
||
|
|
data = await get_all_data_from_service(service_url, tenant_id)
|
||
|
|
data_hash = calculate_data_hash(data)
|
||
|
|
hashes_by_service[service_name].append(data_hash)
|
||
|
|
|
||
|
|
# Cleanup
|
||
|
|
async with httpx.AsyncClient() as client:
|
||
|
|
await client.delete(f"{DEMO_API_URL}/api/demo/sessions/{session['session_id']}")
|
||
|
|
|
||
|
|
if (i + 1) % 10 == 0:
|
||
|
|
print(f" Completed {i + 1}/{iterations} iterations")
|
||
|
|
|
||
|
|
# Check consistency
|
||
|
|
all_consistent = True
|
||
|
|
for service_name, hashes in hashes_by_service.items():
|
||
|
|
unique_hashes = set(hashes)
|
||
|
|
if len(unique_hashes) == 1:
|
||
|
|
print(f"✅ {service_name}: All {iterations} hashes identical")
|
||
|
|
else:
|
||
|
|
print(f"❌ {service_name}: {len(unique_hashes)} different hashes found!")
|
||
|
|
all_consistent = False
|
||
|
|
|
||
|
|
if all_consistent:
|
||
|
|
print("\n✅ DETERMINISM TEST PASSED")
|
||
|
|
return 0
|
||
|
|
else:
|
||
|
|
print("\n❌ DETERMINISM TEST FAILED")
|
||
|
|
return 1
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
exit_code = asyncio.run(test_determinism())
|
||
|
|
exit(exit_code)
|