Files
Jyotisha/frontend/tests/birth-time-guide-agent.test.ts
T
jesse-uxandGrok (agent loop) d2296c2654 把前后端共用的请求契约和小工具收成一份,避免再各抄一份。
报告创建请求、校正入口和同名类型改从 contracts 推导;同义的对象判断与 sha256 改走共享函数。调用处用 import 别名保留原名,使 diff 低于 1500 行。行为、哈希和状态字符串不变。

Co-Authored-By: Grok (agent loop)
2026-10-15 22:22:44 +08:00

230 lines
8.3 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 test from "node:test";
import {
BirthTimeGuideOutputError,
fallbackQuestionCopy,
guideQuestionVariants,
parseEvidenceDraftOutput,
parseGuideQuestionOutput,
renderQuestionVariant,
} from "../src/lib/birth-time-guide-agent.ts";
import { legacyGuidedEvidenceDomains, type QuestionSpec } from "../src/lib/birth-time-question-planner.ts";
import { draftEvidenceStructureTool, getBirthTimeGuideAgent } from "../src/mastra/index.ts";
import type { ResolvedLanguageModel } from "../src/mastra/model.ts";
function question(domain: QuestionSpec["domain"]): QuestionSpec {
return {
questionId: `baseline_${domain}_1`,
phase: "baseline",
domain,
requestedPrecision: ["year", "month"],
allowUnknown: true,
purposeCode: `candidate_difference_${domain}`,
plannerVersion: "candidate-difference-v1",
};
}
test("draft parser fails closed when the model changes the server-selected domain", () => {
assert.throws(
() => parseEvidenceDraftOutput(
{ domain: "relationship", precision: "month", date: "2023-04" },
{ requiredDomain: "career", sourceMessage: "2023 年 4 月换了工作" },
),
BirthTimeGuideOutputError,
);
});
test("ambiguous dates remain incomplete instead of being invented", () => {
const draft = parseEvidenceDraftOutput(
{ domain: "career", precision: null, date: null },
{ requiredDomain: "career", sourceMessage: "大概是前几年" },
);
assert.deepEqual(draft, {
domain: "career",
precision: null,
date: null,
needsReview: true,
});
});
test("an ungrounded model date is discarded", () => {
const draft = parseEvidenceDraftOutput(
{ domain: "career", precision: "month", date: "2023-04" },
{ requiredDomain: "career", sourceMessage: "记不清具体时间" },
);
assert.equal(draft.precision, null);
assert.equal(draft.date, null);
assert.equal(draft.needsReview, true);
});
test("precision and date mismatch remains a review-only draft", () => {
const draft = parseEvidenceDraftOutput(
{ domain: "career", precision: "day", date: "2023-04" },
{ requiredDomain: "career", sourceMessage: "2023 年 4 月换了工作" },
);
assert.equal(draft.precision, "day");
assert.equal(draft.date, "2023-04");
assert.equal(draft.needsReview, true);
});
test("grounded dates can produce a complete review-only proposal", () => {
const draft = parseEvidenceDraftOutput(
{ domain: "career", precision: "month", date: "2023-04" },
{ requiredDomain: "career", sourceMessage: "我在 2023 年 4 月换了工作" },
);
assert.deepEqual(draft, {
domain: "career",
precision: "month",
date: "2023-04",
needsReview: false,
});
});
test("date grounding accepts ISO and year-only evidence but never adds an unstated day", () => {
const iso = parseEvidenceDraftOutput(
{ domain: "career", precision: "month", date: "2023-04" },
{ requiredDomain: "career", sourceMessage: "工作变动发生于 2023-04" },
);
const year = parseEvidenceDraftOutput(
{ domain: "career", precision: "year", date: "2023" },
{ requiredDomain: "career", sourceMessage: "只记得是 2023 年" },
);
const inventedDay = parseEvidenceDraftOutput(
{ domain: "career", precision: "day", date: "2023-04-18" },
{ requiredDomain: "career", sourceMessage: "只记得 2023 年 4 月" },
);
assert.equal(iso.needsReview, false);
assert.equal(year.needsReview, false);
assert.equal(inventedDay.date, null);
assert.equal(inventedDay.precision, null);
assert.equal(inventedDay.needsReview, true);
});
test("date grounding accepts fullwidth Chinese month and explicit Chinese or ISO days", () => {
const cases = [
{
output: { domain: "career", precision: "month", date: "2023-04" },
message: "工作变化发生在 2023 年 4 月",
},
{
output: { domain: "career", precision: "day", date: "2023-04-18" },
message: "工作变化发生在 2023 年 4 月 18 号",
},
{
output: { domain: "career", precision: "day", date: "2023-04-18" },
message: "工作变化发生在 2023-04-18",
},
] as const;
for (const value of cases) {
const draft = parseEvidenceDraftOutput(value.output, {
requiredDomain: "career",
sourceMessage: value.message,
});
assert.equal(draft.needsReview, false, value.message);
}
});
test("date grounding rejects substrings, partial ISO tokens, and scattered numbers", () => {
const unsafeMessages = [
"订单号20230",
"编号 2023-04-X",
"2023 年有变化,另外 4 月去了旅行",
"账号 1202304",
] as const;
for (const sourceMessage of unsafeMessages) {
const draft = parseEvidenceDraftOutput(
{ domain: "career", precision: "month", date: "2023-04" },
{ requiredDomain: "career", sourceMessage },
);
assert.equal(draft.precision, null, sourceMessage);
assert.equal(draft.date, null, sourceMessage);
assert.equal(draft.needsReview, true, sourceMessage);
}
});
test("year-only grounding requires a complete year token or explicit year semantics", () => {
for (const sourceMessage of ["2023", " 2023 ", "大约在 2023 年"] as const) {
const draft = parseEvidenceDraftOutput(
{ domain: "career", precision: "year", date: "2023" },
{ requiredDomain: "career", sourceMessage },
);
assert.equal(draft.needsReview, false, sourceMessage);
}
for (const sourceMessage of ["订单20230", "编号A2023B"] as const) {
const draft = parseEvidenceDraftOutput(
{ domain: "career", precision: "year", date: "2023" },
{ requiredDomain: "career", sourceMessage },
);
assert.equal(draft.date, null, sourceMessage);
}
});
test("server renders every approved variant from its QuestionSpec domain and precision", () => {
const semanticMarkers = {
education: /升学|转学|学习方向/,
relocation: /搬家|离乡|居住地/,
relationship: /关系进入|关系结束|关系.*转变/,
career: /工作|职业方向|身份变化/,
finance: /收入|资产|负债|资源渠道/,
health_pressure: /健康压力|生活压力/,
} as const;
for (const domain of legacyGuidedEvidenceDomains) {
for (const variant of guideQuestionVariants) {
const copy = renderQuestionVariant(question(domain), variant);
assert.equal((copy.match(/[??]/g) ?? []).length, 1);
assert.ok(copy.length <= 120);
assert.match(copy, semanticMarkers[domain]);
assert.doesNotMatch(copy, /哪一天|几号|具体日期|候选|支持|更符合|更接近|出生分钟/);
for (const otherDomain of legacyGuidedEvidenceDomains.filter((item) => item !== domain)) {
assert.doesNotMatch(copy, semanticMarkers[otherDomain]);
}
}
assert.equal(fallbackQuestionCopy(question(domain)), renderQuestionVariant(question(domain), "direct"));
}
});
test("question parser accepts only a finite variant and binds it to the server QuestionSpec", () => {
const parsed = parseGuideQuestionOutput({ variant: "gentle" }, question("career"));
assert.equal(parsed, renderQuestionVariant(question("career"), "gentle"));
for (const unsafe of [
{ variant: "relationship" },
{ variant: "direct", domain: "relationship" },
{ question: "Which relationship changed?" },
{ question: "具体是哪一天?" },
{ question: "04:00 这个候选时间更符合吗?" },
{ variant: "direct", question: "越权覆盖" },
]) {
assert.throws(() => parseGuideQuestionOutput(unsafe, question("career")), BirthTimeGuideOutputError);
}
});
test("model output schemas reject extra fields", () => {
assert.throws(() => parseEvidenceDraftOutput(
{ domain: "career", precision: null, date: null, confidence: "high" },
{ requiredDomain: "career", sourceMessage: "记不清" },
));
assert.throws(() => parseGuideQuestionOutput({ variant: "direct", score: 9 }, question("career")));
});
test("birth-time guide registers only the review-only structure tool", async () => {
const model: ResolvedLanguageModel = {
id: "guide-test-model",
label: "Guide test",
description: "",
creditCost: 1,
isDefault: false,
mode: "openai",
model: "openai/gpt-5-mini",
};
const tools = await getBirthTimeGuideAgent(model).listTools();
assert.deepEqual(Object.keys(tools), ["draftEvidenceStructureTool"]);
assert.equal(draftEvidenceStructureTool.id, "draft-evidence-structure");
});