@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( AIReplyVariantSet.generic.kinds.map(\.systemImage), ["bubble.left.fill", "briefcase.fill", "theatermasks.fill"] ) XCTAssertEqual( AIReplyVariantSet.generic.kinds.map(\.titleKey), [ "keyboard.ai.replyVariant.ordinary", "keyboard.ai.replyVariant.formal", "keyboard.ai.replyVariant.playful" ] ) } func testInvitationParserRequiresExactSceneRoles() throws { let valid = """ {"variants":[ {"kind":"invitationTentative","emotion":"neutral","text":"我确认一下再告诉你。"}, {"kind":"invitationAccept","emotion":"warm","text":"好呀,到时见。"}, {"kind":"invitationDecline","emotion":"grateful","text":"谢谢你,不过这次我先不去了。"} ]} """ let variants = try XCTUnwrap( AIReplyVariantParser.parse( valid, variantSet: .invitation ) ) XCTAssertEqual(variants.map(\.kind), AIReplyVariantSet.invitation.kinds) XCTAssertNil(AIReplyVariantParser.parse(valid, variantSet: .generic)) XCTAssertNil( AIReplyVariantParser.parseOrFallback( "好呀,到时见。", variantSet: .invitation ) ) } func testIntentScenesAlwaysGenerateChoicesWhenPreferenceIsOff() { for scene in [ AIClipboardReplyScene.invitation, .task, .blessing, .clarification ] { XCTAssertTrue( AIReplyVariantSet.shouldGenerate( multipleRepliesEnabled: false, scene: scene ) ) } XCTAssertFalse( AIReplyVariantSet.shouldGenerate( multipleRepliesEnabled: false, scene: .complaint ) ) XCTAssertFalse( AIReplyVariantSet.shouldGenerate( multipleRepliesEnabled: false, scene: nil ) ) } func testIntentChoicePresentationUsesFixedLocalMeaning() { XCTAssertEqual( AIReplyVariantSet.task.kinds.map(\.titleKey), [ "keyboard.ai.replyVariant.taskAcknowledge", "keyboard.ai.replyVariant.taskClarify", "keyboard.ai.replyVariant.taskNegotiate" ] ) XCTAssertEqual( AIReplyVariantSet.invitation.kinds.map(\.systemImage), ["checkmark.circle.fill", "hand.raised.fill", "clock.fill"] ) XCTAssertTrue(AIReplyVariantSet.generic.kinds.allSatisfy(\.usesEmotionIcon)) XCTAssertTrue( AIReplyVariantSet.invitation.kinds.allSatisfy { !$0.usesEmotionIcon } ) } 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" ) } }