Files
wehub-resource-sync 6ede33ccdb
Build and Push Docker Images / create_manifest (web, surfsense-web, , cpu) (push) Has been cancelled
Build and Push Docker Images / finalize_release (push) Has been cancelled
Obsidian Plugin Lint / lint (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_web, cpu, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-24.04-arm, linux/arm64, arm64, , runner, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_web, cpu, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-latest, linux/amd64, amd64, , runner, false, cpu) (push) Has been cancelled
Build and Push Docker Images / compute_version (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cpu, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, , production, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cpu, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, , production, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu126, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, -cuda126, production, true, cuda126) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu126, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, -cuda126, production, true, cuda126) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu128, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, -cuda, production, true, cuda) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu128, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, -cuda, production, true, cuda) (push) Has been cancelled
Build and Push Docker Images / verify_digests (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, , cpu) (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, -cuda, cuda) (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, -cuda126, cuda126) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:33:44 +08:00

269 lines
8.1 KiB
Python

"""Unit tests for ``_resolve_agent_billing_for_workspace``."""
from __future__ import annotations
from dataclasses import dataclass
from types import SimpleNamespace
from uuid import UUID, uuid4
import pytest
pytestmark = pytest.mark.unit
class _FakeExecResult:
def __init__(self, obj):
self._obj = obj
def scalars(self):
return self
def first(self):
return self._obj
class _FakeSession:
def __init__(self, responses: list):
self._responses = list(responses)
async def execute(self, _stmt):
if not self._responses:
return _FakeExecResult(None)
return _FakeExecResult(self._responses.pop(0))
@dataclass
class _FakePinResolution:
resolved_llm_config_id: int
resolved_tier: str = "premium"
from_existing_pin: bool = False
def _make_workspace(*, chat_model_id: int | None, user_id: UUID) -> SimpleNamespace:
return SimpleNamespace(id=42, chat_model_id=chat_model_id, user_id=user_id)
def _make_byok_model(
*, id_: int, base_model: str | None = None, model_id: str = "gpt-byok"
) -> SimpleNamespace:
return SimpleNamespace(
id=id_,
model_id=model_id,
catalog={"base_model": base_model} if base_model else {},
connection=SimpleNamespace(enabled=True, workspace_id=42, user_id=None),
)
@pytest.mark.asyncio
async def test_auto_mode_with_thread_id_resolves_to_premium_global(monkeypatch):
from app.services.billable_calls import _resolve_agent_billing_for_workspace
user_id = uuid4()
session = _FakeSession([_make_workspace(chat_model_id=0, user_id=user_id)])
async def _fake_resolve_pin(*_args, **kwargs):
assert kwargs["selected_llm_config_id"] == 0
assert kwargs["thread_id"] == 99
return _FakePinResolution(resolved_llm_config_id=-1, resolved_tier="premium")
def _fake_get_global(cfg_id):
if cfg_id == -1:
return {
"id": -1,
"model_name": "gpt-5.4",
"billing_tier": "premium",
"litellm_params": {"base_model": "gpt-5.4"},
}
return None
import app.services.auto_model_pin_service as pin_module
import app.services.llm_service as llm_module
monkeypatch.setattr(
pin_module, "resolve_or_get_pinned_llm_config_id", _fake_resolve_pin
)
monkeypatch.setattr(llm_module, "get_global_llm_config", _fake_get_global)
owner, tier, base_model = await _resolve_agent_billing_for_workspace(
session, workspace_id=42, thread_id=99
)
assert owner == user_id
assert tier == "premium"
assert base_model == "gpt-5.4"
@pytest.mark.asyncio
async def test_auto_mode_with_thread_id_resolves_to_byok_is_free(monkeypatch):
from app.services.billable_calls import _resolve_agent_billing_for_workspace
user_id = uuid4()
workspace = _make_workspace(chat_model_id=0, user_id=user_id)
byok_model = _make_byok_model(
id_=17, base_model="anthropic/claude-3-haiku", model_id="my-claude"
)
session = _FakeSession([workspace, byok_model])
async def _fake_resolve_pin(*_args, **_kwargs):
return _FakePinResolution(resolved_llm_config_id=17, resolved_tier="free")
import app.services.auto_model_pin_service as pin_module
monkeypatch.setattr(
pin_module, "resolve_or_get_pinned_llm_config_id", _fake_resolve_pin
)
owner, tier, base_model = await _resolve_agent_billing_for_workspace(
session, workspace_id=42, thread_id=99
)
assert owner == user_id
assert tier == "free"
assert base_model == "anthropic/claude-3-haiku"
@pytest.mark.asyncio
async def test_auto_mode_without_thread_id_falls_back_to_free():
from app.services.billable_calls import _resolve_agent_billing_for_workspace
user_id = uuid4()
session = _FakeSession([_make_workspace(chat_model_id=0, user_id=user_id)])
owner, tier, base_model = await _resolve_agent_billing_for_workspace(
session, workspace_id=42, thread_id=None
)
assert owner == user_id
assert tier == "free"
assert base_model == "auto"
@pytest.mark.asyncio
async def test_auto_mode_pin_failure_falls_back_to_free(monkeypatch):
from app.services.billable_calls import _resolve_agent_billing_for_workspace
user_id = uuid4()
session = _FakeSession([_make_workspace(chat_model_id=0, user_id=user_id)])
async def _fake_resolve_pin(*args, **kwargs):
raise ValueError("thread missing")
import app.services.auto_model_pin_service as pin_module
monkeypatch.setattr(
pin_module, "resolve_or_get_pinned_llm_config_id", _fake_resolve_pin
)
owner, tier, base_model = await _resolve_agent_billing_for_workspace(
session, workspace_id=42, thread_id=99
)
assert owner == user_id
assert tier == "free"
assert base_model == "auto"
@pytest.mark.asyncio
async def test_negative_id_premium_global_returns_premium(monkeypatch):
from app.services.billable_calls import _resolve_agent_billing_for_workspace
user_id = uuid4()
session = _FakeSession([_make_workspace(chat_model_id=-1, user_id=user_id)])
def _fake_get_global(cfg_id):
return {
"id": cfg_id,
"model_name": "gpt-5.4",
"billing_tier": "premium",
"litellm_params": {"base_model": "gpt-5.4"},
}
import app.services.llm_service as llm_module
monkeypatch.setattr(llm_module, "get_global_llm_config", _fake_get_global)
owner, tier, base_model = await _resolve_agent_billing_for_workspace(
session, workspace_id=42, thread_id=99
)
assert owner == user_id
assert tier == "premium"
assert base_model == "gpt-5.4"
@pytest.mark.asyncio
async def test_negative_id_missing_base_model_falls_back_to_model_name(monkeypatch):
from app.services.billable_calls import _resolve_agent_billing_for_workspace
user_id = uuid4()
session = _FakeSession([_make_workspace(chat_model_id=-5, user_id=user_id)])
def _fake_get_global(cfg_id):
return {"id": cfg_id, "model_name": "fallback-model", "billing_tier": "premium"}
import app.services.llm_service as llm_module
monkeypatch.setattr(llm_module, "get_global_llm_config", _fake_get_global)
_, tier, base_model = await _resolve_agent_billing_for_workspace(
session, workspace_id=42
)
assert tier == "premium"
assert base_model == "fallback-model"
@pytest.mark.asyncio
async def test_positive_id_byok_is_always_free():
from app.services.billable_calls import _resolve_agent_billing_for_workspace
user_id = uuid4()
workspace = _make_workspace(chat_model_id=23, user_id=user_id)
byok_model = _make_byok_model(id_=23, base_model="anthropic/claude-3.5-sonnet")
session = _FakeSession([workspace, byok_model])
owner, tier, base_model = await _resolve_agent_billing_for_workspace(
session, workspace_id=42
)
assert owner == user_id
assert tier == "free"
assert base_model == "anthropic/claude-3.5-sonnet"
@pytest.mark.asyncio
async def test_positive_id_byok_missing_returns_free_with_empty_base_model():
from app.services.billable_calls import _resolve_agent_billing_for_workspace
user_id = uuid4()
session = _FakeSession([_make_workspace(chat_model_id=99, user_id=user_id)])
owner, tier, base_model = await _resolve_agent_billing_for_workspace(
session, workspace_id=42
)
assert owner == user_id
assert tier == "free"
assert base_model == ""
@pytest.mark.asyncio
async def test_workspace_not_found_raises_value_error():
from app.services.billable_calls import _resolve_agent_billing_for_workspace
with pytest.raises(ValueError, match="Workspace"):
await _resolve_agent_billing_for_workspace(
_FakeSession([None]), workspace_id=999
)
@pytest.mark.asyncio
async def test_chat_model_id_none_raises_value_error():
from app.services.billable_calls import _resolve_agent_billing_for_workspace
user_id = uuid4()
session = _FakeSession([_make_workspace(chat_model_id=None, user_id=user_id)])
with pytest.raises(ValueError, match="chat_model_id"):
await _resolve_agent_billing_for_workspace(session, workspace_id=42)