Files
Jyotisha/tests/test_api_chart_subprocess_gate.py
T
jesse-uxandGrok (agent loop) c644881a30 feat(report): 完整数据版末尾增加独立年度方法参考
PyJHora 只在子进程里计算请求年及后两年。对话、校正和其余章节保持不导入时的结果。结果指纹不算这一章。

Windows 自测:隐私标记、导入扫描、结果指纹和 S7 退出码 0。阅读页、英文和年运章在此之前已通过。虚构盘 0 中英一起最高 59 秒。

Co-Authored-By: Grok (agent loop)
2026-10-23 20:13:28 +08:00

241 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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:
# 原值:_jhora_importable 为真时两个隔离函数各起一次子进程(calls == 2)
# 新值:产品调用不传 research 时,即使可导入也不起子进程、返回 golden;传 research=True 时仍起子进程(calls == 2)
# 原因:#93 红线 1:装包后旧回放不得进年运章
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()
def _forbid_subprocess(*_args, **_kwargs):
raise AssertionError("product replay must not spawn a child")
monkeypatch.setattr("scripts.annual_pyjhora_replay.subprocess.run", _forbid_subprocess)
tajaka = _run_isolated_tajaka_year_lord(_PROFILE, 2447893.0, _Place(), 30)
annual = _run_isolated_annual_replay(_PROFILE, 2447893.0, _Place(), 30)
assert calls["n"] == 0
tajaka_golden = json.loads((FIXTURES / "tajaka-no-jhora.json").read_text(encoding="utf-8"))
annual_golden = json.loads((FIXTURES / "annual-no-jhora.json").read_text(encoding="utf-8"))
assert _without_hash(tajaka) == tajaka_golden
assert _without_hash(annual) == annual_golden
monkeypatch.setattr("scripts.annual_pyjhora_replay.subprocess.run", _fake_run)
_run_isolated_tajaka_year_lord(_PROFILE, 2447893.0, _Place(), 30, research=True)
_run_isolated_annual_replay(_PROFILE, 2447893.0, _Place(), 30, research=True)
assert calls["n"] == 2
def test_product_replay_calls_a_four_arg_stand_in(monkeypatch) -> None:
"""The 200-chart sweep replaces the runners with four-argument functions."""
from scripts.annual_pyjhora_replay import (
_no_jhora_annual_replay,
_no_jhora_tajaka_replay,
replay_pyjhora_annual,
)
calls = []
def blocked(profile, birth_julian_day, place, age):
calls.append(age)
settings = {"ayanamsa": "LAHIRI", "node_mode": "mean"}
result = _no_jhora_tajaka_replay(settings) if len(calls) == 1 else _no_jhora_annual_replay(settings)
result["request_hash"] = "stand-in"
return result
monkeypatch.setattr("scripts.annual_pyjhora_replay._run_isolated_tajaka_year_lord", blocked)
monkeypatch.setattr("scripts.annual_pyjhora_replay._run_isolated_annual_replay", blocked)
result = replay_pyjhora_annual(_PROFILE, birth_julian_day=2447893.0, place=_Place(), age=30)
assert calls == [30, 30]
assert result["status"] == "blocked"
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()