Files
OSGKeyboard/OSGKeyboardTests/ClipboardReplyFeedbackStoreTests.swift
T
2026-08-27 18:01:46 +08:00

266 lines
8.2 KiB
Swift

// ClipboardReplyFeedbackStoreTests.swift
// OSGKeyboardTests
@testable import OSGKeyboardShared
import XCTest
@MainActor
final class ClipboardReplyFeedbackStoreTests: XCTestCase {
private var suiteName: String!
private var defaults: UserDefaults!
private var store: ClipboardReplyFeedbackStore!
override func setUp() {
super.setUp()
suiteName = "group.com.osgkeyboard.reply-feedback.\(UUID().uuidString)"
defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
store = ClipboardReplyFeedbackStore(defaults: defaults)
}
override func tearDown() {
defaults.removePersistentDomain(forName: suiteName)
super.tearDown()
}
func testSelectionAndVerifiedFinalEditAreRecorded() throws {
let candidates = makeCandidates()
let recordID = try XCTUnwrap(
store.begin(
sourceText: "周六下午去看展吗?",
candidates: candidates,
styleID: "user.personal"
)
)
let answerID = UUID()
store.recordSelection(
recordID: recordID,
candidateID: candidates[2].id,
answerID: answerID
)
store.recordFinalEdit(
answerID: answerID,
text: "可以呀,几点出发?🙂",
revision: 1
)
let record = try XCTUnwrap(store.records().first)
XCTAssertEqual(record.outcome, .selected)
XCTAssertEqual(record.selectedCandidate?.kind, .playful)
XCTAssertEqual(record.answerID, answerID)
XCTAssertEqual(record.finalText, "可以呀,几点出发?🙂")
XCTAssertEqual(record.finalRevision, 1)
}
func testUnverifiedOrUnrelatedEditIsIgnored() throws {
let candidates = makeCandidates()
let recordID = try XCTUnwrap(
store.begin(
sourceText: "今晚要不要一起吃饭?",
candidates: candidates,
styleID: nil
)
)
let answerID = UUID()
store.recordSelection(
recordID: recordID,
candidateID: candidates[0].id,
answerID: answerID
)
store.recordFinalEdit(answerID: UUID(), text: "无关修改", revision: 1)
store.recordFinalEdit(answerID: answerID, text: "零版本", revision: 0)
let record = try XCTUnwrap(store.records().first)
XCTAssertNil(record.finalText)
XCTAssertNil(record.finalRevision)
}
func testSensitiveSourceOrCandidateIsRejected() {
let candidates = makeCandidates()
XCTAssertNil(
store.begin(
sourceText: "123456",
candidates: candidates,
styleID: nil
)
)
var unsafeCandidates = candidates
unsafeCandidates[0] = ClipboardReplyCandidateSnapshot(
kind: .ordinary,
text: "Bearer abcdefghijklmnopqrstuvwxyz",
emotion: "neutral"
)
XCTAssertNil(
store.begin(
sourceText: "普通消息",
candidates: unsafeCandidates,
styleID: nil
)
)
XCTAssertTrue(store.records().isEmpty)
}
func testDuplicateCandidateKindsAreRejected() {
let duplicate = [
ClipboardReplyCandidateSnapshot(
kind: .ordinary,
text: "第一条",
emotion: "neutral"
),
ClipboardReplyCandidateSnapshot(
kind: .ordinary,
text: "第二条",
emotion: "warm"
)
]
XCTAssertNil(
store.begin(
sourceText: "普通消息",
candidates: duplicate,
styleID: nil
)
)
}
func testRecordsAreCappedAndExpiredLocally() {
let now = Date()
for offset in 0..<(ClipboardReplyFeedbackStore.maximumRecords + 5) {
_ = store.begin(
sourceText: "消息\(offset)",
candidates: makeCandidates(suffix: "\(offset)"),
styleID: nil,
now: now.addingTimeInterval(TimeInterval(offset))
)
}
XCTAssertEqual(
store.records(
now: now.addingTimeInterval(
TimeInterval(ClipboardReplyFeedbackStore.maximumRecords + 5)
)
).count,
ClipboardReplyFeedbackStore.maximumRecords
)
XCTAssertTrue(
store.records(
now: now.addingTimeInterval(
ClipboardReplyFeedbackStore.retentionInterval + 1_000
)
).isEmpty
)
}
func testDiscardDoesNotCreatePositiveSelectionEvidence() throws {
let recordID = try XCTUnwrap(
store.begin(
sourceText: "你怎么看?",
candidates: makeCandidates(),
styleID: nil
)
)
store.recordDiscard(recordID: recordID)
let record = try XCTUnwrap(store.records().first)
XCTAssertEqual(record.outcome, .discarded)
XCTAssertNil(record.selectedCandidateID)
XCTAssertNil(record.answerID)
XCTAssertEqual(store.learningExamples().first?.selection, .discarded)
}
func testAIHistoryRevisionBackfillsVerifiedFinalText() throws {
let candidates = makeCandidates()
let recordID = try XCTUnwrap(
store.begin(
sourceText: "这版可以直接发吗?",
candidates: candidates,
styleID: "user.personal"
)
)
let answerID = UUID()
store.recordSelection(
recordID: recordID,
candidateID: candidates[0].id,
answerID: answerID
)
let history = SpeechHistoryStore(
defaults: defaults,
replyFeedbackStore: store
)
_ = history.applyHistoryMutation(
HistoryMutation(
action: .append,
entryID: answerID,
text: candidates[0].text,
source: .ai
)
)
_ = history.applyHistoryMutation(
HistoryMutation(
action: .update,
entryID: answerID,
expectedRevision: 0,
text: "我再看一下这版,确认后回复你。"
)
)
let record = try XCTUnwrap(store.records().first)
XCTAssertEqual(record.finalText, "我再看一下这版,确认后回复你。")
XCTAssertEqual(record.finalRevision, 1)
}
func testSingleOrdinaryAcceptanceRemainsWeakLearningEvidence() throws {
let candidate = ClipboardReplyCandidateSnapshot(
kind: .ordinary,
text: "我先看一下,再回复你。",
emotion: "neutral"
)
let recordID = try XCTUnwrap(
store.begin(
sourceText: "这个方案可以吗?",
candidates: [candidate],
styleID: nil
)
)
store.recordSelection(
recordID: recordID,
candidateID: candidate.id,
answerID: candidate.id
)
let example = try XCTUnwrap(store.learningExamples().first)
XCTAssertEqual(example.selection, .ordinary)
XCTAssertEqual(example.ordinaryCandidate, candidate.text)
XCTAssertNil(example.formalCandidate)
XCTAssertNil(example.playfulCandidate)
}
private func makeCandidates(
suffix: String = ""
) -> [ClipboardReplyCandidateSnapshot] {
[
ClipboardReplyCandidateSnapshot(
kind: .ordinary,
text: "普通回复\(suffix)",
emotion: "neutral"
),
ClipboardReplyCandidateSnapshot(
kind: .formal,
text: "正式回复\(suffix)",
emotion: "professional"
),
ClipboardReplyCandidateSnapshot(
kind: .playful,
text: "趣味回复\(suffix) 🙂",
emotion: "playful"
)
]
}
}