Files
Jyotisha/frontend/tests/consultation-voice-contract.test.ts
T
Jesse_ChenandClaude Fable 5.1 84b293fb47
Independent Staging Quality Gate / validate (push) Failing after 7m5s
Independent Staging Quality Gate / publish (push) Skipped
feat(consult): 对话口气改成反差与扮演象的形状
产品判定现有人设(懂行、可靠、说人话的占星师朋友)出来的是顾问报告。
人设改成把人当一个人认真对待、行动力很强、嘴有点毒但靠谱的同事:直接、
有立场、带一点锋利,毒只对处境不对人且每句锋利都要有盘上的证据。

开场从「一句结论 + 2–3 条短要点 + 一句下一步」换成固定形状,三种模式共用:
反差(表面 A 底下 B,命名成一个格局)→ 谁在推、谁在修(大运主星在推,
行运只负责把结果修得体面)→ 别去应 X 的象,去扮演 Y 的象 → 最多三条短行动
(破折号短句,各 ≤ 20 字)。仍无标题、总长 ≤ 400 字。术语当场用引号里的
白话套住。申报时段与无出生分钟两条降级路线形状照给,只把「谁在推谁在修」
换成窗口内稳定层或公开日历,不编月份。

新增希望纪律:盘上有转机且 answer_policy 允许精确应期时说到月份;没有就说
这段时间是拿来干什么的、可以扮演哪个象。禁「一切都会好 / 相信自己 / 加油 /
你值得更好的 / 宇宙自有安排」。

零业务逻辑改动,Skill 版本不变。tsc 0 错、lint 0 error(118 warning 不变)、
npm test 3468→3471 条且 36 条失败与基线 ff0427cf 逐条相同、/ 仍 Static、
首屏 gzip 两侧字节相同。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
2026-09-17 16:18:07 +00:00

