Files
Jyotisha/frontend/tests/birth-time-journey-dynamic-adapters.test.ts
T
2026-07-25 01:17:14 +08:00

216 lines
7.7 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 {
parseCandidateDifferenceBuild,
parseDynamicChoiceScoring,
} from "../src/lib/birth-time-journey-dynamic-adapters.ts";
const scores = { "05:30": 0, "05:31": 1, "05:32": 1, "05:33": 0 } as const;
const inverseScores = { "05:30": 1, "05:31": 0, "05:32": 0, "05:33": 1 } as const;
const firstPartition = {
partition_id: "career-early",
descriptor: "2014-01-01--2017-12-31",
fallback_label: "2014—2017",
candidate_scores: scores,
} as const;
const secondPartition = {
partition_id: "career-late",
descriptor: "2018-01-01--2021-12-31",
fallback_label: "2018—2021",
candidate_scores: inverseScores,
} as const;
const opportunity = {
opportunity_id: "career-window",
dimension_code: "career",
neutral_context: "career",
estimated_information_gain: 0.5,
candidate_partition_fingerprint: "career-partitions-v2",
fallback_prompt: "哪段经历更接近你的职业变化?",
partitions: [firstPartition, secondPartition],
} as const;
const apiPacket = {
success: true,
endpoint: "dynamic_rectification_opportunities",
case_id: "case-1",
scoring_version: "birth-time-choice-scoring-v2",
current_range: { start_time: "05:30", end_time: "05:33" },
opportunities: [opportunity],
asked_question_fingerprints: ["asked-1"],
candidate_partition_fingerprints: ["partition-1"],
recent_range_history: [{ start_time: "05:30", end_time: "05:33" }],
candidate_model: {
version: "birth-time-choice-scoring-v2",
candidate_times: ["05:30", "05:31", "05:32", "05:33"],
},
} as const;
const apiScore = {
success: true,
endpoint: "dynamic_rectification_score",
result_id: "1d8ee348-61a3-433d-8907-ff6d281b9992",
confidence: "low",
can_apply: false,
winning_segment: {
start_time: "05:31",
end_time: "05:32",
representative_time: "05:31",
width_minutes: 2,
},
event_count: 1,
domain_count: 1,
top_score: 0.5,
second_score: 0,
margin_percent: 50,
reasons: ["insufficient_effective_evidence"],
evidence: [],
algorithm_version: "birth-time-choice-scoring-v2",
evidence_mode: "dynamic_choice",
effective_answer_count: 1,
dimension_count: 1,
} as const;
test("difference packets separate public copy from private score vectors", () => {
const build = parseCandidateDifferenceBuild(apiPacket);
const mappedOpportunity = build.packet.opportunities[0];
const mappedPartition = mappedOpportunity?.partitions[0];
const privatePartition = build.scoringPartitions["career-window"]?.[0];
assert.equal(mappedOpportunity?.opportunityId, "career-window");
assert.equal(mappedOpportunity?.estimatedInformationGain, 0.5);
assert.equal(mappedPartition?.partitionId, "career-early");
assert.equal(mappedPartition && "candidateScores" in mappedPartition, false);
assert.equal(privatePartition?.candidateScores["05:31"], 1);
assert.deepEqual(build.candidateModel, {
version: "birth-time-choice-scoring-v2",
candidate_times: ["05:30", "05:31", "05:32", "05:33"],
});
});
test("difference packets enforce the public prompt limit at the API boundary", () => {
const withPrompt = (length: number) => ({
...apiPacket,
opportunities: [{
...apiPacket.opportunities[0],
fallback_prompt: `${"问".repeat(length - 1)}`,
}],
});
assert.equal(parseCandidateDifferenceBuild(withPrompt(120)).packet.opportunities[0]?.fallbackPrompt.length, 120);
assert.throws(() => parseCandidateDifferenceBuild(withPrompt(121)));
});
test("difference packets reject wrong versions, extra fields, and invalid score keys", () => {
assert.throws(() => parseCandidateDifferenceBuild({
...apiPacket, scoring_version: "birth-time-choice-scoring-v1",
}));
assert.throws(() => parseCandidateDifferenceBuild({ ...apiPacket, confidence: "high" }));
assert.throws(() => parseCandidateDifferenceBuild({
...apiPacket, opportunities: [{ ...opportunity, model_controlled: true }],
}));
assert.throws(() => parseCandidateDifferenceBuild({
...apiPacket,
opportunities: [{ ...opportunity, partitions: [{ ...firstPartition, model_controlled: true }, secondPartition] }],
}));
assert.throws(() => parseCandidateDifferenceBuild({
...apiPacket,
opportunities: [{
...opportunity,
partitions: [{ ...firstPartition, candidate_scores: { "not-a-time": 1 } }, secondPartition],
}],
}));
assert.throws(() => parseCandidateDifferenceBuild({
...apiPacket,
opportunities: [{
...opportunity,
partitions: [{ ...firstPartition, candidate_scores: { ...scores, "05:34": 1 } }, secondPartition],
}],
}));
});
test("difference packets reject duplicate opportunity and partition identifiers", () => {
assert.throws(() => parseCandidateDifferenceBuild({
...apiPacket,
opportunities: [opportunity, opportunity],
}));
assert.throws(() => parseCandidateDifferenceBuild({
...apiPacket,
opportunities: [{
...opportunity,
partitions: [firstPartition, { ...secondPartition, partition_id: firstPartition.partition_id }],
}],
}));
});
test("difference packets preserve an exact cross-midnight score range", () => {
const parsed = parseCandidateDifferenceBuild({
...apiPacket,
current_range: { start_time: "23:59", end_time: "00:00" },
opportunities: [{
...opportunity,
partitions: opportunity.partitions.map((partition) => ({
...partition,
candidate_scores: { "23:59": 1, "00:00": 0 },
})),
}],
});
assert.deepEqual(parsed.packet.currentRange, { startTime: "23:59", endTime: "00:00" });
});
test("dynamic scores reject model-controlled gates and all nested extra fields", () => {
assert.throws(() => parseDynamicChoiceScoring({
...apiScore, confidence: "high", can_apply: true, effective_answer_count: 1,
}));
assert.throws(() => parseDynamicChoiceScoring({
...apiScore,
winning_segment: { ...apiScore.winning_segment, model_controlled: "accepted" },
}));
});
test("dynamic scores require exact counts, mode, empty evidence, and v2", () => {
assert.throws(() => parseDynamicChoiceScoring({ ...apiScore, event_count: 2 }));
assert.throws(() => parseDynamicChoiceScoring({ ...apiScore, domain_count: 2 }));
assert.throws(() => parseDynamicChoiceScoring({ ...apiScore, evidence_mode: "dated_event" }));
assert.throws(() => parseDynamicChoiceScoring({
...apiScore,
evidence: [{
event_id: "5cb071d6-6d99-46be-85dc-a9bf59ef6ac5",
domain: "career",
candidate_time: "05:31",
rule_ids: ["forged"],
points: 1,
}],
}));
assert.throws(() => parseDynamicChoiceScoring({
...apiScore, algorithm_version: "birth-time-event-scoring-v1",
}));
});
test("dynamic scores map independent engine values into guarded candidates", () => {
const parsed = parseDynamicChoiceScoring(apiScore);
assert.equal(parsed.effectiveAnswerCount, 1);
assert.equal(parsed.dimensionCount, 1);
assert.equal(parsed.candidate.eventCount, 1);
assert.equal(parsed.candidate.domainCount, 1);
assert.equal(parsed.candidate.winningSegment?.representativeTime, "05:31");
assert.deepEqual(parsed.candidate.evidence, []);
assert.equal(parsed.candidate.algorithmVersion, "birth-time-choice-scoring-v2");
});
test("dynamic score adapters keep minute confirmation closed before VedAstro validation", () => {
const parsed = parseDynamicChoiceScoring({
...apiScore,
confidence: "high",
can_apply: true,
event_count: 4,
domain_count: 3,
effective_answer_count: 4,
dimension_count: 3,
top_score: 20,
second_score: 10,
margin_percent: 50,
});
assert.equal(parsed.candidate.confidence, "high");
assert.equal(parsed.candidate.canApply, false);
assert.ok(parsed.candidate.reasons.includes("vedastro_validation_required"));
});