feat(keyboard): add English QuickType bar and system lexicon

Show verbatim/correction/completion slots, mmap a 40k-word list, and
use UITextChecker plus supplementary lexicon for conservative autocorrect.
This commit is contained in:
Rocky
2026-08-14 21:49:22 +08:00
parent 2c3a3f80f3
commit 4749a9cbf2
34 changed files with 45539 additions and 3395 deletions
+121
View File
@@ -62,6 +62,86 @@ final class AIUserSkillTests: XCTestCase {
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",
@@ -186,4 +266,45 @@ final class AIUserSkillStoreTests: XCTestCase {
XCTAssertFalse(store.layout.hasConfirmedShortcut(skill.id))
XCTAssertFalse(store.layout.isEnabled(skill.id))
}
func testRemovingShortcutLinkKeepsEnabledTextSkill() throws {
let store = AIAgentSkillLayoutStore(defaults: makeDefaults())
var skill = AIUserSkill(
name: "Custom",
prompt: "Do it",
shortcutICloudURL: URL(
string: "https://www.icloud.com/shortcuts/65bf33ba4206484ba78d582eaf1e9c44"
),
shortcutName: "Run Me"
)
try store.saveUserSkill(skill)
XCTAssertEqual(store.confirmShortcutAndEnable(skill.id), .enabled)
skill.shortcutICloudURL = nil
try store.saveUserSkill(skill)
XCTAssertTrue(store.layout.isEnabled(skill.id))
XCTAssertFalse(store.layout.hasConfirmedShortcut(skill.id))
XCTAssertEqual(store.userSkill(id: skill.id)?.asClipboardSkill().kind, .transform)
}
func testAddingShortcutLinkDisablesTextSkillUntilConfirmed() throws {
let store = AIAgentSkillLayoutStore(defaults: makeDefaults())
var skill = AIUserSkill(
name: "Custom",
prompt: "Do it"
)
try store.saveUserSkill(skill)
XCTAssertEqual(store.enable(skill.id), .enabled)
skill.shortcutICloudURL = URL(
string: "https://www.icloud.com/shortcuts/65bf33ba4206484ba78d582eaf1e9c44"
)
skill.shortcutName = "Run Me"
try store.saveUserSkill(skill)
XCTAssertFalse(store.layout.isEnabled(skill.id))
XCTAssertFalse(store.layout.hasConfirmedShortcut(skill.id))
XCTAssertEqual(store.enable(skill.id), .needsShortcut)
}
}
@@ -0,0 +1,77 @@
// EnglishTypingOnDeviceTests.swift
// OSGKeyboardTests
//
// Hosted in the main app so these can run on a physical iPhone.
// ExtTests stay tool-hosted (simulator-only).
import XCTest
@testable import OSGKeyboardShared
final class EnglishTypingOnDeviceTests: XCTestCase {
func testLexiconLoadsFortyThousandWords() {
let lexicon = EnglishLexicon()
lexicon.prepare()
XCTAssertTrue(lexicon.isLoaded)
XCTAssertGreaterThanOrEqual(lexicon.wordCount, 30_000)
XCTAssertTrue(lexicon.contains("the"))
XCTAssertTrue(lexicon.contains("hello"))
XCTAssertTrue(lexicon.contains("definitely"))
lexicon.unload()
XCTAssertFalse(lexicon.isLoaded)
}
func testTehAutocorrectsToThe() {
let engine = EnglishSuggestionEngine()
engine.prepare()
XCTAssertEqual(
engine.correctionDecision(for: "teh", personalTerms: [], learnedBoosts: [:])?.replacement,
"the"
)
}
func testTitleCaseNamesStay() {
let engine = EnglishSuggestionEngine()
engine.prepare()
XCTAssertNil(engine.correctionDecision(for: "Rocky", personalTerms: [], learnedBoosts: [:]))
XCTAssertNil(engine.correctionDecision(for: "Wang", personalTerms: [], learnedBoosts: [:]))
}
func testProximityGppdBecomesGood() {
let engine = EnglishSuggestionEngine()
engine.prepare()
XCTAssertEqual(
engine.correctionDecision(for: "gppd", personalTerms: [], learnedBoosts: [:])?.replacement,
"good"
)
}
func testFormIsNotCorrectedToFrom() {
let engine = EnglishSuggestionEngine()
engine.prepare()
XCTAssertNil(engine.correctionDecision(for: "form", personalTerms: [], learnedBoosts: [:]))
}
func testThankYouBigram() {
let lexicon = EnglishLexicon()
lexicon.prepare()
XCTAssertTrue(lexicon.nextWords(after: "thank").contains("you"))
}
@MainActor
func testUITextCheckerCompletionsAvailable() {
let system = UIKitEnglishSystemLexicon()
let hits = system.completions(prefix: "hel", limit: 6)
XCTAssertFalse(hits.isEmpty, "device UITextChecker should complete hel")
}
@MainActor
func testQuickTypeBoardMarksCorrection() {
let engine = EnglishSuggestionEngine()
engine.prepare()
let composition = engine.compositionWhileTyping(
EnglishSuggestionContext(currentWord: "teh")
)
XCTAssertEqual(composition.candidates.first?.role, .verbatim)
XCTAssertTrue(composition.candidates.contains { $0.role == .correction && $0.text == "the" })
}
}