```
feat: 添加拍药盒功能和语音直达入口 - 实现拍药盒扫描流程,支持本地OCR识别药品信息 - 在日记页面添加拍药盒和记症状的三选一入口 - 优化按钮点击区域,确保符合苹果HIG最小命中区标准 - 添加用药记录到时间线的独立分类显示 - 实现长按+号语音直达功能,支持语音意图分类跳转 - 更新项目配置文件,启用代码分析和死代码剥离选项 - 增加多项本地化字符串支持新功能 ```
This commit is contained in:
85
康康Tests/MedicationScanServiceTests.swift
Normal file
85
康康Tests/MedicationScanServiceTests.swift
Normal file
@@ -0,0 +1,85 @@
|
||||
import Testing
|
||||
import Foundation
|
||||
import SwiftData
|
||||
@testable import 康康
|
||||
|
||||
/// MedicationScanService.parseMedicationsJSON 纯函数单测(JSON 容错与去重)。
|
||||
struct MedicationScanServiceTests {
|
||||
|
||||
@Test func parsesStandardObject() throws {
|
||||
let raw = """
|
||||
{"medications":[{"name":"缬沙坦胶囊","strength":"80mg×7粒","usage":""}]}
|
||||
"""
|
||||
let meds = try MedicationScanService.parseMedicationsJSON(raw)
|
||||
#expect(meds.count == 1)
|
||||
#expect(meds[0].name == "缬沙坦胶囊")
|
||||
#expect(meds[0].strength == "80mg×7粒")
|
||||
#expect(meds[0].entryText == "缬沙坦胶囊 80mg×7粒")
|
||||
}
|
||||
|
||||
@Test func parsesBareArrayWithFence() throws {
|
||||
let raw = """
|
||||
```json
|
||||
[{"name":"二甲双胍缓释片","strength":"0.5g×30片","usage":"口服,一次1片,一日2次"}]
|
||||
```
|
||||
"""
|
||||
let meds = try MedicationScanService.parseMedicationsJSON(raw)
|
||||
#expect(meds.count == 1)
|
||||
#expect(meds[0].entryText == "二甲双胍缓释片 0.5g×30片 · 口服,一次1片,一日2次")
|
||||
}
|
||||
|
||||
@Test func parsesChineseKeysAndDedupes() throws {
|
||||
let raw = """
|
||||
{"medications":[
|
||||
{"药名":"阿司匹林肠溶片","规格":"100mg","用法":""},
|
||||
{"name":"阿司匹林肠溶片","strength":"100mg","usage":""}
|
||||
]}
|
||||
"""
|
||||
let meds = try MedicationScanService.parseMedicationsJSON(raw)
|
||||
#expect(meds.count == 1)
|
||||
}
|
||||
|
||||
@Test func emptyNameRowsAreDropped() throws {
|
||||
let raw = #"{"medications":[{"name":"","strength":"10mg","usage":""}]}"#
|
||||
let meds = try MedicationScanService.parseMedicationsJSON(raw)
|
||||
#expect(meds.isEmpty)
|
||||
}
|
||||
|
||||
@Test func trailingCommaIsRepaired() throws {
|
||||
let raw = #"{"medications":[{"name":"氯雷他定片","strength":"10mg×6片","usage":"",},]}"#
|
||||
let meds = try MedicationScanService.parseMedicationsJSON(raw)
|
||||
#expect(meds.count == 1)
|
||||
#expect(meds[0].name == "氯雷他定片")
|
||||
}
|
||||
|
||||
@Test func invalidJSONThrows() {
|
||||
#expect(throws: (any Error).self) {
|
||||
try MedicationScanService.parseMedicationsJSON("识别不出来,抱歉")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 「用药」日记 → 时间线分类映射(拍药盒入档落库后在「记录」tab 的归类)。
|
||||
@MainActor
|
||||
struct MedicationTimelineTests {
|
||||
|
||||
private func makeContext() throws -> ModelContext {
|
||||
let schema = Schema([DiaryEntry.self])
|
||||
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
|
||||
return ModelContext(try ModelContainer(for: schema, configurations: [config]))
|
||||
}
|
||||
|
||||
@Test func medicationTaggedDiaryMapsToMedicationKind() throws {
|
||||
let ctx = try makeContext()
|
||||
let med = DiaryEntry(content: "缬沙坦胶囊 80mg×7粒", tags: [DiaryEntry.medicationTag])
|
||||
let plain = DiaryEntry(content: "今天睡得不错")
|
||||
ctx.insert(med); ctx.insert(plain)
|
||||
try ctx.save()
|
||||
|
||||
let medEntry = TimelineEntry.from(diary: med)
|
||||
#expect(medEntry.kind == .medication)
|
||||
#expect(medEntry.title == "缬沙坦胶囊 80mg×7粒")
|
||||
|
||||
#expect(TimelineEntry.from(diary: plain).kind == .diary)
|
||||
}
|
||||
}
|
||||
22
康康Tests/SpeechDictationMergeTests.swift
Normal file
22
康康Tests/SpeechDictationMergeTests.swift
Normal file
@@ -0,0 +1,22 @@
|
||||
import Testing
|
||||
@testable import 康康
|
||||
|
||||
struct SpeechDictationMergeTests {
|
||||
@Test func emptyPrefixReturnsPartial() {
|
||||
#expect(SpeechDictationService.merge(prefix: "", partial: "今天头晕") == "今天头晕")
|
||||
}
|
||||
|
||||
@Test func plainPrefixJoinsWithSpace() {
|
||||
#expect(SpeechDictationService.merge(prefix: "已有内容", partial: "新听写")
|
||||
== "已有内容 新听写")
|
||||
}
|
||||
|
||||
@Test func whitespaceTerminatedPrefixConcatsDirectly() {
|
||||
#expect(SpeechDictationService.merge(prefix: "第一行\n", partial: "新听写")
|
||||
== "第一行\n新听写")
|
||||
}
|
||||
|
||||
@Test func emptyPartialKeepsPrefix() {
|
||||
#expect(SpeechDictationService.merge(prefix: "已有内容", partial: "") == "已有内容")
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,10 @@ struct TimelineGroupingTests {
|
||||
return Calendar(identifier: .gregorian).date(from: c)!
|
||||
}()
|
||||
|
||||
@Test func timelineKindOrderMatchesRecordFilterChips() {
|
||||
#expect(TimelineKind.allCases == [.diary, .symptom, .indicator, .medication, .report])
|
||||
}
|
||||
|
||||
@Test func todaySection() {
|
||||
#expect(TimelineGrouping.section(for: now, now: now) == .today)
|
||||
}
|
||||
|
||||
53
康康Tests/VoiceIntentServiceTests.swift
Normal file
53
康康Tests/VoiceIntentServiceTests.swift
Normal file
@@ -0,0 +1,53 @@
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import 康康
|
||||
|
||||
/// 语音直达的两个纯函数:LLM 输出解析 + 关键词回退。
|
||||
struct VoiceIntentServiceTests {
|
||||
|
||||
// MARK: - parseIntent(LLM 输出容错)
|
||||
|
||||
@Test func parsesStandardJSON() {
|
||||
#expect(VoiceIntentService.parseIntent(from: #"{"intent":"indicator"}"#) == .indicator)
|
||||
}
|
||||
|
||||
@Test func parsesFencedAndThinkWrapped() {
|
||||
let raw = """
|
||||
<think>用户想记血压</think>
|
||||
```json
|
||||
{"intent": "Indicator"}
|
||||
```
|
||||
"""
|
||||
#expect(VoiceIntentService.parseIntent(from: raw) == .indicator)
|
||||
}
|
||||
|
||||
@Test func parsesBareWord() {
|
||||
#expect(VoiceIntentService.parseIntent(from: "symptom") == .symptom)
|
||||
#expect(VoiceIntentService.parseIntent(from: "\"diary\"。") == .diary)
|
||||
}
|
||||
|
||||
@Test func unknownReturnsNil() {
|
||||
#expect(VoiceIntentService.parseIntent(from: #"{"intent":"unknown"}"#) == nil)
|
||||
#expect(VoiceIntentService.parseIntent(from: "我不知道") == nil)
|
||||
}
|
||||
|
||||
// MARK: - keywordMatch(回退规则与优先级)
|
||||
|
||||
@Test func reminderBeatsMedication() {
|
||||
// 「提醒我吃药」是设提醒,不是记用药 —— reminder 规则必须排最前
|
||||
#expect(VoiceIntentService.keywordMatch("每天八点提醒我吃药") == .reminder)
|
||||
}
|
||||
|
||||
@Test func commonUtterances() {
|
||||
#expect(VoiceIntentService.keywordMatch("记一下血压,高压128") == .indicator)
|
||||
#expect(VoiceIntentService.keywordMatch("我有点头疼") == .symptom)
|
||||
#expect(VoiceIntentService.keywordMatch("拍个药盒") == .medication)
|
||||
#expect(VoiceIntentService.keywordMatch("把体检报告存进去") == .archive)
|
||||
#expect(VoiceIntentService.keywordMatch("整理一份给医生看的") == .export)
|
||||
#expect(VoiceIntentService.keywordMatch("写个日记") == .diary)
|
||||
}
|
||||
|
||||
@Test func gibberishReturnsNil() {
|
||||
#expect(VoiceIntentService.keywordMatch("啦啦啦啦") == nil)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user