Rebind after sanitize, write an explicit binding_scope, and make the quality gate recompute coverage. Wall-clock fields stay in the packet but out of the hash.
633 lines
24 KiB
Python
Executable File
633 lines
24 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,
|
|
ALLOWED_BINDING_EXCLUDED_PATHS,
|
|
ALLOWED_BINDING_EXCLUDED_TOP_KEYS,
|
|
RESULT_BINDING_ENVELOPE_KEYS,
|
|
CalculationProfileError,
|
|
attach_calculation_profile,
|
|
bind_result_to_profile,
|
|
build_calculation_profile,
|
|
default_result_binding_scope,
|
|
hash_bound_result,
|
|
)
|
|
|
|
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
|
|
# 原值: {"input_hash", "result_hash"}
|
|
# 新值: 增加 binding_scope.excluded_top_keys / excluded_paths
|
|
# 原因: BUG-694 排除集必须显式落在回执里并被质量门校验
|
|
assert result["result_binding"] == {
|
|
"input_hash": result["calculation_profile"]["input_hash"],
|
|
"result_hash": result["result_hash"],
|
|
"binding_scope": default_result_binding_scope(),
|
|
}
|
|
# 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 hasattr(module, "bind_result_to_profile")
|
|
assert module.build_calculation_profile(BIRTH_PAYLOAD) == build_calculation_profile(BIRTH_PAYLOAD)
|
|
|
|
|
|
REQUIRED_DELIVERED_KEYS = (
|
|
"full_report_pack",
|
|
"chart_identity",
|
|
"timing_precision_contract",
|
|
"birth_provenance",
|
|
"rectification_evidence_contract",
|
|
)
|
|
|
|
|
|
def _delivered_packet(**overrides: object) -> dict:
|
|
profile = build_calculation_profile(BIRTH_PAYLOAD)
|
|
packet: dict = {
|
|
"full_report_pack": {"schema": "pl9.full_report_pack.v1", "sections": {"base": {"status": "verified"}}},
|
|
"chart_identity": {"chart_profile_id": profile["profile_id"], "rectification_status": "not_reviewed"},
|
|
"timing_precision_contract": {"claim_status": "observation_only"},
|
|
"birth_provenance": {"source": "approximate"},
|
|
"rectification_evidence_contract": {"status": "present"},
|
|
"coverage": {"houses": ["D1"]},
|
|
"generated_at": "2026-09-15T00:00:00Z",
|
|
"report_quality_gate": {"status": "passed"},
|
|
"shared_full_report_authority": {"result_hash": "stale"},
|
|
"ai_and_audit": {
|
|
"summary": {
|
|
"elapsed_seconds": 0.9448,
|
|
"modules": 3,
|
|
"stage_timings": [{"name": "chart", "elapsed_seconds": 0.11}],
|
|
},
|
|
"ai_prompt_pack": {"evidence_snapshot": {"source_metadata": {"called_at": "2026-09-15T00:00:00Z"}}},
|
|
},
|
|
"calculation_profile": profile,
|
|
"calculation_profile_id": profile["profile_id"],
|
|
}
|
|
packet.update(overrides)
|
|
return packet
|
|
|
|
|
|
def test_bind_covers_every_non_excluded_delivered_key() -> None:
|
|
packet = _delivered_packet()
|
|
bound = bind_result_to_profile(packet, packet["calculation_profile"])
|
|
scope = bound["result_binding"]["binding_scope"]
|
|
assert set(scope["excluded_top_keys"]) == set(ALLOWED_BINDING_EXCLUDED_TOP_KEYS)
|
|
assert set(scope["excluded_paths"]) == set(ALLOWED_BINDING_EXCLUDED_PATHS)
|
|
hashed_keys = [
|
|
key for key in bound
|
|
if key not in RESULT_BINDING_ENVELOPE_KEYS and key not in scope["excluded_top_keys"]
|
|
]
|
|
assert hash_bound_result(bound, bound["calculation_profile"]["input_hash"], scope) == bound["result_hash"]
|
|
for key in REQUIRED_DELIVERED_KEYS:
|
|
assert key in hashed_keys
|
|
mutated = json.loads(json.dumps(bound))
|
|
mutated[key] = {"mutated": True}
|
|
assert hash_bound_result(
|
|
mutated, bound["calculation_profile"]["input_hash"], scope,
|
|
) != bound["result_hash"]
|
|
|
|
|
|
def test_same_input_result_hash_ignores_wall_clock_and_moves_with_business_fields() -> None:
|
|
profile = build_calculation_profile(BIRTH_PAYLOAD)
|
|
first = bind_result_to_profile(_delivered_packet(), profile)
|
|
second = bind_result_to_profile(
|
|
_delivered_packet(ai_and_audit={
|
|
"summary": {
|
|
"elapsed_seconds": 0.9003,
|
|
"modules": 3,
|
|
"stage_timings": [{"name": "chart", "elapsed_seconds": 0.40}],
|
|
},
|
|
"ai_prompt_pack": {"evidence_snapshot": {"source_metadata": {"called_at": "2026-09-15T00:00:01Z"}}},
|
|
}),
|
|
profile,
|
|
)
|
|
assert first["result_hash"] == second["result_hash"]
|
|
third = bind_result_to_profile(
|
|
_delivered_packet(coverage={"houses": ["D1", "D9"]}),
|
|
profile,
|
|
)
|
|
assert third["result_hash"] != first["result_hash"]
|
|
assert first["ai_and_audit"]["summary"]["elapsed_seconds"] == 0.9448
|
|
|
|
|
|
def test_professional_reference_packet_rebinds_sanitized_delivery() -> None:
|
|
source = (ROOT / "scripts" / "jyotish_engine.py").read_text(encoding="utf-8")
|
|
start = source.index("def build_professional_report_reference_packet")
|
|
end = source.index("\ndef cmd_pl9_export")
|
|
body = source[start:end]
|
|
sanitize_at = body.index("sanitize_professional_report_reference(final_packet)")
|
|
bind_at = body.index("bind_result_to_profile(delivered, profile)")
|
|
assert sanitize_at < bind_at
|
|
assert "attach_calculation_profile(final_packet, args)" not in body
|