Files
Jyotisha/frontend/tests/birth-time-dynamic-choice.test.ts
T
2026-07-19 04:38:20 +08:00

193 lines
7.4 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 { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import test from "node:test";
import { publicDynamicChoiceQuestionSchema } from "../src/lib/birth-time-dynamic-choice.ts";
import {
persistedDynamicChoiceQuestionSchema,
toPublicDynamicChoiceQuestion,
} from "../src/lib/birth-time-dynamic-choice-internal.ts";
import { dynamicJourneyTurnStateSchema, journeyTurnStateSchema } from "../src/lib/birth-time-journey-turn-protocol.ts";
const internalQuestion = {
questionId: "11111111-1111-4111-8111-111111111111",
opportunityId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
dimensionCode: "career_change",
estimatedInformationGain: 0.7,
scoringVersion: "birth-time-choice-scoring-v2",
source: "fallback",
questionFingerprint: "question-fingerprint",
candidatePartitionFingerprint: "partition-fingerprint",
prompt: "哪一个时间段更接近这次工作变化?",
options: [
{
optionId: "22222222-2222-4222-8222-222222222222",
label: "2018—2020 年",
kind: "primary",
partitionId: "career-2018-2020",
candidateScores: { "09:00": 0.8 },
},
{
optionId: "33333333-3333-4333-8333-333333333333",
label: "2021—2023 年",
kind: "primary",
partitionId: "career-2021-2023",
candidateScores: { "09:30": 0.6 },
},
{
optionId: "44444444-4444-4444-8444-444444444444",
label: "不确定 / 不记得",
kind: "unknown",
partitionId: null,
candidateScores: null,
},
{
optionId: "55555555-5555-4555-8555-555555555555",
label: "都不符合",
kind: "unmatched",
partitionId: null,
candidateScores: null,
},
],
} as const;
function sourceFiles(directory: string): readonly string[] {
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const path = join(directory, entry.name);
return entry.isDirectory() ? sourceFiles(path) : [path];
});
}
test("public questions never expose partition ids", () => {
const parsed = publicDynamicChoiceQuestionSchema.parse({
questionId: "11111111-1111-4111-8111-111111111111",
prompt: "哪一个时间段更接近这次工作变化?",
options: [
{ optionId: "22222222-2222-4222-8222-222222222222", label: "2018—2020 年", kind: "primary" },
{ optionId: "33333333-3333-4333-8333-333333333333", label: "2021—2023 年", kind: "primary" },
{ optionId: "44444444-4444-4444-8444-444444444444", label: "不确定 / 不记得", kind: "unknown" },
{ optionId: "55555555-5555-4555-8555-555555555555", label: "都不符合", kind: "unmatched" },
],
});
assert.equal("partitionId" in parsed.options[0], false);
assert.equal(publicDynamicChoiceQuestionSchema.safeParse({
...parsed,
options: [{ ...parsed.options[0], partitionId: "private" }, ...parsed.options.slice(1)],
}).success, false);
});
test("public questions enforce the shared prompt limit", () => {
const publicQuestion = toPublicDynamicChoiceQuestion(internalQuestion);
const promptWithLength = (length: number) => `${"问".repeat(length - 1)}`;
assert.equal(publicDynamicChoiceQuestionSchema.safeParse({
...publicQuestion,
prompt: promptWithLength(120),
}).success, true);
assert.equal(publicDynamicChoiceQuestionSchema.safeParse({
...publicQuestion,
prompt: promptWithLength(121),
}).success, false);
});
test("internal primary choices require a server partition", () => {
assert.equal(persistedDynamicChoiceQuestionSchema.safeParse(internalQuestion).success, true);
assert.equal(persistedDynamicChoiceQuestionSchema.safeParse({
...internalQuestion,
options: internalQuestion.options.map((option) => option.kind === "primary"
? { optionId: option.optionId, label: option.label, kind: option.kind, partitionId: null }
: option),
}).success, false);
});
test("choice questions require a bounded complete option set", () => {
assert.equal(publicDynamicChoiceQuestionSchema.safeParse({
questionId: "11111111-1111-4111-8111-111111111111",
prompt: "哪一个时间段更接近这次工作变化?",
options: [
{ optionId: "22222222-2222-4222-8222-222222222222", label: "2018—2020 年", kind: "primary" },
{ optionId: "44444444-4444-4444-8444-444444444444", label: "不确定 / 不记得", kind: "unknown" },
{ optionId: "55555555-5555-4555-8555-555555555555", label: "都不符合", kind: "unmatched" },
],
}).success, false);
});
test("dynamic schemas accept opaque server-issued identifiers", () => {
const publicQuestion = {
questionId: "question-career-window",
prompt: "哪一个时间段更接近这次工作变化?",
options: [
{ optionId: "window-a", label: "2018—2020 年", kind: "primary" },
{ optionId: "window-b", label: "2021—2023 年", kind: "primary" },
{ optionId: "unknown", label: "不确定 / 不记得", kind: "unknown" },
{ optionId: "unmatched", label: "都不符合", kind: "unmatched" },
],
};
assert.equal(publicDynamicChoiceQuestionSchema.safeParse(publicQuestion).success, true);
assert.equal(persistedDynamicChoiceQuestionSchema.safeParse({
...internalQuestion,
...publicQuestion,
opportunityId: "career-window",
options: [
{ ...publicQuestion.options[0], partitionId: "window-a", candidateScores: { "09:00": 0.8 } },
{ ...publicQuestion.options[1], partitionId: "window-b", candidateScores: { "09:30": 0.6 } },
{ ...publicQuestion.options[2], partitionId: null, candidateScores: null },
{ ...publicQuestion.options[3], partitionId: null, candidateScores: null },
],
}).success, true);
});
test("public code never imports the internal dynamic choice contract", () => {
const publicSourceFiles = [
...sourceFiles(new URL("../src/components", import.meta.url).pathname),
...sourceFiles(new URL("../src/hooks", import.meta.url).pathname),
...sourceFiles(new URL("../src/lib", import.meta.url).pathname).filter((path) =>
path.includes("client") || path.endsWith("response-schema.ts")),
];
for (const path of publicSourceFiles) {
assert.equal(readFileSync(path, "utf8").includes("birth-time-dynamic-choice-internal"), false, path);
}
});
test("dynamic turn state is explicitly discriminated from the legacy protocol", () => {
const dynamicTurn = {
journeyProtocol: "dynamic-choice-v2",
turnVersion: 0,
nextAction: { kind: "generate_dynamic_question" },
progress: {
phase: "question",
answeredCount: 0,
effectiveAnswerCount: 0,
currentRange: { startTime: "09:00", endTime: "10:00" },
previousRange: null,
plateauCount: 0,
},
permissions: { canConfirmCandidate: false },
};
assert.equal(dynamicJourneyTurnStateSchema.safeParse(dynamicTurn).success, true);
assert.equal(journeyTurnStateSchema.safeParse(dynamicTurn).success, false);
assert.equal(dynamicJourneyTurnStateSchema.safeParse({
...dynamicTurn,
journeyProtocol: "legacy-guided-v1",
}).success, false);
assert.equal(dynamicJourneyTurnStateSchema.safeParse({
...dynamicTurn,
nextAction: {
kind: "ask_baseline_evidence",
question: {
questionId: "legacy",
phase: "baseline",
domain: "career",
requestedPrecision: ["year"],
allowUnknown: true,
purposeCode: "candidate_difference_career",
plannerVersion: "candidate-difference-v1",
},
},
}).success, false);
});