111 lines
4.6 KiB
TypeScript
111 lines
4.6 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
import { birthTimeGuideRequestSchema } from "../src/lib/birth-time-guide-agent.ts";
|
|
import { birthTimeJourneyRequestSchema } from "../src/lib/birth-time-journey-request.ts";
|
|
import { parseJourneyResponse } from "../src/lib/birth-time-journey-client.ts";
|
|
import { storedDynamicJourneyResponse } from "../src/lib/birth-time-journey-response.ts";
|
|
import { dynamicCase, persistedQuestion } from "./birth-time-dynamic-persistence-fixture.ts";
|
|
|
|
const caseId = "45857b75-4718-4590-aaf5-7113a03ea765";
|
|
const actionId = "a9890e09-d535-46f0-9a36-86017515a5a1";
|
|
|
|
test("choice commands accept only public ids", () => {
|
|
const valid = {
|
|
type: "answer_dynamic_choice",
|
|
caseId,
|
|
actionId,
|
|
turnVersion: 7,
|
|
questionId: persistedQuestion.questionId,
|
|
optionId: persistedQuestion.options[0].optionId,
|
|
};
|
|
assert.equal(birthTimeJourneyRequestSchema.safeParse(valid).success, true);
|
|
for (const field of ["partitionId", "candidateScores", "confidence", "time"] as const) {
|
|
assert.equal(birthTimeJourneyRequestSchema.safeParse({ ...valid, [field]: "forged" }).success, false);
|
|
}
|
|
});
|
|
|
|
test("unmatched context is optional, trimmed, and bounded", () => {
|
|
const valid = {
|
|
type: "reframe_unmatched",
|
|
caseId,
|
|
actionId,
|
|
turnVersion: 8,
|
|
questionId: persistedQuestion.questionId,
|
|
note: " 更像是 2017 年 ",
|
|
};
|
|
const parsed = birthTimeGuideRequestSchema.parse(valid);
|
|
assert.equal(parsed.type === "reframe_unmatched" ? parsed.note : null, "更像是 2017 年");
|
|
assert.equal(birthTimeGuideRequestSchema.safeParse({ ...valid, note: "字".repeat(241) }).success, false);
|
|
assert.equal(birthTimeGuideRequestSchema.safeParse({ ...valid, partitionId: "private" }).success, false);
|
|
});
|
|
|
|
test("dynamic responses project one exact public shape", () => {
|
|
const stored = {
|
|
...dynamicCase(),
|
|
questionnaire: { questions: [], samples: [], raw: { candidateVector: [0.4, 0.6] } },
|
|
scoring: {
|
|
answeredCount: 1,
|
|
candidateClusterRankings: [{ cluster: "05:00", score: 9 }],
|
|
raw: { candidateVector: [0.4, 0.6] },
|
|
nextRound: null,
|
|
nextRoundQuestions: [],
|
|
},
|
|
answers: { legacy_private_answer: "A" as const },
|
|
lifeEvents: [{
|
|
id: "ec4ab5f0-829c-468a-90ea-42842f9f70d3",
|
|
domain: "career" as const,
|
|
precision: "year" as const,
|
|
date: "2020",
|
|
}],
|
|
};
|
|
const response = storedDynamicJourneyResponse(stored);
|
|
const parsed = parseJourneyResponse(response);
|
|
|
|
assert.deepEqual(Object.keys(parsed).sort(), [
|
|
"answers", "candidateResult", "caseId", "evidenceDraft", "journeyProtocol",
|
|
"lifeEvents", "nextAction", "permissions", "progress", "questionnaire",
|
|
"scoring", "snapshot", "turnVersion",
|
|
]);
|
|
assert.equal(parsed.journeyProtocol, "dynamic-choice-v2");
|
|
assert.deepEqual({
|
|
questionnaire: parsed.questionnaire,
|
|
scoring: parsed.scoring,
|
|
answers: parsed.answers,
|
|
lifeEvents: parsed.lifeEvents,
|
|
}, { questionnaire: null, scoring: null, answers: {}, lifeEvents: [] });
|
|
});
|
|
|
|
test("dynamic response parsing rejects private legacy and candidate payloads", () => {
|
|
const response = storedDynamicJourneyResponse(dynamicCase());
|
|
const scoring = {
|
|
answeredCount: 1,
|
|
candidateClusterRankings: [{ cluster: "05:00", score: 9 }],
|
|
raw: { candidateVectors: { "05:00": [0.4, 0.6] } },
|
|
nextRound: null,
|
|
nextRoundQuestions: [],
|
|
};
|
|
const privatePayloads = [
|
|
{ ...response, scoring },
|
|
{ ...response, questionnaire: { questions: [], samples: [], raw: { candidateVectors: [1] } } },
|
|
{ ...response, answers: { hidden: "A" } },
|
|
{ ...response, lifeEvents: [{ id: "ec4ab5f0-829c-468a-90ea-42842f9f70d3", domain: "career", precision: "year", date: "2020" }] },
|
|
{ ...response, candidateModel: { candidates: ["05:10"] } },
|
|
{ ...response, currentChoiceQuestion: persistedQuestion },
|
|
{ ...response, partitionId: "window-a" },
|
|
{ ...response, nextAction: { ...response.nextAction, question: {
|
|
...persistedQuestion,
|
|
options: persistedQuestion.options,
|
|
} } },
|
|
];
|
|
|
|
for (const payload of privatePayloads) assert.throws(() => parseJourneyResponse(payload));
|
|
});
|
|
|
|
test("dynamic route boundaries authenticate before parsing user input", () => {
|
|
const journeyRoute = readFileSync(new URL("../src/app/api/birth-time-journey/route.ts", import.meta.url), "utf8");
|
|
const guideRoute = readFileSync(new URL("../src/app/api/birth-time-guide/route.ts", import.meta.url), "utf8");
|
|
assert.ok(journeyRoute.indexOf("auth.getUser") < journeyRoute.indexOf("requestPayload(request)"));
|
|
assert.ok(guideRoute.indexOf("auth.getUser") < guideRoute.indexOf("requestPayload(request)"));
|
|
});
|