test: harden AI analysis path
This commit is contained in:
168
modules/ai/types.test.ts
Normal file
168
modules/ai/types.test.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user