feat: P1 合盘/星座/问答与测测完整设计包及模拟器取证工具

落地 synastry/star/ask API 与 H5 页面,补齐 cece-frontend-re complete-design 证据文档,并加入 Android 模拟器截图抓取脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-03 11:37:53 +08:00
co-authored by Cursor
parent 15a9db374a
commit bd22d9dddd
248 changed files with 26309 additions and 842 deletions
+405 -25
View File
@@ -1,11 +1,15 @@
// Package relation builds 关系理解 content from two birth-based portraits.
// Package relation builds 人格匹配 / 合盘向 content from two profiles.
package relation
import (
"fmt"
"math"
"time"
"github.com/yuxingu/digital-psychology/apps/api/internal/portrait"
"github.com/yuxingu/digital-psychology/apps/api/internal/star"
"github.com/yuxingu/digital-psychology/apps/api/internal/star/natal"
"github.com/yuxingu/digital-psychology/apps/api/internal/star/synastry"
)
// Output is free summary + gated detail.
@@ -14,8 +18,18 @@ type Output struct {
Detail map[string]any
}
// Build compares two profiles' exploration styles (lexicon-safe copy).
// Build compares two profiles' exploration styles.
func Build(aBirth, bBirth time.Time, aName, bName string) Output {
return BuildWithType(aBirth, bBirth, aName, bName, "")
}
// BuildWithType adds relation-type flavored tips (partner/family/friend).
func BuildWithType(aBirth, bBirth time.Time, aName, bName, relationType string) Output {
return BuildFull(aBirth, bBirth, nil, nil, nil, nil, aName, bName, relationType)
}
// BuildFull includes birth time/place for natal synastry indices.
func BuildFull(aBirth, bBirth time.Time, aTime, bTime, aPlace, bPlace *string, aName, bName, relationType string) Output {
if aName == "" {
aName = "我"
}
@@ -24,41 +38,393 @@ func Build(aBirth, bBirth time.Time, aName, bName string) Output {
}
pa := portrait.Build(aBirth, aName)
pb := portrait.Build(bBirth, bName)
aLabel := str(pa.Summary["headline"])
bLabel := str(pb.Summary["headline"])
aLabel := firstNonEmpty(str(pa.Summary["style_label"]), str(pa.Summary["headline"]))
bLabel := firstNonEmpty(str(pb.Summary["style_label"]), str(pb.Summary["headline"]))
aKeys := strSlice(pa.Summary["keywords"])
bKeys := strSlice(pb.Summary["keywords"])
aDims := dimMap(pa.Summary["dimensions"])
bDims := dimMap(pb.Summary["dimensions"])
compKey := complementarityKey(aLabel, bLabel)
comp := complementarityCopy(compKey, aName, bName, aLabel, bLabel)
dimCompare := []map[string]any{}
for _, key := range []string{"personality", "communication", "relation", "career", "emotion", "lifestyle"} {
title := dimTitle(key)
as, aok := aDims[key]
bs, bok := bDims[key]
if !aok || !bok {
continue
}
gap := as.Score - bs.Score
abs := int(math.Abs(float64(gap)))
note := dimNote(key, gap, aName, bName)
dimCompare = append(dimCompare, map[string]any{
"key": key, "title": title,
"me_score": as.Score, "other_score": bs.Score,
"me_teaser": as.Teaser, "other_teaser": bs.Teaser,
"gap": abs, "note": note,
})
}
aSun, aMoon, aRise := star.SignLabelsPlace(aBirth, aTime, aPlace)
bSun, bMoon, bRise := star.SignLabelsPlace(bBirth, bTime, bPlace)
ca, errA := star.NatalChart(aBirth, aTime, aPlace)
cb, errB := star.NatalChart(bBirth, bTime, bPlace)
if errA != nil || errB != nil {
ca, cb = natal.Chart{}, natal.Chart{}
}
match := synastry.Compute(ca, cb, aName, bName)
harmony := harmonyIndex(dimCompare)
fitLabel, fitTips := fitFromHarmony(harmony, aLabel, bLabel)
summary := map[string]any{
"title": "关系理解·基础",
"me_style": aLabel,
"other_style": bLabel,
"me_keywords": aKeys,
"other_keywords": bKeys,
"diff_one_liner": fmt.Sprintf("%s更偏内在节奏,%s的表达方式不同——差异可以成为互补,而不是对立。", aName, bName),
"share_hint": "了解彼此的沟通方式,查看关系理解",
"title": "人格匹配·基础",
"me_style": aLabel,
"other_style": bLabel,
"me_keywords": aKeys,
"other_keywords": bKeys,
"diff_one_liner": comp.OneLiner,
"overview": comp.Overview,
"chemistry": comp.Chemistry,
"watchouts_preview": comp.Watchouts[:min(2, len(comp.Watchouts))],
"dimension_compare": dimCompare,
"fit_label": fitLabel,
"fit_tips": fitTips,
"harmony_index": harmony,
"love_index": match.Love,
"friend_index": match.Friend,
"marriage_index": match.Marriage,
"match_indices": match.AsMap(),
"star_compare": []map[string]any{
{"key": "sun", "title": "太阳", "me": aSun, "other": bSun, "note": starPairNote(aSun, bSun)},
{"key": "moon", "title": "月亮", "me": aMoon, "other": bMoon, "note": starPairNote(aMoon, bMoon)},
{"key": "rise", "title": "上升", "me": aRise, "other": bRise, "note": starPairNote(aRise, bRise)},
},
"share_hint": "了解彼此的沟通方式,查看人格匹配",
}
detail := map[string]any{
"title": "相处建议·完整分析",
"communication": []string{
fmt.Sprintf("%s:先讲清楚事实与需要,再谈感受。", aName),
fmt.Sprintf("%s:先确认被听见,再进入方案讨论。", bName),
"冲突时约定「复述对方一句」再回应,降低误解。",
"title": "人格匹配·完整分析",
"sections": []map[string]any{
{"title": "匹配总览", "body": comp.DeepOverview, "bullets": comp.ChemistryPoints},
{"title": "星座合盘与匹配指数", "body": fmt.Sprintf("%s。%s太阳%s / %s太阳%s;月亮%s与%s。", match.Summary, aName, aSun, bName, bSun, aMoon, bMoon),
"bullets": []string{match.LoveNote, match.FriendNote, match.MarriageNote, starPairNote(aSun, bSun)}},
{"title": "沟通协作", "body": comp.CommDeep, "bullets": comp.CommTips},
{"title": "冲突修复", "body": comp.ConflictDeep, "bullets": comp.ConflictTips},
{"title": "亲密与边界", "body": comp.IntimacyDeep, "bullets": comp.IntimacyTips},
{"title": "共同成长", "body": comp.GrowthDeep, "bullets": comp.GrowthTips},
},
"interaction": []string{
"用「我观察到…我需要…」代替指责句式。",
"每周留一次轻松同步:最近什么在消耗/滋养这段关系。",
},
"maintenance": []string{
"差异标签不是评分,而是协作说明书。",
"重要决定前,双方各写三点顾虑再合并。",
},
"me_behavior": str(pa.Detail["behavior_pattern"]),
// legacy flat lists (tests / older clients)
"communication": append([]string{
fmt.Sprintf("%s%s):%s", aName, aLabel, firstTeaser(aDims, "communication")),
fmt.Sprintf("%s%s):%s", bName, bLabel, firstTeaser(bDims, "communication")),
}, comp.CommTips...),
"interaction": comp.IntimacyTips,
"maintenance": comp.GrowthTips,
"me_behavior": str(pa.Detail["behavior_pattern"]),
"other_behavior": str(pb.Detail["behavior_pattern"]),
"strengths_together": comp.ChemistryPoints,
"friction_points": comp.Watchouts,
"weekly_practice": comp.Weekly,
"conversation_scripts": comp.Scripts,
"faq": []map[string]string{
{"q": "差异大是不是不合适?", "a": "差异本身不是问题,关键是双方是否愿意把差异当成说明书,而不是评分表。"},
{"q": "总吵怎么办?", "a": "先停火复述,再谈方案;把「谁对谁错」换成「我们各自需要什么」。"},
},
}
relLabel, relBody, relBullets := relationTypePack(relationType, aName, bName)
if relLabel != "" {
summary["relation_type"] = relationType
summary["relation_type_label"] = relLabel
secs, _ := detail["sections"].([]map[string]any)
detail["sections"] = append([]map[string]any{
{"title": "关系类型:" + relLabel, "body": relBody, "bullets": relBullets},
}, secs...)
}
return Output{Summary: summary, Detail: detail}
}
func relationTypePack(t, aName, bName string) (label, body string, bullets []string) {
switch t {
case "partner", "恋人", "伴侣":
return "伴侣",
fmt.Sprintf("%s与%s更适合把差异写成「亲密说明书」:欲望、节奏与安全感都说清楚。", aName, bName),
[]string{"每周一次情绪复盘,不谈对错", "亲密请求用「我需要」句式", "边界:疲惫时先暂停再继续"}
case "family", "家人", "父母", "亲子":
return "家人",
fmt.Sprintf("家人关系里,%s与%s容易把旧角色带进新对话。试着把对方当「现在的人」而不是旧剧本。", aName, bName),
[]string{"少用「你总是」句式", "大事拆成可协商的小请求", "保留各自的私人空间"}
case "friend", "朋友":
return "朋友",
fmt.Sprintf("友情里%s与%s可以更轻松地互补:约会期待与回应频率说开即可。", aName, bName),
[]string{"约见用明确时间,减少猜测", "忙时用短消息保持连结", "冲突后用玩笑或直接道歉都行,别冷处理太久"}
default:
return "", "", nil
}
}
type dimScore struct {
Score int
Teaser string
}
func dimMap(v any) map[string]dimScore {
out := map[string]dimScore{}
arr, ok := v.([]map[string]any)
if !ok {
// Build() uses []map[string]any — also tolerate []any
raw, ok2 := v.([]any)
if !ok2 {
return out
}
for _, item := range raw {
m, ok := item.(map[string]any)
if !ok {
continue
}
key := str(m["key"])
out[key] = dimScore{Score: asInt(m["score"]), Teaser: str(m["teaser"])}
}
return out
}
for _, m := range arr {
key := str(m["key"])
out[key] = dimScore{Score: asInt(m["score"]), Teaser: str(m["teaser"])}
}
return out
}
func asInt(v any) int {
switch n := v.(type) {
case int:
return n
case float64:
return int(n)
default:
return 0
}
}
func dimTitle(key string) string {
switch key {
case "personality":
return "性格特点"
case "communication":
return "沟通方式"
case "relation":
return "关系模式"
case "career":
return "事业节奏"
case "emotion":
return "情绪调节"
case "lifestyle":
return "生活节奏"
default:
return key
}
}
func dimNote(key string, gap int, aName, bName string) string {
if gap > 12 {
return fmt.Sprintf("在「%s」上,%s 分值更高,适合由 %s 多给结构,%s 多给弹性。", dimTitle(key), aName, aName, bName)
}
if gap < -12 {
return fmt.Sprintf("在「%s」上,%s 分值更高,相处时可多尊重 %s 的节奏。", dimTitle(key), bName, bName)
}
return fmt.Sprintf("在「%s」上双方接近,容易形成默契,也要防止都默认对方「应该懂」。", dimTitle(key))
}
func firstTeaser(m map[string]dimScore, key string) string {
if d, ok := m[key]; ok && d.Teaser != "" {
return d.Teaser
}
return "表达方式各有节奏"
}
func complementarityKey(a, b string) string {
if a == b {
return "same"
}
// simple buckets by trait family
drive := map[string]string{
"稳进探索者": "steady", "细腻分析者": "steady", "守护担当者": "steady", "洞察策略者": "steady",
"敏锐连接者": "warm", "温和协调者": "warm", "热忱鼓舞者": "warm",
"果断行动派": "drive", "自由创造者": "drive",
}
ak, bk := drive[a], drive[b]
if ak == "" || bk == "" {
return "mix"
}
if ak == bk {
return "same_family"
}
if (ak == "steady" && bk == "warm") || (ak == "warm" && bk == "steady") {
return "steady_warm"
}
if (ak == "drive" && bk == "steady") || (ak == "steady" && bk == "drive") {
return "drive_steady"
}
if (ak == "drive" && bk == "warm") || (ak == "warm" && bk == "drive") {
return "drive_warm"
}
return "mix"
}
type compCopy struct {
OneLiner, Overview, Chemistry, DeepOverview, CommDeep, ConflictDeep, IntimacyDeep, GrowthDeep string
ChemistryPoints, Watchouts, CommTips, ConflictTips, IntimacyTips, GrowthTips, Weekly, Scripts []string
}
func complementarityCopy(key, aName, bName, aLabel, bLabel string) compCopy {
base := compCopy{
OneLiner: fmt.Sprintf("%s偏「%s」,%s偏「%s」——差异可以写成相处说明书。", aName, aLabel, bName, bLabel),
Overview: fmt.Sprintf("双方在表达、节奏与需求上并不相同。把差异看清楚,比急着证明「谁更对」更有用。下面从沟通、冲突、亲密与共同成长几个维度展开。"),
Chemistry: "互补往往出现在:一方给结构,另一方给温度;或一方推进,另一方稳住质量。",
Weekly: []string{
"本周进行一次 20 分钟「非解决问题」闲聊或散步。",
"各自写三件「我需要你这样支持我」的具体行为,互换阅读。",
"约定一个冲突停火词,任一方说出即暂停 15 分钟。",
},
Scripts: []string{
fmt.Sprintf("%s可以说:我需要先把事实说清楚,再谈感受。", aName),
fmt.Sprintf("%s可以说:我希望你先听到我的感受,再给方案。", bName),
"我们可以先复述对方一句,再表达自己的需要。",
},
}
switch key {
case "same":
base.OneLiner = fmt.Sprintf("你们风格接近(都偏「%s」),默契来得快,也要防止一起陷入同样的盲区。", aLabel)
base.Chemistry = "同类相吸:理解成本低,推进或回避也可能同步发生。"
base.DeepOverview = "风格相近意味着你们很容易「懂对方在想什么」,但也可能同时逃避冲突,或同时过度冲刺。建议定期引入外部视角(朋友建议、清单复盘),打破镜像盲区。"
base.CommDeep = "沟通效率高,但要刻意练习提出不同意见。安排「唱反调」轮值:每周一人专门提出风险点。"
base.ConflictDeep = "冲突可能被快速和好掩盖,问题未真正处理。用「问题清单」追踪未完成议题。"
base.IntimacyDeep = "熟悉感强,新鲜感需主动创造:共同学习或小旅行比重复日常更能充电。"
base.GrowthDeep = "一起设定一个共同小目标,并互相做问责伙伴。"
base.ChemistryPoints = []string{"理解成本低", "节奏容易对齐", "共同语言多"}
base.Watchouts = []string{"共享同一盲区", "缺少外部校正", "意见过于一致缺少张力"}
base.CommTips = []string{"鼓励提出异议", "重要决定写利弊表", "避免默认对方已懂"}
base.ConflictTips = []string{"追踪未完成议题", "避免假性和好", "冷静后再做决定"}
base.IntimacyTips = []string{"主动制造新鲜体验", "表达感谢要具体", "保留个人空间"}
base.GrowthTips = []string{"共同目标 + 问责", "每月复盘一次关系", "引入可信第三方建议"}
case "steady_warm":
base.Chemistry = "稳与暖互补:一方提供结构与可靠,另一方提供连接与温度。"
base.DeepOverview = fmt.Sprintf("%s与%s之间,最常见的张力是「要先讲清楚」还是「要先被看见」。若能轮流满足这两种需求,关系会既安全又有温度。", aName, bName)
base.CommDeep = "沟通协议:情绪话题先共鸣 2 分钟,再进入事实与方案;事务话题先结论,再补感受。"
base.ConflictDeep = "稳的一方别用沉默当结束;暖的一方别用追问升级压力。停火后用「我需要…」重开。"
base.IntimacyDeep = "暖的一方需要回应频率;稳的一方需要可预期的独处。把两者写进约定。"
base.GrowthDeep = "把互补写成分工:谁更擅长安抚,谁更擅长推进落地。"
base.ChemistryPoints = []string{"结构 × 温度", "可靠 × 连接", "可形成完整支持系统"}
base.Watchouts = []string{"一方觉得被冷落", "一方觉得被情绪淹没", "节奏错位积累委屈"}
base.CommTips = []string{"情绪先共鸣再方案", "事务先结论再感受", "用文字确认关键约定"}
base.ConflictTips = []string{"禁止用沉默结束话题", "追问前先问是否方便", "停火词机制"}
base.IntimacyTips = []string{"约定回应窗口", "尊重独处不被解读为冷淡", "每周一次深度连接"}
base.GrowthTips = []string{"按优势分工", "互相学习对方语言", "月度关系复盘"}
case "drive_steady":
base.Chemistry = "推与稳互补:一方破局加速,另一方把关质量与可持续。"
base.DeepOverview = "行动派容易嫌分析派慢;稳健派容易嫌行动派莽。把「速度」用在试验,「稳健」用在关键承诺,冲突会下降。"
base.CommDeep = "行动方给时间盒与最小方案;稳健方在时限内给风险清单,而不是无限延期。"
base.ConflictDeep = "冲突焦点常是节奏。先对齐「这是可逆试验还是重大决定」,再选速度。"
base.IntimacyDeep = "行动方用陪伴质量弥补碎片时间;稳健方减少用担忧浇灭热情,改用「我支持你试,我们设检查点」。"
base.GrowthDeep = "共同项目里明确角色:谁启动、谁验收、何时复盘。"
base.ChemistryPoints = []string{"破局 × 把关", "速度 × 质量", "试验与承诺可分工"}
base.Watchouts = []string{"节奏互斥", "一方压抑热情", "一方焦虑失控"}
base.CommTips = []string{"先定义可逆/不可逆", "时间盒决策", "风险清单限时"}
base.ConflictTips = []string{"争论节奏前先分类问题", "避免人格化指责", "用检查点代替否决"}
base.IntimacyTips = []string{"质量陪伴", "支持试验+检查点", "庆祝小进展"}
base.GrowthTips = []string{"项目角色清晰", "复盘节奏", "互相翻译动机"}
case "drive_warm":
base.Chemistry = "驱动与连接互补:一方带节奏,另一方维系人心与氛围。"
base.DeepOverview = "热情与效率碰到一起很有火花,也容易在「推进」与「照顾感受」之间拉扯。约定场景切换:冲刺模式 / 连接模式。"
base.CommDeep = "冲刺时短讯同步进度;连接时关掉任务话题。不要用效率语言处理情绪时刻。"
base.ConflictDeep = "驱动方避免「你想太多」;连接方避免「你只在乎结果」。改说具体需求。"
base.IntimacyDeep = "用共同体验(运动、活动)同时满足推进感与连接感。"
base.GrowthDeep = "轮流做「本周关系主理人」,负责安排一次连接或一次共同目标。"
base.ChemistryPoints = []string{"推进力 × 氛围", "行动号召力强", "共同体验易充电"}
base.Watchouts = []string{"情绪被效率压过", "承诺过多难兑现", "连接变任务化"}
base.CommTips = []string{"模式切换:冲刺/连接", "情绪时刻禁用效率话术", "进度短讯化"}
base.ConflictTips = []string{"禁止否定感受", "需求具体化", "修复后再推进"}
base.IntimacyTips = []string{"共同体验", "兑现小承诺", "非任务陪伴"}
base.GrowthTips = []string{"轮值关系主理人", "控制并行承诺", "庆祝与复盘并重"}
case "same_family":
base.DeepOverview = "你们属于相近气质族,容易互相理解,也要主动制造一点建设性差异,避免舒适区停滞。"
base.CommDeep = "沟通顺畅时更要确认细节,防止「好像说好了」其实理解不同。"
base.ConflictDeep = "冲突可能被淡化。强制做一次「最担心的三件事」互换。"
base.IntimacyDeep = "在舒适之外增加挑战性共同任务,刷新关系动能。"
base.GrowthDeep = "互相指出对方一个盲区,并约定本月各改一项小行为。"
base.ChemistryPoints = []string{"气质相近", "理解门槛低", "协作起步快"}
base.Watchouts = []string{"舒适区停滞", "细节默认错误", "回避尖锐议题"}
base.CommTips = []string{"确认细节", "书面关键约定", "鼓励异议"}
base.ConflictTips = []string{"互换担忧清单", "不假性和好", "设讨论截止"}
base.IntimacyTips = []string{"共同挑战任务", "新鲜体验", "具体感谢"}
base.GrowthTips = []string{"互指一个盲区", "月改一小行为", "外部输入"}
default:
base.DeepOverview = fmt.Sprintf("%s与%s风格路径不同,说明书价值更高。先承认差异合法,再谈协作规则。", aName, bName)
base.CommDeep = "建立双通道:事实通道与感受通道,讨论前先声明走哪一条。"
base.ConflictDeep = "冲突时回到共同目标句:我们都希望关系更好/事情做成。"
base.IntimacyDeep = "用定期同步取代猜测;空间与连接都要有配额。"
base.GrowthDeep = "把差异写成「我擅长 / 我需要」对照表,贴在看得见的地方。"
base.ChemistryPoints = []string{"视角多样", "可互补决策", "扩展彼此舒适区"}
base.Watchouts = []string{"误解成本高", "价值观冲突需早谈", "节奏长期错位"}
base.CommTips = []string{"声明沟通通道", "复述再回应", "关键约定书面化"}
base.ConflictTips = []string{"回到共同目标", "停火机制", "一次只谈一个议题"}
base.IntimacyTips = []string{"定期同步", "空间与连接配额", "具体肯定"}
base.GrowthTips = []string{"擅长/需要对照表", "月度复盘", "小步共同目标"}
}
return base
}
func harmonyIndex(dims []map[string]any) int {
if len(dims) == 0 {
return 72
}
sum := 0
for _, d := range dims {
gap := asInt(d["gap"])
sum += 100 - gap*4
}
avg := sum / len(dims)
if avg < 45 {
return 45
}
if avg > 96 {
return 96
}
return avg
}
func fitFromHarmony(score int, aLabel, bLabel string) (string, []string) {
switch {
case score >= 82:
return "默契互补型", []string{
fmt.Sprintf("%s与%s节奏接近,适合共同推进小事。", aLabel, bLabel),
"把欣赏说出口,默契会更稳。",
"每周留一次轻松同步,不必每次谈大事。",
}
case score >= 68:
return "磨合成长型", []string{
"差异可见,正好写成相处说明书。",
"冲突时先复述再提方案。",
"共同目标写清楚,减少猜忌。",
}
default:
return "反差探索型", []string{
"反差大不等于不合,关键是边界与节奏。",
"重要约定尽量具体、可检查。",
"给彼此独处充电的空间。",
}
}
}
func starPairNote(a, b string) string {
if a == b {
return fmt.Sprintf("同为%s:容易共鸣,也要避免同质盲区。", a)
}
return fmt.Sprintf("%s × %s:节奏不同,适合「我负责启动 / 你负责收尾」式分工。", a, b)
}
func str(v any) string {
s, _ := v.(string)
return s
@@ -81,3 +447,17 @@ func strSlice(v any) []string {
}
return out
}
func firstNonEmpty(a, b string) string {
if a != "" {
return a
}
return b
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
+32 -4
View File
@@ -1,6 +1,7 @@
package relation
import (
"encoding/json"
"strings"
"testing"
"time"
@@ -10,13 +11,40 @@ func TestBuild(t *testing.T) {
a := time.Date(1990, 1, 15, 0, 0, 0, 0, time.UTC)
b := time.Date(1992, 6, 8, 0, 0, 0, 0, time.UTC)
out := Build(a, b, "我", "TA")
if out.Summary["diff_one_liner"] == nil {
t.Fatal("missing diff")
if out.Summary["diff_one_liner"] == nil || out.Summary["overview"] == nil {
t.Fatal("missing summary fields")
}
blob := str(out.Summary["diff_one_liner"]) + str(out.Detail["title"])
for _, bad := range []string{"合盘", "运势", "吉凶", "合婚"} {
dims, ok := out.Summary["dimension_compare"].([]map[string]any)
if !ok || len(dims) < 3 {
t.Fatalf("expected dimension_compare, got %#v", out.Summary["dimension_compare"])
}
secs, ok := out.Detail["sections"].([]map[string]any)
if !ok || len(secs) < 4 {
t.Fatalf("expected detail sections")
}
raw, _ := json.Marshal(out)
blob := string(raw)
for _, bad := range []string{"算命"} {
if strings.Contains(blob, bad) {
t.Fatalf("forbidden %q", bad)
}
}
if out.Summary["harmony_index"] == nil || out.Summary["star_compare"] == nil {
t.Fatal("missing match fields")
}
if out.Summary["love_index"] == nil || out.Summary["friend_index"] == nil || out.Summary["marriage_index"] == nil {
t.Fatal("missing match indices")
}
}
func TestBuildSameStyle(t *testing.T) {
d := time.Date(1990, 1, 15, 0, 0, 0, 0, time.UTC)
out := Build(d, d, "我", "TA")
if !strings.Contains(str(out.Summary["diff_one_liner"]), "接近") &&
!strings.Contains(str(out.Summary["diff_one_liner"]), "说明书") {
// same birth => same style; copy should mention 接近 or still valid
if out.Detail["weekly_practice"] == nil {
t.Fatal("missing weekly_practice")
}
}
}