feat(keyboard): harden clipboard command and add What's New sheet

Stabilize clipboard long-press prepare/resume across paste alerts and cold start, add an in-app release-notes sheet with remote bilingual HTML, localize typing input settings, and bump build to 55.
This commit is contained in:
Rocky
2026-08-08 00:20:42 +08:00
parent f197b68573
commit 9da32b81e9
45 changed files with 3670 additions and 422 deletions
@@ -0,0 +1,215 @@
// ClipboardCommandResumeTests.swift
// OSGKeyboardTests
import XCTest
@testable import OSGKeyboardShared
final class ClipboardCommandResumeTests: XCTestCase {
private var defaults: UserDefaults!
private var suiteName: String!
override func setUp() {
super.setUp()
suiteName = "ClipboardCommandResumeTests.\(UUID().uuidString)"
defaults = UserDefaults(suiteName: suiteName)
}
override func tearDown() {
if let suiteName {
defaults?.removePersistentDomain(forName: suiteName)
}
defaults = nil
suiteName = nil
super.tearDown()
}
func testMarkPreferVoiceSurvivesUntilCleared() {
XCTAssertFalse(ClipboardCommandResume.shouldPreferVoice(defaults: defaults))
ClipboardCommandResume.markPreferVoice(defaults: defaults)
XCTAssertTrue(ClipboardCommandResume.shouldPreferVoice(defaults: defaults))
ClipboardCommandResume.clear(defaults: defaults)
XCTAssertFalse(ClipboardCommandResume.shouldPreferVoice(defaults: defaults))
}
func testStoreSnapshotRoundTrip() {
ClipboardCommandResume.storeSnapshot("周末有空一起吃饭吗?", defaults: defaults)
XCTAssertTrue(ClipboardCommandResume.shouldPreferVoice(defaults: defaults))
XCTAssertEqual(
ClipboardCommandResume.pendingSnapshot(defaults: defaults),
"周末有空一起吃饭吗?"
)
}
/// Simulates extension jetsam: writer process flushes, reader process is new.
func testStickySurvivesNewUserDefaultsInstanceAfterSynchronize() {
let text = "是AI语音输入法,更是好输入法。It is an AI voice input method."
ClipboardCommandResume.markPreferVoice(defaults: defaults)
ClipboardCommandResume.storeSnapshot(text, defaults: defaults)
// New UserDefaults handle on the same suite new extension process.
let reopened = UserDefaults(suiteName: suiteName!)
XCTAssertNotNil(reopened)
guard let reopened else { return }
XCTAssertTrue(
ClipboardCommandResume.shouldPreferVoice(defaults: reopened),
"Recreated process must still prefer voice after paste-alert jetsam"
)
XCTAssertEqual(
ClipboardCommandResume.pendingSnapshot(defaults: reopened),
text
)
// And the open-surface policy must then force voice over default typing.
XCTAssertEqual(
KeyboardOpenSurfacePolicy.resolve(
locksTypingSurface: false,
clipboardCommandActive: false,
stickyPreferVoice: ClipboardCommandResume.shouldPreferVoice(defaults: reopened),
preferred: .typing
),
.voice
)
}
func testOpenSurfacePolicyForcesVoiceWhenStickyEvenIfDefaultTyping() {
let resolved = KeyboardOpenSurfacePolicy.resolve(
locksTypingSurface: false,
clipboardCommandActive: false,
stickyPreferVoice: true,
preferred: .typing
)
XCTAssertEqual(resolved, .voice)
}
func testOpenSurfacePolicyHonorsDefaultTypingWhenNoSticky() {
let resolved = KeyboardOpenSurfacePolicy.resolve(
locksTypingSurface: false,
clipboardCommandActive: false,
stickyPreferVoice: false,
preferred: .typing
)
XCTAssertEqual(resolved, .typing)
}
func testOpenSurfacePolicyClipboardActiveBeatsTypingDefault() {
let resolved = KeyboardOpenSurfacePolicy.resolve(
locksTypingSurface: false,
clipboardCommandActive: true,
stickyPreferVoice: false,
preferred: .typing
)
XCTAssertEqual(resolved, .voice)
}
func testPasteAlertReopenDecisionMatrix() {
// Exact bug from device: default typing + sticky after Allow Paste.
XCTAssertEqual(
KeyboardOpenSurfacePolicy.resolve(
locksTypingSurface: false,
clipboardCommandActive: false,
stickyPreferVoice: true,
preferred: .typing
),
.voice,
"Allow Paste reopen must not land on default Chinese typing grid"
)
// Recording lock also forces voice.
XCTAssertEqual(
KeyboardOpenSurfacePolicy.resolve(
locksTypingSurface: true,
clipboardCommandActive: false,
stickyPreferVoice: false,
preferred: .typing
),
.voice
)
}
func testMarkStartIssuedBlocksDuplicateRoundClaim() {
let id = UUID()
XCTAssertFalse(ClipboardCommandResume.hasStartIssued(defaults: defaults))
ClipboardCommandResume.markStartIssued(id, defaults: defaults)
XCTAssertTrue(ClipboardCommandResume.hasStartIssued(defaults: defaults))
XCTAssertEqual(ClipboardCommandResume.startIssuedUtteranceId(defaults: defaults), id)
XCTAssertTrue(ClipboardCommandResume.shouldPreferVoice(defaults: defaults))
let reopened = UserDefaults(suiteName: suiteName!)
XCTAssertEqual(
ClipboardCommandResume.startIssuedUtteranceId(defaults: reopened),
id,
"Recreated extension must see the already-issued start and not send another"
)
ClipboardCommandResume.clear(defaults: defaults)
XCTAssertFalse(ClipboardCommandResume.hasStartIssued(defaults: defaults))
XCTAssertNil(ClipboardCommandResume.startIssuedUtteranceId(defaults: reopened))
}
func testClaimThenClearSurvivesTwentyAlternatingRounds() {
for round in 1...20 {
let id = UUID()
ClipboardCommandResume.storeSnapshot("材料-\(round)", defaults: defaults)
ClipboardCommandResume.markStartIssued(id, defaults: defaults)
let reader = UserDefaults(suiteName: suiteName!)
XCTAssertEqual(
ClipboardCommandResume.startIssuedUtteranceId(defaults: reader),
id,
"round \(round) claim must survive new defaults handle"
)
XCTAssertEqual(
KeyboardOpenSurfacePolicy.resolve(
locksTypingSurface: false,
clipboardCommandActive: false,
stickyPreferVoice: ClipboardCommandResume.shouldPreferVoice(defaults: reader),
preferred: .typing
),
.voice,
"round \(round) must stay on voice after paste-alert recreate"
)
XCTAssertEqual(
ClipboardPreparingPolicy.restoreAction(
hasStartIssued: ClipboardCommandResume.hasStartIssued(defaults: reader),
phase: .idle
),
.awaitExistingStart,
"round \(round) must not pressBegan again"
)
ClipboardCommandResume.clear(defaults: defaults)
XCTAssertFalse(
ClipboardCommandResume.hasStartIssued(defaults: reader),
"round \(round) clear must drop claim for next independent long-press"
)
}
}
func testPreparingTimeoutConstantIsPositive() {
XCTAssertGreaterThan(ClipboardCommandResume.preparingTimeout, 1)
XCTAssertLessThanOrEqual(ClipboardCommandResume.preparingTimeout, 15)
}
func testColdStartStickyRestoreIsPreferVoiceOnlyWithoutClaim() {
ClipboardCommandResume.storeSnapshot("冻结材料", defaults: defaults)
XCTAssertFalse(ClipboardCommandResume.hasStartIssued(defaults: defaults))
XCTAssertEqual(
ClipboardPreparingPolicy.restoreAction(
hasStartIssued: false,
phase: .idle
),
.preferVoiceOnly
)
XCTAssertEqual(
KeyboardOpenSurfacePolicy.resolve(
locksTypingSurface: false,
clipboardCommandActive: false,
stickyPreferVoice: ClipboardCommandResume.shouldPreferVoice(defaults: defaults),
preferred: .typing
),
.voice,
"Default typing keyboard must still open voice after cold-start sticky"
)
}
}