Files
Jyotisha/frontend/tests/daily-starlanguage.test.ts
T
Jesse_Chen 54269fcfcc
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
refactor(chat): extract home helpers, chart library, and starter surfaces
page.tsx still owns the chat main chain, but the first product surfaces now
live in their own modules so later splits can land without editing the 4k-line
Home. Source-lock tests follow the moved tokens; the orphan user-data contract
is aligned and added to the quick gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 22:58:02 +08:00

343 lines
14 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
calendarDateInTimeZone,
composeDailyStarlanguageCard,
dailyStarlanguageCacheKey,
dailyStarlanguageEvidence,
dailyStarlanguageProfileKey,
dailyStarlanguagePrompt,
evidenceFromDailyGuidance,
parseDailyStarlanguageText,
} from "../src/lib/daily-starlanguage.ts";
import { homeSurface } from "./home-surface.ts";
const today = "2026-08-17";
const chart = {
modules: {
chart: {
ascendant: { sign: "Leo", lon: 132.5 },
planets: { Moon: { sign: "Cancer", lon: 98.2 }, Sun: { sign: "Leo", lon: 130.1 } },
},
},
ai_prompt_pack: {
evidence_snapshot: {
functional_benefic_malefic: {
status: "used",
functional_benefics: ["Mars", "Sun"],
functional_malefics: ["Mercury"],
},
},
},
};
const vimshottari = {
vimshottari_analysis: {
nakshatra: { name: "Pushya" },
current: {
mahadasha: { lord: "Venus" },
antardasha: { lord: "Mercury" },
remaining_days: 412,
},
},
};
const narayana = {
periods: [
{ lord: "Aries", start: "2019-01-01", end: "2026-01-01" },
{ lord: "Taurus", start: "2026-01-01", end: "2033-01-01" },
],
};
const varga = {
result: {
D9_Navamsa: { ascendant: { sign: "Sagittarius" }, planets: { Moon: { sign: "Pisces" } } },
D10_Dasamsa: { ascendant: { sign: "Gemini" }, planets: { Moon: { sign: "Virgo" } } },
},
};
const transit = {
summary: {
total_triggers: 2,
top_triggers: [
{ planet: "Saturn", target: "Moon", event: "接近精确合相" },
{ planet: "Jupiter", target: "Ascendant", event: "三分相进入容许度" },
],
},
};
test("the daily card reads every engine layer it claims to use", () => {
const evidence = dailyStarlanguageEvidence({ chart, vimshottari, narayana, varga, transit }, today, true);
// Given: the chart, both dashas, the divisional charts and today's transits all answered.
assert.equal(evidence.ascendantSign, "Leo");
assert.equal(evidence.moonSign, "Cancer");
assert.deepEqual(evidence.vimshottari, {
mahadasha: "Venus",
antardasha: "Mercury",
remainingDays: 412,
nakshatra: "Pushya",
});
// And: the Narayana period is the one actually running today, not simply the first.
assert.equal(evidence.narayanaSign, "Taurus");
assert.deepEqual(evidence.divisional.map((entry) => entry.chart), ["D9_Navamsa", "D10_Dasamsa"]);
assert.equal(evidence.divisional[0].moonSign, "Pisces");
assert.equal(evidence.transit?.totalTriggers, 2);
assert.deepEqual(evidence.transit?.top, ["Saturn → Moon → 接近精确合相", "Jupiter → Ascendant → 三分相进入容许度"]);
assert.deepEqual(evidence.functionalBenefics, ["Mars", "Sun"]);
assert.deepEqual(evidence.functionalMalefics, ["Mercury"]);
assert.deepEqual(evidence.missingLayers, []);
});
test("layers the engine could not return are named instead of quietly missing", () => {
// Given: only the chart answered; the rest timed out and came back null.
const evidence = dailyStarlanguageEvidence(
{ chart, vimshottari: null, narayana: null, varga: null, transit: null },
today,
true,
);
// Then: the model is told which layers are absent, so it cannot pass them off as consulted.
assert.deepEqual(evidence.missingLayers, ["Vimshottari", "Narayana", "分盘", "过境"]);
assert.equal(evidence.vimshottari, null);
assert.equal(evidence.narayanaSign, null);
assert.deepEqual(evidence.divisional, []);
assert.equal(evidence.transit, null);
});
test("a blocked functional-role layer contributes nothing", () => {
const blocked = {
ai_prompt_pack: {
evidence_snapshot: {
functional_benefic_malefic: { status: "blocked", functional_benefics: [], functional_malefics: [] },
},
},
modules: chart.modules,
};
const evidence = dailyStarlanguageEvidence(
{ chart: blocked, vimshottari, narayana, varga, transit },
today,
true,
);
assert.deepEqual(evidence.functionalBenefics, []);
assert.deepEqual(evidence.functionalMalefics, []);
});
test("an unconfirmed birth time is stated to the model as a hard limit", () => {
const evidence = dailyStarlanguageEvidence({ chart, vimshottari, narayana, varga, transit }, today, false);
const prompt = dailyStarlanguagePrompt(evidence);
assert.match(prompt, /出生时间状态:未经校正确认/);
assert.match(prompt, /禁止任何依赖分钟精度的判断/);
assert.match(prompt, /2026-08-17/);
});
test("model output is accepted only as a complete card", () => {
const fenced = "```json\n{\"trend\":\"今天适合把一件悬着的事收口。\",\"action\":\"给最重要的一件事留出不被打断的时间。\",\"caution\":\"先别在情绪最满时下承诺。\"}\n```";
const card = parseDailyStarlanguageText(fenced);
assert.equal(card?.trend, "今天适合把一件悬着的事收口。");
assert.equal(card?.action, "给最重要的一件事留出不被打断的时间。");
// Then: partial, empty or non-JSON output is rejected rather than half-rendered.
assert.equal(parseDailyStarlanguageText("{\"trend\":\"今天适合把一件悬着的事收口。\"}"), null);
assert.equal(parseDailyStarlanguageText("今天没什么特别的。"), null);
assert.equal(parseDailyStarlanguageText("{\"trend\":\"太短\",\"action\":\"做事\",\"caution\":\"小心\"}"), null);
});
test("cached cards cannot cross accounts, profiles or days", () => {
const base = dailyStarlanguageCacheKey("account-1", "payload-a", today);
assert.notEqual(base, dailyStarlanguageCacheKey("account-2", "payload-a", today));
assert.notEqual(base, dailyStarlanguageCacheKey("account-1", "payload-b", today));
assert.notEqual(base, dailyStarlanguageCacheKey("account-1", "payload-a", "2026-08-18"));
assert.equal(base, dailyStarlanguageCacheKey("account-1", "payload-a", today));
});
test("the calendar day follows the birth timezone, not UTC", () => {
const utcEvening = new Date("2026-08-18T16:30:00.000Z");
assert.equal(calendarDateInTimeZone(utcEvening, "Asia/Shanghai"), "2026-08-19");
assert.equal(calendarDateInTimeZone(utcEvening, "America/Los_Angeles"), "2026-08-18");
assert.equal(calendarDateInTimeZone(utcEvening), "2026-08-18");
});
test("an evidence card is personal to this chart, not a four-line rotation", () => {
const venusTwelfth = dailyStarlanguageEvidence({ chart, vimshottari, narayana, varga, transit }, today, true);
const saturnFirst = dailyStarlanguageEvidence({
chart: {
modules: {
chart: {
ascendant: { sign: "Aries" },
planets: { Moon: { sign: "Aries" }, Sun: { sign: "Taurus" } },
},
},
},
vimshottari: {
vimshottari_analysis: {
nakshatra: { name: "Ashwini" },
current: {
mahadasha: { lord: "Saturn" },
antardasha: { lord: "Jupiter" },
remaining_days: 80,
},
},
},
narayana,
varga,
transit: { summary: { total_triggers: 0, top_triggers: [] } },
}, today, true);
const twelfth = composeDailyStarlanguageCard(venusTwelfth);
const first = composeDailyStarlanguageCard(saturnFirst);
assert.match(twelfth.trend, /休整/);
assert.match(twelfth.trend, /金星/);
assert.equal(twelfth.action, "先收尾一件未完成的事,再给自己留一段安静。");
assert.match(twelfth.caution, /观察窗口/);
assert.match(first.trend, /自我/);
assert.match(first.trend, /土星/);
assert.equal(first.action, "整理状态,把节奏重新起起来。");
assert.notEqual(twelfth.trend, first.trend);
assert.notEqual(twelfth.action, first.action);
for (const card of [twelfth, first]) {
assert.doesNotMatch(card.trend, /先收束,再推进|执行力比灵感更重要|适合观察资源流向/);
assert.doesNotMatch(card.action, /选一件最重要的事,给它留出 45 分钟/);
}
});
test("an unconfirmed birth time does not use the ascendant house", () => {
const evidence = dailyStarlanguageEvidence({ chart, vimshottari, narayana, varga, transit }, today, false);
const card = composeDailyStarlanguageCard(evidence);
assert.doesNotMatch(card.trend, /休整|自我|财务|沟通/);
assert.match(card.trend, /金星/);
assert.match(card.caution, /不依赖具体分钟/);
});
test("today's card follows the transiting moon house, not the natal moon house", () => {
const natalTwelfth = dailyStarlanguageEvidence({ chart, vimshottari, narayana, varga, transit }, today, true);
const eighthToday = evidenceFromDailyGuidance({
success: true,
date: today,
theme: "深度",
moon_house: 8,
moon_sign: "Pisces",
natal_moon_sign: "Cancer",
ascendant_sign: "Leo",
mahadasha_lord: "Venus",
}, today, true);
assert.equal(natalTwelfth.moonSign, "Cancer");
assert.match(composeDailyStarlanguageCard(eighthToday!).trend, /复盘/);
assert.doesNotMatch(composeDailyStarlanguageCard(eighthToday!).trend, /休整/);
assert.equal(composeDailyStarlanguageCard(eighthToday!).action, "清理一个旧问题,只推进到可复查的一步。");
});
test("a daily-guidance packet that did not compute is refused rather than padded", () => {
assert.equal(evidenceFromDailyGuidance({ success: false, moon_sign: "Libra" }, today, true), null);
assert.equal(evidenceFromDailyGuidance({ success: true }, today, true), null);
});
test("profile fingerprints ignore object identity and name-only edits", () => {
const base = {
date: "1990-06-15",
time: "12:30",
timezoneId: "Asia/Shanghai",
latitude: 31.2,
longitude: 121.5,
timezoneOffset: 8,
birthTimeStatus: "confirmed",
};
assert.equal(dailyStarlanguageProfileKey(base), dailyStarlanguageProfileKey({ ...base }));
assert.notEqual(dailyStarlanguageProfileKey(base), dailyStarlanguageProfileKey({ ...base, time: "12:31" }));
});
test("the homepage card is engine-backed first, with Agent polish off the request path", () => {
const route = readFileSync(new URL("../src/app/api/daily-starlanguage/route.ts", import.meta.url), "utf8");
const page = homeSurface;
const agents = readFileSync(new URL("../src/mastra/index.ts", import.meta.url), "utf8");
const generateCard = route.slice(route.indexOf("async function generateCard"), route.indexOf("function unavailable"));
const polish = route.slice(route.indexOf("function scheduleAgentPolish"), route.indexOf("async function generateCard"));
assert.match(route, /supabase\.auth\.getUser\(\)/);
assert.match(route, /\{ status: "unauthenticated" \}, \{ status: 401 \}/);
assert.match(route, /from\("profiles"\)/);
assert.match(route, /globalBirthProfileFromAccountRow/);
assert.match(route, /consumeUserRequestRateLimit\("dailyStarlanguage"/);
assert.doesNotMatch(route, /body\?\.profile/);
assert.doesNotMatch(route, /body\?\.today/);
assert.match(route, /dailyStarlanguageCacheKey\(user\.id, JSON\.stringify\(payload\), today\)/);
assert.match(route, /pending\(\)\.get\(key\)/);
assert.match(route, /"\/api\/daily_guidance"/);
assert.doesNotMatch(generateCard, /"\/api\/chart"/);
assert.doesNotMatch(generateCard, /"\/api\/dasha"/);
assert.doesNotMatch(generateCard, /"\/api\/varga_full"/);
assert.doesNotMatch(generateCard, /"\/api\/transit"/);
assert.match(agents, /getDailyStarlanguageAgent/);
assert.doesNotMatch(agents, /jyotish-daily-starlanguage[\s\S]{0,400}skills: \[jyotishSkillPath\]/);
assert.match(generateCard, /evidenceFromDailyGuidance\(packet/);
assert.match(generateCard, /composeDailyStarlanguageCard\(evidence\)/);
assert.doesNotMatch(generateCard, /getDailyStarlanguageAgent/);
assert.match(polish, /void polishWithAgent/);
assert.match(route, /scheduleAgentPolish\(key, today, generated\.evidence\)/);
assert.match(route, /getDailyStarlanguageAgent\(model\)\.generate\(/);
for (const source of [route, page]) {
assert.doesNotMatch(source, /先收束,再推进|执行力比灵感更重要|适合观察资源流向/);
}
assert.match(page, /fetchDailyStarlanguage\(controller\.signal\)/);
assert.match(page, /body: JSON\.stringify\(\{\}\)/);
assert.doesNotMatch(page, /JSON\.stringify\(\{ profile, today \}\)/);
assert.doesNotMatch(page, /buildDailyStarlanguageCard/);
assert.match(route, /status: "unavailable"/);
assert.match(page, /今天的星语还没写出来/);
assert.match(page, /从今日问起/);
assert.doesNotMatch(page, /点这里|不会用通用文案顶替|今天的星语没能生成/);
assert.match(page, /writeStoredDailyStarlanguage/);
});
test("the home requests the card exactly when it renders one, and retries a failed day once", () => {
const page = homeSurface;
const effect = page.slice(
page.indexOf("if (!hydrated || !accountId || !profileComplete || !natalMinuteAvailable) return;"),
page.indexOf("}, [accountId, dailyStarlanguageFingerprint, hydrated, natalMinuteAvailable, profileComplete]);"),
);
assert.match(page, /aria-busy=\{dailyStarlanguageBusy\}/);
assert.ok(effect.length > 0);
assert.doesNotMatch(page, /birthTimeDisplayState\(profile\)/);
assert.match(effect, /next\.kind === "unavailable" && remainingRetries > 0/);
assert.match(effect, /attempt\(1\);/);
assert.match(effect, /clearTimeout\(retryTimer\)/);
assert.match(effect, /readStoredDailyStarlanguage\(accountId\)/);
assert.match(effect, /if \(stored && stored\.day === today && stored\.fingerprint === fingerprint\) return;/);
});
test("no single engine layer can take the whole card down without saying which one", () => {
const route = readFileSync(new URL("../src/app/api/daily-starlanguage/route.ts", import.meta.url), "utf8");
assert.match(route, /fetchEngine\("\/api\/daily_guidance"/);
assert.match(route, /reason: "chart_unavailable"/);
assert.match(route, /return unavailable\(generated\.reason\)/);
assert.doesNotMatch(route, /reason: "model_unavailable"/);
assert.doesNotMatch(route, /reason: "agent_generation_failed"/);
assert.doesNotMatch(route, /fetchEngine\("\/api\/chart"/);
const engineTimeout = Number(route.match(/const engineTimeoutMs = ([\d_]+);/)?.[1]?.replace(/_/g, ""));
const maxDuration = Number(route.match(/export const maxDuration = (\d+);/)?.[1]);
assert.ok(engineTimeout <= 8_000, `homepage must not wait on a full chart: ${engineTimeout}`);
assert.ok(Number.isFinite(maxDuration) && maxDuration <= 20, `route budget still too wide for first paint: ${maxDuration}`);
assert.ok(engineTimeout < (maxDuration ?? 0) * 1000, "engine timeout must fit inside the route budget");
assert.match(route, /composeDailyStarlanguageCard/);
});