from __future__ import annotations import json import re import sys import threading from http.server import ThreadingHTTPServer from pathlib import Path from typing import Any from urllib.error import HTTPError from urllib.request import Request, urlopen import pytest ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "plugins" / "1c" / "agent")) import agent_server as onec_agent # noqa: E402 from core.observability import JsonlAuditStore # noqa: E402 try: import yaml except ModuleNotFoundError: yaml = None def request_json( method: str, *, url: str, path: str, payload: dict[str, Any] | None = None, request_id: str | None = None, ) -> tuple[int, dict[str, Any], bool]: data = json.dumps(payload or {}).encode("utf-8") if payload is not None else None req = Request( f"{url}{path}", data=data, method=method, headers={"Content-Type": "application/json"}, ) if request_id: req.add_header("X-Request-Id", request_id) try: with urlopen(req, timeout=3) as response: body = response.read().decode("utf-8") return response.status, json.loads(body) if body else {}, False except HTTPError as exc: body = exc.read().decode("utf-8") return exc.code, json.loads(body) if body else {}, True def request_raw(method: str, *, url: str, path: str) -> tuple[int, str]: req = Request(f"{url}{path}", method=method) try: with urlopen(req, timeout=3) as response: return response.status, response.read().decode("utf-8") except HTTPError as exc: return exc.code, exc.read().decode("utf-8", errors="replace") @pytest.fixture def onec_agent_server(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr( onec_agent, "route_for_model", lambda model_id=None, *, plugin=onec_agent.DEFAULT_PLUGIN: { "plugin": plugin, "model": {"id": model_id or "stub-model", "task": ["1c"]}, "base_url": "http://127.0.0.1:8000", "served_model_name": "stub-model", "container_name": None, "host": None, "role": None, }, ) monkeypatch.setattr( onec_agent, "call_model", lambda provider, model, messages, temperature=0.2, max_tokens=1200, base_url=None: { "text": f"Stub answer for {model} via {provider.name}", "raw": {"model": model, "provider": provider.name, "messages": messages}, "latency_ms": 0, "provider_base_url": base_url or provider.base_url, }, ) providers = { "default": onec_agent.ProviderConfig( name="default", data={ "type": "openai-compatible", "base_url": "http://127.0.0.1:8000", "model": "stub-model", }, ), } store = onec_agent.Store(tmp_path / "onec-agent.db") audit_store = JsonlAuditStore(tmp_path / "reports" / "observability" / "onec-agent", service="onec-agent") def _handler(*args: Any, **kwargs: Any) -> None: onec_agent.AgentHandler( *args, store=store, providers=providers, system_prompt="Ты помощник по 1С", audit_store=audit_store, **kwargs, ) server = ThreadingHTTPServer(("127.0.0.1", 0), _handler) thread = threading.Thread(target=server.serve_forever, kwargs={"poll_interval": 0.05}, daemon=True) thread.start() try: yield { "url": f"http://127.0.0.1:{server.server_address[1]}", "audit_root": str(tmp_path / "reports" / "observability" / "onec-agent"), } finally: server.shutdown() thread.join(timeout=3) server.server_close() def _assert_has_trace(payload: dict[str, Any], expected: str | None = None) -> None: trace = payload.get("trace_id") assert isinstance(trace, str) if expected is not None: assert trace == expected return assert re.fullmatch(r"[0-9a-f]{32}", trace) is not None def test_health_is_alive(onec_agent_server: dict[str, str]) -> None: status, payload, is_error = request_json("GET", url=onec_agent_server["url"], path="/v1/health") assert not is_error assert status == 200 assert payload["status"] == "ok" _assert_has_trace(payload) def test_ui_served(onec_agent_server: dict[str, str]) -> None: url = onec_agent_server["url"] status, body = request_raw("GET", url=url, path="/") assert status == 200 assert "1С Agent" in body status, js_body = request_raw("GET", url=url, path="/ui/assets/app.js") assert status == 200 assert "sendTurn" in js_body def test_state_endpoint(onec_agent_server: dict[str, str]) -> None: url = onec_agent_server["url"] request_json("POST", url=url, path="/v1/projects", payload={"name": "Состояние"}) request_json("POST", url=url, path="/v1/projects", payload={"name": "Состояние-2"}) status, listing, _ = request_json("GET", url=url, path="/v1/projects") project_id = listing["projects"][0]["id"] status, got_project, _ = request_json("GET", url=url, path=f"/v1/projects/{project_id}") assert status == 200 assert got_project["project"]["id"] == project_id request_json("POST", url=url, path=f"/v1/projects/{project_id}/chats", payload={"title": "СтатусДиалог"}) status, state, is_error = request_json("GET", url=url, path="/v1/state") assert not is_error assert status == 200 assert state["service"] == "onec-agent" assert state["state"]["projects"] >= 2 assert state["state"]["chats"] >= 1 assert "started_at" in state assert "uptime_seconds" in state assert "default" in state["providers"]["ids"] _assert_has_trace(state) def test_openapi_contract_smoke() -> None: contract = ROOT / "plugins" / "1c" / "agent" / "openapi.yaml" assert contract.exists() if yaml is None: return with contract.open("r", encoding="utf-8") as stream: data = yaml.safe_load(stream) assert data["openapi"].startswith("3.") paths = set(data.get("paths", {}).keys()) expected = { "/v1/health", "/v1/state", "/v1/projects", "/v1/projects/{project_id}", "/v1/projects/{project_id}/chats", "/v1/projects/{project_id}/chats/{chat_id}", "/v1/projects/{project_id}/chats/{chat_id}/messages", "/v1/projects/{project_id}/chats/{chat_id}/runtime", "/v1/projects/{project_id}/chats/{chat_id}/turn", "/v1/projects/{project_id}/chats/{chat_id}/onec-tool", "/v1/models", "/v1/providers", } assert expected.issubset(paths) def test_project_chat_crud_flow(onec_agent_server: dict[str, str]) -> None: url = onec_agent_server["url"] status, created, _ = request_json( "POST", url=url, path="/v1/projects", payload={"name": "Проект", "description": "Исходный", "metadata": {"owner": "test"}}, request_id="req-1", ) assert status == 201 project_id = created["project"]["id"] _assert_has_trace(created, expected="req-1") status, listing, _ = request_json("GET", url=url, path="/v1/projects") assert status == 200 assert [p["id"] for p in listing["projects"]] == [project_id] status, updated, _ = request_json("PATCH", url=url, path=f"/v1/projects/{project_id}", payload={"description": "Обновленный"}) assert status == 200 assert updated["project"]["description"] == "Обновленный" status, chat_created, _ = request_json( "POST", url=url, path=f"/v1/projects/{project_id}/chats", payload={"title": "Диалог", "temperature": 0.12, "max_tokens": 512}, ) assert status == 201 chat_id = chat_created["chat"]["id"] status, got_chat, _ = request_json("GET", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}") assert status == 200 assert got_chat["chat"]["id"] == chat_id status, messages_before, _ = request_json( "GET", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}/messages", ) assert status == 200 assert messages_before["messages"] == [] status, _msg, _ = request_json( "POST", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}/messages", payload={"role": "user", "content": "Проверка"}, ) assert status == 201 status, messages, _ = request_json( "GET", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}/messages?limit=10", ) assert status == 200 assert len(messages["messages"]) == 1 assert messages["messages"][0]["content"] == "Проверка" status, chat, _ = request_json( "PATCH", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}", payload={"title": "Диалог 2", "rag_profile": "official", "temperature": 0.5}, ) assert status == 200 assert chat["chat"]["title"] == "Диалог 2" assert chat["chat"]["temperature"] == 0.5 status, bad, _is_error = request_json( "PATCH", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}", payload={"metadata": []}, ) assert status == 400 assert bad["error"]["code"] == "invalid_argument" _assert_has_trace(bad) def test_delete_and_validation_paths(onec_agent_server: dict[str, str]) -> None: url = onec_agent_server["url"] status, created, _ = request_json("POST", url=url, path="/v1/projects", payload={"name": "ДляУдаления"}) project_id = created["project"]["id"] status, chat_created, _ = request_json( "POST", url=url, path=f"/v1/projects/{project_id}/chats", payload={"title": "Удаляемый"}, ) chat_id = chat_created["chat"]["id"] status, bad, is_error = request_json( "GET", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}/messages?limit=abc", ) assert status == 400 assert is_error assert bad["error"]["code"] == "invalid_argument" status, bad_limit, is_error = request_json( "GET", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}/messages?limit=0", ) assert status == 400 assert is_error assert bad_limit["error"]["code"] == "invalid_argument" status, _, _ = request_json("DELETE", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}") assert status == 204 status, _, is_error = request_json( "GET", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}", ) assert status == 404 assert is_error status, _, _ = request_json("DELETE", url=url, path=f"/v1/projects/{project_id}") assert status == 204 status, _, is_error = request_json( "GET", url=url, path=f"/v1/projects/{project_id}", ) assert status == 404 assert is_error def test_turn_runtime_question_fastpath(onec_agent_server: dict[str, str]) -> None: url = onec_agent_server["url"] status, project_data, _ = request_json("POST", url=url, path="/v1/projects", payload={"name": "Проверка runtime вопроса"}) assert status == 201 project_id = project_data["project"]["id"] status, chat_data, _ = request_json( "POST", url=url, path=f"/v1/projects/{project_id}/chats", payload={"title": "Runtime"}, ) assert status == 201 chat_id = chat_data["chat"]["id"] status, turn_data, is_error = request_json( "POST", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}/turn", payload={"message": "Какая модель ии подключена? Какой адаптер и rag?"}, ) assert status == 200 assert not is_error assert "runtime-конфигурация" in turn_data["assistant_message"]["content"] assert turn_data["assistant_message"]["payload"]["guardrail"] == {"type": "runtime_direct_answer"} assert turn_data["assistant_message"]["payload"]["raw"]["reason"] == "runtime_question_fastpath" _assert_has_trace(turn_data) def test_turn_persists_full_payload_journal(onec_agent_server: dict[str, str]) -> None: url = onec_agent_server["url"] status, project_data, _ = request_json("POST", url=url, path="/v1/projects", payload={"name": "Журнал-чата"}) assert status == 201 project_id = project_data["project"]["id"] status, chat_data, _ = request_json( "POST", url=url, path=f"/v1/projects/{project_id}/chats", payload={"title": "Смотрим payload"}, ) assert status == 201 chat_id = chat_data["chat"]["id"] status, turn_data, _ = request_json( "POST", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}/turn", payload={"message": "Опиши коротко, как проверить проблему в 1С.", "use_rag": True}, ) assert status == 200 assistant_payload = turn_data["assistant_message"]["payload"] assert assistant_payload["transport"]["outbound"]["provider_id"] assert assistant_payload["transport"]["inbound"]["provider_response"] assert assistant_payload["transport"]["outbound"]["model"]["id"] user_payload = turn_data["user_message"]["payload"] assert user_payload["transport"]["outbound"]["runtime"]["history_limit"] status, all_messages, _ = request_json( "GET", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}/messages?limit=50", ) assert status == 200 assert len(all_messages["messages"]) == 2 assert "transport" in all_messages["messages"][0]["payload"] assert "transport" in all_messages["messages"][1]["payload"] def test_turn_writes_audit_jsonl(onec_agent_server: dict[str, str]) -> None: url = onec_agent_server["url"] status, project_data, _ = request_json("POST", url=url, path="/v1/projects", payload={"name": "Audit log"}, request_id="req-audit-1") assert status == 201 project_id = project_data["project"]["id"] status, chat_data, _ = request_json( "POST", url=url, path=f"/v1/projects/{project_id}/chats", payload={"title": "Audit chat"}, request_id="req-audit-2", ) assert status == 201 chat_id = chat_data["chat"]["id"] status, turn_data, is_error = request_json( "POST", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}/turn", payload={"message": "Опиши коротко, как проверить проблему в 1С.", "use_rag": False}, request_id="req-audit-3", ) assert status == 200 assert not is_error assert turn_data["turn_id"] audit_root = Path(onec_agent_server["audit_root"]) access_files = sorted((audit_root / "access_events").glob("*.jsonl")) turn_files = sorted((audit_root / "turn_audit").glob("*.jsonl")) model_files = sorted((audit_root / "model_calls").glob("*.jsonl")) assert access_files assert turn_files assert model_files access_records = [json.loads(line) for line in access_files[-1].read_text(encoding="utf-8").splitlines() if line.strip()] turn_records = [json.loads(line) for line in turn_files[-1].read_text(encoding="utf-8").splitlines() if line.strip()] model_records = [json.loads(line) for line in model_files[-1].read_text(encoding="utf-8").splitlines() if line.strip()] assert any(record["request_id"] == "req-audit-3" and record["status_code"] == 200 for record in access_records) assert any(record["turn_id"] == turn_data["turn_id"] and record["outcome"] == "success" for record in turn_records) assert any(record["turn_id"] == turn_data["turn_id"] and record["served_model_name"] == "stub-model" for record in model_records) def test_turn_builds_agent_context_with_rag_tools_and_order( onec_agent_server: dict[str, str], monkeypatch: pytest.MonkeyPatch, ) -> None: url = onec_agent_server["url"] monkeypatch.setattr( onec_agent, "build_rag_prompt", lambda question, profile_name="auto", limit=None: { "prompt": f"RAG PROMPT profile={profile_name} limit={limit}: {question}", "profile": profile_name, "context_count": 1, "sources": [{"source_path": "stub.md", "title": "Stub", "chunk_index": 0, "score": 1.0}], }, ) monkeypatch.setattr( onec_agent, "call_adapter", lambda method, params, base_url=None: {"ok": True, "method": method, "params": params}, ) status, project_data, _ = request_json("POST", url=url, path="/v1/projects", payload={"name": "Контекст turn"}) assert status == 201 project_id = project_data["project"]["id"] status, chat_data, _ = request_json( "POST", url=url, path=f"/v1/projects/{project_id}/chats", payload={"title": "context", "system_prompt": "Chat-only policy", "rag_profile": "official", "rag_limit": 3}, ) assert status == 201 chat_id = chat_data["chat"]["id"] request_json("POST", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}/messages", payload={"role": "user", "content": "first"}) request_json("POST", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}/messages", payload={"role": "assistant", "content": "second"}) status, turn_data, is_error = request_json( "POST", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}/turn", payload={ "message": "Найди реквизит документа", "adapter_calls": [{"method": "tool.echo", "params": {"value": 7}}], "use_rag": True, }, ) assert status == 200 assert not is_error assert turn_data["rag"]["profile"] == "official" assert turn_data["tools"][0]["method"] == "tool.echo" messages = turn_data["assistant_message"]["payload"]["raw"]["messages"] assert messages[0]["role"] == "system" assert "Ты помощник по 1С" in messages[0]["content"] assert "Chat-only policy" in messages[0]["content"] assert messages[1]["role"] == "system" assert "tool.echo" in messages[1]["content"] assert messages[2]["role"] == "system" assert "RAG PROMPT profile=official limit=3" in messages[2]["content"] assert [(item["role"], item["content"]) for item in messages[-3:]] == [ ("user", "first"), ("assistant", "second"), ("user", "Найди реквизит документа"), ] audit_root = Path(onec_agent_server["audit_root"]) retrieval_files = sorted((audit_root / "retrieval_events").glob("*.jsonl")) tool_files = sorted((audit_root / "tool_calls").glob("*.jsonl")) assert retrieval_files assert tool_files retrieval_records = [json.loads(line) for line in retrieval_files[-1].read_text(encoding="utf-8").splitlines() if line.strip()] tool_records = [json.loads(line) for line in tool_files[-1].read_text(encoding="utf-8").splitlines() if line.strip()] assert any(record["profile"] == "official" and record["plugin"] == "1c" for record in retrieval_records) assert any(record["tool_name"] == "tool.echo" and record["target_service"] == "1c-adapter" for record in tool_records) def test_adapter_live_methods_require_base_id(onec_agent_server: dict[str, str]) -> None: onec_agent.validate_adapter_call("help.methods", {}) onec_agent.validate_adapter_call("metadata.saved_state.forms.search", {"base_id": "upo_test"}) with pytest.raises(ValueError, match="requires params.base_id"): onec_agent.validate_adapter_call("metadata.saved_state.forms.search", {}) with pytest.raises(ValueError, match="requires params.base_id"): onec_agent.validate_adapter_call("storage.files.list", {"table": "ConfigSave"}) url = onec_agent_server["url"] status, project_data, _ = request_json("POST", url=url, path="/v1/projects", payload={"name": "base-id guard"}) assert status == 201 project_id = project_data["project"]["id"] status, chat_data, _ = request_json("POST", url=url, path=f"/v1/projects/{project_id}/chats", payload={"title": "guard"}) assert status == 201 chat_id = chat_data["chat"]["id"] status, turn_data, is_error = request_json( "POST", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}/turn", payload={ "message": "Проверь сохраненное состояние", "adapter_calls": [{"method": "metadata.saved_state.forms.search", "params": {"table": "ConfigSave"}}], }, ) assert status == 400 assert is_error assert turn_data["error"]["code"] == "adapter_error" assert "forbidden technical fields: table" in turn_data["error"]["message"] audit_root = Path(onec_agent_server["audit_root"]) turn_files = sorted((audit_root / "turn_audit").glob("*.jsonl")) assert turn_files turn_records = [json.loads(line) for line in turn_files[-1].read_text(encoding="utf-8").splitlines() if line.strip()] assert any( record["outcome"] == "failure" and record["failure_type"] == "adapter_error" and record["error_code"] == "adapter_error" and "forbidden technical fields: table" in str(record["error_message"]) for record in turn_records ) def test_agent_adapter_policy_uses_effective_view_and_blocks_storage_selectors( monkeypatch: pytest.MonkeyPatch, ) -> None: prepared = onec_agent.prepare_agent_adapter_call( "metadata.object.full", {"base_id": "upo_test", "name": "Отчет.Продажи"}, ) assert prepared["configuration_view"] == "effective_working" assert prepared["source_state"] == "working" with pytest.raises(ValueError, match="forbidden technical fields: table"): onec_agent.prepare_agent_adapter_call( "metadata.saved_state.forms.search", {"base_id": "upo_test", "table": "ConfigSave"}, ) with pytest.raises(ValueError, match="forbidden technical fields: module_ref"): onec_agent.prepare_agent_adapter_call( "code.read", {"base_id": "upo_test", "selector": {"module_ref": "private"}}, ) monkeypatch.setenv("ONEC_AGENT_ALLOW_DIAGNOSTIC", "true") assert onec_agent.prepare_agent_adapter_call( "metadata.saved_state.forms.search", {"base_id": "upo_test", "table": "ConfigSave"}, )["table"] == "ConfigSave" def test_agent_calls_adapter_through_mcp(monkeypatch: pytest.MonkeyPatch) -> None: requests: list[tuple[dict[str, Any], dict[str, str], str]] = [] class FakeResponse: def __init__(self, body: dict[str, Any], headers: dict[str, str] | None = None) -> None: self._body = json.dumps(body).encode("utf-8") self.headers = headers or {} def read(self) -> bytes: return self._body def __enter__(self): return self def __exit__(self, *_args: object) -> None: return None def fake_urlopen(request: Request, timeout: float): body = json.loads(request.data.decode("utf-8")) requests.append((body, dict(request.headers), request.full_url)) if body["method"] == "initialize": return FakeResponse({"jsonrpc": "2.0", "id": body["id"], "result": {}}, {"Mcp-Session-Id": "test-session"}) return FakeResponse( { "jsonrpc": "2.0", "id": body["id"], "result": {"content": [{"type": "text", "text": json.dumps({"status": "ok"})}]}, } ) monkeypatch.setattr(onec_agent, "urlopen", fake_urlopen) result = onec_agent.call_adapter("metadata.object.forms", {"base_id": "upo_test", "ref": "Report.Тест"}) assert result == {"status": "ok"} assert len(requests) == 2 assert requests[0][0]["method"] == "initialize" assert requests[1][0]["method"] == "tools/call" assert requests[1][0]["params"]["name"] == "onec_request" assert requests[1][0]["params"]["arguments"]["payload"]["source_state"] == "working" assert requests[1][2] == "http://docker.cin.su:8021/mcp" def test_agent_system_prompt_requires_mcp_name_first_navigation() -> None: prompt = onec_agent.DEFAULT_SYSTEM_PROMPT_PATH.read_text(encoding="utf-8") assert "только инструментом MCP `onec_request`" in prompt assert "source_state=working" in prompt assert "Не проси и не подставляй GUID либо `module_ref`" in prompt def test_get_chat_runtime(onec_agent_server: dict[str, str]) -> None: url = onec_agent_server["url"] status, project_data, _ = request_json( "POST", url=url, path="/v1/projects", payload={"name": "runtime-check"}, ) assert status == 201 project_id = project_data["project"]["id"] status, chat_data, _ = request_json( "POST", url=url, path=f"/v1/projects/{project_id}/chats", payload={"title": "runtime", "model_id": "stub-model", "provider_id": "default", "temperature": 0.2, "max_tokens": 100}, ) assert status == 201 chat_id = chat_data["chat"]["id"] status, runtime_data, _ = request_json( "GET", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}/runtime", ) assert status == 200 assert runtime_data["project_id"] == project_id assert runtime_data["chat_id"] == chat_id runtime = runtime_data["runtime"] assert runtime["provider"]["id"] == "default" assert runtime["project"]["id"] == project_id assert runtime["chat"]["id"] == chat_id assert runtime["model"]["selected_model_id"] == "stub-model" assert runtime["model"]["chat_model_id"] == "stub-model" def test_runtime_question_does_not_fire_on_general_1c_question(onec_agent_server: dict[str, str]) -> None: url = onec_agent_server["url"] status, project_data, _ = request_json( "POST", url=url, path="/v1/projects", payload={"name": "runtime-false-positive-check"}, ) assert status == 201 project_id = project_data["project"]["id"] status, chat_data, _ = request_json( "POST", url=url, path=f"/v1/projects/{project_id}/chats", payload={"title": "test"}, ) assert status == 201 chat_id = chat_data["chat"]["id"] status, turn_data, _ = request_json( "POST", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}/turn", payload={"message": "что такое 1С?", "use_rag": False}, ) assert status == 200 assert "runtime-конфигурация" not in str(turn_data["assistant_message"]["content"]).lower() assert "Stub answer for stub-model via default" in turn_data["assistant_message"]["content"] def test_validation_paths_return_client_errors(onec_agent_server: dict[str, str]) -> None: url = onec_agent_server["url"] status, bad_project, is_error = request_json("POST", url=url, path="/v1/projects", payload={"name": "Bad", "metadata": []}) assert status == 400 assert is_error assert bad_project["error"]["code"] == "invalid_argument" status, project_data, _ = request_json("POST", url=url, path="/v1/projects", payload={"name": "Validation"}) assert status == 201 project_id = project_data["project"]["id"] status, bad_chat, is_error = request_json( "POST", url=url, path=f"/v1/projects/{project_id}/chats", payload={"title": "bad provider", "provider_id": "missing"}, ) assert status == 400 assert is_error assert bad_chat["error"]["code"] == "invalid_argument" status, chat_data, _ = request_json("POST", url=url, path=f"/v1/projects/{project_id}/chats", payload={"title": "ok"}) assert status == 201 chat_id = chat_data["chat"]["id"] status, bad_turn, is_error = request_json( "POST", url=url, path=f"/v1/projects/{project_id}/chats/{chat_id}/turn", payload={"message": "Привет", "history_limit": "bad"}, ) assert status == 400 assert is_error assert bad_turn["error"]["code"] == "invalid_argument"