Files
getDomain/domain-api/tests/test_ops_release_service_package_metadata.py
2026-04-18 23:52:51 +08:00

916 lines
40 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.
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()