#!/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())