已合并
feat:新增实例绑定功能 #468
王欣创建于 22 天前
feat:新增实例绑定功能 #468
已合并
共 40 个文件变更+2645-2853
Mapplications/manager/manager_server/scripts/enterprise_runtime_service_config.py+3-3文件内容审核中,请稍后刷新重试
| @@ -56,6 +56,8 @@ def create_app() -> FastAPI: | |||
| 56 | description="JiuwenClaw EE 管理平面(Claw Manager)", | 56 | description="JiuwenClaw EE 管理平面(Claw Manager)", |
| 57 | version=__version__, | 57 | version=__version__, |
| 58 | lifespan=lifespan, | 58 | lifespan=lifespan, |
| 59 | + # 关闭尾斜杠自动 307:经 manager-web / Ingress 反代时 Location 会泄漏集群内网 DNS | ||
| 60 | + redirect_slashes=False, | ||
| 59 | ) | 61 | ) |
| 60 | application.add_middleware( | 62 | application.add_middleware( |
| 61 | CORSMiddleware, | 63 | CORSMiddleware, |
Mapplications/manager/manager_server/src/manager_server/core/instance/__init__.py+0-4文件内容审核中,请稍后刷新重试
| @@ -1,341 +0,0 @@ | |||
| 1 | -"""本地拉起 Gateway + AgentServer(开发/联调用),并写入 per-instance 工作区配置。""" | ||
| 2 | - | ||
| 3 | -from __future__ import annotations | ||
| 4 | - | ||
| 5 | -import os | ||
| 6 | -import socket | ||
| 7 | -import subprocess | ||
| 8 | -import sys | ||
| 9 | -import time | ||
| 10 | -from pathlib import Path | ||
| 11 | -from typing import Any | ||
| 12 | - | ||
| 13 | -import yaml | ||
| 14 | -from openjiuwen_runtime.foundation.db.handler import DBHandler | ||
| 15 | - | ||
| 16 | -from manager_server.infrastructure.config import Settings | ||
| 17 | -from manager_server.core.instance.instance_service import ( | ||
| 18 | - create_instance_row, | ||
| 19 | - dumps_auth_config, | ||
| 20 | - generate_unique_jiuwenclaw_id, | ||
| 21 | - get_instance_row, | ||
| 22 | - merge_instance_data, | ||
| 23 | -) | ||
| 24 | - | ||
| 25 | - | ||
| 26 | -def _repo_root() -> Path: | ||
| 27 | - # .../claw_manager/src/manager_server/core/instance/instance_provisioner.py -> repo root | ||
| 28 | - return Path(__file__).resolve().parents[7] | ||
| 29 | - | ||
| 30 | - | ||
| 31 | -def _resolve_provision_python(repo_root: Path, settings: Settings) -> str: | ||
| 32 | - """解析用于拉起 Gateway / AgentServer 的 Python 解释器。 | ||
| 33 | - | ||
| 34 | - 优先级:``CLAWMANAGER_PROVISION_PYTHON`` > 仓库根 ``.venv`` > 当前进程的 ``sys.executable``。 | ||
| 35 | - Manager 常在 ``claw_manager/.venv`` 中运行,而 jiuwenclaw 依赖(如 openjiuwen)安装在仓库根 ``.venv``。 | ||
| 36 | - """ | ||
| 37 | - if settings.provision_python: | ||
| 38 | - explicit = Path(settings.provision_python).expanduser() | ||
| 39 | - if not explicit.is_file(): | ||
| 40 | - raise FileNotFoundError(f"provision_python not found: {explicit}") | ||
| 41 | - return str(explicit.resolve()) | ||
| 42 | - | ||
| 43 | - if os.name == "nt": | ||
| 44 | - venv_candidates = ( | ||
| 45 | - repo_root / ".venv" / "Scripts" / "python.exe", | ||
| 46 | - repo_root / ".venv" / "Scripts" / "python", | ||
| 47 | - ) | ||
| 48 | - else: | ||
| 49 | - venv_candidates = ( | ||
| 50 | - repo_root / ".venv" / "bin" / "python", | ||
| 51 | - repo_root / ".venv" / "bin" / "python3", | ||
| 52 | - ) | ||
| 53 | - for candidate in venv_candidates: | ||
| 54 | - if candidate.is_file(): | ||
| 55 | - return str(candidate.resolve()) | ||
| 56 | - | ||
| 57 | - return sys.executable | ||
| 58 | - | ||
| 59 | - | ||
| 60 | -def _pick_port() -> int: | ||
| 61 | - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: | ||
| 62 | - s.bind(("127.0.0.1", 0)) | ||
| 63 | - return int(s.getsockname()[1]) | ||
| 64 | - | ||
| 65 | - | ||
| 66 | -def _materialize_instance_config( | ||
| 67 | - *, | ||
| 68 | - instance_dir: Path, | ||
| 69 | - template_path: Path, | ||
| 70 | - rest_port: int, | ||
| 71 | - agent_sqlite: Path, | ||
| 72 | -) -> None: | ||
| 73 | - instance_dir.mkdir(parents=True, exist_ok=True) | ||
| 74 | - cfg_dir = instance_dir / "config" | ||
| 75 | - cfg_dir.mkdir(parents=True, exist_ok=True) | ||
| 76 | - raw = yaml.safe_load(template_path.read_text(encoding="utf-8")) or {} | ||
| 77 | - if not isinstance(raw, dict): | ||
| 78 | - raw = {} | ||
| 79 | - ext = raw.get("extensions") | ||
| 80 | - if not isinstance(ext, dict): | ||
| 81 | - ext = {} | ||
| 82 | - raw["extensions"] = ext | ||
| 83 | - ext["extension_dirs"] = "${EXTENSION_DIRS}" | ||
| 84 | - ac = ext.get("agent_client_rest") | ||
| 85 | - if not isinstance(ac, dict): | ||
| 86 | - ac = {} | ||
| 87 | - ext["agent_client_rest"] = ac | ||
| 88 | - db = ac.get("database") | ||
| 89 | - if not isinstance(db, dict): | ||
| 90 | - db = {} | ||
| 91 | - ac["database"] = db | ||
| 92 | - | ||
| 93 | - # 支持通过环境变量配置数据库类型 | ||
| 94 | - db_type = os.getenv("JIUWENCLAW_GATEWAY_DB_TYPE", "mysql").strip().lower() | ||
| 95 | - if db_type == "mysql": | ||
| 96 | - db["db_type"] = "mysql" | ||
| 97 | - db["db"] = { | ||
| 98 | - "host": os.getenv("DB_HOST", os.getenv("JIUWENCLAW_GATEWAY_DB_HOST", "localhost")), | ||
| 99 | - "port": int(os.getenv("DB_PORT", os.getenv("JIUWENCLAW_GATEWAY_DB_PORT", "3306"))), | ||
| 100 | - "user": os.getenv("DB_USER", os.getenv("JIUWENCLAW_GATEWAY_DB_USER", "root")), | ||
| 101 | - "password": os.getenv("DB_PASSWORD", os.getenv("JIUWENCLAW_GATEWAY_DB_PASSWORD", "123456")), | ||
| 102 | - "db_name": os.getenv("DB_NAME", os.getenv("JIUWENCLAW_GATEWAY_DB_NAME", "gateway")), | ||
| 103 | - } | ||
| 104 | - else: | ||
| 105 | - # 默认使用 SQLite | ||
| 106 | - db["db_type"] = "sqlite" | ||
| 107 | - db["sqlite_path"] = str(agent_sqlite.resolve()) | ||
| 108 | - | ||
| 109 | - # 保留模板中的 enabled: ${AGENT_CLIENT_REST_ENABLED:-true},便于 AgentServer 进程关闭 REST、仅 Gateway 监听端口 | ||
| 110 | - ac["host"] = "127.0.0.1" | ||
| 111 | - ac["port"] = rest_port | ||
| 112 | - (cfg_dir / "config.yaml").write_text( | ||
| 113 | - yaml.safe_dump(raw, allow_unicode=True, sort_keys=False), | ||
| 114 | - encoding="utf-8", | ||
| 115 | - ) | ||
| 116 | - | ||
| 117 | - | ||
| 118 | -def _child_env_common( | ||
| 119 | - *, | ||
| 120 | - settings: Settings, | ||
| 121 | - jiuwenclaw_id: str, | ||
| 122 | - instance_dir: Path, | ||
| 123 | - management_api_base: str, | ||
| 124 | - extension_dirs: str, | ||
| 125 | -) -> dict[str, str]: | ||
| 126 | - out = os.environ.copy() | ||
| 127 | - out["JIUWENCLAW_DATA_DIR"] = str(instance_dir.resolve()) | ||
| 128 | - out["JIUWENCLAW_CONFIG_DIR"] = str((instance_dir / "config").resolve()) | ||
| 129 | - out["EXTENSION_DIRS"] = extension_dirs | ||
| 130 | - out["JIUWENCLAW_ID"] = jiuwenclaw_id | ||
| 131 | - out["MANAGEMENT_API_BASE"] = management_api_base | ||
| 132 | - | ||
| 133 | - db_type = os.getenv("JIUWENCLAW_GATEWAY_DB_TYPE", "mysql").strip().lower() | ||
| 134 | - out["JIUWENCLAW_GATEWAY_DB_TYPE"] = db_type | ||
| 135 | - out["AGENT_CLIENT_DB_TYPE"] = db_type | ||
| 136 | - out["MANAGER_WS_CLIENT_DB_TYPE"] = db_type | ||
| 137 | - if db_type == "mysql": | ||
| 138 | - host = os.getenv("DB_HOST", os.getenv("JIUWENCLAW_GATEWAY_DB_HOST", "localhost")) | ||
| 139 | - port = os.getenv("DB_PORT", os.getenv("JIUWENCLAW_GATEWAY_DB_PORT", "3306")) | ||
| 140 | - user = os.getenv("DB_USER", os.getenv("JIUWENCLAW_GATEWAY_DB_USER", "root")) | ||
| 141 | - password = os.getenv("DB_PASSWORD", os.getenv("JIUWENCLAW_GATEWAY_DB_PASSWORD", "123456")) | ||
| 142 | - db_name = os.getenv("DB_NAME", os.getenv("JIUWENCLAW_GATEWAY_DB_NAME", "gateway")) | ||
| 143 | - out["DB_HOST"] = host | ||
| 144 | - out["DB_PORT"] = str(port) | ||
| 145 | - out["DB_USER"] = user | ||
| 146 | - out["DB_PASSWORD"] = password | ||
| 147 | - out["DB_NAME"] = db_name | ||
| 148 | - out["JIUWENCLAW_GATEWAY_DB_HOST"] = host | ||
| 149 | - out["JIUWENCLAW_GATEWAY_DB_PORT"] = str(port) | ||
| 150 | - out["JIUWENCLAW_GATEWAY_DB_USER"] = user | ||
| 151 | - out["JIUWENCLAW_GATEWAY_DB_PASSWORD"] = password | ||
| 152 | - out["JIUWENCLAW_GATEWAY_DB_NAME"] = db_name | ||
| 153 | - | ||
| 154 | - return out | ||
| 155 | - | ||
| 156 | - | ||
| 157 | -async def provision_local_jiuwenclaw( | ||
| 158 | - handler: DBHandler, | ||
| 159 | - settings: Settings, | ||
| 160 | - *, | ||
| 161 | - jiuwenclaw_name: str, | ||
| 162 | - creator_id: str, | ||
| 163 | - description: str | None, | ||
| 164 | -) -> dict[str, Any]: | ||
| 165 | - if not settings.allow_local_provision: | ||
| 166 | - raise ValueError("local provision disabled (set MANAGER_ALLOW_LOCAL_PROVISION=true)") | ||
| 167 | - | ||
| 168 | - repo_root = Path(settings.provision_repo_root or _repo_root()).resolve() | ||
| 169 | - gateway_ext = (repo_root / "packages" / "jiuwenclaw-ee" / "gateway" / "extensions").resolve() | ||
| 170 | - ext_dirs = settings.provision_extension_dirs or str(gateway_ext) | ||
| 171 | - tpl = Path( | ||
| 172 | - settings.instance_config_template or (repo_root / "docker" / "config" / "config.yaml") | ||
| 173 | - ).resolve() | ||
| 174 | - if not tpl.is_file(): | ||
| 175 | - raise FileNotFoundError(f"config template not found: {tpl}") | ||
| 176 | - | ||
| 177 | - root = Path(settings.provision_workspace_root).expanduser().resolve() | ||
| 178 | - jiuwenclaw_id = await generate_unique_jiuwenclaw_id(handler) | ||
| 179 | - instance_dir = root / jiuwenclaw_id | ||
| 180 | - if instance_dir.exists(): | ||
| 181 | - raise FileExistsError(f"workspace already exists: {instance_dir}") | ||
| 182 | - | ||
| 183 | - agent_port = _pick_port() | ||
| 184 | - web_port = _pick_port() | ||
| 185 | - rest_port = _pick_port() | ||
| 186 | - agent_db = instance_dir / "agent_client.db" | ||
| 187 | - | ||
| 188 | - _materialize_instance_config( | ||
| 189 | - instance_dir=instance_dir, | ||
| 190 | - template_path=tpl, | ||
| 191 | - rest_port=rest_port, | ||
| 192 | - agent_sqlite=agent_db, | ||
| 193 | - ) | ||
| 194 | - | ||
| 195 | - management_api_base = f"http://127.0.0.1:{rest_port}" | ||
| 196 | - | ||
| 197 | - await create_instance_row( | ||
| 198 | - handler, | ||
| 199 | - { | ||
| 200 | - "jiuwenclaw_id": jiuwenclaw_id, | ||
| 201 | - "jiuwenclaw_name": jiuwenclaw_name, | ||
| 202 | - "creator_id": creator_id, | ||
| 203 | - "description": description, | ||
| 204 | - "k8s_master_host": "local-provision", | ||
| 205 | - "k8s_auth_type": "none", | ||
| 206 | - "k8s_auth_config": dumps_auth_config({}), | ||
| 207 | - "k8s_namespace": "local", | ||
| 208 | - # Gateway 经 Manager WS 注册成功后由 register_gateway_via_ws 置为 online | ||
| 209 | - "status": "pending", | ||
| 210 | - "resource_quota": None, | ||
| 211 | - "data": { | ||
| 212 | - "management_api_base": management_api_base, | ||
| 213 | - "provision": { | ||
| 214 | - "workspace": str(instance_dir), | ||
| 215 | - "ports": { | ||
| 216 | - "agent_server": agent_port, | ||
| 217 | - "web": web_port, | ||
| 218 | - "agent_client_rest": rest_port, | ||
| 219 | - }, | ||
| 220 | - }, | ||
| 221 | - }, | ||
| 222 | - "group_id": "default", | ||
| 223 | - "space_id": "default", | ||
| 224 | - } | ||
| 225 | - ) | ||
| 226 | - | ||
| 227 | - py = _resolve_provision_python(repo_root, settings) | ||
| 228 | - py_path = settings.provision_pythonpath or f"{repo_root}:{repo_root / 'packages'}" | ||
| 229 | - base_env = _child_env_common( | ||
| 230 | - settings=settings, | ||
| 231 | - jiuwenclaw_id=jiuwenclaw_id, | ||
| 232 | - instance_dir=instance_dir, | ||
| 233 | - management_api_base=management_api_base, | ||
| 234 | - extension_dirs=ext_dirs, | ||
| 235 | - ) | ||
| 236 | - base_env["PYTHONPATH"] = py_path | ||
| 237 | - base_env["PYTHONUNBUFFERED"] = "1" | ||
| 238 | - base_env["PYTHONUTF8"] = "1" | ||
| 239 | - base_env["PYTHONIOENCODING"] = "utf-8" | ||
| 240 | - | ||
| 241 | - env_as = dict(base_env) | ||
| 242 | - env_as["AGENT_SERVER_HOST"] = "127.0.0.1" | ||
| 243 | - env_as["AGENT_SERVER_PORT"] = str(agent_port) | ||
| 244 | - # AgentServer 不加载 Gateway 扩展(如 manager_ws_client),避免与 Gateway 争抢 Manager WS 连接 | ||
| 245 | - env_as["EXTENSION_DIRS"] = "" | ||
| 246 | - # 与 Gateway 共用同一 REST 端口时,仅由 Gateway 挂载 agent_client_rest,避免双进程抢端口导致 62160 无监听 | ||
| 247 | - env_as["AGENT_CLIENT_REST_ENABLED"] = "false" | ||
| 248 | - | ||
| 249 | - env_gw = dict(base_env) | ||
| 250 | - env_gw["AGENT_SERVER_URL"] = f"ws://127.0.0.1:{agent_port}" | ||
| 251 | - env_gw["WEB_HOST"] = "127.0.0.1" | ||
| 252 | - env_gw["WEB_PORT"] = str(web_port) | ||
| 253 | - env_gw["AGENT_CLIENT_REST_ENABLED"] = "true" | ||
| 254 | - | ||
| 255 | - log_dir = instance_dir / "provision_logs" | ||
| 256 | - log_dir.mkdir(parents=True, exist_ok=True) | ||
| 257 | - with ( | ||
| 258 | - open( | ||
| 259 | - log_dir / "agentserver.log", | ||
| 260 | - "a", | ||
| 261 | - encoding="utf-8", | ||
| 262 | - errors="replace", | ||
| 263 | - buffering=1, | ||
| 264 | - ) as log_as, | ||
| 265 | - open( | ||
| 266 | - log_dir / "gateway.log", | ||
| 267 | - "a", | ||
| 268 | - encoding="utf-8", | ||
| 269 | - errors="replace", | ||
| 270 | - buffering=1, | ||
| 271 | - ) as log_gw, | ||
| 272 | - ): | ||
| 273 | - proc_as = subprocess.Popen( | ||
| 274 | - [py, "-m", "jiuwenclaw.app_agentserver", "--port", str(agent_port)], | ||
| 275 | - env=env_as, | ||
| 276 | - cwd=str(repo_root), | ||
| 277 | - stdout=log_as, | ||
| 278 | - stderr=subprocess.STDOUT, | ||
| 279 | - ) | ||
| 280 | - time.sleep(1.5) | ||
| 281 | - proc_gw = subprocess.Popen( | ||
| 282 | - [py, "-m", "jiuwenclaw.app_gateway", "--port", str(web_port), "-u", f"ws://127.0.0.1:{agent_port}"], | ||
| 283 | - env=env_gw, | ||
| 284 | - cwd=str(repo_root), | ||
| 285 | - stdout=log_gw, | ||
| 286 | - stderr=subprocess.STDOUT, | ||
| 287 | - ) | ||
| 288 | - | ||
| 289 | - full_prov: dict[str, Any] = { | ||
| 290 | - "workspace": str(instance_dir), | ||
| 291 | - "ports": {"agent_server": agent_port, "web": web_port, "agent_client_rest": rest_port}, | ||
| 292 | - "pids": {"agentserver": proc_as.pid, "gateway": proc_gw.pid}, | ||
| 293 | - "python": py, | ||
| 294 | - "logs": { | ||
| 295 | - "agentserver": str(log_dir / "agentserver.log"), | ||
| 296 | - "gateway": str(log_dir / "gateway.log"), | ||
| 297 | - }, | ||
| 298 | - } | ||
| 299 | - await merge_instance_data(handler, jiuwenclaw_id, {"provision": full_prov}) | ||
| 300 | - | ||
| 301 | - return { | ||
| 302 | - "jiuwenclaw_id": jiuwenclaw_id, | ||
| 303 | - "status": "pending", | ||
| 304 | - "management_api_base": management_api_base, | ||
| 305 | - "ports": {"agent_server": agent_port, "web": web_port, "agent_client_rest": rest_port}, | ||
| 306 | - "workspace": str(instance_dir), | ||
| 307 | - "pids": {"agentserver": proc_as.pid, "gateway": proc_gw.pid}, | ||
| 308 | - "python": py, | ||
| 309 | - "logs": { | ||
| 310 | - "agentserver": str(log_dir / "agentserver.log"), | ||
| 311 | - "gateway": str(log_dir / "gateway.log"), | ||
| 312 | - }, | ||
| 313 | - } | ||
| 314 | - | ||
| 315 | - | ||
| 316 | -async def terminate_local_if_present(handler: DBHandler, jiuwenclaw_id: str) -> None: | ||
| 317 | - row = await get_instance_row(handler, jiuwenclaw_id) | ||
| 318 | - if row is None: | ||
| 319 | - return | ||
| 320 | - prov = (row.data or {}).get("provision") if isinstance(row.data, dict) else None | ||
| 321 | - pids = prov.get("pids") if isinstance(prov, dict) else None | ||
| 322 | - if not isinstance(pids, dict): | ||
| 323 | - return | ||
| 324 | - import signal | ||
| 325 | - | ||
| 326 | - for pid in pids.values(): | ||
| 327 | - if pid is None: | ||
| 328 | - continue | ||
| 329 | - try: | ||
| 330 | - os.kill(int(pid), signal.SIGTERM) | ||
| 331 | - except (ValueError, OSError): | ||
| 332 | - pass | ||
| 333 | - time.sleep(0.5) | ||
| 334 | - for pid in pids.values(): | ||
| 335 | - if pid is None: | ||
| 336 | - continue | ||
| 337 | - try: | ||
| 338 | - os.kill(int(pid), 0) | ||
| 339 | - os.kill(int(pid), signal.SIGKILL) | ||
| 340 | - except (ValueError, OSError): | ||
| 341 | - pass | ||
Mapplications/manager/manager_server/src/manager_server/core/instance/instance_service.py+274-181文件内容审核中,请稍后刷新重试
Mapplications/manager/manager_server/src/manager_server/infrastructure/config.py+1-26文件内容审核中,请稍后刷新重试
Mapplications/manager/manager_server/src/manager_server/models/instance_models.py+20-11文件内容审核中,请稍后刷新重试
Mapplications/manager/manager_server/src/manager_server/routers/instance_routers.py+7-133文件内容审核中,请稍后刷新重试
Mapplications/manager/manager_server/src/manager_server/schedulers/heartbeat_scanner.py+94-27文件内容审核中,请稍后刷新重试
Mapplications/manager/manager_server/src/manager_server/schemas/instance_schemas.py+33-62文件内容审核中,请稍后刷新重试
Aapplications/manager/manager_server/tests/unit_tests/manager/test_gateway_online_full_sync.py+104-0
| @@ -0,0 +1,104 @@ | |||
| 1 | +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. | ||
| 2 | +"""gateway online 全量同步触发条件。""" | ||
| 3 | + | ||
| 4 | +from __future__ import annotations | ||
| 5 | + | ||
| 6 | +from unittest.mock import AsyncMock, MagicMock, patch | ||
| 7 | + | ||
| 8 | +import pytest | ||
| 9 | + | ||
| 10 | +from manager_server.core.instance.instance_service import ( | ||
| 11 | + maybe_full_sync_gateway_on_online, | ||
| 12 | +) | ||
| 13 | + | ||
| 14 | +pytestmark = pytest.mark.unit | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | +async def test_skip_when_already_online(): | ||
| 19 | + handler = AsyncMock() | ||
| 20 | + with patch( | ||
| 21 | + "manager_server.core.instance.instance_data_lifecycle.sync_data_to_gateway_on_register", | ||
| 22 | + new_callable=AsyncMock, | ||
| 23 | + ) as sync_mock: | ||
| 24 | + await maybe_full_sync_gateway_on_online( | ||
| 25 | + handler, "jid-1", previous_gateway_status="online" | ||
| 26 | + ) | ||
| 27 | + sync_mock.assert_not_awaited() | ||
| 28 | + | ||
| 29 | + | ||
| 30 | + | ||
| 31 | +async def test_sync_when_pending_to_online(): | ||
| 32 | + handler = AsyncMock() | ||
| 33 | + row = MagicMock(gateway_config_host="http://gw:8080", data=None) | ||
| 34 | + with ( | ||
| 35 | + patch( | ||
| 36 | + "manager_server.core.instance.instance_service.get_instance_row", | ||
| 37 | + new_callable=AsyncMock, | ||
| 38 | + return_value=row, | ||
| 39 | + ), | ||
| 40 | + patch( | ||
| 41 | + "manager_server.manager_config_push.endpoint.resolve_gateway_endpoint", | ||
| 42 | + return_value="http://gw:8080", | ||
| 43 | + ), | ||
| 44 | + patch( | ||
| 45 | + "manager_server.core.instance.instance_data_lifecycle.sync_data_to_gateway_on_register", | ||
| 46 | + new_callable=AsyncMock, | ||
| 47 | + return_value={"ok": True}, | ||
| 48 | + ) as sync_mock, | ||
| 49 | + ): | ||
| 50 | + await maybe_full_sync_gateway_on_online( | ||
| 51 | + handler, "jid-1", previous_gateway_status="pending" | ||
| 52 | + ) | ||
| 53 | + sync_mock.assert_awaited_once_with(handler, "jid-1") | ||
| 54 | + | ||
| 55 | + | ||
| 56 | + | ||
| 57 | +async def test_sync_when_offline_to_online(): | ||
| 58 | + handler = AsyncMock() | ||
| 59 | + row = MagicMock(gateway_config_host="http://gw:8080", data=None) | ||
| 60 | + with ( | ||
| 61 | + patch( | ||
| 62 | + "manager_server.core.instance.instance_service.get_instance_row", | ||
| 63 | + new_callable=AsyncMock, | ||
| 64 | + return_value=row, | ||
| 65 | + ), | ||
| 66 | + patch( | ||
| 67 | + "manager_server.manager_config_push.endpoint.resolve_gateway_endpoint", | ||
| 68 | + return_value="http://gw:8080", | ||
| 69 | + ), | ||
| 70 | + patch( | ||
| 71 | + "manager_server.core.instance.instance_data_lifecycle.sync_data_to_gateway_on_register", | ||
| 72 | + new_callable=AsyncMock, | ||
| 73 | + return_value={"ok": True}, | ||
| 74 | + ) as sync_mock, | ||
| 75 | + ): | ||
| 76 | + await maybe_full_sync_gateway_on_online( | ||
| 77 | + handler, "jid-1", previous_gateway_status="offline" | ||
| 78 | + ) | ||
| 79 | + sync_mock.assert_awaited_once() | ||
| 80 | + | ||
| 81 | + | ||
| 82 | + | ||
| 83 | +async def test_skip_when_no_gateway_host(): | ||
| 84 | + handler = AsyncMock() | ||
| 85 | + row = MagicMock(gateway_config_host=None, data=None) | ||
| 86 | + with ( | ||
| 87 | + patch( | ||
| 88 | + "manager_server.core.instance.instance_service.get_instance_row", | ||
| 89 | + new_callable=AsyncMock, | ||
| 90 | + return_value=row, | ||
| 91 | + ), | ||
| 92 | + patch( | ||
| 93 | + "manager_server.manager_config_push.endpoint.resolve_gateway_endpoint", | ||
| 94 | + return_value=None, | ||
| 95 | + ), | ||
| 96 | + patch( | ||
| 97 | + "manager_server.core.instance.instance_data_lifecycle.sync_data_to_gateway_on_register", | ||
| 98 | + new_callable=AsyncMock, | ||
| 99 | + ) as sync_mock, | ||
| 100 | + ): | ||
| 101 | + await maybe_full_sync_gateway_on_online( | ||
| 102 | + handler, "jid-1", previous_gateway_status="offline" | ||
| 103 | + ) | ||
| 104 | + sync_mock.assert_not_awaited() | ||
Mapplications/manager/manager_web/src/pages/instance/modal/CreateInstanceModal.tsx+84-94文件内容审核中,请稍后刷新重试