Version: CURRENT_PROJECT_VERSION 130 -> 131 (project.yml, AGENTS.md, READMEs, docs/llms.txt). fix(flow): host-app identification now compiles in every build configuration (Debug, TestFlight, Release). The arbiter probe and HostAppIdentity were DEBUG-only, so TestFlight/App Store builds never knew the host bundle id and could not auto-return after a startflow handoff. Debug-only logging and the Home test card are unchanged. Also includes the pending work in the tree: - Home: single account/setup card surface (HomeAccountSurfacePolicy/Views) - Wide screens: voice key row and centred mic placement, iPhone Duo layout - Keyboard: clipboard skill ranking and AI skill layout updates - Debug: account session handoff to the Simulator (+ import script) - Docs: portfolio assets, privacy policy URL, site/README refresh
621 lines
24 KiB
Swift
621 lines
24 KiB
Swift
// AIHintKeywordExtractorTests.swift
|
||
// OSGKeyboardTests
|
||
|
||
@testable import OSGKeyboardShared
|
||
import XCTest
|
||
|
||
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 testStockHintsKeepDistinctMarketLabelsAndChartIcon() {
|
||
let expectedLabels = [
|
||
"local-zh-stocks-cn": "今日A股",
|
||
"local-zh-stocks-hk": "今日港股",
|
||
"local-zh-stocks-us": "今日美股"
|
||
]
|
||
let cards = AIHintLocalCatalog.cards(locale: "zh")
|
||
.filter { expectedLabels[$0.id] != nil }
|
||
|
||
XCTAssertEqual(cards.count, expectedLabels.count)
|
||
for card in cards {
|
||
XCTAssertEqual(card.visualKind, .stocks)
|
||
XCTAssertEqual(card.resolvedDisplayText, expectedLabels[card.id])
|
||
}
|
||
}
|
||
}
|
||
|
||
final class AIClipboardSkillTests: XCTestCase {
|
||
/// Every built-in skill ships on. "Speak as me" included: it is withheld by
|
||
/// `requiresPersonalReplyStyleIDs` until a style exists, so it can never
|
||
/// become a permanently broken chip, and it needs no opt-in of its own.
|
||
func testVisibleDefaultsContainEveryBuiltInSkill() {
|
||
XCTAssertEqual(
|
||
AIClipboardSkillCatalog.visible().map(\.id),
|
||
AIClipboardSkillCatalog.catalog.map(\.id)
|
||
)
|
||
XCTAssertTrue(AIClipboardSkillCatalog.catalog.allSatisfy(\.isDefault))
|
||
}
|
||
|
||
func testVisibleRespectsEnabledIDsForFutureSettings() {
|
||
XCTAssertEqual(
|
||
AIClipboardSkillCatalog.visible(enabledIDs: ["translate", "reply"]).map(\.id),
|
||
["translate", "reply"]
|
||
)
|
||
XCTAssertEqual(
|
||
AIClipboardSkillCatalog.visible(enabledIDs: ["unknown"]).map(\.id),
|
||
[]
|
||
)
|
||
}
|
||
|
||
func testTranslateUsesPrimarySystemLanguage() {
|
||
let prompt = AIClipboardSkillCatalog.instruction(
|
||
skillID: AIClipboardSkillCatalog.translateID,
|
||
locale: "zh",
|
||
translationTargetLocaleId: "ja",
|
||
preferredLanguages: ["de-DE"]
|
||
)
|
||
XCTAssertTrue(prompt.contains("German"))
|
||
XCTAssertFalse(prompt.contains("Japanese"))
|
||
XCTAssertTrue(prompt.contains("原样输出"))
|
||
}
|
||
|
||
func testTranslateUsesSystemLanguageWhenPostTranslationIsOff() {
|
||
let prompt = AIClipboardSkillCatalog.instruction(
|
||
skillID: AIClipboardSkillCatalog.translateID,
|
||
locale: "en",
|
||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId,
|
||
preferredLanguages: ["ja-JP"]
|
||
)
|
||
XCTAssertTrue(prompt.contains("Japanese"))
|
||
XCTAssertTrue(prompt.contains("system language"))
|
||
}
|
||
|
||
func testTranslateButtonTitleUsesSystemLanguage() {
|
||
XCTAssertEqual(
|
||
AIClipboardSkillCatalog.translateButtonTitle(
|
||
translationTargetLocaleId: "de",
|
||
uiLanguage: .chinese,
|
||
preferredLanguages: ["en-US"]
|
||
),
|
||
"译为英语"
|
||
)
|
||
XCTAssertEqual(
|
||
AIClipboardSkillCatalog.translateButtonTitle(
|
||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId,
|
||
uiLanguage: .english,
|
||
preferredLanguages: ["ja-JP"]
|
||
),
|
||
"To Japanese"
|
||
)
|
||
}
|
||
|
||
func testTranslateToTargetUsesConfiguredTargetLanguage() {
|
||
let prompt = AIClipboardSkillCatalog.instruction(
|
||
skillID: AIClipboardSkillCatalog.translateToTargetID,
|
||
locale: "en",
|
||
translationTargetLocaleId: "ja",
|
||
preferredLanguages: ["zh-Hans"]
|
||
)
|
||
XCTAssertTrue(prompt.contains("Japanese"))
|
||
XCTAssertFalse(prompt.contains("Chinese"))
|
||
}
|
||
|
||
func testTranslateToTargetPromptFallsBackWhenTargetIsOff() {
|
||
let prompt = AIClipboardSkillCatalog.instruction(
|
||
skillID: AIClipboardSkillCatalog.translateToTargetID,
|
||
locale: "en",
|
||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId
|
||
)
|
||
XCTAssertTrue(prompt.contains("English"))
|
||
}
|
||
|
||
func testTranslateToTargetButtonTitleNamesTargetAndHidesWhenOff() {
|
||
XCTAssertEqual(
|
||
AIClipboardSkillCatalog.translateToTargetButtonTitle(
|
||
translationTargetLocaleId: "en",
|
||
uiLanguage: .chinese
|
||
),
|
||
"翻译英语"
|
||
)
|
||
XCTAssertEqual(
|
||
AIClipboardSkillCatalog.translateToTargetButtonTitle(
|
||
translationTargetLocaleId: "ja",
|
||
uiLanguage: .english
|
||
),
|
||
"To Japanese"
|
||
)
|
||
XCTAssertNil(
|
||
AIClipboardSkillCatalog.translateToTargetButtonTitle(
|
||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId,
|
||
uiLanguage: .english
|
||
)
|
||
)
|
||
}
|
||
|
||
func testPolishKeepsOriginalLanguageAndCarriesNeverAnswerBoundary() throws {
|
||
let skill = try XCTUnwrap(
|
||
AIClipboardSkillCatalog.skill(id: AIClipboardSkillCatalog.polishOriginalID)
|
||
)
|
||
let prompt = AIClipboardSkillCatalog.instruction(
|
||
for: skill,
|
||
locale: "en",
|
||
translationTargetLocaleId: "ja"
|
||
)
|
||
XCTAssertTrue(prompt.contains("without changing its language"))
|
||
XCTAssertTrue(prompt.contains("grammar"))
|
||
XCTAssertTrue(prompt.contains("Non-negotiable boundary"))
|
||
XCTAssertFalse(prompt.contains("Japanese"))
|
||
}
|
||
|
||
func testSummarizeAsksForOverviewNotShortening() {
|
||
let prompt = AIClipboardSkillCatalog.instruction(
|
||
skillID: AIClipboardSkillCatalog.summarizeID,
|
||
locale: "zh",
|
||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId
|
||
)
|
||
XCTAssertTrue(prompt.contains("总结"))
|
||
XCTAssertTrue(prompt.contains("决定、结论和下一步"))
|
||
}
|
||
|
||
func testReplyScenesHaveDistinctInstructions() throws {
|
||
let skill = try XCTUnwrap(
|
||
AIClipboardSkillCatalog.skill(id: AIClipboardSkillCatalog.replyID)
|
||
)
|
||
let scenes: [AIClipboardReplyScene] = [
|
||
.invitation,
|
||
.task,
|
||
.blessing,
|
||
.clarification,
|
||
.complaint,
|
||
.negativeQuestion
|
||
]
|
||
let prompts = scenes.map {
|
||
AIClipboardSkillCatalog.instruction(
|
||
for: skill,
|
||
locale: "zh",
|
||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId,
|
||
replyScene: $0
|
||
)
|
||
}
|
||
XCTAssertEqual(Set(prompts).count, scenes.count)
|
||
XCTAssertFalse(prompts.contains { $0.contains("用户选择的操作") })
|
||
}
|
||
|
||
func testPlayfulReplyIsWittyButKeepsSafetyBoundaries() throws {
|
||
let skill = try XCTUnwrap(
|
||
AIClipboardSkillCatalog.skill(id: AIClipboardSkillCatalog.playfulReplyID)
|
||
)
|
||
let instruction = AIClipboardSkillCatalog.instruction(
|
||
for: skill,
|
||
locale: "zh",
|
||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId
|
||
)
|
||
|
||
XCTAssertTrue(skill.supportsReplyStyle)
|
||
XCTAssertTrue(instruction.contains("脱口秀演员"))
|
||
XCTAssertTrue(instruction.contains("不攻击对方"))
|
||
XCTAssertTrue(instruction.contains("严肃或敏感内容时收住幽默"))
|
||
XCTAssertTrue(instruction.contains("普通人在和朋友、好友或同事聊天"))
|
||
}
|
||
|
||
func testReplyUsesConversationalBaselineAndOptionalLearnedStyle() throws {
|
||
let skill = try XCTUnwrap(
|
||
AIClipboardSkillCatalog.skill(id: AIClipboardSkillCatalog.replyID)
|
||
)
|
||
let instruction = AIClipboardSkillCatalog.instruction(
|
||
for: skill,
|
||
locale: "zh",
|
||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId,
|
||
replyStyle: AIClipboardReplyStyleContext(
|
||
styleID: "user.learned",
|
||
prompt: "喜欢短句,常用“行”“可以”,不说客套话。"
|
||
)
|
||
)
|
||
|
||
XCTAssertTrue(instruction.contains("普通人在和朋友、好友或同事聊天"))
|
||
XCTAssertTrue(instruction.contains("1 个合适的表情或 Emoji"))
|
||
XCTAssertTrue(instruction.contains("不得复述、改写、概括"))
|
||
XCTAssertTrue(instruction.contains("<user_reply_style"))
|
||
XCTAssertTrue(instruction.contains("喜欢短句"))
|
||
XCTAssertTrue(instruction.contains("不能改变当前技能的意图"))
|
||
}
|
||
|
||
func testReplyUsesComplaintSceneModifier() throws {
|
||
let skill = try XCTUnwrap(
|
||
AIClipboardSkillCatalog.skill(id: AIClipboardSkillCatalog.replyID)
|
||
)
|
||
let instruction = AIClipboardSkillCatalog.instruction(
|
||
for: skill,
|
||
locale: "zh",
|
||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId,
|
||
replyScene: .complaint
|
||
)
|
||
|
||
XCTAssertTrue(instruction.contains(#"<reply_scene type="complaint">"#))
|
||
XCTAssertTrue(instruction.contains("接住对方的情绪"))
|
||
XCTAssertTrue(instruction.contains("不虚构责任、进度或承诺"))
|
||
XCTAssertTrue(instruction.contains("不能开玩笑"))
|
||
}
|
||
|
||
func testReplyUsesLighterModifierForNegativeQuestion() throws {
|
||
let skill = try XCTUnwrap(
|
||
AIClipboardSkillCatalog.skill(id: AIClipboardSkillCatalog.replyID)
|
||
)
|
||
let instruction = AIClipboardSkillCatalog.instruction(
|
||
for: skill,
|
||
locale: "zh",
|
||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId,
|
||
replyScene: .negativeQuestion
|
||
)
|
||
|
||
XCTAssertTrue(instruction.contains(#"<reply_scene type="negative_question">"#))
|
||
XCTAssertTrue(instruction.contains("不要因为语气负面就默认用户有错"))
|
||
XCTAssertFalse(instruction.contains(#"<reply_scene type="complaint">"#))
|
||
}
|
||
|
||
func testLegacyDecisionSkillUsesUnifiedReplyScene() throws {
|
||
let skill = try XCTUnwrap(
|
||
AIClipboardSkillCatalog.skill(id: AIClipboardSkillCatalog.acceptInvitationID)
|
||
)
|
||
let instruction = AIClipboardSkillCatalog.instruction(
|
||
for: skill,
|
||
locale: "zh",
|
||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId,
|
||
replyScene: .complaint
|
||
)
|
||
|
||
XCTAssertEqual(skill.id, AIClipboardSkillCatalog.replyID)
|
||
XCTAssertTrue(instruction.contains(#"<reply_scene type="complaint">"#))
|
||
}
|
||
|
||
func testReplyStyleIsNotInjectedIntoNonReplySkill() throws {
|
||
let skill = try XCTUnwrap(
|
||
AIClipboardSkillCatalog.skill(id: AIClipboardSkillCatalog.summarizeID)
|
||
)
|
||
let instruction = AIClipboardSkillCatalog.instruction(
|
||
for: skill,
|
||
locale: "zh",
|
||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId,
|
||
replyStyle: AIClipboardReplyStyleContext(
|
||
styleID: "user.learned",
|
||
prompt: "这是个人回复风格"
|
||
)
|
||
)
|
||
|
||
XCTAssertFalse(instruction.contains("user_reply_style"))
|
||
XCTAssertFalse(instruction.contains("这是个人回复风格"))
|
||
}
|
||
|
||
func testReplyStyleResolverAcceptsOnlyDistilledPersonalStyle() {
|
||
let distilled = PolishStylePack(
|
||
id: "user.learned",
|
||
name: "我的风格",
|
||
prompt: "喜欢短句",
|
||
learningMetadata: Self.learningMetadata(),
|
||
kind: .user
|
||
)
|
||
// Hand-written packs live on the Styles page and shape dictation only.
|
||
let handWritten = PolishStylePack(
|
||
id: "user.handwritten",
|
||
name: "我手写的",
|
||
prompt: "使用长句",
|
||
kind: .user
|
||
)
|
||
let builtIn = PolishStylePack(
|
||
id: "builtin.formal",
|
||
name: "正式",
|
||
prompt: "使用正式表达",
|
||
kind: .builtin
|
||
)
|
||
|
||
XCTAssertEqual(
|
||
AIClipboardReplyStyleContext.resolve(personalReplyStyle: distilled)?.prompt,
|
||
"喜欢短句"
|
||
)
|
||
XCTAssertNil(AIClipboardReplyStyleContext.resolve(personalReplyStyle: handWritten))
|
||
XCTAssertNil(AIClipboardReplyStyleContext.resolve(personalReplyStyle: builtIn))
|
||
XCTAssertNil(AIClipboardReplyStyleContext.resolve(personalReplyStyle: nil))
|
||
}
|
||
|
||
// MARK: - Speak as me
|
||
|
||
private func speakAsMeInstruction(
|
||
style: AIClipboardReplyStyleContext?,
|
||
locale: String = "zh"
|
||
) throws -> String {
|
||
let skill = try XCTUnwrap(
|
||
AIClipboardSkillCatalog.skill(id: AIClipboardSkillCatalog.speakAsMeID)
|
||
)
|
||
return AIClipboardSkillCatalog.instruction(
|
||
for: skill,
|
||
locale: locale,
|
||
translationTargetLocaleId: "en",
|
||
replyStyle: style
|
||
)
|
||
}
|
||
|
||
/// Reuses `replyInstruction` and the skill inherits its "usually 1–3
|
||
/// sentences" chat baseline, which would truncate a long clipboard and
|
||
/// fight a formal personal style.
|
||
func testSpeakAsMeSkipsTheReplyChatBaselineAndItsLengthCap() throws {
|
||
let instruction = try speakAsMeInstruction(
|
||
style: AIClipboardReplyStyleContext(styleID: "user.learned", prompt: "偏好长句书面表达")
|
||
)
|
||
|
||
XCTAssertFalse(instruction.contains("通常控制在 1~3 句"))
|
||
XCTAssertFalse(instruction.contains("像一个普通人在和朋友"))
|
||
// The old ±20% length cap fought a formal personal style and forced a
|
||
// single-paraphrase that the model interpreted as "echo the source".
|
||
// Length now follows the user's natural voice.
|
||
XCTAssertFalse(instruction.contains("± 20%"))
|
||
}
|
||
|
||
/// "Speak as me" must tell the model to emit one plain-text rewrite, not the
|
||
/// Reply contract's JSON variants. Otherwise the clipboard answer gets
|
||
/// stuffed with raw JSON like
|
||
/// `{"variants":[{"kind":"ordinary",…}]}` instead of a polished text.
|
||
func testSpeakAsMeForbidsJsonAndVariantsOutputShape() throws {
|
||
let instruction = try speakAsMeInstruction(
|
||
style: AIClipboardReplyStyleContext(styleID: "user.learned", prompt: "偏好短句")
|
||
)
|
||
|
||
// Output shape contract (must be present and unambiguous).
|
||
XCTAssertTrue(instruction.contains("只输出"))
|
||
XCTAssertTrue(instruction.contains("一段"))
|
||
XCTAssertTrue(instruction.contains("禁止 JSON"))
|
||
XCTAssertTrue(instruction.contains("代码块"))
|
||
XCTAssertTrue(instruction.contains("不要拆成多个版本"))
|
||
XCTAssertTrue(instruction.contains("不要提供选项"))
|
||
|
||
// The multi-reply contract must NOT leak into speakAsMe.
|
||
XCTAssertFalse(instruction.contains("MULTI-REPLY OUTPUT CONTRACT"))
|
||
XCTAssertFalse(instruction.contains("\"variants\""))
|
||
XCTAssertFalse(instruction.contains("\"kind\":\"ordinary\""))
|
||
XCTAssertFalse(instruction.contains("\"emotion\":\"neutral\""))
|
||
}
|
||
|
||
/// The personal style block uses XML tags for prompt parsing. The model
|
||
/// occasionally mistakes the tags for output scaffolding and echoes them
|
||
/// back. The prompt must explicitly tell it not to.
|
||
func testSpeakAsMeWarnsAgainstEchoingTheStyleBlockTags() throws {
|
||
let instruction = try speakAsMeInstruction(
|
||
style: AIClipboardReplyStyleContext(styleID: "user.learned", prompt: "短句为主")
|
||
)
|
||
|
||
XCTAssertTrue(instruction.contains("prompt 的内部包装"))
|
||
XCTAssertTrue(instruction.contains("不要把它带到最终输出里"))
|
||
}
|
||
|
||
/// English counterpart: single plain-text rewrite, no JSON, no variants.
|
||
func testSpeakAsMeEnglishForbidsJsonAndVariantsOutputShape() throws {
|
||
let instruction = try speakAsMeInstruction(
|
||
style: AIClipboardReplyStyleContext(styleID: "user.learned", prompt: "short sentences"),
|
||
locale: "en"
|
||
)
|
||
|
||
XCTAssertTrue(instruction.contains("Output **one single** piece of ready-to-send text only"))
|
||
XCTAssertTrue(instruction.contains("Never emit JSON"))
|
||
XCTAssertTrue(instruction.contains("Never split the result into multiple variants"))
|
||
XCTAssertFalse(instruction.contains("±20%"))
|
||
XCTAssertFalse(instruction.contains("within ±20%"))
|
||
}
|
||
|
||
/// Rewriting is not replying: Reply deliberately omits the never-answer
|
||
/// boundary because answering is its job. This skill must carry it.
|
||
func testSpeakAsMeCarriesTheNeverAnswerBoundary() throws {
|
||
let zh = try speakAsMeInstruction(
|
||
style: AIClipboardReplyStyleContext(styleID: "user.learned", prompt: "偏好短句")
|
||
)
|
||
XCTAssertTrue(zh.contains("# 不可协商边界"))
|
||
XCTAssertTrue(zh.contains("偏好短句"))
|
||
XCTAssertTrue(zh.contains("user_reply_style"))
|
||
|
||
let en = try speakAsMeInstruction(
|
||
style: AIClipboardReplyStyleContext(styleID: "user.learned", prompt: "short sentences"),
|
||
locale: "en"
|
||
)
|
||
XCTAssertTrue(en.contains("# Non-negotiable boundary"))
|
||
}
|
||
|
||
/// Without a personal style the skill has nothing to rewrite toward. It is
|
||
/// gated upstream; if it ever runs anyway it must not answer the clipboard.
|
||
func testSpeakAsMeWithoutStyleStillRefusesToAnswer() throws {
|
||
let instruction = try speakAsMeInstruction(style: nil)
|
||
|
||
XCTAssertFalse(instruction.contains("user_reply_style"))
|
||
XCTAssertTrue(instruction.contains("# 不可协商边界"))
|
||
}
|
||
|
||
func testSpeakAsMeShipsEnabledAndIsManagedOutsideTheGenericSkillLists() throws {
|
||
let skill = try XCTUnwrap(
|
||
AIClipboardSkillCatalog.skill(id: AIClipboardSkillCatalog.speakAsMeID)
|
||
)
|
||
|
||
XCTAssertTrue(skill.isDefault)
|
||
XCTAssertTrue(
|
||
AIAgentSkillLayout.defaultEnabledIDs.contains(AIClipboardSkillCatalog.speakAsMeID)
|
||
)
|
||
XCTAssertTrue(
|
||
AIClipboardSkillCatalog.managedOutsideSkillListIDs
|
||
.contains(AIClipboardSkillCatalog.speakAsMeID)
|
||
)
|
||
// Hidden for a different reason than the always-on system actions: it
|
||
// carries a prerequisite they do not.
|
||
XCTAssertFalse(
|
||
AIClipboardSkillCatalog.hiddenFromSkillManagementIDs
|
||
.contains(AIClipboardSkillCatalog.speakAsMeID)
|
||
)
|
||
XCTAssertTrue(
|
||
AIClipboardSkillCatalog.requiresPersonalReplyStyleIDs
|
||
.contains(AIClipboardSkillCatalog.speakAsMeID)
|
||
)
|
||
// The personal style is this skill's task, not a modifier layered on a
|
||
// generated reply, so it must not ride the reply-style path.
|
||
XCTAssertFalse(skill.supportsReplyStyle)
|
||
}
|
||
|
||
private static func learningMetadata(
|
||
generatedAt: Date = Date()
|
||
) -> PolishStylePack.LearningMetadata {
|
||
PolishStylePack.LearningMetadata(
|
||
schemaVersion: 2,
|
||
evidenceStatus: "sufficient",
|
||
confidence: 0.7,
|
||
asrExampleCount: 8,
|
||
asrEffectiveCharacterCount: 2_600,
|
||
replyExampleCount: 3,
|
||
replyFinalEditCount: 1,
|
||
generatedAt: generatedAt
|
||
)
|
||
}
|
||
|
||
func testBusinessReplyKeepsProfessionalBaselineWithoutFriendEmojiGuidance() throws {
|
||
let skill = try XCTUnwrap(
|
||
AIClipboardSkillCatalog.skill(id: AIClipboardSkillCatalog.businessReplyID)
|
||
)
|
||
let instruction = AIClipboardSkillCatalog.instruction(
|
||
for: skill,
|
||
locale: "zh",
|
||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId
|
||
)
|
||
|
||
XCTAssertTrue(instruction.contains("同事之间正常沟通"))
|
||
XCTAssertFalse(instruction.contains("朋友、好友"))
|
||
XCTAssertFalse(instruction.contains("表情或 Emoji"))
|
||
}
|
||
|
||
func testExtractTodosAsksForNONEWhenEmpty() {
|
||
let prompt = AIClipboardSkillCatalog.instruction(
|
||
skillID: AIClipboardSkillCatalog.extractTodosID,
|
||
locale: "zh",
|
||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId
|
||
)
|
||
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("起点|终点"))
|
||
}
|
||
|
||
func testSaveToNotesAsksForTitleNotBody() {
|
||
let prompt = AIClipboardSkillCatalog.instruction(
|
||
skillID: AIClipboardSkillCatalog.saveToNotesID,
|
||
locale: "zh",
|
||
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId
|
||
)
|
||
XCTAssertTrue(prompt.contains("标题"))
|
||
XCTAssertTrue(prompt.contains("不要输出正文"))
|
||
XCTAssertTrue(prompt.contains("不要输出 NONE"))
|
||
}
|
||
}
|