Files
NotesAgentic/backend/tests/test_provider_adapters.py
T
admin 36bc1022f1 fix(backend): 修复全面审阅发现的核心问题
修复索引首次失败回滚、Vault 扫描边界、Markdown 代码围栏和 UTF-16 Citation 偏移。

收紧 Plugin 权限与 JSON Schema 校验,补齐 Note Move、Task、Attachment 和 Transcript Tool。

接入 OpenAI SSE 与 Ollama JSONL 真流式输出,修正 Provider PATCH 语义并限制运行时内存保留。

新增对应回归测试,后端测试增至 62 项。
2026-08-28 09:56:07 +08:00

228 lines
7.3 KiB
Python

import asyncio
import json
import httpx
from app.contracts import (
Message,
MessageRole,
ModelEventType,
ModelRequest,
ToolCall,
ToolDefinition,
)
from app.providers.ollama import OllamaProvider
from app.providers.openai_compatible import OpenAICompatibleProvider
class StaticCredentials:
def resolve(self, credential_id: str | None) -> str | None:
return "secret-test-key" if credential_id else None
def run(coroutine):
return asyncio.run(coroutine)
async def collect(stream):
return [event async for event in stream]
def test_openai_compatible_maps_tool_call_and_credentials() -> None:
captured: dict = {}
def handler(request: httpx.Request) -> httpx.Response:
assert request.headers["Authorization"] == "Bearer secret-test-key"
captured.update(json.loads(request.content))
return httpx.Response(
200,
json={
"choices": [
{
"message": {
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": "math.add",
"arguments": '{"left":1,"right":2}',
},
}
],
}
}
],
"usage": {"prompt_tokens": 8, "completion_tokens": 4},
},
)
provider = OpenAICompatibleProvider(
base_url="https://provider.test/v1",
credential_id="openai-test",
credentials=StaticCredentials(),
transport=httpx.MockTransport(handler),
)
turn = run(
provider.complete(
ModelRequest(
provider_id="test",
model="test-model",
messages=[Message(role=MessageRole.user, content="add")],
tools=[
ToolDefinition(
name="math.add",
description="Add numbers",
parameters={"type": "object"},
)
],
)
)
)
assert captured["tools"][0]["function"]["name"] == "math.add"
assert turn.tool_calls[0].name == "math.add"
assert turn.tool_calls[0].arguments == {"left": 1, "right": 2}
assert turn.input_tokens == 8
def test_openai_compatible_preserves_tool_call_context() -> None:
captured: dict = {}
def handler(request: httpx.Request) -> httpx.Response:
captured.update(json.loads(request.content))
return httpx.Response(
200,
json={
"choices": [{"message": {"role": "assistant", "content": "done"}}],
"usage": {},
},
)
provider = OpenAICompatibleProvider(
base_url="https://provider.test/v1",
credential_id=None,
credentials=StaticCredentials(),
transport=httpx.MockTransport(handler),
)
call = ToolCall(
tool_call_id="call_1", name="math.add", arguments={"left": 1, "right": 2}
)
turn = run(
provider.complete(
ModelRequest(
provider_id="test",
model="test-model",
messages=[
Message(role=MessageRole.user, content="add"),
Message(role=MessageRole.assistant, content="", tool_calls=[call]),
Message(
role=MessageRole.tool,
content='{"value":3}',
name="math.add",
tool_call_id="call_1",
),
],
)
)
)
assert captured["messages"][1]["tool_calls"][0]["id"] == "call_1"
assert captured["messages"][2]["tool_call_id"] == "call_1"
assert turn.text == "done"
def test_ollama_maps_models_and_completion() -> None:
def handler(request: httpx.Request) -> httpx.Response:
if request.url.path == "/api/tags":
return httpx.Response(200, json={"models": [{"name": "qwen3:latest"}]})
return httpx.Response(
200,
json={
"message": {"role": "assistant", "content": "local answer"},
"prompt_eval_count": 5,
"eval_count": 2,
},
)
provider = OllamaProvider(transport=httpx.MockTransport(handler))
models = run(provider.list_models())
turn = run(
provider.complete(
ModelRequest(
provider_id="ollama",
model="qwen3:latest",
messages=[Message(role=MessageRole.user, content="hello")],
)
)
)
assert models[0].model == "qwen3:latest"
assert turn.text == "local answer"
assert turn.input_tokens == 5
assert turn.output_tokens == 2
def test_openai_compatible_streams_incremental_sse() -> None:
def handler(request: httpx.Request) -> httpx.Response:
payload = json.loads(request.content)
assert payload["stream"] is True
body = "\n".join(
[
'data: {"choices":[{"delta":{"content":"hel"}}]}',
'data: {"choices":[{"delta":{"content":"lo"},"finish_reason":"stop"}]}',
'data: {"choices":[],"usage":{"prompt_tokens":2,"completion_tokens":1}}',
"data: [DONE]",
"",
]
)
return httpx.Response(200, text=body)
provider = OpenAICompatibleProvider(
base_url="https://provider.test/v1",
credential_id=None,
credentials=StaticCredentials(),
transport=httpx.MockTransport(handler),
)
request = ModelRequest(
provider_id="test", model="model",
messages=[Message(role=MessageRole.user, content="hello")],
)
events = run(collect(provider.stream(request)))
assert [item.data["text"] for item in events if item.event == ModelEventType.text_delta] == [
"hel", "lo"
]
assert events[-1].event == ModelEventType.done
assert [item.sequence for item in events] == list(range(len(events)))
def test_ollama_streams_incremental_jsonl() -> None:
def handler(request: httpx.Request) -> httpx.Response:
payload = json.loads(request.content)
assert payload["stream"] is True
body = "\n".join(
[
'{"message":{"content":"本"},"done":false}',
'{"message":{"content":"地"},"done":false}',
'{"message":{"content":""},"done":true,"prompt_eval_count":3,"eval_count":2}',
"",
]
)
return httpx.Response(200, text=body)
provider = OllamaProvider(transport=httpx.MockTransport(handler))
request = ModelRequest(
provider_id="ollama", model="qwen",
messages=[Message(role=MessageRole.user, content="hello")],
)
events = run(collect(provider.stream(request)))
assert [item.data["text"] for item in events if item.event == ModelEventType.text_delta] == [
"本", "地"
]
assert events[-2].event == ModelEventType.usage
assert events[-1].event == ModelEventType.done