Add Phase 10-12 implementation: multi-tenant, marketplace, observability
Major additions: - marketplace/: Agent template registry with FTS5 search, ratings, versioning - observability/: Prometheus metrics, distributed tracing, structured logging - ledger/migrations/: Database migration scripts for multi-tenant support - tests/governance/: 15 new test files for phases 6-12 (295 total tests) - bin/validate-phases: Full 12-phase validation script New features: - Multi-tenant support with tenant isolation and quota enforcement - Agent marketplace with semantic versioning and search - Observability with metrics, tracing, and log correlation - Tier-1 agent bootstrap scripts Updated components: - ledger/api.py: Extended API for tenants, marketplace, observability - ledger/schema.sql: Added tenant, project, marketplace tables - testing/framework.ts: Enhanced test framework - checkpoint/checkpoint.py: Improved checkpoint management Archived: - External integrations (Slack/GitHub/PagerDuty) moved to .archive/ - Old checkpoint files cleaned up Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
4b5b3b0a2d
commit
8c6e7831e9
@@ -0,0 +1,356 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Phase 10: Multi-Tenant Support Tests
|
||||
====================================
|
||||
Validates multi-tenant schema, isolation, quotas, and RBAC.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
DB_PATH = Path("/opt/agent-governance/ledger/governance.db")
|
||||
|
||||
|
||||
def get_db():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
def test_tenants_table_exists():
|
||||
"""Verify tenants table exists with required columns"""
|
||||
conn = get_db()
|
||||
cursor = conn.execute("PRAGMA table_info(tenants)")
|
||||
columns = {row[1] for row in cursor.fetchall()}
|
||||
conn.close()
|
||||
|
||||
required = {"tenant_id", "name", "slug", "subscription_tier", "is_active"}
|
||||
assert required.issubset(columns), f"Missing columns: {required - columns}"
|
||||
print("[PASS] tenants table exists with required columns")
|
||||
|
||||
|
||||
def test_projects_table_exists():
|
||||
"""Verify projects table exists with required columns"""
|
||||
conn = get_db()
|
||||
cursor = conn.execute("PRAGMA table_info(projects)")
|
||||
columns = {row[1] for row in cursor.fetchall()}
|
||||
conn.close()
|
||||
|
||||
required = {"project_id", "tenant_id", "name", "slug", "is_active"}
|
||||
assert required.issubset(columns), f"Missing columns: {required - columns}"
|
||||
print("[PASS] projects table exists with required columns")
|
||||
|
||||
|
||||
def test_tenant_quotas_table_exists():
|
||||
"""Verify tenant_quotas table exists with quota fields"""
|
||||
conn = get_db()
|
||||
cursor = conn.execute("PRAGMA table_info(tenant_quotas)")
|
||||
columns = {row[1] for row in cursor.fetchall()}
|
||||
conn.close()
|
||||
|
||||
required = {"tenant_id", "max_projects", "max_agents_per_project", "max_api_calls_per_day"}
|
||||
assert required.issubset(columns), f"Missing columns: {required - columns}"
|
||||
print("[PASS] tenant_quotas table exists with quota fields")
|
||||
|
||||
|
||||
def test_tenant_usage_table_exists():
|
||||
"""Verify tenant_usage table for tracking consumption"""
|
||||
conn = get_db()
|
||||
cursor = conn.execute("PRAGMA table_info(tenant_usage)")
|
||||
columns = {row[1] for row in cursor.fetchall()}
|
||||
conn.close()
|
||||
|
||||
required = {"tenant_id", "period_start", "api_calls", "tokens_used"}
|
||||
assert required.issubset(columns), f"Missing columns: {required - columns}"
|
||||
print("[PASS] tenant_usage table exists for consumption tracking")
|
||||
|
||||
|
||||
def test_api_keys_table_exists():
|
||||
"""Verify api_keys table for tenant authentication"""
|
||||
conn = get_db()
|
||||
cursor = conn.execute("PRAGMA table_info(api_keys)")
|
||||
columns = {row[1] for row in cursor.fetchall()}
|
||||
conn.close()
|
||||
|
||||
required = {"key_id", "tenant_id", "key_hash", "is_active"}
|
||||
assert required.issubset(columns), f"Missing columns: {required - columns}"
|
||||
print("[PASS] api_keys table exists for tenant authentication")
|
||||
|
||||
|
||||
def test_default_tenant_exists():
|
||||
"""Verify default tenant was created"""
|
||||
conn = get_db()
|
||||
cursor = conn.execute("SELECT * FROM tenants WHERE tenant_id = 'default'")
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
assert row is not None, "Default tenant not found"
|
||||
assert row["is_active"] == 1, "Default tenant should be active"
|
||||
print("[PASS] default tenant exists and is active")
|
||||
|
||||
|
||||
def test_default_project_exists():
|
||||
"""Verify default project was created"""
|
||||
conn = get_db()
|
||||
cursor = conn.execute("SELECT * FROM projects WHERE project_id = 'default'")
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
assert row is not None, "Default project not found"
|
||||
assert row["tenant_id"] == "default", "Default project should belong to default tenant"
|
||||
print("[PASS] default project exists under default tenant")
|
||||
|
||||
|
||||
def test_default_quotas_exist():
|
||||
"""Verify default tenant has quotas"""
|
||||
conn = get_db()
|
||||
cursor = conn.execute("SELECT * FROM tenant_quotas WHERE tenant_id = 'default'")
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
assert row is not None, "Default tenant quotas not found"
|
||||
assert row["max_projects"] > 0, "Should have positive project quota"
|
||||
assert row["max_api_calls_per_day"] > 0, "Should have positive API call quota"
|
||||
print("[PASS] default tenant has quotas configured")
|
||||
|
||||
|
||||
def test_agent_metrics_has_tenant_columns():
|
||||
"""Verify agent_metrics table has tenant isolation columns"""
|
||||
conn = get_db()
|
||||
cursor = conn.execute("PRAGMA table_info(agent_metrics)")
|
||||
columns = {row[1] for row in cursor.fetchall()}
|
||||
conn.close()
|
||||
|
||||
assert "tenant_id" in columns, "agent_metrics missing tenant_id"
|
||||
assert "project_id" in columns, "agent_metrics missing project_id"
|
||||
print("[PASS] agent_metrics has tenant isolation columns")
|
||||
|
||||
|
||||
def test_agent_actions_has_tenant_columns():
|
||||
"""Verify agent_actions table has tenant isolation columns"""
|
||||
conn = get_db()
|
||||
cursor = conn.execute("PRAGMA table_info(agent_actions)")
|
||||
columns = {row[1] for row in cursor.fetchall()}
|
||||
conn.close()
|
||||
|
||||
assert "tenant_id" in columns, "agent_actions missing tenant_id"
|
||||
assert "project_id" in columns, "agent_actions missing project_id"
|
||||
print("[PASS] agent_actions has tenant isolation columns")
|
||||
|
||||
|
||||
def test_violations_has_tenant_columns():
|
||||
"""Verify violations table has tenant isolation columns"""
|
||||
conn = get_db()
|
||||
cursor = conn.execute("PRAGMA table_info(violations)")
|
||||
columns = {row[1] for row in cursor.fetchall()}
|
||||
conn.close()
|
||||
|
||||
assert "tenant_id" in columns, "violations missing tenant_id"
|
||||
assert "project_id" in columns, "violations missing project_id"
|
||||
print("[PASS] violations has tenant isolation columns")
|
||||
|
||||
|
||||
def test_promotions_has_tenant_columns():
|
||||
"""Verify promotions table has tenant isolation columns"""
|
||||
conn = get_db()
|
||||
cursor = conn.execute("PRAGMA table_info(promotions)")
|
||||
columns = {row[1] for row in cursor.fetchall()}
|
||||
conn.close()
|
||||
|
||||
assert "tenant_id" in columns, "promotions missing tenant_id"
|
||||
assert "project_id" in columns, "promotions missing project_id"
|
||||
print("[PASS] promotions has tenant isolation columns")
|
||||
|
||||
|
||||
def test_project_members_table_exists():
|
||||
"""Verify project_members table for RBAC"""
|
||||
conn = get_db()
|
||||
cursor = conn.execute("PRAGMA table_info(project_members)")
|
||||
columns = {row[1] for row in cursor.fetchall()}
|
||||
conn.close()
|
||||
|
||||
required = {"project_id", "user_id", "role"}
|
||||
assert required.issubset(columns), f"Missing columns: {required - columns}"
|
||||
print("[PASS] project_members table exists for RBAC")
|
||||
|
||||
|
||||
def test_tenant_index_exists():
|
||||
"""Verify tenant indexes exist for performance"""
|
||||
conn = get_db()
|
||||
cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='index' AND name LIKE '%tenant%'")
|
||||
indexes = [row[0] for row in cursor.fetchall()]
|
||||
conn.close()
|
||||
|
||||
assert len(indexes) > 0, "No tenant indexes found"
|
||||
print(f"[PASS] tenant indexes exist: {len(indexes)} found")
|
||||
|
||||
|
||||
def test_create_tenant():
|
||||
"""Test creating a new tenant"""
|
||||
conn = get_db()
|
||||
test_id = "test-tenant-validate"
|
||||
|
||||
# Clean up first
|
||||
conn.execute("DELETE FROM tenants WHERE tenant_id = ?", (test_id,))
|
||||
conn.commit()
|
||||
|
||||
# Create tenant
|
||||
conn.execute("""
|
||||
INSERT INTO tenants (tenant_id, name, slug, subscription_tier)
|
||||
VALUES (?, 'Test Tenant', 'test-validate', 'free')
|
||||
""", (test_id,))
|
||||
conn.commit()
|
||||
|
||||
# Verify
|
||||
cursor = conn.execute("SELECT * FROM tenants WHERE tenant_id = ?", (test_id,))
|
||||
row = cursor.fetchone()
|
||||
|
||||
# Clean up
|
||||
conn.execute("DELETE FROM tenants WHERE tenant_id = ?", (test_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
assert row is not None, "Failed to create tenant"
|
||||
assert row["name"] == "Test Tenant"
|
||||
print("[PASS] tenant creation works")
|
||||
|
||||
|
||||
def test_create_project_with_tenant():
|
||||
"""Test creating a project under a tenant"""
|
||||
conn = get_db()
|
||||
tenant_id = "test-tenant-proj"
|
||||
project_id = "test-project-validate"
|
||||
|
||||
# Clean up
|
||||
conn.execute("DELETE FROM projects WHERE project_id = ?", (project_id,))
|
||||
conn.execute("DELETE FROM tenants WHERE tenant_id = ?", (tenant_id,))
|
||||
conn.commit()
|
||||
|
||||
# Create tenant and project
|
||||
conn.execute("""
|
||||
INSERT INTO tenants (tenant_id, name, slug) VALUES (?, 'Test', 'test')
|
||||
""", (tenant_id,))
|
||||
conn.execute("""
|
||||
INSERT INTO projects (project_id, tenant_id, name, slug)
|
||||
VALUES (?, ?, 'Test Project', 'test-proj')
|
||||
""", (project_id, tenant_id))
|
||||
conn.commit()
|
||||
|
||||
# Verify foreign key relationship
|
||||
cursor = conn.execute("""
|
||||
SELECT p.*, t.name as tenant_name
|
||||
FROM projects p
|
||||
JOIN tenants t ON p.tenant_id = t.tenant_id
|
||||
WHERE p.project_id = ?
|
||||
""", (project_id,))
|
||||
row = cursor.fetchone()
|
||||
|
||||
# Clean up
|
||||
conn.execute("DELETE FROM projects WHERE project_id = ?", (project_id,))
|
||||
conn.execute("DELETE FROM tenants WHERE tenant_id = ?", (tenant_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
assert row is not None, "Failed to create project"
|
||||
assert row["tenant_name"] == "Test"
|
||||
print("[PASS] project creation with tenant relationship works")
|
||||
|
||||
|
||||
def test_tenant_isolation_query():
|
||||
"""Test that queries can filter by tenant"""
|
||||
conn = get_db()
|
||||
|
||||
# Query agents with tenant filter
|
||||
cursor = conn.execute("""
|
||||
SELECT COUNT(*) as count FROM agent_metrics
|
||||
WHERE tenant_id = 'default' AND project_id = 'default'
|
||||
""")
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
# Should not error - just verify query works
|
||||
assert row is not None, "Tenant-filtered query failed"
|
||||
print(f"[PASS] tenant isolation query works (found {row['count']} agents)")
|
||||
|
||||
|
||||
def test_quota_tracking_insert():
|
||||
"""Test inserting usage tracking records"""
|
||||
conn = get_db()
|
||||
|
||||
# Insert usage record
|
||||
conn.execute("""
|
||||
INSERT OR REPLACE INTO tenant_usage (tenant_id, period_start, api_calls, tokens_used)
|
||||
VALUES ('default', date('now'), 100, 5000)
|
||||
""")
|
||||
conn.commit()
|
||||
|
||||
# Verify
|
||||
cursor = conn.execute("""
|
||||
SELECT * FROM tenant_usage WHERE tenant_id = 'default' AND period_start = date('now')
|
||||
""")
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
assert row is not None, "Failed to track usage"
|
||||
assert row["api_calls"] == 100
|
||||
print("[PASS] quota usage tracking works")
|
||||
|
||||
|
||||
def run_all_tests():
|
||||
"""Run all Phase 10 tests"""
|
||||
print("=" * 60)
|
||||
print("PHASE 10: MULTI-TENANT SUPPORT TESTS")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
tests = [
|
||||
test_tenants_table_exists,
|
||||
test_projects_table_exists,
|
||||
test_tenant_quotas_table_exists,
|
||||
test_tenant_usage_table_exists,
|
||||
test_api_keys_table_exists,
|
||||
test_default_tenant_exists,
|
||||
test_default_project_exists,
|
||||
test_default_quotas_exist,
|
||||
test_agent_metrics_has_tenant_columns,
|
||||
test_agent_actions_has_tenant_columns,
|
||||
test_violations_has_tenant_columns,
|
||||
test_promotions_has_tenant_columns,
|
||||
test_project_members_table_exists,
|
||||
test_tenant_index_exists,
|
||||
test_create_tenant,
|
||||
test_create_project_with_tenant,
|
||||
test_tenant_isolation_query,
|
||||
test_quota_tracking_insert,
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for test in tests:
|
||||
try:
|
||||
test()
|
||||
passed += 1
|
||||
except AssertionError as e:
|
||||
print(f"[FAIL] {test.__name__}: {e}")
|
||||
failed += 1
|
||||
except Exception as e:
|
||||
print(f"[ERROR] {test.__name__}: {e}")
|
||||
failed += 1
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
print(f"RESULTS: {passed} passed, {failed} failed")
|
||||
print("=" * 60)
|
||||
|
||||
return failed == 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = run_all_tests()
|
||||
sys.exit(0 if success else 1)
|
||||
Reference in New Issue
Block a user