feat: complete ops dashboard AI board
This commit is contained in:
@@ -5,7 +5,7 @@ import type { AiRuntimeConfigResult } from "@/modules/ai/server/config";
|
||||
import { getAiRuntimeConfig } from "@/modules/ai/server/config";
|
||||
import type { ElderAiResidentContext } from "@/modules/ai/server/elder-context";
|
||||
import { buildElderAiContext } from "@/modules/ai/server/elder-context";
|
||||
import { generateElderAiAnalysis, listElderAiAnalyses } from "@/modules/ai/server/analysis";
|
||||
import { generateElderAiAnalysis, listAiAnalysisBoard, listElderAiAnalyses } from "@/modules/ai/server/analysis";
|
||||
import { retrieveKnowledge } from "@/modules/ai/server/knowledge";
|
||||
import type { AiCitation, ElderAiAnalysisOutput } from "@/modules/ai/types";
|
||||
import { recordAuditLog } from "@/modules/core/server/audit";
|
||||
@@ -65,6 +65,11 @@ type AnalysisRow = {
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
type ElderNameRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type AnalysisInsertPayload = {
|
||||
organizationId?: unknown;
|
||||
elderId?: unknown;
|
||||
@@ -258,8 +263,9 @@ function createAnalysisRow(values: AnalysisInsertPayload, index: number): Analys
|
||||
};
|
||||
}
|
||||
|
||||
function createDatabaseFake(selectRows: AnalysisRow[] = []): AnalysisDatabaseFake {
|
||||
function createDatabaseFake(selectRows: AnalysisRow[] = [], elderRows: ElderNameRow[] = []): AnalysisDatabaseFake {
|
||||
const insertedValues: AnalysisInsertPayload[] = [];
|
||||
const orderedSelectRows = [...selectRows].sort((left, right) => right.createdAt.getTime() - left.createdAt.getTime());
|
||||
const insert = vi.fn(() => ({
|
||||
values: vi.fn((values: AnalysisInsertPayload) => {
|
||||
insertedValues.push(values);
|
||||
@@ -268,15 +274,23 @@ function createDatabaseFake(selectRows: AnalysisRow[] = []): AnalysisDatabaseFak
|
||||
};
|
||||
}),
|
||||
}));
|
||||
const select = vi.fn(() => ({
|
||||
from: vi.fn(() => ({
|
||||
where: vi.fn(() => ({
|
||||
orderBy: vi.fn(() => ({
|
||||
limit: vi.fn(async () => selectRows),
|
||||
})),
|
||||
const select = vi.fn((selection?: unknown) => {
|
||||
const selectsElderNames = selection !== null && typeof selection === "object" && "name" in selection;
|
||||
return {
|
||||
from: vi.fn(() => ({
|
||||
where: vi.fn(() => {
|
||||
if (selectsElderNames) {
|
||||
return elderRows;
|
||||
}
|
||||
return {
|
||||
orderBy: vi.fn(() => ({
|
||||
limit: vi.fn(async (rowLimit: number) => orderedSelectRows.slice(0, rowLimit)),
|
||||
})),
|
||||
};
|
||||
}),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
};
|
||||
});
|
||||
|
||||
return { insertedValues, insert, select };
|
||||
}
|
||||
@@ -442,6 +456,130 @@ describe("elder AI analysis service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("lists board analysis rows with elder display names and permitted result content", async () => {
|
||||
const analysisResult = createModelOutput({
|
||||
overallRiskLevel: "high",
|
||||
summary: "Night wandering requires a care-plan review.",
|
||||
});
|
||||
const completedRow = createAnalysisRow({
|
||||
organizationId: "org-1",
|
||||
elderId: "elder-2",
|
||||
actorAccountId: "account-1",
|
||||
status: "completed",
|
||||
dataScopes: ["elder", "health"],
|
||||
resultJson: analysisResult,
|
||||
citationsJson: analysisResult.citations,
|
||||
modelSummaryJson: analysisResult.modelSummary,
|
||||
}, 1);
|
||||
useDatabase(createDatabaseFake([completedRow], [{ id: "elder-2", name: "李建国" }]));
|
||||
|
||||
const result = await listAiAnalysisBoard(createAuthContext(["ai:read", "elder:read", "health:read"]), 5);
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
data: {
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
id: "analysis-2",
|
||||
elderId: "elder-2",
|
||||
elderName: "李建国",
|
||||
status: "completed",
|
||||
dataScopes: ["elder", "health"],
|
||||
createdAt: "2026-07-02T01:00:00.000Z",
|
||||
restricted: false,
|
||||
result: analysisResult,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("redacts board analysis rows when a stored data scope is not permitted", async () => {
|
||||
const restrictedResult = createModelOutput({
|
||||
summary: "Health details should not be visible without health permission.",
|
||||
});
|
||||
const restrictedRow = createAnalysisRow({
|
||||
organizationId: "org-1",
|
||||
elderId: "elder-3",
|
||||
actorAccountId: "account-1",
|
||||
status: "completed",
|
||||
dataScopes: ["elder", "health"],
|
||||
resultJson: restrictedResult,
|
||||
citationsJson: restrictedResult.citations,
|
||||
modelSummaryJson: restrictedResult.modelSummary,
|
||||
errorCategory: "provider_error",
|
||||
errorReason: "provider detail should stay hidden",
|
||||
}, 2);
|
||||
useDatabase(createDatabaseFake([restrictedRow], [{ id: "elder-3", name: "周玉珍" }]));
|
||||
|
||||
const result = await listAiAnalysisBoard(createAuthContext(["ai:read", "elder:read"]), 5);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success !== true) {
|
||||
return;
|
||||
}
|
||||
expect(result.data.items).toEqual([
|
||||
{
|
||||
id: "analysis-3",
|
||||
elderId: "elder-3",
|
||||
elderName: "周玉珍",
|
||||
status: "completed",
|
||||
dataScopes: ["elder", "health"],
|
||||
createdAt: "2026-07-02T02:00:00.000Z",
|
||||
restricted: true,
|
||||
},
|
||||
]);
|
||||
expect(result.data.items[0]).not.toHaveProperty("result");
|
||||
expect(result.data.items[0]).not.toHaveProperty("errorCategory");
|
||||
expect(result.data.items[0]).not.toHaveProperty("errorReason");
|
||||
});
|
||||
|
||||
it("returns board rows in newest-first order up to the requested limit", async () => {
|
||||
const newestRow = createAnalysisRow({
|
||||
organizationId: "org-1",
|
||||
elderId: "elder-4",
|
||||
actorAccountId: "account-1",
|
||||
status: "completed",
|
||||
dataScopes: ["elder"],
|
||||
resultJson: createModelOutput({ summary: "Newest analysis" }),
|
||||
}, 3);
|
||||
const olderRow = createAnalysisRow({
|
||||
organizationId: "org-1",
|
||||
elderId: "elder-1",
|
||||
actorAccountId: "account-1",
|
||||
status: "completed",
|
||||
dataScopes: ["elder"],
|
||||
resultJson: createModelOutput({ summary: "Older analysis" }),
|
||||
}, 0);
|
||||
const middleRow = createAnalysisRow({
|
||||
organizationId: "org-1",
|
||||
elderId: "elder-2",
|
||||
actorAccountId: "account-1",
|
||||
status: "completed",
|
||||
dataScopes: ["elder"],
|
||||
resultJson: createModelOutput({ summary: "Middle analysis" }),
|
||||
}, 1);
|
||||
useDatabase(createDatabaseFake(
|
||||
[olderRow, newestRow, middleRow],
|
||||
[
|
||||
{ id: "elder-1", name: "王阿姨" },
|
||||
{ id: "elder-2", name: "李建国" },
|
||||
{ id: "elder-4", name: "陈桂兰" },
|
||||
],
|
||||
));
|
||||
|
||||
const result = await listAiAnalysisBoard(createAuthContext(["ai:read", "elder:read"]), 2);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success !== true) {
|
||||
return;
|
||||
}
|
||||
expect(result.data.items.map((item) => ({ id: item.id, elderName: item.elderName, createdAt: item.createdAt }))).toEqual([
|
||||
{ id: "analysis-4", elderName: "陈桂兰", createdAt: "2026-07-02T03:00:00.000Z" },
|
||||
{ id: "analysis-2", elderName: "李建国", createdAt: "2026-07-02T01:00:00.000Z" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects model output that cites IDs outside resident and knowledge citations", async () => {
|
||||
const database = createDatabaseFake();
|
||||
useDatabase(database);
|
||||
|
||||
Reference in New Issue
Block a user