98 lines
3.6 KiB
Python
98 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"]
|
|
event_types = [event["event_type"] for event in events]
|
|
# Core events that must always be present
|
|
assert "task_created" in event_types
|
|
assert "task_completed" in event_types
|
|
# Memory policy decision is now recorded after task completion
|
|
assert "memory_policy_decision" in event_types
|
|
|
|
|
|
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"
|