Files
Jyotisha/frontend/tests/chart-dasha-yogini.test.ts
T
jesse-uxGrok (agent loop)Cursor
7a296c7348 feat(dasha): 星盘按需取 Yogini,缓存按体系分开
同一人的 Chara、Yogini 和以后的大运键各走各的缓存。页面日期直接用引擎返回的日子。

Co-Authored-By: Grok (agent loop)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-22 03:55:35 +08:00

153 lines
6.2 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import test from "node:test";
import { ChartDashaTab } from "../src/components/chart-page/chart-dasha-tab.tsx";
import { chartViewOkSchema, type ChartViewOk } from "../src/lib/chart-view-contract.ts";
import {
chartViewEngineCacheKey,
chartViewEngineCachePath,
dashaCacheScope,
} from "../src/lib/chart-view-engine.ts";
import { assembleChartView, chartDashaLayerRequest } from "../src/lib/chart-view-load.ts";
import { ANTARDASHA_DERIVED_NOTE, CHARA_ANTARDASHA_DATE_NOTE, YOGINI_ANTARDASHA_UNVERIFIED_NOTE } from "../src/lib/chart-view-labels.ts";
Object.assign(globalThis, { React });
const fixtureText = readFileSync(new URL("./fixtures/yogini-dasha-api-1990-01-01.golden.json", import.meta.url), "utf8");
const yoginiFixture = JSON.parse(fixtureText) as {
sourceCommand: string;
ayanamsa: string;
node_mode: string;
response: Record<string, unknown>;
};
const golden = JSON.parse(
readFileSync(new URL("./fixtures/chart-view-golden.json", import.meta.url), "utf8"),
) as { chart: Record<string, unknown>; chara: Record<string, unknown> };
const profile = {
name: "示例",
date: "1990-06-15",
time: "12:00",
placeLabel: "北京",
latitude: 39.9042,
longitude: 116.4074,
timezoneOffset: 8,
timezoneId: "Asia/Shanghai",
ayanamsa: "raman",
birthTimeStatus: "confirmed" as const,
};
const person = {
userId: "user-1",
subjectId: "self",
date: "1990-01-01",
time: "12:00",
latitude: 28.6139,
longitude: 77.209,
timezoneOffset: 5.5,
ayanamsa: "lahiri",
nodeMode: "mean",
};
test("the yogini fixture is a real dasha response, not a hand-built shape", () => {
assert.match(yoginiFixture.sourceCommand, /_compute_dasha_system/);
assert.match(yoginiFixture.sourceCommand, /dasha=yogini/);
assert.equal(yoginiFixture.ayanamsa, "lahiri");
assert.equal(yoginiFixture.node_mode, "mean");
const periods = yoginiFixture.response.periods;
assert.ok(Array.isArray(periods) && periods.length > 0);
});
test("the same person gets a different cache key for each dasha system", () => {
const followUp = { date: person.date, time: person.time };
const chara = chartDashaLayerRequest("chara", followUp);
const yogini = chartDashaLayerRequest("yogini", followUp);
const kalachakra = chartDashaLayerRequest("kalachakra", followUp);
assert.equal(chara.path, "/api/dasha/chara");
assert.equal(chara.cacheScope, undefined);
assert.equal(yogini.path, "/api/dasha");
assert.equal(yogini.cacheScope, "dasha:yogini");
assert.equal(yogini.body.dasha, "yogini");
assert.equal(kalachakra.path, "/api/dasha");
assert.equal(kalachakra.cacheScope, dashaCacheScope("kalachakra"));
assert.equal(kalachakra.body.dasha, "kalachakra");
const key = (path: string, cacheScope?: string) => chartViewEngineCacheKey({
...person,
path: chartViewEngineCachePath(path, cacheScope),
});
const keys = [
key(chara.path, chara.cacheScope),
key(yogini.path, yogini.cacheScope),
key(kalachakra.path, kalachakra.cacheScope),
];
assert.equal(new Set(keys).size, 3);
assert.notEqual(JSON.stringify(chara.body), JSON.stringify(yogini.body));
assert.notEqual(JSON.stringify(yogini.body), JSON.stringify(kalachakra.body));
});
async function yoginiView(): Promise<ChartViewOk> {
const scopes: Array<string | undefined> = [];
const result = await assembleChartView({
userId: "user-1",
profile,
asOf: "1990-06-01",
layers: ["chara", "yogini"],
postEngine: async (path, body, cacheScope) => {
if (path === "/api/chart") return { status: "ok", payload: golden.chart };
if (path === "/api/dasha/chara") return { status: "ok", payload: golden.chara };
if (path === "/api/dasha") {
scopes.push(cacheScope);
assert.equal(body.dasha, "yogini");
return { status: "ok", payload: yoginiFixture.response };
}
return { status: "http_error", path, elapsedMs: 1, httpStatus: 500 };
},
});
assert.deepEqual(scopes, ["dasha:yogini"]);
return chartViewOkSchema.parse(result.body);
}
test("a real yogini response maps dates and labels without deriving them", async () => {
const view = await yoginiView();
const track = view.dasha.yogini;
assert.equal(track.id, "yogini");
const first = track.periods[0];
assert.ok(first);
assert.equal(first.label, "Pingala / 太阳");
assert.equal(first.derived, false);
assert.equal(first.start, "1990-01-01");
assert.equal(first.end, "1990-01-12");
assert.equal(first.antardashas.length, 1);
assert.equal(first.antardashas[0]?.label, "Mangala / 月亮");
assert.equal(first.antardashas[0]?.derived, false);
assert.equal(first.antardashas[0]?.start, "1990-01-01");
const dhanya = track.periods.find((period) => period.label === "Dhanya / 木星");
assert.ok(dhanya);
assert.equal(dhanya.derived, false);
assert.equal(dhanya.antardashas.length, 8);
assert.equal(dhanya.antardashas.every((item) => item.derived === false), true);
assert.equal(dhanya.current, true);
assert.equal(dhanya.antardashas.some((item) => item.current), true);
assert.equal(track.antardashaNote, YOGINI_ANTARDASHA_UNVERIFIED_NOTE);
});
test("the yogini note sits under that track only", async () => {
const view = await yoginiView();
const yogini = renderToStaticMarkup(React.createElement(ChartDashaTab, { view, system: "yogini" }));
assert.equal((yogini.match(/小运未经外部软件核对。/g) ?? []).length, 1);
assert.match(yogini, /<dt>大运<\/dt>/);
assert.match(yogini, /<dt>小运<\/dt>/);
assert.match(yogini, /<dt>小运剩余<\/dt><dd>\d+ 个月<\/dd>/);
assert.match(yogini, /Dhanya \/ 木星/);
assert.match(yogini, new RegExp(ANTARDASHA_DERIVED_NOTE));
assert.doesNotMatch(yogini, new RegExp(CHARA_ANTARDASHA_DATE_NOTE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
const chara = renderToStaticMarkup(React.createElement(ChartDashaTab, { view }));
assert.equal((chara.match(/小运未经外部软件核对。/g) ?? []).length, 0);
assert.match(chara, new RegExp(CHARA_ANTARDASHA_DATE_NOTE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
assert.match(chara, new RegExp(ANTARDASHA_DERIVED_NOTE));
assert.match(chara, /aria-selected="true"[^>]*>Chara</);
});