feat: add ops center and node onboarding flow
This commit is contained in:
915
domain-api/tests/test_ops_release_service_package_metadata.py
Normal file
915
domain-api/tests/test_ops_release_service_package_metadata.py
Normal file
@@ -0,0 +1,915 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user