import Testing import Foundation @testable import 康康 /// `ExportTrendBuilder` 是纯函数,覆盖:方向判定、血压合并、相关性过滤、 /// 点数 <2 过滤、跨参考范围边界标记、非数值点丢弃、整行文案格式。 struct ExportTrendBuilderTests { private func ind( name: String = "血糖", value: String, unit: String = "mmol/L", range: String = "3.9-6.1", status: IndicatorStatus = .normal, daysAgo: Int, seriesKey: String? = nil ) -> Indicator { let date = Calendar.current.date(byAdding: .day, value: -daysAgo, to: .now)! return Indicator(name: name, value: value, unit: unit, range: range, status: status, capturedAt: date, seriesKey: seriesKey) } @Test func upDirectionFlaggedAndLineFormat() { let items = [ ind(value: "5.2", status: .normal, daysAgo: 27), ind(value: "6.8", status: .high, daysAgo: 0), ] let trends = ExportTrendBuilder.build(allInWindow: items, relevant: items) let t = try! #require(trends.first) #expect(t.direction == .up) #expect(t.flagged) // 末值 high #expect(t.count == 2) #expect(t.valueText == "5.2→6.8") #expect(t.rangeText == "3.9-6.1") #expect(t.line() == "⚠️ 血糖 5.2→6.8 mmol/L ↑(参考 3.9-6.1),近 27 天 2 次") } @Test func downDirection() { let items = [ ind(value: "6.0", daysAgo: 10), ind(value: "5.0", daysAgo: 1), ] let t = try! #require(ExportTrendBuilder.build(allInWindow: items, relevant: items).first) #expect(t.direction == .down) #expect(!t.flagged) // 两点都 normal } @Test func flatWithinThreshold() { // (5.1-5.0)/5.0 = 0.02 < 0.05 → 平稳 let items = [ ind(value: "5.0", daysAgo: 5), ind(value: "5.1", daysAgo: 1), ] let t = try! #require(ExportTrendBuilder.build(allInWindow: items, relevant: items).first) #expect(t.direction == .flat) } @Test func filtersSeriesWithFewerThanTwoPoints() { let items = [ind(value: "5.0", daysAgo: 1)] #expect(ExportTrendBuilder.build(allInWindow: items, relevant: items).isEmpty) } @Test func excludesIrrelevantSeries() { let glucose = [ ind(name: "血糖", value: "5.0", unit: "mmol/L", daysAgo: 3), ind(name: "血糖", value: "5.5", unit: "mmol/L", daysAgo: 1), ] let weight = [ ind(name: "体重", value: "68", unit: "kg", range: "", daysAgo: 3), ind(name: "体重", value: "67", unit: "kg", range: "", daysAgo: 1), ] // weight 有 ≥2 点,但不在 relevant 集 → 不出趋势 let trends = ExportTrendBuilder.build(allInWindow: glucose + weight, relevant: glucose) #expect(trends.count == 1) #expect(trends.first?.title == "血糖") } @Test func bloodPressureMergesToSingleLine() { let items = [ ind(name: "收缩压", value: "150", unit: "mmHg", range: "", daysAgo: 20, seriesKey: "bp.systolic"), ind(name: "舒张压", value: "95", unit: "mmHg", range: "", daysAgo: 20, seriesKey: "bp.diastolic"), ind(name: "收缩压", value: "138", unit: "mmHg", range: "", daysAgo: 1, seriesKey: "bp.systolic"), ind(name: "舒张压", value: "88", unit: "mmHg", range: "", daysAgo: 1, seriesKey: "bp.diastolic"), ] let t = try! #require(ExportTrendBuilder.build(allInWindow: items, relevant: items).first) #expect(t.title == "血压") #expect(t.unit == "mmHg") #expect(t.valueText == "150/95→138/88") #expect(t.direction == .down) // 收缩压为准 #expect(t.rangeText == nil) // 血压双范围不展示 } @Test func flaggedWhenStatusCrossesBoundary() { // 首高末正常:跨参考范围边界 → 仍 flagged,提示医生注意变化 let items = [ ind(value: "6.8", status: .high, daysAgo: 5), ind(value: "5.5", status: .normal, daysAgo: 1), ] let t = try! #require(ExportTrendBuilder.build(allInWindow: items, relevant: items).first) #expect(t.flagged) } @Test func nonNumericPointDropped() { let items = [ ind(value: "高", daysAgo: 3), ind(value: "5.0", daysAgo: 2), ind(value: "5.5", daysAgo: 1), ] let t = try! #require(ExportTrendBuilder.build(allInWindow: items, relevant: items).first) #expect(t.count == 2) // "高" 解析失败被丢 } }