This commit is contained in:
Your Name
2026-04-22 14:13:21 +08:00
parent e0406b5d0e
commit 7cbde2aa78
145 changed files with 23086 additions and 2243 deletions

View File

@@ -0,0 +1,93 @@
from __future__ import annotations
import unittest
from unittest.mock import MagicMock, patch
from app.services import cluster_runtime_service
class RuntimeSchemaGuardTests(unittest.TestCase):
def test_ensure_runtime_schema_executes_only_once_per_process(self) -> None:
conn = MagicMock()
cursor_cm = MagicMock()
cursor = MagicMock()
conn.cursor.return_value = cursor_cm
cursor_cm.__enter__.return_value = cursor
db_cm = MagicMock()
db_cm.__enter__.return_value = conn
with patch.object(cluster_runtime_service, "_RUNTIME_SCHEMA_READY", False):
with patch.object(cluster_runtime_service, "get_db", return_value=db_cm) as mocked_get_db:
cluster_runtime_service.ensure_runtime_schema()
cluster_runtime_service.ensure_runtime_schema()
mocked_get_db.assert_called_once()
self.assertEqual(cursor.execute.call_count, 2)
cursor.execute.assert_any_call(
"SELECT pg_advisory_xact_lock(%s)",
(cluster_runtime_service._RUNTIME_SCHEMA_ADVISORY_LOCK_ID,),
)
cursor.execute.assert_any_call(cluster_runtime_service._RUNTIME_SCHEMA_SQL)
conn.commit.assert_called_once()
def test_control_node_supports_worker_only_on_mainland_with_worker_signals(self) -> None:
self.assertFalse(
cluster_runtime_service._control_node_supports_worker(
region="overseas",
metadata={
"worker_online": False,
"detect_participating": True,
"active_threads": 0,
"max_threads": 0,
},
)
)
self.assertTrue(
cluster_runtime_service._control_node_supports_worker(
region="mainland",
metadata={
"worker_online": True,
"detect_participating": False,
"active_threads": 0,
"max_threads": 0,
},
)
)
def test_metadata_idle_without_runtime_work_detects_stale_idle_heartbeat(self) -> None:
self.assertTrue(
cluster_runtime_service._metadata_idle_without_runtime_work(
{
"phase_label": "idle",
"phase_detail": "Worker 已启动,等待检测指令",
"active_threads": 323,
"max_threads": 4,
"active_job_code": "",
"job_items_total": 0,
"job_items_claimed": 0,
"job_items_running": 0,
"job_items_completed": 0,
"job_items_failed": 0,
}
)
)
self.assertFalse(
cluster_runtime_service._metadata_idle_without_runtime_work(
{
"phase_label": "running",
"active_threads": 12,
}
)
)
self.assertFalse(
cluster_runtime_service._metadata_idle_without_runtime_work(
{
"phase_label": "idle",
"active_job_code": "sync-overseas-1",
}
)
)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,34 @@
from psycopg2 import errors
from app.core.db import db_read_retry
def test_db_read_retry_retries_retryable_error_once() -> None:
attempts = {"count": 0}
@db_read_retry(attempts=3, initial_delay_seconds=0)
def flaky() -> str:
attempts["count"] += 1
if attempts["count"] == 1:
raise errors.DeadlockDetected()
return "ok"
assert flaky() == "ok"
assert attempts["count"] == 2
def test_db_read_retry_does_not_swallow_non_retryable_error() -> None:
attempts = {"count": 0}
@db_read_retry(attempts=3, initial_delay_seconds=0)
def broken() -> str:
attempts["count"] += 1
raise ValueError("boom")
try:
broken()
except ValueError as exc:
assert str(exc) == "boom"
else:
raise AssertionError("expected ValueError")
assert attempts["count"] == 1

View File

