Files
workphone-sdk/sdk/scripts/console_runtime_smoke.py

184 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""总控平台独立运行验收。
只在本机启动临时服务,所有可写数据都进内存测试库;不会连接手机、微信或生产库。
"""
from __future__ import annotations
import argparse
import asyncio
import json
import socket
import sys
import threading
import time
from pathlib import Path
from typing import Any
import httpx
import uvicorn
APP_ROOT = Path(__file__).resolve().parents[1] / "app"
sys.path.insert(0, str(APP_ROOT))
from main import app # noqa: E402
from routers import ai_tasks as ai_tasks_router # noqa: E402
from routers import device_groups as device_groups_router # noqa: E402
from routers import integrations_admin as integrations_router # noqa: E402
from routers import kb as kb_router # noqa: E402
from routers import releases as releases_router # noqa: E402
from routers import workbench as workbench_router # noqa: E402
from services.ai_tasks import AITaskService # noqa: E402
from services.authorization_grants import AuthorizationGrantService # noqa: E402
from services.device_groups import DeviceGroupService # noqa: E402
from services.docs_catalog import DocumentCatalogService # noqa: E402
from services.integrations_admin import IntegrationAdminService # noqa: E402
from services.releases import ReleaseService # noqa: E402
from services.workbench import MemoryControlPlaneStore, WorkbenchService # noqa: E402
def _install_test_services() -> MemoryControlPlaneStore:
"""把总控各服务切到同一个内存库,避免误碰真实 Mongo。"""
store = MemoryControlPlaneStore()
integrations = IntegrationAdminService(store)
docs = DocumentCatalogService(store)
grants = AuthorizationGrantService(integrations, docs, store)
integrations_router.integration_admin_service = integrations
integrations_router.docs_catalog_service = docs
integrations_router.authorization_grant_service = grants
device_groups_router.device_group_service = DeviceGroupService(store)
releases_router.release_service = ReleaseService(store)
workbench_router.workbench_service = WorkbenchService(store)
ai_tasks_router.ai_task_service = AITaskService(
store,
online_checker=lambda _device_id: asyncio.sleep(0, result=False),
)
# 健康检查本身会尝试扫描 ADB这里替换成空扫描避免本次验收碰手机。
import services.adb_device as adb_device_module
class _NoDeviceADB:
async def async_scan_devices(self):
return []
adb_device_module.adb_manager = _NoDeviceADB()
return store
async def _seed(store: MemoryControlPlaneStore) -> None:
docs = integrations_router.docs_catalog_service
await docs.ensure_seeded()
await store.insert(
"release_records",
{
"_key": "smoke-release",
"channel": "stable",
"version": "3.0.0-smoke",
"version_code": 30000,
"sha256": "a" * 64,
"size": 1024,
"min_android": "8.0",
"download_url": "/static/smoke.apk",
"published_at": "2026-08-09T00:00:00+00:00",
},
)
def _start_server(port: int) -> tuple[uvicorn.Server, threading.Thread]:
config = uvicorn.Config(app, host="127.0.0.1", port=port, log_level="error", access_log=False)
server = uvicorn.Server(config)
thread = threading.Thread(target=server.run, name="console-smoke-server", daemon=True)
thread.start()
deadline = time.time() + 10
while time.time() < deadline:
try:
with socket.create_connection(("127.0.0.1", port), timeout=0.2):
return server, thread
except OSError:
time.sleep(0.05)
server.should_exit = True
raise RuntimeError("临时总控服务未能在10秒内启动")
def _check(response: httpx.Response, *, status: int = 200) -> dict[str, Any]:
assert response.status_code == status, f"{response.request.method} {response.request.url} -> {response.status_code}: {response.text[:400]}"
if "application/json" in response.headers.get("content-type", ""):
return response.json()
return {"text": response.text}
async def run_smoke(port: int) -> dict[str, Any]:
store = _install_test_services()
await _seed(store)
server, thread = _start_server(port)
base = f"http://127.0.0.1:{port}"
checks: list[str] = []
try:
async with httpx.AsyncClient(base_url=base, timeout=8) as client:
health = _check(await client.get("/health"))
checks.append("health")
root = await client.get("/")
_check(root)
assert "/assets/" in root.text and "旧页面" not in root.text
checks.append("new_console_home")
hub = await client.get("/hub")
_check(hub)
checks.append("hub")
openapi = _check(await client.get("/openapi.json"))
pairs = [(method, path) for path, item in openapi["paths"].items() for method in item]
assert len(pairs) == len(set(pairs))
checks.append("openapi_unique")
for path in (
"/api/v3/workbench/overview",
"/api/v3/devices",
"/api/v3/device-groups",
"/api/v3/releases/latest",
"/api/v3/integrations/apps",
"/api/v3/integrations/usage",
"/api/v3/integrations/docs/catalog",
"/api/v3/kb/catalog?limit=5",
"/api/v3/kb/search?q=总控&limit=5",
"/api/v3/kb/coverage",
):
body = _check(await client.get(path))
assert body.get("code") == 200, (path, body)
checks.append(path.split("?")[0])
headers = {"Idempotency-Key": "smoke-group-1", "X-Actor-Id": "smoke-operator"}
create_payload = {"name": "联调测试组", "device_ids": ["SAMPLE-DEVICE"], "description": "临时测试数据"}
first = _check(await client.post("/api/v3/device-groups", json=create_payload, headers=headers))
group_id = first["data"]["group"]["group_id"]
replay = _check(await client.post("/api/v3/device-groups", json=create_payload, headers=headers))
assert replay["data"]["group"]["group_id"] == group_id
conflict = _check(await client.post("/api/v3/device-groups", json={**create_payload, "name": "异参"}, headers=headers), status=409)
assert conflict["code"] == "idempotency_conflict"
readback = _check(await client.get(f"/api/v3/device-groups/{group_id}"))
assert readback["data"]["name"] == "联调测试组"
checks.extend(["group_write", "group_replay", "group_conflict", "group_readback"])
app_body = _check(await client.post("/api/v3/integrations/apps", json={"name": "联调第三方", "organization": "SAMPLE"}, headers={"X-Actor": "smoke-operator"}))
app_id = app_body["data"]["app_id"]
listed = _check(await client.get("/api/v3/integrations/apps"))
assert any(item["app_id"] == app_id for item in listed["data"]["items"])
checks.extend(["integration_write", "integration_readback"])
finally:
server.should_exit = True
thread.join(timeout=5)
return {"pass": len(checks), "checks": checks, "port": port, "health": health}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--port", type=int, default=18765)
args = parser.parse_args()
result = asyncio.run(run_smoke(args.port))
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())