// ClipboardSkillSemanticRankerTests.swift // OSGKeyboardTests @testable import OSGKeyboardShared import XCTest final class ClipboardSkillSemanticRankerTests: XCTestCase { func testForeignQuestionPromotesSystemTranslationAndSourceLanguageReply() { let ranked = rank( text: "Could you send me the final proposal by Friday?", analysis: analysis(language: "en", question: detected()) ) XCTAssertEqual( Array(ranked.prefix(3)), [ AIClipboardSkillCatalog.translateID, AIClipboardSkillCatalog.replyID, AIClipboardSkillCatalog.clarifyRequestID ] ) } func testTranslationIsNotRecommendedForSystemLanguage() { let recommendations = ClipboardSkillSemanticRanker.recommended( skills: AIClipboardSkillCatalog.catalog, sourceText: "Could you send me the final proposal?", analysis: analysis(language: "en", question: detected()), uiLanguage: .chinese, limit: 5, preferredLanguages: ["en-US"] ).map(\.id) XCTAssertFalse(recommendations.contains(AIClipboardSkillCatalog.translateID)) XCTAssertTrue(recommendations.contains(AIClipboardSkillCatalog.replyID)) } func testChineseScriptMismatchPromotesTranslation() { let recommendations = ClipboardSkillSemanticRanker.recommended( skills: AIClipboardSkillCatalog.catalog, sourceText: "這是一段繁體中文。", analysis: analysis(language: "zh-Hant"), uiLanguage: .chinese, limit: 5, preferredLanguages: ["zh-Hans"] ).map(\.id) XCTAssertEqual(recommendations, [AIClipboardSkillCatalog.translateID]) } func testInvitationWithDatePromotesCalendarAndBothReplyChoices() { let ranked = rank( text: "今晚七点老地方吃饭,你能来吗?", analysis: analysis( hasDate: true, question: detected(), invitation: detected() ) ) XCTAssertEqual(ranked.first, AIClipboardSkillCatalog.extractEventsID) XCTAssertLessThan( tryIndex(AIClipboardSkillCatalog.acceptInvitationID, in: ranked), tryIndex(AIClipboardSkillCatalog.summarizeID, in: ranked) ) XCTAssertLessThan( tryIndex(AIClipboardSkillCatalog.declineInvitationID, in: ranked), tryIndex(AIClipboardSkillCatalog.summarizeID, in: ranked) ) } func testAddressPromotesNavigation() { let ranked = rank( text: "北京市朝阳区望京街 10 号,到了给我电话。", analysis: analysis(hasAddress: true) ) XCTAssertEqual(ranked.first, AIClipboardSkillCatalog.navigateID) } func testSingleHTTPSLinkOffersOpenAndWebpageSummary() { let recommendations = ClipboardSkillSemanticRanker.recommended( skills: AIClipboardSkillCatalog.catalog, sourceText: "https://example.com/article", analysis: analysis(urls: [URL(string: "https://example.com/article")!]), uiLanguage: .chinese, limit: 5 ).map(\.id) XCTAssertEqual( recommendations, [ AIClipboardSkillCatalog.openLinkID, AIClipboardSkillCatalog.summarizeWebPageID ] ) } func testRealStandaloneURLAnalysisOnlyOffersLinkSkills() async { let text = "https://www.apple.com/newsroom/" let detected = await ClipboardSemanticAnalyzer().analyze(text) let recommendations = ClipboardSkillSemanticRanker.recommended( skills: AIClipboardSkillCatalog.catalog, sourceText: text, analysis: detected, uiLanguage: .chinese, limit: 5 ).map(\.id) XCTAssertEqual( recommendations, [ AIClipboardSkillCatalog.openLinkID, AIClipboardSkillCatalog.summarizeWebPageID ] ) } func testMultipleLinksDoNotChooseAnAmbiguousTarget() { let recommendations = ClipboardSkillSemanticRanker.recommended( skills: AIClipboardSkillCatalog.catalog, sourceText: "https://example.com/a https://example.com/b", analysis: analysis(urls: [ URL(string: "https://example.com/a")!, URL(string: "https://example.com/b")! ]), uiLanguage: .chinese, limit: 5 ).map(\.id) XCTAssertFalse(recommendations.contains(AIClipboardSkillCatalog.openLinkID)) XCTAssertFalse(recommendations.contains(AIClipboardSkillCatalog.summarizeWebPageID)) } func testSinglePhoneNumberOffersCallAndCreateContact() { let recommendations = ClipboardSkillSemanticRanker.recommended( skills: AIClipboardSkillCatalog.catalog, sourceText: "请拨打 +1 408-996-1010", analysis: analysis( phoneNumbers: [ ClipboardTextLabel(sourceText: "+1 408-996-1010") ] ), uiLanguage: .chinese, limit: 5 ).map(\.id) XCTAssertEqual( recommendations, [ AIClipboardSkillCatalog.callPhoneID, AIClipboardSkillCatalog.createContactID ] ) } func testRealPhoneAnalysisOnlyOffersPhoneActions() async { let text = "联系电话:+1 408-996-1010" let detected = await ClipboardSemanticAnalyzer().analyze(text) let recommendations = ClipboardSkillSemanticRanker.recommended( skills: AIClipboardSkillCatalog.catalog, sourceText: text, analysis: detected, uiLanguage: .chinese, limit: 5 ).map(\.id) XCTAssertEqual( recommendations, [ AIClipboardSkillCatalog.callPhoneID, AIClipboardSkillCatalog.createContactID ] ) } func testMultiplePhoneNumbersDoNotChooseAnAmbiguousTarget() { let recommendations = ClipboardSkillSemanticRanker.recommended( skills: AIClipboardSkillCatalog.catalog, sourceText: "+1 408-996-1010 / 400-666-8800", analysis: analysis( phoneNumbers: [ ClipboardTextLabel(sourceText: "+1 408-996-1010"), ClipboardTextLabel(sourceText: "400-666-8800") ] ), uiLanguage: .chinese, limit: 5 ).map(\.id) XCTAssertFalse(recommendations.contains(AIClipboardSkillCatalog.callPhoneID)) XCTAssertFalse(recommendations.contains(AIClipboardSkillCatalog.createContactID)) } func testTaskListPromotesTodoAndOrganizationSkills() { let ranked = rank( text: "- 更新报价单\n- 给客户回邮件\n- 周五前提交合同", analysis: analysis(task: detected()) ) XCTAssertEqual(ranked.first, AIClipboardSkillCatalog.extractTodosID) XCTAssertLessThan( tryIndex(AIClipboardSkillCatalog.organizeListID, in: ranked), tryIndex(AIClipboardSkillCatalog.replyID, in: ranked) ) XCTAssertLessThan( tryIndex(AIClipboardSkillCatalog.acceptTaskID, in: ranked), tryIndex(AIClipboardSkillCatalog.replyID, in: ranked) ) } func testAdvisoryComplaintPromotesEmpathyWithoutAutomaticApproval() { let complaint = ClipboardIntentLabel( confidence: 0.82, threshold: 0.6, isDetected: false, isApprovedForAutomaticRouting: false ) let ranked = rank( text: "这个问题已经发生三次了,请尽快处理。", analysis: analysis( sentiment: .negative, complaint: complaint ) ) XCTAssertEqual(ranked.first, AIClipboardSkillCatalog.empathyReplyID) XCTAssertEqual(ranked.dropFirst().first, AIClipboardSkillCatalog.clarifyRequestID) } func testLongTextPromotesIntegratedSummaryAndNotes() { let ranked = rank( text: String(repeating: "这是需要阅读和整理的长文内容。", count: 40), analysis: analysis() ) XCTAssertEqual( Array(ranked.prefix(2)), [ AIClipboardSkillCatalog.summarizeID, AIClipboardSkillCatalog.saveToNotesID ] ) } func testNoSignalPreservesSavedOrder() { let baseline = [ AIClipboardSkillCatalog.businessReplyID, AIClipboardSkillCatalog.translateID, AIClipboardSkillCatalog.replyID ] let ranked = ClipboardSkillSemanticRanker.ranked( skills: skills(ids: baseline), sourceText: "好的", analysis: analysis(), uiLanguage: .chinese ).map(\.id) XCTAssertEqual(ranked, baseline) } func testRecommendationsSelectOnlySemanticallyRelevantSkills() { let recommendations = ClipboardSkillSemanticRanker.recommended( skills: AIClipboardSkillCatalog.catalog, sourceText: "北京市朝阳区望京街 10 号,到了给我电话。", analysis: analysis(hasAddress: true), uiLanguage: .chinese, limit: 5 ).map(\.id) XCTAssertEqual(recommendations, [AIClipboardSkillCatalog.navigateID]) XCTAssertFalse(recommendations.contains(AIClipboardSkillCatalog.summarizeID)) XCTAssertFalse(recommendations.contains(AIClipboardSkillCatalog.translateID)) } func testRecommendationsStayEmptyWhenNoSemanticLabelMatches() { let recommendations = ClipboardSkillSemanticRanker.recommended( skills: AIClipboardSkillCatalog.catalog, sourceText: "知道了", analysis: analysis(), uiLanguage: .chinese, limit: 5 ).map(\.id) XCTAssertTrue(recommendations.isEmpty) } func testNegativeReplyableMessageDoesNotOfferPlayfulReply() { let recommendations = ClipboardSkillSemanticRanker.recommended( skills: AIClipboardSkillCatalog.catalog, sourceText: "我刚到家,今天真是累坏了。", analysis: analysis( sentiment: .negative, replyableMessage: detected() ), uiLanguage: .chinese, limit: 5 ).map(\.id) XCTAssertEqual(recommendations, [AIClipboardSkillCatalog.replyID]) } func testInvitationKeepsTwoSpecificRepliesWithoutGenericReplies() { let recommendations = ClipboardSkillSemanticRanker.recommended( skills: AIClipboardSkillCatalog.catalog, sourceText: "今晚七点老地方吃饭,你能来吗?", analysis: analysis( hasDate: true, question: detected(), invitation: detected(), replyableMessage: detected() ), uiLanguage: .chinese, limit: 5 ).map(\.id) XCTAssertEqual( recommendations, [ AIClipboardSkillCatalog.extractEventsID, AIClipboardSkillCatalog.acceptInvitationID, AIClipboardSkillCatalog.declineInvitationID ] ) } func testRecommendationsNeverContainMoreThanTwoReplySkills() { let recommendations = ClipboardSkillSemanticRanker.recommended( skills: AIClipboardSkillCatalog.catalog, sourceText: "Could you send the final proposal by Friday?", analysis: analysis( language: "en", question: detected(), replyableMessage: detected() ), uiLanguage: .chinese, limit: 5, preferredLanguages: ["zh-Hans"] ) let replyCount = recommendations.filter(\.supportsReplyStyle).count XCTAssertLessThanOrEqual(replyCount, 2) XCTAssertEqual( recommendations.map(\.id), [ AIClipboardSkillCatalog.translateID, AIClipboardSkillCatalog.replyID, AIClipboardSkillCatalog.clarifyRequestID ] ) } private func rank( text: String, analysis: ClipboardSemanticAnalysis ) -> [String] { ClipboardSkillSemanticRanker.ranked( skills: AIClipboardSkillCatalog.catalog, sourceText: text, analysis: analysis, uiLanguage: .chinese, preferredLanguages: ["zh-Hans"] ).map(\.id) } private func skills(ids: [String]) -> [AIClipboardSkill] { ids.compactMap { AIClipboardSkillCatalog.skill(id: $0) } } private func tryIndex(_ id: String, in ids: [String]) -> Int { ids.firstIndex(of: id) ?? Int.max } private func detected() -> ClipboardIntentLabel { ClipboardIntentLabel( confidence: 0.95, threshold: 0.6, isDetected: true, isApprovedForAutomaticRouting: true ) } private func absent() -> ClipboardIntentLabel { ClipboardIntentLabel( confidence: 0, threshold: 1, isDetected: false, isApprovedForAutomaticRouting: false ) } private func analysis( language: String? = nil, hasDate: Bool = false, hasAddress: Bool = false, urls: [URL] = [], phoneNumbers: [ClipboardTextLabel] = [], sentiment: ClipboardSentimentLabel = .unknown, task: ClipboardIntentLabel? = nil, question: ClipboardIntentLabel? = nil, invitation: ClipboardIntentLabel? = nil, complaint: ClipboardIntentLabel? = nil, replyableMessage: ClipboardIntentLabel? = nil ) -> ClipboardSemanticAnalysis { ClipboardSemanticAnalysis( language: language.map { ClipboardLanguageLabel(identifier: $0, confidence: 0.99) }, dates: hasDate ? [ClipboardDateLabel( sourceText: "今晚七点", date: Date(), duration: 0, timeZoneIdentifier: nil )] : [], addresses: hasAddress ? [ClipboardTextLabel(sourceText: "望京街 10 号")] : [], phoneNumbers: phoneNumbers, urls: urls, personNames: [], organizationNames: [], sentiment: sentiment, sentimentConfidence: sentiment == .unknown ? 0 : 0.9, task: task ?? absent(), question: question ?? absent(), invitation: invitation ?? absent(), complaint: complaint ?? absent(), replyableMessage: replyableMessage ?? absent() ) } }