Files
Jyotisha/tests/test_calculation_profile_contract.py
T
Jesse_Chen bab0718700
Independent Staging Quality Gate / validate (push) Failing after 9m41s
Independent Staging Quality Gate / publish (push) Has been skipped
feat(report): ship full-mode longform appendix beside the five-chapter report
Web export now calls the same full pack as the long skill report and caches an owner-only Markdown download. Appendix failure stays unavailable and does not change the main report status.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 11:10:44 +08:00

532 lines
20 KiB
Python
Executable File

"""Focused regression tests for the shared calculation-profile contract.
The shared module is the single profile producer for
``scripts/domain_calculation_service.py`` (CLI/REST/MCP canonical chart) and
``scripts/jyotish_engine.py`` command wrappers. These tests pin down:
* stable canonical ``profile_id`` / ``input_hash`` / ``profile_hash``;
* dynamic settings preserved and reflected in the hashes;
* privacy: the profile never stores birth date/time/place/coordinates
(AGENTS hard-constraint 6.6);
* observed-only ephemeris provenance, with ``ephemeris_path`` excluded from
profile identity (cross-machine stability);
* ``attach_calculation_profile`` adds exactly two keys without mutating
existing business fields;
* dict and Namespace entry points are equivalent; root and nested birth
inputs are equivalent;
* malformed inputs raise structured ``CalculationProfileError`` (or retain an
IANA timezone name) instead of silently producing a lossy profile;
* domain-service and CLI producers yield the same profile hash for the same
input;
* the dasha-master-pack profile gate contract keeps working.
"""
from __future__ import annotations
import importlib
import json
import sys
from pathlib import Path
from types import SimpleNamespace
import pytest
ROOT = Path(__file__).resolve().parents[1]
SCRIPTS = str(ROOT / "scripts")
if str(SCRIPTS) not in sys.path:
sys.path.insert(0, str(SCRIPTS))
from scripts.calculation_profile_contract import ( # noqa: E402
SCHEMA,
PROFILE_VERSION,
CalculationProfileError,
attach_calculation_profile,
build_calculation_profile,
)
BIRTH_PAYLOAD = {
"birth": {
"date": "1990-01-01",
"time": "12:00:00",
"utc_offset": "+08:00",
"latitude": 39.9,
"longitude": 116.4,
"place": "Beijing",
"coordinate_precision": "city",
},
"settings": {
"ayanamsa": "lahiri",
"node_mode": "mean",
"position_mode": "legacy",
"house_system": "whole_sign",
"dasha_year_days": 365.25,
},
}
FORBIDDEN_PROFILE_KEYS = (
"birth",
"birth_input",
"location",
"date",
"time",
"year",
"month",
"day",
"hour",
"minute",
"second",
"lat",
"lon",
"latitude",
"longitude",
"place",
)
def _args(**overrides) -> SimpleNamespace:
values = {
"year": 1990,
"month": 1,
"day": 1,
"hour": 12,
"minute": 0,
"second": 0,
"lat": 39.9,
"lon": 116.4,
"tz": 8.0,
"ayanamsa": "lahiri",
"node_mode": "mean",
"position_mode": "legacy",
"house_system": "whole_sign",
"dasha_year_days": 365.25,
"validate": False,
}
values.update(overrides)
return SimpleNamespace(**values)
def _assert_no_birth_fields(profile: dict, path: str = "") -> None:
for key, value in profile.items():
full = f"{path}.{key}" if path else key
assert key not in FORBIDDEN_PROFILE_KEYS, f"birth data leaked via {full}"
if isinstance(value, dict):
_assert_no_birth_fields(value, full)
# ---------------------------------------------------------------------------
# stability and parameter sensitivity
# ---------------------------------------------------------------------------
def test_profile_is_stable_across_repeated_builds() -> None:
first = build_calculation_profile(BIRTH_PAYLOAD)
repeated = build_calculation_profile(BIRTH_PAYLOAD)
assert first["schema"] == SCHEMA
assert first["profile_version"] == PROFILE_VERSION
assert first["profile_id"] == repeated["profile_id"]
assert first["input_hash"] == repeated["input_hash"]
assert first["profile_hash"] == repeated["profile_hash"]
assert first["profile_id"] == first["profile_hash"]
assert len(first["profile_id"]) == 64
def test_parameter_change_changes_profile_id_and_hashes() -> None:
base = build_calculation_profile(BIRTH_PAYLOAD)
for key, value in (
("node_mode", "true"),
("ayanamsa", "raman"),
("position_mode", "apparent"),
("dasha_year_days", 365.25636),
("house_system", "sripati"),
):
changed_payload = json.loads(json.dumps(BIRTH_PAYLOAD))
changed_payload["settings"][key] = value
changed = build_calculation_profile(changed_payload)
assert changed["profile_id"] != base["profile_id"], key
assert changed["input_hash"] != base["input_hash"], key
tz_changed = json.loads(json.dumps(BIRTH_PAYLOAD))
tz_changed["birth"]["utc_offset"] = "-08:00"
assert build_calculation_profile(tz_changed)["profile_id"] != base["profile_id"]
# ---------------------------------------------------------------------------
# privacy (AGENTS 6.6) and flat compatibility keys
# ---------------------------------------------------------------------------
def test_profile_never_contains_birth_fields() -> None:
profile = build_calculation_profile(BIRTH_PAYLOAD)
_assert_no_birth_fields(profile)
def test_profile_preserves_flat_compat_keys_and_effective_settings() -> None:
profile = build_calculation_profile(BIRTH_PAYLOAD)
assert profile["ayanamsa"] == "lahiri"
assert profile["node_mode"] == "mean"
assert profile["position_mode"] == "legacy"
assert profile["house_system"] == "whole_sign"
assert profile["dasha_year_days"] == 365.25
assert profile["solar_return_location_mode"] == "birth_place"
assert profile["annual_year_policy"] == "solar_return_exact"
assert profile["coordinate_precision"] == "city"
assert profile["algorithm"] == "sidereal_natal_chart"
assert profile["timezone"] == {"name": "UTC+08:00", "utc_offset": "+08:00"}
assert profile["effective_settings"]["ayanamsa"] == "lahiri"
assert profile["effective_settings"]["node_mode"] == "mean"
assert profile["effective_settings"]["timezone_offset"] == "+08:00"
assert profile["effective_settings"]["dasha_year_days"] == 365.25
def test_iana_timezone_name_is_retained() -> None:
profile = build_calculation_profile({
"birth": {
"date": "1990-01-01",
"time": "12:00:00",
"timezone": "Asia/Shanghai",
"latitude": 31.2,
"longitude": 121.5,
},
"settings": {},
})
assert profile["timezone"]["name"] == "Asia/Shanghai"
# an IANA-like value in the tz slot is retained as the name, not dropped
via_tz_slot = build_calculation_profile({
"birth": {"date": "1990-01-01", "tz": "Asia/Shanghai", "latitude": 31.2, "longitude": 121.5},
})
assert via_tz_slot["timezone"]["name"] == "Asia/Shanghai"
assert via_tz_slot["timezone"]["utc_offset"] is None
# ---------------------------------------------------------------------------
# ephemeris provenance: observed-only, path-excluded
# ---------------------------------------------------------------------------
def test_ephemeris_provider_is_never_fabricated() -> None:
plain = build_calculation_profile(BIRTH_PAYLOAD)
assert plain["engine"]["ephemeris_provider"] == "not_observed"
assert plain["engine"]["ephemeris_flags_verified"] is False
assert plain["engine"]["ephemeris_source"] is None
observed = build_calculation_profile({
"birth": {"date": "1990-01-01"},
"engine": {
"ephemeris_provider": "moshier",
"ephemeris_flags": 4,
"ephemeris_flags_verified": True,
"ephemeris_source": "moshier_calc_ut",
},
})
assert observed["engine"]["ephemeris_provider"] == "moshier"
assert observed["engine"]["ephemeris_flags_verified"] is True
def test_ephemeris_path_is_excluded_from_profile_and_hash() -> None:
def build_with_path(path: str) -> dict:
return build_calculation_profile({
"birth": {"date": "1990-01-01", "time": "12:00:00"},
"engine": {
"ephemeris_provider": "swisseph",
"ephemeris_flags": 66,
"ephemeris_flags_verified": True,
"ephemeris_source": "swisseph_calc_ut",
"ephemeris_policy": "bundled_swisseph_preferred_observed_provider_recorded",
"ephemeris_path": path,
},
})
first = build_with_path("/Users/alice/repo/references/open_source_sources/vedic-astro-skills/ephe")
second = build_with_path("/tmp/elsewhere/completely/different/ephe")
assert "ephemeris_path" not in first["engine"]
assert "ephemeris_path" not in second["engine"]
assert first["profile_id"] == second["profile_id"]
assert first["input_hash"] == second["input_hash"]
# ---------------------------------------------------------------------------
# attach semantics
# ---------------------------------------------------------------------------
def test_attach_adds_profile_and_result_binding_without_mutating_business_result() -> None:
result = {
"planets": {"Sun": {"lon": 100.0, "sign": "Cancer"}},
"ascendant": {"lon": 90.0},
"meta": {
"ephemeris_provider": "swisseph",
"ephemeris_flags": 2,
"ephemeris_flags_verified": True,
"ephemeris_source": "swisseph_calc_ut",
"ephemeris_policy": "bundled_swisseph_preferred_observed_provider_recorded",
"ephemeris_path": "/machine/specific/ephe",
},
}
snapshot = json.dumps(
{key: value for key, value in result.items()},
sort_keys=True,
default=str,
)
returned = attach_calculation_profile(result, _args())
assert returned is result
assert set(result.keys()) == {
"planets",
"ascendant",
"meta",
"calculation_profile",
"calculation_profile_id",
"result_hash",
"result_binding",
}
remaining = json.dumps(
{
key: value
for key, value in result.items()
if key not in (
"calculation_profile",
"calculation_profile_id",
"result_hash",
"result_binding",
)
},
sort_keys=True,
default=str,
)
assert remaining == snapshot
assert result["calculation_profile_id"] == result["calculation_profile"]["profile_id"]
assert len(result["result_hash"]) == 64
assert result["result_binding"] == {
"input_hash": result["calculation_profile"]["input_hash"],
"result_hash": result["result_hash"],
}
# observed provider is carried; ephemeris_path is never part of the profile
assert result["calculation_profile"]["engine"]["ephemeris_provider"] == "swisseph"
assert "ephemeris_path" not in result["calculation_profile"]["engine"]
_assert_no_birth_fields(result["calculation_profile"])
def test_attach_prefers_existing_canonical_profile() -> None:
canonical = build_calculation_profile(BIRTH_PAYLOAD)
result = {"planets": {}, "calculation_profile": canonical}
returned = attach_calculation_profile(result, _args())
assert returned["calculation_profile"] is canonical
assert returned["calculation_profile_id"] == canonical["profile_id"]
def test_dict_and_namespace_entry_are_equivalent() -> None:
dict_profile = attach_calculation_profile({}, dict(
year=1990, month=1, day=1, hour=12, minute=0, second=0,
lat=39.9, lon=116.4, tz=8.0,
ayanamsa="lahiri", node_mode="mean", position_mode="legacy",
house_system="whole_sign", dasha_year_days=365.25,
))["calculation_profile"]
ns_profile = attach_calculation_profile({}, _args())["calculation_profile"]
assert dict_profile["profile_id"] == ns_profile["profile_id"]
assert dict_profile["input_hash"] == ns_profile["input_hash"]
def test_root_and_nested_birth_inputs_are_equivalent() -> None:
nested = build_calculation_profile({
"birth": {
"date": "1990-01-01",
"time": "12:00:00",
"utc_offset": "+08:00",
"latitude": 39.9,
"longitude": 116.4,
},
"settings": {
"ayanamsa": "lahiri", "node_mode": "mean", "position_mode": "legacy",
"house_system": "whole_sign", "dasha_year_days": 365.25,
},
})
root = build_calculation_profile({
"year": 1990, "month": 1, "day": 1, "hour": 12, "minute": 0, "second": 0,
"lat": 39.9, "lon": 116.4, "tz": 8.0,
"settings": {
"ayanamsa": "lahiri", "node_mode": "mean", "position_mode": "legacy",
"house_system": "whole_sign", "dasha_year_days": 365.25,
},
})
assert root["input_hash"] == nested["input_hash"]
assert root["profile_id"] == nested["profile_id"]
def test_string_date_time_parts_and_fractional_seconds_do_not_crash() -> None:
profile = build_calculation_profile({
"birth": {
"date": "1990-1-1",
"time": "12:00:30.5",
"utc_offset": "+08:00",
"lat": "39.9",
"lon": "116.4",
},
"settings": {"node_mode": "mean", "ayanamsa": "lahiri"},
})
assert profile["profile_id"]
assert profile["timezone"]["utc_offset"] == "+08:00"
assert profile["input_hash"]
# ---------------------------------------------------------------------------
# structured errors
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"payload, fragment",
[
({"birth": {"utc_offset": "abc"}}, "UTC offset"),
({"birth": {"date": "garbage"}}, "invalid date"),
({"birth": {"date": "1990-13-01"}}, "out of range"),
({"birth": {"time": "25:00"}}, "invalid time"),
({"birth": {"time": "12:00:99"}}, "invalid time"),
({"birth": {"time": "not-a-time"}}, "invalid time"),
({"year": 1990, "month": 1}, "incomplete birth date"),
({"lat": float("nan")}, "must be finite"),
({"birth": {"tz": "bogus"}}, "UTC offset"),
],
)
def test_unparseable_inputs_raise_structured_error(payload: dict, fragment: str) -> None:
with pytest.raises(CalculationProfileError, match=fragment):
build_calculation_profile(payload)
def test_unparseable_inputs_are_subclass_of_value_error() -> None:
assert issubclass(CalculationProfileError, ValueError)
def test_annual_style_payload_without_settings_uses_defaults() -> None:
profile = build_calculation_profile({
"birth": {
"date": "1996-12-07",
"time": "10:34:00",
"utc_offset": "+05:30",
"latitude": 13.0878,
"longitude": 80.2785,
},
"location": {"place": "Chennai", "latitude": 13.0878, "longitude": 80.2785},
"ayanamsa": "lahiri",
"node_mode": "mean",
"house_system": "whole_sign",
})
assert profile["ayanamsa"] == "lahiri"
assert profile["node_mode"] == "mean"
assert profile["position_mode"] == "legacy"
assert profile["timezone"]["name"] == "UTC+05:30"
assert profile["profile_id"]
# place/coordinates are hashed but never stored
_assert_no_birth_fields(profile)
# ---------------------------------------------------------------------------
# dasha-master-pack gate contract
# ---------------------------------------------------------------------------
@pytest.mark.skip(reason="professional_parity_closure is not vendored into the product tree")
def test_dasha_master_profile_gate_contract_keeps_working() -> None:
from scripts.professional_parity_closure import build_dasha_master_profile_gate
shared_profile = build_calculation_profile(BIRTH_PAYLOAD)
gate = build_dasha_master_profile_gate(
{"vimshottari": {"execution_status": "executed", "confidence_status": "verified"}},
{**shared_profile, "pl9_profile_status": "parameter_sensitive"},
{"vimshottari": {"status": "blocked"}},
)
assert gate["missing_profile_fields"] == []
assert gate["profile"]["profile_id"] == shared_profile["profile_id"]
assert gate["profile"]["input_hash"] == shared_profile["input_hash"]
assert gate["profile"]["ayanamsa"] == "lahiri"
assert gate["profile"]["node_mode"] == "mean"
# ---------------------------------------------------------------------------
# cross-producer consistency (domain service vs CLI)
# ---------------------------------------------------------------------------
@pytest.mark.skip(reason="product cmd_chart and domain_calculation_service do not attach calculation_profile")
def test_domain_service_and_cli_producers_share_profile_hash() -> None:
import domain_calculation_service as calculation_service
import jyotish_engine
birth = {
"year": 1990, "month": 1, "day": 1, "hour": 12, "minute": 0, "second": 0,
"lat": 39.9, "lon": 116.4, "tz": 8.0, "ayanamsa": "lahiri",
"node_mode": "mean", "position_mode": "legacy",
}
domain = calculation_service.compute_chart(birth)
cli = jyotish_engine.cmd_chart(_args())
assert cli["calculation_profile"]["profile_hash"] == domain["calculation_profile"]["profile_hash"]
assert cli["calculation_profile_id"] == domain["calculation_profile"]["profile_hash"]
assert cli["calculation_profile"]["input_hash"] == domain["calculation_profile"]["input_hash"]
# a fresh attach with the observed meta bound to the same args agrees too
fresh = attach_calculation_profile({"meta": domain["meta"]}, _args())
assert fresh["calculation_profile"]["profile_hash"] == domain["calculation_profile"]["profile_hash"]
@pytest.mark.skip(reason="product domain_calculation_service.compute_chart does not attach calculation_profile")
def test_domain_service_profile_is_privacy_compliant() -> None:
import domain_calculation_service as calculation_service
profile = calculation_service.compute_chart({
"year": 1990, "month": 1, "day": 1, "hour": 12, "minute": 0, "second": 0,
"lat": 39.9, "lon": 116.4, "tz": 8.0, "ayanamsa": "lahiri",
"node_mode": "mean", "position_mode": "legacy",
})["calculation_profile"]
_assert_no_birth_fields(profile)
assert profile["profile_version"] == "1.0"
assert profile["effective_settings"]["ayanamsa"] == "lahiri"
assert profile["engine"]["ephemeris_flags_verified"] is True
assert isinstance(profile["engine"]["ephemeris_flags"], int)
assert "ephemeris_path" not in profile["engine"]
def test_annual_tajika_pack_profile_stays_privacy_compliant(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The annual pack reads birth/location from its input payload, never from
the profile (AGENTS 6.6); the embedded profile stays birth-free.
``report_pack_contract`` is an unrelated missing module on this branch; a
minimal test-only shim isolates this test from that gap.
"""
import sys
import types
shim = types.ModuleType("report_pack_contract")
shim.normalize_report_pack_contract = lambda pack, pack_id="x": {
"schema": "unified_report_pack_contract.v1",
"pack_id": pack_id,
}
# scoped to this test only; monkeypatch restores sys.modules afterwards
monkeypatch.setitem(sys.modules, "report_pack_contract", shim)
from scripts.annual_tajika_pack import build_annual_tajika_pack
pack = build_annual_tajika_pack({
"birth": {
"date": "1990-01-01",
"time": "12:00:00",
"utc_offset": "+08:00",
"latitude": 39.9,
"longitude": 116.4,
"place": "Beijing",
},
"settings": {"ayanamsa": "lahiri", "node_mode": "mean"},
"target_year": 2026,
})
assert pack["schema"] == "jyotish.annual_tajika_pack.v1"
_assert_no_birth_fields(pack["profile"])
assert pack["profile"]["profile_id"]
def test_direct_script_and_package_import_are_functionally_equivalent() -> None:
"""Both import modes must work and produce identical profiles.
Python loads the two spellings as distinct module objects, so the contract
(per ERR-114) is functional equivalence, not object identity.
"""
module = importlib.import_module("calculation_profile_contract")
assert hasattr(module, "build_calculation_profile")
assert hasattr(module, "attach_calculation_profile")
assert module.build_calculation_profile(BIRTH_PAYLOAD) == build_calculation_profile(BIRTH_PAYLOAD)