2bf7472645
Web answers were thinner than a local Agent calling yinduzhanxing-skill: theme-subset vargas, no visible audit table, and a prompt that dropped the invocation contract. Bind the commercial method, compute D1–D60 plus Western layers, and deliver the same Full-Spectrum checklist. Co-authored-by: Cursor <cursoragent@cursor.com>
336 lines
13 KiB
Python
336 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from scripts.consultation_domain_registry import CANONICAL_DOMAINS
|
|
from scripts.consultation_plan_contract import (
|
|
ConsultationPlanContractError,
|
|
apply_plan_precision_boundary,
|
|
declared_workflow_route,
|
|
plan_route_contract,
|
|
validate_consultation_plan_contract,
|
|
)
|
|
|
|
|
|
def plan_body(route: str, **overrides):
|
|
"""Build the metadata the product runtime sends for one declared domain."""
|
|
contract = plan_route_contract(route)
|
|
assert contract is not None
|
|
body = {
|
|
"plan_version": "consultation-plan-v2",
|
|
"strict_workflow_route": route,
|
|
"required_layers": list(contract.required_layers),
|
|
"claim_boundary": contract.claim_boundary,
|
|
"plan_depth": "standard",
|
|
"requested_domains": list(contract.requested_domains),
|
|
"timing_horizon": "next_12_months" if route in {"timing", "annual"} else None,
|
|
"precision_boundary": "server_evidence_required",
|
|
"required_evidence_categories": list(contract.required_evidence_categories),
|
|
}
|
|
body.update(overrides)
|
|
return body
|
|
|
|
|
|
def timing_body(**overrides):
|
|
body = {
|
|
"plan_version": "consultation-plan-v2",
|
|
"strict_workflow_route": "timing",
|
|
"required_layers": ["Vimshottari", "Narayana", "Transit", "Varga", "negative holdout gate"],
|
|
"claim_boundary": "candidate_day_month_window_only_until_holdout_passes",
|
|
"plan_depth": "standard",
|
|
"requested_domains": ["timing"],
|
|
"timing_horizon": "next_12_months",
|
|
"precision_boundary": "precise_timing_blocked",
|
|
"required_evidence_categories": ["natal_foundation", "domain", "timing", "validation"],
|
|
}
|
|
body.update(overrides)
|
|
return body
|
|
|
|
|
|
def test_versioned_plan_is_validated_against_server_route_allowlist():
|
|
contract = validate_consultation_plan_contract(
|
|
timing_body(),
|
|
themes=["timing"],
|
|
route_packet={"question_type": "timing"},
|
|
)
|
|
|
|
assert contract == {
|
|
"plan_version": "consultation-plan-v2",
|
|
"strict_workflow_route": "timing",
|
|
"plan_depth": "standard",
|
|
"requested_domains": ["timing"],
|
|
"timing_horizon": "next_12_months",
|
|
"required_evidence_categories": ["natal_foundation", "domain", "timing", "validation"],
|
|
"required_layers": ["Vimshottari", "Narayana", "Transit", "Varga", "negative holdout gate"],
|
|
"claim_boundary": "candidate_day_month_window_only_until_holdout_passes",
|
|
"precision_boundary": "precise_timing_blocked",
|
|
"enforcement": "server_allowlist_validated",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("override", "message"),
|
|
[
|
|
({"plan_version": "consultation-plan-v1"}, "version"),
|
|
({"strict_workflow_route": "free_script"}, "route"),
|
|
({"required_layers": ["D1"]}, "required layers"),
|
|
({"claim_boundary": "exact_date_allowed"}, "claim boundary"),
|
|
({"requested_domains": ["career"]}, "requested domains"),
|
|
({"required_evidence_categories": ["natal_foundation"]}, "evidence categories"),
|
|
({"plan_depth": "unbounded"}, "depth"),
|
|
({"timing_horizon": None}, "requires a horizon"),
|
|
({"precision_boundary": "precise_timing_allowed"}, "precision boundary"),
|
|
],
|
|
)
|
|
def test_versioned_plan_rejects_free_form_or_tampered_workflow_control(override, message):
|
|
with pytest.raises(ConsultationPlanContractError, match=message):
|
|
validate_consultation_plan_contract(
|
|
timing_body(**override),
|
|
themes=["timing"],
|
|
route_packet={"question_type": "timing"},
|
|
)
|
|
|
|
|
|
def test_versioned_plan_rejects_theme_and_resolved_route_mismatch():
|
|
with pytest.raises(ConsultationPlanContractError, match="theme mismatch"):
|
|
validate_consultation_plan_contract(
|
|
timing_body(),
|
|
themes=["marriage"],
|
|
route_packet={"question_type": "timing"},
|
|
)
|
|
with pytest.raises(ConsultationPlanContractError, match="route mismatch"):
|
|
validate_consultation_plan_contract(
|
|
timing_body(),
|
|
themes=["timing"],
|
|
route_packet={"question_type": "career"},
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("route", "required_layers", "claim_boundary", "evidence_categories", "timing_horizon"),
|
|
[
|
|
(
|
|
"health",
|
|
["D1", "D6", "D8", "6th/8th houses", "Dasha", "non-medical boundary"],
|
|
"wellbeing_pressure_patterns_not_medical_diagnosis",
|
|
["natal_foundation", "domain", "timing", "validation"],
|
|
None,
|
|
),
|
|
(
|
|
"education",
|
|
["D1", "D24", "5th/9th houses", "Mercury/Jupiter", "Dasha"],
|
|
"learning_pattern_and_broad_timing_only",
|
|
["natal_foundation", "domain", "timing", "validation"],
|
|
None,
|
|
),
|
|
(
|
|
"migration",
|
|
["D1", "D4", "D12", "4th/12th houses", "Dasha", "Narayana"],
|
|
"migration_and_home_direction_broad_window_only",
|
|
["natal_foundation", "domain", "timing", "validation"],
|
|
None,
|
|
),
|
|
(
|
|
"family",
|
|
["D1", "D7", "D12", "4th/5th/9th houses", "Dasha"],
|
|
"family_pattern_not_deterministic_event_claim",
|
|
["natal_foundation", "domain", "timing", "validation"],
|
|
None,
|
|
),
|
|
(
|
|
"annual",
|
|
["D1", "Annual chart boundary", "Dasha", "Transit", "Tajika candidate"],
|
|
"annual_report_broad_periods_only",
|
|
["natal_foundation", "domain", "timing", "validation"],
|
|
"next_12_months",
|
|
),
|
|
],
|
|
)
|
|
def test_expanded_staging_domains_use_canonical_plan_contracts(
|
|
route, required_layers, claim_boundary, evidence_categories, timing_horizon,
|
|
):
|
|
body = {
|
|
"plan_version": "consultation-plan-v2",
|
|
"strict_workflow_route": route,
|
|
"required_layers": required_layers,
|
|
"claim_boundary": claim_boundary,
|
|
"plan_depth": "standard",
|
|
"requested_domains": [route],
|
|
"timing_horizon": timing_horizon,
|
|
"precision_boundary": "server_evidence_required",
|
|
"required_evidence_categories": evidence_categories,
|
|
}
|
|
contract = validate_consultation_plan_contract(
|
|
body,
|
|
themes=[route],
|
|
route_packet={"question_type": route},
|
|
)
|
|
|
|
assert contract is not None
|
|
assert contract["strict_workflow_route"] == route
|
|
assert contract["requested_domains"] == [route]
|
|
assert contract["enforcement"] == "server_allowlist_validated"
|
|
|
|
|
|
def test_legacy_research_callers_without_plan_metadata_remain_compatible():
|
|
assert validate_consultation_plan_contract(
|
|
{"question": "事业如何"},
|
|
themes=["career"],
|
|
route_packet={"question_type": "career"},
|
|
) is None
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"partial_body",
|
|
[
|
|
{"strict_workflow_route": "timing"},
|
|
{"required_layers": ["Vimshottari"]},
|
|
{"claim_boundary": "candidate_day_month_window_only_until_holdout_passes"},
|
|
{"plan_version": "consultation-plan-v2"},
|
|
],
|
|
)
|
|
def test_partial_plan_metadata_cannot_downgrade_to_legacy(partial_body):
|
|
with pytest.raises(ConsultationPlanContractError, match="incomplete consultation plan metadata"):
|
|
validate_consultation_plan_contract(
|
|
partial_body,
|
|
themes=["career"],
|
|
route_packet={"question_type": "timing"},
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("missing_key", list(timing_body()))
|
|
def test_versioned_plan_requires_the_complete_metadata_set(missing_key):
|
|
body = timing_body()
|
|
body.pop(missing_key)
|
|
with pytest.raises(ConsultationPlanContractError, match="incomplete consultation plan metadata"):
|
|
validate_consultation_plan_contract(
|
|
body,
|
|
themes=["timing"],
|
|
route_packet={"question_type": "timing"},
|
|
)
|
|
|
|
|
|
def test_blocked_plan_can_only_restrict_evidence_owned_precision_policy():
|
|
context = {
|
|
"answer_policy": {
|
|
"can_answer_precise_timing": True,
|
|
"should_lead_with_limitations": False,
|
|
}
|
|
}
|
|
blocked = apply_plan_precision_boundary(context, {"precision_boundary": "precise_timing_blocked"})
|
|
evidence_owned = apply_plan_precision_boundary(context, {"precision_boundary": "server_evidence_required"})
|
|
|
|
assert blocked["answer_policy"]["can_answer_precise_timing"] is False
|
|
assert blocked["answer_policy"]["should_lead_with_limitations"] is True
|
|
assert evidence_owned == context
|
|
|
|
|
|
def test_declared_workflow_route_is_only_read_from_a_complete_allowlisted_plan():
|
|
assert declared_workflow_route({"question": "事业如何"}) is None
|
|
assert declared_workflow_route(timing_body()) == "timing"
|
|
assert declared_workflow_route(plan_body("wealth")) == "wealth"
|
|
|
|
with pytest.raises(ConsultationPlanContractError, match="incomplete consultation plan metadata"):
|
|
declared_workflow_route({"strict_workflow_route": "wealth"})
|
|
with pytest.raises(ConsultationPlanContractError, match="version"):
|
|
declared_workflow_route(timing_body(plan_version="consultation-plan-v1"))
|
|
with pytest.raises(ConsultationPlanContractError, match="unsupported consultation workflow route"):
|
|
declared_workflow_route(timing_body(strict_workflow_route="free_script"))
|
|
|
|
|
|
@pytest.mark.parametrize("domain", CANONICAL_DOMAINS)
|
|
def test_plan_route_allowlist_covers_every_canonical_domain(domain: str):
|
|
contract = plan_route_contract(domain)
|
|
|
|
assert contract is not None
|
|
assert contract.themes == (domain,)
|
|
assert domain in contract.resolved_routes
|
|
assert plan_route_contract("free_script") is None
|
|
|
|
|
|
def _consultation_body(**overrides):
|
|
body = {
|
|
"dry_run": True,
|
|
"entry_mode": "direct_chart",
|
|
"year": 1990,
|
|
"month": 1,
|
|
"day": 1,
|
|
"hour": 12,
|
|
"minute": 0,
|
|
"lat": 25,
|
|
"lon": 121,
|
|
"tz": 8,
|
|
}
|
|
body.update(overrides)
|
|
return body
|
|
|
|
|
|
def test_declared_route_executes_even_when_question_text_names_another_domain():
|
|
"""One question, several domains: text routing can only ever agree with one of them."""
|
|
from scripts.jyotish_api_server import JyotishAPIHandler
|
|
|
|
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
|
|
question = "我的事业和财运接下来会怎么走,两者之间该怎么取舍"
|
|
|
|
wealth = handler._compute_consultation_workflow(_consultation_body(
|
|
**plan_body("wealth"), question=question, theme=["wealth"],
|
|
))
|
|
career = handler._compute_consultation_workflow(_consultation_body(
|
|
**plan_body("career"), question=question, theme=["career"],
|
|
))
|
|
|
|
assert wealth["routing"]["question_type"] == "wealth"
|
|
assert wealth["routing"]["primary_theme"] == "wealth"
|
|
assert "D2" in wealth["routing"]["focus_techniques"]
|
|
assert wealth["consultation_plan_contract"]["strict_workflow_route"] == "wealth"
|
|
assert career["routing"]["primary_theme"] == "career"
|
|
|
|
|
|
def test_declared_route_cannot_smuggle_a_route_off_the_server_allowlist():
|
|
from scripts.jyotish_api_server import BadRequest, JyotishAPIHandler
|
|
|
|
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
|
|
|
|
with pytest.raises(BadRequest, match="unsupported consultation workflow route"):
|
|
handler._compute_consultation_workflow(_consultation_body(
|
|
**plan_body("wealth", strict_workflow_route="free_script"),
|
|
question="财运如何",
|
|
theme=["wealth"],
|
|
))
|
|
with pytest.raises(BadRequest, match="theme mismatch"):
|
|
handler._compute_consultation_workflow(_consultation_body(
|
|
**plan_body("wealth"), question="财运如何", theme=["career"],
|
|
))
|
|
with pytest.raises(BadRequest, match="required layers mismatch"):
|
|
handler._compute_consultation_workflow(_consultation_body(
|
|
**plan_body("wealth", required_layers=["D1"]), question="财运如何", theme=["wealth"],
|
|
))
|
|
|
|
|
|
def test_api_dry_run_exposes_validated_plan_contract_and_rejects_tampering():
|
|
from scripts.jyotish_api_server import BadRequest, JyotishAPIHandler
|
|
|
|
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
|
|
body = {
|
|
**timing_body(),
|
|
"dry_run": True,
|
|
"entry_mode": "direct_chart",
|
|
"question": "未来哪些阶段值得把握?",
|
|
"theme": ["timing"],
|
|
"year": 1990,
|
|
"month": 1,
|
|
"day": 1,
|
|
"hour": 12,
|
|
"minute": 0,
|
|
"lat": 25,
|
|
"lon": 121,
|
|
"tz": 8,
|
|
}
|
|
|
|
result = handler._compute_consultation_workflow(body)
|
|
assert result["routing"]["question_type"] == "timing"
|
|
assert result["consultation_plan_contract"]["enforcement"] == "server_allowlist_validated"
|
|
|
|
with pytest.raises(BadRequest, match="claim boundary mismatch"):
|
|
handler._compute_consultation_workflow({**body, "claim_boundary": "exact_date_allowed"})
|