256 lines
9.0 KiB
Python
256 lines
9.0 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
import uuid
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from urllib.error import HTTPError, URLError
|
|
from urllib.request import Request, urlopen
|
|
|
|
|
|
def request_json(
|
|
method: str,
|
|
base_url: str,
|
|
path: str,
|
|
*,
|
|
payload: dict[str, Any] | None = None,
|
|
timeout: float = 5.0,
|
|
) -> tuple[int, dict[str, Any] | str, bool]:
|
|
body = json.dumps(payload).encode("utf-8") if payload is not None else None
|
|
req = Request(
|
|
f"{base_url}{path}",
|
|
data=body,
|
|
method=method,
|
|
headers={"Content-Type": "application/json"},
|
|
)
|
|
try:
|
|
with urlopen(req, timeout=timeout) as response:
|
|
raw = response.read().decode("utf-8")
|
|
if not raw:
|
|
return response.status, {}, False
|
|
return response.status, json.loads(raw), False
|
|
except HTTPError as exc:
|
|
raw = exc.read().decode("utf-8")
|
|
return exc.code, (json.loads(raw) if raw else {}), True
|
|
except URLError as exc:
|
|
return 0, str(exc), True
|
|
|
|
|
|
def assert_has_trace(payload: dict[str, Any]) -> None:
|
|
trace = payload.get("trace_id")
|
|
assert isinstance(trace, str), "trace_id is required"
|
|
assert re.fullmatch(r"[0-9a-f]{32}", trace), "trace_id format is invalid"
|
|
|
|
|
|
def unwrap(payload: dict[str, Any], wrapper: str) -> dict[str, Any]:
|
|
wrapped = payload.get(wrapper)
|
|
if isinstance(wrapped, dict):
|
|
return wrapped
|
|
return payload
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Smoke test for 1C agent API.")
|
|
parser.add_argument(
|
|
"--base-url",
|
|
default="http://docker-test.cin.su:8090",
|
|
help="Base API URL",
|
|
)
|
|
parser.add_argument(
|
|
"--timeout",
|
|
type=float,
|
|
default=5.0,
|
|
help="Request timeout in seconds",
|
|
)
|
|
parser.add_argument(
|
|
"--with-turn-check",
|
|
action="store_true",
|
|
help="Run /turn check (may be slower on first warm-up).",
|
|
)
|
|
parser.add_argument(
|
|
"--report",
|
|
type=Path,
|
|
help="Write JSON smoke report to file.",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
steps: list[dict[str, Any]] = []
|
|
project_id: str | None = None
|
|
chat_id: str | None = None
|
|
passed = 0
|
|
failed: list[dict[str, str]] = []
|
|
|
|
try:
|
|
status, health, is_error = request_json("GET", args.base_url, "/v1/health", timeout=args.timeout)
|
|
step = {"name": "health", "status": status, "error": is_error}
|
|
if not is_error:
|
|
assert status == 200 and health["status"] == "ok"
|
|
assert_has_trace(health)
|
|
step["service"] = health.get("service")
|
|
else:
|
|
raise AssertionError(f"Health request failed: {health}")
|
|
steps.append(step)
|
|
passed += 1
|
|
|
|
status, state, is_error = request_json("GET", args.base_url, "/v1/state", timeout=args.timeout)
|
|
step = {"name": "state", "status": status, "error": is_error}
|
|
if is_error:
|
|
raise AssertionError(f"State request failed: {state}")
|
|
assert status == 200
|
|
assert state.get("state", {}).get("projects", 0) >= 0
|
|
assert_has_trace(state)
|
|
step["project_count"] = state["state"]["projects"]
|
|
steps.append(step)
|
|
passed += 1
|
|
|
|
project_name = f"smoke-{uuid.uuid4()}"
|
|
status, project, is_error = request_json(
|
|
"POST",
|
|
args.base_url,
|
|
"/v1/projects",
|
|
payload={"name": project_name, "description": "agent smoke"},
|
|
timeout=args.timeout,
|
|
)
|
|
step = {"name": "create_project", "status": status, "error": is_error}
|
|
if is_error:
|
|
raise AssertionError(f"Create project failed: {project}")
|
|
assert status == 201 and "project" in project
|
|
project_id = project["project"]["id"]
|
|
assert_has_trace(project)
|
|
steps.append(step)
|
|
passed += 1
|
|
|
|
status, project_check, is_error = request_json(
|
|
"GET",
|
|
args.base_url,
|
|
f"/v1/projects/{project_id}",
|
|
timeout=args.timeout,
|
|
)
|
|
step = {"name": "get_project", "status": status, "error": is_error}
|
|
if is_error:
|
|
raise AssertionError(f"Get project failed: {project_check}")
|
|
assert status == 200
|
|
assert unwrap(project_check, "project")["id"] == project_id
|
|
assert_has_trace(project_check)
|
|
steps.append(step)
|
|
passed += 1
|
|
|
|
status, chat, is_error = request_json(
|
|
"POST",
|
|
args.base_url,
|
|
f"/v1/projects/{project_id}/chats",
|
|
payload={"title": "Диалог для smoke", "rag_profile": "default", "temperature": 0.12, "max_tokens": 128},
|
|
timeout=args.timeout,
|
|
)
|
|
step = {"name": "create_chat", "status": status, "error": is_error}
|
|
if is_error:
|
|
raise AssertionError(f"Create chat failed: {chat}")
|
|
assert status == 201 and "chat" in chat
|
|
chat_id = chat["chat"]["id"]
|
|
assert_has_trace(chat)
|
|
steps.append(step)
|
|
passed += 1
|
|
|
|
status, message, is_error = request_json(
|
|
"POST",
|
|
args.base_url,
|
|
f"/v1/projects/{project_id}/chats/{chat_id}/messages",
|
|
payload={"role": "user", "content": "Тестовое сообщение smoke"},
|
|
timeout=args.timeout,
|
|
)
|
|
step = {"name": "add_message", "status": status, "error": is_error}
|
|
if is_error:
|
|
raise AssertionError(f"Add message failed: {message}")
|
|
assert status == 201 and "message" in message
|
|
assert message["message"]["content"] == "Тестовое сообщение smoke"
|
|
assert_has_trace(message)
|
|
steps.append(step)
|
|
passed += 1
|
|
|
|
status, listed, is_error = request_json(
|
|
"GET",
|
|
args.base_url,
|
|
f"/v1/projects/{project_id}/chats/{chat_id}/messages?limit=10",
|
|
timeout=args.timeout,
|
|
)
|
|
step = {"name": "list_messages", "status": status, "error": is_error}
|
|
if is_error:
|
|
raise AssertionError(f"List messages failed: {listed}")
|
|
assert status == 200
|
|
assert len(listed.get("messages", [])) == 1
|
|
assert_has_trace(listed)
|
|
steps.append(step)
|
|
passed += 1
|
|
|
|
status, state, is_error = request_json(
|
|
"GET",
|
|
args.base_url,
|
|
f"/v1/projects/{project_id}/chats/{chat_id}",
|
|
timeout=args.timeout,
|
|
)
|
|
step = {"name": "get_chat", "status": status, "error": is_error}
|
|
if is_error:
|
|
raise AssertionError(f"Get chat failed: {state}")
|
|
assert status == 200
|
|
chat_payload = unwrap(state, "chat")
|
|
assert chat_payload["id"] == chat_id
|
|
assert_has_trace(state)
|
|
steps.append(step)
|
|
passed += 1
|
|
|
|
status, not_found, is_error = request_json(
|
|
"POST",
|
|
args.base_url,
|
|
"/v1/projects/does-not-exist/chats",
|
|
payload={"title": "fail-fast"},
|
|
timeout=args.timeout,
|
|
)
|
|
step = {"name": "negative_not_found_chat", "status": status, "error": is_error}
|
|
if not is_error or status != 404:
|
|
raise AssertionError(f"Expected 404 for unknown project, got {status} {not_found}")
|
|
assert_has_trace(not_found)
|
|
steps.append(step)
|
|
passed += 1
|
|
|
|
if args.with_turn_check:
|
|
status, turn, is_error = request_json(
|
|
"POST",
|
|
args.base_url,
|
|
f"/v1/projects/{project_id}/chats/{chat_id}/turn",
|
|
payload={"message": "Коротко: проверка маршрутизации.", "use_rag": False},
|
|
timeout=max(args.timeout, 60.0),
|
|
)
|
|
step = {"name": "turn", "status": status, "error": is_error}
|
|
if is_error:
|
|
raise AssertionError(f"Turn failed: {turn}")
|
|
assert status == 200
|
|
assert_has_trace(turn)
|
|
steps.append(step)
|
|
passed += 1
|
|
|
|
result = {"status": "ok", "passed": passed, "steps": steps}
|
|
if args.report:
|
|
args.report.parent.mkdir(parents=True, exist_ok=True)
|
|
args.report.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
print(json.dumps(result, ensure_ascii=False))
|
|
return 0
|
|
except Exception as exc:
|
|
failed.append({"error": str(exc), "at": steps[-1]["name"] if steps else "start"})
|
|
result = {"status": "failed", "passed": passed, "steps": steps, "errors": failed}
|
|
if args.report:
|
|
args.report.parent.mkdir(parents=True, exist_ok=True)
|
|
args.report.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
print(json.dumps(result, ensure_ascii=False), file=sys.stderr)
|
|
return 1
|
|
finally:
|
|
if project_id is not None:
|
|
request_json("DELETE", args.base_url, f"/v1/projects/{project_id}", timeout=args.timeout)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|