Fix workspace detail races and provider default handling

Keep analysis and Skill detail responses bound to the selected request, reject stale publish confirmations, and clear failed position-history results with retry controls.

Reject removal of the current default provider with HTTP 409 and reflect that contract in the form. Track the OpenAPI page and API module by restricting the docs ignore rule to the repository root.

Validate the staged provider contract with four isolated tests. Reuse the matching candidate's frontend lint, type check, 26 test groups, and production build results.
This commit is contained in:
eezd committed 2026-10-26 14:22:40 +08:00
1 parent 3178a2cc31
commit d1a6325ed7
12 files changed
+405 -23

No files matched your search

+3
View File
@@ -67,6 +67,7 @@ class ProviderPatch(BaseModel):
context_window: Annotated[int | None, Field(ge=4096, le=2000000)] = None
# 显式传 null 表示不传思考强度
reasoning_effort: llm.Effort | None = None
# 只能将另一模型设为默认,不能直接取消当前默认模型。
is_default: bool | None = None
@@ -120,6 +121,8 @@ async def create_provider(body: ProviderIn, session: Annotated[AsyncSession, Dep
@router.patch("/providers/{provider_id}")
async def patch_provider(provider_id: int, body: ProviderPatch, session: Annotated[AsyncSession, Depends(get_session)]) -> ApiResponse[ProviderOut]:
provider = await get_provider(session, provider_id)
if body.is_default is False and provider.is_default:
raise HTTPException(status.HTTP_409_CONFLICT, "不能直接取消默认模型,请将其他模型设为默认")
values = body.model_dump(exclude_unset=True, exclude={"api_key", "base_url", "is_default"})
# reasoning_effort 显式传 null 表示清除,其他字段为 null 时忽略
for field, value in values.items():
+53
View File
@@ -0,0 +1,53 @@
"""模型默认配置的接口契约;数据库和认证均隔离。"""
import asyncio
from datetime import UTC, datetime
from types import SimpleNamespace
from unittest.mock import AsyncMock
import httpx
import pytest
@pytest.mark.parametrize("initial,requested,status", [
(True, False, 409), (False, False, 200), (False, True, 200), (True, True, 200),
])
def test_patch_provider_default_contract(trading, monkeypatch, initial, requested, status):
from alphapulse.api import settings
from alphapulse.api.main import app
from alphapulse.core.auth import require_admin
from alphapulse.core.db import get_session
provider = SimpleNamespace(
id=1, name="test", protocol="openai", base_url="https://example.test",
api_key_hint="test", model="test", max_tokens=8192, context_window=200000,
reasoning_effort=None, is_default=initial, updated_at=datetime.now(UTC),
)
session = SimpleNamespace(commit=AsyncMock(), refresh=AsyncMock(), execute=AsyncMock())
monkeypatch.setattr(settings, "get_provider", AsyncMock(return_value=provider))
async def isolated_session():
yield session
app.dependency_overrides[require_admin] = lambda: None
app.dependency_overrides[get_session] = isolated_session
async def check():
async with httpx.AsyncClient(transport=httpx.ASGITransport(app), base_url="http://test") as client:
response = await client.patch("/api/settings/providers/1", json={"is_default": requested})
assert response.status_code == status
body = response.json()
if status == 409:
assert body == {"code": 409, "data": None, "message": "不能直接取消默认模型,请将其他模型设为默认"}
assert provider.is_default is initial
session.commit.assert_not_awaited()
session.execute.assert_not_awaited()
else:
assert body["code"] == 0 and body["data"]["is_default"] is requested
session.commit.assert_awaited_once()
assert session.execute.await_count == int(requested)
try:
asyncio.run(check())
finally:
app.dependency_overrides.clear()