feat(keyboard): add clipboard AI skills, hint keywords, and voice session fixes
Idle chips show entities with category icons; a fresh copy surfaces Reply/Summarize/Translate; abort/cancel/empty-tap no longer leave the mic stuck.
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
// AIHintKeywordExtractorTests.swift
|
||||
// OSGKeyboardTests
|
||||
|
||||
import XCTest
|
||||
@testable import OSGKeyboardShared
|
||||
|
||||
final class AIHintKeywordExtractorTests: XCTestCase {
|
||||
func testTrendingPrefersMetadataTitleOverHotPrefix() throws {
|
||||
let json = """
|
||||
{"id":"hot-1","text":"全网热点:朱镕基同志逝世","prompt":"请概括","category":"society","source":"tophub-open-hot","locale":"zh","metadata":{"title":"朱镕基同志逝世"}}
|
||||
"""
|
||||
let card = try JSONDecoder().decode(AIHintCard.self, from: Data(json.utf8))
|
||||
XCTAssertEqual(card.visualKind, .trending)
|
||||
XCTAssertEqual(card.visualKind.systemImage, "flame.fill")
|
||||
XCTAssertEqual(card.resolvedDisplayText, "朱镕基同志逝世")
|
||||
}
|
||||
|
||||
func testWeatherUsesCityAndTemperature() {
|
||||
let card = AIHintCard(
|
||||
id: "weather-zh-上海",
|
||||
displayText: "上海天气速览",
|
||||
prompt: "请说明天气",
|
||||
category: "weather",
|
||||
source: "open-meteo",
|
||||
locale: "zh",
|
||||
metadata: AIHintMetadata(city: "上海", tempC: 25.6)
|
||||
)
|
||||
XCTAssertEqual(card.visualKind, .weather)
|
||||
XCTAssertEqual(card.resolvedDisplayText, "上海 26°")
|
||||
}
|
||||
|
||||
func testHolidayStripsPrefixAndUsesChineseName() {
|
||||
let card = AIHintCard(
|
||||
id: "holiday-next-cn",
|
||||
displayText: "临近节日:中秋节",
|
||||
prompt: "介绍中秋",
|
||||
category: "holiday",
|
||||
source: "nager-holidays",
|
||||
locale: "zh",
|
||||
conditions: ["date"],
|
||||
metadata: AIHintMetadata(name: "Mid-Autumn Festival")
|
||||
)
|
||||
XCTAssertEqual(card.visualKind, .calendar)
|
||||
XCTAssertEqual(card.resolvedDisplayText, "中秋节")
|
||||
}
|
||||
|
||||
func testEnglishHolidayUsesMetadataName() {
|
||||
let card = AIHintCard(
|
||||
id: "holiday-next-us",
|
||||
displayText: "Upcoming: Labour Day",
|
||||
prompt: "Explain Labour Day",
|
||||
category: "holiday",
|
||||
source: "nager-holidays",
|
||||
locale: "en",
|
||||
metadata: AIHintMetadata(name: "Labour Day")
|
||||
)
|
||||
XCTAssertEqual(card.resolvedDisplayText, "Labour Day")
|
||||
}
|
||||
|
||||
func testLongOrgTitleFallsBackToLastChunk() {
|
||||
let text = "中共中央 全国人大常委会 国务院 全国政协讣告 朱镕基同志逝世"
|
||||
XCTAssertEqual(
|
||||
AIHintKeywordExtractor.finalize(text, locale: "zh"),
|
||||
"朱镕基同志逝世"
|
||||
)
|
||||
}
|
||||
|
||||
func testMixedTitlePrefersLeadingLatin() {
|
||||
XCTAssertEqual(
|
||||
AIHintKeywordExtractor.finalize(
|
||||
"DeepSeek Pro 正式版已经发布,如何评价该模型?",
|
||||
locale: "zh"
|
||||
),
|
||||
"DeepSeek"
|
||||
)
|
||||
}
|
||||
|
||||
func testDailyBriefIsNewsAndSoulQuoteIsSearch() {
|
||||
let brief = AIHintCard(
|
||||
id: "tophub-daily-brief-1",
|
||||
displayText: "看看今日早报",
|
||||
prompt: "写早报",
|
||||
category: "daily",
|
||||
source: "tophub-daily",
|
||||
locale: "zh"
|
||||
)
|
||||
XCTAssertEqual(brief.visualKind, .news)
|
||||
XCTAssertEqual(brief.resolvedDisplayText, "今日早报")
|
||||
|
||||
let soul = AIHintCard(
|
||||
id: "tophub-daily-soul-1",
|
||||
displayText: "今日一句:展开聊聊",
|
||||
prompt: "解释这句话",
|
||||
category: "daily",
|
||||
source: "tophub-daily",
|
||||
locale: "zh",
|
||||
metadata: AIHintMetadata(soul: "为了防止我这个月又乱花钱")
|
||||
)
|
||||
XCTAssertEqual(soul.visualKind, .search)
|
||||
XCTAssertEqual(soul.resolvedDisplayText, "今日金句")
|
||||
}
|
||||
|
||||
func testCapabilityMapsToSearch() {
|
||||
let card = AIHintLocalCatalog.cards(locale: "zh")
|
||||
.first { $0.id == "local-zh-encyclopedia" }!
|
||||
XCTAssertEqual(card.visualKind, .search)
|
||||
XCTAssertEqual(card.resolvedDisplayText, "有趣概念")
|
||||
}
|
||||
|
||||
func testStocksMapsToChartIcon() {
|
||||
let card = AIHintLocalCatalog.cards(locale: "zh")
|
||||
.first { $0.id == "local-zh-stocks" }!
|
||||
XCTAssertEqual(card.visualKind, .stocks)
|
||||
XCTAssertEqual(card.resolvedDisplayText, "今日大盘")
|
||||
}
|
||||
}
|
||||
|
||||
final class AIClipboardSkillTests: XCTestCase {
|
||||
func testVisibleDefaultsAreReplySummarizeTranslate() {
|
||||
XCTAssertEqual(
|
||||
AIClipboardSkillCatalog.visible().map(\.id),
|
||||
["reply", "summarize", "translate"]
|
||||
)
|
||||
}
|
||||
|
||||
func testVisibleRespectsEnabledIDsForFutureSettings() {
|
||||
XCTAssertEqual(
|
||||
AIClipboardSkillCatalog.visible(enabledIDs: ["translate", "reply"]).map(\.id),
|
||||
["translate", "reply"]
|
||||
)
|
||||
XCTAssertEqual(
|
||||
AIClipboardSkillCatalog.visible(enabledIDs: ["unknown"]).map(\.id),
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
func testTranslateFollowsUserTargetLanguage() {
|
||||
let ja = AIClipboardSkillCatalog.instruction(
|
||||
skillID: AIClipboardSkillCatalog.translateID,
|
||||
locale: "zh",
|
||||
translationTargetLocaleId: "ja"
|
||||
)
|
||||
XCTAssertTrue(ja.contains("Japanese"))
|
||||
XCTAssertFalse(ja.contains("互译"))
|
||||
}
|
||||
|
||||
func testTranslateFallsBackToChineseEnglishWhenUnset() {
|
||||
let zh = AIClipboardSkillCatalog.instruction(
|
||||
skillID: AIClipboardSkillCatalog.translateID,
|
||||
locale: "zh",
|
||||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId
|
||||
)
|
||||
XCTAssertTrue(zh.contains("中文与英文"))
|
||||
let en = AIClipboardSkillCatalog.instruction(
|
||||
skillID: AIClipboardSkillCatalog.translateID,
|
||||
locale: "en",
|
||||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId
|
||||
)
|
||||
XCTAssertTrue(en.lowercased().contains("chinese"))
|
||||
XCTAssertTrue(en.lowercased().contains("english"))
|
||||
}
|
||||
|
||||
func testTranslateButtonTitleUsesDirectionPairsAndTargetShortNames() {
|
||||
XCTAssertEqual(
|
||||
AIClipboardSkillCatalog.translateButtonTitle(
|
||||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId,
|
||||
uiLanguage: .chinese
|
||||
),
|
||||
"中↔英"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
AIClipboardSkillCatalog.translateButtonTitle(
|
||||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId,
|
||||
uiLanguage: .english
|
||||
),
|
||||
"CN↔EN"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
AIClipboardSkillCatalog.translateButtonTitle(
|
||||
translationTargetLocaleId: "en",
|
||||
uiLanguage: .chinese
|
||||
),
|
||||
"中译英"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
AIClipboardSkillCatalog.translateButtonTitle(
|
||||
translationTargetLocaleId: "de",
|
||||
uiLanguage: .chinese
|
||||
),
|
||||
"中译德"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
AIClipboardSkillCatalog.translateButtonTitle(
|
||||
translationTargetLocaleId: "ja",
|
||||
uiLanguage: .english
|
||||
),
|
||||
"To JP"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
AIClipboardSkillCatalog.translateButtonTitle(
|
||||
translationTargetLocaleId: "zh-Hans",
|
||||
uiLanguage: .english
|
||||
),
|
||||
"To CN"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
AIClipboardSkillCatalog.translateButtonTitle(
|
||||
translationTargetLocaleId: "zh-Hant",
|
||||
uiLanguage: .english
|
||||
),
|
||||
"To TW"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
AIClipboardSkillCatalog.translateButtonTitle(
|
||||
translationTargetLocaleId: "zh-Hans",
|
||||
uiLanguage: .chinese
|
||||
),
|
||||
"简↔繁"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
AIClipboardSkillCatalog.translateButtonTitle(
|
||||
translationTargetLocaleId: "zh-Hant",
|
||||
uiLanguage: .chinese
|
||||
),
|
||||
"简↔繁"
|
||||
)
|
||||
}
|
||||
|
||||
func testSummarizeAsksForOverviewNotShortening() {
|
||||
let prompt = AIClipboardSkillCatalog.instruction(
|
||||
skillID: AIClipboardSkillCatalog.summarizeID,
|
||||
locale: "zh",
|
||||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId
|
||||
)
|
||||
XCTAssertTrue(prompt.contains("概括"))
|
||||
XCTAssertTrue(prompt.contains("不要改写成可发送的短消息"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user