import json import os import tempfile import unittest from pathlib import Path from unittest.mock import patch from app.services.ops_release_service import ( build_smart_release_rollout_plan, create_release_and_smart_rollout_from_latest_package, create_release_from_latest_package, create_smart_release_rollout, get_latest_release_package_metadata, get_release_package_archive_path, get_release_package_sha256_path, ) class OpsReleaseServicePackageMetadataTests(unittest.TestCase): def test_get_latest_release_package_metadata_reads_latest_linux_package(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: release_root = Path(tmpdir) package_name = "domaincheck_release_20260418_120000" staging_path = release_root / package_name archive_path = release_root / f"{package_name}.tar.gz" sha256_path = release_root / f"{package_name}.sha256.txt" staging_path.mkdir(parents=True, exist_ok=True) archive_path.write_bytes(b"linux-package") sha256_path.write_text("sha256=test-sha256", encoding="utf-8") (staging_path / "release_manifest.json").write_text( json.dumps( { "package_name": package_name, "package_type": "tar.gz", "included": { "docs": ["08_domainCheck_交付说明.md"], "scripts": ["package_domain_release.sh", "verify_domain_release.sh"], }, }, ensure_ascii=False, ), encoding="utf-8", ) (release_root / "latest_release.json").write_text( json.dumps( { "package_name": package_name, "package_type": "tar.gz", "generated_at": "2026-04-18T12:00:00", "staging_path": str(staging_path), "archive_path": str(archive_path), "sha256_path": str(sha256_path), "sha256": "test-sha256", "smoke_test_ok": True, }, ensure_ascii=False, ), encoding="utf-8", ) (release_root / "final_release_report.json").write_text( json.dumps( { "ok": True, "latest_release": { "package_name": package_name, "archive_path": str(archive_path), "sha256": "test-sha256", }, "release_gate": { "decision": "ready", "blocked_reasons": [], "verify_ok": True, "smoke_test_ok": True, }, }, ensure_ascii=False, ), encoding="utf-8", ) with patch.dict(os.environ, {"DOMAINCHECK_RELEASE_ROOT": str(release_root)}): data = get_latest_release_package_metadata() self.assertTrue(data["available"]) self.assertEqual(package_name, data["package_name"]) self.assertEqual("tar.gz", data["package_type"]) self.assertTrue(data["archive_present"]) self.assertTrue(data["sha256_present"]) self.assertTrue(data["staging_present"]) self.assertEqual("test-sha256", data["sha256"]) self.assertEqual(f"/api/v1/ops/releases/packages/{package_name}/download", data["download_path"]) self.assertEqual(package_name, data["release_version_suggestion"]) self.assertIn("自检通过", data["notes_suggestion"]) self.assertTrue(data["final_release_ok"]) self.assertTrue(data["final_release_report_available"]) self.assertTrue(data["final_release_report_matches_latest"]) self.assertEqual("ready", data["final_release_gate_decision"]) self.assertEqual([], data["final_release_gate_blocked_reasons"]) self.assertEqual(package_name, data["metadata"]["manifest"]["package_name"]) def test_get_release_package_paths_support_metadata_fallback_for_zip(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: release_root = Path(tmpdir) package_name = "domaincheck_release_windows_demo" package_dir = release_root / "exports" package_dir.mkdir(parents=True, exist_ok=True) archive_path = package_dir / f"{package_name}.zip" sha256_path = package_dir / f"{package_name}.sha256.txt" archive_path.write_bytes(b"zip-package") sha256_path.write_text("sha256=zip-sha256", encoding="utf-8") (release_root / "latest_release.json").write_text( json.dumps( { "package_name": package_name, "package_type": "zip", "generated_at": "2026-04-18T12:10:00", "zip_path": str(archive_path), "sha256_path": str(sha256_path), "sha256": "zip-sha256", "smoke_test_ok": False, }, ensure_ascii=False, ), encoding="utf-8", ) with patch.dict(os.environ, {"DOMAINCHECK_RELEASE_ROOT": str(release_root)}): resolved_archive = get_release_package_archive_path(package_name) resolved_sha256 = get_release_package_sha256_path(package_name) metadata = get_latest_release_package_metadata() self.assertEqual(archive_path, resolved_archive) self.assertEqual(sha256_path, resolved_sha256) self.assertEqual("zip", metadata["package_type"]) self.assertEqual(archive_path.name, metadata["archive_filename"]) self.assertFalse(metadata["smoke_test_ok"]) def test_get_latest_release_package_metadata_returns_unavailable_when_missing(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: with patch.dict(os.environ, {"DOMAINCHECK_RELEASE_ROOT": tmpdir}): data = get_latest_release_package_metadata() self.assertFalse(data["available"]) self.assertIn("latest_release.json", data["reason"]) def test_get_latest_release_package_metadata_marks_stale_final_release_report(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: release_root = Path(tmpdir) package_name = "domaincheck_release_20260418_130000" archive_path = release_root / f"{package_name}.tar.gz" sha256_path = release_root / f"{package_name}.sha256.txt" archive_path.write_bytes(b"linux-package") sha256_path.write_text("sha256=test-sha256", encoding="utf-8") (release_root / "latest_release.json").write_text( json.dumps( { "package_name": package_name, "package_type": "tar.gz", "archive_path": str(archive_path), "sha256_path": str(sha256_path), "sha256": "test-sha256", "smoke_test_ok": True, }, ensure_ascii=False, ), encoding="utf-8", ) (release_root / "final_release_report.json").write_text( json.dumps( { "ok": True, "latest_release": { "package_name": "domaincheck_release_20260418_old", "archive_path": str(release_root / "domaincheck_release_20260418_old.tar.gz"), "sha256": "old-sha256", }, "release_gate": { "decision": "ready", "blocked_reasons": [], "verify_ok": True, "smoke_test_ok": True, }, }, ensure_ascii=False, ), encoding="utf-8", ) with patch.dict(os.environ, {"DOMAINCHECK_RELEASE_ROOT": str(release_root)}): data = get_latest_release_package_metadata() self.assertTrue(data["available"]) self.assertFalse(data["final_release_ok"]) self.assertTrue(data["final_release_report_available"]) self.assertTrue(data["final_release_report_ok"]) self.assertFalse(data["final_release_report_matches_latest"]) self.assertIn("package_name", data["final_release_report_stale_reason"]) @patch("app.services.ops_release_service.create_release") @patch("app.services.ops_release_service._get_release_by_version") @patch("app.services.ops_release_service.get_latest_release_package_metadata") def test_create_release_from_latest_package_builds_payload( self, mock_get_latest_release_package_metadata, mock_get_release_by_version, mock_create_release, ) -> None: mock_get_latest_release_package_metadata.return_value = { "available": True, "package_name": "domaincheck_release_20260418_120000", "release_version_suggestion": "domaincheck_release_20260418_120000", "download_path": "/api/v1/ops/releases/packages/domaincheck_release_20260418_120000/download", "sha256_download_path": "/api/v1/ops/releases/packages/domaincheck_release_20260418_120000/sha256", "sha256": "test-sha256", "commit_sha": "bd6bcb2", "notes_suggestion": "导入自本机发布包 domaincheck_release_20260418_120000.tar.gz;自检通过", "smoke_test_ok": True, "final_release_ok": True, "final_release_report_available": True, "final_release_report_ok": True, "final_release_report_matches_latest": True, "final_release_gate_decision": "ready", "final_release_gate_blocked_reasons": [], "metadata": { "package_name": "domaincheck_release_20260418_120000", "package_type": "tar.gz", }, } mock_get_release_by_version.return_value = {} mock_create_release.return_value = ( True, "Release 已创建", { "release": { "id": 7, "release_version": "domaincheck_release_20260418_120000", } }, ) ok, message, data = create_release_from_latest_package( { "channel": "stable", "status": "ready", "created_by": "test-suite", }, control_plane_base_url="https://ops.example.com/api/v1", ) self.assertTrue(ok) self.assertEqual("Release 已创建", message) mock_create_release.assert_called_once() create_payload = mock_create_release.call_args.args[0] self.assertEqual("domaincheck_release_20260418_120000", create_payload["release_version"]) self.assertEqual("bd6bcb2", create_payload["commit_sha"]) self.assertEqual( "https://ops.example.com/api/v1/ops/releases/packages/domaincheck_release_20260418_120000/download", create_payload["artifact_url"], ) self.assertEqual("test-sha256", create_payload["checksum"]) self.assertEqual("latest_release_package", create_payload["metadata"]["created_from"]) self.assertEqual( "https://ops.example.com/api/v1/ops/releases/packages/domaincheck_release_20260418_120000/download", create_payload["metadata"]["package_source"]["download_url"], ) self.assertFalse(data["deduplicated"]) @patch("app.services.ops_release_service.create_release") @patch("app.services.ops_release_service._get_release_by_version") @patch("app.services.ops_release_service.get_latest_release_package_metadata") def test_create_release_from_latest_package_is_idempotent( self, mock_get_latest_release_package_metadata, mock_get_release_by_version, mock_create_release, ) -> None: mock_get_latest_release_package_metadata.return_value = { "available": True, "package_name": "domaincheck_release_20260418_120000", "release_version_suggestion": "domaincheck_release_20260418_120000", "smoke_test_ok": True, "final_release_ok": True, "final_release_report_available": True, "final_release_report_ok": True, "final_release_report_matches_latest": True, "final_release_gate_decision": "ready", "final_release_gate_blocked_reasons": [], } mock_get_release_by_version.return_value = { "id": 9, "release_version": "domaincheck_release_20260418_120000", } ok, message, data = create_release_from_latest_package({}, control_plane_base_url="https://ops.example.com") self.assertTrue(ok) self.assertEqual("Release 已存在,按幂等返回", message) self.assertTrue(data["deduplicated"]) self.assertEqual(9, data["release"]["id"]) mock_create_release.assert_not_called() @patch("app.services.ops_release_service.create_release") @patch("app.services.ops_release_service._get_release_by_version") @patch("app.services.ops_release_service.get_latest_release_package_metadata") def test_create_release_from_latest_package_blocks_when_final_signoff_missing( self, mock_get_latest_release_package_metadata, mock_get_release_by_version, mock_create_release, ) -> None: mock_get_latest_release_package_metadata.return_value = { "available": True, "package_name": "domaincheck_release_20260418_130000", "release_version_suggestion": "domaincheck_release_20260418_130000", "download_path": "/api/v1/ops/releases/packages/domaincheck_release_20260418_130000/download", "sha256_download_path": "/api/v1/ops/releases/packages/domaincheck_release_20260418_130000/sha256", "sha256": "test-sha256", "commit_sha": "bd6bcb2", "smoke_test_ok": False, "final_release_ok": False, "final_release_report_available": False, "final_release_report_ok": None, "final_release_report_matches_latest": None, "final_release_gate_decision": "", "final_release_gate_blocked_reasons": [], "metadata": { "package_name": "domaincheck_release_20260418_130000", "package_type": "tar.gz", }, } mock_get_release_by_version.return_value = {} ok, message, data = create_release_from_latest_package( { "channel": "stable", "status": "ready", "created_by": "test-suite", }, control_plane_base_url="https://ops.example.com/api/v1", ) self.assertFalse(ok) self.assertIn("最终签收", message) self.assertFalse(data["final_release_gate"]["ready"]) self.assertIn("smoke_test_failed_or_skipped", data["final_release_gate"]["blocked_reasons"]) self.assertIn("final_release_report_missing", data["final_release_gate"]["blocked_reasons"]) mock_create_release.assert_not_called() @patch("app.services.ops_release_service.create_release") @patch("app.services.ops_release_service._get_release_by_version") @patch("app.services.ops_release_service.get_latest_release_package_metadata") def test_create_release_from_latest_package_blocks_when_final_report_is_stale( self, mock_get_latest_release_package_metadata, mock_get_release_by_version, mock_create_release, ) -> None: mock_get_latest_release_package_metadata.return_value = { "available": True, "package_name": "domaincheck_release_20260418_130001", "release_version_suggestion": "domaincheck_release_20260418_130001", "download_path": "/api/v1/ops/releases/packages/domaincheck_release_20260418_130001/download", "sha256_download_path": "/api/v1/ops/releases/packages/domaincheck_release_20260418_130001/sha256", "sha256": "test-sha256", "commit_sha": "bd6bcb2", "smoke_test_ok": True, "final_release_ok": False, "final_release_report_available": True, "final_release_report_ok": True, "final_release_report_matches_latest": False, "final_release_report_stale_reason": "final_release_report does not match latest_release: package_name", "final_release_gate_decision": "ready", "final_release_gate_blocked_reasons": [], "metadata": { "package_name": "domaincheck_release_20260418_130001", "package_type": "tar.gz", }, } mock_get_release_by_version.return_value = { "id": 19, "release_version": "domaincheck_release_20260418_130001", } ok, message, data = create_release_from_latest_package( {"created_by": "test-suite"}, control_plane_base_url="https://ops.example.com/api/v1", ) self.assertFalse(ok) self.assertIn("最终签收", message) self.assertFalse(data["final_release_gate"]["ready"]) self.assertEqual( "final_release_report does not match latest_release: package_name", data["final_release_gate"]["stale_reason"], ) self.assertIn("final_release_report_not_matching_latest", data["final_release_gate"]["blocked_reasons"]) self.assertEqual(19, data["release"]["id"]) mock_create_release.assert_not_called() @patch("app.services.ops_agent_service.list_managed_nodes_with_agent_state") def test_build_smart_release_rollout_plan_prefers_remote_ready_workers( self, mock_list_managed_nodes_with_agent_state, ) -> None: mock_list_managed_nodes_with_agent_state.return_value = { "nodes": [ { "node_code": "mainland-worker-01", "region": "mainland", "role": "worker", "is_enabled": True, "is_managed": True, "is_agent_online": True, "cluster_status": "online", "cluster_current_load": 0, "cluster_is_effective_worker": True, "cluster_detect_participating": False, }, { "node_code": "mainland-controller-01", "region": "mainland", "role": "control", "is_enabled": True, "is_managed": True, "is_agent_online": True, "cluster_status": "online", "cluster_current_load": 1, "cluster_is_effective_worker": True, "cluster_detect_participating": True, }, { "node_code": "mainland-worker-02", "region": "mainland", "role": "worker", "is_enabled": True, "is_managed": True, "is_agent_online": False, "cluster_status": "online", "cluster_current_load": 0, "cluster_is_effective_worker": True, "cluster_detect_participating": False, }, ] } worker_plan = build_smart_release_rollout_plan("worker") control_plan = build_smart_release_rollout_plan("control") self.assertTrue(worker_plan["available"]) self.assertEqual("worker", worker_plan["mode"]) self.assertEqual(["mainland-worker-01"], worker_plan["target_selector"]["node_codes"]) self.assertEqual(["domaincheck-worker"], worker_plan["policy"]["deploy_payload"]["restart_services"]) self.assertTrue(control_plan["available"]) self.assertEqual("control", control_plan["mode"]) self.assertEqual(["mainland-controller-01"], control_plan["target_selector"]["node_codes"]) self.assertIn("domaincheck-api", control_plan["policy"]["deploy_payload"]["restart_services"]) self.assertEqual("remote-agent", worker_plan["policy"]["execution_mode"]) @patch("app.services.ops_agent_service.list_managed_nodes_with_agent_state") def test_build_smart_release_rollout_plan_falls_back_to_ssh_ready_workers( self, mock_list_managed_nodes_with_agent_state, ) -> None: mock_list_managed_nodes_with_agent_state.return_value = { "nodes": [ { "node_code": "mainland-worker-01", "region": "mainland", "role": "worker", "is_enabled": True, "is_managed": True, "is_agent_online": False, "has_ssh_access": True, "cluster_status": "online", "cluster_current_load": 0, "cluster_is_effective_worker": True, "cluster_detect_participating": False, }, { "node_code": "mainland-worker-02", "region": "mainland", "role": "worker", "is_enabled": True, "is_managed": True, "is_agent_online": False, "has_ssh_access": True, "cluster_status": "busy", "cluster_current_load": 1, "cluster_is_effective_worker": True, "cluster_detect_participating": True, }, ] } worker_plan = build_smart_release_rollout_plan("worker") self.assertTrue(worker_plan["available"]) self.assertEqual("worker", worker_plan["mode"]) self.assertEqual("ssh", worker_plan["execution_mode"]) self.assertEqual("SSH", worker_plan["execution_mode_label"]) self.assertEqual( ["mainland-worker-01", "mainland-worker-02"], worker_plan["target_selector"]["node_codes"], ) self.assertEqual("ssh", worker_plan["policy"]["execution_mode"]) @patch("app.services.ops_agent_service.list_managed_nodes_with_agent_state") def test_build_smart_release_rollout_plan_excludes_dead_letter_nodes_from_ready_candidates( self, mock_list_managed_nodes_with_agent_state, ) -> None: mock_list_managed_nodes_with_agent_state.return_value = { "nodes": [ { "node_code": "mainland-worker-01", "region": "mainland", "role": "worker", "is_enabled": True, "is_managed": True, "is_agent_online": True, "has_ssh_access": True, "delivery_queue_state": "dead_letter", "cluster_status": "online", "cluster_current_load": 0, "cluster_is_effective_worker": True, "cluster_detect_participating": False, }, { "node_code": "mainland-worker-02", "region": "mainland", "role": "worker", "is_enabled": True, "is_managed": True, "is_agent_online": True, "has_ssh_access": True, "delivery_queue_state": "healthy", "cluster_status": "online", "cluster_current_load": 0, "cluster_is_effective_worker": True, "cluster_detect_participating": False, }, ] } worker_plan = build_smart_release_rollout_plan("worker") self.assertTrue(worker_plan["available"]) self.assertEqual(["mainland-worker-02"], worker_plan["target_selector"]["node_codes"]) self.assertEqual("healthy", worker_plan["target_nodes"][0]["delivery_queue_state"]) @patch("app.services.ops_agent_service.list_managed_nodes_with_agent_state") def test_build_smart_release_rollout_plan_returns_unavailable_when_only_dead_letter_nodes_exist( self, mock_list_managed_nodes_with_agent_state, ) -> None: mock_list_managed_nodes_with_agent_state.return_value = { "nodes": [ { "node_code": "mainland-worker-01", "region": "mainland", "role": "worker", "is_enabled": True, "is_managed": True, "is_agent_online": True, "has_ssh_access": True, "delivery_queue_state": "dead_letter", "cluster_status": "online", "cluster_current_load": 0, "cluster_is_effective_worker": True, "cluster_detect_participating": False, } ] } worker_plan = build_smart_release_rollout_plan("worker") self.assertFalse(worker_plan["available"]) self.assertIn("死信回执", worker_plan["reason"]) self.assertEqual("dead_letter", worker_plan["target_nodes"][0]["delivery_queue_state"]) @patch("app.services.ops_release_service.create_release_rollout") @patch("app.services.ops_policy_service.preview_ops_job_policy") @patch("app.services.ops_agent_service.list_managed_nodes_with_agent_state") @patch("app.services.ops_release_service.create_release_from_latest_package") def test_create_release_and_smart_rollout_stops_when_latest_package_gate_blocks( self, mock_create_release_from_latest_package, mock_list_managed_nodes_with_agent_state, mock_preview_ops_job_policy, mock_create_release_rollout, ) -> None: mock_create_release_from_latest_package.return_value = ( False, "最新发布包尚未完成最终签收,暂不允许创建 Release 或发起基于最新包的智能 Rollout。", { "package": { "available": True, "package_name": "domaincheck_release_20260418_blocked", "final_release_ok": False, }, "final_release_gate": { "ready": False, "status": "blocked", "blocked_reasons": ["final_release_report_missing"], }, }, ) ok, message, data = create_release_and_smart_rollout_from_latest_package( {"mode": "worker"}, control_plane_base_url="https://ops.example.com", ) self.assertFalse(ok) self.assertIn("最终签收", message) self.assertFalse(data["final_release_gate"]["ready"]) mock_list_managed_nodes_with_agent_state.assert_not_called() mock_preview_ops_job_policy.assert_not_called() mock_create_release_rollout.assert_not_called() @patch("app.services.ops_release_service.create_release_rollout") @patch("app.services.ops_policy_service.preview_ops_job_policy") @patch("app.services.ops_agent_service.list_managed_nodes_with_agent_state") @patch("app.services.ops_release_service.create_release_from_latest_package") def test_create_release_and_smart_rollout_requires_confirmation_before_rollout( self, mock_create_release_from_latest_package, mock_list_managed_nodes_with_agent_state, mock_preview_ops_job_policy, mock_create_release_rollout, ) -> None: mock_create_release_from_latest_package.return_value = ( True, "Release 已创建", { "release": {"id": 21, "release_version": "domaincheck_release_20260418_120000"}, "deduplicated": False, }, ) mock_list_managed_nodes_with_agent_state.return_value = { "nodes": [ { "node_code": "mainland-controller-01", "region": "mainland", "role": "control", "is_enabled": True, "is_managed": True, "is_agent_online": True, "cluster_status": "online", "cluster_current_load": 0, "cluster_is_effective_worker": True, "cluster_detect_participating": False, } ] } mock_preview_ops_job_policy.return_value = { "blocked": False, "approval_required": True, "warnings": ["本次 rollout 覆盖 control 节点,建议审批。"], "release_gate": {"status_label": "待补巡检", "summary": "建议先确认控制面健康。"}, } ok, message, data = create_release_and_smart_rollout_from_latest_package( {"mode": "control"}, control_plane_base_url="https://ops.example.com", ) self.assertTrue(ok) self.assertEqual("Release 已创建,智能 Rollout 需要确认", message) self.assertTrue(data["requires_confirmation"]) self.assertFalse(data["rollout_created"]) self.assertEqual(["mainland-controller-01"], data["smart_rollout"]["target_selector"]["node_codes"]) mock_create_release_rollout.assert_not_called() @patch("app.services.ops_release_service.create_release_rollout") @patch("app.services.ops_policy_service.preview_ops_job_policy") @patch("app.services.ops_agent_service.list_managed_nodes_with_agent_state") @patch("app.services.ops_release_service.create_release_from_latest_package") def test_create_release_and_smart_rollout_creates_rollout_after_confirmation( self, mock_create_release_from_latest_package, mock_list_managed_nodes_with_agent_state, mock_preview_ops_job_policy, mock_create_release_rollout, ) -> None: mock_create_release_from_latest_package.return_value = ( True, "Release 已创建", { "release": {"id": 22, "release_version": "domaincheck_release_20260418_120000"}, "deduplicated": True, }, ) mock_list_managed_nodes_with_agent_state.return_value = { "nodes": [ { "node_code": "mainland-worker-01", "region": "mainland", "role": "worker", "is_enabled": True, "is_managed": True, "is_agent_online": True, "cluster_status": "online", "cluster_current_load": 0, "cluster_is_effective_worker": True, "cluster_detect_participating": False, } ] } mock_preview_ops_job_policy.return_value = { "blocked": False, "approval_required": False, "warnings": [], "release_gate": {"status_label": "可发 Rollout", "summary": "当前可推进。"}, } mock_create_release_rollout.return_value = ( True, "Release rollout 已创建", { "rollout": {"id": 5, "release_id": 22, "rollout_code": "rollout-20260418-demo"}, "jobs": [], }, ) ok, message, data = create_release_and_smart_rollout_from_latest_package( {"mode": "worker", "confirm_risky": True}, control_plane_base_url="https://ops.example.com", ) self.assertTrue(ok) self.assertEqual("Release rollout 已创建", message) self.assertTrue(data["rollout_created"]) self.assertEqual(5, data["rollout"]["id"]) mock_create_release_rollout.assert_called_once() rollout_payload = mock_create_release_rollout.call_args.args[1] self.assertEqual(["mainland-worker-01"], rollout_payload["target_selector"]["node_codes"]) self.assertEqual("remote-agent", rollout_payload["policy"]["execution_mode"]) @patch("app.services.ops_release_service.create_release_rollout") @patch("app.services.ops_policy_service.preview_ops_job_policy") @patch("app.services.ops_agent_service.list_managed_nodes_with_agent_state") @patch("app.services.ops_release_service.create_release_from_latest_package") def test_create_release_and_smart_rollout_uses_ssh_when_only_ssh_path_is_ready( self, mock_create_release_from_latest_package, mock_list_managed_nodes_with_agent_state, mock_preview_ops_job_policy, mock_create_release_rollout, ) -> None: mock_create_release_from_latest_package.return_value = ( True, "Release 已创建", { "release": {"id": 23, "release_version": "domaincheck_release_20260418_ssh"}, "deduplicated": True, }, ) mock_list_managed_nodes_with_agent_state.return_value = { "nodes": [ { "node_code": "mainland-worker-01", "region": "mainland", "role": "worker", "is_enabled": True, "is_managed": True, "is_agent_online": False, "has_ssh_access": True, "cluster_status": "online", "cluster_current_load": 0, "cluster_is_effective_worker": True, "cluster_detect_participating": False, } ] } mock_preview_ops_job_policy.return_value = { "blocked": False, "approval_required": False, "warnings": [], "release_gate": {"status_label": "可发 Rollout", "summary": "当前可推进。"}, } mock_create_release_rollout.return_value = ( True, "Release rollout 已创建", { "rollout": {"id": 6, "release_id": 23, "rollout_code": "rollout-20260418-ssh"}, "jobs": [], }, ) ok, message, data = create_release_and_smart_rollout_from_latest_package( {"mode": "worker", "confirm_risky": True}, control_plane_base_url="https://ops.example.com", ) self.assertTrue(ok) self.assertEqual("Release rollout 已创建", message) self.assertTrue(data["rollout_created"]) rollout_payload = mock_create_release_rollout.call_args.args[1] self.assertEqual("ssh", rollout_payload["policy"]["execution_mode"]) @patch("app.services.ops_release_service.get_release") @patch("app.services.ops_release_service.create_release_rollout") @patch("app.services.ops_policy_service.preview_ops_job_policy") @patch("app.services.ops_agent_service.list_managed_nodes_with_agent_state") def test_create_smart_release_rollout_for_existing_release_requires_confirmation( self, mock_list_managed_nodes_with_agent_state, mock_preview_ops_job_policy, mock_create_release_rollout, mock_get_release, ) -> None: mock_get_release.return_value = { "id": 41, "release_version": "domaincheck_release_existing", "channel": "stable", } mock_list_managed_nodes_with_agent_state.return_value = { "nodes": [ { "node_code": "mainland-controller-01", "region": "mainland", "role": "control", "is_enabled": True, "is_managed": True, "is_agent_online": True, "cluster_status": "online", "cluster_current_load": 0, "cluster_is_effective_worker": True, "cluster_detect_participating": False, } ] } mock_preview_ops_job_policy.return_value = { "blocked": False, "approval_required": True, "warnings": ["控制面节点建议审批。"], "release_gate": {"status_label": "待审批", "summary": "需要审批。"}, } ok, message, data = create_smart_release_rollout(41, {"mode": "control"}) self.assertTrue(ok) self.assertEqual("智能 Rollout 需要确认", message) self.assertFalse(data["release_created"]) self.assertTrue(data["requires_confirmation"]) self.assertFalse(data["rollout_created"]) self.assertEqual(["mainland-controller-01"], data["smart_rollout"]["target_selector"]["node_codes"]) mock_create_release_rollout.assert_not_called() @patch("app.services.ops_release_service.get_release") @patch("app.services.ops_release_service.create_release_rollout") @patch("app.services.ops_policy_service.preview_ops_job_policy") @patch("app.services.ops_agent_service.list_managed_nodes_with_agent_state") def test_create_smart_release_rollout_for_existing_release_creates_rollout( self, mock_list_managed_nodes_with_agent_state, mock_preview_ops_job_policy, mock_create_release_rollout, mock_get_release, ) -> None: mock_get_release.return_value = { "id": 42, "release_version": "domaincheck_release_existing", "channel": "stable", } mock_list_managed_nodes_with_agent_state.return_value = { "nodes": [ { "node_code": "mainland-worker-01", "region": "mainland", "role": "worker", "is_enabled": True, "is_managed": True, "is_agent_online": True, "cluster_status": "online", "cluster_current_load": 0, "cluster_is_effective_worker": True, "cluster_detect_participating": False, } ] } mock_preview_ops_job_policy.return_value = { "blocked": False, "approval_required": False, "warnings": [], "release_gate": {"status_label": "可发 Rollout", "summary": "当前可推进。"}, } mock_create_release_rollout.return_value = ( True, "Release rollout 已创建", { "rollout": {"id": 8, "release_id": 42, "rollout_code": "rollout-20260418-existing"}, "jobs": [], }, ) ok, message, data = create_smart_release_rollout(42, {"mode": "worker", "confirm_risky": True}) self.assertTrue(ok) self.assertEqual("Release rollout 已创建", message) self.assertFalse(data["release_created"]) self.assertTrue(data["rollout_created"]) self.assertEqual(8, data["rollout"]["id"]) mock_create_release_rollout.assert_called_once() rollout_payload = mock_create_release_rollout.call_args.args[1] self.assertEqual(["mainland-worker-01"], rollout_payload["target_selector"]["node_codes"]) self.assertEqual("remote-agent", rollout_payload["policy"]["execution_mode"]) if __name__ == "__main__": unittest.main()