Files
Jyotisha/frontend/tests/rectification-varga-style-copy.test.ts
T
Jesse_Chen 95f5851261
Independent Staging Quality Gate / validate (push) Successful in 9m28s
Independent Staging Quality Gate / publish (push) Successful in 7m26s
fix(rectification): rewrite D9/D10 style options and stem spacing
Choice cards were showing type-table tokens as options, dating long-term style questions, and stacking the stem on the first option because legend is outside the fieldset grid.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 10:01:32 +08:00

351 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { buildCandidateContrastPacket, selectDiscriminatorProbe } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
import { buildChoiceFrame, serverOwnedChoiceCopy } from "../src/lib/rectification-agentic/v9/choice-card.ts";
import {
EXISTENCE_STYLE_OPTIONS,
LABEL_MAX,
LABEL_MIN,
QUALITY_STYLE_OPTIONS,
completeStyleOptions,
isRenderableStyleOptions,
} from "../src/lib/rectification-agentic/v9/probe-question-contract.ts";
import type { DiscriminatingEventProbe } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
import {
D9_TYPE_TABLE,
D10_TYPE_TABLE,
d9StyleLabel,
d10StyleLabel,
} from "../src/lib/rectification-agentic/v9/varga-type-tables.ts";
const SIGNS = [
"白羊座", "金牛座", "双子座", "巨蟹座", "狮子座", "处女座",
"天秤座", "天蝎座", "射手座", "摩羯座", "水瓶座", "双鱼座",
] as const;
const FROZEN_D9 = {
: { trait: "主动、热情、冲动", spouse: "独立、有活力", marriage: "早期婚姻、激情型" },
: { trait: "稳定、务实、占有欲强", spouse: "稳重、有经济基础", marriage: "晚婚、稳定型" },
: { trait: "沟通、多变、好奇心强", spouse: "聪明、善交流", marriage: "多次恋爱、友谊型" },
: { trait: "情感丰富、家庭导向", spouse: "温柔、顾家", marriage: "家庭型" },
: { trait: "骄傲、戏剧性、领导欲", spouse: "有魅力、有地位", marriage: "戏剧性" },
: { trait: "完美主义、挑剔、服务型", spouse: "细致、有技能", marriage: "晚婚、服务型" },
: { trait: "和谐、美感、合作", spouse: "优雅、有艺术气质", marriage: "美满、合作型" },
: { trait: "深刻、占有欲、转化", spouse: "神秘、有深度", marriage: "深刻、转化型" },
: { trait: "自由、哲学、冒险", spouse: "开放、有学识", marriage: "自由型、精神伴侣" },
: { trait: "务实、责任、延迟", spouse: "成熟、有事业", marriage: "晚婚、责任型" },
: { trait: "独立、非传统、友谊", spouse: "独特、有理想", marriage: "非传统、友谊型" },
: { trait: "浪漫、牺牲、灵性", spouse: "灵性、有艺术天赋", marriage: "灵性、牺牲型" },
} as const;
const FROZEN_D10 = {
: { trait: "领导、创业、竞争", occupation: "创业者、运动员、军人", style: "主动、竞争" },
: { trait: "稳定、财富、艺术", occupation: "金融、艺术、农业", style: "稳定、务实" },
: { trait: "沟通、写作、教育", occupation: "教师、作家、销售", style: "多变、沟通" },
: { trait: "照顾、家庭、情感", occupation: "护理、餐饮、房地产", style: "照顾、情感" },
: { trait: "领导、表演、创意", occupation: "管理、娱乐、政治", style: "领导、表演" },
: { trait: "服务、分析、健康", occupation: "医疗、分析、服务", style: "细致、服务" },
: { trait: "合作、美学、法律", occupation: "法律、艺术、咨询", style: "合作、和谐" },
: { trait: "研究、转化、危机", occupation: "研究、心理学、危机管理", style: "深度、转化" },
: { trait: "教育、哲学、国际", occupation: "教育、出版、国际事务", style: "自由、哲学" },
: { trait: "管理、责任、延迟", occupation: "管理、政府、建筑", style: "务实、责任" },
: { trait: "创新、科技、人道", occupation: "科技、人道主义、创新", style: "创新、独立" },
: { trait: "灵性、艺术、服务", occupation: "艺术、灵性、医疗", style: "灵性、服务" },
} as const;
const FROZEN_EXISTENCE = [
{ label: "明确发生且时间吻合", answer_class: "yes" },
{ label: "发生过但程度较弱", answer_class: "weak_yes" },
{ label: "明确没有发生", answer_class: "no" },
{ label: "这段记不清楚", answer_class: "unsure" },
] as const;
const FROZEN_QUALITY = [
{ label: "发挥明显失常或压力很大", answer_class: "yes" },
{ label: "有压力但不算明显失常", answer_class: "weak_yes" },
{ label: "发挥正常、没有明显失常", answer_class: "no" },
{ label: "这段记不清楚", answer_class: "unsure" },
] as const;
const YEAR_OR_SPAN = /\d{4}|这段/;
function styleCard(
domain: "relationship" | "career",
signs: readonly [string, string, string],
labelFor: (sign: string) => string,
): ReturnType<typeof buildChoiceFrame> {
const probe: DiscriminatingEventProbe = {
year: 2024,
year_label: "20242026 年这段里",
domain,
event_family: domain === "relationship" ? "相处方式更接近其中一种" : "做事风格更接近其中一种",
source: "dasha_activation",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: "对照类型表只作校时方法,不是命运承诺。",
role: "distinguish",
information_gain: 1.2,
candidate_ids: ["05:00", "05:03", "05:04"],
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:03", "05:04"] },
{ answer_class: "weak_yes", supports: ["05:03"], conflicts: ["05:00", "05:04"] },
{ answer_class: "no", supports: ["05:04"], conflicts: ["05:00", "05:03"] },
],
choice_kind: "varga_style",
semantic_key: domain === "relationship" ? "varga.d9.05:00|05:03/05:04" : "varga.d10.05:00|05:03/05:04",
style_options: [
{ sign: signs[0], label: labelFor(signs[0]), answer_class: "yes" },
{ sign: signs[1], label: labelFor(signs[1]), answer_class: "weak_yes" },
{ sign: signs[2], label: labelFor(signs[2]), answer_class: "no" },
],
};
return buildChoiceFrame({
method_id: domain === "relationship" ? "d9_refine" : "d10_career",
ask_theme: domain === "relationship" ? "relationship_style" : "career_style",
domain,
user_prompt_hint: "unused",
choice_kind: "varga_style",
semantic_key: probe.semantic_key,
style_options: probe.style_options,
}, {
evidence: [
{ status: "confirmed", domain, datePrecision: "year", occurredFrom: "2024-01-01", occurredTo: null },
{ status: "confirmed", domain, datePrecision: "year", occurredFrom: "2026-01-01", occurredTo: null },
],
probes: [probe],
});
}
test("D9/D10 interpretation fields stay frozen; userChoice is the card label", () => {
assert.deepEqual(Object.keys(D9_TYPE_TABLE), [...SIGNS]);
assert.deepEqual(Object.keys(D10_TYPE_TABLE), [...SIGNS]);
for (const sign of SIGNS) {
const d9 = D9_TYPE_TABLE[sign];
const d10 = D10_TYPE_TABLE[sign];
assert.ok(d9);
assert.ok(d10);
assert.deepEqual({ trait: d9.trait, spouse: d9.spouse, marriage: d9.marriage }, FROZEN_D9[sign]);
assert.deepEqual(
{ trait: d10.trait, occupation: d10.occupation, style: d10.style },
FROZEN_D10[sign],
);
assert.ok(d9.userChoice.trim().length >= LABEL_MIN && d9.userChoice.length <= LABEL_MAX);
assert.ok(d10.userChoice.trim().length >= LABEL_MIN && d10.userChoice.length <= LABEL_MAX);
assert.notEqual(d9.userChoice, d9.trait);
assert.notEqual(d10.userChoice, d10.style);
assert.notEqual(d10.userChoice, d10.trait);
assert.equal(d9StyleLabel(sign), d9.userChoice);
assert.equal(d10StyleLabel(sign), d10.userChoice);
assert.equal(d9StyleLabel(sign.replace(/座$/, "")), d9.userChoice);
assert.equal(d10StyleLabel(sign.replace(/座$/, "")), d10.userChoice);
}
assert.equal(d9StyleLabel("天秤座"), "尽量避免冲突,凡事想商量着来");
assert.equal(d9StyleLabel("天蝎座"), "投入很深、在意独占,也容易大起大落");
assert.equal(d9StyleLabel("射手座"), "看重各自的空间,不喜欢被绑太紧");
assert.equal(d10StyleLabel("巨蟹座"), "做事以照顾人为主,在意团队里的感受");
assert.equal(d10StyleLabel("狮子座"), "习惯带头,也不排斥站到台前");
assert.equal(d10StyleLabel("处女座"), "盯细节、把事做扎实,偏后端支持");
});
test("varga_style options keep sign → answer_class mapping and are renderable", () => {
const d9 = completeStyleOptions({
choiceKind: "varga_style",
styleOptions: [
{ sign: "天秤座", label: d9StyleLabel("天秤座"), answer_class: "yes" },
{ sign: "天蝎座", label: d9StyleLabel("天蝎座"), answer_class: "weak_yes" },
{ sign: "射手座", label: d9StyleLabel("射手座"), answer_class: "no" },
],
});
assert.equal(d9.ok, true);
if (!d9.ok) return;
assert.equal(isRenderableStyleOptions(d9.options), true);
assert.equal(d9.options.find((item) => item.answer_class === "yes")?.sign, "天秤座");
assert.equal(d9.options.find((item) => item.answer_class === "weak_yes")?.sign, "天蝎座");
assert.equal(d9.options.find((item) => item.answer_class === "no")?.sign, "射手座");
assert.equal(d9.options.find((item) => item.answer_class === "yes")?.label, D9_TYPE_TABLE..userChoice);
assert.notEqual(d9.options.find((item) => item.answer_class === "yes")?.label, D9_TYPE_TABLE..trait);
const no = d9.options.find((item) => item.answer_class === "no")?.label ?? "";
const unsure = d9.options.find((item) => item.answer_class === "unsure")?.label ?? "";
assert.doesNotMatch(no, /特质/);
assert.doesNotMatch(unsure, /特质|这段/);
assert.equal(unsure, "一时说不好");
const d10 = completeStyleOptions({
choiceKind: "varga_style",
styleOptions: [
{ sign: "巨蟹座", label: d10StyleLabel("巨蟹座"), answer_class: "yes" },
{ sign: "狮子座", label: d10StyleLabel("狮子座"), answer_class: "weak_yes" },
{ sign: "处女座", label: d10StyleLabel("处女座"), answer_class: "no" },
],
});
assert.equal(d10.ok, true);
if (!d10.ok) return;
assert.equal(isRenderableStyleOptions(d10.options), true);
assert.equal(d10.options.find((item) => item.answer_class === "yes")?.sign, "巨蟹座");
assert.equal(d10.options.find((item) => item.answer_class === "weak_yes")?.sign, "狮子座");
assert.equal(d10.options.find((item) => item.answer_class === "no")?.sign, "处女座");
assert.notEqual(d10.options.find((item) => item.answer_class === "yes")?.label, D10_TYPE_TABLE..style);
const filled = completeStyleOptions({
choiceKind: "varga_style",
styleOptions: [
{ sign: "天秤座", label: d9StyleLabel("天秤座"), answer_class: "yes" },
{ sign: "天蝎座", label: d9StyleLabel("天蝎座"), answer_class: "weak_yes" },
],
});
assert.equal(filled.ok, true);
if (!filled.ok) return;
assert.equal(filled.options.find((item) => item.answer_class === "no")?.label, "都不太像我");
assert.equal(filled.options.find((item) => item.answer_class === "unsure")?.label, "一时说不好");
assert.doesNotMatch(filled.options.find((item) => item.answer_class === "no")?.label ?? "", /特质/);
assert.doesNotMatch(filled.options.find((item) => item.answer_class === "unsure")?.label ?? "", /这段/);
});
test("existence and event_quality fixed options stay on the old copy", () => {
assert.deepEqual(EXISTENCE_STYLE_OPTIONS, [...FROZEN_EXISTENCE]);
assert.deepEqual(QUALITY_STYLE_OPTIONS, [...FROZEN_QUALITY]);
const existence = completeStyleOptions({ choiceKind: "existence" });
const quality = completeStyleOptions({ choiceKind: "event_quality" });
assert.equal(existence.ok && quality.ok, true);
if (!existence.ok || !quality.ok) return;
assert.deepEqual(existence.options, [...FROZEN_EXISTENCE]);
assert.deepEqual(quality.options, [...FROZEN_QUALITY]);
assert.equal(existence.options.find((item) => item.answer_class === "unsure")?.label, "这段记不清楚");
assert.equal(quality.options.find((item) => item.answer_class === "unsure")?.label, "这段记不清楚");
});
test("varga_style stems are long-term; existence and quality still lock the year", () => {
const relationship = styleCard("relationship", ["天秤座", "天蝎座", "射手座"], d9StyleLabel);
const career = styleCard("career", ["巨蟹座", "狮子座", "处女座"], d10StyleLabel);
assert.ok(relationship);
assert.ok(career);
const relationshipCopy = relationship ? serverOwnedChoiceCopy(relationship) : null;
const careerCopy = career ? serverOwnedChoiceCopy(career) : null;
assert.ok(relationshipCopy);
assert.ok(careerCopy);
assert.doesNotMatch(relationship!.prompt, YEAR_OR_SPAN);
assert.doesNotMatch(career!.prompt, YEAR_OR_SPAN);
assert.doesNotMatch(relationship!.period, YEAR_OR_SPAN);
assert.doesNotMatch(career!.period, YEAR_OR_SPAN);
assert.equal(relationship!.prompt, "亲密关系里,你更接近哪一种相处方式?");
assert.equal(career!.prompt, "平时做事,你更接近下面哪一种?");
assert.doesNotMatch(relationshipCopy!.prompt, YEAR_OR_SPAN);
assert.doesNotMatch(careerCopy!.prompt, YEAR_OR_SPAN);
assert.match(relationship!.why, /校时方法/);
assert.match(career!.why, /校时方法/);
const existence = buildChoiceFrame({
method_id: "d4_home",
ask_theme: "home_change",
domain: "relocation",
user_prompt_hint: "unused",
choice_kind: "existence",
}, {
probes: [{
year: 2016,
year_label: "2016 年前后",
domain: "relocation",
event_family: "搬家、离乡或长期异地",
source: "dasha_activation",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: "年份锁定 2016 年前后。",
role: "distinguish",
information_gain: 0.4,
candidate_ids: ["05:00", "05:20"],
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:20"] },
{ answer_class: "no", supports: ["05:20"], conflicts: ["05:00"] },
],
}],
});
assert.ok(existence);
assert.match(existence!.prompt, /2016 年前后/);
assert.match(existence!.prompt, /搬家、离乡或长期异地/);
assert.equal(existence!.unsure_label, "这段记不清楚");
const quality = buildChoiceFrame({
method_id: "d24_learn",
ask_theme: "education_style",
domain: "education",
user_prompt_hint: "unused",
choice_kind: "event_quality",
}, {
probes: [{
year: 2020,
year_label: "2020 年前后",
domain: "education",
event_family: "学业或考试发挥失常、压力特别大",
source: "known_event_quality",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: "年份锁定 2020 年前后。",
role: "distinguish",
information_gain: 0.5,
candidate_ids: ["05:00", "05:20"],
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:20"] },
{ answer_class: "no", supports: ["05:20"], conflicts: ["05:00"] },
],
choice_kind: "event_quality",
}],
});
assert.ok(quality);
assert.match(quality!.prompt, /2020 年前后/);
assert.equal(quality!.unsure_label, "这段记不清楚");
});
test("remaining D9/D10 packets use userChoice labels and long-term questions", () => {
const d9 = selectDiscriminatorProbe(buildCandidateContrastPacket({
candidateSetVersion: "05:00-05:04",
candidateTimes: ["05:00", "05:03", "05:04"],
transitions: [
{ layer: "d9", at: "05:03", from_sign: "天秤座", to_sign: "天蝎座" },
{ layer: "d9", at: "05:04", from_sign: "天蝎座", to_sign: "射手座" },
],
}));
assert.ok(d9);
assert.equal(d9!.choiceKind, "varga_style");
assert.doesNotMatch(d9!.question, YEAR_OR_SPAN);
assert.equal(d9!.question, "亲密关系里,你更接近哪一种相处方式?");
assert.equal(d9!.styleOptions?.find((item) => item.answerClass === "yes")?.sign, "天秤座");
assert.equal(d9!.styleOptions?.find((item) => item.answerClass === "weak_yes")?.sign, "天蝎座");
assert.equal(d9!.styleOptions?.find((item) => item.answerClass === "no")?.sign, "射手座");
assert.equal(d9!.styleOptions?.find((item) => item.answerClass === "yes")?.label, d9StyleLabel("天秤座"));
assert.notEqual(d9!.styleOptions?.find((item) => item.answerClass === "yes")?.label, D9_TYPE_TABLE..trait);
const d10 = selectDiscriminatorProbe(buildCandidateContrastPacket({
candidateSetVersion: "05:00-05:04",
candidateTimes: ["05:00", "05:03", "05:04"],
transitions: [
{ layer: "d10", at: "05:03", from_sign: "巨蟹座", to_sign: "狮子座" },
{ layer: "d10", at: "05:04", from_sign: "狮子座", to_sign: "处女座" },
],
}));
assert.ok(d10);
assert.equal(d10!.choiceKind, "varga_style");
assert.doesNotMatch(d10!.question, YEAR_OR_SPAN);
assert.equal(d10!.question, "平时做事,你更接近下面哪一种?");
assert.equal(d10!.styleOptions?.find((item) => item.answerClass === "yes")?.label, d10StyleLabel("巨蟹座"));
assert.notEqual(d10!.styleOptions?.find((item) => item.answerClass === "yes")?.label, D10_TYPE_TABLE..style);
});
test("choice-card legend spacing does not depend on fieldset grid gap", () => {
const choiceCss = readFileSync(new URL("../src/app/birth-time-choice.css", import.meta.url), "utf8");
const globals = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const card = readFileSync(new URL("../src/components/rectification-choice-card.tsx", import.meta.url), "utf8");
const legendRule = choiceCss.match(/\.birth-time-choice-question legend \{[^}]+\}/)?.[0] ?? "";
const whyRule = globals.match(/\.rectification-choice-why \{[^}]+\}/)?.[0] ?? "";
assert.match(legendRule, /margin:\s*0\s+0\s+var\(--space-[3-9]\)/);
assert.match(whyRule, /margin:\s*0\s+0\s+var\(--space-[3-9]\)/);
assert.match(card, /<legend>\{props\.card\.prompt\}<\/legend>/);
assert.match(card, /className="rectification-choice-why"/);
assert.doesNotMatch(card, /choice-question-wrap|choice-stem-wrap|legend-spacer/);
assert.match(card, /<legend>\{props\.card\.prompt\}<\/legend>\s*\{props\.card\.why \? <p className="rectification-choice-why">/);
});