134 lines
8.6 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";
const voice = readFileSync(new URL("../src/mastra/product-voice.ts", import.meta.url), "utf8");
const mastra = readFileSync(new URL("../src/mastra/index.ts", import.meta.url), "utf8");
const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8");
const binding = readFileSync(new URL("../src/mastra/skill-binding.ts", import.meta.url), "utf8");
const workflow = readFileSync(new URL("../src/mastra/consultation-workflow.ts", import.meta.url), "utf8");
test("product voice uses a spoken opener then the skill Level 2 report skeleton", () => {
assert.match(voice, /VISIBLE VOICE/);
assert.match(voice, /natalSpokenReportContract/);
assert.match(voice, /governs METHOD, TECHNIQUE INVOCATION, TRUTH BOUNDARIES/);
assert.match(voice, /Raman six-step house judgment/);
assert.match(voice, /Yoga table/);
assert.match(voice, /Technique Audit Table at the end of the answer body/);
assert.match(voice, /已执行 \/ 阻塞 \/ 不适用/);
assert.match(voice, /governed_support_only_candidate_ready/);
assert.match(voice, /Skip an executed layer the question needs/);
// 原值: assert.match(voice, /你更适合把已经积累的专业能力做成长期事业/) —— 它是 Good 例。
// 新值: 同一句改断言成 Bad 例(下方 voice-opener-shape 测试里锁住它在 Bad 段)。
// 原因: 产品判定这句像顾问报告——没有反差、没说谁在推谁在修、没有可扮演的象。
// 原值: assert.match(voice, /3–6 heading-free sentences/)
// 新值: assert.match(voice, /open with the OPENER SHAPE above, heading-free/)
// 原因: 开场从「3–6 句」改成固定形状,句数不再是约束。
assert.match(voice, /open with the OPENER SHAPE above, heading-free/);
// 原值: assert.match(voice, /one conclusion sentence, then 2–3 short point-sentences/)
// 新值: assert.match(voice, /别去应 X 的象,去扮演 Y 的象/)
// 原因: 旧形状(结论+要点+下一步)被反差→谁推谁修→扮演象→短行动取代。
assert.match(voice, /别去应 X 的象,去扮演 Y 的象/);
assert.match(voice, /Spoken-layer total ≤ 400 Chinese characters/);
assert.match(voice, /骨架不可省略,但必须以直接回应开场/);
assert.match(mastra, /productConversationVoice/);
assert.match(mastra, /natalSpokenReportContract/);
assert.match(mastra, /skill Level 2 report skeleton/);
assert.match(mastra, /Paste that Technique Audit Table at the end of the answer body/);
assert.match(binding, /Follow this method and its truth boundaries/);
assert.match(binding, /present its Level 2 report template in the chat body after a 3-6 sentence spoken reply/);
assert.match(mastra, /The bound skill method is this product's answering contract/);
assert.match(mastra, /Do not use a restricted technique/);
assert.match(mastra, /must_use_layers is the executed shortlist/);
assert.match(mastra, /do not invent it from model knowledge/);
assert.match(mastra, /search_period is the searched observation window/);
assert.match(voice, /## 统一参数与原始结构/);
assert.match(mastra, /consultationSpokenHeadingRule\("natal"\)/);
assert.match(workflow, /template: "skill_level_2"/);
assert.match(workflow, /raman_six_step/);
assert.doesNotMatch(mastra, /Name the domains you did cover, say plainly that the remaining ones were not calculated/);
assert.doesNotMatch(mastra, /2-5 short paragraphs/);
assert.doesNotMatch(voice, /Do not paste the Technique Audit Table/);
});
test("general and window get voice without the natal Level 2 skeleton", () => {
// 原值: 另切 onboardingInstructions,断言它没有 natal 骨架。
// 新值: onboarding agent 已删除;general 与 window 仍无骨架。
// 原因: 建议问题生成接口下线。
const generalStart = mastra.indexOf("const generalJyotishInstructions");
const windowStart = mastra.indexOf("const windowJyotishInstructions");
const natalStart = mastra.indexOf("const jyotishInstructions");
const general = mastra.slice(generalStart, mastra.indexOf("const dailyStarlanguageInstructions"));
const window = mastra.slice(windowStart, mastra.indexOf("export function getWindowJyotishAgent"));
const natal = mastra.slice(natalStart, mastra.indexOf("export function getJyotishAgent"));
assert.match(natal, /natalSpokenReportContract/);
// 新增: natal 也必须带 productConversationVoice —— 开场形状对三种模式共用。
assert.match(natal, /productConversationVoice/);
assert.match(general, /productConversationVoice/);
assert.doesNotMatch(general, /natalSpokenReportContract/);
assert.match(window, /productConversationVoice/);
assert.doesNotMatch(window, /natalSpokenReportContract/);
assert.doesNotMatch(mastra, /const onboardingInstructions/);
});
test("consult user turn no longer dumps tool JSON that the model would parrot", () => {
const chartBranch = route.slice(route.indexOf("const toolInput = consultationInputSchema.parse"));
// 原值: /先用 3–6 句口语直接回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/
// 新值: /先用不超过 400 字口语直接回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/
// 原因: 用户回合文案与新开场形状对齐,约束从句数改成字数加形状。
assert.match(chartBranch, /先用不超过 400 字口语直接回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/);
assert.match(chartBranch, /别去应 X 的象,去扮演 Y 的象/);
assert.doesNotMatch(chartBranch, /JSON\.stringify\(toolInput\)/);
assert.doesNotMatch(chartBranch, /经过服务端校验的工具参数/);
assert.match(route, /文末技法审计表/);
assert.match(chartBranch, /await runConsultationWorkflow\(toolInput/);
});
test("the opener is a shape — 反差, 谁推谁修, 扮演哪个象, then at most three actions", () => {
assert.match(voice, /OPENER SHAPE \(natal \/ general \/ declared-window 三种模式共用\)/);
assert.match(voice, /反差/);
assert.match(voice, /表面 A,底下 B/);
assert.match(voice, /谁在推、谁在修/);
assert.match(voice, /扮演/);
assert.match(voice, /最多三条短行动/);
assert.match(voice, /≤ 20 Chinese characters/);
// 降级路线:申报时段与无出生分钟都不编月份,但「扮演哪个象」照给。
assert.match(voice, /申报时段(declared birth window)[\s\S]*?也不写月份。第 3 步照给/);
assert.match(voice, /无出生分钟(no birth minute)[\s\S]*?不写月份。第 3 步照给/);
});
test("persona is the sharp-but-reliable colleague, and hope never becomes a platitude", () => {
assert.match(voice, /把对面的人当一个人认真对待/);
assert.match(voice, /嘴有点毒但靠谱的同事/);
assert.match(voice, /毒只对处境,不对人/);
// 含糊问题不猜、不反问一串:给默认解读再作答。
assert.match(voice, /这句我按『……』理解了,不对你纠正我/);
assert.match(voice, /HOPE DISCIPLINE/);
assert.match(voice, /answer_policy\.can_answer_precise_timing 允许精确应期时/);
assert.match(voice, /不是每个人都有转机,但每个人都有能扮演的象/);
// 空话必须留在禁令里,但不得出现在 Good 例里。
assert.match(voice, /Never write 一切都会好, 相信自己, 加油, 你值得更好的, or 宇宙自有安排。?/);
assert.match(voice, /No 亲爱的, no 感谢你的信任, no emoji/);
});
test("the pinned Good example carries the new shape; the old consultant-report line is now Bad", () => {
const goodStart = voice.indexOf("Good (口气与形状的定稿样例");
const goodEnd = voice.indexOf("Good (追问轮");
assert.ok(goodStart > -1 && goodEnd > goodStart);
const good = voice.slice(goodStart, goodEnd);
assert.match(good, /今年你的事业是「外松内紧」/);
assert.match(good, /推动这一年的是土星,不是木星/);
assert.match(good, /去扮演土星的「守」/);
assert.match(good, /- 9 月木星换宫之后再谈升职,之前谈是白谈。/);
// 拍板样例里不得出现空话安慰;禁令本身仍写在 HOPE DISCIPLINE 段。
assert.doesNotMatch(good, /一切都会好|相信自己|加油|宇宙自有安排/);
assert.match(voice, /Good \(追问轮,用户说「你在说什么鬼」\): 说直接点:这份工作还值得留/);
const badLine = voice.split("\n").find((line) => line.startsWith("Bad: 你更适合把已经积累的专业能力做成长期事业"));
assert.ok(badLine, "旧 Good 例必须改挂在 Bad 上");
assert.match(badLine!, /像顾问报告:没有反差,没说谁在推谁在修,没有可扮演的象/);
});