from __future__ import annotations import json import tempfile import unittest from pathlib import Path from unittest.mock import patch from app.services.ops_service import get_ops_go_live_review class OpsServiceGoLiveReviewTests(unittest.TestCase): @patch("app.services.ops_service.get_ops_contract_registry") def test_go_live_review_marks_missing_when_no_manifest_exists(self, mock_get_ops_contract_registry) -> None: mock_get_ops_contract_registry.return_value = { "contracts_by_key": { "ops_go_live_review_contract": {"version": "v1"}, } } with patch("app.services.ops_service._find_latest_go_live_bundle_manifest", return_value=(None, Path("/tmp/go-live-bundles"))): payload = get_ops_go_live_review(base_url="http://127.0.0.1:8100") self.assertEqual("missing", payload["status"]) self.assertEqual("待导出", payload["status_label"]) self.assertIn("go-live-export", payload["recommended_commands"]["go_live_export"]) @patch("app.services.ops_service.get_ops_contract_registry") def test_go_live_review_reads_manifest_and_alignment(self, mock_get_ops_contract_registry) -> None: mock_get_ops_contract_registry.return_value = { "contracts_by_key": { "ops_go_live_review_contract": {"version": "v1"}, } } with tempfile.TemporaryDirectory() as temp_dir: report_dir = Path(temp_dir) / "go-live-bundle-001" report_dir.mkdir(parents=True, exist_ok=True) env_audit_path = report_dir / "00_env_audit.txt" env_audit_path.write_text( "[8/8] condensed env audit summary\n" + json.dumps( { "status": "attention", "headline": "环境仍有轻微缺口", "missing_items": ["missing_tool:jq"], "recommended_actions": ["bash runtime-refresh-recover"], }, ensure_ascii=False, ), encoding="utf-8", ) go_live_summary_path = report_dir / "02_go_live_summary.json" go_live_summary_path.write_text( json.dumps( { "go_live_status": "attention", "publish_status": "attention", "operator_title": "先补接入缺口", "next_step_action_code": "fix_managed_nodes", "launchpad_recommended_target_node_code": "mainland-worker-01", "launchpad_recommended_recovery_label": "先补接入缺口", "launchpad_recommended_recovery_summary": "mainland-worker-01 还缺接入收口。", "launchpad_onboarding_bootstrap_pending_nodes": 1, "launchpad_onboarding_acceptance_ready_nodes": 0, }, ensure_ascii=False, ), encoding="utf-8", ) stack_diagnosis_path = report_dir / "03_stack_diagnosis.json" stack_diagnosis_path.write_text( json.dumps( { "diagnosis": { "stack_status": "attention", "operator_decision": {"title": "先补接入缺口"}, "next_step": {"action_code": "fix_managed_nodes"}, "launchpad_recommended_target_node_code": "mainland-worker-02", "launchpad_recommended_recovery_label": "先看节点接管", "launchpad_recommended_recovery_summary": "stack 与 summary 存在漂移。", "launchpad_onboarding_bootstrap_pending_nodes": 1, "launchpad_onboarding_acceptance_ready_nodes": 0, } }, ensure_ascii=False, ), encoding="utf-8", ) driver_feed_path = report_dir / "04_driver_feed.json" driver_feed_path.write_text( json.dumps( { "headline": "driver ok", "summary": { "launchpad_recommended_target_node_code": "mainland-worker-01", "launchpad_recommended_recovery_label": "先补接入缺口", "launchpad_recommended_recovery_summary": "mainland-worker-01 还缺接入收口。", "launchpad_onboarding_bootstrap_pending_nodes": 1, "launchpad_onboarding_acceptance_ready_nodes": 0, }, "automation_coverage": {"launch_status": "attention", "launch_ready": False}, }, ensure_ascii=False, ), encoding="utf-8", ) codex_brief_path = report_dir / "05_codex_brief.json" codex_brief_path.write_text( json.dumps( { "headline": "codex ok", "summary": { "launchpad_recommended_target_node_code": "mainland-worker-01", "launchpad_recommended_recovery_label": "先补接入缺口", "launchpad_recommended_recovery_summary": "mainland-worker-01 还缺接入收口。", "launchpad_onboarding_bootstrap_pending_nodes": 1, "launchpad_onboarding_acceptance_ready_nodes": 0, }, "automation_coverage": {"launch_status": "attention", "launch_ready": False}, "focus": {"entry_key": "fix_managed_nodes"}, }, ensure_ascii=False, ), encoding="utf-8", ) manifest_path = report_dir / "manifest.json" manifest_path.write_text( json.dumps( { "report_dir": str(report_dir), "artifacts": [ {"key": "env_audit", "path": str(env_audit_path), "ok": True}, {"key": "go_live_summary", "path": str(go_live_summary_path), "ok": True}, {"key": "stack_diagnosis", "path": str(stack_diagnosis_path), "ok": True}, {"key": "driver_feed", "path": str(driver_feed_path), "ok": True}, {"key": "codex_brief", "path": str(codex_brief_path), "ok": True}, {"key": "release_launchpad", "path": str(report_dir / "06_release_launchpad.json"), "ok": True}, ], "failures": ["doctor_export"], "summary": { "artifact_total": 9, "failure_total": 1, "recommended_reading_order": ["00_env_audit.txt", "02_go_live_summary.json"], }, }, ensure_ascii=False, indent=2, ), encoding="utf-8", ) with patch( "app.services.ops_service._find_latest_go_live_bundle_manifest", return_value=(manifest_path, report_dir), ): payload = get_ops_go_live_review(base_url="http://127.0.0.1:8100") self.assertEqual("attention", payload["status"]) self.assertEqual(9, payload["artifact_total"]) self.assertEqual(["doctor_export"], payload["noncritical_failures"]) self.assertFalse(payload["launchpad_alignment"]["consistent"]) self.assertIn("go_live_summary", payload["launchpad_alignment"]["available_sources"]) self.assertIn("mainland-worker-01", payload["recommended_next_steps"][0] + "".join(payload["recommended_next_steps"])) self.assertIn(str(manifest_path), payload["recommended_commands"]["go_live_review"]) if __name__ == "__main__": unittest.main()