根据提供的code differences信息,我发现没有具体的代码变更内容。因此生成一个通用的commit message:
``` docs(readme): 更新文档说明 - 添加项目使用说明 - 完善配置指南 - 修正错误描述 ```
This commit is contained in:
103
康康Tests/DiaryChatPromptTests.swift
Normal file
103
康康Tests/DiaryChatPromptTests.swift
Normal file
@@ -0,0 +1,103 @@
|
||||
import Testing
|
||||
@testable import 康康
|
||||
|
||||
/// 日记追问对话的 prompt 拼装契约:收尾/继续两种口径互斥、红线短语、
|
||||
/// dataJSON 原文嵌入、蒸馏截断与 /no_think 结尾。
|
||||
/// 对齐 DiaryOrganizePromptTests 风格:纯字符串断言,不涉隔离。
|
||||
struct DiaryChatPromptTests {
|
||||
|
||||
/// 唯一、易断言的注入片段。
|
||||
private let dataJSON = #"{"indicators":[{"name":"收缩压","value":"145"}]}"#
|
||||
|
||||
// MARK: 收尾口径(roundsLeft == 0)
|
||||
|
||||
@Test func replyAtZeroRoundsForbidsFurtherQuestions() {
|
||||
let prompt = DiaryChatPrompts.reply(
|
||||
transcript: "我: 我今天头疼",
|
||||
latest: "我头疼",
|
||||
dataJSON: dataJSON,
|
||||
roundsLeft: 0
|
||||
)
|
||||
#expect(prompt.contains("不要再提任何问题"))
|
||||
#expect(!prompt.contains("最多问 1 个新问题"))
|
||||
}
|
||||
|
||||
// MARK: 继续口径(roundsLeft > 0)
|
||||
|
||||
@Test func replyWithRoundsLeftAllowsOneMoreQuestion() {
|
||||
let prompt = DiaryChatPrompts.reply(
|
||||
transcript: "我: 我今天头疼",
|
||||
latest: "我头疼",
|
||||
dataJSON: dataJSON,
|
||||
roundsLeft: 2
|
||||
)
|
||||
#expect(prompt.contains("最多问 1 个新问题"))
|
||||
#expect(!prompt.contains("不要再提任何问题"))
|
||||
}
|
||||
|
||||
// MARK: 红线短语 + dataJSON 原文嵌入(opening 与 reply 都要有)
|
||||
|
||||
@Test func openingCarriesNoReferralRuleAndEmbedsDataJSON() {
|
||||
let prompt = DiaryChatPrompts.opening(dataJSON: dataJSON)
|
||||
#expect(prompt.contains("不写「建议就医」"))
|
||||
#expect(prompt.contains(dataJSON))
|
||||
}
|
||||
|
||||
@Test func replyCarriesNoReferralRuleAndEmbedsDataJSON() {
|
||||
let prompt = DiaryChatPrompts.reply(
|
||||
transcript: "我: 我今天头疼",
|
||||
latest: "我头疼",
|
||||
dataJSON: dataJSON,
|
||||
roundsLeft: 1
|
||||
)
|
||||
#expect(prompt.contains("不写「建议就医」"))
|
||||
#expect(prompt.contains(dataJSON))
|
||||
}
|
||||
|
||||
// MARK: 蒸馏 prompt 的取材约束短语
|
||||
|
||||
@Test func distillContainsUserOnlyAndForbiddenRule() {
|
||||
let prompt = DiaryChatPrompts.distill(transcript: "我: 我今天头疼\n康康: 收到")
|
||||
#expect(prompt.contains("只取「我:」"))
|
||||
#expect(prompt.contains("绝对不许"))
|
||||
}
|
||||
|
||||
// MARK: 三个 prompt trim 后都以 /no_think 结尾
|
||||
|
||||
@Test func openingEndsWithNoThink() {
|
||||
let prompt = DiaryChatPrompts.opening(dataJSON: dataJSON)
|
||||
#expect(prompt.trimmingCharacters(in: .whitespacesAndNewlines).hasSuffix("/no_think"))
|
||||
}
|
||||
|
||||
@Test func replyEndsWithNoThink() {
|
||||
let prompt = DiaryChatPrompts.reply(
|
||||
transcript: "我: 我今天头疼",
|
||||
latest: "我头疼",
|
||||
dataJSON: dataJSON,
|
||||
roundsLeft: 1
|
||||
)
|
||||
#expect(prompt.trimmingCharacters(in: .whitespacesAndNewlines).hasSuffix("/no_think"))
|
||||
}
|
||||
|
||||
@Test func distillEndsWithNoThink() {
|
||||
let prompt = DiaryChatPrompts.distill(transcript: "我: 我今天头疼")
|
||||
#expect(prompt.trimmingCharacters(in: .whitespacesAndNewlines).hasSuffix("/no_think"))
|
||||
}
|
||||
|
||||
// MARK: 蒸馏对超长稿件截断到 distillTranscriptLimit(2000)
|
||||
|
||||
@Test func distillTruncatesTranscriptBeyondLimit() {
|
||||
// 起始标记落在前 2000 内应保留;越限标记落在第 2500 字符附近应被截掉。
|
||||
let earlyMark = "起始标记AAA"
|
||||
let lateMark = "越限标记ZZZ"
|
||||
let transcript = earlyMark
|
||||
+ String(repeating: "文", count: 2500 - earlyMark.count) // 把越限标记推到 index≈2500
|
||||
+ lateMark
|
||||
+ String(repeating: "字", count: 500) // 凑到 3000+ 字符
|
||||
#expect(transcript.count > DiaryChatPrompts.distillTranscriptLimit)
|
||||
|
||||
let prompt = DiaryChatPrompts.distill(transcript: transcript)
|
||||
#expect(prompt.contains(earlyMark))
|
||||
#expect(!prompt.contains(lateMark))
|
||||
}
|
||||
}
|
||||
130
康康Tests/DiaryChatServiceTests.swift
Normal file
130
康康Tests/DiaryChatServiceTests.swift
Normal file
@@ -0,0 +1,130 @@
|
||||
import Testing
|
||||
@testable import 康康
|
||||
|
||||
/// 日记追问对话的纯逻辑:轮数计数、收尾判定、离线兜底草稿。
|
||||
/// DiaryChatService 是 @MainActor struct,测试整体标 @MainActor 规避隔离。
|
||||
@MainActor
|
||||
struct DiaryChatServiceTests {
|
||||
|
||||
// MARK: roundsUsed —— 开场白不算追问(assistant 轮数 − 1),user 轮不计
|
||||
|
||||
@Test func roundsUsedIsZeroForEmptyTurns() {
|
||||
#expect(DiaryChatService.roundsUsed(in: []) == 0)
|
||||
}
|
||||
|
||||
@Test func roundsUsedIsZeroForOpeningOnly() {
|
||||
// 仅 1 条 assistant(开场白)→ max(0, 1-1) = 0
|
||||
let turns: [HealthExportDialogueTurn] = [.assistant("说说你今天的情况")]
|
||||
#expect(DiaryChatService.roundsUsed(in: turns) == 0)
|
||||
}
|
||||
|
||||
@Test func roundsUsedCountsAssistantRepliesAfterOpening() {
|
||||
// 开场 + 2 条追问 = 3 条 assistant → max(0, 3-1) = 2
|
||||
let turns: [HealthExportDialogueTurn] = [
|
||||
.assistant("开场"),
|
||||
.user("我头疼"),
|
||||
.assistant("追问一"),
|
||||
.user("还发烧"),
|
||||
.assistant("追问二")
|
||||
]
|
||||
#expect(DiaryChatService.roundsUsed(in: turns) == 2)
|
||||
}
|
||||
|
||||
@Test func roundsUsedIgnoresUserTurns() {
|
||||
// 夹杂多条 user 不影响计数:仍是 3 条 assistant → 2
|
||||
let turns: [HealthExportDialogueTurn] = [
|
||||
.assistant("开场"),
|
||||
.user("u1"), .user("u2"),
|
||||
.assistant("追问一"),
|
||||
.user("u3"), .user("u4"), .user("u5"),
|
||||
.assistant("追问二"),
|
||||
.user("u6")
|
||||
]
|
||||
#expect(DiaryChatService.roundsUsed(in: turns) == 2)
|
||||
}
|
||||
|
||||
// MARK: isWrappedUp —— roundsUsed >= maxRounds(4)
|
||||
|
||||
@Test func isNotWrappedUpAtThreeRounds() {
|
||||
// 4 条 assistant(开场 + 3 追问)→ roundsUsed = 3 < 4 → 未收尾
|
||||
let turns: [HealthExportDialogueTurn] = [
|
||||
.assistant("开场"),
|
||||
.user("a"), .assistant("q1"),
|
||||
.user("b"), .assistant("q2"),
|
||||
.user("c"), .assistant("q3")
|
||||
]
|
||||
#expect(DiaryChatService.roundsUsed(in: turns) == 3)
|
||||
#expect(DiaryChatService.isWrappedUp(turns: turns) == false)
|
||||
}
|
||||
|
||||
@Test func isWrappedUpAtFourRounds() {
|
||||
// 5 条 assistant(开场 + 4 追问)→ roundsUsed = 4 = maxRounds → 收尾
|
||||
let turns: [HealthExportDialogueTurn] = [
|
||||
.assistant("开场"),
|
||||
.user("a"), .assistant("q1"),
|
||||
.user("b"), .assistant("q2"),
|
||||
.user("c"), .assistant("q3"),
|
||||
.user("d"), .assistant("q4")
|
||||
]
|
||||
#expect(DiaryChatService.roundsUsed(in: turns) == DiaryChatPrompts.maxRounds)
|
||||
#expect(DiaryChatService.isWrappedUp(turns: turns) == true)
|
||||
}
|
||||
|
||||
// MARK: fallbackDraft —— 只拼 user 原话,assistant 绝不进草稿
|
||||
|
||||
@Test func fallbackDraftIsEmptyWhenNoUserTurns() {
|
||||
let turns: [HealthExportDialogueTurn] = [
|
||||
.assistant("开场"),
|
||||
.assistant("再问一句")
|
||||
]
|
||||
#expect(DiaryChatService.fallbackDraft(turns: turns) == "")
|
||||
}
|
||||
|
||||
@Test func fallbackDraftReturnsSingleUserVerbatimWithoutBullet() {
|
||||
// 恰 1 条 user → 原话本身,无「· 」前缀;数值原样
|
||||
let turns: [HealthExportDialogueTurn] = [
|
||||
.assistant("说说你今天的情况"),
|
||||
.user("今天头疼,量了血压 140/90")
|
||||
]
|
||||
let draft = DiaryChatService.fallbackDraft(turns: turns)
|
||||
#expect(draft == "今天头疼,量了血压 140/90")
|
||||
#expect(!draft.contains("· "))
|
||||
}
|
||||
|
||||
@Test func fallbackDraftBulletsMultipleUserTurns() {
|
||||
// ≥2 条 user → 每条前缀「· 」按换行拼接
|
||||
let turns: [HealthExportDialogueTurn] = [
|
||||
.assistant("开场"),
|
||||
.user("今天头疼"),
|
||||
.assistant("量血压了吗?"),
|
||||
.user("量了血压 140/90")
|
||||
]
|
||||
let draft = DiaryChatService.fallbackDraft(turns: turns)
|
||||
#expect(draft == "· 今天头疼\n· 量了血压 140/90")
|
||||
}
|
||||
|
||||
@Test func fallbackDraftDropsBlankUserTurns() {
|
||||
// 纯空格 / 换行空白的 user 轮被丢弃,剩两条有效仍带「· 」
|
||||
let turns: [HealthExportDialogueTurn] = [
|
||||
.user("第一条有效"),
|
||||
.user(" "),
|
||||
.user("\n \n"),
|
||||
.user("第二条有效")
|
||||
]
|
||||
let draft = DiaryChatService.fallbackDraft(turns: turns)
|
||||
#expect(draft == "· 第一条有效\n· 第二条有效")
|
||||
}
|
||||
|
||||
@Test func fallbackDraftNeverIncludesAssistantTextAndKeepsNumbers() {
|
||||
// assistant 文本绝不出现;140/90 原样保留
|
||||
let turns: [HealthExportDialogueTurn] = [
|
||||
.assistant("上次你记过头疼"),
|
||||
.user("今天又头疼了,血压 140/90"),
|
||||
.assistant("需要提醒复诊吗?")
|
||||
]
|
||||
let draft = DiaryChatService.fallbackDraft(turns: turns)
|
||||
#expect(!draft.contains("上次你记过头疼"))
|
||||
#expect(!draft.contains("需要提醒复诊吗?"))
|
||||
#expect(draft.contains("140/90"))
|
||||
}
|
||||
}
|
||||
@@ -133,4 +133,24 @@ struct FileDownloaderTests {
|
||||
}
|
||||
#expect(!FileManager.default.fileExists(atPath: dst.path))
|
||||
}
|
||||
|
||||
@Test func failsFastOnServerTotalMismatchAndRemovesPart() async throws {
|
||||
// 上游仓库更新后文件变大的场景:206 Content-Range 报告的总大小(100)≠ 清单预期(5),
|
||||
// 应在响应头阶段就抛 sizeMismatch(got=服务器总大小),并删掉 .part 防跨版本拼接。
|
||||
let url = uniqueURL()
|
||||
MockURLProtocol.register(url, body: Data(repeating: 0xAB, count: 100))
|
||||
let dst = tempFile()
|
||||
defer { try? FileManager.default.removeItem(at: dst.deletingLastPathComponent()) }
|
||||
|
||||
let dl = FileDownloader(configuration: mockConfiguration())
|
||||
do {
|
||||
try await dl.download(from: url, to: dst, expectedBytes: 5)
|
||||
Issue.record("预期抛 sizeMismatch,实际成功")
|
||||
} catch let DownloadError.sizeMismatch(expected, got) {
|
||||
#expect(expected == 5)
|
||||
#expect(got == 100) // 快速失败拿到的是响应头里的服务器总大小,不是下完的字节数
|
||||
}
|
||||
#expect(!FileManager.default.fileExists(atPath: dst.path))
|
||||
#expect(!FileManager.default.fileExists(atPath: dst.appendingPathExtension("part").path))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,9 @@ struct ModelManifestTests {
|
||||
}
|
||||
|
||||
@Test func llmTotalBytesMatchesManifest() {
|
||||
// gemma-4-e2b-it-4bit 全部运行文件字节之和(ModelScope repo/files 实测,2026-07)。
|
||||
#expect(ModelManifest.totalBytes(for: .llm) == 3_613_528_388)
|
||||
// gemma-4-e2b-it-4bit 全部运行文件字节之和(魔搭/hf-mirror 双源实测,2026-07-14,
|
||||
// 上游 7 月中旬更新过 checkpoint 后的新值)。
|
||||
#expect(ModelManifest.totalBytes(for: .llm) == 3_583_086_498)
|
||||
}
|
||||
|
||||
@Test func vlTotalBytesMatchesManifest() {
|
||||
|
||||
Reference in New Issue
Block a user