feat(flow): implement ABCD session policy and host return whitelist

- Scheme A: on-demand session start, inactivity-based expiry, handoff auto-recording
- Scheme B: cold-start overlay with swipe guidance and return alert
- Scheme C+D: HostAppURLRegistry (20 apps) and sourceApplication capture
- Settings: skip app switch toggle and inactivity duration picker
- Add LSApplicationQueriesSchemes for canOpenURL checks

Co-authored-by: Rocky <hkgood@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-07-06 07:38:26 +00:00
parent 6489340b2e
commit 13cd7b30f9
25 changed files with 936 additions and 77 deletions
@@ -30,6 +30,8 @@ final class AppGroupConfigurationTests: XCTestCase {
XCTAssertTrue(config.cursorDragNavigationEnabled)
XCTAssertEqual(config.polishIntensity, .default)
XCTAssertTrue(config.personalDictionary.entries.isEmpty)
XCTAssertTrue(config.flowSkipAppSwitch)
XCTAssertEqual(config.flowInactivityDuration, .twelveHours)
}
func testSaveAndLoadRoundTrip() {
@@ -49,6 +51,8 @@ final class AppGroupConfigurationTests: XCTestCase {
config.handednessPreference = .right
config.cursorDragNavigationEnabled = false
config.polishIntensity = .light
config.flowSkipAppSwitch = false
config.flowInactivityDuration = .thirtyMinutes
config.save(to: defaults)
let loaded = AppGroupConfiguration.load(fromAvailable: defaults)
@@ -66,6 +70,8 @@ final class AppGroupConfigurationTests: XCTestCase {
XCTAssertEqual(loaded.handednessPreference, .right)
XCTAssertFalse(loaded.cursorDragNavigationEnabled)
XCTAssertEqual(loaded.polishIntensity, .light)
XCTAssertFalse(loaded.flowSkipAppSwitch)
XCTAssertEqual(loaded.flowInactivityDuration, .thirtyMinutes)
}
func testTranslationEnabledDerivedFromTargetLocale() {
@@ -0,0 +1,46 @@
// FlowSessionPolicyTests.swift
// OSGKeyboardTests
import XCTest
@testable import OSGKeyboardShared
final class FlowSessionPolicyTests: XCTestCase {
private func makeDefaults() -> UserDefaults {
let suite = "group.com.osgkeyboard.shared.tests.policy.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
defaults.removePersistentDomain(forName: suite)
return defaults
}
func testSkipAppSwitchDefaultsToTrue() {
let defaults = makeDefaults()
XCTAssertTrue(FlowSessionPolicy.skipAppSwitch(defaults: defaults))
}
func testInactivityDurationDefaultsToTwelveHours() {
let defaults = makeDefaults()
XCTAssertEqual(FlowSessionPolicy.inactivityDuration(defaults: defaults), .twelveHours)
XCTAssertEqual(FlowSessionPolicy.sessionDuration(defaults: defaults), 12 * 60 * 60)
}
func testTouchLastActivityExtendsExpiry() {
let defaults = makeDefaults()
defaults.set(FlowInactivityDuration.tenMinutes.rawValue, forKey: AppGroupConfiguration.Keys.flowInactivityDuration)
FlowSessionBridge.markSessionActive(defaults: defaults)
let staleExpiry = Date().timeIntervalSince1970 + 30
defaults.set(staleExpiry, forKey: FlowSessionKeys.flowSessionExpires)
FlowSessionBridge.touchLastActivity(defaults: defaults)
let refreshed = FlowSessionBridge.sessionExpiresAt(defaults: defaults) ?? 0
XCTAssertGreaterThan(refreshed, staleExpiry)
}
func testPendingHostBundleIdRoundTrip() {
let defaults = makeDefaults()
FlowSessionBridge.setPendingHostBundleId("com.tencent.xin", defaults: defaults)
XCTAssertEqual(FlowSessionBridge.pendingHostBundleId(defaults: defaults), "com.tencent.xin")
FlowSessionBridge.clearPendingHostBundleId(defaults: defaults)
XCTAssertNil(FlowSessionBridge.pendingHostBundleId(defaults: defaults))
}
}
@@ -0,0 +1,28 @@
// HostAppURLRegistryTests.swift
// OSGKeyboardTests
import XCTest
@testable import OSGKeyboardShared
final class HostAppURLRegistryTests: XCTestCase {
func testWeChatLookup() {
let entry = HostAppURLRegistry.lookup(bundleId: "com.tencent.xin")
XCTAssertEqual(entry?.returnURLString, "weixin://")
XCTAssertEqual(entry?.displayNameKey, "hostApp.wechat")
}
func testUnknownBundleReturnsNil() {
XCTAssertNil(HostAppURLRegistry.lookup(bundleId: "com.apple.MobileSMS"))
XCTAssertNil(HostAppURLRegistry.lookup(bundleId: nil))
XCTAssertNil(HostAppURLRegistry.lookup(bundleId: ""))
}
func testQuerySchemesIncludesWeChat() {
XCTAssertTrue(HostAppURLRegistry.querySchemes.contains("weixin"))
}
func testEntriesHaveUniqueBundleIds() {
let ids = HostAppURLRegistry.entries.map(\.bundleId)
XCTAssertEqual(Set(ids).count, ids.count)
}
}