Files
hunjing/backend/tests/test_api.py
T
chenxin 462d570751 M4b:ai_provider 删除手术①-⑥——神识全面接管模型出口
- ① 后端删除 app/ai_provider/(38 文件,含 anthropic/openai 格式、billing、audio、embedding)、services/ai_provider_admin.py、core/config/model_config.py、tests/ai_provider/(21 文件)
- ② 连带退役 application/agent/(旧 Agent 循环/steer/follow-up/确认注册表)、application/skill_executor.py、app/skills/(python skills 孤儿)、endpoints/audio.py、application/imports.py(ProviderManager 消费方);settings 端点收敛为生成偏好
- ③ runs.py 修剪:AgentRunRegistry/steer/follow-up/confirmations 端点退役,保留 list/get/events/cancel/stream;safety 确认改为直接落库裁决(pending→approved/denied)
- ④ 前端删除:models 页、ModelGatewayView、gateway.ts、GatewayPanel(玄关)、ModelGatewayPanel/ModelStrategyForm、audio.ts/useVoiceInput、网关类型与 mock;设置页收敛为外观/语言/通知
- ⑤ 配置收敛:config/models|providers|pricing.example.yaml 归档 shenshi/config/;.env.example 删 provider keys/MODEL_ENDPOINT_POOL/MODEL_CONFIG_PATH,收敛 SHENSHI_*
- ⑥ 验收:rg 审计灵台 Provider SDK import 与 ai_provider 引用归零;后端 528 passed(2 个预存失败经 HEAD 复跑验证与本手术无关)、前端 type-check+201 passed、神识 go vet+test、BFF 4 passed
2026-08-27 10:02:44 +08:00

