diff --git a/tests/test_chatapp_cron_deploy.py b/tests/test_chatapp_cron_deploy.py
new file mode 100644
index 0000000..0c087c7
--- /dev/null
+++ b/tests/test_chatapp_cron_deploy.py
@@ -0,0 +1,84 @@
+from __future__ import annotations
+
+from pathlib import Path
+import tempfile
+import unittest
+from unittest.mock import patch
+
+from monitors.yumi import chatapp_cron_deploy as cron
+
+
+class ChatappCronDeployTests(unittest.TestCase):
+ def test_overview_does_not_read_remote_metadata_when_disabled(self) -> None:
+ with (
+ patch.object(cron, "CHATAPP_CRON_ENABLED", False),
+ patch.object(cron, "read_chatapp_cron_metadata") as read_metadata_mock,
+ ):
+ payload = cron.build_chatapp_cron_overview_payload()
+
+ self.assertTrue(payload["ok"])
+ self.assertFalse(payload["enabled"])
+ self.assertEqual(payload["metadata"], {})
+ read_metadata_mock.assert_not_called()
+
+ def test_activate_uploads_binary_and_recreates_compose_service(self) -> None:
+ host = {
+ "host": "cron",
+ "ip": "10.3.1.14",
+ "instanceId": "ins-nt6bsay0",
+ "sshHost": "10.3.1.14",
+ "sshUser": "root",
+ "sshUseSudo": False,
+ "sshPort": 22,
+ }
+
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ binary_path = Path(tmp_dir) / "week-star-job"
+ binary_path.write_text("binary", encoding="utf-8")
+ binary_path.chmod(0o755)
+
+ with (
+ patch.object(cron, "CHATAPP_CRON_SERVICE_NAME", "chatapp-cron"),
+ patch.object(cron, "CHATAPP_CRON_HOST", "cron"),
+ patch.object(cron, "CHATAPP_CRON_REMOTE_DIR", "/opt/deploy/runtime/current"),
+ patch.object(cron, "CHATAPP_CRON_COMPOSE_FILE", "/opt/deploy/runtime/current/docker-compose.yml"),
+ patch.object(cron, "CHATAPP_CRON_COMPOSE_SERVICE", "week-star-job"),
+ patch.object(cron, "CHATAPP_CRON_BINARY_PATH", "/opt/deploy/runtime/current/week-star-job/service"),
+ patch.object(
+ cron,
+ "CHATAPP_CRON_RELEASE_META_PATH",
+ "/opt/deploy/runtime/current/.hy-app-monitor-chatapp-cron-release.json",
+ ),
+ patch.object(cron, "chatapp_cron_host_record", return_value=host),
+ patch.object(cron, "copy_local_path_to_host") as copy_mock,
+ patch.object(
+ cron,
+ "run_host_script_checked",
+ return_value={"target": "cron", "status": "ok", "output": "week-star-job running"},
+ ) as run_remote_mock,
+ ):
+ result = cron.activate_chatapp_cron_binary(
+ {
+ "binaryPath": str(binary_path),
+ "gitRef": "main",
+ "commit": "f0f6205abc1234567890",
+ }
+ )
+
+ copy_mock.assert_called_once()
+ self.assertEqual(copy_mock.call_args.args[0], host)
+ self.assertEqual(copy_mock.call_args.args[1], binary_path)
+
+ run_remote_mock.assert_called_once()
+ remote_script = run_remote_mock.call_args.args[1]
+ self.assertIn("COMPOSE_SERVICE=week-star-job", remote_script)
+ self.assertIn("BINARY_PATH=/opt/deploy/runtime/current/week-star-job/service", remote_script)
+ self.assertIn('up -d --no-deps --force-recreate "$COMPOSE_SERVICE"', remote_script)
+ self.assertIn("META_BLOB=", remote_script)
+
+ self.assertEqual(result["metadata"]["commit"], "f0f6205abc1234567890")
+ self.assertEqual(result["metadata"]["component"], "week-star-job")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_gateway_release_clb.py b/tests/test_gateway_release_clb.py
index 5badfa9..a2cc2c9 100644
--- a/tests/test_gateway_release_clb.py
+++ b/tests/test_gateway_release_clb.py
@@ -492,6 +492,115 @@ class GatewayReleaseFlowTests(unittest.TestCase):
self.assertTrue(result["ok"])
prepare_gateway_release_clb_snapshot_mock.assert_not_called()
+ @patch("monitors.yumi.service_release.build_service_images")
+ @patch("monitors.yumi.service_release.service_records_by_name")
+ @patch("monitors.yumi.service_release.buildable_service_names", return_value=["other"])
+ @patch("monitors.yumi.service_release.host_image_plan")
+ @patch("monitors.yumi.service_release.update_local_service_image")
+ @patch("monitors.yumi.service_release.sync_remote_host_compose_template")
+ @patch("monitors.yumi.service_release.prepull_host_images")
+ @patch("monitors.yumi.service_release.drain_java_registration")
+ @patch("monitors.yumi.service_release.restart_service_instance")
+ @patch("monitors.yumi.service_release.wait_http_ready")
+ @patch("monitors.yumi.service_release.restore_java_registration")
+ def test_other_update_drains_nacos_before_restart(
+ self,
+ restore_java_registration_mock,
+ wait_http_ready_mock,
+ restart_service_instance_mock,
+ drain_java_registration_mock,
+ prepull_host_images_mock,
+ sync_remote_host_compose_template_mock,
+ update_local_service_image_mock,
+ host_image_plan_mock,
+ buildable_service_names_mock,
+ service_records_by_name_mock,
+ build_service_images_mock,
+ ) -> None:
+ del buildable_service_names_mock
+ other_record = {
+ "host": "app-1",
+ "ip": "10.0.0.3",
+ "instanceId": "ins-app-1",
+ "port": 2400,
+ "path": "/actuator/health",
+ "service": "other",
+ "kind": "java",
+ }
+ service_records_by_name_mock.return_value = {"other": [other_record]}
+ build_service_images_mock.return_value = {
+ "serviceImages": {"other": "registry/other:new"},
+ "builds": [],
+ }
+ host_image_plan_mock.return_value = [
+ {
+ "host": "app-1",
+ "ip": "10.0.0.3",
+ "services": ["other"],
+ "images": ["registry/other:new"],
+ }
+ ]
+ update_local_service_image_mock.return_value = {"host": "app-1", "service": "other"}
+ sync_remote_host_compose_template_mock.return_value = {
+ "path": "/tmp/docker-compose.yml",
+ "changed": False,
+ "source": "local",
+ "backupPath": "",
+ }
+ prepull_host_images_mock.return_value = {"status": "SUCCESS", "output": "ok"}
+ wait_http_ready_mock.return_value = {"ok": True, "statusCode": 200, "latencyMs": 11, "detail": "ok"}
+ drain_java_registration_mock.return_value = {
+ "checked": True,
+ "enabled": False,
+ "source": "instance/list",
+ "drainSeconds": 8.0,
+ }
+ restore_java_registration_mock.return_value = {
+ "checked": True,
+ "healthy": True,
+ "enabled": True,
+ "source": "instance/list",
+ "restored": True,
+ }
+
+ call_order: list[str] = []
+ drain_java_registration_mock.side_effect = lambda service: call_order.append("drain") or {
+ "checked": True,
+ "enabled": False,
+ "source": "instance/list",
+ "drainSeconds": 8.0,
+ }
+ restart_service_instance_mock.side_effect = lambda service: call_order.append("restart") or {
+ "status": "SUCCESS",
+ "output": "restarted",
+ }
+ wait_http_ready_mock.side_effect = lambda service, timeout_seconds: call_order.append("http") or {
+ "ok": True,
+ "statusCode": 200,
+ "latencyMs": 11,
+ "detail": "ok",
+ }
+ restore_java_registration_mock.side_effect = lambda service: call_order.append("restore") or {
+ "checked": True,
+ "healthy": True,
+ "enabled": True,
+ "source": "instance/list",
+ "restored": True,
+ }
+
+ result = deploy_runtime_services_payload(
+ ["other"],
+ java_git_ref="main",
+ go_git_ref="main",
+ image_tag="20260423",
+ skip_maven_build=True,
+ )
+
+ self.assertTrue(result["ok"])
+ self.assertEqual(call_order, ["drain", "restart", "http", "restore"])
+ self.assertEqual(result["steps"][0]["nacosDrain"]["enabled"], False)
+ self.assertEqual(result["steps"][0]["nacos"]["restored"], True)
+
if __name__ == "__main__":
unittest.main()