harden calculation contracts and local API boundaries

This commit is contained in:
732642856
2026-07-11 23:24:54 +08:00
parent 4a268c75f2
commit a8f6a57ccc
17 changed files with 1075 additions and 157 deletions
+16
View File
@@ -8,6 +8,16 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPTS = str(ROOT / "scripts")
WORKBUDDY_SKILL_SCRIPTS = ".workbuddy/skills/jyotish-vedic-astrology/scripts"
SLOW_API_SECURITY_PREFIXES = (
"test_vedastro_",
"test_high_rigor_",
"test_professional_reading",
"test_api_prompt_pack",
"test_consultation_workflow",
"test_thematic_report",
"test_capability_audit",
"test_technique_catalog",
)
def ensure_project_scripts_first() -> None:
@@ -28,4 +38,10 @@ def pytest_runtest_setup() -> None:
ensure_project_scripts_first()
def pytest_collection_modifyitems(items) -> None:
for item in items:
if item.fspath.basename == "test_api_server_security.py" and item.name.startswith(SLOW_API_SECURITY_PREFIXES):
item.add_marker("slow")
ensure_project_scripts_first()
+6 -3
View File
@@ -134,7 +134,10 @@ class _HighRigorJobCaptureHandler(JyotishAPIHandler):
class _PostCaptureHandler(JyotishAPIHandler):
def __init__(self, path: str, payload: dict) -> None:
raw = json.dumps(payload).encode('utf-8')
self.headers = _FakeHeaders({'Content-Length': str(len(raw))})
self.headers = _FakeHeaders({
'Content-Length': str(len(raw)),
'Content-Type': 'application/json',
})
self.server = _FakeServer()
self.path = path
self.rfile = BytesIO(raw)
@@ -3377,7 +3380,7 @@ def test_chart_async_submit_returns_job_id(monkeypatch: pytest.MonkeyPatch) -> N
def test_high_rigor_job_poll_endpoint_returns_cached_job_payload(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(jyotish_api_server, '_load_high_rigor_job_record', lambda job_id: {
monkeypatch.setattr(jyotish_api_server, '_load_high_rigor_job_record', lambda job_id, **_kwargs: {
'success': True,
'endpoint': 'high_rigor_workflow_async',
'mode': 'async_result',
@@ -3397,7 +3400,7 @@ def test_high_rigor_job_poll_endpoint_returns_cached_job_payload(monkeypatch: py
def test_chart_job_poll_endpoint_returns_cached_job_payload(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(jyotish_api_server, '_load_async_job_record', lambda scope, job_id: {
monkeypatch.setattr(jyotish_api_server, '_load_async_job_record', lambda scope, job_id, **_kwargs: {
'success': True,
'endpoint': 'chart_async',
'mode': 'async_result',
+117
View File
@@ -0,0 +1,117 @@
from __future__ import annotations
import sys
from datetime import datetime
from pathlib import Path
from types import SimpleNamespace
import pytest
SCRIPTS = Path(__file__).resolve().parents[1] / "scripts"
if str(SCRIPTS) not in sys.path:
sys.path.insert(0, str(SCRIPTS))
import domain_calculation_service as calculation_service # noqa: E402
import jyotish_api_server # noqa: E402
from jyotish_api_server import JyotishAPIHandler # noqa: E402
from jyotish_engine import _compute_chart_from_args # noqa: E402
BIRTH = {
"year": 1990,
"month": 1,
"day": 1,
"hour": 12,
"minute": 0,
"second": 0,
"lat": 28.6139,
"lon": 77.2090,
"tz": 5.5,
"ayanamsa": "lahiri",
}
def test_true_node_changes_effective_rahu_and_contract() -> None:
mean = calculation_service.compute_chart({**BIRTH, "node_mode": "mean"})
true = calculation_service.compute_chart({**BIRTH, "node_mode": "true"})
assert mean["planets"]["Rahu"]["lon"] != pytest.approx(
true["planets"]["Rahu"]["lon"], abs=1e-8
)
assert mean["calculation_contract"]["effective"]["node_mode"] == "mean"
assert true["calculation_contract"]["effective"]["node_mode"] == "true"
assert mean["result_hash"] != true["result_hash"]
def test_vimshottari_uses_birth_balance_as_canonical_timeline() -> None:
birth_dt = datetime(1990, 1, 1, 12, 0)
result = calculation_service.compute_vimshottari_timeline(
birth_dt=birth_dt,
moon_lon=100.0,
current_date=birth_dt,
)
first = result["periods"][0]
assert first["lord"] == "Saturn"
assert first["start"] == "1980-07-02"
assert first["end"] == "1999-07-02"
assert result["birth_balance"]["remaining_years"] == pytest.approx(9.5)
assert result["calculation_contract"]["algorithm"] == "vimshottari_birth_balance"
def test_sade_sati_uses_real_saturn_transit_for_reference_date() -> None:
result = calculation_service.compute_sade_sati(
moon_degree=300.0,
asc_degree=330.0,
reference_date="2026-07-11",
tz=5.5,
ayanamsa="lahiri",
)
oracle = calculation_service.compute_transit_longitude(
planet="Saturn",
reference_date="2026-07-11",
tz=5.5,
ayanamsa="lahiri",
)
assert result["transit_saturn_lon"] == pytest.approx(oracle["longitude"], abs=1e-8)
assert result["provenance"]["data_layer"] == "true_transit_positions"
assert result["provenance"]["reference_date"] == "2026-07-11"
def test_timezone_inference_fails_closed(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
calculation_service,
"_lookup_timezone_name",
lambda _lat, _lon: None,
)
with pytest.raises(calculation_service.TimezoneInferenceError, match="timezone inference"):
calculation_service.infer_timezone_offset(
lat=0.0,
lon=0.0,
local_datetime=datetime(1990, 1, 1, 12, 0),
)
def test_chart_hash_matches_domain_cli_and_rest(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("JYOTISH_API_CHART_CACHE_TTL_SECONDS", "0")
monkeypatch.setenv("VEDASTRO_ENABLE_NETWORK", "0")
monkeypatch.setattr(
jyotish_api_server,
"_attach_vedastro_main_entry_overview",
lambda result, _birth: result,
)
expected = calculation_service.compute_chart({**BIRTH, "node_mode": "true"})
cli, _asc_idx, _jd, _ayanamsa = _compute_chart_from_args(
SimpleNamespace(**BIRTH, node_mode="true")
)
rest = JyotishAPIHandler.__new__(JyotishAPIHandler)._compute_chart_sync(
{**BIRTH, "node_mode": "true", "transit_date": "2026-07-11"}
)
assert cli["result_hash"] == expected["result_hash"]
assert rest["result_hash"] == expected["result_hash"]
assert rest["birth"]["node_mode"] == "true"
assert rest["calculation_contract"]["effective"]["node_mode"] == "true"
+156
View File
@@ -0,0 +1,156 @@
from __future__ import annotations
import hashlib
import sys
import time
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
SCRIPTS = ROOT / "scripts"
if str(SCRIPTS) not in sys.path:
sys.path.insert(0, str(SCRIPTS))
import jyotish_api_server as api # noqa: E402
import report_builder # noqa: E402
class _Headers(dict):
def get(self, key, default=None):
return super().get(key, default)
class _Server:
allowed_origins = {"http://localhost:3456"}
server_address = ("127.0.0.1", 5200)
def _handler(headers: dict[str, str]):
handler = api.JyotishAPIHandler.__new__(api.JyotishAPIHandler)
handler.headers = _Headers(headers)
handler.server = _Server()
return handler
def test_untrusted_origin_is_rejected_before_post_side_effects() -> None:
handler = _handler(
{
"Origin": "https://evil.example",
"Host": "127.0.0.1:5200",
"Content-Type": "application/json",
}
)
with pytest.raises(api.Forbidden, match="Origin"):
handler._enforce_request_security(require_json=True)
def test_post_requires_json_content_type() -> None:
handler = _handler(
{
"Origin": "http://localhost:3456",
"Host": "127.0.0.1:5200",
"Content-Type": "text/plain",
}
)
with pytest.raises(api.UnsupportedMediaType):
handler._enforce_request_security(require_json=True)
@pytest.mark.parametrize(
"url",
[
"https://example.com/image.png",
"http://127.0.0.1:8080/private",
"file:///etc/passwd",
"ftp://example.com/file",
],
)
def test_report_renderer_blocks_external_and_local_resources(url: str) -> None:
assert report_builder.is_allowed_report_resource_url(
url,
report_url="file:///tmp/report.html",
) is False
def test_report_renderer_allows_only_document_and_embedded_resources() -> None:
assert report_builder.is_allowed_report_resource_url(
"file:///tmp/report.html",
report_url="file:///tmp/report.html",
) is True
assert report_builder.is_allowed_report_resource_url(
"data:image/png;base64,AA==",
report_url="file:///tmp/report.html",
) is True
def test_async_job_identity_is_random_and_capability_protected(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
monkeypatch.setattr(api, "_async_job_dir", lambda _scope: tmp_path)
first = api._new_async_job_identity("chart")
second = api._new_async_job_identity("chart")
assert first["job_id"] != second["job_id"]
assert len(first["job_id"].split("_", 1)[1]) >= 32
assert first["access_token"] != second["access_token"]
record = {
"job_id": first["job_id"],
"status": "queued",
"access_token_hash": hashlib.sha256(first["access_token"].encode()).hexdigest(),
"expires_at_unix": time.time() + 60,
}
api._write_async_job_record("chart", first["job_id"], record)
assert api._load_async_job_record(
"chart", first["job_id"], access_token=first["access_token"]
)["status"] == "queued"
with pytest.raises(api.JobAccessDenied):
api._load_async_job_record("chart", first["job_id"], access_token="wrong")
def test_expired_async_job_is_deleted(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
monkeypatch.setattr(api, "_async_job_dir", lambda _scope: tmp_path)
identity = api._new_async_job_identity("chart")
api._write_async_job_record(
"chart",
identity["job_id"],
{
"job_id": identity["job_id"],
"access_token_hash": hashlib.sha256(identity["access_token"].encode()).hexdigest(),
"expires_at_unix": time.time() - 1,
},
)
assert api._load_async_job_record(
"chart", identity["job_id"], access_token=identity["access_token"]
) is None
assert not (tmp_path / f"{identity['job_id']}.json").exists()
def test_authenticated_frontend_does_not_use_local_storage_api_base() -> None:
auth_source = (ROOT / "jyotish-app" / "auth.js").read_text(encoding="utf-8")
chat_source = (ROOT / "jyotish-app" / "ai-chat.js").read_text(encoding="utf-8")
assert "localStorage.getItem(API_BASE_KEY)" not in auth_source
assert "localStorage.getItem('jyotish_api_base')" not in chat_source
def test_frontend_async_poll_uses_ephemeral_job_capability() -> None:
bridge_source = (ROOT / "jyotish-app" / "api-bridge.js").read_text(encoding="utf-8")
assert "pollAsyncJob(data, { base })" in bridge_source
assert "Authorization: `Bearer ${job.access_token}`" in bridge_source
assert "sessionStorage.setItem('jyotish_job" not in bridge_source
def test_background_job_queue_rejects_when_capacity_is_full(
monkeypatch: pytest.MonkeyPatch,
) -> None:
class _FullCapacity:
def acquire(self, blocking=False):
return False
monkeypatch.setattr(api, "_ASYNC_JOB_CAPACITY", _FullCapacity())
with pytest.raises(api.JobQueueFull):
api._submit_background_job(lambda: None)
+26
View File
@@ -0,0 +1,26 @@
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPTS = ROOT / "scripts"
if str(SCRIPTS) not in sys.path:
sys.path.insert(0, str(SCRIPTS))
from three_engine_parity_runner import build_public_case_replay # noqa: E402
def test_public_same_chart_replay_never_promotes_missing_vedastro_raw(tmp_path: Path) -> None:
report = build_public_case_replay(output_dir=tmp_path, allow_vedastro_network=False)
assert report["case_id"] == "steve_jobs_public_1955_lahiri"
assert report["birth_data_policy"] == "public_case_only"
assert report["engines"]["PyJHora_JHora"]["status"] == "raw_imported"
assert report["engines"]["jyotishganit"]["status"] == "raw_captured"
assert report["engines"]["VedAstro"]["status"] == "blocked"
assert report["status"] in {"partial", "blocked"}
assert report["tested"] is False
assert report["comparison_rows"]
assert all(row["status"] in {"blocked", "not_comparable"} for row in report["comparison_rows"])