169 lines
5.3 KiB
TypeScript
169 lines
5.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import type { AiCitation, ElderAiAnalysisOutput, ElderAiFinding, ElderAiRecommendation, KnowledgeEntryInput } from "@/modules/ai/types";
|
|
import {
|
|
parseDataScopes,
|
|
validateElderAiAnalysisOutput,
|
|
validateKnowledgeEntryInput,
|
|
} from "@/modules/ai/types";
|
|
|
|
const validKnowledgeInput: KnowledgeEntryInput = {
|
|
scope: "organization",
|
|
organizationId: "org-1",
|
|
title: "Fall prevention guidance",
|
|
category: "Safety",
|
|
tags: "fall,night",
|
|
body: "Residents with night wandering need bed exit checks and clear walkways.",
|
|
status: "enabled",
|
|
};
|
|
|
|
const validFinding: ElderAiFinding = {
|
|
category: "fall-risk",
|
|
severity: "warning",
|
|
evidence: "Night wandering and prior fall history are present.",
|
|
citationIds: ["resident-1"],
|
|
};
|
|
|
|
const validRecommendation: ElderAiRecommendation = {
|
|
title: "Increase night checks",
|
|
priority: "high",
|
|
rationale: "Additional observation reduces missed night wandering events.",
|
|
suggestedNextStep: "Add a night-shift round note for the next care review.",
|
|
citationIds: ["resident-1"],
|
|
};
|
|
|
|
const validCitation: AiCitation = {
|
|
id: "resident-1",
|
|
sourceType: "resident_context",
|
|
sourceId: "elder-1",
|
|
title: "Resident profile",
|
|
excerpt: "Night wandering and prior fall history.",
|
|
};
|
|
|
|
const validAnalysisOutput: ElderAiAnalysisOutput = {
|
|
overallRiskLevel: "medium",
|
|
summary: "Resident has elevated fall risk at night.",
|
|
keyFindings: [validFinding],
|
|
recommendations: [validRecommendation],
|
|
dataGaps: ["Recent gait assessment is unavailable."],
|
|
citations: [validCitation],
|
|
confidence: 0.72,
|
|
modelSummary: {
|
|
provider: "openai-compatible",
|
|
chatModel: "gpt-test",
|
|
embeddingModel: "embedding-test",
|
|
},
|
|
};
|
|
|
|
describe("AI validators", () => {
|
|
describe("validateKnowledgeEntryInput", () => {
|
|
it("accepts a scoped entry and trims user-authored text fields", () => {
|
|
const result = validateKnowledgeEntryInput({
|
|
...validKnowledgeInput,
|
|
title: " Fall prevention guidance ",
|
|
category: " Safety ",
|
|
tags: " fall,night ",
|
|
body: " Residents need clear walkways. ",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
success: true,
|
|
input: {
|
|
...validKnowledgeInput,
|
|
title: "Fall prevention guidance",
|
|
category: "Safety",
|
|
tags: "fall,night",
|
|
body: "Residents need clear walkways.",
|
|
},
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{ name: "non-object payload", value: null, reason: "请求数据格式无效" },
|
|
{ name: "unknown scope", value: { ...validKnowledgeInput, scope: "facility" }, reason: "知识库范围无效" },
|
|
{ name: "unknown status", value: { ...validKnowledgeInput, status: "archived" }, reason: "知识库状态无效" },
|
|
{ name: "blank title", value: { ...validKnowledgeInput, title: " " }, reason: "知识标题不能为空" },
|
|
{ name: "blank body", value: { ...validKnowledgeInput, body: "\n\t" }, reason: "知识内容不能为空" },
|
|
])("rejects $name", ({ value, reason }) => {
|
|
expect(validateKnowledgeEntryInput(value)).toEqual({ success: false, reason });
|
|
});
|
|
});
|
|
|
|
describe("validateElderAiAnalysisOutput", () => {
|
|
it("accepts valid model output, coerces numeric confidence strings, and clamps the public score", () => {
|
|
const result = validateElderAiAnalysisOutput({
|
|
...validAnalysisOutput,
|
|
confidence: "1.25",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
...validAnalysisOutput,
|
|
confidence: 1,
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "unknown risk level",
|
|
value: { ...validAnalysisOutput, overallRiskLevel: "urgent" },
|
|
},
|
|
{
|
|
name: "finding with invalid severity",
|
|
value: {
|
|
...validAnalysisOutput,
|
|
keyFindings: [{ ...validFinding, severity: "debug" }],
|
|
},
|
|
},
|
|
{
|
|
name: "recommendation with invalid priority",
|
|
value: {
|
|
...validAnalysisOutput,
|
|
recommendations: [{ ...validRecommendation, priority: "soon" }],
|
|
},
|
|
},
|
|
{
|
|
name: "mixed citation id array",
|
|
value: {
|
|
...validAnalysisOutput,
|
|
keyFindings: [{ ...validFinding, citationIds: ["resident-1", 42] }],
|
|
},
|
|
},
|
|
{
|
|
name: "invalid citation source",
|
|
value: {
|
|
...validAnalysisOutput,
|
|
citations: [{ ...validCitation, sourceType: "web" }],
|
|
},
|
|
},
|
|
{
|
|
name: "unexpected model provider",
|
|
value: {
|
|
...validAnalysisOutput,
|
|
modelSummary: { ...validAnalysisOutput.modelSummary, provider: "anthropic" },
|
|
},
|
|
},
|
|
{
|
|
name: "non-finite confidence",
|
|
value: { ...validAnalysisOutput, confidence: "not-a-number" },
|
|
},
|
|
])("rejects $name", ({ value }) => {
|
|
expect(validateElderAiAnalysisOutput(value)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("parseDataScopes", () => {
|
|
it("keeps only recognized scope strings in caller order", () => {
|
|
expect(parseDataScopes(["elder", "health", "unknown", 7, "knowledge", "elder"])).toEqual([
|
|
"elder",
|
|
"health",
|
|
"knowledge",
|
|
"elder",
|
|
]);
|
|
});
|
|
|
|
it.each([undefined, null, "elder", { scopes: ["elder"] }])("returns an empty list for non-array input %#", (value) => {
|
|
expect(parseDataScopes(value)).toEqual([]);
|
|
});
|
|
});
|
|
});
|