9958e00abc
Rectification stays optional. Reported minutes can consult and generate reports; date-plus-period uses a declared window instead of a midpoint or 00:00. Updates BUG-341. Co-authored-by: Cursor <cursoragent@cursor.com>
342 lines
14 KiB
TypeScript
342 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";
|
|
|
|
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 = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
|
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 = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
|
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/);
|
|
});
|