From 7d3bb0c5ede4277ad7a9a44aea985442e3f98a94 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Wed, 9 Sep 2026 16:58:46 +0800 Subject: [PATCH] fix: feed current dasha into web marriage path and keep chart fences on failure (BUG-609, BUG-610, BUG-611) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consultation was passing the birth mahadasha into event-class splits, so marriage hits stayed empty and timing still said 缔结. Read live MD/AD from dasha_sub_periods, derive activation copy from the split, and isolate fence failures so one bad varga cannot 500 the whole report. Co-authored-by: Cursor --- CHANGELOG.md | 5 + docs/BUG_HISTORY.md | 49 ++++ .../PROGRESS-upstream-sync2-fix-20260909.md | 38 +++ docs/tasks/README.md | 2 +- docs/testing/upstream-sync2-20260909.md | 3 +- scripts/jyotish_api_server.py | 41 ++-- scripts/jyotish_engine.py | 19 +- scripts/report_orchestrator.py | 26 +- scripts/run_quality_gate.py | 2 + .../consultation_marriage_1990_fictional.json | 229 ++++++++++++++++++ tests/test_api_server_security.py | 1 + .../test_relationship_event_class_evidence.py | 35 +++ tests/test_report_chart_block.py | 12 + ...est_report_orchestrator_marriage_timing.py | 59 +++++ tests/test_thematic_dasha_info_source.py | 77 ++++++ 15 files changed, 573 insertions(+), 25 deletions(-) create mode 100644 docs/tasks/PROGRESS-upstream-sync2-fix-20260909.md create mode 100644 tests/golden/upstream_sync2/consultation_marriage_1990_fictional.json create mode 100644 tests/test_report_orchestrator_marriage_timing.py create mode 100644 tests/test_thematic_dasha_info_source.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1212b56d..9404e1ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 印度占星 Skill 更新日志 +## 2026-09-09 — 婚恋激活句按心动 / 成对 / 领证出,网页咨询接上当前大小运 + +网页咨询路径现在用当前大运/小运喂给婚恋三层,不再拿出生时大运空转。没有领证命中时,激活句只写观察、并标明不等于领证;没有事件类命中时不再写「关键期」。小运缺失时大运名不再拼 `Unknown`。Skill 版本不变。 + + ## 2026-09-09 — 婚恋分心动 / 成对 / 领证三层作答;长报告时间系统表加十套条件大运 婚恋问题不再把 Venus / Jupiter / Moon 大运写成一句「婚恋机会」。心动、成对、领证分开说:心动只是观察,不能说成领证;法律婚只认大运/小运命中,不认更细的一层。长报告「时间系统证据状态」表在原来五套后面加上十套条件大运,适用的标 `parameter_sensitive`,不适用的标 `not_applicable`。Skill 版本 6.9.16。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 83641e5a..63233dc4 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -9445,3 +9445,52 @@ - 相关记录:`TASK-upstream-sync2-20260909.md`;上游 09-04 婚恋触发审计 - 复发自:无(上游审计结论在本仓补登记) - 修复版本:待发布 + +## BUG-609 | 网页咨询婚恋路径喂的是出生大运,事件类 hits 恒空 + +- 状态:resolved +- 首次发现:2026-09-09 +- 最近更新:2026-09-09 +- 影响面:`/api/consultation_workflow` 婚恋主题、`_thematic_dasha_info`、`report_orchestrator` 婚恋 `timing` +- 用户现象:问结婚时机时,心动 / 成对 / 领证拆分没有大运命中;时间标签可能出现 `Unknown`,并仍写出「缔结或调整伴侣关系的关键期」。 +- 触发条件:网页咨询走复用 chart 模块的婚恋主题,chart 模块 dasha 没有 `vimshottari_analysis`。 +- 根因:`_thematic_dasha_info` 只从 full-reading 的 `vimshottari_analysis` 取当前 MD/AD。咨询路径 09-06 起复用 chart 模块后这个字段不存在,于是只剩出生时 `current_md`。BUG-608 把 `dasha_info` 接上后,断链第一次变成空 hits。激活句也不看 `event_class_split`。 +- 修复:当前 MD/AD/PD 改从 `chart.modules.dasha_sub_periods.current` 读取,缺了再回退 `vimshottari_analysis`;婚恋 `activation_description` 按事件类出句;AD 缺失时 `dasha_period` 只写 MD。 +- 验证:`tests/test_thematic_dasha_info_source.py`、`tests/test_report_orchestrator_marriage_timing.py`、`tests/test_relationship_event_class_evidence.py` 咨询形黄金件。 +- 防复发:上述测试列入 `CORE_PYTEST_TARGETS`。咨询契约只增不改。感情分析模块逻辑未改,只修喂数。 +- 相关记录:BUG-608、BUG-555、`TASK-upstream-sync2-fix-20260909.md` +- 复发自:无(BUG-608 接入后的喂数断链) +- 修复版本:待发布 + +## BUG-610 | 分盘围栏构造失败会让整份长报告 500 + +- 状态:resolved +- 首次发现:2026-09-09 +- 最近更新:2026-09-09 +- 影响面:`render_pl9_markdown`、`/api/reports//professional-reference`、`pl9-export` +- 用户现象:某一张分盘上升星座名不在英文十二星座内时,专业参考 Markdown 与导出一起失败。 +- 触发条件:分盘 `Ascendant.sign` 缺失或为中文星座名,围栏 `build_chart_block` / `_sign_name` 抛 `ValueError`。 +- 根因:南印度 SVG 在 `try` 内,失败只写「图盘生成失败」;紧接着的围栏构造在 `try` 外,一张分盘出错就中断整份报告。 +- 修复:围栏失败只丢掉该张围栏、保留 SVG,并打一条不含用户串的 warning。 +- 验证:`tests/test_report_chart_block.py` 中文上升名用例:Markdown 仍含 ` str: for name in PLANET_ORDER for row in [natal_planets.get(name)] } - block = build_chart_block( - resolve_chart_id(chart_id, title), - title, - positions if isinstance(positions, dict) else {}, - ascendant_row if isinstance(ascendant_row, dict) else {}, - retrograde_by_planet, - ) + try: + block = build_chart_block( + resolve_chart_id(chart_id, title), + title, + positions if isinstance(positions, dict) else {}, + ascendant_row if isinstance(ascendant_row, dict) else {}, + retrograde_by_planet, + ) + except Exception: + logging.warning('report chart fence failed id=%s', chart_id or 'unknown') + return svg return f"{svg}\n\n{block}" def _d1_chart_svg() -> str | None: diff --git a/scripts/report_orchestrator.py b/scripts/report_orchestrator.py index 9b11b341..b9895238 100644 --- a/scripts/report_orchestrator.py +++ b/scripts/report_orchestrator.py @@ -241,6 +241,7 @@ class BirthChartData: dasha_timeline: List[Dict[str, Any]] = field(default_factory=list) yogas: List[Dict[str, Any]] = field(default_factory=list) ashtakavarga: Dict[str, Any] = field(default_factory=dict) + event_class_split: Dict[str, Any] = field(default_factory=dict) # ═══════════════════════════════════════════════════════════════ @@ -685,9 +686,15 @@ class TimingAnchorBuilder: # 根据主题生成激活描述 activation = self._activation_text(theme, current) + md = str(current.get("mahadasha") or "").strip() + ad = str(current.get("antardasha") or "").strip() + if ad and ad != "Unknown": + dasha_period = f"{md}-{ad}" if md else ad + else: + dasha_period = md or "Unknown" return TimingAnchor( - dasha_period=current.get("mahadasha", "Unknown") + "-" + current.get("antardasha", "Unknown"), + dasha_period=dasha_period, start_year=current.get("start", now), end_year=current.get("end", now + 3), activation_description=activation, @@ -695,10 +702,11 @@ class TimingAnchorBuilder: ) def _activation_text(self, theme: ThemeName, period: Dict[str, Any]) -> str: + if theme == ThemeName.MARRIAGE: + return self._marriage_activation_text() md = period.get("mahadasha", "") ad = period.get("antardasha", "") theme_desc = { - ThemeName.MARRIAGE: "婚姻相关宫位与 karaka 将被激活,是缔结或调整伴侣关系的关键期", ThemeName.CAREER: "事业宫位能量被引动,可能出现职位变动、项目突破或行业转换的契机", ThemeName.WEALTH: "财富宫位与 Dhana Yoga 被触发,收入结构可能发生变化", ThemeName.HEALTH: "健康宫位能量显现,是关注身体信号、调整作息的重要时期", @@ -706,6 +714,20 @@ class TimingAnchorBuilder: } return f"{md} 主运配合 {ad} 副运,{theme_desc.get(theme, '相关能量将被激活')}" + def _marriage_activation_text(self) -> str: + split = self.chart.event_class_split if isinstance(getattr(self.chart, "event_class_split", None), dict) else {} + legal = split.get("legal_marriage") if isinstance(split.get("legal_marriage"), dict) else {} + romantic = split.get("romantic_activation") if isinstance(split.get("romantic_activation"), dict) else {} + formation = split.get("relationship_formation") if isinstance(split.get("relationship_formation"), dict) else {} + legal_hits = legal.get("hits") if isinstance(legal.get("hits"), list) else [] + romantic_hits = romantic.get("hits") if isinstance(romantic.get("hits"), list) else [] + formation_hits = formation.get("hits") if isinstance(formation.get("hits"), list) else [] + if legal_hits: + return "婚姻相关宫位与 karaka 将被激活,是缔结或调整伴侣关系的关键期" + if romantic_hits or formation_hits: + return "感情被激活的观察期,不等于领证" + return "本期无婚恋事件类命中" + # ═══════════════════════════════════════════════════════════════ # 主题化解盘编排器(主控类) diff --git a/scripts/run_quality_gate.py b/scripts/run_quality_gate.py index 68768934..7c4ceb3b 100644 --- a/scripts/run_quality_gate.py +++ b/scripts/run_quality_gate.py @@ -91,6 +91,8 @@ CORE_PYTEST_TARGETS = [ "tests/test_mcp_strict_workflow_relationship.py", "tests/test_punarphoo_observation.py", "tests/test_relationship_event_class_evidence.py", + "tests/test_thematic_dasha_info_source.py", + "tests/test_report_orchestrator_marriage_timing.py", "tests/test_full_reading_conditional_dashas.py", "tests/test_capability_evidence_pool.py", "tests/test_readme_badges.py", diff --git a/tests/golden/upstream_sync2/consultation_marriage_1990_fictional.json b/tests/golden/upstream_sync2/consultation_marriage_1990_fictional.json new file mode 100644 index 00000000..b5d05efe --- /dev/null +++ b/tests/golden/upstream_sync2/consultation_marriage_1990_fictional.json @@ -0,0 +1,229 @@ +{ + "note": "Fictional 1990-06-15 10:30 Beijing; Leo lagna; 5L=Jupiter, 7L=Saturn, Moon in 7H. Current MD Saturn / AD Jupiter. Trimmed from consultation-shaped chart modules.", + "birth": { + "year": 1990, + "month": 6, + "day": 15, + "hour": 10, + "minute": 30, + "lat": 39.9042, + "lon": 116.4074, + "tz": 8, + "today": "2026-09-09" + }, + "chart": { + "ascendant": { + "sign": "Leo", + "sign_idx": 4, + "lon": 131.4482, + "degree": 11.45, + "degree_in_sign": 11.45 + }, + "planets": { + "Jupiter": { + "sign": "Gemini", + "house": 11, + "lon": 83.52630829337346, + "degree": 23.5263, + "degree_in_sign": 23.5263, + "sign_idx": 2, + "longitude": 83.52630829337346 + }, + "Ketu": { + "sign": "Cancer", + "house": 12, + "lon": 107.4414312800169, + "degree": 17.4414, + "degree_in_sign": 17.4414, + "sign_idx": 3, + "longitude": 107.4414312800169 + }, + "Mars": { + "sign": "Pisces", + "house": 8, + "lon": 348.4792230303051, + "degree": 18.4792, + "degree_in_sign": 18.4792, + "sign_idx": 11, + "longitude": 348.4792230303051 + }, + "Mercury": { + "sign": "Taurus", + "house": 10, + "lon": 42.73765469811987, + "degree": 12.7377, + "degree_in_sign": 12.7377, + "sign_idx": 1, + "longitude": 42.73765469811987 + }, + "Moon": { + "sign": "Aquarius", + "house": 7, + "lon": 317.8190269192091, + "degree": 17.819, + "degree_in_sign": 17.819, + "sign_idx": 10, + "longitude": 317.8190269192091 + }, + "Rahu": { + "sign": "Capricorn", + "house": 6, + "lon": 287.4414312800169, + "degree": 17.4414, + "degree_in_sign": 17.4414, + "sign_idx": 9, + "longitude": 287.4414312800169 + }, + "Saturn": { + "sign": "Capricorn", + "house": 6, + "lon": 271.7776735771559, + "degree": 1.7777, + "degree_in_sign": 1.7777, + "sign_idx": 9, + "longitude": 271.7776735771559 + }, + "Sun": { + "sign": "Gemini", + "house": 11, + "lon": 61.47407825034283, + "degree": 1.4741, + "degree_in_sign": 1.4741, + "sign_idx": 2, + "longitude": 61.47407825034283 + }, + "Venus": { + "sign": "Aries", + "house": 9, + "lon": 26.034770273615777, + "degree": 26.0348, + "degree_in_sign": 26.0348, + "sign_idx": 0, + "longitude": 26.034770273615777 + } + }, + "modules": { + "dasha": { + "current_md": "Rahu", + "start_date": "1975-05-25", + "remaining_years": 2.94, + "total_years": 18, + "birth_balance": { + "elapsed_years": 15.06, + "lord": "Rahu", + "remaining_years": 2.94 + }, + "periods": [ + { + "lord": "Rahu", + "start": "1975-05-25", + "end": "1993-05-25" + }, + { + "lord": "Jupiter", + "start": "1993-05-25", + "end": "2009-05-25" + }, + { + "lord": "Saturn", + "start": "2009-05-25", + "end": "2028-05-25" + }, + { + "lord": "Mercury", + "start": "2028-05-25", + "end": "2045-05-25" + }, + { + "lord": "Ketu", + "start": "2045-05-25", + "end": "2052-05-25" + }, + { + "lord": "Venus", + "start": "2052-05-25", + "end": "2072-05-25" + }, + { + "lord": "Sun", + "start": "2072-05-25", + "end": "2078-05-25" + }, + { + "lord": "Moon", + "start": "2078-05-25", + "end": "2088-05-25" + }, + { + "lord": "Mars", + "start": "2088-05-25", + "end": "2095-05-25" + } + ] + }, + "dasha_sub_periods": { + "status": "ready", + "current": { + "mahadasha": { + "lord": "Saturn", + "start": "2009-05-25", + "end": "2028-05-25" + }, + "antardasha": { + "lord": "Jupiter", + "start": "2025-11-11", + "end": "2028-05-25" + } + } + } + } + }, + "thematic_report": { + "themes": { + "marriage": { + "timing": { + "dasha_period": "Saturn-Jupiter", + "start_year": 2009, + "end_year": 2028, + "activation_description": "感情被激活的观察期,不等于领证", + "is_current": true + }, + "evidence": [ + { + "technique": "D1-7th-house", + "details": {} + }, + { + "technique": "Spouse-status-yoga", + "details": {} + }, + { + "technique": "DK-UL-Dasha timing", + "details": {} + }, + { + "technique": "Romantic-activation", + "details": { + "hits": [ + "5L_dasha" + ], + "natal_structure": [ + "7H_Moon" + ], + "event_class": "romantic_activation" + } + }, + { + "technique": "Relationship-formation", + "details": { + "hits": [ + "7L_dasha" + ], + "event_class": "relationship_formation" + } + } + ] + } + } + } +} diff --git a/tests/test_api_server_security.py b/tests/test_api_server_security.py index 9faa52e3..73b6e7bc 100644 --- a/tests/test_api_server_security.py +++ b/tests/test_api_server_security.py @@ -214,6 +214,7 @@ def test_health_endpoint_exposes_runtime_accuracy_metadata() -> None: assert handler.status_code == 200 payload = handler.payload() assert payload['status'] == 'ok' + assert payload['version'] == jyotish_api_server.__version__ assert payload['ayanamsa_default'] == 'raman' assert 'swisseph_available' in payload assert 'swisseph_version' in payload diff --git a/tests/test_relationship_event_class_evidence.py b/tests/test_relationship_event_class_evidence.py index fc766531..32f7f01b 100644 --- a/tests/test_relationship_event_class_evidence.py +++ b/tests/test_relationship_event_class_evidence.py @@ -2,12 +2,14 @@ from __future__ import annotations +import json import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "scripts")) +from jyotish_api_server import JyotishAPIHandler # noqa: E402 from relationship_analysis import analyze_relationship # noqa: E402 from relationship_event_class_evidence import build_event_class_evidence # noqa: E402 @@ -63,3 +65,36 @@ def test_pratyantar_venus_does_not_emit_legal_marriage_item() -> None: assert "Venus_dasha" not in split["legal_marriage"]["hits"] items = build_event_class_evidence(relationship, _theme_evidence) assert all(item["technique"] != "Legal-marriage" for item in items) + + +def test_consultation_shaped_1990_chart_feeds_nonempty_event_class_hits() -> None: + golden = json.loads( + (ROOT / "tests/golden/upstream_sync2/consultation_marriage_1990_fictional.json").read_text( + encoding="utf-8" + ) + ) + chart = golden["chart"] + handler = JyotishAPIHandler.__new__(JyotishAPIHandler) + dasha = (chart.get("modules") or {}).get("dasha") or {} + dasha_info = handler._thematic_dasha_info(chart, dasha) + assert dasha_info["maha_dasha"] == "Saturn" + assert dasha_info["antar_dasha"] == "Jupiter" + timing = (((golden.get("thematic_report") or {}).get("themes") or {}).get("marriage") or {}).get("timing") or {} + assert "Unknown" not in str(timing.get("dasha_period") or "") + assert timing.get("dasha_period") == "Saturn-Jupiter" + relationship = analyze_relationship( + chart["planets"], + asc_sign=chart["ascendant"]["sign"], + dasha_info=dasha_info, + ) + items = handler._derived_marriage_evidence( + chart, + {"relationship": relationship, "full_modules": chart.get("modules") or {}}, + ) + by_name = {item["technique"]: item for item in items} + nonempty = [ + name + for name in ("Romantic-activation", "Relationship-formation") + if (by_name.get(name) or {}).get("details", {}).get("hits") + ] + assert nonempty, {name: (by_name.get(name) or {}).get("details") for name in by_name} diff --git a/tests/test_report_chart_block.py b/tests/test_report_chart_block.py index ab367de2..07fee1f9 100644 --- a/tests/test_report_chart_block.py +++ b/tests/test_report_chart_block.py @@ -2,6 +2,7 @@ from __future__ import annotations +import copy import json from functools import lru_cache from types import SimpleNamespace @@ -195,3 +196,14 @@ def test_chart_block_suite_is_on_the_quick_quality_gate() -> None: from scripts.run_quality_gate import CORE_PYTEST_TARGETS assert "tests/test_report_chart_block.py" in CORE_PYTEST_TARGETS + + +def test_chinese_ascendant_sign_keeps_svg_and_drops_fence() -> None: + packet = copy.deepcopy(_golden()["packet"]) + packet["core_chart"]["ascendant"]["sign"] = "白羊" + markdown = render_pl9_markdown(packet) + start = markdown.index("#### D1 — Rashi Chart(本命盘)") + end = markdown.index("#### D9 — Navamsa(婚盘)") + section = markdown[start:end] + assert " TimingAnchorBuilder: + chart = BirthChartData( + dasha_timeline=[ + {"mahadasha": "Saturn", "antardasha": antardasha, "start": 2025, "end": 2028}, + ], + event_class_split=split, + ) + return TimingAnchorBuilder(chart) + + +def test_romantic_only_says_not_legal_marriage() -> None: + timing = _builder({"romantic_activation": {"hits": ["5L_dasha"]}}).build_for_theme(ThemeName.MARRIAGE) + assert timing is not None + assert "不等于领证" in timing.activation_description + assert "缔结" not in timing.activation_description + assert "关键期" not in timing.activation_description + + +def test_legal_md_ad_hit_may_say_solemnize() -> None: + timing = _builder( + {"legal_marriage": {"hits": ["Venus_dasha"]}}, + antardasha="Venus", + ).build_for_theme(ThemeName.MARRIAGE) + assert timing is not None + assert "缔结" in timing.activation_description + assert timing.dasha_period == "Saturn-Venus" + + +def test_no_hits_omits_key_period_and_solemnize() -> None: + timing = _builder({}).build_for_theme(ThemeName.MARRIAGE) + assert timing is not None + assert "关键期" not in timing.activation_description + assert "缔结" not in timing.activation_description + assert timing.activation_description == "本期无婚恋事件类命中" + assert "Unknown" not in timing.dasha_period + assert timing.dasha_period == "Saturn" + + +def test_legal_phrase_only_lives_on_the_legal_branch() -> None: + source = ORCHESTRATOR.read_text(encoding="utf-8") + assert source.count("缔结或调整伴侣关系的关键期") == 1 + legal_fn = source[source.index("def _marriage_activation_text") :] + legal_fn = legal_fn[: legal_fn.index("class ThematicReportOrchestrator")] + assert "if legal_hits:" in legal_fn + before_legal, after_legal = legal_fn.split("if legal_hits:", 1) + assert "缔结或调整伴侣关系的关键期" not in before_legal + assert "缔结或调整伴侣关系的关键期" in after_legal.split("return", 1)[1].split("\n", 1)[0] diff --git a/tests/test_thematic_dasha_info_source.py b/tests/test_thematic_dasha_info_source.py new file mode 100644 index 00000000..ea4256cb --- /dev/null +++ b/tests/test_thematic_dasha_info_source.py @@ -0,0 +1,77 @@ +"""Current MD/AD for thematic reports come from dasha_sub_periods, not birth MD.""" + +from __future__ import annotations + +from scripts.jyotish_api_server import JyotishAPIHandler + + +def _handler() -> JyotishAPIHandler: + return JyotishAPIHandler.__new__(JyotishAPIHandler) + + +def test_prefers_dasha_sub_periods_current_over_birth_md() -> None: + info = _handler()._thematic_dasha_info( + { + "dasha": {"current_md": "Rahu"}, + "modules": { + "dasha_sub_periods": { + "current": { + "mahadasha": {"lord": "Saturn"}, + "antardasha": {"lord": "Jupiter"}, + } + } + }, + }, + {"current_md": "Rahu", "periods": []}, + ) + assert info["maha_dasha"] == "Saturn" + assert info["antar_dasha"] == "Jupiter" + + +def test_falls_back_to_vimshottari_analysis_when_sub_periods_missing() -> None: + info = _handler()._thematic_dasha_info( + {"dasha": {"current_md": "Rahu"}}, + { + "vimshottari_analysis": { + "current": { + "mahadasha": {"lord": "Venus"}, + "antardasha": {"lord": "Moon"}, + } + } + }, + ) + assert info["maha_dasha"] == "Venus" + assert info["antar_dasha"] == "Moon" + + +def test_omits_maha_dasha_when_neither_source_has_current_lords() -> None: + info = _handler()._thematic_dasha_info({"dasha": {"current_md": "Rahu"}}, {"periods": []}) + assert "maha_dasha" not in info + assert "antar_dasha" not in info + + +def test_timeline_fills_current_antardasha_from_sub_periods() -> None: + timeline = _handler()._normalize_thematic_dasha_timeline( + { + "periods": [ + {"lord": "Saturn", "start": "2009-05-25", "end": "2028-05-25"}, + ], + "modules": { + "dasha_sub_periods": { + "current": { + "mahadasha": {"lord": "Saturn"}, + "antardasha": {"lord": "Jupiter"}, + } + } + }, + } + ) + assert timeline[0]["mahadasha"] == "Saturn" + assert timeline[0]["antardasha"] == "Jupiter" + + +def test_source_tests_are_on_the_quick_quality_gate() -> None: + from scripts.run_quality_gate import CORE_PYTEST_TARGETS + + assert "tests/test_thematic_dasha_info_source.py" in CORE_PYTEST_TARGETS + assert "tests/test_report_orchestrator_marriage_timing.py" in CORE_PYTEST_TARGETS