feat: complete ops dashboard AI board

This commit is contained in:
2026-07-08 19:47:28 -07:00
parent 6a1add3422
commit a8776f9d79
14 changed files with 1401 additions and 189 deletions

View File

@@ -1,9 +1,19 @@
import { describe, expect, it, vi } from "vitest";
import { validateElderAiAnalysisOutput, validateKnowledgeEntryInput } from "@/modules/ai/types";
import { getDatabase, type AppDatabase } from "@/modules/core/server/db";
import {
DEFAULT_AI_KNOWLEDGE_ENTRIES,
DEFAULT_ALERT_RULES,
DEFAULT_ALERT_TRIGGERS,
DEFAULT_DEVICE_ASSETS,
DEFAULT_FAMILY_CONTACTS,
DEFAULT_FAMILY_FEEDBACK,
DEFAULT_FAMILY_VISITS,
DEFAULT_MAINTENANCE_TICKETS,
DEFAULT_NOTICES,
DEFAULT_PREPARED_AI_ANALYSES,
seedDefaultCollaborationWorkspaceData,
} from "@/modules/core/server/default-workspace-data";
const FORBIDDEN_DEMO_WORDING_PATTERN = /mock|simulation|simulated|demo|sample|placeholder|fixture|fake|test data|模拟|仿真|演示|样例|示例|占位|测试数据/i;
@@ -13,6 +23,258 @@ vi.mock("@/modules/core/server/db", () => ({
getDatabase: vi.fn(),
}));
function expectStableUniqueNaturalKeys<T>(items: T[], getKey: (item: T) => string, expectedKeys: string[]): void {
const keys = items.map(getKey);
expect(keys).toEqual(expectedKeys);
expect(new Set(keys)).toHaveLength(items.length);
for (const key of keys) {
expect(key.trim()).toBe(key);
expect(key).not.toBe("");
}
}
type SeedInsertRow = Record<string, unknown>;
function isSeedInsertRow(value: unknown): value is SeedInsertRow {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function toSeedInsertRows(values: unknown): SeedInsertRow[] {
return Array.isArray(values) ? values.filter(isSeedInsertRow) : [];
}
function rowsWithKey(rows: SeedInsertRow[], key: string): SeedInsertRow[] {
return rows.filter((row) => key in row);
}
function createCollaborationSeedDatabaseMock(params: {
missingDeviceCode: string;
missingTicketTitle: string;
missingNoticeTitle: string;
missingAlertRuleName: string;
missingAlertTriggerTitle: string;
missingFamilyContactName: string;
missingFamilyVisitNotes: string;
missingFamilyFeedbackContent: string;
}): { database: AppDatabase; insertCalls: Array<{ values: SeedInsertRow[] }> } {
const elderNames = Array.from(
new Set([
...DEFAULT_FAMILY_CONTACTS.map((contact) => contact.elderName),
...DEFAULT_ALERT_TRIGGERS.flatMap((trigger) => (trigger.elderName ? [trigger.elderName] : [])),
]),
);
const elderRows = elderNames.map((name, index) => ({ id: `elder-${index}`, name }));
const getElderId = (name: string): string => {
const elder = elderRows.find((row) => row.name === name);
if (!elder) {
throw new Error(`Missing test elder row for ${name}`);
}
return elder.id;
};
const allFamilyContactRows = DEFAULT_FAMILY_CONTACTS.map((contact, index) => ({
id: `contact-${index}`,
elderId: getElderId(contact.elderName),
name: contact.name,
}));
const getContactId = (elderName: string, contactName: string): string => {
const elderId = getElderId(elderName);
const contact = allFamilyContactRows.find((row) => row.elderId === elderId && row.name === contactName);
if (!contact) {
throw new Error(`Missing test contact row for ${elderName}/${contactName}`);
}
return contact.id;
};
const selectResults = [
elderRows,
DEFAULT_DEVICE_ASSETS.filter((device) => device.code !== params.missingDeviceCode).map((device) => ({ id: `device-${device.code}`, code: device.code })),
DEFAULT_MAINTENANCE_TICKETS.filter((ticket) => ticket.title !== params.missingTicketTitle).map((ticket) => ({ title: ticket.title })),
DEFAULT_NOTICES.filter((notice) => notice.title !== params.missingNoticeTitle).map((notice) => ({ title: notice.title })),
DEFAULT_ALERT_RULES.filter((rule) => rule.name !== params.missingAlertRuleName).map((rule) => ({ id: `rule-${rule.name}`, name: rule.name })),
DEFAULT_ALERT_TRIGGERS.filter((trigger) => trigger.title !== params.missingAlertTriggerTitle).map((trigger) => ({ title: trigger.title })),
allFamilyContactRows.filter((contact) => contact.name !== params.missingFamilyContactName),
DEFAULT_FAMILY_VISITS.filter((visit) => visit.notes !== params.missingFamilyVisitNotes).map((visit) => ({
elderId: getElderId(visit.elderName),
contactId: getContactId(visit.elderName, visit.contactName),
notes: visit.notes,
})),
DEFAULT_FAMILY_FEEDBACK.filter((feedback) => feedback.content !== params.missingFamilyFeedbackContent).map((feedback) => ({
elderId: getElderId(feedback.elderName),
contactId: getContactId(feedback.elderName, feedback.contactName),
content: feedback.content,
})),
];
let selectIndex = 0;
const insertCalls: Array<{ values: SeedInsertRow[] }> = [];
const transaction = {
insert: vi.fn(() => ({
values: vi.fn((values: unknown) => {
const rows = toSeedInsertRows(values);
insertCalls.push({ values: rows });
return {
returning: vi.fn(() => rows.map((row, index) => ({ ...row, id: `inserted-${index}` }))),
};
}),
})),
};
const database = {
select: vi.fn(() => ({
from: vi.fn(() => ({
where: vi.fn(() => {
const result = selectResults[selectIndex] ?? [];
selectIndex += 1;
return result;
}),
})),
})),
transaction: vi.fn(async (callback: (transactionClient: typeof transaction) => Promise<void> | void) => {
await callback(transaction);
}),
};
return { database: database as unknown as AppDatabase, insertCalls };
}
describe("default collaboration seed data", () => {
it("keeps idempotent seed natural keys stable, populated, and unique per collaboration table", () => {
expectStableUniqueNaturalKeys(DEFAULT_DEVICE_ASSETS, (device) => device.code, [
"DEV-CALL-A102-2",
"DEV-SPO2-S101",
"DEV-OXY-A302-2",
"DEV-REHAB-R101",
]);
expectStableUniqueNaturalKeys(DEFAULT_MAINTENANCE_TICKETS, (ticket) => ticket.title, [
"床头呼叫器按键失灵",
"氧气接口保养延期",
"康复步行带日常校准",
]);
expectStableUniqueNaturalKeys(DEFAULT_NOTICES, (notice) => notice.title, [
"本周家属探访安排",
"夜间重点巡房提醒",
"设备巡检培训材料",
]);
expectStableUniqueNaturalKeys(DEFAULT_ALERT_RULES, (rule) => rule.name, [
"血氧低值连续告警",
"高优先级护理任务逾期",
"设备维修工单未派单",
]);
expectStableUniqueNaturalKeys(DEFAULT_ALERT_TRIGGERS, (trigger) => trigger.title, [
"S101 血氧低值需复核",
"重点照护夜间巡检逾期",
"氧气接口保养待派单",
]);
expectStableUniqueNaturalKeys(DEFAULT_FAMILY_CONTACTS, (contact) => `${contact.elderName}/${contact.name}`, [
"王桂兰/张敏",
"钱松柏/钱宁",
"林玉琴/林晓",
"赵国华/赵蕾",
]);
expectStableUniqueNaturalKeys(DEFAULT_FAMILY_VISITS, (visit) => `${visit.elderName}/${visit.contactName}/${visit.notes}`, [
"王桂兰/张敏/安排护理楼一层会客区。",
"钱松柏/钱宁/需医生确认夜间血氧情况后再回复。",
"林玉琴/林晓/已完成探访和短住事项确认。",
]);
expectStableUniqueNaturalKeys(DEFAULT_FAMILY_FEEDBACK, (feedback) => `${feedback.elderName}/${feedback.contactName}/${feedback.content}`, [
"赵国华/赵蕾/希望夜间巡房后能同步重点观察结果。",
"林玉琴/林晓/短住房间清洁和呼叫器说明比较清楚。",
]);
});
it("keeps default collaboration records resolvable through their natural-key dependencies", () => {
const deviceCodes = new Set(DEFAULT_DEVICE_ASSETS.map((device) => device.code));
const alertRuleNames = new Set(DEFAULT_ALERT_RULES.map((rule) => rule.name));
const elderNames = new Set(DEFAULT_FAMILY_CONTACTS.map((contact) => contact.elderName));
const contactKeys = new Set(DEFAULT_FAMILY_CONTACTS.map((contact) => `${contact.elderName}/${contact.name}`));
expect(DEFAULT_DEVICE_ASSETS.length).toBeGreaterThanOrEqual(3);
expect(DEFAULT_MAINTENANCE_TICKETS.length).toBeGreaterThanOrEqual(1);
expect(DEFAULT_NOTICES.length).toBeGreaterThanOrEqual(1);
expect(DEFAULT_ALERT_RULES.length).toBeGreaterThanOrEqual(1);
expect(DEFAULT_ALERT_TRIGGERS.length).toBeGreaterThanOrEqual(1);
expect(DEFAULT_FAMILY_CONTACTS.length).toBeGreaterThanOrEqual(1);
expect(DEFAULT_FAMILY_VISITS.length).toBeGreaterThanOrEqual(1);
expect(DEFAULT_FAMILY_FEEDBACK.length).toBeGreaterThanOrEqual(1);
for (const ticket of DEFAULT_MAINTENANCE_TICKETS) {
expect(deviceCodes.has(ticket.deviceCode)).toBe(true);
}
for (const trigger of DEFAULT_ALERT_TRIGGERS) {
expect(alertRuleNames.has(trigger.ruleName)).toBe(true);
if (trigger.elderName) {
expect(elderNames.has(trigger.elderName)).toBe(true);
}
}
for (const visit of DEFAULT_FAMILY_VISITS) {
expect(contactKeys.has(`${visit.elderName}/${visit.contactName}`)).toBe(true);
}
for (const feedback of DEFAULT_FAMILY_FEEDBACK) {
expect(contactKeys.has(`${feedback.elderName}/${feedback.contactName}`)).toBe(true);
}
});
it("keeps user-visible collaboration defaults realistic", () => {
const userVisibleContent = [
...DEFAULT_DEVICE_ASSETS.map((device) => [device.name, device.category, device.location, device.notes].join("\n")),
...DEFAULT_MAINTENANCE_TICKETS.map((ticket) => [ticket.title, ticket.description, ticket.assigneeLabel, ticket.resolutionNotes].join("\n")),
...DEFAULT_NOTICES.map((notice) => [notice.title, notice.content, notice.audience].join("\n")),
...DEFAULT_ALERT_RULES.map((rule) => [rule.name, rule.ruleType, rule.conditionSummary, rule.suggestion].join("\n")),
...DEFAULT_ALERT_TRIGGERS.map((trigger) => [trigger.title, trigger.description, trigger.source, trigger.handlingNotes].join("\n")),
...DEFAULT_FAMILY_CONTACTS.map((contact) => [contact.elderName, contact.name, contact.relationship, contact.phone, contact.notes].join("\n")),
...DEFAULT_FAMILY_VISITS.map((visit) => [visit.elderName, visit.contactName, visit.status, visit.notes].join("\n")),
...DEFAULT_FAMILY_FEEDBACK.map((feedback) => [feedback.elderName, feedback.contactName, feedback.feedbackType, feedback.content, feedback.responseNotes].join("\n")),
].join("\n");
expect(userVisibleContent).not.toMatch(FORBIDDEN_DEMO_WORDING_PATTERN);
});
it("inserts only collaboration defaults missing from existing natural-key sets", async () => {
const missingDevice = DEFAULT_DEVICE_ASSETS.find((device) => device.code === "DEV-CALL-A102-2");
const missingTicket = DEFAULT_MAINTENANCE_TICKETS.find((ticket) => ticket.title === "床头呼叫器按键失灵");
const missingNotice = DEFAULT_NOTICES.find((notice) => notice.title === "设备巡检培训材料");
const missingAlertRule = DEFAULT_ALERT_RULES.find((rule) => rule.name === "血氧低值连续告警");
const missingAlertTrigger = DEFAULT_ALERT_TRIGGERS.find((trigger) => trigger.title === "S101 血氧低值需复核");
const missingFamilyContact = DEFAULT_FAMILY_CONTACTS.find((contact) => contact.name === "张敏");
const missingFamilyVisit = DEFAULT_FAMILY_VISITS.find((visit) => visit.notes === "安排护理楼一层会客区。");
const missingFamilyFeedback = DEFAULT_FAMILY_FEEDBACK.find((feedback) => feedback.content === "希望夜间巡房后能同步重点观察结果。");
if (!missingDevice || !missingTicket || !missingNotice || !missingAlertRule || !missingAlertTrigger || !missingFamilyContact || !missingFamilyVisit || !missingFamilyFeedback) {
throw new Error("Missing collaboration seed fixture for behavior test");
}
const { database, insertCalls } = createCollaborationSeedDatabaseMock({
missingDeviceCode: missingDevice.code,
missingTicketTitle: missingTicket.title,
missingNoticeTitle: missingNotice.title,
missingAlertRuleName: missingAlertRule.name,
missingAlertTriggerTitle: missingAlertTrigger.title,
missingFamilyContactName: missingFamilyContact.name,
missingFamilyVisitNotes: missingFamilyVisit.notes,
missingFamilyFeedbackContent: missingFamilyFeedback.content,
});
vi.mocked(getDatabase).mockReturnValue(database);
await seedDefaultCollaborationWorkspaceData("organization-1");
const insertedRows = insertCalls.flatMap((call) => call.values);
expect(insertedRows).toHaveLength(8);
expect(rowsWithKey(insertedRows, "code")).toEqual([expect.objectContaining({ code: missingDevice.code })]);
expect(rowsWithKey(insertedRows, "priority")).toEqual([expect.objectContaining({ title: missingTicket.title })]);
expect(rowsWithKey(insertedRows, "audience")).toEqual([expect.objectContaining({ title: missingNotice.title })]);
expect(rowsWithKey(insertedRows, "conditionSummary")).toEqual([expect.objectContaining({ name: missingAlertRule.name })]);
expect(rowsWithKey(insertedRows, "source")).toEqual([expect.objectContaining({ title: missingAlertTrigger.title })]);
expect(rowsWithKey(insertedRows, "relationship")).toEqual([expect.objectContaining({ name: missingFamilyContact.name })]);
expect(rowsWithKey(insertedRows, "scheduledAt")).toEqual([expect.objectContaining({ notes: missingFamilyVisit.notes })]);
expect(rowsWithKey(insertedRows, "feedbackType")).toEqual([expect.objectContaining({ content: missingFamilyFeedback.content })]);
});
});
describe("default AI workspace seed data", () => {
it("keeps user-visible AI knowledge content realistic and validator-ready", () => {
expect(DEFAULT_AI_KNOWLEDGE_ENTRIES.length).toBeGreaterThanOrEqual(3);