498f407585
Introduce optional Apple account-backed credits with scoped gateway access while preserving local and BYOK paths. Refresh assistant behavior, tests, privacy disclosures, docs, and the website for the 2.0 experience.
241 lines
8.5 KiB
Swift
241 lines
8.5 KiB
Swift
// AIUserSkillTests.swift
|
|
// OSGKeyboardTests
|
|
|
|
@testable import OSGKeyboardShared
|
|
import XCTest
|
|
|
|
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 testTextOnlySkillAllowsEmptyShortcutConfiguration() throws {
|
|
var catalog = AIUserSkillCatalog()
|
|
let skill = AIUserSkill(
|
|
name: "Rewrite",
|
|
prompt: "Rewrite the clipboard"
|
|
)
|
|
|
|
try catalog.upsert(skill)
|
|
|
|
let saved = try XCTUnwrap(catalog.entries.first)
|
|
let clipboardSkill = saved.asClipboardSkill()
|
|
XCTAssertNil(saved.shortcutICloudURL)
|
|
XCTAssertEqual(saved.shortcutName, "")
|
|
XCTAssertEqual(clipboardSkill.kind, .transform)
|
|
XCTAssertFalse(clipboardSkill.requiresShortcut)
|
|
XCTAssertNil(clipboardSkill.shortcutName)
|
|
}
|
|
|
|
func testShortcutNameWithoutLinkDoesNotExport() throws {
|
|
var catalog = AIUserSkillCatalog()
|
|
let skill = AIUserSkill(
|
|
name: "Rewrite",
|
|
prompt: "Rewrite the clipboard",
|
|
shortcutName: "Ignored without a link"
|
|
)
|
|
|
|
try catalog.upsert(skill)
|
|
|
|
let saved = try XCTUnwrap(catalog.entries.first)
|
|
XCTAssertEqual(saved.shortcutName, "Ignored without a link")
|
|
XCTAssertEqual(saved.asClipboardSkill().kind, .transform)
|
|
XCTAssertNil(saved.asClipboardSkill().shortcutName)
|
|
}
|
|
|
|
func testShortcutSkillRequiresNameAndValidShareLink() {
|
|
var catalog = AIUserSkillCatalog()
|
|
XCTAssertThrowsError(
|
|
try catalog.upsert(
|
|
AIUserSkill(
|
|
name: "Export",
|
|
prompt: "Export it",
|
|
shortcutICloudURL: sampleURL
|
|
)
|
|
)
|
|
) { error in
|
|
XCTAssertEqual(error as? AIUserSkillValidationError, .emptyShortcutName)
|
|
}
|
|
|
|
XCTAssertThrowsError(
|
|
try catalog.upsert(
|
|
AIUserSkill(
|
|
name: "Export",
|
|
prompt: "Export it",
|
|
shortcutICloudURL: URL(string: "https://example.com/not-a-shortcut"),
|
|
shortcutName: "Run Me"
|
|
)
|
|
)
|
|
) { error in
|
|
XCTAssertEqual(error as? AIUserSkillValidationError, .invalidShortcutLink)
|
|
}
|
|
}
|
|
|
|
func testExistingShortcutSkillEncodingDecodesWithOptionalURL() throws {
|
|
let original = AIUserSkill(
|
|
name: "Export",
|
|
prompt: "Export it",
|
|
shortcutICloudURL: sampleURL,
|
|
shortcutName: "Run Me"
|
|
)
|
|
|
|
let decoded = try JSONDecoder().decode(
|
|
AIUserSkill.self,
|
|
from: JSONEncoder().encode(original)
|
|
)
|
|
|
|
XCTAssertEqual(decoded.shortcutICloudURL, sampleURL)
|
|
XCTAssertEqual(decoded.asClipboardSkill().kind, .export)
|
|
XCTAssertTrue(decoded.asClipboardSkill().requiresShortcut)
|
|
}
|
|
|
|
func testThinkingDefaultsOffAndBuiltinCannotEnable() {
|
|
let user = AIUserSkill(
|
|
name: "Custom",
|
|
prompt: "Do it",
|
|
shortcutICloudURL: sampleURL,
|
|
shortcutName: "Run Me"
|
|
)
|
|
XCTAssertFalse(user.thinkingEnabled)
|
|
XCTAssertFalse(user.asClipboardSkill().thinkingEnabled)
|
|
XCTAssertEqual(user.asClipboardSkill().managedGatewayTaskKind, .customSkill)
|
|
|
|
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)
|
|
XCTAssertEqual(builtin?.managedGatewayTaskKind, .clipboardTransform)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|