@@ -0,0 +1,32 @@
from __future__ import annotations
import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from app.core import files
class RuntimeRootTests(unittest.TestCase):
def test_runtime_root_prefers_shared_runtime_outside_release_tree(self) -> None:
fake_file = "/opt/domaincheck/releases/domaincheck_release_20260420_222047/domain-api/app/core/files.py"
with patch.object(files, "__file__", fake_file):
with patch.object(Path, "mkdir", autospec=True, return_value=None) as mocked_mkdir:
runtime_root = files.runtime_root()
self.assertEqual(Path("/opt/domaincheck/runtime/domain-api"), runtime_root)
mocked_mkdir.assert_called_once()
def test_runtime_root_respects_env_override(self) -> None:
with tempfile.TemporaryDirectory() as tempdir:
override = Path(tempdir) / "custom-runtime"
with patch.dict(os.environ, {"DOMAIN_API_RUNTIME_ROOT": str(override)}, clear=False):
runtime_root = files.runtime_root()
self.assertEqual(override, runtime_root)
self.assertTrue(runtime_root.exists())
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,237 @@
import unittest
from unittest.mock import patch
from app.services.dashboard import fetch_overview
class _FakeCursor:
def __init__(self, responses):
self._responses = list(responses)
def execute(self, sql, params=None):
self._last_sql = sql
self._last_params = params
def fetchone(self):
if self._responses:
return self._responses.pop(0)
return (0,)
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
class _FakeConnection:
def __init__(self, responses):
self._cursor = _FakeCursor(responses)
def cursor(self):
return self._cursor
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
class DashboardServiceTests(unittest.TestCase):
@patch("app.services.dashboard._fetch_active_jobs_aggregate")
@patch("app.services.dashboard.get_detect_capacity_plan")
@patch("app.services.dashboard.get_detect_queue_health")
@patch("app.services.dashboard.get_runtime_status")
@patch("app.services.dashboard.get_active_detect_job_summary")
@patch("app.services.dashboard.get_db")
def test_fetch_overview_includes_ops_metrics(
self,
mock_get_db,
mock_get_active_detect_job_summary,
mock_get_runtime_status,
mock_get_detect_queue_health,
mock_get_detect_capacity_plan,
mock_fetch_active_jobs_aggregate,
) -> None:
mock_get_db.return_value = _FakeConnection(
responses=[
(1000,),
(900,),
(10,),
(5,),
(0,),
(1,),
(430,),
(420,),
(17,),
]
)
mock_fetch_active_jobs_aggregate.return_value = {
"active_jobs_total": 3,
"queue": {
"items_total": 5200,
"pending": 4700,
"claimed": 11,
"running": 165,
"completed": 300,
"blacklisted": 12,
"failed": 7,
},
"throughput": {
"processed_recent": 30,
"processed_per_minute": 2.0,
"completed_recent": 20,
"blacklisted_recent": 1,
"failed_recent": 2,
},
"retry_total": 12,
"steps": [
{
"step_code": "detect_register",
"step_name": "注册状态检测",
"items_pending": 185,
"items_running": 3,
"items_claimed": 0,
"items_completed": 7,
"items_blacklisted": 0,
"items_failed": 0,
"processed_recent": 20,
"processed_per_minute": 1.33,
}
],
"nodes": [
{
"node_code": "mainland-controller-01",
"items_running": 120,
"items_claimed": 0,
"processed_recent": 20,
"processed_per_minute": 1.33,
"completed_recent": 15,
"failed_recent": 1,
"blacklisted_recent": 0,
}
],
}
mock_get_active_detect_job_summary.return_value = {
"job_id": 76,
"job_code": "sync-overseas-3612",
"items_pending": 185,
"items_running": 8,
"items_completed": 7,
"items_blacklisted": 0,
"items_failed": 0,
}
mock_get_runtime_status.return_value = {
"worker": {"running": True, "mode": "linux-systemd", "expected_on_this_node": True},
"node": {"region": "overseas", "role": "control"},
"cluster": {"summary": {"online_worker_nodes": 2, "dedicated_online_worker_nodes": 1, "online_control_nodes": 1}},
"detect": {
"backlog": {
"pending_total": 9438,
"claimed_total": 410,
"running_total": 15,
"completed_total": 951,
"blacklisted_total": 0,
"failed_total": 137,
"register_pending": 8487,
"downstream_pending": 951,
}
},
}
mock_get_detect_queue_health.return_value = {
"has_active_job": True,
"job": {"job_id": 76, "job_code": "sync-overseas-3612", "status": "running", "progress_percent": 3.5},
"queue": {
"items_total": 200,
"pending": 185,
"claimed": 0,
"display_claimed": 0,
"running": 8,
"display_running": 165,
"completed": 7,
"blacklisted": 0,
"failed": 0,
},
"throughput": {"processed_recent": 0, "processed_per_minute": 0},
"steps": [
{
"step_code": "detect_register",
"step_name": "注册状态检测",
"items_pending": 185,
"items_running": 165,
"items_claimed": 0,
"items_completed": 7,
"items_blacklisted": 0,
"items_failed": 0,
"processed_recent": 0,
"processed_per_minute": 0.0,
}
],
"runtime_activity": {
"step_stats": {
"detect_360_site": {
"step_code": "detect_360_site",
"started_recent": 269,
"processed_recent": 6,
"completed_recent": 6,
"failed_recent": 0,
"blacklisted_recent": 0,
}
}
},
"nodes": [
{
"node_code": "mainland-controller-01",
"items_running": 165,
"items_claimed": 0,
"processed_recent": 0,
"processed_per_minute": 0.0,
"completed_recent": 0,
"failed_recent": 0,
"blacklisted_recent": 0,
}
],
}
mock_get_detect_capacity_plan.return_value = {
"estimated_hours_remaining": 1.5,
"remaining_items": 193,
"recommended_additional_workers": 1,
}
data = fetch_overview()
self.assertEqual("sync-overseas-3612", data["active_job"]["job_code"])
self.assertEqual(3, data["active_jobs_aggregate"]["active_jobs_total"])
self.assertEqual(9438, data["active_jobs_aggregate"]["queue"]["pending"])
self.assertEqual(410, data["active_jobs_aggregate"]["queue"]["claimed"])
self.assertEqual(15, data["active_jobs_aggregate"]["queue"]["running"])
self.assertEqual(951, data["active_jobs_aggregate"]["queue"]["completed"])
self.assertEqual(137, data["active_jobs_aggregate"]["queue"]["failed"])
self.assertEqual("mainland-controller-01", data["active_jobs_aggregate"]["nodes"][0]["node_code"])
self.assertEqual(900, data["pending_total"])
self.assertEqual(10, data["completed_total"])
self.assertEqual(430, data["registerable_total"])
self.assertEqual(420, data["purchasable_total"])
self.assertEqual(185, data["queue_pending_total"])
self.assertEqual(8, data["queue_running_total"])
self.assertEqual(165, data["queue_display_running_total"])
self.assertEqual(7, data["queue_completed_total"])
self.assertEqual(9438, data["backlog_pending_total"])
self.assertEqual(8487, data["backlog_register_pending_total"])
self.assertEqual(951, data["backlog_downstream_pending_total"])
self.assertEqual(12, data["retry_total"])
self.assertEqual("注册状态检测", data["bottleneck_step"]["step_name"])
self.assertEqual(0.0, data["processed_per_minute"])
self.assertEqual(0, data["processed_recent"])
self.assertEqual(0.0, data["ops_summary"]["processed_per_minute"])
self.assertEqual(1.5, data["ops_summary"]["estimated_hours_remaining"])
self.assertEqual(1, data["active_execution_nodes"])
self.assertEqual(1, data["ops_summary"]["active_execution_nodes"])
self.assertEqual(2, len(data["step_queue"]))
self.assertTrue(any(item["step_code"] == "detect_360_site" for item in data["step_queue"]))
self.assertEqual(1, len(data["node_throughput"]))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,75 @@
from __future__ import annotations
import unittest
from unittest.mock import patch
from app.api.routes import detect as detect_route
class DetectApiRoutesTestCase(unittest.TestCase):
@patch("app.api.routes.detect.create_detect_run_snapshot")
@patch("app.api.routes.detect._dispatch_remote_detect_start")
@patch("app.api.routes.detect.get_settings_payload")
@patch("app.api.routes.detect.get_detect_status")
@patch("app.api.routes.detect.send_worker_command")
@patch("app.api.routes.detect.start_worker")
@patch("app.api.routes.detect.append_detect_job_event")
@patch("app.api.routes.detect.create_detect_job_if_needed")
def test_start_detect_skips_local_worker_on_overseas_control(
self,
mock_create_job,
mock_append_event,
mock_start_worker,
mock_send_worker_command,
mock_get_detect_status,
mock_get_settings_payload,
mock_dispatch_remote,
mock_create_snapshot,
) -> None:
mock_create_job.return_value = {
"job_id": 29,
"job_code": "sync-overseas-29",
"status": "running",
"items_pending": 100,
"items_claimed": 0,
"items_running": 0,
"task_mode": "single_step",
"step_code": "detect_register",
}
mock_get_detect_status.return_value = {
"worker_mode": "linux-systemd",
"worker_online": False,
"worker_process_count": 0,
"worker_latest_start_time": "",
"worker_runtime_message": "not-applicable",
"progress": {},
}
mock_get_settings_payload.return_value = {
"runtime": {"thread_count": 2000},
"proxy_config": {"proxy_enable": True, "allow_direct": False, "proxy_urls": ["a"]},
}
mock_dispatch_remote.return_value = {
"queued_jobs": [{"node_code": "mainland-controller-01"}],
"queued_total": 1,
"failed_total": 0,
"target_summary": {"controller_nodes": ["mainland-controller-01"], "worker_nodes": ["mainland-worker-01"]},
}
with patch.object(detect_route.settings, "node_region", "overseas"), patch.object(
detect_route.settings, "node_role", "control"
):
response = detect_route.start_detect()
self.assertEqual(0, response.code)
self.assertIn("海外控制面", response.message)
mock_start_worker.assert_not_called()
mock_send_worker_command.assert_not_called()
mock_dispatch_remote.assert_called_once()
mock_create_snapshot.assert_called_once()
event_types = [call.kwargs.get("event_type") for call in mock_append_event.call_args_list]
self.assertIn("job_dispatch_requested", event_types)
self.assertIn("job_dispatch_skipped_local", event_types)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,121 +1,791 @@
import unittest
from unittest.mock import MagicMock, patch
from app.services.detect_job_service import _build_effective_node_stats, _build_effective_summary
from psycopg2 import errors
from app.services.detect_job_service import (
append_detect_job_event,
_build_step_payload,
_classify_pipeline_item_outcome,
_classify_runtime_debug_event,
_enrich_active_job_summary_with_runtime,
_build_display_summary,
_build_runtime_display_bucket,
normalize_detect_step_code,
get_detect_queue_health,
process_detect_pipeline_now,
resolve_initial_domain_pipeline_item,
resolve_domain_pipeline_step,
resolve_detect_job_definition,
)
class DetectJobServiceTests(unittest.TestCase):
def test_build_effective_node_stats_keeps_runtime_nodes_and_backfills_unassigned(self) -> None:
distributed_node_stats = [
{
"node_code": "mainland-controller-01",
"items_total": 200,
"items_pending": 195,
"items_claimed": 0,
"items_running": 5,
"items_completed": 0,
"items_blacklisted": 0,
"items_failed": 0,
},
{
"node_code": "mainland-worker-01",
"items_total": 70,
"items_pending": 56,
"items_claimed": 9,
"items_running": 5,
"items_completed": 0,
"items_blacklisted": 0,
"items_failed": 0,
},
{
"node_code": "overseas-control-01",
"items_total": 50,
"items_pending": 0,
"items_claimed": 25,
"items_running": 4,
"items_completed": 21,
"items_blacklisted": 0,
"items_failed": 0,
},
{
"node_code": "unassigned",
"items_total": 950,
"items_pending": 950,
"items_claimed": 0,
"items_running": 0,
"items_completed": 0,
"items_blacklisted": 0,
"items_failed": 0,
},
]
def test_append_detect_job_event_skips_missing_job_fk(self) -> None:
class FakeCursor:
def __init__(self) -> None:
self.exec_calls = []
self.selects = 0
node_stats = _build_effective_node_stats(
distributed_node_stats=distributed_node_stats,
raw_items_total=1000,
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def execute(self, sql, params=None):
self.exec_calls.append((sql, params))
if "INSERT INTO detect_run_events" in sql:
raise AssertionError("should not insert detect_run_events when job row is missing")
if "SELECT 1 FROM detect_jobs" in sql:
self.selects += 1
def fetchone(self):
return None
class FakeConn:
def __init__(self) -> None:
self.cursor_obj = FakeCursor()
self.commit_calls = 0
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def cursor(self):
return self.cursor_obj
def commit(self):
self.commit_calls += 1
fake_conn = FakeConn()
with patch("app.services.detect_job_service.get_db", return_value=fake_conn):
with patch("app.services.detect_job_service.push_debug_event") as mock_push:
append_detect_job_event(
5,
event_type="job_dispatch_requested",
message="控制面已发送检测启动请求",
)
self.assertEqual(1, fake_conn.cursor_obj.selects)
self.assertEqual(0, fake_conn.commit_calls)
mock_push.assert_called_once()
def test_process_detect_pipeline_now_retries_deadlock_once(self) -> None:
calls = {"count": 0}
def _run_pipeline(*, limit, job_id):
calls["count"] += 1
if calls["count"] == 1:
raise errors.DeadlockDetected()
return {
"processed_items": 7,
"advanced_items": 3,
"retried_items": 1,
}
with patch("app.services.detect_job_service.process_detect_pipeline", side_effect=_run_pipeline):
with patch("app.services.detect_job_service.time.sleep") as mock_sleep:
ok, message, data = process_detect_pipeline_now(limit=123, job_id=45)
self.assertTrue(ok)
self.assertIn("deadlock 自动重试 1 次后成功", message)
self.assertEqual(1, data["retry_attempts"])
self.assertEqual(7, data["processed_items"])
self.assertEqual(2, calls["count"])
mock_sleep.assert_called_once()
def test_classify_runtime_debug_event_maps_single_step_finalized_completed(self) -> None:
classified = _classify_runtime_debug_event(
event_type="worker_log",
message="检测步骤跟踪: domain=example.com | step=注册状态检测 | stage=single_step_finalized | elapsed_ms=2700 | ok=1 | detect_key=detect_register | final_status=completed | result_state=passed",
payload={"job_code": "sync-overseas-27456", "node_code": "mainland-worker-01"},
)
self.assertEqual(4, len(node_stats))
unassigned = next(item for item in node_stats if item["node_code"] == "unassigned")
self.assertEqual(680, unassigned["items_total"])
self.assertEqual(680, unassigned["items_pending"])
self.assertEqual("central_queue", unassigned["metrics_source"])
self.assertEqual("sync-overseas-27456", classified["job_code"])
self.assertTrue(classified["terminal"])
self.assertEqual("completed", classified["terminal_status"])
self.assertEqual("detect_register", classified["step_code"])
def test_build_effective_summary_uses_effective_node_stats_and_raw_blacklisted(self) -> None:
node_stats = [
{
"node_code": "mainland-controller-01",
"items_total": 200,
"items_pending": 195,
"items_claimed": 0,
"items_running": 5,
"items_completed": 0,
"items_blacklisted": 0,
"items_failed": 0,
},
{
"node_code": "mainland-worker-01",
"items_total": 70,
"items_pending": 56,
"items_claimed": 9,
"items_running": 5,
"items_completed": 0,
"items_blacklisted": 0,
"items_failed": 0,
},
{
"node_code": "overseas-control-01",
"items_total": 50,
"items_pending": 0,
"items_claimed": 25,
"items_running": 4,
"items_completed": 21,
"items_blacklisted": 0,
"items_failed": 0,
},
{
"node_code": "unassigned",
"items_total": 680,
"items_pending": 680,
"items_claimed": 0,
"items_running": 0,
"items_completed": 0,
"items_blacklisted": 0,
"items_failed": 0,
},
]
summary = _build_effective_summary(
node_stats=node_stats,
raw_items_total=1000,
raw_items_blacklisted=3,
def test_build_runtime_display_bucket_prefers_real_active_threads_over_raw_current_load(self) -> None:
bucket = _build_runtime_display_bucket(
(
"mainland-worker-01",
"mainland",
"worker",
"busy",
753,
{
"job_items_total": 1000,
"job_items_claimed": 0,
"job_items_running": 0,
"job_items_completed": 342,
"active_threads": 61,
"max_threads": 400,
"detect_participating": True,
},
None,
)
)
self.assertIsNotNone(bucket)
self.assertEqual(61, bucket["current_load"])
self.assertEqual(61, bucket["display_running"])
self.assertEqual(61, bucket["active_threads"])
def test_build_runtime_display_bucket_skips_local_overseas_control_plane_load(self) -> None:
with patch("app.services.detect_job_service.settings.node_region", "overseas"):
with patch("app.services.detect_job_service.settings.node_role", "control"):
with patch("app.services.detect_job_service.settings.node_code", "overseas-control-01"):
bucket = _build_runtime_display_bucket(
(
"overseas-control-01",
"overseas",
"control",
"busy",
371,
{
"active_threads": 371,
"max_threads": 3200,
"detect_participating": False,
},
None,
)
)
self.assertIsNone(bucket)
def test_build_runtime_display_bucket_skips_any_control_node_without_worker_support(self) -> None:
bucket = _build_runtime_display_bucket(
(
"overseas-control-01",
"overseas",
"control",
"busy",
378,
{
"active_threads": 378,
"max_threads": 3200,
"detect_participating": False,
"worker_online": False,
},
None,
)
)
self.assertIsNone(bucket)
def test_build_display_summary_ignores_inflated_raw_current_load(self) -> None:
summary = _build_display_summary(
[
{
"node_code": "mainland-controller-01",
"items_claimed": 0,
"items_running": 0,
"display_running": 729,
"current_load": 729,
"active_threads": 729,
"max_threads": 800,
"items_completed": 196,
"items_failed": 0,
},
{
"node_code": "mainland-worker-01",
"items_claimed": 0,
"items_running": 0,
"display_running": 753,
"current_load": 753,
"active_threads": 61,
"max_threads": 400,
"items_completed": 146,
"items_failed": 0,
},
]
)
self.assertEqual(790, summary["display_running"])
self.assertEqual(790, summary["current_load"])
self.assertEqual(790, summary["active_threads"])
def test_enrich_active_job_summary_with_runtime_adds_runtime_job_code_and_recent_events(self) -> None:
summary = _enrich_active_job_summary_with_runtime(
{
"job_id": 275,
"job_code": "sync-overseas-5297",
"processed_recent": 0,
"processed_per_minute": 0,
"completed_recent": 0,
"failed_recent": 0,
"blacklisted_recent": 0,
"recent_domain_events": [],
},
event_limit=20,
window_minutes=15,
runtime_activity={
"focus_job_code": "sync-overseas-9506",
"job_codes": ["sync-overseas-9506", "sync-overseas-27456"],
"processed_recent": 120,
"completed_recent": 118,
"failed_recent": 1,
"blacklisted_recent": 1,
},
runtime_snapshot={
"job": {
"job_id": 1835,
"job_code": "sync-overseas-9506",
"progress_percent": 44.2,
},
"queue_health": {
"queue": {
"items_total": 1000,
"pending": 176,
"claimed": 223,
"running": 159,
"completed": 442,
"display_running": 220,
}
},
},
recent_domain_events=[
{
"event_type": "domain_completed",
"message": "域名检测完成: example.com",
"created_at": "2026-04-21 04:10:00",
}
],
)
self.assertEqual("sync-overseas-9506", summary["runtime_job_code"])
self.assertEqual(["sync-overseas-9506", "sync-overseas-27456"], summary["runtime_job_codes"])
self.assertEqual(120, summary["processed_recent"])
self.assertEqual(8.0, summary["processed_per_minute"])
self.assertEqual(118, summary["completed_recent"])
self.assertEqual(1, summary["failed_recent"])
self.assertEqual(1, summary["blacklisted_recent"])
self.assertEqual(1, len(summary["recent_domain_events"]))
self.assertEqual("sync-overseas-9506", summary["runtime_snapshot_job_code"])
self.assertEqual(1000, summary["runtime_snapshot_queue"]["items_total"])
self.assertEqual(1000, summary["items_total"])
self.assertEqual(931, summary["items_pending"])
self.assertEqual(34, summary["items_claimed"])
self.assertEqual(14, summary["items_running"])
self.assertEqual(21, summary["items_completed"])
self.assertEqual(3, summary["items_blacklisted"])
self.assertEqual(24, summary["items_terminal"])
self.assertEqual(176, summary["items_pending"])
self.assertEqual(223, summary["items_claimed"])
self.assertEqual(159, summary["items_running"])
self.assertEqual(442, summary["items_completed"])
self.assertEqual(220, summary["display_items_running"])
def test_enrich_active_job_summary_with_runtime_prefers_snapshot_identity_and_events(self) -> None:
summary = _enrich_active_job_summary_with_runtime(
{
"job_id": 275,
"job_code": "sync-overseas-5297",
"status": "running",
"recent_events": [
{
"node_code": "mainland-worker-01",
"event_type": "domain_started",
"message": "开始检测域名: stale.com",
"payload": {"job_code": "sync-overseas-5297"},
"created_at": "2026-04-21 11:58:00",
}
],
"current_cycle_events": [],
"latest_event": None,
"display_items_running": 0,
"display_current_load": 0,
"display_active_threads": 0,
"display_max_threads": 0,
"display_active_node_codes": [],
},
event_limit=10,
window_minutes=15,
runtime_activity={
"focus_job_code": "sync-overseas-31437",
"job_codes": ["sync-overseas-31437"],
"processed_recent": 10,
"completed_recent": 10,
"failed_recent": 0,
"blacklisted_recent": 0,
},
runtime_snapshot={
"job": {
"job_id": 1902,
"job_code": "sync-overseas-31437",
"status": "running",
"progress_percent": 34.3,
"node_stats": [
{
"node_code": "mainland-controller-01",
"items_claimed": 230,
"items_running": 67,
"items_completed": 343,
"items_failed": 0,
"active_threads": 170,
"max_threads": 2000,
},
{
"node_code": "mainland-worker-01",
"items_claimed": 75,
"items_running": 0,
"items_completed": 0,
"items_failed": 0,
"active_threads": 19,
"max_threads": 1200,
},
],
},
"queue_health": {
"queue": {
"items_total": 1000,
"pending": 285,
"claimed": 305,
"running": 67,
"completed": 343,
"display_running": 189,
}
},
"recent_events": [
{
"job_id": 2137,
"node_code": "mainland-controller-01",
"event_type": "job_created",
"message": "同步拉取待检测批次 sync-overseas-35461共 1000 个任务项",
"payload": {"source_record_id": 35461},
"created_at": "2026-04-22 01:21:29",
},
{
"job_id": 1902,
"node_code": "mainland-controller-01",
"event_type": "worker_log",
"message": "从任务队列获取到 125 个需要检测的域名",
"payload": {"job_code": "sync-overseas-31437"},
"created_at": "2026-04-22 01:19:25",
},
],
},
)
self.assertEqual(1902, summary["job_id"])
self.assertEqual("sync-overseas-31437", summary["job_code"])
self.assertEqual("sync-overseas-31437", summary["runtime_job_code"])
self.assertEqual(189, summary["display_items_running"])
self.assertEqual(189, summary["display_current_load"])
self.assertEqual(189, summary["display_active_threads"])
self.assertEqual(3200, summary["display_max_threads"])
self.assertEqual(["mainland-controller-01", "mainland-worker-01"], summary["display_active_node_codes"])
self.assertEqual(1, len(summary["recent_events"]))
self.assertEqual("worker_log", summary["latest_event"]["event_type"])
self.assertEqual("sync-overseas-31437", summary["recent_events"][0]["payload"]["job_code"])
def test_get_detect_queue_health_preserves_runtime_node_throughput_after_snapshot_override(self) -> None:
lease_row = (None, None, 0, 0)
throughput_rows = []
step_throughput_rows = []
runtime_display_rows = [
(
"mainland-controller-01",
"mainland",
"control",
"busy",
170,
{
"job_items_total": 265,
"job_items_claimed": 85,
"job_items_running": 0,
"job_items_completed": 105,
"active_threads": 170,
"max_threads": 2000,
"detect_participating": True,
},
None,
),
(
"mainland-worker-01",
"mainland",
"worker",
"busy",
19,
{
"job_items_total": 139,
"job_items_claimed": 64,
"job_items_running": 0,
"job_items_completed": 4,
"active_threads": 19,
"max_threads": 1200,
"detect_participating": True,
},
None,
),
(
"overseas-control-01",
"overseas",
"control",
"busy",
371,
{
"active_threads": 371,
"max_threads": 3200,
"detect_participating": False,
},
None,
),
]
conn = MagicMock()
cursor_cm = MagicMock()
cursor = MagicMock()
conn.cursor.return_value = cursor_cm
cursor_cm.__enter__.return_value = cursor
db_cm = MagicMock()
db_cm.__enter__.return_value = conn
cursor.fetchone.return_value = lease_row
cursor.fetchall.side_effect = [throughput_rows, step_throughput_rows, runtime_display_rows]
active_job = {
"job_id": 1937,
"job_code": "sync-overseas-31987",
"status": "running",
"items_total": 1000,
"items_pending": 596,
"items_claimed": 149,
"items_running": 146,
"items_completed": 109,
"items_blacklisted": 0,
"items_failed": 0,
"progress_percent": 10.9,
"node_stats": [],
"distributed_node_stats": [
{
"node_code": "mainland-controller-01",
"items_total": 265,
"items_pending": 0,
"items_claimed": 85,
"items_running": 75,
"items_completed": 105,
"items_blacklisted": 0,
"items_failed": 0,
"metrics_source": "runtime",
},
{
"node_code": "mainland-worker-01",
"items_total": 139,
"items_pending": 0,
"items_claimed": 64,
"items_running": 71,
"items_completed": 4,
"items_blacklisted": 0,
"items_failed": 0,
"metrics_source": "runtime",
},
],
"step_stats": [
{
"step_code": "detect_register",
"items_total": 1000,
"items_pending": 449,
"items_claimed": 131,
"items_running": 273,
"items_completed": 147,
"items_blacklisted": 0,
"items_failed": 0,
}
],
}
runtime_activity = {
"processed_recent": 133,
"completed_recent": 113,
"failed_recent": 20,
"blacklisted_recent": 0,
"focus_job_code": "sync-overseas-31563",
"job_codes": ["sync-overseas-31563"],
"step_code": "detect_register",
"nodes": {
"mainland-controller-01": {
"node_code": "mainland-controller-01",
"processed_recent": 74,
"completed_recent": 54,
"failed_recent": 20,
"blacklisted_recent": 0,
},
"mainland-worker-01": {
"node_code": "mainland-worker-01",
"processed_recent": 59,
"completed_recent": 59,
"failed_recent": 0,
"blacklisted_recent": 0,
},
},
}
runtime_snapshot = {
"job": {
"job_id": 1937,
"job_code": "sync-overseas-31987",
"progress_percent": 10.9,
},
"queue_health": {
"queue": {
"items_total": 1000,
"pending": 596,
"claimed": 149,
"running": 146,
"completed": 109,
"blacklisted": 0,
"failed": 0,
"display_claimed": 149,
"display_running": 403,
},
"nodes": [
{
"node_code": "mainland-controller-01",
"items_total": 265,
"items_pending": 0,
"items_claimed": 85,
"items_running": 75,
"items_completed": 105,
"items_blacklisted": 0,
"items_failed": 0,
"processed_recent": 0,
"processed_per_minute": 0,
"completed_recent": 0,
"blacklisted_recent": 0,
"failed_recent": 0,
"metrics_source": "runtime",
},
{
"node_code": "mainland-worker-01",
"items_total": 139,
"items_pending": 0,
"items_claimed": 64,
"items_running": 71,
"items_completed": 4,
"items_blacklisted": 0,
"items_failed": 0,
"processed_recent": 0,
"processed_per_minute": 0,
"completed_recent": 0,
"blacklisted_recent": 0,
"failed_recent": 0,
"metrics_source": "runtime",
},
],
"steps": [],
},
}
with patch("app.services.detect_job_service.settings.node_region", "overseas"):
with patch("app.services.detect_job_service.settings.node_role", "control"):
with patch("app.services.detect_job_service.settings.node_code", "overseas-control-01"):
with patch("app.services.detect_job_service.get_active_detect_job_summary", return_value=active_job):
with patch("app.services.detect_job_service._load_runtime_activity_snapshot", return_value=runtime_activity):
with patch("app.services.detect_job_service._load_latest_runtime_active_job_snapshot", return_value=runtime_snapshot):
with patch("app.services.detect_job_service.get_db", return_value=db_cm):
health = get_detect_queue_health(window_minutes=15)
node_map = {item["node_code"]: item for item in health["nodes"]}
self.assertEqual(74, node_map["mainland-controller-01"]["processed_recent"])
self.assertEqual(54, node_map["mainland-controller-01"]["completed_recent"])
self.assertEqual(20, node_map["mainland-controller-01"]["failed_recent"])
self.assertEqual(170, node_map["mainland-controller-01"]["items_running"])
self.assertEqual(59, node_map["mainland-worker-01"]["processed_recent"])
self.assertEqual(59, node_map["mainland-worker-01"]["completed_recent"])
self.assertEqual(19, node_map["mainland-worker-01"]["items_running"])
self.assertEqual(113, health["throughput"]["completed_recent"])
self.assertEqual(189, health["queue"]["display_running"])
self.assertNotIn("overseas-control-01", node_map)
def test_normalize_detect_step_code_accepts_supported_single_step(self) -> None:
self.assertEqual("detect_baidu_site", normalize_detect_step_code("detect_baidu_site"))
self.assertEqual("detect_wayback", normalize_detect_step_code("detect_wayback"))
def test_normalize_detect_step_code_rejects_unknown_step(self) -> None:
self.assertEqual("", normalize_detect_step_code("detect_unknown"))
def test_resolve_detect_job_definition_builds_single_step_job(self) -> None:
definition = resolve_detect_job_definition("detect_baidu_site")
self.assertTrue(definition["is_single_step"])
self.assertEqual("single_step", definition["task_mode"])
self.assertEqual("detect_baidu_site", definition["step_code"])
def test_resolve_detect_job_definition_builds_wayback_single_step_job(self) -> None:
definition = resolve_detect_job_definition("detect_wayback")
self.assertTrue(definition["is_single_step"])
self.assertEqual("single_step", definition["task_mode"])
self.assertEqual("detect_wayback", definition["step_code"])
def test_build_step_payload_adds_wayback_recent_years_strategy(self) -> None:
payload = _build_step_payload(
step_code="detect_wayback",
domain_snapshot={"domain": "example.com", "source_type": 2},
settings_payload={"detect_options": {"detect_wayback": True}},
)
self.assertEqual("detect_wayback", payload["step_code"])
self.assertEqual("recent_years", payload["wayback_strategy"])
self.assertEqual(5, payload["wayback_recent_years"])
self.assertTrue(payload["wayback_stop_on_first_hit"])
def test_resolve_detect_job_definition_defaults_to_domain_pipeline(self) -> None:
definition = resolve_detect_job_definition(None)
self.assertFalse(definition["is_single_step"])
self.assertEqual("domain_pipeline", definition["task_mode"])
self.assertEqual("", definition["step_code"])
def test_resolve_domain_pipeline_step_skips_yikoujia_register(self) -> None:
step_code = resolve_domain_pipeline_step(
{
"source_type": 1,
"register_status": 0,
"baidu_site": {},
"qihu360_site": {},
"chinaz_info": {},
"aizhan_info": {},
"wayback_info": {},
"jucha_info": {},
"juziseo_info": {},
},
settings_payload={
"detect_options": {
"detect_register": True,
"detect_baidu_site": True,
"detect_360_site": False,
"detect_chinaz": False,
"detect_aizhan": False,
"detect_wayback": False,
"detect_jucha": False,
"detect_juziseo": False,
"detect_order": ["detect_register", "detect_baidu_site"],
}
},
)
self.assertEqual("detect_baidu_site", step_code)
def test_resolve_domain_pipeline_step_moves_to_next_incomplete_step(self) -> None:
step_code = resolve_domain_pipeline_step(
{
"source_type": 2,
"register_status": 3,
"baidu_site": {"state": "passed"},
"qihu360_site": {},
"chinaz_info": {},
"aizhan_info": {},
"wayback_info": {},
"jucha_info": {},
"juziseo_info": {},
},
settings_payload={
"detect_options": {
"detect_register": True,
"detect_baidu_site": True,
"detect_360_site": True,
"detect_chinaz": False,
"detect_aizhan": False,
"detect_wayback": False,
"detect_jucha": False,
"detect_juziseo": False,
"detect_order": ["detect_register", "detect_baidu_site", "detect_360_site"],
}
},
after_step_code="detect_baidu_site",
)
self.assertEqual("detect_360_site", step_code)
def test_resolve_initial_domain_pipeline_item_builds_first_step_payload(self) -> None:
step_code, payload = resolve_initial_domain_pipeline_item(
{
"id": 10,
"domain": "example.com",
"source_type": 2,
"register_status": 0,
"baidu_site": {},
"qihu360_site": {},
"chinaz_info": {},
"aizhan_info": {},
"wayback_info": {},
"jucha_info": {},
"juziseo_info": {},
},
settings_payload={
"detect_options": {
"detect_register": True,
"detect_baidu_site": True,
"detect_order": ["detect_register", "detect_baidu_site"],
}
},
)
self.assertEqual("detect_register", step_code)
self.assertIsNotNone(payload)
self.assertEqual("detect_register", payload["step_code"])
self.assertEqual("example.com", payload["domain"])
def test_resolve_initial_domain_pipeline_item_returns_empty_when_pipeline_already_done(self) -> None:
step_code, payload = resolve_initial_domain_pipeline_item(
{
"id": 11,
"domain": "done.com",
"source_type": 2,
"register_status": 3,
"baidu_site": {"state": "passed"},
"qihu360_site": {},
"chinaz_info": {},
"aizhan_info": {},
"wayback_info": {},
"jucha_info": {},
"juziseo_info": {},
},
settings_payload={
"detect_options": {
"detect_register": True,
"detect_baidu_site": True,
"detect_order": ["detect_register", "detect_baidu_site"],
}
},
)
self.assertEqual("", step_code)
self.assertIsNone(payload)
def test_classify_pipeline_item_outcome_retries_external_failure(self) -> None:
outcome = _classify_pipeline_item_outcome(
item_status="failed",
result_payload={"state": "degraded", "message": "timeout", "retry_recommended": True},
step_code="detect_baidu_site",
attempt_count=0,
)
self.assertEqual("retry", outcome["action"])
self.assertTrue(outcome["should_retry"])
self.assertEqual("external_retry", outcome["reason_code"])
def test_classify_pipeline_item_outcome_rejects_business_failure(self) -> None:
outcome = _classify_pipeline_item_outcome(
item_status="failed",
result_payload={"state": "rejected", "message": "title contains forbidden keyword"},
step_code="detect_chinaz",
attempt_count=0,
)
self.assertEqual("reject", outcome["action"])
self.assertFalse(outcome["should_retry"])
self.assertEqual("business_reject", outcome["reason_code"])
def test_classify_pipeline_item_outcome_marks_blacklisted_terminal(self) -> None:
outcome = _classify_pipeline_item_outcome(
item_status="blacklisted",
result_payload={"state": "blacklisted", "message": "risk hit"},
step_code="detect_baidu_site",
attempt_count=0,
)
self.assertEqual("black_hit", outcome["action"])
self.assertFalse(outcome["should_retry"])
self.assertEqual("blacklisted", outcome["reason_code"])
if __name__ == "__main__":

View File

@@ -0,0 +1,69 @@
import unittest
from unittest.mock import patch
from app.services import detect_service
class DetectServiceStatusFallbackTests(unittest.TestCase):
def test_get_detect_status_keeps_runtime_snapshot_when_db_is_unreachable(self) -> None:
runtime_state = {
"service_running": True,
"detecting": True,
"active_threads": 7,
"max_threads": 120,
"phase": "running",
"detail": "Worker 正在处理 7 个检测任务",
"updated_at": "2026-04-20 23:59:00",
"available_proxy_count": 18,
}
with patch("app.services.detect_service.ensure_runtime_schema"), \
patch("app.services.detect_service.get_db", side_effect=RuntimeError("db down")), \
patch("app.services.detect_service.get_settings_payload", return_value={"proxy_config": {"proxy_enable": True, "allow_direct": False, "proxy_urls": ["a"]}}), \
patch("app.services.detect_service.get_runtime_settings", return_value={"worker_log_sync_enabled": False, "worker_log_sync_mode": "full"}), \
patch("app.services.detect_service._load_recent_worker_lines", return_value=(True, "", [])), \
patch("app.services.detect_service.detect_worker_runtime", return_value={"mode": "linux-systemd", "running": True, "process_count": 1, "latest_start_time": "2026-04-20 23:58:00", "message": "active/running"}), \
patch("app.services.detect_service._load_runtime_state", return_value=runtime_state), \
patch("app.services.detect_service._load_runtime_state_from_cluster_node", return_value={}), \
patch("app.services.detect_service._extract_available_proxy_count", return_value=0), \
patch("app.services.detect_service._extract_active_thread_snapshot", return_value={"active": 0, "max": 0}), \
patch("app.services.detect_service._normalize_recent_warning", return_value=""), \
patch("app.services.detect_service._build_proxy_runtime_snapshot", return_value={"state": "healthy", "label": "代理正常", "detail": "healthy", "direct_fallback_active": False, "reason": "healthy", "last_refresh_status": "ok", "last_refresh_time": "", "source_count": 2, "raw_items": 18, "validated_count": 18, "available_count": 18, "source_stats": [], "supplier_empty": False}), \
patch("app.services.detect_service.resolve_thread_count", return_value={"effective_thread_count": 120, "default_thread_count": 5, "source": "node_override", "override_thread_count": 120, "node_code": "mainland-worker-01"}), \
patch("app.services.detect_service.get_active_detect_job_summary", side_effect=RuntimeError("db down")), \
patch("app.services.detect_service.sync_detect_runs", return_value=[]), \
patch("app.services.detect_service._resolve_remote_log_snapshot", return_value={}), \
patch("app.services.detect_service._extract_dependency_alerts", return_value=[]), \
patch("app.services.detect_service.append_detect_result_projection_if_changed"):
payload = detect_service.get_detect_status()
self.assertTrue(payload["worker_online"])
self.assertTrue(payload["detecting"])
self.assertEqual(7, payload["active_thread_count"])
self.assertEqual(120, payload["max_thread_count"])
self.assertEqual(0, payload["progress"]["pending"])
self.assertEqual(0, payload["progress"]["completed"])
def test_filter_lines_since_supports_journalctl_syslog_timestamps(self) -> None:
lines = [
"Apr 21 20:12:42 mainland-controller python[1]: 当前实际线程数量: 323/4",
"Apr 21 20:17:20 mainland-controller python[2]: Worker 已启动,等待检测指令",
]
filtered = detect_service._filter_lines_since(lines, "2026-04-21 20:17:00")
self.assertEqual(
["Apr 21 20:17:20 mainland-controller python[2]: Worker 已启动,等待检测指令"],
filtered,
)
def test_filter_lines_since_falls_back_when_no_timestamp_is_parseable(self) -> None:
lines = ["no timestamp line 1", "no timestamp line 2"]
filtered = detect_service._filter_lines_since(lines, "2026-04-21 20:17:00")
self.assertEqual(lines, filtered)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,205 @@
from __future__ import annotations
import json
import os
import tempfile
import unittest
from contextlib import contextmanager
from unittest.mock import patch
from requests.cookies import RequestsCookieJar
from app.services import juming_service, juming_task_service
class JumingServiceTests(unittest.TestCase):
def test_crawl_juming_rejects_invalid_cookie(self) -> None:
cookie_jar = RequestsCookieJar()
cookie_jar.set("sid", "expired")
with patch.object(juming_service, "_load_juming_cookie", return_value=(cookie_jar, "local")):
with patch.object(juming_service, "_validate_juming_cookie", return_value=(False, "聚名登录态已失效,请重新登录")):
with self.assertRaisesRegex(ValueError, "已失效"):
juming_service.crawl_juming({"mode": "delete_list"})
def test_get_juming_status_exposes_remote_validation(self) -> None:
cookie_jar = RequestsCookieJar()
cookie_jar.set("sid", "alive")
with patch.object(juming_service, "_load_juming_cookie", return_value=(cookie_jar, "local")):
with patch.object(juming_service, "_validate_juming_cookie", return_value=(False, "聚名登录态已失效,请重新登录")):
payload = juming_service.get_juming_status()
self.assertTrue(payload["cookie_present"])
self.assertFalse(payload["cookie_valid"])
self.assertFalse(payload["cookie_ready"])
self.assertIn("已失效", payload["cookie_message"])
def test_insert_domains_uses_copy_stage_import_path(self) -> None:
class FakeCursor:
def __init__(self) -> None:
self.executed: list[tuple[str, object]] = []
self.copy_calls: list[tuple[str, tuple[str, ...], str]] = []
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def execute(self, sql: str, params: object = None) -> None:
self.executed.append((sql, params))
def copy_from(self, file_obj, table: str, columns: tuple[str, ...]) -> None:
self.copy_calls.append((table, columns, file_obj.read()))
def fetchone(self):
return (2, 2, 0)
class FakeConn:
def __init__(self) -> None:
self.cursor_obj = FakeCursor()
self.commit_calls = 0
def cursor(self):
return self.cursor_obj
def commit(self) -> None:
self.commit_calls += 1
fake_conn = FakeConn()
@contextmanager
def fake_get_db():
yield fake_conn
with patch.object(juming_service, "get_db", fake_get_db):
with patch.object(juming_service, "IMPORT_BATCH_SIZE", 2):
stats = juming_service._insert_domains(
["alpha.com", "beta.net"],
juming_service.DELETE_LIST_SOURCE_TYPE,
)
self.assertEqual(2, stats["added"])
self.assertEqual(1, fake_conn.commit_calls)
self.assertTrue(fake_conn.cursor_obj.copy_calls)
table, columns, payload = fake_conn.cursor_obj.copy_calls[0]
self.assertEqual("juming_import_stage", table)
self.assertEqual(("domain", "tld"), columns)
self.assertIn("alpha.com\tcom", payload)
self.assertIn("beta.net\tnet", payload)
executed_sql = "\n".join(sql for sql, _params in fake_conn.cursor_obj.executed)
self.assertIn("create temporary table if not exists juming_import_stage", executed_sql.lower())
self.assertIn("inserted as", executed_sql.lower())
self.assertIn("left join domains existing", executed_sql.lower())
def test_delete_list_import_skips_already_imported_same_signature(self) -> None:
cookie_jar = RequestsCookieJar()
cookie_jar.set("sid", "alive")
class FakeJM:
cookie = cookie_jar
def new_cha_del(self, current_date: str):
if current_date == "2026-03-21":
return ["alpha.com", "beta.net"]
return []
logs: list[str] = []
signature = juming_service._compute_domains_signature(["alpha.com", "beta.net"])
cached_state = {
"2026-03-21": {
"signature": signature,
"total": 2,
"valid": 2,
"invalid": 0,
}
}
with patch.object(juming_service, "_load_juming_cookie", return_value=(cookie_jar, "local")):
with patch.object(juming_service, "JM", return_value=FakeJM()):
with patch.object(juming_service, "_load_delete_import_state", return_value=cached_state):
with patch.object(juming_service, "_insert_domains") as mock_insert:
result = juming_service._crawl_delete_list_and_import(
"2026-03-21",
False,
log=logs.append,
)
mock_insert.assert_not_called()
self.assertEqual(0, result["stats"]["added"])
self.assertEqual(2, result["stats"]["exists"])
self.assertTrue(any("跳过重复入库" in line for line in logs))
class JumingTaskServiceTests(unittest.TestCase):
def setUp(self) -> None:
self._tmpdir = tempfile.TemporaryDirectory()
self._old_runtime_root = os.environ.get("DOMAIN_API_RUNTIME_ROOT")
os.environ["DOMAIN_API_RUNTIME_ROOT"] = self._tmpdir.name
juming_task_service._ACTIVE_TASK_IDS.clear()
def tearDown(self) -> None:
juming_task_service._ACTIVE_TASK_IDS.clear()
if self._old_runtime_root is None:
os.environ.pop("DOMAIN_API_RUNTIME_ROOT", None)
else:
os.environ["DOMAIN_API_RUNTIME_ROOT"] = self._old_runtime_root
self._tmpdir.cleanup()
def _write_tasks(self, records: list[dict]) -> None:
path = os.path.join(self._tmpdir.name, "juming_tasks.json")
with open(path, "w", encoding="utf-8") as handle:
json.dump(records, handle, ensure_ascii=False, indent=2)
def test_cleanup_orphaned_tasks_marks_running_task_failed(self) -> None:
self._write_tasks(
[
{
"task_id": "task-1",
"status": "running",
"phase": "importing",
"phase_label": "入库中",
"cancel_requested": False,
"message": "开始入库处理",
"created_at": "2026-04-21 21:00:00",
"updated_at": "2026-04-21 21:00:00",
"started_at": "2026-04-21 21:00:00",
"completed_at": "",
"result": None,
"logs": [],
}
]
)
tasks = juming_task_service.list_juming_tasks()
self.assertEqual("failed", tasks[0]["status"])
self.assertIn("中断", tasks[0]["message"])
def test_create_task_rejects_parallel_active_task(self) -> None:
self._write_tasks(
[
{
"task_id": "task-1",
"status": "running",
"phase": "fetching",
"phase_label": "抓取中",
"cancel_requested": False,
"message": "正在抓取",
"created_at": "2026-04-21 21:00:00",
"updated_at": "2026-04-21 21:00:00",
"started_at": "2026-04-21 21:00:00",
"completed_at": "",
"result": None,
"logs": [],
}
]
)
juming_task_service._ACTIVE_TASK_IDS.add("task-1")
with self.assertRaisesRegex(ValueError, "已有聚名采集任务正在运行"):
juming_task_service.create_juming_task({"mode": "delete_list"})
if __name__ == "__main__":
unittest.main()

View File

@@ -20,6 +20,111 @@ class NodeAgentDeliveryQueueTests(unittest.TestCase):
self.assertEqual(0, delivery_queue["pending_count"])
self.assertEqual(0, delivery_queue["dead_letter_count"])
def test_base_payload_prefers_non_loopback_identity(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
with patch.object(node_agent, "AGENT_QUEUE_DIR", temp_dir), patch.object(
node_agent,
"CONTROL_PLANE_BASE_URL",
"http://152.53.37.118:8100",
), patch.object(
node_agent.socket,
"gethostname",
return_value="localhost",
), patch.object(
node_agent.socket,
"getfqdn",
return_value="localhost.localdomain",
), patch.object(
node_agent.os,
"uname",
return_value=type("Uname", (), {"nodename": "localhost"})(),
), patch.object(
node_agent,
"NODE_CODE",
"mainland-controller-01",
), patch.object(
node_agent.socket,
"getaddrinfo",
return_value=[(None, None, None, None, ("127.0.0.1", 0))],
), patch.object(
node_agent.socket,
"gethostbyname",
return_value="127.0.0.1",
):
class FakeSocket:
def connect(self, target):
self.target = target
def getsockname(self):
return ("121.204.244.188", 12345)
def close(self):
return None
with patch.object(node_agent.socket, "socket", return_value=FakeSocket()):
payload = node_agent._base_payload()
self.assertNotIn(payload["hostname"], {"", "localhost", "localhost.localdomain"})
self.assertEqual("121.204.244.188", payload["ip"])
def test_detect_runtime_snapshot_degrades_to_worker_runtime_when_detect_status_fails(self) -> None:
with patch.dict(os.environ, {}, clear=False):
with patch(
"app.services.worker_control_service.detect_worker_runtime",
return_value={
"running": True,
"process_count": 1,
"latest_start_time": "2026-04-20 18:00:00",
"message": "active/running",
},
), patch(
"app.services.detect_service.get_detect_status",
side_effect=RuntimeError('connection to server at "127.0.0.1", port 5432 failed'),
):
snapshot = node_agent._detect_runtime_snapshot()
self.assertTrue(snapshot["worker_online"])
self.assertTrue(snapshot["service_running"])
self.assertFalse(snapshot["detecting"])
self.assertEqual("active/running", snapshot["phase_detail"])
self.assertEqual("2026-04-20 18:00:00", snapshot["updated_at"])
self.assertIn("127.0.0.1", snapshot["error"])
def test_detect_runtime_snapshot_infers_worker_online_from_active_threads(self) -> None:
with patch.dict(os.environ, {}, clear=False):
with patch(
"app.services.worker_control_service.detect_worker_runtime",
return_value={
"running": False,
"process_count": 0,
"latest_start_time": "",
"message": "",
},
), patch(
"app.services.detect_service.get_detect_status",
return_value={
"worker_online": False,
"detecting": False,
"active_thread_count": 19,
"max_thread_count": 1200,
"phase_label": "检测中",
"phase_detail": "Worker 正在处理 162 个检测任务",
"runtime_state": {
"service_running": False,
"updated_at": "2026-04-21 00:00:06",
},
"active_job": {"items_running": 0},
},
):
snapshot = node_agent._detect_runtime_snapshot()
self.assertTrue(snapshot["worker_online"])
self.assertTrue(snapshot["service_running"])
self.assertTrue(snapshot["detecting"])
self.assertTrue(snapshot["detect_participating"])
self.assertEqual(19, snapshot["active_threads"])
self.assertEqual(19, snapshot["current_load"])
def test_job_event_network_failure_is_queued_for_retry(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
with patch.object(node_agent, "AGENT_QUEUE_DIR", temp_dir), patch.object(
@@ -279,6 +384,63 @@ class NodeAgentDeliveryQueueTests(unittest.TestCase):
self.assertEqual("failed_local", event_payload["start_delivery_state"])
self.assertIn("temporary offline", event_payload["start_delivery_error"])
def test_register_heartbeat_and_pull_use_configured_timeouts(self) -> None:
with patch.object(node_agent, "_post", return_value={"code": 0, "message": "ok", "data": {"jobs": []}}) as mock_post, patch.object(
node_agent,
"AGENT_REGISTER_TIMEOUT_SECONDS",
91,
), patch.object(
node_agent,
"AGENT_HEARTBEAT_TIMEOUT_SECONDS",
92,
), patch.object(
node_agent,
"AGENT_PULL_TIMEOUT_SECONDS",
93,
):
node_agent._register()
node_agent._heartbeat()
jobs = node_agent._pull_jobs()
self.assertEqual([], jobs)
self.assertEqual(3, mock_post.call_count)
self.assertEqual(91, mock_post.call_args_list[0].kwargs["timeout"])
self.assertEqual(92, mock_post.call_args_list[1].kwargs["timeout"])
self.assertEqual(93, mock_post.call_args_list[2].kwargs["timeout"])
def test_job_delivery_uses_configured_timeouts(self) -> None:
with patch.object(
node_agent,
"_deliver_or_queue",
side_effect=lambda **kwargs: {"state": "queued", "timeout": kwargs["timeout"]},
) as mock_deliver, patch.object(
node_agent,
"AGENT_JOB_COMPLETE_TIMEOUT_SECONDS",
94,
), patch.object(
node_agent,
"AGENT_JOB_EVENT_TIMEOUT_SECONDS",
47,
):
complete_result = node_agent._job_complete(
11,
status="success",
stdout="ok",
stderr="",
result={"ok": True},
)
event_result = node_agent._job_event(
11,
event_type="executor_received",
message="accepted",
)
self.assertEqual("queued", complete_result["state"])
self.assertEqual("queued", event_result["state"])
self.assertEqual(2, mock_deliver.call_count)
self.assertEqual(94, mock_deliver.call_args_list[0].kwargs["timeout"])
self.assertEqual(47, mock_deliver.call_args_list[1].kwargs["timeout"])
if __name__ == "__main__":
unittest.main()

View File

@@ -110,6 +110,34 @@ class OpsActionExecutorCoreTests(unittest.TestCase):
self.assertTrue(data["fallback_used"])
self.assertIn("detect_worker.log", data["log_paths"][0])
@patch("app.services.ops_action_executor_core._reset_lab_state")
def test_runtime_reset_lab_state_returns_helper_result_with_host_context(self, mock_reset_lab_state) -> None:
runner = _FakeRunner({})
mock_reset_lab_state.return_value = (
True,
"lab runtime reset completed",
{"database": {"include_domains": True}},
)
ok, message, data = execute_structured_action(
"runtime.reset_lab_state",
{"clear_database": True, "clear_domains": True},
service_names=build_service_name_map(
api_service_name="domaincheck-api",
worker_service_name="domaincheck-worker",
sync_agent_service_name="domaincheck-sync-agent",
),
runner=runner,
host_context={"hostname": "mainland-controller-01", "ip": "121.204.244.188"},
)
self.assertTrue(ok)
self.assertEqual("lab runtime reset completed", message)
self.assertEqual("mainland-controller-01", data["hostname"])
self.assertEqual("121.204.244.188", data["ip"])
self.assertTrue(data["database"]["include_domains"])
mock_reset_lab_state.assert_called_once()
if __name__ == "__main__":
unittest.main()

View File

@@ -4,8 +4,11 @@ from unittest.mock import patch
from app.api.routes.ops_agent import _build_agent_response
from app.services.ops_agent_service import (
_build_agent_runtime_config_bundle,
_runtime_config_bundle_hash_payload,
agent_append_job_event,
agent_complete_job,
agent_mark_job_started,
agent_pull_jobs,
agent_register,
build_node_agent_bootstrap_plan,
@@ -102,6 +105,64 @@ class _SequenceConnection:
class OpsAgentServiceTests(unittest.TestCase):
def test_runtime_config_bundle_hash_ignores_generated_at(self) -> None:
bundle_a = {
"node_code": "mainland-worker-01",
"thread_count": 100,
"node_thread_counts": {"mainland-worker-01": 100},
"runtime_settings": {"worker_log_sync_enabled": True},
"generated_at": "2026-04-19 17:30:00",
"config_hash": "stale-hash",
}
bundle_b = {
"node_code": "mainland-worker-01",
"thread_count": 100,
"node_thread_counts": {"mainland-worker-01": 100},
"runtime_settings": {"worker_log_sync_enabled": True},
"generated_at": "2026-04-19 17:35:00",
"config_hash": "other-stale-hash",
}
self.assertEqual(
_runtime_config_bundle_hash_payload(bundle_a),
_runtime_config_bundle_hash_payload(bundle_b),
)
@patch("app.services.ops_agent_service.get_sensitive_words_payload")
@patch("app.services.ops_agent_service.get_runtime_settings")
@patch("app.services.ops_agent_service.get_settings_payload")
def test_build_agent_runtime_config_bundle_keeps_hash_stable_for_same_config(
self,
mock_get_settings_payload,
mock_get_runtime_settings,
mock_get_sensitive_words_payload,
) -> None:
mock_get_settings_payload.return_value = {
"detect_options": {"detect_wayback": True},
"proxy_config": {"enabled": True},
"thread_count": 100,
"node_thread_counts": {"mainland-worker-01": 100},
}
mock_get_runtime_settings.return_value = {
"worker_log_sync_enabled": True,
"worker_log_sync_mode": "full",
}
mock_get_sensitive_words_payload.return_value = {
"text": "foo\nbar",
"total": 2,
"items": ["foo", "bar"],
}
with patch(
"app.services.ops_agent_service._format_time",
side_effect=["2026-04-19 17:30:00", "2026-04-19 17:35:00"],
):
first_bundle = _build_agent_runtime_config_bundle("mainland-worker-01")
second_bundle = _build_agent_runtime_config_bundle("mainland-worker-01")
self.assertNotEqual(first_bundle["generated_at"], second_bundle["generated_at"])
self.assertEqual(first_bundle["config_hash"], second_bundle["config_hash"])
def test_build_agent_response_extracts_detail_code(self) -> None:
response = _build_agent_response(False, "bad request", {"detail_code": "agent_token_invalid", "foo": "bar"})
@@ -730,6 +791,76 @@ class OpsAgentServiceTests(unittest.TestCase):
self.assertEqual(0, payload["summary"]["agent_ready"])
self.assertEqual(1, payload["summary"]["remote_access_ready"])
@patch("app.services.runtime_status_service.get_runtime_status")
@patch("app.services.cluster_runtime_service.get_cluster_snapshot")
@patch("app.services.ops_job_service.list_managed_nodes")
@patch("app.services.ops_agent_service.get_db")
@patch("app.services.ops_agent_service.ensure_ops_agent_schema")
def test_list_managed_nodes_with_agent_state_prefers_non_loopback_cluster_identity_and_infers_worker_online(
self,
mock_ensure_ops_agent_schema,
mock_get_db,
mock_list_managed_nodes,
mock_get_cluster_snapshot,
mock_get_runtime_status,
) -> None:
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
mock_ensure_ops_agent_schema.return_value = None
mock_get_db.return_value = _EmptyConnection()
mock_list_managed_nodes.return_value = [
{
"node_code": "mainland-worker-01",
"region": "mainland",
"role": "worker",
"title": "Mainland Worker 01",
"is_enabled": True,
"metadata": {
"hostname": "S244-248",
"ip": "121.204.244.248",
},
"last_seen_at": now,
}
]
mock_get_cluster_snapshot.return_value = {
"nodes": [
{
"node_code": "mainland-worker-01",
"region": "mainland",
"role": "worker",
"status": "busy",
"current_load": 19,
"hostname": "localhost",
"ip": "127.0.0.1",
"last_heartbeat_at": now,
"is_effective_worker": True,
"detect_participating": False,
"metadata": {
"worker_online": False,
"active_threads": 19,
"max_threads": 1200,
"phase_label": "检测中",
"phase_detail": "Worker 正在处理 162 个检测任务",
},
}
]
}
mock_get_runtime_status.return_value = {
"detect": {
"participating_nodes": [],
"non_participating_nodes": [],
}
}
payload = list_managed_nodes_with_agent_state()
self.assertEqual(1, len(payload["nodes"]))
row = payload["nodes"][0]
self.assertEqual("S244-248", row["cluster_hostname"])
self.assertEqual("121.204.244.248", row["cluster_ip"])
self.assertTrue(row["detect_runtime"]["worker_online"])
self.assertTrue(row["detect_runtime"]["detect_participating"])
self.assertEqual(19, row["detect_runtime"]["active_threads"])
def test_agent_register_requires_node_code_with_detail_code(self) -> None:
ok, message, data = agent_register({}, token="ignored")
@@ -1176,6 +1307,119 @@ class OpsAgentServiceTests(unittest.TestCase):
self.assertEqual("当前控制面仅支持对可见头部记录执行单条动作", message)
self.assertEqual("evt-hidden", data["record_id"])
@patch("app.services.ops_agent_service.get_ops_job")
@patch("app.services.ops_agent_service.append_ops_job_event")
@patch("app.services.ops_agent_service._authenticate_agent_token")
@patch("app.services.ops_agent_service.get_db")
def test_agent_pull_jobs_commits_before_appending_event(
self,
mock_get_db,
mock_authenticate_agent_token,
mock_append_ops_job_event,
mock_get_ops_job,
) -> None:
cursor = _SequenceCursor(fetchall_values=[[(36,)]] )
connection = _SequenceConnection(cursor)
mock_get_db.return_value = connection
mock_authenticate_agent_token.return_value = (True, "ok", {"node_code": "mainland-worker-01"})
mock_get_ops_job.return_value = {"id": 36, "job_code": "ops-demo", "action": "health.snapshot"}
def _assert_after_commit(**kwargs):
self.assertTrue(connection.committed)
self.assertEqual(36, kwargs["job_id"])
self.assertEqual("agent_dispatched", kwargs["event_type"])
mock_append_ops_job_event.side_effect = _assert_after_commit
ok, message, data = agent_pull_jobs({"node_code": "mainland-worker-01"}, token="agent-token", limit=1)
self.assertTrue(ok)
self.assertEqual("ok", message)
self.assertEqual(1, data["count"])
self.assertTrue(connection.committed)
mock_append_ops_job_event.assert_called_once()
@patch("app.services.ops_agent_service.get_ops_job")
@patch("app.services.ops_agent_service.append_ops_job_event")
@patch("app.services.ops_agent_service._authenticate_agent_token")
@patch("app.services.ops_agent_service.get_db")
def test_agent_mark_job_started_commits_before_appending_event(
self,
mock_get_db,
mock_authenticate_agent_token,
mock_append_ops_job_event,
mock_get_ops_job,
) -> None:
cursor = _SequenceCursor(fetchone_values=[(36,)], fetchall_values=[[(101,), (102,)]])
connection = _SequenceConnection(cursor)
mock_get_db.return_value = connection
mock_authenticate_agent_token.return_value = (True, "ok", {"node_code": "mainland-worker-01"})
mock_get_ops_job.return_value = {"id": 36, "job_code": "ops-demo", "action": "health.snapshot"}
def _assert_after_commit(**kwargs):
self.assertTrue(connection.committed)
self.assertEqual(36, kwargs["job_id"])
self.assertEqual("agent_started", kwargs["event_type"])
self.assertEqual({"step_ids": [101, 102]}, kwargs["payload"])
mock_append_ops_job_event.side_effect = _assert_after_commit
ok, message, data = agent_mark_job_started(36, {"node_code": "mainland-worker-01"}, token="agent-token")
self.assertTrue(ok)
self.assertEqual("任务已标记为运行中", message)
self.assertEqual(36, data["job"]["id"])
self.assertTrue(connection.committed)
mock_append_ops_job_event.assert_called_once()
@patch("app.services.ops_release_service.refresh_release_rollout_for_job")
@patch("app.services.ops_agent_service.get_ops_job")
@patch("app.services.ops_agent_service.append_ops_job_event")
@patch("app.services.ops_agent_service._authenticate_agent_token")
@patch("app.services.ops_agent_service.get_db")
def test_agent_complete_job_commits_before_appending_event(
self,
mock_get_db,
mock_authenticate_agent_token,
mock_append_ops_job_event,
mock_get_ops_job,
mock_refresh_release_rollout_for_job,
) -> None:
cursor = _SequenceCursor(fetchone_values=[(36, "")], fetchall_values=[[(101,)]] )
connection = _SequenceConnection(cursor)
mock_get_db.return_value = connection
mock_authenticate_agent_token.return_value = (True, "ok", {"node_code": "mainland-worker-01"})
mock_get_ops_job.return_value = {"id": 36, "job_code": "ops-demo", "status": "success"}
def _assert_after_commit(**kwargs):
self.assertTrue(connection.committed)
self.assertEqual(36, kwargs["job_id"])
self.assertEqual("agent_completed", kwargs["event_type"])
self.assertEqual("info", kwargs["level"])
self.assertEqual([101], kwargs["payload"]["step_ids"])
mock_append_ops_job_event.side_effect = _assert_after_commit
ok, message, data = agent_complete_job(
36,
{
"node_code": "mainland-worker-01",
"status": "success",
"stdout": "ok",
"stderr": "",
"result": {"summary": "done"},
"duration_ms": 120,
},
token="agent-token",
)
self.assertTrue(ok)
self.assertEqual("任务结果已回写", message)
self.assertEqual(36, data["job"]["id"])
self.assertTrue(connection.committed)
mock_append_ops_job_event.assert_called_once()
mock_refresh_release_rollout_for_job.assert_called_once_with(36)
if __name__ == "__main__":
unittest.main()

View File

@@ -4,11 +4,13 @@ from datetime import datetime
from unittest.mock import MagicMock, patch
from app.core.config import settings
from app.core.db import get_db
from app.services.ops_job_service import (
_execute_control_plane_job,
_serialize_job_row,
create_ops_job,
dispatch_ops_job,
list_managed_nodes,
upsert_managed_node,
)
@@ -109,6 +111,39 @@ class OpsJobServiceTests(unittest.TestCase):
self.assertTrue(job["steps_loaded"])
self.assertIn("mainland-worker-02", job["summary_text"])
def test_upsert_managed_node_supports_ssh_entry_and_secret_flags(self) -> None:
node_code = "test-managed-node-secret"
ok, message, data = upsert_managed_node(
{
"node_code": node_code,
"region": "mainland",
"role": "worker",
"ssh_entry": "121.204.244.188 root <demo-password>",
"is_enabled": True,
}
)
self.assertTrue(ok, message)
node = data["node"]
self.assertEqual("121.204.244.188", node["ssh_host"])
self.assertEqual("root", node["ssh_user"])
self.assertEqual(22, node["ssh_port"])
self.assertEqual("password", node["auth_mode"])
self.assertTrue(node["ssh_password_configured"])
managed = next(item for item in list_managed_nodes() if item["node_code"] == node_code)
self.assertTrue(managed["ssh_password_configured"])
with get_db() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT ssh_password FROM ops_managed_node_secrets WHERE node_code = %s",
(node_code,),
)
row = cur.fetchone()
self.assertIsNotNone(row)
self.assertEqual("demo-password", str(row[0] or ""))
@patch("app.services.ops_job_service.list_managed_nodes")
@patch("app.services.ops_agent_service.build_node_agent_bootstrap_plan")
def test_execute_control_plane_bootstrap_uses_managed_node_defaults(
@@ -237,7 +272,11 @@ class OpsJobServiceTests(unittest.TestCase):
self.assertEqual("keep-me", node["metadata"]["operator_note"])
self.assertEqual("online", node["metadata"]["cluster_status"])
insert_call = cursor.execute.call_args_list[-1]
insert_call = next(
call
for call in cursor.execute.call_args_list
if "INSERT INTO ops_managed_nodes" in str(call.args[0])
)
insert_params = insert_call.args[1]
self.assertEqual("121.204.244.248", insert_params[4])
self.assertEqual(22, insert_params[5])

View File

@@ -248,6 +248,42 @@ class OpsPolicyServiceTests(unittest.TestCase):
self.assertTrue(preview["blocked"])
self.assertIn("local-runtime 仅支持当前控制面本机节点", " ".join(preview["blocking_reasons"]))
@patch("app.services.ops_policy_service.get_cluster_snapshot")
def test_runtime_reset_lab_state_is_critical_and_requires_approval(self, mock_cluster_snapshot) -> None:
mock_cluster_snapshot.return_value = {
"nodes": [
{
"node_code": "mainland-controller-01",
"region": "mainland",
"role": "control",
"status": "online",
"current_load": 0,
"is_effective_worker": True,
"detect_participating": False,
}
],
"summary": {
"online_worker_nodes": 2,
"dedicated_online_worker_nodes": 1,
"online_control_nodes": 2,
"busy_nodes": [],
},
}
preview = preview_ops_job_policy(
{
"action": "runtime.reset_lab_state",
"target_type": "node",
"target_node_code": "mainland-controller-01",
"execution_mode": "remote-agent",
"payload": {"clear_database": True, "clear_redis": True},
}
)
self.assertEqual("critical", preview["risk_level"])
self.assertTrue(preview["approval_required"])
self.assertFalse(preview["blocked"])
if __name__ == "__main__":
unittest.main()

View File

@@ -1,10 +1,18 @@
import io
import grp
import os
import pwd
import tarfile
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from app.services.ops_release_executor_core import build_remote_release_action_script, execute_release_action
from app.services.ops_release_executor_core import (
_pick_release_owner_group,
build_remote_release_action_script,
execute_release_action,
)
class _BytesResponse:
@@ -24,20 +32,45 @@ class _BytesResponse:
def _build_release_archive() -> bytes:
buffer = io.BytesIO()
with tarfile.open(fileobj=buffer, mode="w:gz") as archive:
content = b"hello-release"
info = tarfile.TarInfo(name="bundle/README.txt")
info.size = len(content)
archive.addfile(info, io.BytesIO(content))
files = {
"README.txt": b"hello-release",
"domain-api/deploy/systemd/domain-node-agent.service": b"[Service]\nEnvironmentFile=-/etc/default/domaincheck-worker\n",
"domain-api/deploy/systemd/domain-worker.service": b"[Service]\nEnvironmentFile=-/etc/default/domaincheck-worker\n",
"domain-api/deploy/systemd/domain-api.service": b"[Service]\nEnvironmentFile=-/etc/default/domaincheck-api\n",
"domain-api/deploy/systemd/domain-sync-agent.service": b"[Service]\nEnvironmentFile=-/etc/default/domaincheck-worker\n",
}
for name, content in files.items():
info = tarfile.TarInfo(name=name)
info.size = len(content)
archive.addfile(info, io.BytesIO(content))
return buffer.getvalue()
class OpsReleaseExecutorCoreTests(unittest.TestCase):
def test_pick_release_owner_group_prefers_service_identity_over_path_owner(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
with patch(
"app.services.ops_release_executor_core._resolve_path_owner_group",
return_value=("root", "root"),
):
owner_user, owner_group = _pick_release_owner_group(
Path(tmpdir),
[{"service_name": "domaincheck-worker", "user": "www", "group": "www"}],
)
self.assertEqual(("www", "www"), (owner_user, owner_group))
def test_execute_release_action_deploys_archive_and_switches_current(self) -> None:
archive_bytes = _build_release_archive()
commands: list[tuple[str, ...]] = []
def run_command(command: list[str], *, timeout: int = 60):
commands.append(tuple(command))
if command[:3] == ["systemctl", "show", "domaincheck-worker"] and "-p" in command:
current_user = pwd.getpwuid(os.getuid()).pw_name
current_group = grp.getgrgid(os.getgid()).gr_name
return 0, f"{current_user}\n{current_group}\n", ""
if command[:2] == ["chown", "-R"]:
return 0, "", ""
if command[:2] == ["systemctl", "restart"]:
return 0, "", ""
if command[:2] == ["systemctl", "is-active"]:
@@ -45,11 +78,13 @@ class OpsReleaseExecutorCoreTests(unittest.TestCase):
return 0, "", ""
with tempfile.TemporaryDirectory() as tmpdir:
systemd_root = Path(tmpdir) / "systemd"
ok, message, data = execute_release_action(
{
"release_version": "2026.04.18-rc1",
"artifact_url": "https://example.com/domaincheck.tar.gz",
"install_root": tmpdir,
"systemd_unit_root": str(systemd_root),
"restart_services": ["domaincheck-worker"],
"health_check_services": ["domaincheck-worker"],
"health_check_urls": [],
@@ -70,19 +105,29 @@ class OpsReleaseExecutorCoreTests(unittest.TestCase):
release_dir = Path(data["release_dir"])
self.assertTrue(release_dir.exists())
self.assertTrue((release_dir / ".release-meta.json").exists())
self.assertTrue(any(cmd[:2] == ("chown", "-R") for cmd in commands))
self.assertIn(("systemctl", "daemon-reload"), commands)
self.assertIn(("systemctl", "restart", "domaincheck-worker"), commands)
self.assertIn(("systemctl", "is-active", "domaincheck-worker"), commands)
self.assertTrue((systemd_root / "domaincheck-node-agent.service").exists())
self.assertTrue((systemd_root / "domaincheck-node-agent.service.d" / "current-path.conf").exists())
def test_build_remote_release_action_script_is_valid_python(self) -> None:
script = build_remote_release_action_script(
{
"release_version": "2026.04.18-rc1",
"artifact_url": "https://example.com/domaincheck.tar.gz",
"switch_current": True,
"rollback_on_failure": False,
}
)
compile(script, "<remote-release-script>", "exec")
self.assertIn("execute_release_action(", script)
self.assertIn("True", script)
self.assertIn("False", script)
self.assertIn("def collect_service_identity(", script)
self.assertIn("def apply_release_permissions(", script)
if __name__ == "__main__":

View File

@@ -2,14 +2,17 @@ import subprocess
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from unittest.mock import MagicMock, patch
from app.services import ops_runtime_executor_service
from app.services.ops_runtime_executor_service import execute_local_support_action, execute_ssh_action
class OpsRuntimeExecutorServiceTests(unittest.TestCase):
@patch("app.services.ops_runtime_executor_service.subprocess.run")
def test_execute_ssh_action_parses_structured_json_result(self, mock_run) -> None:
@patch("app.services.ops_runtime_executor_service._load_ssh_secret")
def test_execute_ssh_action_parses_structured_json_result(self, mock_load_secret, mock_run) -> None:
mock_load_secret.return_value = {"ssh_password": "", "ssh_private_key": ""}
mock_run.return_value = subprocess.CompletedProcess(
args=["ssh"],
returncode=0,
@@ -113,6 +116,105 @@ class OpsRuntimeExecutorServiceTests(unittest.TestCase):
issued_command = mock_run.call_args.args[0]
self.assertEqual(["systemctl", "restart", "domaincheck-worker"], issued_command)
@patch("app.services.ops_runtime_executor_service.subprocess.run")
@patch("app.services.ops_runtime_executor_service._load_ssh_secret")
@patch("app.services.ops_runtime_executor_service.paramiko.SSHClient")
def test_execute_ssh_action_uses_paramiko_for_password_auth(self, mock_ssh_client_cls, mock_load_secret, mock_run) -> None:
mock_load_secret.return_value = {"ssh_password": "demo-pass", "ssh_private_key": ""}
mock_client = MagicMock()
stdout = MagicMock()
stderr = MagicMock()
stdout.channel.recv_exit_status.return_value = 0
stdout.read.return_value = (
b'{"ok": true, "message": "health snapshot collected", "result": {"checks": {"api": {"state": "active"}}}}\n'
)
stderr.read.return_value = b""
mock_client.exec_command.return_value = (None, stdout, stderr)
mock_ssh_client_cls.return_value = mock_client
ok, message, data = execute_ssh_action(
{
"node_code": "mainland-worker-01",
"ssh_host": "121.204.244.248",
"ssh_port": 22,
"ssh_user": "root",
"auth_mode": "password",
},
"health.snapshot",
{},
)
self.assertTrue(ok)
self.assertEqual("health snapshot collected", message)
self.assertEqual("active", data["checks"]["api"]["state"])
self.assertEqual("password", data["transport"]["auth_mode"])
mock_client.connect.assert_called_once()
self.assertFalse(mock_run.called)
@patch("app.services.ops_runtime_executor_service.subprocess.run")
@patch("app.services.ops_runtime_executor_service._load_ssh_secret")
@patch("app.services.ops_runtime_executor_service.paramiko.SSHClient")
def test_execute_ssh_action_falls_back_to_password_when_node_auth_mode_is_key_but_only_password_exists(
self,
mock_ssh_client_cls,
mock_load_secret,
mock_run,
) -> None:
mock_load_secret.return_value = {"ssh_password": "demo-pass", "ssh_private_key": ""}
mock_client = MagicMock()
stdout = MagicMock()
stderr = MagicMock()
stdout.channel.recv_exit_status.return_value = 0
stdout.read.return_value = (
b'{"ok": true, "message": "health snapshot collected", "result": {"checks": {"api": {"state": "active"}}}}\n'
)
stderr.read.return_value = b""
mock_client.exec_command.return_value = (None, stdout, stderr)
mock_ssh_client_cls.return_value = mock_client
ok, message, data = execute_ssh_action(
{
"node_code": "mainland-controller-01",
"ssh_host": "121.204.244.188",
"ssh_port": 22,
"ssh_user": "root",
"auth_mode": "key",
},
"health.snapshot",
{},
)
self.assertTrue(ok)
self.assertEqual("health snapshot collected", message)
self.assertEqual("password", data["transport"]["auth_mode"])
mock_client.connect.assert_called_once()
self.assertFalse(mock_run.called)
@patch("app.services.ops_runtime_executor_service._load_ssh_secret")
def test_execute_ssh_action_returns_graceful_error_when_paramiko_missing_for_password_auth(
self,
mock_load_secret,
) -> None:
mock_load_secret.return_value = {"ssh_password": "demo-pass", "ssh_private_key": ""}
with patch.object(ops_runtime_executor_service, "paramiko", None):
ok, message, data = execute_ssh_action(
{
"node_code": "mainland-controller-01",
"ssh_host": "121.204.244.188",
"ssh_port": 22,
"ssh_user": "root",
"auth_mode": "password",
},
"health.snapshot",
{},
)
self.assertFalse(ok)
self.assertIn("paramiko", message)
self.assertEqual("password", data["transport"]["auth_mode"])
self.assertEqual("121.204.244.188", data["transport"]["ssh_host"])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,131 @@
import unittest
from app.services.detect_service import _slice_remote_log_lines_fairly
from app.services.runtime_status_service import _align_queue_health_with_backlog, _build_detect_node_row
class RuntimeStatusServiceTests(unittest.TestCase):
def test_build_detect_node_row_merges_queue_running_metrics(self) -> None:
row = _build_detect_node_row(
node_code="mainland-worker-01",
cluster_node={
"node_code": "mainland-worker-01",
"region": "mainland",
"role": "worker",
"status": "busy",
"is_effective_worker": True,
"current_load": 61,
"metadata": {
"job_items_total": 1000,
"job_items_claimed": 0,
"job_items_running": 61,
},
},
job_node={
"node_code": "mainland-worker-01",
"items_total": 0,
"items_pending": 0,
"items_claimed": 0,
"items_running": 0,
"items_completed": 0,
"items_failed": 0,
},
queue_node={
"node_code": "mainland-worker-01",
"items_total": 1000,
"items_pending": 739,
"items_claimed": 200,
"items_running": 61,
"items_completed": 191,
"items_blacklisted": 0,
"items_failed": 0,
"processed_recent": 632,
"processed_per_minute": 42.13,
},
)
self.assertEqual(1000, row["items_total"])
self.assertEqual(200, row["items_claimed"])
self.assertEqual(61, row["items_running"])
self.assertEqual("running", row["participation_state"])
self.assertTrue(row["is_current_participant"])
self.assertTrue(row["is_dispatch_active"])
def test_build_detect_node_row_uses_active_threads_as_current_execution_signal(self) -> None:
row = _build_detect_node_row(
node_code="mainland-worker-01",
cluster_node={
"node_code": "mainland-worker-01",
"region": "mainland",
"role": "worker",
"status": "busy",
"is_effective_worker": True,
"current_load": 61,
"metadata": {
"active_threads": 61,
"max_threads": 400,
},
},
job_node={},
queue_node={
"node_code": "mainland-worker-01",
"processed_recent": 632,
"processed_per_minute": 42.13,
},
)
self.assertEqual(61, row["active_threads"])
self.assertEqual(400, row["max_threads"])
self.assertEqual("runtime_active", row["participation_state"])
self.assertEqual("执行中", row["participation_label"])
self.assertTrue(row["is_dispatch_active"])
def test_slice_remote_log_lines_fairly_keeps_secondary_node_visible(self) -> None:
lines = [f"[2026-04-19 17:30:{i:02d}] [mainland-controller-01] controller-{i}" for i in range(20)]
lines.extend(
[f"[2026-04-19 17:31:{i:02d}] [mainland-worker-01] worker-{i}" for i in range(2)]
)
sliced = _slice_remote_log_lines_fairly(lines, limit=6, min_per_node=2)
self.assertEqual(6, len(sliced))
self.assertTrue(any("[mainland-controller-01]" in line for line in sliced))
self.assertTrue(any("[mainland-worker-01]" in line for line in sliced))
def test_align_queue_health_with_backlog_prefers_larger_runtime_snapshot(self) -> None:
aligned = _align_queue_health_with_backlog(
{
"has_active_job": True,
"queue": {
"items_total": 120,
"pending": 100,
"claimed": 10,
"running": 5,
"completed": 5,
"blacklisted": 0,
"failed": 0,
"terminal": 5,
},
},
{
"pending_total": 1200,
"claimed_total": 230,
"running_total": 40,
"completed_total": 300,
"blacklisted_total": 12,
"failed_total": 8,
},
)
self.assertTrue(aligned["has_active_job"])
self.assertEqual(1200, aligned["queue"]["pending"])
self.assertEqual(230, aligned["queue"]["claimed"])
self.assertEqual(40, aligned["queue"]["running"])
self.assertEqual(300, aligned["queue"]["completed"])
self.assertEqual(12, aligned["queue"]["blacklisted"])
self.assertEqual(8, aligned["queue"]["failed"])
self.assertEqual(1790, aligned["queue"]["items_total"])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,31 @@
import unittest
from app.services.settings_service import _normalize_thread_count, resolve_thread_count
class SettingsServiceTests(unittest.TestCase):
def test_normalize_thread_count_allows_values_above_256(self) -> None:
self.assertEqual(512, _normalize_thread_count(512))
def test_normalize_thread_count_rejects_non_positive_values(self) -> None:
with self.assertRaises(ValueError):
_normalize_thread_count(0)
def test_resolve_thread_count_uses_large_node_override(self) -> None:
payload = {
"thread_count": 8,
"node_thread_counts": {
"mainland-controller-01": 512,
},
}
resolved = resolve_thread_count(node_code="mainland-controller-01", settings_payload=payload)
self.assertEqual(8, resolved["default_thread_count"])
self.assertEqual(512, resolved["override_thread_count"])
self.assertEqual(512, resolved["effective_thread_count"])
self.assertEqual("node_override", resolved["source"])
if __name__ == "__main__":
unittest.main()

View File

@@ -1,7 +1,15 @@
import unittest
from unittest.mock import patch
from app.sync_agent import _append_detect_result_projection_snapshot
from app.sync_agent import (
_append_detect_result_projection_snapshot,
_build_aligned_queue_health_snapshot,
_emit_structured_tick,
_emit_sync_result_breakdown,
_filter_runtime_events_for_job,
_run_pipeline_stage_processor,
_select_projection_job_snapshots,
)
class SyncAgentTests(unittest.TestCase):
@@ -30,6 +38,155 @@ class SyncAgentTests(unittest.TestCase):
self.assertEqual(1, payload["progress"]["blacklisted"])
self.assertEqual(2, payload["progress"]["failed"])
@patch("app.sync_agent.get_latest_unprojected_detect_job_summary")
@patch("app.sync_agent.get_active_detect_job_summary")
def test_select_projection_job_snapshots_returns_active_and_latest_finished(
self,
mock_get_active_detect_job_summary,
mock_get_latest_unprojected_detect_job_summary,
) -> None:
mock_get_active_detect_job_summary.return_value = {"job_id": 101, "job_code": "running-job"}
mock_get_latest_unprojected_detect_job_summary.return_value = {"job_id": 102, "job_code": "finished-job"}
snapshots = _select_projection_job_snapshots()
self.assertEqual(["running-job", "finished-job"], [item["job_code"] for item in snapshots])
@patch("app.sync_agent.get_latest_unprojected_detect_job_summary")
@patch("app.sync_agent.get_active_detect_job_summary")
def test_select_projection_job_snapshots_deduplicates_same_job(
self,
mock_get_active_detect_job_summary,
mock_get_latest_unprojected_detect_job_summary,
) -> None:
mock_get_active_detect_job_summary.return_value = {"job_id": 101, "job_code": "same-job"}
mock_get_latest_unprojected_detect_job_summary.return_value = {"job_id": 101, "job_code": "same-job"}
snapshots = _select_projection_job_snapshots()
self.assertEqual(1, len(snapshots))
self.assertEqual("same-job", snapshots[0]["job_code"])
def test_build_aligned_queue_health_snapshot_overrides_mismatched_job(self) -> None:
active_job = {
"job_id": 1891,
"job_code": "sync-overseas-31230",
"status": "running",
"progress_percent": 75.9,
"items_total": 1000,
"items_pending": 155,
"items_claimed": 45,
"items_running": 41,
"items_completed": 759,
"items_blacklisted": 0,
"items_failed": 0,
"node_stats": [
{
"node_code": "mainland-worker-01",
"items_total": 710,
"items_claimed": 45,
"items_running": 41,
"items_completed": 624,
}
],
}
queue_health = {
"job": {"job_code": "sync-overseas-31273"},
"queue": {"items_total": 1000, "pending": 838, "claimed": 162, "running": 0, "completed": 0},
"nodes": [{"node_code": "mainland-controller-01", "items_total": 162}],
}
data = _build_aligned_queue_health_snapshot(active_job, queue_health)
self.assertEqual("sync-overseas-31230", data["job"]["job_code"])
self.assertEqual(155, data["queue"]["pending"])
self.assertEqual(45, data["queue"]["claimed"])
self.assertEqual(41, data["queue"]["running"])
self.assertEqual(759, data["queue"]["completed"])
self.assertEqual("mainland-worker-01", data["nodes"][0]["node_code"])
self.assertEqual("unassigned", data["nodes"][-1]["node_code"])
@patch("app.sync_agent.push_debug_event")
def test_emit_sync_result_breakdown_marks_idle_projection_as_idle(self, mock_push_debug_event) -> None:
_emit_sync_result_breakdown(
{
"results": [
{
"sync_type": "detect_result_projection",
"ok": False,
"message": "当前没有可推送的detect_result_projection",
"data": {},
}
]
}
)
mock_push_debug_event.assert_called_once()
self.assertEqual("detect_result_projection_sync_idle", mock_push_debug_event.call_args.kwargs["event_type"])
self.assertEqual("info", mock_push_debug_event.call_args.kwargs["level"])
@patch("app.sync_agent.push_debug_event")
def test_emit_structured_tick_marks_idle_sync_push_as_idle(self, mock_push_debug_event) -> None:
_emit_structured_tick(
base_event_type="sync_push",
ok=False,
message="当前没有需要立即推送的同步投影",
data={"results": []},
)
mock_push_debug_event.assert_called_once()
self.assertEqual("sync_push_idle", mock_push_debug_event.call_args.kwargs["event_type"])
self.assertEqual("info", mock_push_debug_event.call_args.kwargs["level"])
def test_filter_runtime_events_for_job_discards_mismatched_jobs(self) -> None:
events = _filter_runtime_events_for_job(
[
{
"job_id": 2137,
"node_code": "mainland-controller-01",
"event_type": "job_created",
"message": "new job",
"payload": {"source_record_id": 35461},
"created_at": "2026-04-22 01:21:29",
},
{
"job_id": 1902,
"node_code": "mainland-controller-01",
"event_type": "worker_log",
"message": "claim batch",
"payload": {"job_code": "sync-overseas-31437"},
"created_at": "2026-04-22 01:19:25",
},
],
job_code="sync-overseas-31437",
job_id=1902,
limit=8,
)
self.assertEqual(1, len(events))
self.assertEqual("worker_log", events[0]["event_type"])
@patch("app.sync_agent.push_debug_event")
@patch("app.sync_agent.process_detect_pipeline_now")
def test_run_pipeline_stage_processor_uses_high_default_limit(
self,
mock_process_detect_pipeline_now,
mock_push_debug_event,
) -> None:
mock_process_detect_pipeline_now.return_value = (
True,
"pipeline ok",
{"processed_items": 512},
)
ok, message, data = _run_pipeline_stage_processor()
self.assertTrue(ok)
self.assertEqual("pipeline ok", message)
self.assertEqual({"processed_items": 512}, data)
mock_process_detect_pipeline_now.assert_called_once_with(limit=5000)
self.assertEqual("pipeline_tick_success", mock_push_debug_event.call_args.kwargs["event_type"])
if __name__ == "__main__":
unittest.main()

View File

@@ -2,7 +2,10 @@ import unittest
from unittest.mock import patch
from app.services.sync_push_service import (
_build_task_pull_backlog_limits,
_extract_detect_result_projection_events,
_resolve_detect_result_target_job_id,
_should_throttle_task_pull,
ingest_runtime_projection,
)
@@ -46,6 +49,61 @@ class _FakeConnection:
class SyncPushServiceTests(unittest.TestCase):
def test_build_task_pull_backlog_limits_scales_with_thread_configuration(self) -> None:
limits = _build_task_pull_backlog_limits(
5000,
settings_payload={
"thread_count": 100,
"node_thread_counts": {
"mainland-controller-01": 2000,
"mainland-worker-01": 1200,
},
},
)
self.assertEqual(3200, limits["estimated_total_threads"])
self.assertEqual(6400, limits["max_pending_total"])
self.assertEqual(3200, limits["max_register_pending"])
self.assertEqual(800, limits["max_downstream_pending"])
def test_should_throttle_task_pull_when_register_backlog_overwhelms_downstream(self) -> None:
should_throttle, reason = _should_throttle_task_pull(
{
"pending_total": 9438,
"claimed_total": 410,
"running_total": 15,
"register_pending": 8487,
"downstream_pending": 951,
},
{
"max_pending_total": 12000,
"max_register_pending": 3200,
"max_downstream_pending": 800,
},
)
self.assertTrue(should_throttle)
self.assertEqual("register_pending", reason)
def test_should_not_throttle_task_pull_when_backlog_is_within_limits(self) -> None:
should_throttle, reason = _should_throttle_task_pull(
{
"pending_total": 1800,
"claimed_total": 100,
"running_total": 300,
"register_pending": 600,
"downstream_pending": 120,
},
{
"max_pending_total": 6400,
"max_register_pending": 3200,
"max_downstream_pending": 800,
},
)
self.assertFalse(should_throttle)
self.assertEqual("", reason)
def test_extract_detect_result_projection_events_adds_import_metadata(self) -> None:
projection = {
"job": {
@@ -79,6 +137,43 @@ class SyncPushServiceTests(unittest.TestCase):
self.assertTrue(event["payload"]["imported_from_projection"])
self.assertTrue(event["payload"]["import_fingerprint"])
@patch("app.services.sync_push_service.get_db")
def test_resolve_detect_result_target_job_id_prefers_matching_job_code(self, mock_get_db) -> None:
fake_conn = _FakeConnection(rows=[(456,)])
mock_get_db.return_value = fake_conn
target_job_id = _resolve_detect_result_target_job_id(
projection={
"job": {
"job_code": "sync-mainland-123",
}
}
)
self.assertEqual(456, target_job_id)
self.assertIn("WHERE job_code = %s", fake_conn.cursor_obj.executed[0][0])
@patch("app.services.detect_job_service.get_active_detect_job_summary")
@patch("app.services.sync_push_service.get_db")
def test_resolve_detect_result_target_job_id_falls_back_to_active_job(
self,
mock_get_db,
mock_get_active_detect_job_summary,
) -> None:
fake_conn = _FakeConnection(rows=[None])
mock_get_db.return_value = fake_conn
mock_get_active_detect_job_summary.return_value = {"job_id": 789}
target_job_id = _resolve_detect_result_target_job_id(
projection={
"job": {
"job_code": "sync-mainland-999",
}
}
)
self.assertEqual(789, target_job_id)
@patch("app.services.sync_push_service._import_detect_result_projection_events")
@patch("app.services.sync_push_service.get_db")
def test_ingest_runtime_projection_imports_detect_result_events_on_new_record(

View File

@@ -1,6 +1,10 @@
import unittest
from unittest.mock import patch
from app.services.sync_record_service import _collect_recent_domain_events
from app.services.sync_record_service import (
_build_runtime_projection_payload,
_collect_recent_domain_events,
)
class SyncRecordServiceTests(unittest.TestCase):
@@ -50,6 +54,82 @@ class SyncRecordServiceTests(unittest.TestCase):
self.assertEqual("domain_completed", events[1]["event_type"])
self.assertEqual("a.com", events[1]["payload"]["domain"])
@patch("app.services.sync_record_service._resolve_local_ip", return_value="152.53.37.118")
@patch("app.services.sync_record_service.socket.gethostname", return_value="overseas-control-01")
@patch("app.services.sync_record_service.settings")
def test_build_runtime_projection_payload_zeros_execution_for_overseas_control(
self,
mock_settings,
_mock_hostname,
_mock_resolve_ip,
) -> None:
mock_settings.node_code = "overseas-control-01"
mock_settings.node_region = "overseas"
mock_settings.node_role = "control"
mock_settings.sync_source_region = "overseas"
mock_settings.sync_target_region = "mainland"
payload = _build_runtime_projection_payload(
detect={
"worker_online": False,
"worker_mode": "linux-systemd",
"active_thread_count": 0,
"max_thread_count": 0,
"phase_label": "当前节点不承载",
"phase_detail": "当前节点为海外控制面,仅承载 API 控制与同步接收,不执行本机检测任务。",
"proxy_runtime_label": "不适用",
"proxy_runtime_reason": "not_applicable",
"progress": {
"pending": 125,
"running": 293,
"completed": 71,
"blacklisted": 0,
"failed": 1,
},
"active_job": {
"job_id": 2027,
"job_code": "sync-overseas-33501",
"status": "running",
"progress_percent": 7.2,
"items_total": 1000,
"items_terminal": 72,
"items_pending": 125,
"items_running": 293,
"items_failed": 1,
"node_stats": [{"node_code": "mainland-controller-01", "items_running": 293}],
},
},
cluster={
"nodes_total": 3,
"nodes": [
{
"node_code": "overseas-control-01",
"current_load": 293,
"detect_participating": True,
}
],
"summary": {
"online_worker_nodes": 3,
"dedicated_online_worker_nodes": 1,
"online_control_nodes": 2,
"busy_nodes": ["overseas-control-01"],
"stale_nodes": [],
"offline_nodes": [],
},
},
source_region="overseas",
target_region="mainland",
)
projection = payload["projection"]
self.assertFalse(projection["worker_online"])
self.assertFalse(projection["detect_participating"])
self.assertEqual(0, projection["active_thread_count"])
self.assertEqual(0, projection["max_thread_count"])
self.assertEqual(0, projection["progress"]["running"])
self.assertEqual("", projection["active_job"]["job_code"])
self.assertEqual([], projection["active_job"]["node_stats"])
if __name__ == "__main__":
unittest.main()