Files
Jyotisha/frontend/tests/consultation-voice-contract.test.ts
T
jesse-uxandClaude Code 539d4daee4
Independent Staging Quality Gate / validate (push) Successful in 9m35s
Independent Staging Quality Gate / publish (push) Successful in 3m52s
feat(consult): add free model-classified smalltalk fast path
Keep full consultation tool contracts unchanged. Persist short replies and refund the original reservation atomically while recording actual model usage. Verify Linux frontend 3566/3566, database 40/40, Static home and gzip +0.0493%.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 12:58:34 +08:00

138 lines
8.9 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, /Do not write 统一参数与原始结构 or 技法审计表 into the body/);
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, /36 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 23 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, /Do not paste the Technique Audit Table into 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/);
});
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/);
const exemption = "只是打招呼、道谢、告别时,回一句话,不要套开场形状";
const sharedVoice = voice.slice(0, voice.indexOf("export const natalSpokenReportContract"));
for (const instructions of [natal, general, window]) {
// Expand only the shared voice literal; no runtime Skill filesystem is needed.
assert.ok(instructions.replace("${productConversationVoice}", sharedVoice).includes(exemption));
}
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!, /像顾问报告:没有反差,没说谁在推谁在修,没有可扮演的象/);
});