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

196 lines
8.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
# -*- coding: utf-8 -*-
"""WP-CONSOLE-18 本机临时 Mongo 真实留存验收。
这个脚本只接受本机 Mongo 和带 ``wp_console_18_test_`` 前缀的数据库名。
它会初始化总控平台的 13 个集合、70 个索引,写入少量测试资料,
断开连接后重新连接读取,再回滚本任务索引,最后删除自己的临时数据库。
"""
from __future__ import annotations
import argparse
import json
import re
import sys
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import urlparse
from pymongo import MongoClient
APP_DIR = Path(__file__).resolve().parents[1] / "app"
if str(APP_DIR) not in sys.path:
sys.path.insert(0, str(APP_DIR))
from services.control_plane_indexes import ( # noqa: E402
CONTROL_PLANE_COLLECTIONS,
CONTROL_PLANE_INDEXES,
apply_control_plane_indexes,
rollback_control_plane_indexes,
)
DB_PREFIX = "wp_console_18_test_"
LOCAL_HOSTS = {"127.0.0.1", "localhost", "::1"}
DB_RE = re.compile(r"^wp_console_18_test_[a-z0-9][a-z0-9_-]{3,80}$")
def _guard_target(uri: str, database: str) -> None:
parsed = urlparse(uri)
host = (parsed.hostname or "").lower()
if host not in LOCAL_HOSTS:
raise ValueError("只允许连接本机Mongo")
if parsed.port == 27017:
raise ValueError("默认27017可能是正式库本任务只允许独立测试端口")
if not DB_RE.fullmatch(database):
raise ValueError(f"数据库名必须以{DB_PREFIX}开头并带随机后缀")
def _utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
def _wait_ping(client: MongoClient, timeout_seconds: float = 8.0) -> None:
deadline = time.monotonic() + timeout_seconds
last_error: Exception | None = None
while time.monotonic() < deadline:
try:
client.admin.command("ping")
return
except Exception as exc: # pragma: no cover - 真实环境重试分支
last_error = exc
time.sleep(0.25)
raise RuntimeError(f"Mongo在规定时间内没有回应: {last_error}")
def _index_count(db) -> int:
return sum(
max(0, len(db[name].index_information()) - 1)
for name in CONTROL_PLANE_COLLECTIONS
if name in db.list_collection_names()
)
def _fixture_documents(run_id: str) -> dict[str, dict]:
now = _utc_now()
return {
"device_groups": {"group_id": f"grp-{run_id}", "name": "WP-CONSOLE-18测试分组", "device_ids": ["device-test-18"], "updated_at": now},
"device_releases": {"release_id": f"rel-{run_id}", "version": "18.0.0-test", "status": "ready", "created_at": now},
"integration_applications": {"app_id": f"app-{run_id}", "name": "WP-CONSOLE-18测试应用", "organization": "wp_console_18_test", "status": "active", "created_at": now},
"authorization_grants": {"grant_id": f"grant-{run_id}", "app_id": f"app-{run_id}", "status": "active", "device_ids": ["device-test-18"], "created_at": now},
"integration_usage_records": {"usage_id": f"usage-{run_id}", "app_id": f"app-{run_id}", "interface_scope": "console.read", "result": "success", "status_code": 200, "occurred_at": now},
"integration_documents": {"document_id": f"doc-{run_id}", "title": "WP-CONSOLE-18测试知识库条目", "category": "test", "visibility": "private", "status": "published", "updated_at": now},
"ai_tasks": {"task_id": f"task-{run_id}", "action": "console_test_read", "actor": "wp-console-18-test", "status": "completed", "trace_id": f"trace-{run_id}", "created_at": now, "updated_at": now},
}
def _write_fixtures(db, run_id: str) -> dict[str, str]:
documents = _fixture_documents(run_id)
for collection, document in documents.items():
db[collection].insert_one(document)
return {collection: document[next(key for key in document if key.endswith("_id"))] for collection, document in documents.items()}
def _read_fixtures(db, ids: dict[str, str]) -> dict[str, bool]:
result: dict[str, bool] = {}
for collection, expected_id in ids.items():
# Mongo会自动补一个内部的_id读回时必须使用业务自己的编号。
key = next(
key for key in db[collection].find_one({})
if key != "_id" and key.endswith("_id")
)
result[collection] = db[collection].find_one({key: expected_id}) is not None
return result
def run(uri: str, database: str, report_dir: Path) -> dict:
_guard_target(uri, database)
report_dir.mkdir(parents=True, exist_ok=True)
run_id = uuid.uuid4().hex[:12]
result: dict = {
"task_id": "WP-CONSOLE-18",
"run_id": run_id,
"uri_host": urlparse(uri).hostname,
"database": database,
"target_guard": "local_only_test_database",
"started_at": _utc_now(),
"connection_reopened": False,
"cross_reopen_readback": False,
"business_data_preserved_after_rollback": False,
"cleaned_up": False,
}
client = MongoClient(uri, serverSelectionTimeoutMS=5000, connectTimeoutMS=5000)
try:
_wait_ping(client)
db = client[database]
first = apply_control_plane_indexes(db)
second = apply_control_plane_indexes(db)
result["first_init"] = {key: first[key] for key in ("collection_count", "index_count", "created_count", "existing_count")}
result["second_init"] = {key: second[key] for key in ("collection_count", "index_count", "created_count", "existing_count")}
if first["collection_count"] != 13 or first["index_count"] != 70 or first["created_count"] != 70:
raise AssertionError("首次初始化没有得到13个集合和70个索引")
if second["created_count"] != 0 or second["existing_count"] != 70:
raise AssertionError("第二次初始化没有做到幂等")
result["index_count_after_second_init"] = _index_count(db)
fixture_ids = _write_fixtures(db, run_id)
result["fixture_ids"] = fixture_ids
result["fixture_write_count"] = len(fixture_ids)
client.close()
result["connection_reopened"] = True
client = MongoClient(uri, serverSelectionTimeoutMS=5000, connectTimeoutMS=5000)
_wait_ping(client)
db = client[database]
readback = _read_fixtures(db, fixture_ids)
result["readback_after_reopen"] = readback
result["cross_reopen_readback"] = all(readback.values())
if not result["cross_reopen_readback"]:
raise AssertionError("断开重连后测试资料没有全部读回")
preview = rollback_control_plane_indexes(db, dry_run=True)
rollback = rollback_control_plane_indexes(db)
result["rollback_preview"] = {key: preview[key] for key in ("planned_count", "dropped_count")}
result["rollback"] = {key: rollback[key] for key in ("planned_count", "dropped_count")}
result["remaining_custom_indexes"] = _index_count(db)
preserved = _read_fixtures(db, fixture_ids)
result["readback_after_rollback"] = preserved
result["business_data_preserved_after_rollback"] = all(preserved.values())
if rollback["dropped_count"] != 70 or result["remaining_custom_indexes"] != 0 or not result["business_data_preserved_after_rollback"]:
raise AssertionError("回滚没有只移除本任务索引或业务数据未保留")
result["result"] = "PASS"
finally:
if client:
client.close()
# 只删除本任务自己生成的临时数据库,不删除任何集合或正式库。
cleanup = MongoClient(uri, serverSelectionTimeoutMS=5000, connectTimeoutMS=5000)
try:
_wait_ping(cleanup)
cleanup.drop_database(database)
result["cleaned_up"] = True
finally:
cleanup.close()
result["finished_at"] = _utc_now()
(report_dir / "RESULT.json").write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
return result
def main() -> int:
parser = argparse.ArgumentParser(description="WP-CONSOLE-18临时Mongo真实留存验收")
parser.add_argument("--uri", required=True)
parser.add_argument("--database", required=True)
parser.add_argument("--report-dir", type=Path, required=True)
args = parser.parse_args()
try:
result = run(args.uri, args.database, args.report_dir)
except Exception as exc:
print(json.dumps({"task_id": "WP-CONSOLE-18", "result": "BLOCKED_OR_FAILED", "error": str(exc)}, ensure_ascii=False))
return 2
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())