ducklm/tests/smoke/test_chat_api.py

101 lines
3.6 KiB
Python

from dataclasses import dataclass
import json
from fastapi.testclient import TestClient
from duck_core.api import create_app
from duck_core.model_client import ModelResponse
@dataclass
class FakeResponse:
role: str = "thinker"
model: str = "local-main"
content: str = "Я DuckLM, локальная агентная система."
raw: dict = None
latency_ms: float = 1.0
prompt_tokens: int | None = 1
completion_tokens: int | None = 1
total_tokens: int | None = 2
def test_chat_api_uses_runtime_and_records_events(tmp_path, monkeypatch):
monkeypatch.setenv("DUCK_DB_PATH", str(tmp_path / "duck.sqlite3"))
monkeypatch.setenv("DUCK_SKIP_LIVE_LLM_TESTS", "1")
async def fake_chat(self, role, messages, temperature=None, max_output_tokens=None, response_format=None):
return ModelResponse(
role="thinker",
model="local-main",
content="Я DuckLM, локальная агентная система.",
reasoning_content=None,
raw={},
latency_ms=1.0,
prompt_tokens=1,
completion_tokens=1,
total_tokens=2,
)
monkeypatch.setattr("duck_core.model_client.ModelClient.chat", fake_chat)
app = create_app()
client = TestClient(app)
response = client.post("/v1/chat", json={"message": "Кто ты?", "debug": True})
payload = response.json()
events = client.get(f"/v1/tasks/{payload['task_id']}/events").json()
assert payload["status"] == "completed"
assert "DuckLM" in payload["final_response"]
assert [event["event_type"] for event in events] == [
"task_created",
"model_call_started",
"action_directive_failed",
"model_call_started",
"cognition_response",
"model_call_finished",
"task_completed",
]
def test_chat_api_exposes_pending_approval_from_runtime_tool_gate(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": "run command",
"risk_level": "medium",
"actions": [
{
"tool": "shell_exec_safe",
"args": {"command": "hostname --pending-approval-test"},
"reason": "needs shell command",
}
],
}
),
reasoning_content=None,
raw={},
latency_ms=1.0,
)
raise AssertionError("thinker should not run while approval is pending")
monkeypatch.setattr("duck_core.model_client.ModelClient.chat", fake_chat)
client = TestClient(create_app())
response = client.post("/v1/chat", json={"message": "run uname", "debug": True})
approvals = client.get("/v1/approvals/pending").json()
approval = next(
item for item in approvals if item["task_id"] == response.json()["task_id"]
)
assert response.status_code == 200
assert response.json()["status"] == "waiting_for_approval"
assert approval["normalized_action"]["tool"] == "shell_exec_safe"
assert approval["normalized_action"]["args"]["command"] == "hostname --pending-approval-test"