Independent Staging Quality Gate / validate (pull_request) Successful in 9m16s
Independent Staging Quality Gate / publish (pull_request) Has been skipped
Independent Staging Quality Gate / validate (push) Successful in 9m44s
Independent Staging Quality Gate / publish (push) Successful in 8m41s
The daily starlanguage card sat on "正在结合你的星盘写今天的星语。" forever for every account whose birth time was usable. Its effect bailed out on birthTimeDisplayState(profile), which returns a value precisely when the birth time is candidate, accepted or confirmed, so the request went out only for accounts that had nothing to read. The guard predates the Agent rewrite and was masked by the written-in client fallback that rewrite deleted. It now gates on personalChartAvailable, the same fact the card uses to render personal content, and retries once before admitting that today has no card. The route stops letting one engine call take the whole card down silently: /api/chart fails into a named reason like the other four layers, and the engine and agent budgets leave room for a cold chart and an observed 30s generation inside the 60s ceiling. The home also had three greeting implementations. The hero heading drew from a static pool while the time-aware greeting lived elsewhere and the Agent's own greeting was overwritten client-side into a field nothing rendered. createStartGreeting now exposes its salutation and question halves, the hero uses both, and the served greeting reaches the hero note. Co-authored-by: Cursor <cursoragent@cursor.com>
220 lines
9.4 KiB
TypeScript
220 lines
9.4 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
dailyStarlanguageCacheKey,
|
|
dailyStarlanguageEvidence,
|
|
dailyStarlanguagePrompt,
|
|
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 daily card is Agent-written per signed-in account, with no written-in card left anywhere", () => {
|
|
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");
|
|
|
|
// Given: generation costs tokens, so it is gated on a session and reused per account per day.
|
|
assert.match(route, /supabase\.auth\.getUser\(\)/);
|
|
assert.match(route, /\{ status: "unauthenticated" \}, \{ status: 401 \}/);
|
|
assert.match(route, /dailyStarlanguageCacheKey\(user\.id, JSON\.stringify\(payload\), today\)/);
|
|
assert.match(route, /pending\(\)\.get\(key\)/);
|
|
|
|
// And: the answer comes from the Agent over engine evidence, never from a rotation of written cards.
|
|
assert.match(route, /getDailyStarlanguageAgent\(model\)\.generate\(/);
|
|
assert.match(route, /"\/api\/dasha", \{ \.\.\.withPoints, dasha: "vimshottari"/);
|
|
assert.match(route, /"\/api\/dasha", \{ \.\.\.withPoints, dasha: "narayana"/);
|
|
assert.match(route, /"\/api\/varga_full"/);
|
|
assert.match(agents, /getDailyStarlanguageAgent/);
|
|
|
|
// Then: a failed generation degrades honestly instead of printing a generic reading.
|
|
for (const source of [route, page]) {
|
|
assert.doesNotMatch(source, /先收束,再推进|执行力比灵感更重要|适合观察资源流向/);
|
|
}
|
|
assert.doesNotMatch(page, /buildDailyStarlanguageCard/);
|
|
assert.match(route, /status: "unavailable"/);
|
|
assert.match(page, /今天的星语没能生成/);
|
|
});
|
|
|
|
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 || !profileComplete || !personalChartAvailable) return;"),
|
|
page.indexOf("}, [hydrated, personalChartAvailable, profile, profileComplete]);"),
|
|
);
|
|
|
|
// Given: the card only renders personal content behind personalChartAvailable, so the fetch is gated on the same fact.
|
|
assert.match(page, /aria-busy=\{personalChartAvailable && dailyStarlanguage\.kind === "pending"\}/);
|
|
assert.ok(effect.length > 0);
|
|
|
|
// Then: an accepted, candidate or confirmed birth time no longer suppresses the request.
|
|
assert.doesNotMatch(page, /birthTimeDisplayState\(profile\)/);
|
|
|
|
// And: one transient failure is retried before the card admits defeat, and the timer is cleared on teardown.
|
|
assert.match(effect, /next\.kind === "unavailable" && remainingRetries > 0/);
|
|
assert.match(effect, /attempt\(1\);/);
|
|
assert.match(effect, /clearTimeout\(retryTimer\)/);
|
|
});
|
|
|
|
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");
|
|
|
|
// Given: every engine call, the chart included, fails into a reason instead of throwing the request away.
|
|
assert.match(route, /fetchEngine\("\/api\/chart", payload\)\.catch\(/);
|
|
assert.match(route, /reason: "chart_unavailable"/);
|
|
assert.match(route, /reason: "model_unavailable"/);
|
|
assert.match(route, /return unavailable\(generated\.reason\)/);
|
|
|
|
// Then: the budgets leave room for a cold engine and a slow model inside the route's 60s ceiling.
|
|
const engineTimeout = Number(route.match(/const engineTimeoutMs = ([\d_]+);/)?.[1]?.replace(/_/g, ""));
|
|
const agentTimeout = Number(route.match(/const agentTimeoutMs = ([\d_]+);/)?.[1]?.replace(/_/g, ""));
|
|
assert.ok(engineTimeout >= 15_000, `engine timeout too tight for a cold chart: ${engineTimeout}`);
|
|
assert.ok(agentTimeout >= 45_000, `agent timeout too tight for observed generation: ${agentTimeout}`);
|
|
assert.match(route, /export const maxDuration = 60;/);
|
|
});
|