Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
117 lines
5.0 KiB
TypeScript
117 lines
5.0 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import test from "node:test";
|
||
|
||
import { dailyStarlanguageCardSchema, type DailyStarlanguageEvidence } from "../src/lib/daily-starlanguage.ts";
|
||
import {
|
||
HOUSE_ACTIONS,
|
||
HOUSE_CAUTIONS,
|
||
HOUSE_LINES,
|
||
PHASE_TAILS,
|
||
SIGN_MOODS,
|
||
composeDailyCard,
|
||
lunarPhase,
|
||
moonAgeDays,
|
||
type LunarPhase,
|
||
} from "../src/lib/daily-starlanguage-library.ts";
|
||
|
||
// BUG-1109: the home 「今日星语」 comes from a fixed library, not a model.
|
||
|
||
function evidence(overrides: Partial<DailyStarlanguageEvidence> = {}): DailyStarlanguageEvidence {
|
||
return {
|
||
today: "2026-09-30",
|
||
birthTimeVerified: true,
|
||
ascendantSign: "Aries",
|
||
moonSign: "Cancer",
|
||
transitMoonSign: "Capricorn",
|
||
transitMoonHouse: 10,
|
||
vimshottari: { mahadasha: "Saturn", antardasha: null, remainingDays: null, nakshatra: null },
|
||
narayanaSign: null,
|
||
divisional: [],
|
||
transit: null,
|
||
functionalBenefics: [],
|
||
functionalMalefics: [],
|
||
missingLayers: [],
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
/** One date per phase and per variant: walk forward until every (phase, variant) pair is covered. */
|
||
function datesCovering(): Map<string, string> {
|
||
const found = new Map<string, string>();
|
||
const start = Date.UTC(2026, 0, 1);
|
||
for (let offset = 0; offset < 400 && found.size < 12; offset += 1) {
|
||
const date = new Date(start + offset * 86_400_000).toISOString().slice(0, 10);
|
||
const day = Math.floor((start + offset * 86_400_000) / 86_400_000);
|
||
const key = `${lunarPhase(date)}:${day % 3}`;
|
||
if (!found.has(key)) found.set(key, date);
|
||
}
|
||
return found;
|
||
}
|
||
|
||
test("the library is complete: 12 houses × 3 lines, 4 phases × 2 tails, 2 actions and 2 cautions per house, 12 sign moods", () => {
|
||
for (let house = 1; house <= 12; house += 1) {
|
||
assert.equal(HOUSE_LINES[house]?.length, 3, `house ${house}`);
|
||
assert.equal(HOUSE_ACTIONS[house]?.length, 2);
|
||
assert.equal(HOUSE_CAUTIONS[house]?.length, 2);
|
||
}
|
||
for (const phase of ["new", "waxing", "full", "waning"] as LunarPhase[]) assert.equal(PHASE_TAILS[phase].length, 2);
|
||
assert.equal(Object.keys(SIGN_MOODS).length, 12);
|
||
});
|
||
|
||
test("every house × phase × variant composes a valid card within the schema limits", () => {
|
||
const dates = datesCovering();
|
||
assert.equal(dates.size, 12, "all four phases × three variants are reachable");
|
||
let cards = 0;
|
||
for (let house = 1; house <= 12; house += 1) {
|
||
for (const date of dates.values()) {
|
||
const card = composeDailyCard(evidence({ transitMoonHouse: house, today: date }), date);
|
||
const parsed = dailyStarlanguageCardSchema.safeParse(card);
|
||
assert.ok(parsed.success, `${house} ${date}: ${JSON.stringify(card)}`);
|
||
assert.ok(card.trend.length <= 90 && card.action.length <= 90 && card.caution.length <= 90);
|
||
assert.doesNotMatch(card.trend, /宫主|相位|大运|第\d+宫/, "no jargon in the trend line");
|
||
cards += 1;
|
||
}
|
||
}
|
||
assert.equal(cards, 144);
|
||
});
|
||
|
||
test("same date, same card; consecutive days in the same house read differently", () => {
|
||
const today = composeDailyCard(evidence(), "2026-09-30");
|
||
assert.deepEqual(composeDailyCard(evidence(), "2026-09-30"), today);
|
||
const tomorrow = composeDailyCard(evidence(), "2026-10-01");
|
||
assert.notEqual(tomorrow.trend, today.trend);
|
||
});
|
||
|
||
test("the lunar phase follows the synodic month from a known new moon", () => {
|
||
// 2000-01-06 was a new moon; about 14.8 days later a full moon.
|
||
assert.equal(lunarPhase("2000-01-06"), "new");
|
||
assert.equal(lunarPhase("2000-01-21"), "full");
|
||
assert.equal(lunarPhase("2000-01-13"), "waxing");
|
||
assert.equal(lunarPhase("2000-01-28"), "waning");
|
||
// Known new moon 2026-09-11 (±1 day) and full moon 2026-09-26.
|
||
assert.equal(lunarPhase("2026-09-11"), "new");
|
||
assert.equal(lunarPhase("2026-09-26"), "full");
|
||
const age = moonAgeDays("2026-09-11");
|
||
assert.ok(age < 1.5 || age > 28, `new moon within about a day, age ${age}`);
|
||
});
|
||
|
||
test("without a confirmed birth time the house is not used; the line falls back to the Moon's sign", () => {
|
||
const card = composeDailyCard(evidence({ birthTimeVerified: false }), "2026-09-30");
|
||
assert.match(card.trend, /^今天月亮在摩羯座,/);
|
||
assert.match(card.caution, /出生时间尚未确认/);
|
||
assert.ok(dailyStarlanguageCardSchema.safeParse(card).success);
|
||
const noHouse = composeDailyCard(evidence({ transitMoonHouse: null }), "2026-09-30");
|
||
assert.match(noHouse.trend, /^今天月亮在摩羯座,/);
|
||
// Neither house nor sign: the older evidence composer still gives a valid card.
|
||
const bare = composeDailyCard(evidence({ transitMoonHouse: null, transitMoonSign: null }), "2026-09-30");
|
||
assert.ok(dailyStarlanguageCardSchema.safeParse(bare).success);
|
||
});
|
||
|
||
test("the route calls no model for the daily line", () => {
|
||
const route = readFileSync(new URL("../src/app/api/daily-starlanguage/route.ts", import.meta.url), "utf8");
|
||
assert.match(route, /composeDailyCard\(evidence, today\)/);
|
||
assert.match(route, /source: "library"/);
|
||
assert.doesNotMatch(route, /@\/mastra|getDailyStarlanguageAgent|loadLanguageModelCatalog|generate\(/);
|
||
});
|