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 { 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 { const found = new Map(); 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\(/); });