139 lines
5.6 KiB
TypeScript
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);
|
|
});
|