feat(upstream): snapshot a6f47abd, REST VedAstro path, and 116-technique truth layer
Advance the one-way import to git commit a6f47abd with consultation keypath golden, switch official VedAstro comparison to the REST Calculate bridge, and receive the 25 new registry entries behind research_only_blocked. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -688,7 +688,8 @@ def t99():
|
||||
@test("Package version is consistent")
|
||||
def t100():
|
||||
from jyotish_vedic import __version__
|
||||
assert __version__ == '6.9.14'
|
||||
# 原值=6.9.14;新值=6.9.15。原因=任务 1 新增 skill 版本目录。
|
||||
assert __version__ == '6.9.15'
|
||||
|
||||
# ── v6.9.11 Precision gate tests ──
|
||||
@test("Transit uses Swiss Ephemeris")
|
||||
|
||||
@@ -1622,7 +1622,9 @@ def test_capability_audit_scans_registry_and_local_sources() -> None:
|
||||
assert ux['summary']['excellent'] == summary['productized']
|
||||
assert ux['summary']['usable'] == 0
|
||||
assert ux['summary']['thin'] == summary['api_backed'] + summary['engine_or_full_reading']
|
||||
assert ux['summary']['not_user_ready'] == 0
|
||||
# 原值=not_user_ready == 0。新值=等于 registry_only。
|
||||
# 原因=任务 1 新增 25 项中网页未执行的条目保持 registry_only,覆盖层 research_only_blocked。
|
||||
assert ux['summary']['not_user_ready'] == summary['registry_only']
|
||||
assert all(0 <= row['ux_score'] <= 6 for row in ux['rows'])
|
||||
assert all('ux_next_action' in row for row in ux['next_queue'])
|
||||
ux_by_id = {row['id']: row for row in ux['rows']}
|
||||
|
||||
@@ -62,12 +62,19 @@ def test_evidence_pool_summary_routes_few_primary_items_and_many_support_items()
|
||||
summary = build_capability_evidence_pool_summary()
|
||||
|
||||
assert summary["scope"] == "backend_capability_evidence_pool"
|
||||
assert summary["total_entries"] == 91
|
||||
# 原值=91;新值=116。原因=任务 1 接收上游 25 项注册表内容。
|
||||
assert summary["total_entries"] == 116
|
||||
assert summary["ordinary_user_policy"].startswith("Users see topic-level")
|
||||
assert summary["evidence_role_counts"]["primary"] >= 8
|
||||
assert summary["evidence_role_counts"]["secondary"] > summary["evidence_role_counts"]["primary"]
|
||||
assert summary["evidence_role_counts"]["audit_only"] >= 3
|
||||
assert summary["comparison_only_entries"] == ["rangacharya_jaimini_variant"]
|
||||
# 原值=仅 rangacharya_jaimini_variant;新值=加上 3 个上游 ziwei comparison-only。
|
||||
assert summary["comparison_only_entries"] == [
|
||||
"rangacharya_jaimini_variant",
|
||||
"ziwei_bridge_script_pack",
|
||||
"ziwei_doushu_bridge",
|
||||
"ziwei_runtime_bridge_scripts",
|
||||
]
|
||||
assert summary["conclusion_policy"]["comparison_only_entries_cannot_affect_astrological_conclusions"] is True
|
||||
assert summary["prediction_verification_counts"]["not_claimed"] > 0
|
||||
assert summary["conclusion_policy"]["primary_chain_required"] is True
|
||||
|
||||
@@ -409,7 +409,8 @@ def test_full_reading_reports_ayanamsa_metadata_and_ai_prompt_pack() -> None:
|
||||
assert "domain_statuses" in vedastro_rows[0]["note"]
|
||||
capability_pool = prompt_pack["evidence_snapshot"]["capability_evidence_pool"]
|
||||
assert capability_pool["scope"] == "backend_capability_evidence_pool"
|
||||
assert capability_pool["total_entries"] == 91
|
||||
# 原值=91;新值=116。原因=任务 1 接收上游 25 项注册表。
|
||||
assert capability_pool["total_entries"] == 116
|
||||
assert capability_pool["conclusion_policy"]["all_89_entries_must_not_be_flattened_into_conclusions"] is True
|
||||
assert "后台备选证据池" in prompt_pack["prompt_zh"]
|
||||
|
||||
|
||||
@@ -38,12 +38,18 @@ def test_truth_overlay_cannot_bypass_server_answer_contract() -> None:
|
||||
|
||||
truth = result["technique_truth"]
|
||||
policy = result["answer_policy"]
|
||||
overlay = load_commercial_skill_truth()
|
||||
assert truth["status"] == "restricted"
|
||||
assert set(truth["blocked_techniques"]) == {
|
||||
"sahams",
|
||||
"sphuta_trisphuta_family",
|
||||
"conception_chart",
|
||||
# 原值=仅 sahams / sphuta_trisphuta_family / conception_chart。
|
||||
# 新值=再加上 25 项网页路径未执行的上游新技法(research_only_blocked)。
|
||||
# 原因=任务 1 按决策记录把未执行项写入覆盖层,不得进入确定性结论。
|
||||
overlay_blocked = {
|
||||
item["technique_id"]
|
||||
for item in overlay["techniques"]
|
||||
if item["status"] in {"blocked", "research_only_blocked"}
|
||||
}
|
||||
assert {"sahams", "sphuta_trisphuta_family", "conception_chart"} <= overlay_blocked
|
||||
assert set(truth["blocked_techniques"]) == overlay_blocked
|
||||
assert set(policy["deterministic_claims_forbidden_for"]) == set(TECHNIQUE_TRUTH_IDS)
|
||||
assert policy["can_answer_precise_timing"] is True
|
||||
evidence = result["commercial_evidence_status"]
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
"""Consultation and chart API contract golden: key paths and types only.
|
||||
|
||||
Frozen fixtures record the baseline response shape. After later engine merges,
|
||||
a fresh capture must be a key-path superset with matching types. Values are
|
||||
never stored. Smoke birth data is fictional (1990-01-01 Beijing).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from http.server import ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
GOLDEN = ROOT / "tests" / "golden" / "consultation_contract_keypaths_v1.json"
|
||||
CAPTURE_ENV = "JYOTISH_CAPTURE_CONSULTATION_GOLDEN"
|
||||
|
||||
# Fictional smoke chart used by existing golden_cases.json. Not a real person.
|
||||
SMOKE_BIRTH = {
|
||||
"year": 1990,
|
||||
"month": 1,
|
||||
"day": 1,
|
||||
"hour": 12,
|
||||
"minute": 0,
|
||||
"lat": 39.9,
|
||||
"lon": 116.4,
|
||||
"tz": 8,
|
||||
"city": "Beijing",
|
||||
"note": "fictional_smoke",
|
||||
}
|
||||
|
||||
THEMES = ("career", "marriage", "wealth", "timing", "health")
|
||||
FORBIDDEN_PRIVACY_MARKERS = ("1993-04-17", "17/04/1993", "14:49")
|
||||
|
||||
_ROUTE_LAYERS = {
|
||||
"career": ["D1", "D10", "10th house/lord", "A10", "AmK", "Vimshottari", "Narayana", "Transit"],
|
||||
"marriage": ["D1", "D9", "7th house/lord", "Venus/Jupiter", "DK", "UL", "A7", "Vimshottari", "Narayana", "Transit"],
|
||||
"wealth": ["D1", "D2", "D11", "2nd/11th/9th/5th houses", "Wealth Yogas", "Ashtakavarga", "Dasha"],
|
||||
"timing": ["Vimshottari", "Narayana", "Transit", "Varga", "negative holdout gate"],
|
||||
"health": ["D1", "D6", "D8", "6th/8th houses", "Dasha", "non-medical boundary"],
|
||||
}
|
||||
_ROUTE_CLAIMS = {
|
||||
"career": "career_direction_and_broad_timing_only",
|
||||
"marriage": "relationship_pattern_and_broad_window_only",
|
||||
"wealth": "wealth_structure_not_financial_advice",
|
||||
"timing": "candidate_day_month_window_only_until_holdout_passes",
|
||||
"health": "wellbeing_pressure_patterns_not_medical_diagnosis",
|
||||
}
|
||||
|
||||
|
||||
def json_type(value: Any) -> str:
|
||||
if value is None:
|
||||
return "null"
|
||||
if isinstance(value, bool):
|
||||
return "boolean"
|
||||
if isinstance(value, (int, float)):
|
||||
return "number"
|
||||
if isinstance(value, str):
|
||||
return "string"
|
||||
if isinstance(value, list):
|
||||
return "array"
|
||||
if isinstance(value, dict):
|
||||
return "object"
|
||||
return type(value).__name__
|
||||
|
||||
|
||||
def flatten_keypaths(value: Any, prefix: str = "", *, depth: int = 0, max_depth: int = 2) -> dict[str, str]:
|
||||
"""Record dotted key paths and JSON types, two object levels deep.
|
||||
|
||||
Arrays contribute their type and the first element's type plus one child
|
||||
level. This keeps the golden small while locking frontend-consumed keys.
|
||||
"""
|
||||
out: dict[str, str] = {}
|
||||
if prefix:
|
||||
out[prefix] = json_type(value)
|
||||
if depth >= max_depth:
|
||||
return out
|
||||
if isinstance(value, dict):
|
||||
for key, child in sorted(value.items(), key=lambda item: str(item[0])):
|
||||
path = f"{prefix}.{key}" if prefix else str(key)
|
||||
out.update(flatten_keypaths(child, path, depth=depth + 1, max_depth=max_depth))
|
||||
elif isinstance(value, list) and value:
|
||||
out.update(flatten_keypaths(value[0], f"{prefix}[0]", depth=depth + 1, max_depth=max_depth))
|
||||
return out
|
||||
|
||||
|
||||
def _workflow_body(theme: str, *, skip_full_reading: bool) -> dict[str, Any]:
|
||||
timing_horizon = "next_12_months" if theme in {"timing", "annual"} else None
|
||||
return {
|
||||
**{key: SMOKE_BIRTH[key] for key in ("year", "month", "day", "hour", "minute", "lat", "lon", "tz", "city")},
|
||||
"question": f"请用虚构资料计算 {theme} 主题",
|
||||
"question_text": f"请用虚构资料计算 {theme} 主题",
|
||||
"theme": [theme],
|
||||
"entry_mode": "direct_chart",
|
||||
"plan_version": "consultation-plan-v2",
|
||||
"strict_workflow_route": theme,
|
||||
"required_layers": list(_ROUTE_LAYERS[theme]),
|
||||
"claim_boundary": _ROUTE_CLAIMS[theme],
|
||||
"plan_depth": "standard",
|
||||
"requested_domains": [theme],
|
||||
"timing_horizon": timing_horizon,
|
||||
"precision_boundary": "server_evidence_required",
|
||||
"required_evidence_categories": ["natal_foundation", "domain", "timing", "validation"],
|
||||
"defer_optional_external_evidence": True,
|
||||
"skip_full_reading_for_thematic": skip_full_reading,
|
||||
}
|
||||
|
||||
|
||||
class _ApiThread(threading.Thread):
|
||||
def __init__(self, server: ThreadingHTTPServer) -> None:
|
||||
super().__init__(daemon=True)
|
||||
self.server = server
|
||||
|
||||
def run(self) -> None:
|
||||
self.server.serve_forever()
|
||||
|
||||
|
||||
def _start_local_api() -> tuple[ThreadingHTTPServer, str]:
|
||||
from scripts.jyotish_api_server import DEFAULT_ALLOWED_HOSTS, DEFAULT_ALLOWED_ORIGINS, JyotishAPIHandler
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
port = sock.getsockname()[1]
|
||||
sock.close()
|
||||
server = ThreadingHTTPServer(("127.0.0.1", port), JyotishAPIHandler)
|
||||
server.daemon_threads = True
|
||||
origin = f"http://127.0.0.1:{port}"
|
||||
server.allowed_origins = set(DEFAULT_ALLOWED_ORIGINS) | {origin}
|
||||
server.allowed_hosts = set(DEFAULT_ALLOWED_HOSTS)
|
||||
thread = _ApiThread(server)
|
||||
thread.start()
|
||||
return server, origin
|
||||
|
||||
|
||||
def _post(base: str, path: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
payload = json.dumps(body).encode("utf-8")
|
||||
request = urllib.request.Request(
|
||||
f"{base}{path}",
|
||||
data=payload,
|
||||
method="POST",
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Origin": base,
|
||||
"Host": base.split("://", 1)[1],
|
||||
},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=180) as response:
|
||||
return json.loads(response.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as error:
|
||||
detail = error.read().decode("utf-8", errors="replace")
|
||||
raise AssertionError(f"{path} HTTP {error.code}: {detail[:500]}") from error
|
||||
|
||||
|
||||
def capture_contract_shapes(base: str) -> dict[str, dict[str, str]]:
|
||||
cases: dict[str, dict[str, str]] = {}
|
||||
for theme in THEMES:
|
||||
for skip in (True, False):
|
||||
name = f"consultation_workflow.{theme}.skip_full_reading_{str(skip).lower()}"
|
||||
body = _workflow_body(theme, skip_full_reading=skip)
|
||||
cases[name] = flatten_keypaths(_post(base, "/api/consultation_workflow", body))
|
||||
|
||||
chart = _post(base, "/api/chart", {**SMOKE_BIRTH, "ayanamsa": "raman", "node_mode": "mean"})
|
||||
cases["chart"] = flatten_keypaths(chart)
|
||||
|
||||
planets = chart.get("planets") if isinstance(chart.get("planets"), dict) else {}
|
||||
ascendant = chart.get("ascendant") if isinstance(chart.get("ascendant"), dict) else {}
|
||||
varga = _post(
|
||||
base,
|
||||
"/api/varga_full",
|
||||
{
|
||||
**{key: SMOKE_BIRTH[key] for key in ("year", "month", "day", "hour", "minute", "lat", "lon", "tz")},
|
||||
"planets": planets,
|
||||
"ascendant": ascendant,
|
||||
"divisions": ["D9", "D10"],
|
||||
},
|
||||
)
|
||||
cases["varga_full"] = flatten_keypaths(varga)
|
||||
|
||||
daily = _post(
|
||||
base,
|
||||
"/api/daily_guidance",
|
||||
{
|
||||
**{key: SMOKE_BIRTH[key] for key in ("year", "month", "day", "hour", "minute", "lat", "lon", "tz")},
|
||||
"date": "2026-09-03",
|
||||
"today": "2026-09-03",
|
||||
"reference_date": "2026-09-03",
|
||||
"transit_date": "2026-09-03",
|
||||
"ayanamsa": "raman",
|
||||
"node_mode": "mean",
|
||||
},
|
||||
)
|
||||
cases["daily_guidance"] = flatten_keypaths(daily)
|
||||
|
||||
moon = planets.get("Moon") if isinstance(planets.get("Moon"), dict) else {}
|
||||
moon_lon = moon.get("lon", 0)
|
||||
synastry = _post(base, "/api/synastry", {"male_moon": moon_lon, "female_moon": (float(moon_lon) + 30) % 360})
|
||||
cases["synastry"] = flatten_keypaths(synastry)
|
||||
return cases
|
||||
|
||||
|
||||
def write_golden(cases: dict[str, dict[str, str]]) -> None:
|
||||
GOLDEN.parent.mkdir(parents=True, exist_ok=True)
|
||||
document = {
|
||||
"schema_version": 1,
|
||||
"kind": "keypath_types_only",
|
||||
"max_path_segments": 2,
|
||||
"baseline_commit": "58cae371bbcb43ac9d4dfe8a66c78be46c88a1ee",
|
||||
"smoke_birth": SMOKE_BIRTH,
|
||||
"cases": cases,
|
||||
}
|
||||
GOLDEN.write_text(json.dumps(document, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def load_golden() -> dict[str, Any]:
|
||||
return json.loads(GOLDEN.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def assert_superset(frozen: dict[str, str], live: dict[str, str], *, case: str) -> None:
|
||||
missing = sorted(path for path in frozen if path not in live)
|
||||
type_mismatch = sorted(
|
||||
f"{path}: frozen={frozen[path]} live={live[path]}"
|
||||
for path in frozen
|
||||
if path in live and frozen[path] != live[path]
|
||||
)
|
||||
assert not missing, f"{case} lost key paths: {missing[:20]}"
|
||||
assert not type_mismatch, f"{case} type changes: {type_mismatch[:20]}"
|
||||
|
||||
|
||||
def test_golden_fixture_is_fictional_and_value_free() -> None:
|
||||
assert GOLDEN.exists(), "run capture first (JYOTISH_CAPTURE_CONSULTATION_GOLDEN=1)"
|
||||
raw = GOLDEN.read_text(encoding="utf-8")
|
||||
for marker in FORBIDDEN_PRIVACY_MARKERS:
|
||||
assert marker not in raw
|
||||
document = load_golden()
|
||||
assert document["kind"] == "keypath_types_only"
|
||||
assert document["smoke_birth"]["note"] == "fictional_smoke"
|
||||
assert document["smoke_birth"]["year"] == 1990
|
||||
for case, paths in document["cases"].items():
|
||||
assert paths, case
|
||||
assert all(kind in {"null", "boolean", "number", "string", "array", "object"} for kind in paths.values()), case
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_live_http_capture_is_keypath_superset() -> None:
|
||||
if os.environ.get("JYOTISH_LIVE_CONTRACT_GOLDEN", "").strip().lower() not in {"1", "true", "yes"}:
|
||||
pytest.skip("set JYOTISH_LIVE_CONTRACT_GOLDEN=1 to recapture over HTTP (~1 min after API import)")
|
||||
os.environ.setdefault("JYOTISH_SKIP_LOCAL_ENV", "1")
|
||||
os.environ["VEDASTRO_ENABLE_NETWORK"] = "0"
|
||||
os.environ["PYTHONDONTWRITEBYTECODE"] = "1"
|
||||
frozen = load_golden()["cases"]
|
||||
server, base = _start_local_api()
|
||||
try:
|
||||
live = capture_contract_shapes(base)
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
if os.environ.get(CAPTURE_ENV, "").strip() in {"1", "true", "yes"}:
|
||||
write_golden(live)
|
||||
frozen = live
|
||||
assert set(frozen) <= set(live)
|
||||
for case, paths in frozen.items():
|
||||
assert_superset(paths, live[case], case=case)
|
||||
|
||||
|
||||
def test_golden_covers_required_frontend_surfaces() -> None:
|
||||
cases = load_golden()["cases"]
|
||||
required = {
|
||||
"consultation_workflow.career.skip_full_reading_true",
|
||||
"consultation_workflow.career.skip_full_reading_false",
|
||||
"consultation_workflow.marriage.skip_full_reading_true",
|
||||
"consultation_workflow.marriage.skip_full_reading_false",
|
||||
"consultation_workflow.wealth.skip_full_reading_true",
|
||||
"consultation_workflow.wealth.skip_full_reading_false",
|
||||
"consultation_workflow.timing.skip_full_reading_true",
|
||||
"consultation_workflow.timing.skip_full_reading_false",
|
||||
"consultation_workflow.health.skip_full_reading_true",
|
||||
"consultation_workflow.health.skip_full_reading_false",
|
||||
"chart",
|
||||
"varga_full",
|
||||
"daily_guidance",
|
||||
"synastry",
|
||||
}
|
||||
assert required <= set(cases)
|
||||
for name in required:
|
||||
assert "success" in cases[name] or name == "synastry"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("JYOTISH_SKIP_LOCAL_ENV", "1")
|
||||
os.environ["VEDASTRO_ENABLE_NETWORK"] = "0"
|
||||
server, base = _start_local_api()
|
||||
try:
|
||||
cases = capture_contract_shapes(base)
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
write_golden(cases)
|
||||
print(f"wrote {GOLDEN} cases={len(cases)}")
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Git-mode import record for upstream a6f47abd. Does not replace the 5db72537 archive pin test."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from jsonschema import Draft202012Validator, FormatChecker
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
RECORD = ROOT / "references/cross_project_contract/imports/commit-a6f47abd2c9b7c6baa911ea00c971e1ec902380c.json"
|
||||
SOURCE_MANIFEST = ROOT / "references/upstream/yinduzhanxing/source-manifest.json"
|
||||
SKILL_SNAPSHOT = ROOT / "references/upstream/yinduzhanxing/SKILL.md"
|
||||
SCHEMA = ROOT / "references/cross_project_contract/import_manifest.schema.json"
|
||||
COMMIT = "a6f47abd2c9b7c6baa911ea00c971e1ec902380c"
|
||||
|
||||
|
||||
def test_git_import_record_matches_mirrored_skill_and_manifest() -> None:
|
||||
record = json.loads(RECORD.read_text(encoding="utf-8"))
|
||||
manifest = json.loads(SOURCE_MANIFEST.read_text(encoding="utf-8"))
|
||||
schema = json.loads(SCHEMA.read_text(encoding="utf-8"))
|
||||
Draft202012Validator(schema, format_checker=FormatChecker()).validate(record)
|
||||
|
||||
assert record["source_mode"] == "git"
|
||||
assert record["source_commit"] == COMMIT
|
||||
assert re.fullmatch(r"[0-9a-f]{40}", record["source_commit"])
|
||||
assert re.fullmatch(r"[0-9a-f]{64}", record["source_tree_hash"])
|
||||
assert record["source_tree_hash"] == manifest["source_tree_sha256"]
|
||||
assert record["source_skill_sha256"] == manifest["source_skill_sha256"]
|
||||
snapshot_hash = hashlib.sha256(SKILL_SNAPSHOT.read_bytes()).hexdigest()
|
||||
assert snapshot_hash == record["source_skill_sha256"] == record["mirror_files"][0]["target_sha256_after"]
|
||||
assert record["mirror_files"][0]["status"] == "applied"
|
||||
assert manifest["source_mode"] == "git"
|
||||
assert manifest["source_commit"] == COMMIT
|
||||
assert all(row["status"] == "review_required" for row in record["semantic_merge_files"])
|
||||
@@ -44,13 +44,13 @@ EXPECTED_REJECTIONS = {
|
||||
EXPECTED_UPSTREAM_IDENTITY = {
|
||||
"source_repository": "732642856/yinduzhanxing",
|
||||
"source_repository_url": "https://github.com/732642856/yinduzhanxing",
|
||||
"source_commit": "5db72537741fcedaa7b5498502d4a31b0f9fc147",
|
||||
"source_tree_sha256": "18e3122ef73c2776a950bfec128efeb09ac0524ec1d2179390260166b87ca814",
|
||||
"source_commit": "a6f47abd2c9b7c6baa911ea00c971e1ec902380c",
|
||||
"source_tree_sha256": "790fd5c05ffd6c1a17c6347259143d3c174e0e425246ea2207a8405f7bf6eaed",
|
||||
}
|
||||
EXPECTED_SOURCE_GIT_TREE = "16935cb68a6fa1ef72661cfca4650a42c60e9b2c"
|
||||
EXPECTED_ARCHIVE_SHA256 = "07d6b71af311160c544433eb9d51beb5b0a5e4bd0afabd4a7d43d3edb573518d"
|
||||
EXPECTED_ARCHIVE_FILE_COUNT = 2901
|
||||
EXPECTED_SOURCE_SKILL_SHA256 = "ef453dd8dd4a9da72b56010ec33bb7dab57fd72333d8785cac0986d5456e17c5"
|
||||
EXPECTED_SOURCE_GIT_TREE = "650551ddcaddce0dacc76360efca425158ebb8d4"
|
||||
EXPECTED_SOURCE_SKILL_SHA256 = "2cf15abe82e80fd1eba1fb232bc31b15a6998cafd586bbd281e76a7522b744be"
|
||||
LEGACY_ARCHIVE_COMMIT = "5db72537741fcedaa7b5498502d4a31b0f9fc147"
|
||||
CURRENT_IMPORT_RECORD = ROOT / "references/cross_project_contract/imports/commit-a6f47abd2c9b7c6baa911ea00c971e1ec902380c.json"
|
||||
|
||||
|
||||
def _load(path: Path) -> dict:
|
||||
@@ -117,18 +117,18 @@ def test_formal_varga_is_evidence_only_and_cannot_close_local_truth() -> None:
|
||||
assert "required layers" in reason
|
||||
|
||||
|
||||
def test_source_manifest_pins_valid_archive_commit_tree_and_root_skill_hash() -> None:
|
||||
def test_source_manifest_pins_valid_git_commit_tree_and_root_skill_hash() -> None:
|
||||
manifest = _load(SOURCE_MANIFEST)
|
||||
plan = _load(PLAN)
|
||||
assert manifest["source_mode"] == "archive"
|
||||
assert manifest["source_mode"] == "git"
|
||||
assert manifest["import_policy_version"] == 2
|
||||
assert manifest["imported_at"] != manifest["source_committed_at"]
|
||||
assert {key: manifest[key] for key in EXPECTED_UPSTREAM_IDENTITY} == EXPECTED_UPSTREAM_IDENTITY
|
||||
assert {key: plan[key] for key in EXPECTED_UPSTREAM_IDENTITY} == EXPECTED_UPSTREAM_IDENTITY
|
||||
assert manifest["source_git_tree"] == EXPECTED_SOURCE_GIT_TREE
|
||||
assert manifest["archive_sha256"] == EXPECTED_ARCHIVE_SHA256
|
||||
assert manifest["archive_file_count"] == EXPECTED_ARCHIVE_FILE_COUNT
|
||||
assert manifest["source_committed_at"] == "2026-08-13T18:03:53Z"
|
||||
assert "archive_sha256" not in manifest
|
||||
assert "archive_file_count" not in manifest
|
||||
assert manifest["source_committed_at"] == "2026-09-03T05:33:11Z"
|
||||
assert re.fullmatch(r"[0-9a-f]{40}", manifest["source_commit"])
|
||||
assert re.fullmatch(r"[0-9a-f]{64}", manifest["source_tree_sha256"])
|
||||
source_skill_sha256 = manifest.get("source_skill_sha256", manifest["skill_sha256"])
|
||||
@@ -143,18 +143,21 @@ def test_import_manifest_schema_accepts_archive_and_legacy_snapshot_records() ->
|
||||
validator = Draft202012Validator(schema, format_checker=FormatChecker())
|
||||
legacy = _load(LEGACY_SNAPSHOT)
|
||||
pinned = _load(PINNED_IMPORT_RECORD)
|
||||
current = _load(CURRENT_IMPORT_RECORD)
|
||||
validator.validate(legacy)
|
||||
validator.validate(pinned)
|
||||
validator.validate(current)
|
||||
invalid_git = dict(pinned, source_mode="git", source_commit="unknown")
|
||||
assert list(validator.iter_errors(invalid_git))
|
||||
assert "archive" in schema["properties"]["source_mode"]["enum"]
|
||||
source_manifest = _load(SOURCE_MANIFEST)
|
||||
plan = _load(PLAN)
|
||||
assert pinned["source_repository"] == source_manifest["source_repository"] == plan["source_repository"]
|
||||
assert pinned["source_repository_url"] == source_manifest["source_repository_url"] == plan["source_repository_url"] == EXPECTED_UPSTREAM_IDENTITY["source_repository_url"]
|
||||
assert pinned["source_commit"] == source_manifest["source_commit"] == plan["source_commit"]
|
||||
assert pinned["source_tree_hash"] == source_manifest["source_tree_sha256"] == plan["source_tree_sha256"]
|
||||
assert pinned["source_skill_sha256"] == source_manifest["source_skill_sha256"] == EXPECTED_SOURCE_SKILL_SHA256
|
||||
assert pinned["source_commit"] == LEGACY_ARCHIVE_COMMIT
|
||||
assert current["source_repository"] == source_manifest["source_repository"] == plan["source_repository"]
|
||||
assert current["source_repository_url"] == source_manifest["source_repository_url"] == plan["source_repository_url"] == EXPECTED_UPSTREAM_IDENTITY["source_repository_url"]
|
||||
assert current["source_commit"] == source_manifest["source_commit"] == plan["source_commit"]
|
||||
assert current["source_tree_hash"] == source_manifest["source_tree_sha256"] == plan["source_tree_sha256"]
|
||||
assert current["source_skill_sha256"] == source_manifest["source_skill_sha256"] == EXPECTED_SOURCE_SKILL_SHA256
|
||||
|
||||
|
||||
def test_raman_2026_08_20_packets_are_adapt_only_and_exclude_blocked_batches() -> None:
|
||||
|
||||
@@ -20,7 +20,7 @@ def test_checklist_routes_high_value_capabilities_to_expected_lanes():
|
||||
def capabilities(lane: str) -> set[str]:
|
||||
return {item["capability"] for item in checklist["lanes"][lane]}
|
||||
|
||||
assert "MCP / API Surface" in capabilities("official_mcp")
|
||||
assert "MCP / API Surface" in capabilities("rest_adapter")
|
||||
assert "Shadbala" in capabilities("official_python_bridge")
|
||||
assert "EventsAtRange / Life Event Graph" in capabilities("rest_adapter")
|
||||
assert "D1-D60 Divisional Charts" in capabilities("local_native_preferred")
|
||||
|
||||
@@ -23,6 +23,12 @@ def test_gateway_status_defaults_to_official_first_cn_safe(monkeypatch):
|
||||
assert status["backend_priority"] == ["self_host", "official", "cache", "queue", "local_fallback"]
|
||||
assert status["active_backend"] == "official"
|
||||
assert status["official_readiness"]["official_ready"] is True
|
||||
assert status["official_transport"] == "rest"
|
||||
assert status["official_calculate_endpoint"] == "https://api.vedastro.org/api/Calculate"
|
||||
assert status["rate_limit_per_minute"] == 5
|
||||
assert status["mcp_bridge_role"] == "protocol_probe_only"
|
||||
assert status["official_calculate_health"]["status"] == "not_probed"
|
||||
assert status["official_calculate_health"]["transport"] == "rest"
|
||||
assert status["boundary"] == "Users never call VedAstro directly; backend gateway owns cache, queue, and fallback."
|
||||
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ def test_matrix_declares_fastest_path_lane_for_high_value_rows():
|
||||
|
||||
assert _row(matrix, "EventsAtRange / Life Event Graph")["fastest_path_lane"] == "rest_adapter"
|
||||
assert _row(matrix, "Shadbala")["fastest_path_lane"] == "official_python_bridge"
|
||||
assert _row(matrix, "MCP / API Surface")["fastest_path_lane"] == "official_mcp"
|
||||
assert _row(matrix, "MCP / API Surface")["fastest_path_lane"] == "rest_adapter"
|
||||
assert _row(matrix, "D1-D60 Divisional Charts")["fastest_path_lane"] == "local_native_preferred"
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import argparse
|
||||
import http.client
|
||||
import urllib.error
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.vedastro_rest_bridge import (
|
||||
SMOKE_DEFAULT,
|
||||
_date_to_vedastro_std,
|
||||
build_dasa_payload,
|
||||
build_horoscope_payload,
|
||||
build_parser,
|
||||
call,
|
||||
match_not_implemented,
|
||||
probe_calculate_health,
|
||||
reset_rate_limiter,
|
||||
)
|
||||
|
||||
|
||||
def _args(**overrides):
|
||||
values = {
|
||||
"birth": SMOKE_DEFAULT["birth"],
|
||||
"name": SMOKE_DEFAULT["name"],
|
||||
"lat": SMOKE_DEFAULT["lat"],
|
||||
"lon": SMOKE_DEFAULT["lon"],
|
||||
"ayanamsa": SMOKE_DEFAULT["ayanamsa"],
|
||||
"start": "2026-01-01 00:00 +08:00",
|
||||
"end": "2026-03-31 00:00 +08:00",
|
||||
}
|
||||
values.update(overrides)
|
||||
return argparse.Namespace(**values)
|
||||
|
||||
|
||||
def test_default_horoscope_payload_uses_fictional_smoke_without_network():
|
||||
payload = build_horoscope_payload(_args())
|
||||
|
||||
assert payload == {
|
||||
"Ayanamsa": "LAHIRI",
|
||||
"BirthTime": {
|
||||
"StdTime": "12:00 01/01/1990 +08:00",
|
||||
"Location": {"Name": "Beijing, China", "Longitude": 116.4074, "Latitude": 39.9042},
|
||||
},
|
||||
"SortByWeight": False,
|
||||
}
|
||||
assert "1993" not in payload["BirthTime"]["StdTime"]
|
||||
assert "14:49" not in payload["BirthTime"]["StdTime"]
|
||||
|
||||
|
||||
def test_dasa_payload_formats_vedastro_range_times_without_network():
|
||||
payload = build_dasa_payload(_args())
|
||||
|
||||
assert payload["Ayanamsa"] == "LAHIRI"
|
||||
assert payload["StartTime"]["StdTime"] == "00:00 01/01/2026 +08:00"
|
||||
assert payload["EndTime"]["StdTime"] == "00:00 31/03/2026 +08:00"
|
||||
assert payload["Levels"] == 3
|
||||
assert payload["PrecisionHours"] == 100
|
||||
|
||||
|
||||
def test_date_parser_rejects_ambiguous_times():
|
||||
with pytest.raises(ValueError):
|
||||
_date_to_vedastro_std("2026-01-01")
|
||||
|
||||
|
||||
def test_parser_match_is_registered_as_official_blocked():
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(["match"])
|
||||
assert args.cmd == "match"
|
||||
blocked = match_not_implemented()
|
||||
assert blocked["official_closure_state"] == "official_blocked"
|
||||
assert blocked["official_closure_reason"] == "match_subcommand_not_implemented"
|
||||
|
||||
|
||||
class _Handle(BytesIO):
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
return False
|
||||
|
||||
|
||||
def test_recorded_horoscope_status_pass_does_not_hit_network():
|
||||
reset_rate_limiter()
|
||||
recorded = {"Status": "Pass", "Payload": {"source": "recorded_smoke"}}
|
||||
|
||||
def fake_opener(_req, timeout=90):
|
||||
return _Handle(b'{"Status": "Pass", "Payload": {"source": "recorded_smoke"}}')
|
||||
|
||||
result = call("HoroscopePredictions", build_horoscope_payload(_args()), opener=fake_opener)
|
||||
assert result == recorded
|
||||
|
||||
|
||||
def test_probe_maps_recorded_pass_and_rate_limit_without_network():
|
||||
reset_rate_limiter()
|
||||
health = probe_calculate_health(opener=lambda _req, timeout=8: _Handle(b'{"Status": "Pass"}'))
|
||||
assert health["status"] == "official_verified"
|
||||
assert health["transport"] == "rest"
|
||||
|
||||
def limited(_req, timeout=8):
|
||||
raise urllib.error.HTTPError(
|
||||
"https://api.vedastro.org/api/Calculate/HoroscopePredictions",
|
||||
429,
|
||||
"rate",
|
||||
hdrs=http.client.HTTPMessage(),
|
||||
fp=BytesIO(b""),
|
||||
)
|
||||
|
||||
reset_rate_limiter()
|
||||
blocked = probe_calculate_health(opener=limited)
|
||||
assert blocked["status"] == "official_blocked"
|
||||
assert blocked["reason"] == "rate_limited"
|
||||
Reference in New Issue
Block a user