feat(keyboard): add custom skills plus events and navigate Shortcuts
Ship user-defined Shortcut skills, companion Events/Navigate recipes, shared skill/style card chrome, and bump the build to 69.
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
// AIAddressExtractionTests.swift
|
||||
// OSGKeyboardTests
|
||||
|
||||
import XCTest
|
||||
@testable import OSGKeyboardShared
|
||||
|
||||
final class AIAddressExtractionTests: XCTestCase {
|
||||
func testNONEAndEmptyProduceNoItems() {
|
||||
XCTAssertEqual(AIAddressExtraction.lines(from: "NONE"), [])
|
||||
XCTAssertEqual(AIAddressExtraction.lines(from: "没有地址"), [])
|
||||
XCTAssertEqual(AIAddressExtraction.lines(from: "no address"), [])
|
||||
XCTAssertEqual(AIAddressExtraction.lines(from: " \n "), [])
|
||||
}
|
||||
|
||||
func testDestinationOnlyKeepsLeadingPipe() {
|
||||
XCTAssertEqual(
|
||||
AIAddressExtraction.lines(from: "|三里屯太古里"),
|
||||
["|三里屯太古里"]
|
||||
)
|
||||
XCTAssertEqual(
|
||||
AIAddressExtraction.lines(from: "朝阳区酒仙桥路10号"),
|
||||
["|朝阳区酒仙桥路10号"]
|
||||
)
|
||||
XCTAssertEqual(
|
||||
AIAddressExtraction.lines(from: "|三里屯|"),
|
||||
["|三里屯"]
|
||||
)
|
||||
}
|
||||
|
||||
func testOriginAndDestination() {
|
||||
XCTAssertEqual(
|
||||
AIAddressExtraction.lines(from: "北京南站|三里屯太古里"),
|
||||
["北京南站|三里屯太古里"]
|
||||
)
|
||||
}
|
||||
|
||||
func testFirstValidLineOnly() {
|
||||
let raw = """
|
||||
NONE
|
||||
北京南站|三里屯
|
||||
国贸|望京
|
||||
"""
|
||||
XCTAssertEqual(AIAddressExtraction.lines(from: raw), ["北京南站|三里屯"])
|
||||
}
|
||||
|
||||
func testSameOriginAndDestinationDropsOrigin() {
|
||||
XCTAssertEqual(
|
||||
AIAddressExtraction.lines(from: "故宫|故宫"),
|
||||
["|故宫"]
|
||||
)
|
||||
}
|
||||
|
||||
func testRejectsURLDestinations() {
|
||||
XCTAssertEqual(
|
||||
AIAddressExtraction.lines(from: "|https://maps.apple.com/?daddr=x"),
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
func testStripsBullet() {
|
||||
XCTAssertEqual(
|
||||
AIAddressExtraction.lines(from: "- |三里屯"),
|
||||
["|三里屯"]
|
||||
)
|
||||
}
|
||||
|
||||
func testWholeClipboardEchoIsRejected() {
|
||||
let source = String(repeating: "这是一段很长的会议纪要内容,包含许多句子。", count: 4)
|
||||
XCTAssertEqual(
|
||||
AIAddressExtraction.lines(from: source, sourceClipboard: source),
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
func testPromptIncludesPipeContractAndNONE() {
|
||||
let prompt = AIClipboardSkillCatalog.instruction(
|
||||
skillID: AIClipboardSkillCatalog.navigateID,
|
||||
locale: "zh",
|
||||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId
|
||||
)
|
||||
XCTAssertTrue(prompt.contains("起点|终点"))
|
||||
XCTAssertTrue(prompt.contains("NONE"))
|
||||
XCTAssertTrue(prompt.contains("不要把整段原文当成一个地点"))
|
||||
}
|
||||
|
||||
func testEnglishPromptIncludesNONE() {
|
||||
let prompt = AIClipboardSkillCatalog.instruction(
|
||||
skillID: AIClipboardSkillCatalog.navigateID,
|
||||
locale: "en",
|
||||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId
|
||||
)
|
||||
XCTAssertTrue(prompt.contains("origin|destination"))
|
||||
XCTAssertTrue(prompt.contains("NONE"))
|
||||
}
|
||||
}
|
||||
|
||||
final class AIMapNavigationTests: XCTestCase {
|
||||
private let destinationOnly = AIMapRoute(origin: nil, destination: "三里屯太古里")
|
||||
private let twoPoint = AIMapRoute(origin: "北京南站", destination: "三里屯太古里")
|
||||
|
||||
func testPrefersAmapWhenIosamapOpens() {
|
||||
let url = AIMapNavigation.url(for: destinationOnly) { $0.scheme == "iosamap" }
|
||||
XCTAssertEqual(url.scheme, "iosamap")
|
||||
XCTAssertEqual(url.host, "path")
|
||||
let items = query(url)
|
||||
XCTAssertEqual(items["sourceApplication"], "OSGKeyboard")
|
||||
XCTAssertEqual(items["dname"], "三里屯太古里")
|
||||
XCTAssertEqual(items["sname"], "我的位置")
|
||||
XCTAssertEqual(items["t"], "0")
|
||||
}
|
||||
|
||||
func testAmapUsesAmapuriWhenOnlyAmapuriOpens() {
|
||||
let url = AIMapNavigation.url(for: twoPoint) { $0.scheme == "amapuri" }
|
||||
XCTAssertEqual(url.scheme, "amapuri")
|
||||
XCTAssertEqual(url.host, "route")
|
||||
XCTAssertTrue(url.absoluteString.contains("://route/plan/?"))
|
||||
let items = query(url)
|
||||
XCTAssertEqual(items["sname"], "北京南站")
|
||||
XCTAssertEqual(items["dname"], "三里屯太古里")
|
||||
}
|
||||
|
||||
func testFallsBackToBaiduWhenAmapMissing() {
|
||||
let url = AIMapNavigation.url(for: twoPoint) { $0.scheme == "baidumap" }
|
||||
XCTAssertEqual(url.scheme, "baidumap")
|
||||
XCTAssertEqual(url.host, "map")
|
||||
XCTAssertEqual(url.path, "/direction")
|
||||
let items = query(url)
|
||||
XCTAssertEqual(items["origin"], "name:北京南站")
|
||||
XCTAssertEqual(items["destination"], "name:三里屯太古里")
|
||||
XCTAssertEqual(items["mode"], "driving")
|
||||
}
|
||||
|
||||
func testBaiduOmitsOriginWhenStartingFromHere() {
|
||||
let url = AIMapNavigation.url(for: destinationOnly) { $0.scheme == "baidumap" }
|
||||
let items = query(url)
|
||||
XCTAssertNil(items["origin"])
|
||||
XCTAssertEqual(items["destination"], "name:三里屯太古里")
|
||||
}
|
||||
|
||||
func testFallsBackToAppleMaps() {
|
||||
let url = AIMapNavigation.url(for: twoPoint) { _ in false }
|
||||
XCTAssertEqual(url.scheme, "maps")
|
||||
XCTAssertTrue(url.absoluteString.hasPrefix("maps://"))
|
||||
let items = query(url)
|
||||
XCTAssertEqual(items["saddr"], "北京南站")
|
||||
XCTAssertEqual(items["daddr"], "三里屯太古里")
|
||||
XCTAssertEqual(items["dirflg"], "d")
|
||||
}
|
||||
|
||||
func testAppleOmitsSaddrWhenStartingFromHere() {
|
||||
let url = AIMapNavigation.url(for: destinationOnly) { _ in false }
|
||||
let items = query(url)
|
||||
XCTAssertNil(items["saddr"])
|
||||
XCTAssertEqual(items["daddr"], "三里屯太古里")
|
||||
}
|
||||
|
||||
func testShortcutInputEncodesFirstRoute() {
|
||||
let text = AIMapNavigation.shortcutInput(from: "北京南站|三里屯") { _ in false }
|
||||
XCTAssertEqual(text?.hasPrefix("maps:"), true)
|
||||
XCTAssertTrue(text?.contains("daddr") == true)
|
||||
}
|
||||
|
||||
func testShortcutInputRejectsNONE() {
|
||||
XCTAssertNil(AIMapNavigation.shortcutInput(from: "NONE") { _ in false })
|
||||
}
|
||||
|
||||
func testProviderOrder() {
|
||||
XCTAssertEqual(AIMapNavigation.provider { $0.scheme == "iosamap" }, .amap)
|
||||
XCTAssertEqual(AIMapNavigation.provider { $0.scheme == "baidumap" }, .baidu)
|
||||
XCTAssertEqual(AIMapNavigation.provider { _ in false }, .apple)
|
||||
}
|
||||
|
||||
private func query(_ url: URL) -> [String: String] {
|
||||
let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? []
|
||||
return Dictionary(uniqueKeysWithValues: items.compactMap { item in
|
||||
guard let value = item.value else { return nil }
|
||||
return (item.name, value)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,28 @@ final class AIAgentSkillLayoutTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testReorderMovesEnabledSkillToIndex() {
|
||||
let store = AIAgentSkillLayoutStore(defaults: makeDefaults())
|
||||
store.moveEnabled(id: AIClipboardSkillCatalog.summarizeID, toIndex: 2)
|
||||
XCTAssertEqual(
|
||||
store.layout.enabledIDs,
|
||||
[
|
||||
AIClipboardSkillCatalog.replyID,
|
||||
AIClipboardSkillCatalog.translateID,
|
||||
AIClipboardSkillCatalog.summarizeID,
|
||||
]
|
||||
)
|
||||
store.moveEnabled(id: AIClipboardSkillCatalog.summarizeID, toIndex: 0)
|
||||
XCTAssertEqual(
|
||||
store.layout.enabledIDs,
|
||||
[
|
||||
AIClipboardSkillCatalog.summarizeID,
|
||||
AIClipboardSkillCatalog.replyID,
|
||||
AIClipboardSkillCatalog.translateID,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
func testVisibleEmptyEnabledIDsShowsNoChips() {
|
||||
XCTAssertEqual(AIClipboardSkillCatalog.visible(enabledIDs: []).map(\.id), [])
|
||||
}
|
||||
@@ -183,17 +205,52 @@ final class AIAgentSkillLayoutTests: XCTestCase {
|
||||
)
|
||||
XCTAssertEqual(skill?.shortcutICloudURL?.host, "www.icloud.com")
|
||||
XCTAssertEqual(skill?.shortcutName, "OSG · 提取待办")
|
||||
XCTAssertEqual(skill?.shortcutResourceName, "OSGExtractTodos")
|
||||
}
|
||||
|
||||
func testExtractEventsUsesICloudShareLink() {
|
||||
let skill = AIClipboardSkillCatalog.skill(id: AIClipboardSkillCatalog.extractEventsID)
|
||||
XCTAssertEqual(
|
||||
skill?.shortcutICloudURL,
|
||||
AIClipboardSkillCatalog.extractEventsShortcutICloudURL
|
||||
)
|
||||
XCTAssertEqual(skill?.shortcutICloudURL?.host, "www.icloud.com")
|
||||
XCTAssertEqual(skill?.shortcutName, "OSG · 提取日程")
|
||||
XCTAssertEqual(
|
||||
skill?.shortcutResourceName,
|
||||
"OSGExtractEvents"
|
||||
)
|
||||
XCTAssertEqual(skill?.systemImage, "calendar")
|
||||
XCTAssertFalse(skill?.isDefault ?? true)
|
||||
XCTAssertEqual(
|
||||
AIAgentShortcutRun.iCloudShareToken(from: skill!.shortcutICloudURL!),
|
||||
"1f4afcf7ee22400cbf84e319d969aadf"
|
||||
)
|
||||
}
|
||||
|
||||
func testNavigateUsesBundledShortcut() {
|
||||
let skill = AIClipboardSkillCatalog.skill(id: AIClipboardSkillCatalog.navigateID)
|
||||
XCTAssertNil(skill?.shortcutICloudURL)
|
||||
XCTAssertEqual(skill?.shortcutName, "OSG · 导航")
|
||||
XCTAssertEqual(skill?.shortcutResourceName, "OSGNavigate")
|
||||
XCTAssertEqual(
|
||||
skill?.systemImage,
|
||||
"arrow.triangle.turn.up.right.diamond.fill"
|
||||
)
|
||||
XCTAssertFalse(skill?.isDefault ?? true)
|
||||
XCTAssertEqual(skill?.kind, .export)
|
||||
XCTAssertTrue(skill?.requiresShortcut ?? false)
|
||||
}
|
||||
|
||||
func testICloudShareLinkMapsToShortcutsInstallURL() {
|
||||
let share = URL(string: "https://www.icloud.com/shortcuts/520317da7ae74759b64d5fb069c71f81")!
|
||||
let share = URL(string: "https://www.icloud.com/shortcuts/65bf33ba4206484ba78d582eaf1e9c44")!
|
||||
let url = AIAgentShortcutRun.shortcutsInstallURL(from: share)
|
||||
XCTAssertEqual(url?.scheme, "shortcuts")
|
||||
XCTAssertEqual(url?.host, "shortcuts")
|
||||
XCTAssertEqual(url?.path, "/520317da7ae74759b64d5fb069c71f81")
|
||||
XCTAssertEqual(url?.path, "/65bf33ba4206484ba78d582eaf1e9c44")
|
||||
XCTAssertEqual(
|
||||
AIAgentShortcutRun.iCloudShareToken(from: share),
|
||||
"520317da7ae74759b64d5fb069c71f81"
|
||||
"65bf33ba4206484ba78d582eaf1e9c44"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// AIEventExtractionTests.swift
|
||||
// OSGKeyboardTests
|
||||
|
||||
import XCTest
|
||||
@testable import OSGKeyboardShared
|
||||
|
||||
final class AIEventExtractionTests: XCTestCase {
|
||||
private var calendar: Calendar {
|
||||
var calendar = Calendar(identifier: .gregorian)
|
||||
calendar.timeZone = TimeZone(identifier: "Asia/Shanghai")!
|
||||
return calendar
|
||||
}
|
||||
|
||||
private var now: Date {
|
||||
date(2026, 8, 13, 20, 24)
|
||||
}
|
||||
|
||||
func testNONEAndEmptyProduceNoItems() {
|
||||
XCTAssertEqual(lines("NONE"), [])
|
||||
XCTAssertEqual(lines("没有日期或时间"), [])
|
||||
XCTAssertEqual(lines("no events"), [])
|
||||
XCTAssertEqual(lines(" \n "), [])
|
||||
}
|
||||
|
||||
func testAllDayDateOnly() {
|
||||
XCTAssertEqual(
|
||||
lines("2026-08-15||提交周报|"),
|
||||
["2026-08-15|ALLDAY|提交周报|"]
|
||||
)
|
||||
}
|
||||
|
||||
func testTimedFillsDefaultOneHour() {
|
||||
XCTAssertEqual(
|
||||
lines("2026-08-15 14:00||项目评审|"),
|
||||
["2026-08-15 14:00|2026-08-15 15:00|项目评审|"]
|
||||
)
|
||||
}
|
||||
|
||||
func testTimedKeepsExplicitEndAndLocation() {
|
||||
XCTAssertEqual(
|
||||
lines("2026-08-15 14:00|2026-08-15 16:00|项目评审|3楼会议室"),
|
||||
["2026-08-15 14:00|2026-08-15 16:00|项目评审|3楼会议室"]
|
||||
)
|
||||
}
|
||||
|
||||
func testTimeOnlyUsesToday() {
|
||||
XCTAssertEqual(
|
||||
lines("15:00||打电话给客户|"),
|
||||
["2026-08-13 15:00|2026-08-13 16:00|打电话给客户|"]
|
||||
)
|
||||
}
|
||||
|
||||
func testTimeOnlyEndOnSameDayOvernightRollsForward() {
|
||||
XCTAssertEqual(
|
||||
lines("2026-08-15 23:00|01:00|跨夜值班|"),
|
||||
["2026-08-15 23:00|2026-08-16 01:00|跨夜值班|"]
|
||||
)
|
||||
}
|
||||
|
||||
func testTwoFieldStartAndTitle() {
|
||||
XCTAssertEqual(
|
||||
lines("2026-08-15 14:00|开会"),
|
||||
["2026-08-15 14:00|2026-08-15 15:00|开会|"]
|
||||
)
|
||||
}
|
||||
|
||||
func testThreeFieldStartTitleLocationWhenMiddleIsNotATime() {
|
||||
XCTAssertEqual(
|
||||
lines("2026-08-15 14:00|开会|会议室A"),
|
||||
["2026-08-15 14:00|2026-08-15 15:00|开会|会议室A"]
|
||||
)
|
||||
}
|
||||
|
||||
func testDropsLinesWithoutStartOrTitle() {
|
||||
let raw = """
|
||||
买牛奶
|
||||
||无开始|
|
||||
2026-08-15 14:00||
|
||||
2026-08-16||有效全天|
|
||||
"""
|
||||
XCTAssertEqual(lines(raw), ["2026-08-16|ALLDAY|有效全天|"])
|
||||
}
|
||||
|
||||
func testMultipleEventsCapAtTwenty() {
|
||||
let raw = (1...25).map { "2026-08-15 10:00||任务\($0)|" }.joined(separator: "\n")
|
||||
let items = lines(raw)
|
||||
XCTAssertEqual(items.count, 20)
|
||||
XCTAssertEqual(items.first, "2026-08-15 10:00|2026-08-15 11:00|任务1|")
|
||||
XCTAssertEqual(items.last, "2026-08-15 10:00|2026-08-15 11:00|任务20|")
|
||||
}
|
||||
|
||||
func testWholeClipboardEchoIsRejected() {
|
||||
let source = String(repeating: "这是一段很长的会议纪要内容,包含许多句子。", count: 4)
|
||||
XCTAssertEqual(
|
||||
AIEventExtraction.lines(
|
||||
from: "2026-08-15||\(source)|",
|
||||
sourceClipboard: source,
|
||||
now: now,
|
||||
calendar: calendar
|
||||
),
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
func testPromptIncludesClockPipeContractAndNONE() {
|
||||
let prompt = AIClipboardSkillCatalog.instruction(
|
||||
skillID: AIClipboardSkillCatalog.extractEventsID,
|
||||
locale: "zh",
|
||||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId,
|
||||
now: now
|
||||
)
|
||||
XCTAssertTrue(prompt.contains("本地时区"))
|
||||
XCTAssertTrue(prompt.contains("开始|结束|标题|地点"))
|
||||
XCTAssertTrue(prompt.contains("NONE"))
|
||||
XCTAssertTrue(prompt.contains("不要把整段原文当成一条日程"))
|
||||
}
|
||||
|
||||
func testEnglishPromptIncludesClockAndNONE() {
|
||||
let prompt = AIClipboardSkillCatalog.instruction(
|
||||
skillID: AIClipboardSkillCatalog.extractEventsID,
|
||||
locale: "en",
|
||||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId,
|
||||
now: now
|
||||
)
|
||||
XCTAssertTrue(prompt.contains("local timezone"))
|
||||
XCTAssertTrue(prompt.contains("NONE"))
|
||||
XCTAssertTrue(prompt.contains("all-day"))
|
||||
}
|
||||
|
||||
private func lines(_ raw: String) -> [String] {
|
||||
AIEventExtraction.lines(from: raw, now: now, calendar: calendar)
|
||||
}
|
||||
|
||||
private func date(_ y: Int, _ m: Int, _ d: Int, _ h: Int, _ min: Int) -> Date {
|
||||
var components = DateComponents()
|
||||
components.year = y
|
||||
components.month = m
|
||||
components.day = d
|
||||
components.hour = h
|
||||
components.minute = min
|
||||
return calendar.date(from: components)!
|
||||
}
|
||||
}
|
||||
@@ -245,4 +245,24 @@ final class AIClipboardSkillTests: XCTestCase {
|
||||
XCTAssertTrue(prompt.contains("NONE"))
|
||||
XCTAssertTrue(prompt.contains("不要把整段原文当成一条待办"))
|
||||
}
|
||||
|
||||
func testExtractEventsAsksForNONEWhenEmpty() {
|
||||
let prompt = AIClipboardSkillCatalog.instruction(
|
||||
skillID: AIClipboardSkillCatalog.extractEventsID,
|
||||
locale: "zh",
|
||||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId
|
||||
)
|
||||
XCTAssertTrue(prompt.contains("NONE"))
|
||||
XCTAssertTrue(prompt.contains("开始|结束|标题|地点"))
|
||||
}
|
||||
|
||||
func testNavigateAsksForNONEWhenEmpty() {
|
||||
let prompt = AIClipboardSkillCatalog.instruction(
|
||||
skillID: AIClipboardSkillCatalog.navigateID,
|
||||
locale: "zh",
|
||||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId
|
||||
)
|
||||
XCTAssertTrue(prompt.contains("NONE"))
|
||||
XCTAssertTrue(prompt.contains("起点|终点"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,18 @@ final class AIModeLLMClientTests: XCTestCase {
|
||||
XCTAssertTrue(client is AIModeSearchFallbackClient)
|
||||
}
|
||||
|
||||
func testFactorySkipsSearchWhenThinkingDisabled() {
|
||||
let client = AIModeLLMClientFactory.make(
|
||||
providerId: "openai",
|
||||
baseURL: "https://api.openai.com/v1",
|
||||
apiKey: "sk-test",
|
||||
model: "gpt-5.4-mini",
|
||||
thinkingEnabled: false
|
||||
)
|
||||
XCTAssertFalse(client is AIModeSearchFallbackClient)
|
||||
XCTAssertTrue(client is OpenAICompatibleClient)
|
||||
}
|
||||
|
||||
func testFactoryPlainForGroq() {
|
||||
let client = AIModeLLMClientFactory.make(
|
||||
providerId: "groq",
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
// AIUserSkillTests.swift
|
||||
// OSGKeyboardTests
|
||||
|
||||
import XCTest
|
||||
@testable import OSGKeyboardShared
|
||||
|
||||
final class AIUserSkillTests: XCTestCase {
|
||||
private let sampleURL = URL(
|
||||
string: "https://www.icloud.com/shortcuts/65bf33ba4206484ba78d582eaf1e9c44"
|
||||
)!
|
||||
|
||||
func testValidICloudShareLink() {
|
||||
XCTAssertNotNil(AIShortcutShareLink.parse(
|
||||
"https://www.icloud.com/shortcuts/65bf33ba4206484ba78d582eaf1e9c44"
|
||||
))
|
||||
XCTAssertNotNil(AIShortcutShareLink.parse(
|
||||
"https://icloud.com/shortcuts/1f4afcf7ee22400cbf84e319d969aadf"
|
||||
))
|
||||
XCTAssertNil(AIShortcutShareLink.parse("https://example.com/shortcuts/abc"))
|
||||
XCTAssertNil(AIShortcutShareLink.parse("https://www.icloud.com/shortcuts/api/records/x"))
|
||||
XCTAssertNil(AIShortcutShareLink.parse("not a url"))
|
||||
XCTAssertNil(AIShortcutShareLink.parse("https://www.icloud.com/shortcuts/short"))
|
||||
}
|
||||
|
||||
func testParsesShortcutNameFromRecordsJSON() throws {
|
||||
let json = """
|
||||
{"fields":{"name":{"type":"STRING","value":"OSG · 提取待办"}}}
|
||||
""".data(using: .utf8)!
|
||||
XCTAssertEqual(try AIShortcutShareMetadata.name(fromRecordsJSON: json), "OSG · 提取待办")
|
||||
}
|
||||
|
||||
func testParsesShortcutNameFromLegacyRecordsArray() throws {
|
||||
let json = """
|
||||
{"records":[{"fields":{"name":{"value":"OSG · 提取日程"}}}]}
|
||||
""".data(using: .utf8)!
|
||||
XCTAssertEqual(try AIShortcutShareMetadata.name(fromRecordsJSON: json), "OSG · 提取日程")
|
||||
}
|
||||
|
||||
func testRejectsRecordsJSONWithoutName() {
|
||||
let json = Data(#"{"fields":{}}"#.utf8)
|
||||
XCTAssertThrowsError(try AIShortcutShareMetadata.name(fromRecordsJSON: json))
|
||||
}
|
||||
|
||||
func testCatalogUpsertKeepsShortcutNameIndependentOfSkillName() throws {
|
||||
var catalog = AIUserSkillCatalog()
|
||||
var skill = AIUserSkill(
|
||||
name: "会议纪要",
|
||||
summary: "抽要点",
|
||||
prompt: "提取要点",
|
||||
shortcutICloudURL: sampleURL,
|
||||
shortcutName: "OSG · 提取待办"
|
||||
)
|
||||
try catalog.upsert(skill)
|
||||
XCTAssertEqual(catalog.entries.first?.name, "会议纪要")
|
||||
XCTAssertEqual(catalog.entries.first?.shortcutName, "OSG · 提取待办")
|
||||
|
||||
skill.name = "纪要"
|
||||
skill.shortcutName = "My Tasks"
|
||||
try catalog.upsert(skill)
|
||||
XCTAssertEqual(catalog.entries.count, 1)
|
||||
XCTAssertEqual(catalog.entries.first?.name, "纪要")
|
||||
XCTAssertEqual(catalog.entries.first?.shortcutName, "My Tasks")
|
||||
}
|
||||
|
||||
func testThinkingDefaultsOffAndBuiltinCannotEnable() {
|
||||
let user = AIUserSkill(
|
||||
name: "Custom",
|
||||
prompt: "Do it",
|
||||
shortcutICloudURL: sampleURL,
|
||||
shortcutName: "Run Me"
|
||||
)
|
||||
XCTAssertFalse(user.thinkingEnabled)
|
||||
XCTAssertFalse(user.asClipboardSkill().thinkingEnabled)
|
||||
|
||||
let withThinking = AIUserSkill(
|
||||
name: "Custom",
|
||||
prompt: "Do it",
|
||||
shortcutICloudURL: sampleURL,
|
||||
shortcutName: "Run Me",
|
||||
thinkingEnabled: true
|
||||
)
|
||||
XCTAssertTrue(withThinking.asClipboardSkill().thinkingEnabled)
|
||||
|
||||
let builtin = AIClipboardSkillCatalog.skill(id: AIClipboardSkillCatalog.replyID)
|
||||
XCTAssertEqual(builtin?.thinkingEnabled, false)
|
||||
}
|
||||
|
||||
func testInstructionUsesCustomPrompt() {
|
||||
let skill = AIUserSkill(
|
||||
name: "Custom",
|
||||
prompt: "只输出一行标题",
|
||||
shortcutICloudURL: sampleURL,
|
||||
shortcutName: "Run Me"
|
||||
).asClipboardSkill()
|
||||
XCTAssertEqual(
|
||||
AIClipboardSkillCatalog.instruction(
|
||||
for: skill,
|
||||
locale: "zh",
|
||||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId
|
||||
),
|
||||
"只输出一行标题"
|
||||
)
|
||||
}
|
||||
|
||||
func testVisibleIncludesUserSkills() throws {
|
||||
var catalog = AIUserSkillCatalog()
|
||||
let skill = AIUserSkill(
|
||||
name: "Custom",
|
||||
prompt: "Do it",
|
||||
shortcutICloudURL: sampleURL,
|
||||
shortcutName: "Run Me"
|
||||
)
|
||||
try catalog.upsert(skill)
|
||||
let visible = AIClipboardSkillCatalog.visible(
|
||||
enabledIDs: [skill.id],
|
||||
userCatalog: catalog
|
||||
)
|
||||
XCTAssertEqual(visible.map(\.id), [skill.id])
|
||||
XCTAssertEqual(visible.first?.customName, "Custom")
|
||||
}
|
||||
|
||||
func testSanitizeKeepsConfirmedUserExportSkill() throws {
|
||||
var catalog = AIUserSkillCatalog()
|
||||
let skill = AIUserSkill(
|
||||
name: "Custom",
|
||||
prompt: "Do it",
|
||||
shortcutICloudURL: sampleURL,
|
||||
shortcutName: "Run Me"
|
||||
)
|
||||
try catalog.upsert(skill)
|
||||
let layout = AIAgentSkillLayout(
|
||||
enabledIDs: [skill.id],
|
||||
confirmedShortcutIDs: [skill.id]
|
||||
).sanitized(catalog: AIClipboardSkillCatalog.all(userCatalog: catalog))
|
||||
XCTAssertEqual(layout.enabledIDs, [skill.id])
|
||||
}
|
||||
|
||||
func testGenericExportSplitsLinesAndHonorsNONE() {
|
||||
XCTAssertEqual(AIGenericSkillExport.items(from: "NONE"), [])
|
||||
XCTAssertEqual(AIGenericSkillExport.items(from: "买牛奶\n回邮件"), ["买牛奶", "回邮件"])
|
||||
XCTAssertEqual(AIGenericSkillExport.items(from: "一段没有换行的结果"), ["一段没有换行的结果"])
|
||||
}
|
||||
|
||||
func testNoUserSkillLimit() throws {
|
||||
var catalog = AIUserSkillCatalog()
|
||||
for index in 0..<12 {
|
||||
try catalog.upsert(
|
||||
AIUserSkill(
|
||||
name: "Skill \(index)",
|
||||
prompt: "Do it",
|
||||
shortcutICloudURL: sampleURL,
|
||||
shortcutName: "Run \(index)"
|
||||
)
|
||||
)
|
||||
}
|
||||
XCTAssertEqual(catalog.entries.count, 12)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class AIUserSkillStoreTests: XCTestCase {
|
||||
private func makeDefaults() -> UserDefaults {
|
||||
let suite = "group.com.osgkeyboard.shared.tests.userSkills.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
defaults.removePersistentDomain(forName: suite)
|
||||
return defaults
|
||||
}
|
||||
|
||||
func testChangingShortcutLinkDropsConfirmation() throws {
|
||||
let store = AIAgentSkillLayoutStore(defaults: makeDefaults())
|
||||
let firstURL = URL(string: "https://www.icloud.com/shortcuts/65bf33ba4206484ba78d582eaf1e9c44")!
|
||||
let secondURL = URL(string: "https://www.icloud.com/shortcuts/1f4afcf7ee22400cbf84e319d969aadf")!
|
||||
var skill = AIUserSkill(
|
||||
name: "Custom",
|
||||
prompt: "Do it",
|
||||
shortcutICloudURL: firstURL,
|
||||
shortcutName: "One"
|
||||
)
|
||||
try store.saveUserSkill(skill)
|
||||
XCTAssertEqual(store.confirmShortcutAndEnable(skill.id), .enabled)
|
||||
XCTAssertTrue(store.layout.isEnabled(skill.id))
|
||||
|
||||
skill.shortcutICloudURL = secondURL
|
||||
skill.shortcutName = "Two"
|
||||
try store.saveUserSkill(skill)
|
||||
XCTAssertFalse(store.layout.hasConfirmedShortcut(skill.id))
|
||||
XCTAssertFalse(store.layout.isEnabled(skill.id))
|
||||
}
|
||||
}
|
||||
@@ -323,6 +323,47 @@ final class FlowSessionBridgeTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(decoded, command)
|
||||
XCTAssertEqual(decoded.aiQuestionText, "总结这段剪贴板内容")
|
||||
XCTAssertNil(decoded.aiThinkingEnabled)
|
||||
}
|
||||
|
||||
func testSubmitAIQuestionCommandRoundTripsThinkingOverride() throws {
|
||||
let command = FlowCommand(
|
||||
sessionId: UUID(),
|
||||
utteranceId: UUID(),
|
||||
commandSeq: 45,
|
||||
action: .submitAIQuestion,
|
||||
localeId: "zh-Hans",
|
||||
utteranceMode: .aiQuestion,
|
||||
aiConversationID: UUID(),
|
||||
aiQuestionText: "总结这段剪贴板内容",
|
||||
aiThinkingEnabled: false
|
||||
)
|
||||
let decoded = try JSONDecoder().decode(
|
||||
FlowCommand.self,
|
||||
from: JSONEncoder().encode(command)
|
||||
)
|
||||
XCTAssertEqual(decoded.aiThinkingEnabled, false)
|
||||
}
|
||||
|
||||
func testFlowCommandDecodesLegacyJSONWithoutThinkingKey() throws {
|
||||
let command = FlowCommand(
|
||||
sessionId: UUID(),
|
||||
utteranceId: UUID(),
|
||||
commandSeq: 44,
|
||||
action: .submitAIQuestion,
|
||||
localeId: "zh-Hans",
|
||||
utteranceMode: .aiQuestion,
|
||||
aiConversationID: UUID(),
|
||||
aiQuestionText: "总结这段剪贴板内容"
|
||||
)
|
||||
var object = try JSONSerialization.jsonObject(
|
||||
with: JSONEncoder().encode(command)
|
||||
) as! [String: Any]
|
||||
object.removeValue(forKey: "aiThinkingEnabled")
|
||||
let data = try JSONSerialization.data(withJSONObject: object)
|
||||
let decoded = try JSONDecoder().decode(FlowCommand.self, from: data)
|
||||
XCTAssertNil(decoded.aiThinkingEnabled)
|
||||
XCTAssertEqual(decoded.aiQuestionText, "总结这段剪贴板内容")
|
||||
}
|
||||
|
||||
func testSecureFieldContextRedactsText() {
|
||||
|
||||
Reference in New Issue
Block a user