mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
- 删除 aether-hub、aether-proxy 独立项目及其 Dockerfile/配置 - 删除 crates/aether-executor 和 crates/aether-gateway 全部模块 - 新增 apps/ 目录作为应用入口 - 将 hub 概念重构为 gateway tunnel transport - 将 executor 重构为 execution runtime - 新增 tunnel.rs 合约定义和 testkit tunnel/execution_runtime 模块 - 更新 Python 服务层和测试适配新架构命名
290 lines
8.7 KiB
Python
290 lines
8.7 KiB
Python
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
|
|
from src.config import config as runtime_config
|
|
from src.services.provider_ops.service import ProviderOpsService
|
|
from src.services.provider_ops.types import (
|
|
ActionStatus,
|
|
ConnectorAuthType,
|
|
ProviderActionType,
|
|
ProviderOpsConfig,
|
|
)
|
|
from src.services.request.execution_runtime_client import ExecutionRuntimeSyncResult
|
|
|
|
|
|
class _FakeDB:
|
|
new: tuple[Any, ...] = ()
|
|
dirty: tuple[Any, ...] = ()
|
|
deleted: tuple[Any, ...] = ()
|
|
|
|
def in_transaction(self) -> bool:
|
|
return False
|
|
|
|
def commit(self) -> None:
|
|
pass
|
|
|
|
def rollback(self) -> None:
|
|
pass
|
|
|
|
|
|
class _FailingArchitecture:
|
|
def get_verify_endpoint(self) -> str:
|
|
return "/verify"
|
|
|
|
async def prepare_verify_config(
|
|
self,
|
|
_base_url: str,
|
|
_config: dict[str, Any],
|
|
_credentials: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
raise ValueError("invalid refresh token")
|
|
|
|
def build_verify_headers(
|
|
self,
|
|
_config: dict[str, Any],
|
|
_credentials: dict[str, Any],
|
|
) -> dict[str, str]:
|
|
raise AssertionError("build_verify_headers should not be reached")
|
|
|
|
|
|
class _FakeRegistry:
|
|
def __init__(self, architecture: Any) -> None:
|
|
self._architecture = architecture
|
|
|
|
def get_or_default(self, _architecture_id: str) -> Any:
|
|
return self._architecture
|
|
|
|
|
|
class _ExplodingConnector:
|
|
async def is_authenticated(self) -> bool:
|
|
raise AssertionError("Python connector auth check should not run")
|
|
|
|
def get_client(self) -> Any:
|
|
raise AssertionError("Python connector client should not be created")
|
|
|
|
|
|
class _SuccessResult:
|
|
def __init__(self) -> None:
|
|
self.success = True
|
|
self.quota = 200.0
|
|
self.extra = {"window": "day"}
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {"success": True, "quota": self.quota}
|
|
|
|
|
|
class _SuccessArchitecture:
|
|
default_action_configs = {ProviderActionType.QUERY_BALANCE: {"quota_divisor": 100}}
|
|
|
|
def get_verify_endpoint(self) -> str:
|
|
return "/verify"
|
|
|
|
async def prepare_verify_config(
|
|
self,
|
|
_base_url: str,
|
|
_config: dict[str, Any],
|
|
_credentials: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
return {}
|
|
|
|
def build_verify_headers(
|
|
self,
|
|
_config: dict[str, Any],
|
|
_credentials: dict[str, Any],
|
|
) -> dict[str, str]:
|
|
return {"authorization": "Bearer test"}
|
|
|
|
def parse_verify_response(self, status_code: int, data: dict[str, Any]) -> _SuccessResult:
|
|
assert status_code == 200
|
|
assert data == {"ok": True}
|
|
return _SuccessResult()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verify_auth_returns_failure_when_prepare_verify_config_raises_value_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service = ProviderOpsService(_FakeDB())
|
|
architecture = _FailingArchitecture()
|
|
|
|
monkeypatch.setattr(
|
|
"src.services.provider_ops.service.get_registry",
|
|
lambda: _FakeRegistry(architecture),
|
|
)
|
|
|
|
result = await service.verify_auth(
|
|
base_url="https://example.com",
|
|
architecture_id="sub2api",
|
|
auth_type=ConnectorAuthType.SESSION_LOGIN,
|
|
config={},
|
|
credentials={"refresh_token": "stale-token"},
|
|
)
|
|
|
|
assert result == {"success": False, "message": "invalid refresh token"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verify_auth_prefers_rust_executor(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
from src.services.provider_ops import service as module
|
|
from src.services.request import execution_runtime_client as runtime_module
|
|
|
|
service = ProviderOpsService(_FakeDB())
|
|
architecture = _SuccessArchitecture()
|
|
|
|
monkeypatch.setattr(
|
|
module,
|
|
"get_registry",
|
|
lambda: _FakeRegistry(architecture),
|
|
)
|
|
monkeypatch.setattr(module.config, "executor_backend", "rust")
|
|
monkeypatch.setattr(
|
|
"src.services.proxy_node.resolver.resolve_ops_proxy_config_async",
|
|
AsyncMock(return_value=(None, "node-1")),
|
|
)
|
|
|
|
cache_balance = AsyncMock()
|
|
monkeypatch.setattr(service, "_cache_balance_from_verify", cache_balance)
|
|
|
|
captured: dict[str, Any] = {}
|
|
|
|
async def _fake_execute_sync_json(self: object, plan: Any) -> ExecutionRuntimeSyncResult:
|
|
captured["plan"] = plan
|
|
return ExecutionRuntimeSyncResult(
|
|
status_code=200,
|
|
response_json={"ok": True},
|
|
headers={"content-type": "application/json"},
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
runtime_module.ExecutionRuntimeClient,
|
|
"execute_sync_json",
|
|
_fake_execute_sync_json,
|
|
)
|
|
|
|
result = await service.verify_auth(
|
|
base_url="https://example.com",
|
|
architecture_id="sub2api",
|
|
auth_type=ConnectorAuthType.SESSION_LOGIN,
|
|
config={},
|
|
credentials={"access_token": "token"},
|
|
provider_id="provider-1",
|
|
)
|
|
|
|
assert result == {"success": True, "quota": 200.0}
|
|
assert captured["plan"].method == "GET"
|
|
assert captured["plan"].url == "https://example.com/verify"
|
|
assert captured["plan"].proxy is not None
|
|
assert captured["plan"].proxy.mode == "tunnel"
|
|
assert captured["plan"].proxy.node_id == "node-1"
|
|
cache_balance.assert_awaited_once_with("provider-1", 2.0, {"window": "day"})
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verify_auth_returns_explicit_failure_when_rust_verifier_unavailable(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service = ProviderOpsService(_FakeDB())
|
|
architecture = _SuccessArchitecture()
|
|
|
|
monkeypatch.setattr(
|
|
"src.services.provider_ops.service.get_registry",
|
|
lambda: _FakeRegistry(architecture),
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_try_rust_verify_response",
|
|
AsyncMock(return_value=None),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.services.proxy_node.resolver.resolve_ops_proxy_config_async",
|
|
AsyncMock(return_value=(None, None)),
|
|
)
|
|
|
|
result = await service.verify_auth(
|
|
base_url="https://example.com",
|
|
architecture_id="sub2api",
|
|
auth_type=ConnectorAuthType.SESSION_LOGIN,
|
|
config={},
|
|
credentials={"access_token": "token"},
|
|
)
|
|
|
|
assert result == {"success": False, "message": "认证验证仅支持 Rust executor"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("backend", ["python", "rust"])
|
|
async def test_connect_returns_explicit_failure_without_python_connector(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
backend: str,
|
|
) -> None:
|
|
service = ProviderOpsService(_FakeDB())
|
|
service._connectors["provider-1"] = object() # type: ignore[assignment]
|
|
monkeypatch.setattr(runtime_config, "executor_backend", backend)
|
|
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_get_provider",
|
|
lambda _provider_id: SimpleNamespace(base_url="https://example.com"),
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"get_config",
|
|
lambda _provider_id: ProviderOpsConfig(
|
|
architecture_id="sub2api",
|
|
base_url="https://example.com",
|
|
connector_auth_type=ConnectorAuthType.API_KEY,
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.services.provider_ops.service.get_registry",
|
|
lambda: (_ for _ in ()).throw(
|
|
AssertionError("Python provider connector registry should not be used")
|
|
),
|
|
)
|
|
|
|
success, message = await service.connect("provider-1", {"api_key": "token"})
|
|
|
|
assert success is False
|
|
assert message == "Provider 连接仅支持 Rust executor"
|
|
assert "provider-1" not in service._connectors
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("backend", ["python", "rust"])
|
|
async def test_execute_action_returns_not_supported_without_python_connector(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
backend: str,
|
|
) -> None:
|
|
service = ProviderOpsService(_FakeDB())
|
|
service._connectors["provider-1"] = _ExplodingConnector() # type: ignore[assignment]
|
|
monkeypatch.setattr(runtime_config, "executor_backend", backend)
|
|
|
|
monkeypatch.setattr(
|
|
service,
|
|
"get_config",
|
|
lambda _provider_id: ProviderOpsConfig(
|
|
architecture_id="sub2api",
|
|
base_url="https://example.com",
|
|
connector_auth_type=ConnectorAuthType.API_KEY,
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.services.provider_ops.service.get_registry",
|
|
lambda: (_ for _ in ()).throw(
|
|
AssertionError("Python provider action architecture should not be used")
|
|
),
|
|
)
|
|
|
|
result = await service.execute_action("provider-1", ProviderActionType.QUERY_BALANCE)
|
|
|
|
assert result.status == ActionStatus.NOT_SUPPORTED
|
|
assert result.action_type == ProviderActionType.QUERY_BALANCE
|
|
assert result.message == "Provider 操作仅支持 Rust executor"
|