test_agentpaas.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. """Tests for agentpaas core components."""
  2. import pytest
  3. from agentpaas.config import AgentPaaSConfig
  4. from agentpaas.engine.cache import CompileCache
  5. from agentpaas.engine.retry import RetryPolicy, CircuitBreaker
  6. from agentpaas.billing.pricing import calculate_cost, PRICING
  7. from agentpaas.observability.metrics import Metrics
  8. from agentpaas.tenant.rbac import check_permission
  9. from agentpaas.api.middleware.auth import TenantContext, hash_key, verify_key_hash
  10. # Config
  11. def test_config_defaults():
  12. cfg = AgentPaaSConfig()
  13. assert cfg.port == 8000
  14. assert "sqlite" in cfg.database_url
  15. # CompileCache
  16. def test_cache_hit_miss():
  17. cache = CompileCache(maxsize=3)
  18. cache.put("h1", "term1")
  19. assert cache.get("h1") == "term1"
  20. assert cache.get("h2") is None
  21. assert cache.hits == 1
  22. assert cache.misses == 1
  23. def test_cache_lru():
  24. cache = CompileCache(maxsize=2)
  25. cache.put("a", 1)
  26. cache.put("b", 2)
  27. cache.put("c", 3) # evicts "a"
  28. assert cache.get("a") is None
  29. assert cache.get("c") == 3
  30. # RetryPolicy
  31. def test_retry_should_retry():
  32. rp = RetryPolicy(max_attempts=3)
  33. assert rp.should_retry(TimeoutError("timeout"), 0) == True
  34. assert rp.should_retry(TimeoutError("timeout"), 3) == False
  35. assert rp.should_retry(ValueError("bad"), 0) == False
  36. def test_retry_delay():
  37. rp = RetryPolicy(base_delay=1.0, exponential_base=2.0, max_delay=10.0)
  38. assert rp.get_delay(0) == 1.0
  39. assert rp.get_delay(1) == 2.0
  40. assert rp.get_delay(2) == 4.0
  41. assert rp.get_delay(10) == 10.0 # capped
  42. # CircuitBreaker
  43. def test_circuit_breaker_flow():
  44. cb = CircuitBreaker(failure_threshold=3, recovery_timeout=0.1)
  45. assert cb.allow_request() == True
  46. cb.record_failure()
  47. cb.record_failure()
  48. assert cb.allow_request() == True # not yet at threshold
  49. cb.record_failure()
  50. assert cb.state == CircuitBreaker.OPEN
  51. assert cb.allow_request() == False
  52. def test_circuit_breaker_recovery():
  53. import time
  54. cb = CircuitBreaker(failure_threshold=2, recovery_timeout=0.1)
  55. cb.record_failure()
  56. cb.record_failure()
  57. assert cb.state == CircuitBreaker.OPEN
  58. time.sleep(0.15)
  59. assert cb.allow_request() == True # half-open
  60. cb.record_success()
  61. assert cb.state == CircuitBreaker.CLOSED
  62. # Billing
  63. def test_pricing():
  64. cost = calculate_cost("gpt-4o", 1000, 500)
  65. assert cost > 0
  66. assert cost == (1000 / 1_000_000 * 2.5 + 500 / 1_000_000 * 10.0)
  67. def test_pricing_unknown_model():
  68. cost = calculate_cost("unknown-model", 1000, 1000)
  69. assert cost > 0 # uses default pricing
  70. # Metrics
  71. def test_metrics_counter():
  72. m = Metrics()
  73. m.inc("requests", {"agent": "a1"})
  74. m.inc("requests", {"agent": "a1"})
  75. snap = m.snapshot()
  76. assert snap["counters"]["requests{agent=a1}"] == 2
  77. def test_metrics_observe():
  78. m = Metrics()
  79. m.observe("latency", 100.0)
  80. m.observe("latency", 200.0)
  81. snap = m.snapshot()
  82. assert snap["histograms"]["latency"]["mean"] == 150.0
  83. # RBAC
  84. def test_rbac_admin():
  85. ctx = TenantContext("t1", "org", "pro", role="admin")
  86. assert check_permission(ctx, "tenants:read") == True
  87. assert check_permission(ctx, "agents:execute") == True
  88. def test_rbac_viewer():
  89. ctx = TenantContext("t1", "org", "pro", role="viewer")
  90. assert check_permission(ctx, "agents:read") == True
  91. assert check_permission(ctx, "agents:write") == False
  92. def test_rbac_scopes():
  93. ctx = TenantContext("t1", "org", "pro", role="viewer", scopes=["agents:execute"])
  94. assert check_permission(ctx, "agents:execute") == True
  95. # Auth
  96. def test_hash_key():
  97. # FIX-08: hash_key now uses PBKDF2 with a random salt per call.
  98. # Format: "pbkdf2:<32-hex salt>:<64-hex hash>" — 7 + 32 + 1 + 64 = 104 chars.
  99. h1 = hash_key("ap_test123")
  100. h2 = hash_key("ap_test123")
  101. assert h1.startswith("pbkdf2:")
  102. assert len(h1) == 104
  103. # Random salt → two calls on the same input must NOT collide.
  104. assert h1 != h2, "random-salt PBKDF2 should produce different hashes per call"
  105. # But both must verify back to the original key.
  106. assert verify_key_hash("ap_test123", h1)
  107. assert verify_key_hash("ap_test123", h2)
  108. assert not verify_key_hash("wrong_key", h1)