agent-governance/tests/governance/test_phase10_multi_tenant.py
profit 8c6e7831e9 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>
2026-01-24 18:39:47 -05:00

357 lines
11 KiB
Python

#!/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)