130 lines
4.6 KiB
Python
130 lines
4.6 KiB
Python
import json
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from duck_core.api import create_app
|
|
from duck_core.model_client import ModelResponse
|
|
|
|
|
|
def test_conversations_api_stores_different_workspaces(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("DUCK_DB_PATH", str(tmp_path / "duck.sqlite3"))
|
|
client = TestClient(create_app())
|
|
|
|
first = client.post(
|
|
"/v1/conversations",
|
|
json={"title": "Project A", "workspace": "/tmp/project-a"},
|
|
).json()
|
|
second = client.post(
|
|
"/v1/conversations",
|
|
json={"title": "Project B", "workspace": "/tmp/project-b"},
|
|
).json()
|
|
listed = client.get("/v1/conversations").json()
|
|
|
|
assert first["conversation_id"] != second["conversation_id"]
|
|
assert {item["workspace"] for item in listed} >= {"/tmp/project-a", "/tmp/project-b"}
|
|
|
|
|
|
def test_chat_api_persists_messages_in_conversation(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("DUCK_DB_PATH", str(tmp_path / "duck.sqlite3"))
|
|
|
|
async def fake_chat(self, role, messages, temperature=None, max_output_tokens=None, response_format=None):
|
|
if role == "action":
|
|
return ModelResponse(
|
|
role=role,
|
|
model="local-main",
|
|
content=json.dumps(
|
|
{
|
|
"kind": "action_directive",
|
|
"intent": "answer directly",
|
|
"risk_level": "none",
|
|
"actions": [],
|
|
}
|
|
),
|
|
reasoning_content=None,
|
|
raw={},
|
|
latency_ms=1.0,
|
|
)
|
|
return ModelResponse(
|
|
role=role,
|
|
model="local-main",
|
|
content="Первый ответ сохранен.",
|
|
reasoning_content="short reasoning",
|
|
raw={},
|
|
latency_ms=1.0,
|
|
)
|
|
|
|
monkeypatch.setattr("duck_core.model_client.ModelClient.chat", fake_chat)
|
|
client = TestClient(create_app())
|
|
conversation = client.post(
|
|
"/v1/conversations",
|
|
json={"title": "Saved chat", "workspace": str(tmp_path)},
|
|
).json()
|
|
|
|
response = client.post(
|
|
"/v1/chat",
|
|
json={
|
|
"conversation_id": conversation["conversation_id"],
|
|
"message": "Запомни это сообщение",
|
|
"debug": True,
|
|
},
|
|
).json()
|
|
loaded = client.get(f"/v1/conversations/{conversation['conversation_id']}").json()
|
|
|
|
assert response["conversation_id"] == conversation["conversation_id"]
|
|
assert loaded["workspace"] == str(tmp_path)
|
|
assert [message["role"] for message in loaded["messages"]] == ["user", "assistant"]
|
|
assert loaded["messages"][0]["content"] == "Запомни это сообщение"
|
|
assert loaded["messages"][1]["content"] == "Первый ответ сохранен."
|
|
assert loaded["messages"][1]["reasoning_content"] == "short reasoning"
|
|
|
|
|
|
def test_conversation_history_is_sent_to_model(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("DUCK_DB_PATH", str(tmp_path / "duck.sqlite3"))
|
|
seen_thinker_messages = []
|
|
|
|
async def fake_chat(self, role, messages, temperature=None, max_output_tokens=None, response_format=None):
|
|
if role == "action":
|
|
return ModelResponse(
|
|
role=role,
|
|
model="local-main",
|
|
content=json.dumps(
|
|
{
|
|
"kind": "action_directive",
|
|
"intent": "answer directly",
|
|
"risk_level": "none",
|
|
"actions": [],
|
|
}
|
|
),
|
|
reasoning_content=None,
|
|
raw={},
|
|
latency_ms=1.0,
|
|
)
|
|
seen_thinker_messages.append(messages)
|
|
return ModelResponse(
|
|
role=role,
|
|
model="local-main",
|
|
content=f"answer {len(seen_thinker_messages)}",
|
|
reasoning_content=None,
|
|
raw={},
|
|
latency_ms=1.0,
|
|
)
|
|
|
|
monkeypatch.setattr("duck_core.model_client.ModelClient.chat", fake_chat)
|
|
client = TestClient(create_app())
|
|
conversation = client.post(
|
|
"/v1/conversations",
|
|
json={"title": "History", "workspace": str(tmp_path)},
|
|
).json()
|
|
|
|
client.post(
|
|
"/v1/chat",
|
|
json={"conversation_id": conversation["conversation_id"], "message": "first"},
|
|
)
|
|
client.post(
|
|
"/v1/chat",
|
|
json={"conversation_id": conversation["conversation_id"], "message": "second"},
|
|
)
|
|
|
|
second_call_content = [message["content"] for message in seen_thinker_messages[-1]]
|
|
assert second_call_content == ["first", "answer 1", "second"]
|