"""Issue #64: skip useless report subprocesses and observe the heavy-compute gate.""" from __future__ import annotations import json import logging from pathlib import Path import pytest from scripts import api_heavy_compute_gate as gate from scripts.annual_pyjhora_replay import ( _run_isolated_annual_replay, _run_isolated_tajaka_year_lord, reset_jhora_import_cache, ) from scripts.jyotish_engine import ( _oracle_progress_snapshot, reset_oracle_progress_snapshot_cache, ) from scripts.tajika import ( _resolve_external_pyjhora_python, reset_external_pyjhora_python_cache, ) ROOT = Path(__file__).resolve().parents[1] FIXTURES = ROOT / "tests" / "fixtures" / "api-chart-subprocess-gate" class _Place: name = "Delhi" latitude = 28.6139 longitude = 77.209 timezone = 5.5 elevation = 0 _PROFILE = {"ayanamsa": "LAHIRI", "node_mode": "mean"} def _without_hash(payload: dict) -> dict: body = dict(payload) body.pop("request_hash", None) return body def test_no_jhora_tajaka_skips_subprocess_and_matches_golden(monkeypatch) -> None: reset_jhora_import_cache() monkeypatch.setattr("scripts.annual_pyjhora_replay._jhora_importable", lambda: False) def _forbid_subprocess(*_args, **_kwargs): raise AssertionError("subprocess.run must not run when jhora cannot be imported") monkeypatch.setattr("scripts.annual_pyjhora_replay.subprocess.run", _forbid_subprocess) result = _run_isolated_tajaka_year_lord(_PROFILE, 2447893.0, _Place(), 30) golden = json.loads((FIXTURES / "tajaka-no-jhora.json").read_text(encoding="utf-8")) assert result["request_hash"] assert _without_hash(result) == golden def test_no_jhora_annual_skips_subprocess_and_matches_golden(monkeypatch) -> None: reset_jhora_import_cache() monkeypatch.setattr("scripts.annual_pyjhora_replay._jhora_importable", lambda: False) def _forbid_subprocess(*_args, **_kwargs): raise AssertionError("subprocess.run must not run when jhora cannot be imported") monkeypatch.setattr("scripts.annual_pyjhora_replay.subprocess.run", _forbid_subprocess) result = _run_isolated_annual_replay(_PROFILE, 2447893.0, _Place(), 30) golden = json.loads((FIXTURES / "annual-no-jhora.json").read_text(encoding="utf-8")) assert result["request_hash"] assert _without_hash(result) == golden def test_jhora_available_still_uses_subprocess(monkeypatch) -> None: reset_jhora_import_cache() monkeypatch.setattr("scripts.annual_pyjhora_replay._jhora_importable", lambda: True) calls = {"n": 0} class _Completed: returncode = 0 stdout = json.dumps({"status": "partial_verified", "settings": {}}) def _fake_run(*_args, **_kwargs): calls["n"] += 1 return _Completed() monkeypatch.setattr("scripts.annual_pyjhora_replay.subprocess.run", _fake_run) tajaka = _run_isolated_tajaka_year_lord(_PROFILE, 2447893.0, _Place(), 30) annual = _run_isolated_annual_replay(_PROFILE, 2447893.0, _Place(), 30) assert calls["n"] == 2 assert tajaka["request_hash"] assert annual["request_hash"] def test_pyjhora_interpreter_lookup_is_cached(monkeypatch) -> None: reset_external_pyjhora_python_cache() calls = {"n": 0} def _missing(): calls["n"] += 1 return None monkeypatch.setattr("scripts.tajika._lookup_external_pyjhora_python", _missing) assert _resolve_external_pyjhora_python() is None assert _resolve_external_pyjhora_python() is None assert calls["n"] == 1 def test_oracle_progress_snapshot_reuses_until_the_file_changes(monkeypatch) -> None: reset_oracle_progress_snapshot_cache() calls = {"n": 0} real_run = __import__("subprocess").run def _counting_run(*args, **kwargs): calls["n"] += 1 return real_run(*args, **kwargs) monkeypatch.setattr("subprocess.run", _counting_run) first = _oracle_progress_snapshot() after_first = calls["n"] assert after_first >= 1 second = _oracle_progress_snapshot() assert calls["n"] == after_first assert second == first oracle = ROOT / "references" / "oracle" / "dasha_shadbala_oracle_cases.json" stat = oracle.stat() later = stat.st_mtime + 5 try: oracle.touch() import os os.utime(oracle, (later, later)) third = _oracle_progress_snapshot() assert calls["n"] > after_first assert third["scope"] == first["scope"] finally: os_utime = __import__("os").utime os_utime(oracle, (stat.st_atime, stat.st_mtime)) reset_oracle_progress_snapshot_cache() def test_chart_overview_gate_is_off_by_default(monkeypatch) -> None: monkeypatch.delenv(gate.ENV_GATE_CHART_OVERVIEW, raising=False) gate.reset_heavy_compute_gate(limit=1) held = gate.acquire_heavy_compute_slot("/api/high_rigor_workflow") assert held is not None assert gate.acquire_heavy_compute_slot_for_request("/api/chart", {"year": 1990}) is None gate.release_heavy_compute_slot(held) assert gate.heavy_compute_status()["in_use"] == 0 gate.reset_heavy_compute_gate() def test_chart_overview_gate_only_sync_unskipped_requests(monkeypatch) -> None: monkeypatch.setenv(gate.ENV_GATE_CHART_OVERVIEW, "1") # 原值:reset(limit=1) 后第二次立刻 HeavyComputeBusy # 新值:wait_seconds=0 # 原因:默认改为短排队;本测试仍锁「名额被占时同步排盘被拒绝」,等待行为不在这里 gate.reset_heavy_compute_gate(limit=1, wait_seconds=0) body = {"year": 1990, "lat": 28.6, "lon": 77.2} slot = gate.acquire_heavy_compute_slot_for_request("/api/chart", body) assert slot is not None assert gate.heavy_compute_status() == {"limit": 1, "in_use": 1} with pytest.raises(gate.HeavyComputeBusy): gate.acquire_heavy_compute_slot_for_request("/api/chart", {"year": 1991}) gate.release_heavy_compute_slot(slot) assert gate.heavy_compute_status()["in_use"] == 0 assert gate.acquire_heavy_compute_slot_for_request( "/api/chart", {"skip_vedastro_main_entry_overview": True} ) is None assert gate.acquire_heavy_compute_slot_for_request("/api/chart", {"async": True}) is None assert gate.acquire_heavy_compute_slot_for_request("/api/chart", {"enqueue": 1}) is None assert "/api/chart" not in gate.HEAVY_COMPUTE_PATHS gate.reset_heavy_compute_gate() def test_busy_log_names_holders_and_not_the_body(caplog) -> None: # 原值:reset(limit=1) 后拒绝日志立刻出现 # 新值:wait_seconds=0 # 原因:默认会先等 8 秒;本测试仍锁日志只有路径、没有请求正文 gate.reset_heavy_compute_gate(limit=1, wait_seconds=0) held = gate.acquire_heavy_compute_slot("/api/professional_report_reference") assert held is not None secret_body = {"birth_date": "1990-01-01", "name": "must-not-appear"} with caplog.at_level(logging.WARNING, logger="jyotish.heavy_compute"): with pytest.raises(gate.HeavyComputeBusy): gate.acquire_heavy_compute_slot_for_request("/api/professional_report_reference", secret_body) text = caplog.text assert "/api/professional_report_reference" in text assert "1990-01-01" not in text assert "must-not-appear" not in text gate.release_heavy_compute_slot(held) assert gate.heavy_compute_status()["in_use"] == 0 gate.reset_heavy_compute_gate() def test_warm_consultation_still_skips_the_slot() -> None: gate.reset_heavy_compute_gate(limit=1) assert gate.acquire_heavy_compute_slot_for_request( "/api/consultation_workflow", {"consult_card_warm_only": True} ) is None assert gate.heavy_compute_status()["in_use"] == 0 gate.reset_heavy_compute_gate()