| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289 |
- """测试 Router 核心接口
- 用法:
- uv run python tests/test_router_api.py # 只跑 health check
- uv run python tests/test_router_api.py --search # 搜索工具列表
- uv run python tests/test_router_api.py --search image # 关键词搜索
- uv run python tests/test_router_api.py --status # 工具运行状态
- uv run python tests/test_router_api.py --select image_stitcher # 调用指定工具
- uv run python tests/test_router_api.py --stitch # 测试图片拼接
- uv run python tests/test_router_api.py --create # 默认任务
- uv run python tests/test_router_api.py --create image_stitcher # 指定任务文件
- """
- import argparse
- import base64
- import json
- import sys
- import time
- from pathlib import Path
- import httpx
- BASE_URL = "http://43.106.118.91:8001"
- TASKS_DIR = Path(__file__).parent / "tasks"
- TEST_IMAGES_DIR = TASKS_DIR / "stitcher_images"
- OUTPUT_DIR = Path(__file__).parent / "output"
- def check_connection():
- try:
- httpx.get(f"{BASE_URL}/health", timeout=3)
- except httpx.ConnectError:
- print(f"ERROR: Cannot connect to {BASE_URL}")
- print("Please start the service first:")
- print(" uv run python -m tool_agent")
- sys.exit(1)
- def test_health():
- print("=== Health Check ===")
- resp = httpx.get(f"{BASE_URL}/health")
- print(f" Status : {resp.status_code}")
- print(f" Body : {json.dumps(resp.json(), ensure_ascii=False, indent=4)}")
- assert resp.status_code == 200
- print(" [PASS]")
- def test_search_tools(keyword: str = None):
- print(f"=== Search Tools{f' (keyword={keyword!r})' if keyword else ''} ===")
- payload = {"keyword": keyword} if keyword else {}
- resp = httpx.post(f"{BASE_URL}/search_tools", json=payload)
- print(f" Status : {resp.status_code}")
- if resp.status_code != 200:
- print(f" Body : {resp.text}")
- print(" [FAIL]")
- return
- data = resp.json()
- print(f" Total : {data['total']}")
- for t in data["tools"]:
- print(f"\n [{t['tool_id']}]")
- print(f" name : {t['name']}")
- print(f" category : {t.get('category', '')}")
- print(f" state : {t['state']}")
- print(f" runtime : {t.get('runtime_type', '')} host_dir={t.get('host_dir', '')}")
- print(f" endpoint : {t.get('http_method', '')} {t.get('endpoint_path', '')} port={t.get('port')}")
- print(f" stream_support: {t.get('stream_support', False)}")
- print(f" description : {t.get('description', '')}")
- print(f" params ({len(t.get('params', []))}):")
- for p in t.get("params", []):
- req_mark = "*" if p["required"] else " "
- default_str = f" default={p['default']}" if p.get("default") is not None else ""
- enum_str = f" enum={p['enum']}" if p.get("enum") else ""
- print(f" {req_mark} {p['name']:<25} {p['type']:<12} {p.get('description', '')}{default_str}{enum_str}")
- if t.get("output_schema"):
- out_props = t["output_schema"].get("properties", {})
- print(f" output ({len(out_props)}):")
- for oname, odef in out_props.items():
- print(f" {oname:<25} {odef.get('type', ''):<12} {odef.get('description', '')}")
- print("\n [PASS]")
- def test_tools_status():
- print("=== Tools Status ===")
- resp = httpx.get(f"{BASE_URL}/tools/status")
- print(f" Status : {resp.status_code}")
- data = resp.json()
- print(f" Total : {len(data['tools'])}")
- for t in data["tools"]:
- print(f" - {t['tool_id']}")
- print(f" state : {t['state']}")
- print(f" port : {t.get('port')}")
- print(f" pid : {t.get('pid')}")
- print(f" sources: {[s['type'] for s in t.get('sources', [])]}")
- if t.get("last_error"):
- print(f" error : {t['last_error']}")
- print(" [PASS]")
- def test_select_tool(tool_id: str):
- print(f"=== Select Tool (tool_id={tool_id!r}) ===")
- resp = httpx.post(f"{BASE_URL}/select_tool", json={
- "tool_id": tool_id,
- "params": {}
- }, timeout=30)
- print(f" Status : {resp.status_code}")
- data = resp.json()
- print(f" Result :")
- print(f" status: {data.get('status')}")
- if data.get("error"):
- print(f" error : {data['error']}")
- else:
- result_str = json.dumps(data.get("result"), ensure_ascii=False, indent=6)
- print(f" result: {result_str[:500]}")
- print(" [PASS]")
- def test_stitch_images():
- print("=== Test Image Stitcher ===")
- if not TEST_IMAGES_DIR.exists():
- print(f" ERROR: Test images directory not found: {TEST_IMAGES_DIR}")
- print(" [SKIP]")
- return
- image_files = sorted(TEST_IMAGES_DIR.glob("*.png"))
- if len(image_files) < 2:
- print(f" ERROR: Need at least 2 images, found {len(image_files)}")
- print(" [SKIP]")
- return
- print(f" Images : {len(image_files)} found")
- images_b64 = []
- for img_path in image_files[:6]:
- with open(img_path, "rb") as f:
- images_b64.append(base64.b64encode(f.read()).decode())
- print(f" - {img_path.name}")
- print(f" Calling image_stitcher (grid, 2 columns)...")
- try:
- resp = httpx.post(f"{BASE_URL}/select_tool", json={
- "tool_id": "image_stitcher",
- "params": {
- "images": images_b64,
- "direction": "grid",
- "columns": 2,
- "spacing": 10,
- "background_color": "#FFFFFF",
- }
- }, timeout=60)
- print(f" Status : {resp.status_code}")
- data = resp.json()
- if data["status"] == "success":
- result = data["result"]
- print(f" Result :")
- print(f" width : {result['width']}")
- print(f" height: {result['height']}")
- OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
- output_path = OUTPUT_DIR / "stitched_result.png"
- with open(output_path, "wb") as f:
- f.write(base64.b64decode(result["image"]))
- print(f" saved : {output_path}")
- print(" [PASS]")
- else:
- print(f" ERROR : {data.get('error', 'unknown')}")
- print(" [FAIL]")
- except httpx.TimeoutException:
- print(" ERROR : Request timeout")
- print(" [FAIL]")
- except Exception as e:
- print(f" ERROR : {e}")
- print(" [FAIL]")
- def load_task_spec(task_name: str) -> dict:
- task_file = TASKS_DIR / f"{task_name}.json"
- if not task_file.exists():
- print(f" ERROR: Task file not found: {task_file}")
- print(" Available tasks:")
- if TASKS_DIR.exists():
- for f in TASKS_DIR.glob("*.json"):
- print(f" - {f.stem}")
- sys.exit(1)
- with open(task_file, "r", encoding="utf-8") as f:
- return json.load(f)
- def test_create_tool(task_name: str = None):
- print(f"=== Create Tool{f' (task={task_name!r})' if task_name else ''} ===")
- if task_name:
- task_data = load_task_spec(task_name)
- print(f" File : tests/tasks/{task_name}.json")
- print(f" Description: {task_data['description'][:80]}")
- else:
- task_data = {"description": "创建一个简单的文本计数工具,输入文本,返回字数和字符数"}
- print(f" Description: {task_data['description']}")
- resp = httpx.post(f"{BASE_URL}/create_tool", json=task_data)
- data = resp.json()
- task_id = data["task_id"]
- print(f" Task ID : {task_id}")
- print(f" Status : {data['status']}")
- assert data["status"] == "pending"
- print(" [SUBMITTED]")
- print(f"\n Polling task {task_id} (timeout 10min)...")
- for i in range(120):
- time.sleep(5)
- resp = httpx.get(f"{BASE_URL}/tasks/{task_id}", timeout=30)
- task = resp.json()
- status = task["status"]
- if i % 6 == 0:
- print(f" [{i*5}s] status={status}")
- if status == "completed":
- print(f"\n Completed!")
- print(f" Result : {str(task.get('result', ''))[:300]}")
- resp2 = httpx.post(f"{BASE_URL}/search_tools", json={})
- tools = resp2.json()["tools"]
- print(f" Registered : {[t['tool_id'] for t in tools]}")
- print(" [PASS]")
- return
- if status == "failed":
- print(f"\n Failed!")
- print(f" Error : {task.get('error', 'unknown')}")
- print(" [FAIL]")
- return
- print(f"\n Timeout after 600s")
- print(" [TIMEOUT]")
- def main():
- parser = argparse.ArgumentParser(description="Router API Test")
- parser.add_argument("--search", nargs="?", const="", metavar="KEYWORD",
- help="search tools, optional keyword")
- parser.add_argument("--status", action="store_true",
- help="show tools status")
- parser.add_argument("--select", metavar="TOOL_ID",
- help="call a tool by tool_id")
- parser.add_argument("--stitch", action="store_true",
- help="test image stitcher with sample images")
- parser.add_argument("--create", nargs="?", const="", metavar="TASK_NAME",
- help="create tool, optional task file name")
- args = parser.parse_args()
- print(f"Target: {BASE_URL}\n")
- check_connection()
- # 始终跑 health check
- test_health()
- ran_any = False
- if args.search is not None:
- print()
- test_search_tools(args.search or None)
- ran_any = True
- if args.status:
- print()
- test_tools_status()
- ran_any = True
- if args.select:
- print()
- test_select_tool(args.select)
- ran_any = True
- if args.stitch:
- print()
- test_stitch_images()
- ran_any = True
- if args.create is not None:
- print()
- test_create_tool(args.create or None)
- ran_any = True
- if not ran_any:
- print()
- print("No test specified. Available options:")
- parser.print_help()
- print("\n=== DONE ===")
- if __name__ == "__main__":
- main()
|