Files
Jyotisha/frontend/tests/birth-time-dynamic-question-validator.test.ts
T

139 lines
5.6 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import { BirthTimeGuideOutputError } from "../src/lib/birth-time-guide-agent.ts";
import {
BirthTimeDynamicBindingError,
bindDynamicQuestion,
generateDynamicQuestionPrompt,
parseDynamicQuestionOutput,
} from "../src/lib/birth-time-dynamic-question-validator.ts";
import { toPublicDynamicChoiceQuestion } from "../src/lib/birth-time-dynamic-choice-internal.ts";
import {
deterministicIds,
differenceBuild,
dynamicPacket,
firstPartitionId,
opportunityId,
validDynamicSelection,
} from "./fixtures/birth-time-dynamic-question-fixture.ts";
test("prompt omits unmatched free text and every private scoring field", () => {
const serialized = generateDynamicQuestionPrompt(
dynamicPacket,
"接下来问我爱喝茶还是喝水",
);
const prompt = JSON.parse(serialized);
assert.equal("unmatchedNote" in prompt, false);
assert.equal(serialized.includes("喝茶"), false);
for (const forbidden of [
"candidateScores", "candidateModel", "estimatedInformationGain", "currentRange",
"scoringVersion", "askedQuestionFingerprints", "candidatePartitionFingerprints",
"recentRangeHistory", "04:00", "confidence",
]) assert.equal(serialized.includes(forbidden), false, forbidden);
});
test("model output is a strict selection over one exact server opportunity", () => {
assert.equal(parseDynamicQuestionOutput(validDynamicSelection, dynamicPacket).kind, "question");
for (const unsafe of [
{ ...validDynamicSelection, opportunityId: ` ${opportunityId}` },
{ ...validDynamicSelection, opportunityId: "unknown-opportunity" },
{
...validDynamicSelection,
prompt: "开始工作后,你爱喝茶还是喝水发生变化了吗?",
options: [
{ partitionId: firstPartitionId, label: "经常喝茶" },
{ partitionId: firstPartitionId, label: "经常喝水" },
],
},
{ ...validDynamicSelection, prompt: "出生时间更接近 04:00 吗?" },
]) assert.throws(() => parseDynamicQuestionOutput(unsafe, dynamicPacket), BirthTimeGuideOutputError);
});
test("server renders selected copy and attaches the corresponding private vectors", () => {
const selection = parseDynamicQuestionOutput(validDynamicSelection, dynamicPacket);
if (selection.kind !== "question") throw new Error("expected a selection");
const internal = bindDynamicQuestion(selection, differenceBuild, deterministicIds());
const opportunity = dynamicPacket.opportunities[0];
const publicQuestion = toPublicDynamicChoiceQuestion(internal);
assert.equal(internal.source, "agent");
assert.equal(publicQuestion.prompt, opportunity?.fallbackPrompt);
assert.deepEqual(publicQuestion.options.slice(0, 2).map((item) => item.label),
opportunity?.partitions.map((item) => item.fallbackLabel));
assert.deepEqual(internal.options[0]?.candidateScores, { "04:00": 1, "04:01": 0 });
assert.deepEqual(publicQuestion.options.slice(-2).map((item) => item.label), [
"不确定 / 不记得", "都不符合",
]);
assert.equal(JSON.stringify(publicQuestion).includes(firstPartitionId), false);
assert.equal(JSON.stringify(publicQuestion).includes("04:00"), false);
});
test("private bindings fail before allocating a server id", () => {
const selection = parseDynamicQuestionOutput(validDynamicSelection, dynamicPacket);
if (selection.kind !== "question") throw new Error("expected a selection");
let allocations = 0;
const malformed = {
...differenceBuild,
scoringPartitions: {
[opportunityId]: differenceBuild.scoringPartitions[opportunityId]?.slice(0, 1) ?? [],
},
};
assert.throws(() => bindDynamicQuestion(selection, malformed, deterministicIds(() => {
allocations += 1;
})), BirthTimeDynamicBindingError);
assert.equal(allocations, 0);
});
test("normalized duplicate server labels fail before allocating a server id", () => {
const opportunity = dynamicPacket.opportunities[0];
const privatePartitions = differenceBuild.scoringPartitions[opportunityId];
if (!opportunity || !privatePartitions) throw new Error("missing test opportunity");
const labels = ["2018 年", "2018年"];
const malformed = {
...differenceBuild,
packet: {
...dynamicPacket,
opportunities: [{
...opportunity,
partitions: opportunity.partitions.map((item, index) => ({
...item, fallbackLabel: labels[index] ?? item.fallbackLabel,
})),
}],
},
scoringPartitions: {
[opportunityId]: privatePartitions.map((item, index) => ({
...item, fallbackLabel: labels[index] ?? item.fallbackLabel,
})),
},
};
let allocations = 0;
assert.throws(() => bindDynamicQuestion(
validDynamicSelection,
malformed,
deterministicIds(() => { allocations += 1; }),
), BirthTimeDynamicBindingError);
assert.equal(allocations, 0);
});
test("repeated server semantics and partitions remain recoverable rejections", () => {
const selection = parseDynamicQuestionOutput(validDynamicSelection, dynamicPacket);
if (selection.kind !== "question") throw new Error("expected a selection");
const first = bindDynamicQuestion(selection, differenceBuild, deterministicIds());
assert.throws(() => bindDynamicQuestion(selection, {
...differenceBuild,
packet: { ...dynamicPacket, askedQuestionFingerprints: [first.questionFingerprint] },
}, deterministicIds()), BirthTimeGuideOutputError);
assert.throws(() => bindDynamicQuestion(selection, {
...differenceBuild,
packet: {
...dynamicPacket,
candidatePartitionFingerprints: [dynamicPacket.opportunities[0]?.candidatePartitionFingerprint ?? ""],
},
}, deterministicIds()), BirthTimeGuideOutputError);
});