宫头与宫界<\/caption>/);
assert.equal((markup.match(/class="is-shifted"/g) ?? []).length, shifted.length);
assert.equal((markup.match(/是<\/td>/g) ?? []).length, shifted.length);
diff --git a/frontend/tests/chart-moon-transit.test.tsx b/frontend/tests/chart-moon-transit.test.tsx
index 79237cb0..283b9014 100644
--- a/frontend/tests/chart-moon-transit.test.tsx
+++ b/frontend/tests/chart-moon-transit.test.tsx
@@ -87,6 +87,12 @@ test("the Moon chart is the D1 counted from the Moon's sign, projected without a
const markup = renderToStaticMarkup(React.createElement(ChartVedicTab, { view, vargaId: "MOON", onSelectVarga: () => {} }));
assert.match(markup, /月亮盘 · 以月亮所在星座([^)]+)为第 1 宫<\/caption>/);
assert.deepEqual(ascendantMarkHouses(markup), [moon.ascendantHouse], "the lagna mark sits in the natal ascendant's house");
+ const table = markup.slice(markup.indexOf("月亮盘"));
+ const ascendantRow = table.match(/| [\s\S]*?上升[\s\S]*?<\/tr>/)?.[0] ?? "";
+ assert.match(ascendantRow, / | Uttara Phalguni<\/td>/);
+ assert.match(ascendantRow, /is-sun/);
+ assert.match(ascendantRow, />1);
+ assert.match(ascendantRow, />不适用);
});
test("a chart counted from the ascendant still draws the lagna mark in house 1 only", async () => {
@@ -125,7 +131,8 @@ test("the transit layer posts today's noon with the natal ayanamsa and node mode
const markup = renderToStaticMarkup(React.createElement(ChartVedicTab, { view, vargaId: "TRANSIT", onSelectVarga: () => {} }));
assert.match(markup, /行运 · 2026-09-15 12:00(Asia\/Shanghai)<\/caption>/);
- for (const column of ["行星", "今天的位置", "速度", "落本命", "本命同星"]) assert.match(markup, new RegExp(`| ${column} | `));
+ for (const column of ["行星", "今天的位置", "落本命", "本命同星"]) assert.match(markup, new RegExp(`${column} | `));
+ assert.match(markup, /速度<\/th>/);
assert.ok(markup.includes(CHART_VIEW_COPY.transitNote));
assert.match(markup, //);
assert.match(markup, /- 岁差<\/dt>/);
diff --git a/frontend/tests/chart-page-mobile-width-20260929.test.ts b/frontend/tests/chart-page-mobile-width-20260929.test.ts
index 24b4742b..3ea5335d 100644
--- a/frontend/tests/chart-page-mobile-width-20260929.test.ts
+++ b/frontend/tests/chart-page-mobile-width-20260929.test.ts
@@ -48,7 +48,21 @@ test("the Vedic waiting slot reserves the result legend band without changing th
test("only the house column in frozen natal and Moon tables overrides centering", () => {
assert.match(cssDeclarations(".chart-page-planet-table td.chart-page-planet-mid", globalStyles), /text-align:\s*center/);
+ assert.match(cssDeclarations(".chart-page-planet-table th.chart-page-planet-mid", globalStyles), /text-align:\s*center/);
assert.match(cssDeclarations(".chart-page-planet-table:has(> thead > tr > th:nth-child(8)) > tbody > tr > td:nth-child(4)", globalStyles), /text-align:\s*left/);
+ for (const child of [3, 6, 7]) {
+ assert.match(
+ cssDeclarations(`.chart-page-planet-table:has(> thead > tr > th:nth-child(8)) > thead > tr > th:nth-child(${child})`, globalStyles),
+ /text-align:\s*center/,
+ );
+ }
+});
+
+test("the current dasha card keeps its tint and drops the left accent line", () => {
+ const current = cssDeclarations(".chart-page-dasha-period.is-current", globalStyles);
+ assert.match(current, /background:\s*var\(--color-selected\)/);
+ assert.match(current, /border-color:\s*var\(--color-border-strong\)/);
+ assert.doesNotMatch(current, /box-shadow/);
});
test("wide chart-page content still scrolls inside its own box instead of widening the page", () => {
diff --git a/frontend/tests/chart-varga-table.test.tsx b/frontend/tests/chart-varga-table.test.tsx
index 12451038..cefabcf5 100644
--- a/frontend/tests/chart-varga-table.test.tsx
+++ b/frontend/tests/chart-varga-table.test.tsx
@@ -99,8 +99,11 @@ test("the D1 table is byte-for-byte the table the page drew before BUG-1076, asc
// (Leo 29°23′ = Uttara Phalguni pada 1, lord Sun), where the baseline had dashes.
const baseline = readFileSync(new URL("./fixtures/chart-d1-table-baseline.html", import.meta.url), "utf8");
const dashes = '
| — | — | — | — | ';
+ // 原值:上升行状态格 — |
+ // 新值:不适用 |
+ // 原因:产品把「—」看成没数据。上升没有尊贵,与分盘表、报告同一口径写「不适用」。基线文件仍是改前的表。
const filled = 'Uttara Phalguni | ☉ | '
- + '1 | — | ';
+ + '1 | 不适用 | ';
assert.equal(baseline.split(dashes).length, 2, "the baseline's ascendant row is the only all-dash row");
assert.equal(table, baseline.replace(dashes, filled));
});
@@ -113,6 +116,8 @@ test("the D1 ascendant row shows the engine's nakshatra, lord and pada", async (
const ascendant = row(render(view, "D1"), "上升");
assert.match(ascendant, /Uttara Phalguni<\/td>/);
assert.match(ascendant, /is-sun/, "Uttara Phalguni's lord is the Sun");
+ assert.match(ascendant, />1, "pada 1");
+ assert.match(ascendant, />不适用);
// An older cached chart without the fields still renders, with dashes.
const chart = structuredClone(golden.chart);
for (const key of ["nakshatra", "nakshatra_pada", "nakshatra_lord"]) delete (chart.ascendant as Record)[key];
@@ -136,6 +141,8 @@ test("divisional rows carry engine dignity and vargottama, and drop nakshatra co
const markup = render(view, "D9");
const header = markup.match(/([\s\S]*?)<\/thead>/)?.[1] ?? "";
for (const column of ["行星", "分盘星座", "宫位", "本命星座", "庙旺", "同位"]) assert.match(header, new RegExp(`>${column}<`));
+ assert.match(header, /| 庙旺<\/th>/);
+ assert.match(header, / | 同位<\/th>/);
assert.doesNotMatch(header, /星宿|宿主|pada/);
// Same wording as the D1 status column (BUG-1202), friendship levels included.
// 原值:同位行星单元格是「同位」,非同位是「—」
diff --git a/frontend/tests/fixtures/chart-view-golden.json b/frontend/tests/fixtures/chart-view-golden.json
index 8d47e425..a1dfc734 100644
--- a/frontend/tests/fixtures/chart-view-golden.json
+++ b/frontend/tests/fixtures/chart-view-golden.json
@@ -1,5 +1,5 @@
{
- "sourceCommand": "anaconda python: domain_calculation_service.compute_chart + compute_vimshottari_timeline; divisional_charts_extended.DivisionalChartsCalculator; jaimini.calc_chara_dasha_with_antardasha (pratyantar_dashas stripped; unused by the page); western_chart_engine.build_tropical_natal_chart. Fictional 1990-06-15 12:00 Beijing 39.9042N 116.4074E tz+8 Raman mean node. D40/D45/D60 appended 2026-09-28 from the same chart.planets[*].lon and ascendant.lon via DivisionalChartsCalculator._calculate_single_varga (recompute of D9/D10/D30 from those inputs equals the stored ones). 2026-10-03 (BUG-1201/1202): chart.ascendant nakshatra fields from jyotish_engine._nakshatra_fields(ascendant.lon) and varga_full.result[*].planets[*].dignity_level from scripts/varga_dignity.annotate_varga_dignity on the stored positions (the same calls /api/chart and /api/varga_full now make).",
+ "sourceCommand": "anaconda python: domain_calculation_service.compute_chart + compute_vimshottari_timeline; divisional_charts_extended.DivisionalChartsCalculator; jaimini.calc_chara_dasha_with_antardasha (pratyantar_dashas stripped; unused by the page); western_chart_engine.build_tropical_natal_chart. Fictional 1990-06-15 12:00 Beijing 39.9042N 116.4074E tz+8 Raman mean node. D40/D45/D60 appended 2026-09-28 from the same chart.planets[*].lon and ascendant.lon via DivisionalChartsCalculator._calculate_single_varga (recompute of D9/D10/D30 from those inputs equals the stored ones). 2026-10-03 (BUG-1202): varga_full.result[*].planets[*].dignity_level from scripts/varga_dignity.annotate_varga_dignity on the stored positions. 2026-10-15 (BUG-1352): chart.ascendant nakshatra, nakshatra_pada and nakshatra_lord are the fields returned by JyotishAPIHandler._compute_chart_sync for this birth (tests/test_api_chart_ascendant_nakshatra.py); they are not filled in by hand.",
"chart": {
"success": true,
"birth": {
diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py
index ca07c6cd..432467e5 100644
--- a/scripts/jyotish_api_server.py
+++ b/scripts/jyotish_api_server.py
@@ -2589,12 +2589,24 @@ def _vedastro_runtime_fingerprint() -> dict:
}
+def _chart_ascendant_nakshatra(ascendant_data: dict, lon: float) -> dict:
+ """Keep the engine's nakshatra fields when /api/chart rebuilds the ascendant."""
+ fields = {
+ key: ascendant_data[key]
+ for key in ('nakshatra', 'nakshatra_pada', 'nakshatra_lord')
+ if ascendant_data.get(key) not in (None, '')
+ }
+ if 'nakshatra' in fields:
+ return fields
+ return _load_local_module('jyotish_engine')._nakshatra_fields(lon)
+
+
def _build_api_chart_cache_payload(body: dict) -> dict:
import api_scratch_retention as retention
today = body.get('today') or body.get('current_date')
return {
- 'cache_schema_version': 6,
+ 'cache_schema_version': 7,
'birth': retention.normalize_chart_cache_birth(body),
'calculation': {
'ayanamsa': _request_ayanamsa(body),
@@ -6663,6 +6675,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM
'degree': round(asc_lon % 30, 2),
'degree_in_sign': round(asc_lon % 30, 2),
'lon': round(asc_lon, 4),
+ **_chart_ascendant_nakshatra(ascendant_data, asc_lon),
},
'planets': planets_data, 'houses': houses, 'shadbala': shadbala_summary,
'dasha': {
diff --git a/tests/test_api_chart_ascendant_nakshatra.py b/tests/test_api_chart_ascendant_nakshatra.py
new file mode 100644
index 00000000..8232dc87
--- /dev/null
+++ b/tests/test_api_chart_ascendant_nakshatra.py
@@ -0,0 +1,56 @@
+"""The chart API must keep the ascendant nakshatra the engine already computed."""
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+import pytest
+
+ROOT = Path(__file__).resolve().parents[1]
+SCRIPTS = ROOT / "scripts"
+TESTS = ROOT / "tests"
+for path in (SCRIPTS, TESTS):
+ if str(path) not in sys.path:
+ sys.path.insert(0, str(path))
+
+import jyotish_api_server # noqa: E402
+from test_api_server_security import _handler # noqa: E402
+
+BEIJING = {
+ "year": 1990,
+ "month": 6,
+ "day": 15,
+ "hour": 12,
+ "minute": 0,
+ "second": 0,
+ "lat": 39.9042,
+ "lon": 116.4074,
+ "tz": 8,
+ "ayanamsa": "raman",
+ "node_mode": "mean",
+ "transit_date": "2026-10-15",
+ "skip_vedastro_main_entry_overview": True,
+}
+
+
+def test_api_chart_keeps_ascendant_nakshatra_from_the_engine(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setenv("JYOTISH_API_CHART_CACHE_TTL_SECONDS", "0")
+ monkeypatch.setenv("VEDASTRO_ENABLE_NETWORK", "0")
+ monkeypatch.setattr(
+ jyotish_api_server,
+ "_attach_vedastro_main_entry_overview",
+ lambda result, _birth: result,
+ )
+ engine = jyotish_api_server._load_local_module("jyotish_engine")
+ result = _handler()._compute_chart_sync(BEIJING)
+ lon = result["ascendant"]["lon"]
+ expected = engine._nakshatra_fields(lon)
+
+ for packet in (result["ascendant"], result["modules"]["chart"]["ascendant"]):
+ assert packet["nakshatra"] == expected["nakshatra"]
+ assert packet["nakshatra_pada"] == expected["nakshatra_pada"]
+ assert packet["nakshatra_lord"] == expected["nakshatra_lord"]
+ assert result["ascendant"]["nakshatra"] == "Uttara Phalguni"
+ assert result["ascendant"]["nakshatra_pada"] == 1
+ assert result["ascendant"]["nakshatra_lord"] == "Sun"
diff --git a/tests/test_api_scratch_retention.py b/tests/test_api_scratch_retention.py
index 65dab310..9c401d36 100644
--- a/tests/test_api_scratch_retention.py
+++ b/tests/test_api_scratch_retention.py
@@ -139,7 +139,10 @@ def test_numeric_birth_spellings_share_one_chart_cache_key():
assert _chart_key(dict(base, hour=12, tz="Asia/Kolkata")) not in keys
skipped = _chart_key(dict(base, hour=12, skip_vedastro_main_entry_overview=True))
assert skipped not in keys
- assert api._build_api_chart_cache_payload(base)["cache_schema_version"] == 6
+ # 原值:cache_schema_version == 6
+ # 新值:7
+ # 原因:/api/chart 的上升补上星宿三项,旧缓存里没有这三项,必须换键。
+ assert api._build_api_chart_cache_payload(base)["cache_schema_version"] == 7
def test_audit_artifact_default_off_and_compact_when_on(monkeypatch, tmp_path):
| | | | |