127 lines
4.4 KiB
TypeScript
127 lines
4.4 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { z } from "zod";
|
|
import {
|
|
ConsultationPlanValidationError,
|
|
consultationPlanMaxCreditCost,
|
|
createConsultationPlan,
|
|
validateConsultationPlan,
|
|
} from "../src/lib/consultation-plan.ts";
|
|
import { consultationDomainIds } from "../src/lib/consultation-domain-registry.ts";
|
|
|
|
for (const theme of consultationDomainIds) {
|
|
test(`creates a bounded server-owned ${theme} plan`, () => {
|
|
const plan = createConsultationPlan({
|
|
userIntent: "请回答我的问题",
|
|
theme,
|
|
consultationMode: "verified_chart",
|
|
modelCreditCost: 1,
|
|
});
|
|
assert.equal(plan.plan_version, "consultation-plan-v2");
|
|
assert.equal(plan.birthTimeMode, "verified_chart");
|
|
assert.equal(plan.precisionBoundary, "server_evidence_required");
|
|
assert.equal(plan.maxCreditCost, consultationPlanMaxCreditCost);
|
|
});
|
|
}
|
|
|
|
test("rejects a theme outside the consultation allowlist", () => {
|
|
assert.throws(
|
|
() => createConsultationPlan({
|
|
userIntent: "请回答我的问题",
|
|
theme: "medical" as "career",
|
|
consultationMode: "verified_chart",
|
|
}),
|
|
(error: unknown) => error instanceof z.ZodError,
|
|
);
|
|
});
|
|
|
|
test("server birth-time mode fixes the precision boundary", () => {
|
|
assert.equal(createConsultationPlan({
|
|
userIntent: "未来什么时候适合行动",
|
|
theme: "timing",
|
|
consultationMode: "unverified_birth_time",
|
|
}).precisionBoundary, "precise_timing_blocked");
|
|
assert.equal(createConsultationPlan({
|
|
userIntent: "什么是上升星座",
|
|
theme: "general",
|
|
consultationMode: "general_no_birth_time",
|
|
}).precisionBoundary, "precise_timing_blocked");
|
|
});
|
|
|
|
test("rejects mode or precision tampering", () => {
|
|
const plan = createConsultationPlan({
|
|
userIntent: "未来什么时候适合行动",
|
|
theme: "timing",
|
|
consultationMode: "unverified_birth_time",
|
|
});
|
|
assert.throws(
|
|
() => validateConsultationPlan({ ...plan, precisionBoundary: "server_evidence_required" }, {
|
|
consultationMode: "unverified_birth_time",
|
|
modelCreditCost: 1,
|
|
}),
|
|
(error: unknown) => error instanceof ConsultationPlanValidationError
|
|
&& error.code === "plan_precision_mismatch",
|
|
);
|
|
assert.throws(
|
|
() => validateConsultationPlan(plan, {
|
|
consultationMode: "verified_chart",
|
|
modelCreditCost: 1,
|
|
}),
|
|
(error: unknown) => error instanceof ConsultationPlanValidationError
|
|
&& error.code === "plan_mode_mismatch",
|
|
);
|
|
});
|
|
|
|
test("rejects model cost above the release consultation ceiling", () => {
|
|
assert.throws(
|
|
() => createConsultationPlan({
|
|
userIntent: "事业如何",
|
|
theme: "career",
|
|
consultationMode: "verified_chart",
|
|
modelCreditCost: consultationPlanMaxCreditCost + 1,
|
|
}),
|
|
(error: unknown) => error instanceof ConsultationPlanValidationError
|
|
&& error.code === "plan_cost_ceiling_exceeded",
|
|
);
|
|
});
|
|
|
|
|
|
test("rejects non-positive or fractional model costs", () => {
|
|
for (const modelCreditCost of [0, -1, 0.5, Number.NaN, Number.POSITIVE_INFINITY]) {
|
|
assert.throws(
|
|
() => createConsultationPlan({
|
|
userIntent: "事业如何",
|
|
theme: "career",
|
|
consultationMode: "verified_chart",
|
|
modelCreditCost,
|
|
}),
|
|
(error: unknown) => error instanceof ConsultationPlanValidationError
|
|
&& error.code === "plan_cost_ceiling_exceeded",
|
|
);
|
|
}
|
|
});
|
|
|
|
test("deep-freezes the server-owned plan and nested evidence lists", () => {
|
|
const plan = createConsultationPlan({
|
|
userIntent: "事业如何",
|
|
theme: "career",
|
|
consultationMode: "verified_chart",
|
|
modelCreditCost: 1,
|
|
});
|
|
const mutable = plan as unknown as {
|
|
precisionBoundary: string;
|
|
requestedDomains: string[];
|
|
requiredEvidenceCategories: string[];
|
|
};
|
|
|
|
assert.equal(Object.isFrozen(plan), true);
|
|
assert.equal(Object.isFrozen(plan.requestedDomains), true);
|
|
assert.equal(Object.isFrozen(plan.requiredEvidenceCategories), true);
|
|
assert.throws(() => { mutable.precisionBoundary = "precise_timing_blocked"; }, TypeError);
|
|
assert.throws(() => mutable.requestedDomains.push("timing"), TypeError);
|
|
assert.throws(() => mutable.requiredEvidenceCategories.splice(0, 1), TypeError);
|
|
assert.equal(plan.precisionBoundary, "server_evidence_required");
|
|
assert.deepEqual(plan.requestedDomains, ["career"]);
|
|
assert.deepEqual(plan.requiredEvidenceCategories, ["natal_foundation", "domain"]);
|
|
});
|