mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
feat(pool,trace): 账号封禁原因细分与请求追踪 attempted_only 过滤
- 将账号封禁原因从笼统的"账号异常"细分为封禁/停用/需要验证三类, 前后端关键词组同步拆分,号池管理页面展示对应分类标签 - trace API 新增 attempted_only 参数,支持仅返回实际尝试过的候选, 前端时间线组件默认启用过滤,排除 available/unused/skipped 记录
This commit is contained in:
@@ -29,7 +29,7 @@ def test_resolve_from_antigravity_forbidden_metadata() -> None:
|
||||
assert state.reason == "403"
|
||||
|
||||
|
||||
def test_resolve_from_structured_oauth_reason() -> None:
|
||||
def test_resolve_from_structured_oauth_reason_verification() -> None:
|
||||
state = resolve_pool_account_state(
|
||||
provider_type="codex",
|
||||
upstream_metadata=None,
|
||||
@@ -41,15 +41,38 @@ def test_resolve_from_structured_oauth_reason() -> None:
|
||||
assert state.reason == "Google requires verification"
|
||||
|
||||
|
||||
def test_resolve_from_keyword_oauth_reason() -> None:
|
||||
def test_resolve_from_structured_oauth_reason_suspended() -> None:
|
||||
state = resolve_pool_account_state(
|
||||
provider_type="codex",
|
||||
upstream_metadata=None,
|
||||
oauth_invalid_reason="[ACCOUNT_BLOCK] account suspended by admin",
|
||||
)
|
||||
assert state.blocked is True
|
||||
assert state.code == "account_suspended"
|
||||
assert state.label == "账号封禁"
|
||||
assert state.reason == "account suspended by admin"
|
||||
|
||||
|
||||
def test_resolve_from_keyword_oauth_reason_disabled() -> None:
|
||||
state = resolve_pool_account_state(
|
||||
provider_type=None,
|
||||
upstream_metadata={},
|
||||
oauth_invalid_reason="organization has been disabled by admin",
|
||||
)
|
||||
assert state.blocked is True
|
||||
assert state.code == "account_blocked"
|
||||
assert state.label == "账号异常"
|
||||
assert state.code == "account_disabled"
|
||||
assert state.label == "账号停用"
|
||||
|
||||
|
||||
def test_resolve_from_keyword_oauth_reason_verification() -> None:
|
||||
state = resolve_pool_account_state(
|
||||
provider_type=None,
|
||||
upstream_metadata={},
|
||||
oauth_invalid_reason="validation_required: please verify your identity",
|
||||
)
|
||||
assert state.blocked is True
|
||||
assert state.code == "account_verification"
|
||||
assert state.label == "需要验证"
|
||||
|
||||
|
||||
def test_resolve_healthy_state() -> None:
|
||||
@@ -72,21 +95,23 @@ def test_bare_forbidden_not_treated_as_account_block() -> None:
|
||||
assert state.blocked is False
|
||||
|
||||
|
||||
def test_kiro_oauth_reason_text_detected_as_block() -> None:
|
||||
def test_kiro_oauth_reason_text_detected_as_suspended() -> None:
|
||||
state = resolve_pool_account_state(
|
||||
provider_type="kiro",
|
||||
upstream_metadata={},
|
||||
oauth_invalid_reason="账户已封禁: Terms of Service violation",
|
||||
)
|
||||
assert state.blocked is True
|
||||
assert state.code == "account_blocked"
|
||||
assert state.code == "account_suspended"
|
||||
assert state.label == "账号封禁"
|
||||
|
||||
|
||||
def test_antigravity_oauth_reason_text_detected_as_block() -> None:
|
||||
def test_antigravity_oauth_reason_text_detected_as_disabled() -> None:
|
||||
state = resolve_pool_account_state(
|
||||
provider_type="antigravity",
|
||||
upstream_metadata={},
|
||||
oauth_invalid_reason="账户访问被禁止: 403 Forbidden",
|
||||
)
|
||||
assert state.blocked is True
|
||||
assert state.code == "account_blocked"
|
||||
assert state.code == "account_disabled"
|
||||
assert state.label == "账号停用"
|
||||
|
||||
@@ -8,9 +8,16 @@ from unittest.mock import MagicMock
|
||||
from src.api.admin.monitoring.trace import AdminGetRequestTraceAdapter
|
||||
from src.services.request.candidate import RequestCandidateService
|
||||
|
||||
_STARTED_STATUSES = {"pending", "streaming", "success", "failed", "cancelled"}
|
||||
|
||||
def _candidate(*, status: str, latency_ms: int | None = None) -> SimpleNamespace:
|
||||
|
||||
def _candidate(
|
||||
*, status: str, latency_ms: int | None = None, started_at: datetime | None = ... # type: ignore[assignment]
|
||||
) -> SimpleNamespace:
|
||||
now = datetime.now(timezone.utc)
|
||||
resolved_started_at = (
|
||||
(now if status in _STARTED_STATUSES else None) if started_at is ... else started_at
|
||||
)
|
||||
return SimpleNamespace(
|
||||
id=f"cand-{status}",
|
||||
request_id="req-1",
|
||||
@@ -30,7 +37,7 @@ def _candidate(*, status: str, latency_ms: int | None = None) -> SimpleNamespace
|
||||
concurrent_requests=None,
|
||||
extra_data=None,
|
||||
created_at=now,
|
||||
started_at=None,
|
||||
started_at=resolved_started_at,
|
||||
finished_at=None,
|
||||
)
|
||||
|
||||
@@ -80,3 +87,46 @@ def test_trace_returns_unattempted_candidates(monkeypatch: object) -> None:
|
||||
assert response.total_candidates == 2
|
||||
assert len(response.candidates) == 2
|
||||
assert {c.status for c in response.candidates} == {"available", "unused"}
|
||||
|
||||
|
||||
def test_trace_attempted_only_filters_unattempted_candidates(monkeypatch: object) -> None:
|
||||
candidates = [
|
||||
_candidate(status="available"),
|
||||
_candidate(status="unused"),
|
||||
_candidate(status="skipped"),
|
||||
_candidate(status="failed", latency_ms=123),
|
||||
_candidate(status="success", latency_ms=456),
|
||||
]
|
||||
monkeypatch.setattr(
|
||||
RequestCandidateService,
|
||||
"get_candidates_by_request_id",
|
||||
lambda _db, _request_id: candidates,
|
||||
)
|
||||
|
||||
adapter = AdminGetRequestTraceAdapter(request_id="req-1", attempted_only=True)
|
||||
response = asyncio.run(adapter.handle(_context()))
|
||||
|
||||
assert response.total_candidates == 2
|
||||
assert len(response.candidates) == 2
|
||||
assert {c.status for c in response.candidates} == {"failed", "success"}
|
||||
assert response.total_latency_ms == 579
|
||||
|
||||
|
||||
def test_trace_attempted_only_excludes_pending_without_started_at(monkeypatch: object) -> None:
|
||||
candidates = [
|
||||
_candidate(status="available"),
|
||||
_candidate(status="pending", started_at=None),
|
||||
_candidate(status="pending"),
|
||||
]
|
||||
monkeypatch.setattr(
|
||||
RequestCandidateService,
|
||||
"get_candidates_by_request_id",
|
||||
lambda _db, _request_id: candidates,
|
||||
)
|
||||
|
||||
adapter = AdminGetRequestTraceAdapter(request_id="req-1", attempted_only=True)
|
||||
response = asyncio.run(adapter.handle(_context()))
|
||||
|
||||
assert response.total_candidates == 1
|
||||
assert len(response.candidates) == 1
|
||||
assert response.candidates[0].status == "pending"
|
||||
|
||||
Reference in New Issue
Block a user