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

147 lines
5.4 KiB
Swift

@testable import OSGKeyboardShared
import XCTest
final class AIReplyVariantTests: XCTestCase {
func testStrictParserReturnsThreeKindsInStableUIOrder() throws {
let raw = """
{"variants":[
{"kind":"playful","emotion":"celebratory","text":"好呀,走起 🎉"},
{"kind":"ordinary","emotion":"warm","text":"好呀,到时见。"},
{"kind":"formal","emotion":"neutral","text":"好的,届时见。"}
]}
"""
let variants = try XCTUnwrap(AIReplyVariantParser.parse(raw))
XCTAssertEqual(variants.map(\.kind), [.ordinary, .formal, .playful])
XCTAssertEqual(variants.map(\.emotion), [.warm, .neutral, .celebratory])
XCTAssertEqual(variants.map(\.text), ["好呀,到时见。", "好的,届时见。", "好呀,走起 🎉"])
}
func testUnknownEmotionSafelyDowngradesToNeutral() throws {
let raw = """
{"variants":[
{"kind":"ordinary","emotion":"unsafe-symbol-name","text":"A"},
{"kind":"formal","emotion":"neutral","text":"B"},
{"kind":"playful","emotion":"playful","text":"C"}
]}
"""
let variants = try XCTUnwrap(AIReplyVariantParser.parse(raw))
XCTAssertEqual(variants[0].emotion, .neutral)
}
func testStrictParserRejectsUnknownFieldsAndMissingKinds() {
let extraField = """
{"variants":[
{"kind":"ordinary","emotion":"neutral","text":"A","icon":"star"},
{"kind":"formal","emotion":"neutral","text":"B"},
{"kind":"playful","emotion":"playful","text":"C"}
]}
"""
let duplicatedKind = """
{"variants":[
{"kind":"ordinary","emotion":"neutral","text":"A"},
{"kind":"ordinary","emotion":"warm","text":"B"},
{"kind":"playful","emotion":"playful","text":"C"}
]}
"""
XCTAssertNil(AIReplyVariantParser.parse(extraField))
XCTAssertNil(AIReplyVariantParser.parse(duplicatedKind))
}
func testFencedJSONFallsBackToOneCleanOrdinaryReply() throws {
let raw = """
```json
{"variants":[
{"kind":"ordinary","emotion":"warm","text":"先确认一下具体时间,可以吗?"},
{"kind":"formal","emotion":"neutral","text":"请先确认具体时间。"}
]}
```
"""
let result = try XCTUnwrap(AIReplyVariantParser.parseOrFallback(raw))
guard case .single(let fallback) = result else {
return XCTFail("Expected a safe single fallback")
}
XCTAssertEqual(fallback.kind, .ordinary)
XCTAssertEqual(fallback.emotion, .neutral)
XCTAssertEqual(fallback.text, "先确认一下具体时间,可以吗?")
XCTAssertFalse(fallback.text.contains("variants"))
XCTAssertFalse(fallback.text.contains("```"))
}
func testMalformedJSONExtractsReplyInsteadOfReturningStructure() throws {
let raw = #"{"kind":"ordinary","text":"可以,周五见。","emotion":"warm",}"#
XCTAssertEqual(
AIReplyVariantParser.fallbackText(from: raw),
"可以,周五见。"
)
}
func testParserRejectsSourceRestatementAndKeepsConversationalReplies() throws {
let source = "630语音因为技术改造,能力回退😪"
let restatement = """
{"variants":[
{"kind":"ordinary","emotion":"neutral","text":"630语音因技术改造,能力回退了,有点无奈。"},
{"kind":"formal","emotion":"neutral","text":"630语音因技术改造,能力有所回退,特此说明。"},
{"kind":"playful","emotion":"playful","text":"630语音被技术改造坑了一把,能力回退了 😅"}
]}
"""
let replies = """
{"variants":[
{"kind":"ordinary","emotion":"empathetic","text":"那确实有点可惜,希望后面尽快恢复。"},
{"kind":"formal","emotion":"calm","text":"了解,希望后续改造完成后能恢复原有能力。"},
{"kind":"playful","emotion":"playful","text":"这是先退两步,准备以后起飞吗 😅"}
]}
"""
XCTAssertNil(
AIReplyVariantParser.parseOrFallback(
restatement,
sourceText: source
)
)
XCTAssertEqual(
try XCTUnwrap(
AIReplyVariantParser.parse(replies, sourceText: source)
).count,
3
)
}
func testReplyKindOwnsLocalPresentationMetadata() {
XCTAssertEqual(
AIReplyVariant.Kind.allCases.map(\.systemImage),
["bubble.left.fill", "briefcase.fill", "theatermasks.fill"]
)
XCTAssertEqual(
AIReplyVariant.Kind.allCases.map(\.titleKey),
[
"keyboard.ai.replyVariant.ordinary",
"keyboard.ai.replyVariant.formal",
"keyboard.ai.replyVariant.playful"
]
)
}
func testEmotionMapsOnlyToLocalSFSymbolAllowlist() {
XCTAssertEqual(
AIReplyVariant.Emotion.celebratory.systemImage(fallback: .ordinary),
"party.popper.fill"
)
XCTAssertEqual(
AIReplyVariant.Emotion.empathetic.systemImage(fallback: .playful),
"heart.text.square.fill"
)
XCTAssertEqual(
AIReplyVariant.Emotion.neutral.systemImage(fallback: .formal),
"briefcase.fill"
)
}
}