267 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""API 集成测试。"""
from collections.abc import AsyncGenerator
import pytest
from httpx import ASGITransport, AsyncClient
from app.api.v1.deps import require_admin
from app.main import app
@pytest.fixture
async def client(
monkeypatch: pytest.MonkeyPatch, auth_headers: dict[str, str]
) -> AsyncGenerator[AsyncClient, None]:
transport = ASGITransport(app=app)
async with AsyncClient(
transport=transport,
base_url="http://test",
headers=auth_headers,
) as http_client:
yield http_client
@pytest.mark.asyncio
async def test_health_and_defaults(client: AsyncClient) -> None:
health = await client.get("/api/v1/health")
assert health.status_code == 200
character = await client.get("/api/v1/characters/active")
assert character.status_code == 200
assert character.json()["id"].startswith("character_")
personality = await client.get("/api/v1/personality/current")
assert personality.status_code == 200
@pytest.mark.asyncio
async def test_personality_propose_approve_reject_and_rollback(client: AsyncClient) -> None:
# 初始当前人格为 v1
before = (await client.get("/api/v1/personality/current")).json()
assert before["version"] == 1
# 空字段提议被拒(invalid_request)
empty = await client.post(
"/api/v1/personality/propose-update",
json={"fields": {}, "reason": "空提议"},
)
assert empty.status_code == 400
# 提交 pending 建议(覆盖 string + json 字段)
propose = await client.post(
"/api/v1/personality/propose-update",
json={
"fields": {"identity_profile": "测试身份", "traits": {"warmth": 0.5}},
"reason": "联调人格建议",
},
)
assert propose.status_code == 201
proposal = propose.json()
assert proposal["status"] == "pending"
assert proposal["fields"] == {"identity_profile": "测试身份", "traits": {"warmth": 0.5}}
proposal_id = proposal["proposal_id"]
# 审核通过:派生新版本并切换当前指针,字段覆盖生效
approve = await client.post(f"/api/v1/personality/proposals/{proposal_id}/approve")
assert approve.status_code == 200
applied = approve.json()
assert applied["status"] == "applied"
assert applied["resulting_version_id"]
after = (await client.get("/api/v1/personality/current")).json()
assert after["version"] == before["version"] + 1
assert after["identity_profile"] == "测试身份"
assert after["traits"]["warmth"] == 0.5
new_version_id = after["id"]
# 重复审核同一建议 → conflict
again = await client.post(f"/api/v1/personality/proposals/{proposal_id}/approve")
assert again.status_code == 409
# 驳回另一条建议:不生成版本,不改当前指针
propose2 = await client.post(
"/api/v1/personality/propose-update",
json={"fields": {"expression_style": "被驳回"}, "reason": "测试驳回"},
)
pid2 = propose2.json()["proposal_id"]
reject = await client.post(f"/api/v1/personality/proposals/{pid2}/reject")
assert reject.status_code == 200
assert reject.json()["status"] == "rejected"
assert (await client.get("/api/v1/personality/current")).json()["id"] == new_version_id
# 回滚到 v1:当前指针切回,旧版本数不变
versions = (await client.get("/api/v1/personality/versions")).json()
v1 = next(v for v in versions["items"] if v["version"] == 1)
rollback = await client.post(f"/api/v1/personality/versions/{v1['id']}/rollback")
assert rollback.status_code == 200
assert (await client.get("/api/v1/personality/current")).json()["id"] == v1["id"]
# 回滚到不存在的版本 → 404
missing = await client.post("/api/v1/personality/versions/personality_nope/rollback")
assert missing.status_code == 404
# 写操作均入起居注(personality_core 枢)
# /admin/* 强制 require_admin 鉴权,联调测试在此局部绕过(鉴权本身覆盖见 test_admin_auth)
app.dependency_overrides[require_admin] = lambda: None
try:
audit = await client.get("/api/v1/admin/audit/timeline?pivot=personality_core")
assert audit.status_code == 200
actions = {item["action"] for item in audit.json()["items"]}
assert "personality.propose" in actions
assert "personality.proposal.approved" in actions
assert "personality.proposal.rejected" in actions
assert "personality.rollback" in actions
finally:
app.dependency_overrides.pop(require_admin, None)
@pytest.mark.asyncio
async def test_skills_crud(client: AsyncClient) -> None:
"""Skill 能力包 CRUD——覆盖 services/skills 路径。"""
created = await client.post(
"/api/v1/tools/skills",
json={
"name": "测试技能",
"description": "用于集成测试",
"trigger": "测试触发",
"prompt_fragment": "测试提示片段",
"tool_bindings": [],
"risk_level": "L0",
},
)
assert created.status_code == 201
skill_id = created.json()["id"]
listed = await client.get("/api/v1/tools/skills")
assert listed.status_code == 200
assert any(item["id"] == skill_id for item in listed.json())
# Skills 没有 GET single 端点,跳过
updated = await client.patch(
f"/api/v1/tools/skills/{skill_id}",
json={"description": "改描述", "trigger": "新触发"},
)
assert updated.status_code == 200
assert updated.json()["trigger"] == "新触发"
deleted = await client.delete(f"/api/v1/tools/skills/{skill_id}")
assert deleted.status_code == 204
@pytest.mark.asyncio
async def test_mcp_server_crud(client: AsyncClient) -> None:
"""MCP 服务器注册 CRUD——覆盖 services/mcp 路径。"""
created = await client.post(
"/api/v1/tools/mcp-servers",
json={
"name": "测试 MCP",
"transport": "stdio",
"command": "echo",
"args_json": '["hello"]',
},
)
assert created.status_code == 201
mcp_id = created.json()["id"]
listed = await client.get("/api/v1/tools/mcp-servers")
assert listed.status_code == 200
assert any(item["id"] == mcp_id for item in listed.json())
# MCP 服务器没有 GET single 端点,跳过
# PATCH: UpdateMcpServerRequest 不含 name 字段,改 command 验证路径可通
updated = await client.patch(
f"/api/v1/tools/mcp-servers/{mcp_id}",
json={"command": "cat", "args_json": '["-n"]'},
)
assert updated.status_code == 200
assert updated.json()["command"] == "cat"
deleted = await client.delete(f"/api/v1/tools/mcp-servers/{mcp_id}")
assert deleted.status_code == 204
@pytest.mark.asyncio
async def test_world_entities_crud(client: AsyncClient) -> None:
"""河图世界实体 CRUD——覆盖 services/world 路径。"""
created = await client.post(
"/api/v1/world/entities",
json={
"name": "测试地点",
"type": "location",
"attributes": {"climate": "温暖"},
"state_type": "fixed",
"sources": [],
},
)
assert created.status_code == 200
entity_id = created.json()["id"]
listed = await client.get("/api/v1/world/entities")
assert listed.status_code == 200
assert len(listed.json()["items"]) >= 1
fetched = await client.get(f"/api/v1/world/entities/{entity_id}")
assert fetched.status_code == 200
assert fetched.json()["name"] == "测试地点"
# 世界实体无 PATCH/DELETE API,仅测试 GET 和建表
deleted = await client.delete(f"/api/v1/world/entities/{entity_id}")
assert deleted.status_code in (404, 405) # 无删除端点,接受未实现
gone = await client.get(f"/api/v1/world/entities/{entity_id}")
assert gone.status_code in (200, 404)
@pytest.mark.asyncio
async def test_safety_policy_crud(client: AsyncClient) -> None:
"""安全策略 CRUD——覆盖 services/governance 部分路径。"""
import uuid
policy_name = f"test-policy-{uuid.uuid4().hex[:8]}"
created = await client.post(
"/api/v1/safety/policies",
json={
"name": policy_name,
"description": "集成测试策略",
"risk_level": "L3",
"permission_policy": "ask",
"side_effect": True,
"audit_required": True,
},
)
assert created.status_code == 201, created.text
data = created.json()
assert data["name"] == policy_name
listed = await client.get("/api/v1/safety/policies")
assert listed.status_code == 200
# SafetyPolicyItem 使用 tool 字段名(不是 name)
assert any(item["tool"] == policy_name for item in listed.json())
updated = await client.patch(
f"/api/v1/safety/policies/{policy_name}",
json={"description": "改后描述", "risk_level": "L2"},
)
assert updated.status_code == 200, updated.text
assert updated.json()["risk_level"] == "L2"
deleted = await client.delete(f"/api/v1/safety/policies/{policy_name}")
assert deleted.status_code == 204
@pytest.mark.asyncio
async def test_preferences_flow(client: AsyncClient) -> None:
"""偏好设置读写——覆盖 services/preferences 路径。"""
fetched = await client.get("/api/v1/settings/preferences")
assert fetched.status_code == 200
assert "temperature" in fetched.json()
updated = await client.patch(
"/api/v1/settings/preferences",
json={"temperature": 0.7, "max_output_tokens": 4096},
)
assert updated.status_code == 200
assert updated.json()["temperature"] == 0.7
assert updated.json()["max_output_tokens"] == 4096