feat(capture): 统一报告捕获流程并集成视觉语言模型识别
- 替换 QuickCaptureFlow 和 ArchiveFlow 为 UnifiedCaptureFlow 统一流程 - 新增 VLSession 封装 Qwen2.5-VL 模型进行图像文本推理 - 实现 AIRuntime 中 VL 模型的准备和分析功能 - 添加 VLPrompts 定义体检化验单识别的 JSON 输出模板 - 创建 CaptureReviewForm 提供 VL 解析结果的可编辑表单界面 - 集成 VisionKit 文档扫描器支持真机多页文档扫描 - 为模拟器实现 PhotosPicker 回退方案选择已有照片 - 在 RootView 中统一使用 UnifiedCaptureFlow 处理快速和归档流程 - 添加 CustomMetricEditor 支持自定义监测指标的创建编辑删除 - 扩展 KangkangApp 模型配置以支持新数据类型 - 实现档案列表中症状结束功能通过时间线行点击触发
This commit is contained in:
145
康康Tests/CustomMonitorMetricTests.swift
Normal file
145
康康Tests/CustomMonitorMetricTests.swift
Normal file
@@ -0,0 +1,145 @@
|
||||
import Testing
|
||||
import SwiftData
|
||||
import Foundation
|
||||
@testable import 康康
|
||||
|
||||
struct CustomMonitorMetricTests {
|
||||
|
||||
private func makeContainer() throws -> ModelContainer {
|
||||
let schema = Schema([
|
||||
CustomMonitorMetric.self,
|
||||
Indicator.self,
|
||||
])
|
||||
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
|
||||
return try ModelContainer(for: schema, configurations: [config])
|
||||
}
|
||||
|
||||
@Test func seriesKeyAutoPrefixedWithCustom() {
|
||||
let m = CustomMonitorMetric(name: "腰围", unit: "cm")
|
||||
#expect(m.seriesKey.hasPrefix("custom."))
|
||||
#expect(m.seriesKey.count > "custom.".count)
|
||||
}
|
||||
|
||||
@Test func seriesKeyUniquePerInstance() {
|
||||
let a = CustomMonitorMetric(name: "腰围", unit: "cm")
|
||||
let b = CustomMonitorMetric(name: "腰围", unit: "cm")
|
||||
#expect(a.seriesKey != b.seriesKey)
|
||||
}
|
||||
|
||||
@Test func referenceRangeNilWhenBoundsAbsentOrInverted() {
|
||||
let none = CustomMonitorMetric(name: "x", unit: "")
|
||||
#expect(none.referenceRange == nil)
|
||||
|
||||
let inverted = CustomMonitorMetric(name: "x", unit: "", lowerBound: 100, upperBound: 50)
|
||||
#expect(inverted.referenceRange == nil)
|
||||
|
||||
let valid = CustomMonitorMetric(name: "x", unit: "", lowerBound: 60, upperBound: 100)
|
||||
#expect(valid.referenceRange == 60...100)
|
||||
}
|
||||
|
||||
@Test func rangeTextFormattingDropsTrailingZero() {
|
||||
let intRange = CustomMonitorMetric(name: "x", unit: "cm",
|
||||
lowerBound: 70, upperBound: 90)
|
||||
#expect(intRange.rangeText == "70 - 90")
|
||||
|
||||
let decimalRange = CustomMonitorMetric(name: "y", unit: "kg",
|
||||
lowerBound: 60.5, upperBound: 65.5)
|
||||
#expect(decimalRange.rangeText == "60.5 - 65.5")
|
||||
}
|
||||
|
||||
@Test func roundtripsThroughSwiftData() throws {
|
||||
let container = try makeContainer()
|
||||
let ctx = ModelContext(container)
|
||||
let m = CustomMonitorMetric(name: "腰围", unit: "cm",
|
||||
lowerBound: 70, upperBound: 90,
|
||||
icon: "flame.fill")
|
||||
ctx.insert(m)
|
||||
try ctx.save()
|
||||
|
||||
let fetched = try #require(try ctx.fetch(FetchDescriptor<CustomMonitorMetric>()).first)
|
||||
#expect(fetched.name == "腰围")
|
||||
#expect(fetched.unit == "cm")
|
||||
#expect(fetched.lowerBound == 70)
|
||||
#expect(fetched.upperBound == 90)
|
||||
#expect(fetched.icon == "flame.fill")
|
||||
#expect(fetched.seriesKey.hasPrefix("custom."))
|
||||
}
|
||||
|
||||
@Test func seriesBucketResolvesCustomTitleAndRange() {
|
||||
let custom = CustomMonitorMetric(name: "腰围", unit: "cm",
|
||||
lowerBound: 70, upperBound: 90)
|
||||
let key = custom.seriesKey
|
||||
let now = Date()
|
||||
let day = { (offset: Int) -> Date in
|
||||
Calendar.current.date(byAdding: .day, value: offset, to: now)!
|
||||
}
|
||||
let items = [
|
||||
Indicator(name: "腰围", value: "80", unit: "cm", range: "70-90",
|
||||
status: .normal, capturedAt: day(-2), seriesKey: key),
|
||||
Indicator(name: "腰围", value: "82", unit: "cm", range: "70-90",
|
||||
status: .normal, capturedAt: day(-1), seriesKey: key),
|
||||
]
|
||||
let buckets = SeriesBucket.build(from: items, customMetrics: [custom])
|
||||
|
||||
#expect(buckets.count == 1)
|
||||
let b = try! #require(buckets.first)
|
||||
#expect(b.title == "腰围")
|
||||
#expect(b.unit == "cm")
|
||||
#expect(b.lines.first?.referenceRange == 70...90)
|
||||
}
|
||||
|
||||
@Test func nameConflictEmptyNameYieldsNone() {
|
||||
let result = detectNameConflict(candidate: " ", customs: [])
|
||||
#expect(result == .none)
|
||||
}
|
||||
|
||||
@Test func nameConflictDetectsBuiltinMatch() {
|
||||
let result = detectNameConflict(candidate: "血压", customs: [])
|
||||
#expect(result == .builtin("血压"))
|
||||
}
|
||||
|
||||
@Test func nameConflictBuiltinIgnoresWhitespace() {
|
||||
let result = detectNameConflict(candidate: " 空腹血糖 ", customs: [])
|
||||
#expect(result == .builtin("空腹血糖"))
|
||||
}
|
||||
|
||||
@Test func nameConflictDetectsExistingCustom() {
|
||||
let existing = CustomMonitorMetric(name: "腰围", unit: "cm")
|
||||
let result = detectNameConflict(candidate: "腰围", customs: [existing])
|
||||
#expect(result == .existingCustom("腰围"))
|
||||
}
|
||||
|
||||
@Test func nameConflictAllowsRenamingSelf() {
|
||||
// 编辑自己时,即使没改名也不应该报冲突
|
||||
let me = CustomMonitorMetric(name: "腰围", unit: "cm")
|
||||
let result = detectNameConflict(
|
||||
candidate: "腰围",
|
||||
customs: [me],
|
||||
excludingSeriesKey: me.seriesKey
|
||||
)
|
||||
#expect(result == .none)
|
||||
}
|
||||
|
||||
@Test func nameConflictUnique() {
|
||||
let result = detectNameConflict(candidate: "步数", customs: [])
|
||||
#expect(result == .none)
|
||||
}
|
||||
|
||||
@Test func seriesBucketFallsBackToIndicatorNameWhenCustomMissing() {
|
||||
// 用户删了 CustomMonitorMetric 但 Indicator 还在 → title fallback 到 indicator.name
|
||||
let orphanKey = "custom.deleted-xxxxx"
|
||||
let now = Date()
|
||||
let items = [
|
||||
Indicator(name: "睡眠时长", value: "7", unit: "h", range: "",
|
||||
status: .normal, capturedAt: now, seriesKey: orphanKey),
|
||||
Indicator(name: "睡眠时长", value: "8", unit: "h", range: "",
|
||||
status: .normal, capturedAt: now.addingTimeInterval(60),
|
||||
seriesKey: orphanKey),
|
||||
]
|
||||
let buckets = SeriesBucket.build(from: items, customMetrics: [])
|
||||
let b = try! #require(buckets.first)
|
||||
#expect(b.title == "睡眠时长")
|
||||
#expect(b.unit == "h")
|
||||
#expect(b.lines.first?.referenceRange == nil)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user