import Testing import SwiftData import Foundation @testable import 康康 @MainActor struct TimelineEntryBPMergeTests { private func makeContext() throws -> ModelContext { let schema = Schema([Indicator.self]) let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true) return ModelContext(try ModelContainer(for: schema, configurations: [config])) } private func bp(sys: Int, dia: Int, at: Date, in ctx: ModelContext) -> (Indicator, Indicator) { let s = Indicator(name: "收缩压", value: "\(sys)", unit: "mmHg", range: "90-140", status: .normal, capturedAt: at, pinned: true, seriesKey: "bp.systolic") let d = Indicator(name: "舒张压", value: "\(dia)", unit: "mmHg", range: "60-90", status: .normal, capturedAt: at, pinned: true, seriesKey: "bp.diastolic") ctx.insert(s); ctx.insert(d) return (s, d) } @Test func bpPairAtSameTimeMergesIntoSingleEntry() throws { let ctx = try makeContext() let now = Date(timeIntervalSince1970: 1_716_000_000) _ = bp(sys: 120, dia: 80, at: now, in: ctx) try ctx.save() let all = try ctx.fetch(FetchDescriptor()) let entries = TimelineEntry.from(indicators: all) #expect(entries.count == 1) let e = try #require(entries.first) #expect(e.title == "血压") #expect(e.trailing?.contains("120/80") == true) #expect(e.trailingIsAlert == false) } @Test func bpPairWithAbnormalSystolicMarksAlert() throws { let ctx = try makeContext() let now = Date() let (s, _) = bp(sys: 150, dia: 80, at: now, in: ctx) s.statusRaw = IndicatorStatus.high.rawValue try ctx.save() let all = try ctx.fetch(FetchDescriptor()) let entries = TimelineEntry.from(indicators: all) #expect(entries.count == 1) #expect(entries.first?.trailingIsAlert == true) #expect(entries.first?.trailing?.contains("↑") == true) } @Test func nonBPSeriesStayAsSeparateEntries() throws { let ctx = try makeContext() let now = Date() let glu = Indicator(name: "空腹血糖", value: "5.4", unit: "mmol/L", range: "3.9-6.1", status: .normal, capturedAt: now, pinned: true, seriesKey: "glucose.fasting") let weight = Indicator(name: "体重", value: "68", unit: "kg", range: "", status: .normal, capturedAt: now, pinned: true, seriesKey: "weight") ctx.insert(glu); ctx.insert(weight) try ctx.save() let all = try ctx.fetch(FetchDescriptor()) let entries = TimelineEntry.from(indicators: all) #expect(entries.count == 2) #expect(entries.contains { $0.title == "空腹血糖" }) #expect(entries.contains { $0.title == "体重" }) } @Test func bpAtDifferentTimesDoNotMerge() throws { let ctx = try makeContext() let t1 = Date(timeIntervalSince1970: 1_716_000_000) let t2 = t1.addingTimeInterval(3600) // 1 小时后 _ = bp(sys: 120, dia: 80, at: t1, in: ctx) _ = bp(sys: 130, dia: 85, at: t2, in: ctx) try ctx.save() let all = try ctx.fetch(FetchDescriptor()) let entries = TimelineEntry.from(indicators: all) #expect(entries.count == 2) } @Test func unpairedSystolicFallsBackToSingleEntry() throws { let ctx = try makeContext() let s = Indicator(name: "收缩压", value: "120", unit: "mmHg", range: "90-140", status: .normal, capturedAt: .now, pinned: true, seriesKey: "bp.systolic") ctx.insert(s) try ctx.save() let all = try ctx.fetch(FetchDescriptor()) let entries = TimelineEntry.from(indicators: all) // 没找到 diastolic 配对,落到单 from(indicator:),显示 "收缩压" #expect(entries.count == 1) #expect(entries.first?.title == "收缩压") } @Test func freeformIndicatorWithoutSeriesKeyShowsAsItself() throws { let ctx = try makeContext() let i = Indicator(name: "ALT", value: "32", unit: "U/L", range: "9-50", status: .normal, capturedAt: .now) ctx.insert(i) try ctx.save() let all = try ctx.fetch(FetchDescriptor()) let entries = TimelineEntry.from(indicators: all) #expect(entries.count == 1) #expect(entries.first?.title == "ALT") } }