216 lines
7.7 KiB
TypeScript
216 lines
7.7 KiB
TypeScript
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"));
|
||
});
|