Independent FastAPI backend for the MaskanX agentic growth platform. Includes the agent runtime, MCP client integrations (Meta Ads, LinkedIn, HubSpot, Tavily, Exa, xAI, Citedy, image generation), PostgreSQL storage for chats and cron jobs, provider and secret management, and the CLI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
254 lines
7.6 KiB
Python
254 lines
7.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Tests for MaskanX AI usage visibility in hosted sandboxes."""
|
|
|
|
import json
|
|
from io import BytesIO
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
from adclaw.app.routers import providers as provider_routes
|
|
from adclaw.providers.models import (
|
|
CustomProviderData,
|
|
ModelInfo,
|
|
ProviderDefinition,
|
|
ProvidersData,
|
|
)
|
|
from adclaw.providers.store import (
|
|
ProviderUsageRequestError,
|
|
fetch_provider_usage,
|
|
read_providers_json,
|
|
)
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
class _FakeResponse:
|
|
def __init__(self, payload: dict):
|
|
self._body = json.dumps(payload).encode("utf-8")
|
|
self.status = 200
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *_args):
|
|
return None
|
|
|
|
def read(self):
|
|
return BytesIO(self._body).read()
|
|
|
|
|
|
def _host_ai_data(api_key: str = "ach_real_secret") -> ProvidersData:
|
|
return ProvidersData(
|
|
custom_providers={
|
|
"maskanx-host-ai": CustomProviderData(
|
|
id="maskanx-host-ai",
|
|
name="MaskanX AI",
|
|
default_base_url="https://real.maskanx.app/api/host-ai/v1",
|
|
base_url="https://real.maskanx.app/api/host-ai/v1",
|
|
api_key=api_key,
|
|
models=[
|
|
ModelInfo(
|
|
id="@cf/meta/llama-3.1-8b-instruct-fp8-fast",
|
|
name="Fast default",
|
|
),
|
|
],
|
|
),
|
|
},
|
|
)
|
|
|
|
|
|
def test_fetch_provider_usage_uses_secret_and_usage_endpoint():
|
|
captured = {}
|
|
|
|
def fake_urlopen(request, timeout=0):
|
|
captured["url"] = request.full_url
|
|
captured["authorization"] = request.get_header("Authorization")
|
|
captured["user_agent"] = request.get_header("User-agent")
|
|
captured["timeout"] = timeout
|
|
return _FakeResponse(
|
|
{
|
|
"provider_id": "maskanx-host-ai",
|
|
"provider_name": "MaskanX AI",
|
|
"messages_limit": 1500,
|
|
"messages_used": 17,
|
|
"messages_remaining": 1483,
|
|
},
|
|
)
|
|
|
|
payload = fetch_provider_usage(
|
|
"maskanx-host-ai",
|
|
data=_host_ai_data(),
|
|
urlopen_fn=fake_urlopen,
|
|
)
|
|
|
|
assert captured == {
|
|
"url": "https://real.maskanx.app/api/host-ai/v1/usage",
|
|
"authorization": "Bearer ach_real_secret",
|
|
"user_agent": "MaskanX/host-ai-usage",
|
|
"timeout": 5.0,
|
|
}
|
|
assert payload["messages_remaining"] == 1483
|
|
assert payload["messages_limit"] == 1500
|
|
|
|
|
|
def test_fetch_provider_usage_requires_configured_secret():
|
|
with pytest.raises(ValueError, match="API key"):
|
|
fetch_provider_usage(
|
|
"maskanx-host-ai",
|
|
data=_host_ai_data(api_key=""),
|
|
urlopen_fn=lambda *_args, **_kwargs: None,
|
|
)
|
|
|
|
|
|
def test_fetch_provider_usage_translates_network_failures():
|
|
def failing_urlopen(*_args, **_kwargs):
|
|
raise OSError("connection refused")
|
|
|
|
with pytest.raises(ProviderUsageRequestError, match="usage request failed"):
|
|
fetch_provider_usage(
|
|
"maskanx-host-ai",
|
|
data=_host_ai_data(),
|
|
urlopen_fn=failing_urlopen,
|
|
)
|
|
|
|
|
|
def test_fetch_provider_usage_rejects_redirect_responses():
|
|
class RedirectResponse:
|
|
status = 302
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *_args):
|
|
return None
|
|
|
|
def read(self):
|
|
raise AssertionError("redirect response body should not be read")
|
|
|
|
with pytest.raises(ProviderUsageRequestError, match="302"):
|
|
fetch_provider_usage(
|
|
"maskanx-host-ai",
|
|
data=_host_ai_data(),
|
|
urlopen_fn=lambda *_args, **_kwargs: RedirectResponse(),
|
|
)
|
|
|
|
|
|
def test_fetch_provider_usage_rejects_host_ai_token_on_unmanaged_origin():
|
|
data = _host_ai_data()
|
|
data.custom_providers["maskanx-host-ai"].base_url = (
|
|
"https://evil.example/api/host-ai/v1"
|
|
)
|
|
|
|
def unexpected_urlopen(*_args, **_kwargs):
|
|
raise AssertionError("Host AI token must not be sent to unmanaged origin")
|
|
|
|
with pytest.raises(ValueError, match="managed MaskanX AI endpoint"):
|
|
fetch_provider_usage(
|
|
"maskanx-host-ai",
|
|
data=data,
|
|
urlopen_fn=unexpected_urlopen,
|
|
)
|
|
|
|
|
|
def test_read_providers_json_does_not_rewrite_provider_file(tmp_path):
|
|
providers_path = tmp_path / "providers.json"
|
|
payload = {
|
|
"providers": {},
|
|
"custom_providers": {
|
|
"maskanx-host-ai": _host_ai_data()
|
|
.custom_providers["maskanx-host-ai"]
|
|
.model_dump(mode="json"),
|
|
},
|
|
"active_llm": {
|
|
"provider_id": "maskanx-host-ai",
|
|
"model": "@cf/meta/llama-3.1-8b-instruct-fp8-fast",
|
|
},
|
|
"fallback": {"enabled": False, "timeout_seconds": 30, "chain": []},
|
|
}
|
|
original = json.dumps(payload, indent=2)
|
|
providers_path.write_text(original, encoding="utf-8")
|
|
|
|
data = read_providers_json(providers_path)
|
|
|
|
assert data.custom_providers["maskanx-host-ai"].api_key == "ach_real_secret"
|
|
assert providers_path.read_text(encoding="utf-8") == original
|
|
|
|
|
|
def test_provider_usage_route_passes_readonly_provider_data(monkeypatch):
|
|
marker = _host_ai_data()
|
|
captured = {}
|
|
|
|
def fake_fetch(provider_id, *, data=None):
|
|
captured["provider_id"] = provider_id
|
|
captured["data"] = data
|
|
return {
|
|
"provider_id": "maskanx-host-ai",
|
|
"messages_limit": 1500,
|
|
"messages_used": 10,
|
|
"messages_remaining": 1490,
|
|
}
|
|
|
|
monkeypatch.setattr(provider_routes, "read_providers_json", lambda: marker)
|
|
monkeypatch.setattr(provider_routes, "fetch_provider_usage", fake_fetch)
|
|
|
|
response = provider_routes.get_provider_usage("maskanx-host-ai")
|
|
|
|
assert response.provider_id == "maskanx-host-ai"
|
|
assert captured == {"provider_id": "maskanx-host-ai", "data": marker}
|
|
|
|
|
|
def test_provider_usage_route_maps_upstream_failures_to_bad_gateway(monkeypatch):
|
|
monkeypatch.setattr(provider_routes, "read_providers_json", _host_ai_data)
|
|
|
|
def failing_fetch(*_args, **_kwargs):
|
|
raise ProviderUsageRequestError("Provider 'maskanx-host-ai' usage request failed.")
|
|
|
|
monkeypatch.setattr(provider_routes, "fetch_provider_usage", failing_fetch)
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
provider_routes.get_provider_usage("maskanx-host-ai")
|
|
|
|
assert exc_info.value.status_code == 502
|
|
|
|
|
|
def test_provider_usage_route_is_threadpool_safe_and_validation_guarded():
|
|
source = (ROOT / "src/adclaw/app/routers/providers.py").read_text(
|
|
encoding="utf-8",
|
|
)
|
|
|
|
assert "def get_provider_usage(" in source
|
|
assert "async def get_provider_usage(" not in source
|
|
assert "except (ValueError, ValidationError)" in source
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_all_providers_prioritizes_adclaw_ai(monkeypatch):
|
|
data = _host_ai_data()
|
|
xiaomi = ProviderDefinition(
|
|
id="xiaomi-codingplan",
|
|
name="Xiaomi Coding Plan",
|
|
models=[ModelInfo(id="mimo-v2.5", name="MiMo v2.5")],
|
|
)
|
|
host_ai = ProviderDefinition(
|
|
id="maskanx-host-ai",
|
|
name="MaskanX AI",
|
|
models=data.custom_providers["maskanx-host-ai"].models,
|
|
is_custom=True,
|
|
)
|
|
|
|
monkeypatch.setattr(provider_routes, "load_providers_json", lambda: data)
|
|
monkeypatch.setattr(
|
|
provider_routes,
|
|
"list_providers",
|
|
lambda: [xiaomi, host_ai],
|
|
)
|
|
|
|
providers = await provider_routes.list_all_providers()
|
|
|
|
assert [provider.id for provider in providers[:2]] == [
|
|
"maskanx-host-ai",
|
|
"xiaomi-codingplan",
|
|
]
|