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"