Files
kangkang/康康Tests/ModelsSchemaTests.swift
link2026 9d856fcfc4 ```
feat(AI): 集成MNN推理引擎替换MLX作为主AI运行时

- 引入MNN(alibaba) + Arm SME2 + CPU作为主AI运行时,支持A19/iPhone17的
  SME2和A17的NEON加速
- 添加MLX Swift作为兜底GPU推理方案,实现双后端切换机制
- 使用单一Qwen3.5-2B多模态模型(1.2GB),替代原有的LLM+VL分离架构
- 实现InferenceEngine.current引擎选择逻辑,真机默认MNN,模拟器回退MLX
- 更新AIAgent架构,通过MNNLLMBridge(ObjC++) → MNNBackend进行推理
- 修改队列机制防止并发推理导致OOM,使用信号量闸门控制显存占用
- 更新文档中的技术栈说明、模块边界和周次交付计划
```
2026-06-15 09:24:59 +08:00

233 lines
7.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Testing
import SwiftData
import Foundation
import CoreGraphics
@testable import
struct ModelsSchemaTests {
private func makeContainer() throws -> ModelContainer {
let schema = Schema([
Indicator.self,
Report.self,
DiaryEntry.self,
Asset.self,
ChatTurn.self,
Symptom.self,
UserProfile.self,
MetricReminder.self,
CustomMonitorMetric.self,
Medication.self,
])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
return try ModelContainer(for: schema, configurations: [config])
}
@Test func insertIndicatorWithReportRelationship() throws {
let container = try makeContainer()
let ctx = ModelContext(container)
let report = Report(title: "春检", type: .checkup, reportDate: .now)
let indicator = Indicator(
name: "ALT",
value: "32",
unit: "U/L",
range: "9-50",
status: .normal,
report: report
)
ctx.insert(report)
ctx.insert(indicator)
try ctx.save()
#expect(report.indicators.count == 1)
#expect(indicator.report?.title == "春检")
}
@Test func cascadeDeleteReportRemovesIndicators() throws {
let container = try makeContainer()
let ctx = ModelContext(container)
let report = Report(title: "春检", type: .checkup, reportDate: .now)
let indicator = Indicator(
name: "ALT", value: "32", unit: "U/L", range: "9-50",
status: .normal, report: report
)
ctx.insert(report)
ctx.insert(indicator)
try ctx.save()
ctx.delete(report)
try ctx.save()
let remaining = try ctx.fetch(FetchDescriptor<Indicator>())
#expect(remaining.isEmpty)
}
@Test func ongoingSymptomQueryFiltersByEndedAt() throws {
let container = try makeContainer()
let ctx = ModelContext(container)
let active = Symptom(name: "头痛", startedAt: .now.addingTimeInterval(-3600))
let ended = Symptom(
name: "咳嗽",
startedAt: .now.addingTimeInterval(-7200),
endedAt: .now.addingTimeInterval(-1800)
)
ctx.insert(active)
ctx.insert(ended)
try ctx.save()
let predicate = #Predicate<Symptom> { $0.endedAt == nil }
let ongoing = try ctx.fetch(FetchDescriptor<Symptom>(predicate: predicate))
#expect(ongoing.count == 1)
#expect(ongoing.first?.name == "头痛")
#expect(active.isOngoing)
#expect(!ended.isOngoing)
#expect(active.duration >= 3600)
}
@Test func symptomSeverityClampedToRange() throws {
let high = Symptom(name: "腹痛", severity: 99)
let low = Symptom(name: "失眠", severity: -3)
#expect(high.severity == 5)
#expect(low.severity == 1)
}
@Test func chatTurnPersistsReferencedIDs() throws {
let container = try makeContainer()
let ctx = ModelContext(container)
let turn = ChatTurn(
question: "我的 LDL 怎么样?",
answer: "近 3 个月 LDL 偏高 [1]",
referencedIndicatorIDs: ["abc"],
referencedReportIDs: [],
decodeRate: 24.3
)
ctx.insert(turn)
try ctx.save()
let all = try ctx.fetch(FetchDescriptor<ChatTurn>())
#expect(all.count == 1)
#expect(all.first?.referencedIndicatorIDs == ["abc"])
}
@Test func indicatorSeriesKeyRoundtrip() throws {
let container = try makeContainer()
let ctx = ModelContext(container)
let bp = Indicator(
name: "收缩压",
value: "125",
unit: "mmHg",
range: "90-140",
status: .normal,
pinned: true,
seriesKey: "bp.systolic"
)
ctx.insert(bp)
try ctx.save()
let fetched = try #require(try ctx.fetch(FetchDescriptor<Indicator>()).first)
#expect(fetched.seriesKey == "bp.systolic")
#expect(fetched.pinned == true)
}
@Test func indicatorSeriesKeyDefaultsToNil() {
let i = Indicator(name: "ALT", value: "32", unit: "U/L", range: "9-50", status: .normal)
#expect(i.seriesKey == nil)
}
@Test func indicatorEvidenceLocationRoundtrip() throws {
let container = try makeContainer()
let ctx = ModelContext(container)
let indicator = Indicator(
name: "尿酸",
value: "486",
unit: "μmol/L",
range: "208 - 428",
status: .high,
source: .report,
sourcePageIndex: 1,
sourceBoxX: 0.18,
sourceBoxY: 0.42,
sourceBoxWidth: 0.68,
sourceBoxHeight: 0.08
)
ctx.insert(indicator)
try ctx.save()
let fetched = try #require(try ctx.fetch(FetchDescriptor<Indicator>()).first)
#expect(fetched.sourcePageIndex == 1)
#expect(fetched.sourceBoxX == 0.18)
#expect(fetched.sourceBoxY == 0.42)
#expect(fetched.sourceBoxWidth == 0.68)
#expect(fetched.sourceBoxHeight == 0.08)
#expect(fetched.hasEvidenceBox)
#expect(fetched.evidenceRect?.width == 0.68)
}
@Test func userProfileSchemaPersistsAcrossSave() throws {
let container = try makeContainer()
let ctx = ModelContext(container)
let p = UserProfile(
birthYear: 1985,
biologicalSexRaw: "male",
heightCM: 175,
bloodTypeRaw: "A",
chronicConditions: ["高血压"]
)
ctx.insert(p)
try ctx.save()
let fetched = try #require(try ctx.fetch(FetchDescriptor<UserProfile>()).first)
#expect(fetched.birthYear == 1985)
#expect(fetched.sex == .male)
#expect(fetched.heightCM == 175)
#expect(fetched.bloodTypeRaw == "A")
#expect(fetched.chronicConditions == ["高血压"])
}
@Test func medicationRoundtripAndDetailLine() throws {
let container = try makeContainer()
let ctx = ModelContext(container)
let med = Medication(name: "缬沙坦胶囊", strength: "80mg×7粒", usage: "一日一次,一次一粒")
ctx.insert(med)
try ctx.save()
let fetched = try #require(try ctx.fetch(FetchDescriptor<Medication>()).first)
#expect(fetched.name == "缬沙坦胶囊")
#expect(fetched.detailLine == "80mg×7粒 · 一日一次,一次一粒")
#expect(fetched.updatedAt == fetched.createdAt)
}
@Test func medicationDetailLineOmitsEmptyParts() {
#expect(Medication(name: "维生素C").detailLine == "")
#expect(Medication(name: "钙片", strength: "600mg").detailLine == "600mg")
}
@Test func cascadeDeleteMedicationRemovesAssets() throws {
let container = try makeContainer()
let ctx = ModelContext(container)
let med = Medication(name: "二甲双胍缓释片", strength: "0.5g×30片")
let asset = Asset(relativePath: "med-1.jpg", bytes: 2048)
ctx.insert(asset)
med.assets.append(asset)
ctx.insert(med)
try ctx.save()
#expect(med.assets.count == 1)
ctx.delete(med)
try ctx.save()
#expect(try ctx.fetch(FetchDescriptor<Medication>()).isEmpty)
#expect(try ctx.fetch(FetchDescriptor<Asset>()).isEmpty)
}
}