"""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)