2cc81c4628
Add local clipboard intent recommendations, webpage and phone actions, and safer host handoffs. Refine managed gateway, catalog refresh, onboarding, and adaptive polish behavior.
434 lines
15 KiB
Swift
434 lines
15 KiB
Swift
// 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()
|
|
)
|
|
}
|
|
}
|