refactor(flow): remove Live Activity and keep-alive mode picker
Keep voice sessions on silent low-profile PiP only, and fix custom polish style editor presentation via sheet(item:).
This commit is contained in:
@@ -34,22 +34,9 @@ final class AppGroupConfigurationTests: XCTestCase {
|
||||
XCTAssertEqual(config.polishIntensity, .light)
|
||||
XCTAssertTrue(config.personalDictionary.entries.isEmpty)
|
||||
XCTAssertTrue(config.flowSkipAppSwitch)
|
||||
XCTAssertEqual(config.flowKeepAliveMode, .pictureInPicture)
|
||||
XCTAssertEqual(config.flowInactivityDuration, .fiveMinutes)
|
||||
}
|
||||
|
||||
func testLoadPreservesStoredLiveActivityKeepAliveMode() {
|
||||
let defaults = makeDefaults()
|
||||
defaults.set(
|
||||
FlowKeepAliveMode.liveActivity.rawValue,
|
||||
forKey: AppGroupConfiguration.Keys.flowKeepAliveMode
|
||||
)
|
||||
|
||||
let config = AppGroupConfiguration.load(fromAvailable: defaults)
|
||||
|
||||
XCTAssertEqual(config.flowKeepAliveMode, .liveActivity)
|
||||
}
|
||||
|
||||
func testSaveAndLoadRoundTrip() {
|
||||
let defaults = makeDefaults()
|
||||
var config = AppGroupConfiguration.load(fromAvailable: defaults)
|
||||
@@ -73,7 +60,6 @@ final class AppGroupConfigurationTests: XCTestCase {
|
||||
config.keyboardHapticIntensity = .strong
|
||||
config.polishIntensity = .heavy
|
||||
config.flowSkipAppSwitch = false
|
||||
config.flowKeepAliveMode = .pictureInPicture
|
||||
// Use a non-default value so the round-trip actually proves persistence.
|
||||
config.flowInactivityDuration = .threeHours
|
||||
config.save(to: defaults)
|
||||
@@ -98,7 +84,6 @@ final class AppGroupConfigurationTests: XCTestCase {
|
||||
XCTAssertEqual(loaded.keyboardHapticIntensity, .strong)
|
||||
XCTAssertEqual(loaded.polishIntensity, .heavy)
|
||||
XCTAssertFalse(loaded.flowSkipAppSwitch)
|
||||
XCTAssertEqual(loaded.flowKeepAliveMode, .pictureInPicture)
|
||||
XCTAssertEqual(loaded.flowInactivityDuration, .threeHours)
|
||||
}
|
||||
|
||||
|
||||
@@ -294,7 +294,7 @@ final class FlowHandoffPolicyTests: XCTestCase {
|
||||
defaults.removePersistentDomain(forName: suite)
|
||||
|
||||
let sessionId = UUID()
|
||||
FlowSessionBridge.markSessionActive(duration: 1_800, sessionId: sessionId, defaults: defaults)
|
||||
FlowSessionBridge.markSessionActivePersistent(sessionId: sessionId, defaults: defaults)
|
||||
FlowSessionBridge.writeReadySnapshot(
|
||||
FlowReadySnapshot(
|
||||
sessionId: sessionId,
|
||||
@@ -303,7 +303,7 @@ final class FlowHandoffPolicyTests: XCTestCase {
|
||||
engineMode: "local",
|
||||
localeId: "zh-Hans",
|
||||
busyUtteranceId: UUID(),
|
||||
sessionExpiresAt: FlowSessionBridge.sessionExpiresAt(defaults: defaults),
|
||||
sessionExpiresAt: nil,
|
||||
hostGeneration: FlowSessionBridge.currentHostGeneration(defaults: defaults)
|
||||
),
|
||||
defaults: defaults
|
||||
|
||||
@@ -15,7 +15,7 @@ final class FlowSessionBridgeTests: XCTestCase {
|
||||
|
||||
func testSessionActiveSurvivesStaleHeartbeatWhileNotExpired() {
|
||||
let defaults = makeDefaults()
|
||||
FlowSessionBridge.markSessionActive(duration: 60, defaults: defaults)
|
||||
FlowSessionBridge.markSessionActivePersistent(defaults: defaults)
|
||||
XCTAssertTrue(FlowSessionBridge.isSessionActive(defaults: defaults))
|
||||
XCTAssertTrue(FlowSessionBridge.isHostReachable(defaults: defaults))
|
||||
|
||||
@@ -28,7 +28,7 @@ final class FlowSessionBridgeTests: XCTestCase {
|
||||
|
||||
func testHostStaleWhenHeartbeatVeryOld() {
|
||||
let defaults = makeDefaults()
|
||||
FlowSessionBridge.markSessionActive(duration: 3_600, defaults: defaults)
|
||||
FlowSessionBridge.markSessionActivePersistent(defaults: defaults)
|
||||
let zombieHeartbeat = Date().timeIntervalSince1970 - 120
|
||||
defaults.set(zombieHeartbeat, forKey: FlowSessionKeys.flowHeartbeat)
|
||||
|
||||
@@ -39,7 +39,7 @@ final class FlowSessionBridgeTests: XCTestCase {
|
||||
|
||||
func testClearIfHostStaleRemovesZombieSession() {
|
||||
let defaults = makeDefaults()
|
||||
FlowSessionBridge.markSessionActive(duration: 3_600, defaults: defaults)
|
||||
FlowSessionBridge.markSessionActivePersistent(defaults: defaults)
|
||||
FlowSessionBridge.setRecordingState(.stopped, defaults: defaults)
|
||||
let zombieHeartbeat = Date().timeIntervalSince1970 - 120
|
||||
defaults.set(zombieHeartbeat, forKey: FlowSessionKeys.flowHeartbeat)
|
||||
@@ -59,18 +59,13 @@ final class FlowSessionBridgeTests: XCTestCase {
|
||||
XCTAssertFalse(FlowSessionBridge.isSessionActive(defaults: defaults))
|
||||
}
|
||||
|
||||
func testSessionInactiveWhenExpired() {
|
||||
func testPersistentSessionIgnoresLegacyExpiry() {
|
||||
let defaults = makeDefaults()
|
||||
// Expiry only applies on the Live Activity keep-alive path (PiP is persistent).
|
||||
defaults.set(
|
||||
FlowKeepAliveMode.liveActivity.rawValue,
|
||||
forKey: AppGroupConfiguration.Keys.flowKeepAliveMode
|
||||
)
|
||||
FlowSessionBridge.markSessionActive(duration: 1, defaults: defaults)
|
||||
FlowSessionBridge.markSessionActivePersistent(defaults: defaults)
|
||||
let expired = Date().timeIntervalSince1970 - 5
|
||||
defaults.set(expired, forKey: FlowSessionKeys.flowSessionExpires)
|
||||
XCTAssertFalse(FlowSessionBridge.isSessionActive(defaults: defaults))
|
||||
XCTAssertFalse(FlowSessionBridge.isHostReachable(defaults: defaults))
|
||||
XCTAssertTrue(FlowSessionBridge.isSessionActive(defaults: defaults))
|
||||
XCTAssertTrue(FlowSessionBridge.isHostReachable(defaults: defaults))
|
||||
}
|
||||
|
||||
func testRecordingStateRoundTrip() {
|
||||
@@ -104,7 +99,7 @@ final class FlowSessionBridgeTests: XCTestCase {
|
||||
|
||||
func testClearFlowStateRemovesSessionKeys() {
|
||||
let defaults = makeDefaults()
|
||||
FlowSessionBridge.markSessionActive(defaults: defaults)
|
||||
FlowSessionBridge.markSessionActivePersistent(defaults: defaults)
|
||||
FlowSessionBridge.storeTranscriptionResult("x", defaults: defaults)
|
||||
FlowSessionBridge.clearFlowState(defaults: defaults)
|
||||
|
||||
@@ -113,18 +108,11 @@ final class FlowSessionBridgeTests: XCTestCase {
|
||||
XCTAssertEqual(FlowSessionBridge.recordingState(defaults: defaults), .idle)
|
||||
}
|
||||
|
||||
func testRemainingSessionDurationNilWhenExpired() {
|
||||
func testPersistentActivationClearsLegacyExpiry() {
|
||||
let defaults = makeDefaults()
|
||||
defaults.set(
|
||||
FlowKeepAliveMode.liveActivity.rawValue,
|
||||
forKey: AppGroupConfiguration.Keys.flowKeepAliveMode
|
||||
)
|
||||
FlowSessionBridge.markSessionActive(duration: 1, defaults: defaults)
|
||||
XCTAssertNotNil(FlowSessionBridge.remainingSessionDuration(defaults: defaults))
|
||||
|
||||
let expired = Date().timeIntervalSince1970 - 5
|
||||
defaults.set(expired, forKey: FlowSessionKeys.flowSessionExpires)
|
||||
XCTAssertNil(FlowSessionBridge.remainingSessionDuration(defaults: defaults))
|
||||
defaults.set(Date().timeIntervalSince1970 + 60, forKey: FlowSessionKeys.flowSessionExpires)
|
||||
FlowSessionBridge.markSessionActivePersistent(defaults: defaults)
|
||||
XCTAssertNil(defaults.object(forKey: FlowSessionKeys.flowSessionExpires))
|
||||
}
|
||||
|
||||
func testConsumeTranscriptionErrorIncludesKind() {
|
||||
@@ -156,7 +144,7 @@ final class FlowSessionBridgeTests: XCTestCase {
|
||||
|
||||
func testHostReadyRequiresExplicitContract() {
|
||||
let defaults = makeDefaults()
|
||||
FlowSessionBridge.markSessionActive(duration: 60, defaults: defaults)
|
||||
FlowSessionBridge.markSessionActivePersistent(defaults: defaults)
|
||||
XCTAssertTrue(FlowSessionBridge.isHostReachable(defaults: defaults))
|
||||
XCTAssertFalse(FlowSessionBridge.isHostReady(defaults: defaults))
|
||||
|
||||
@@ -166,7 +154,7 @@ final class FlowSessionBridgeTests: XCTestCase {
|
||||
|
||||
func testHostReadyFalseWhenHeartbeatStale() {
|
||||
let defaults = makeDefaults()
|
||||
FlowSessionBridge.markSessionActive(duration: 3_600, defaults: defaults)
|
||||
FlowSessionBridge.markSessionActivePersistent(defaults: defaults)
|
||||
FlowSessionBridge.setHostReady(true, defaults: defaults)
|
||||
XCTAssertTrue(FlowSessionBridge.isHostReady(defaults: defaults))
|
||||
|
||||
@@ -177,7 +165,7 @@ final class FlowSessionBridgeTests: XCTestCase {
|
||||
|
||||
func testHeartbeatRefreshKeepsHostReadyPublished() {
|
||||
let defaults = makeDefaults()
|
||||
FlowSessionBridge.markSessionActive(duration: 3_600, defaults: defaults)
|
||||
FlowSessionBridge.markSessionActivePersistent(defaults: defaults)
|
||||
FlowSessionBridge.setHostReady(true, defaults: defaults)
|
||||
|
||||
FlowSessionBridge.writeHeartbeat(defaults: defaults)
|
||||
@@ -187,7 +175,7 @@ final class FlowSessionBridgeTests: XCTestCase {
|
||||
|
||||
func testClearFlowStateClearsHostReady() {
|
||||
let defaults = makeDefaults()
|
||||
FlowSessionBridge.markSessionActive(defaults: defaults)
|
||||
FlowSessionBridge.markSessionActivePersistent(defaults: defaults)
|
||||
FlowSessionBridge.setHostReady(true, defaults: defaults)
|
||||
FlowSessionBridge.clearFlowState(defaults: defaults)
|
||||
XCTAssertFalse(defaults.bool(forKey: FlowSessionKeys.flowHostReady))
|
||||
@@ -305,7 +293,7 @@ final class FlowSessionBridgeTests: XCTestCase {
|
||||
|
||||
func testNotReadySnapshotDoesNotRefreshHeartbeat() {
|
||||
let defaults = makeDefaults()
|
||||
FlowSessionBridge.markSessionActive(duration: 3_600, defaults: defaults)
|
||||
FlowSessionBridge.markSessionActivePersistent(defaults: defaults)
|
||||
let zombieHeartbeat = Date().timeIntervalSince1970 - 120
|
||||
defaults.set(zombieHeartbeat, forKey: FlowSessionKeys.flowHeartbeat)
|
||||
|
||||
@@ -328,7 +316,7 @@ final class FlowSessionBridgeTests: XCTestCase {
|
||||
|
||||
func testBusySnapshotStillRefreshesHeartbeat() {
|
||||
let defaults = makeDefaults()
|
||||
FlowSessionBridge.markSessionActive(duration: 3_600, defaults: defaults)
|
||||
FlowSessionBridge.markSessionActivePersistent(defaults: defaults)
|
||||
let staleHeartbeat = Date().timeIntervalSince1970 - 10
|
||||
defaults.set(staleHeartbeat, forKey: FlowSessionKeys.flowHeartbeat)
|
||||
|
||||
@@ -359,7 +347,7 @@ final class FlowSessionBridgeTests: XCTestCase {
|
||||
|
||||
func testNotReadyStartingSnapshotIsRetainedWithoutRevivingHeartbeat() {
|
||||
let defaults = makeDefaults()
|
||||
FlowSessionBridge.markSessionActive(duration: 3_600, defaults: defaults)
|
||||
FlowSessionBridge.markSessionActivePersistent(defaults: defaults)
|
||||
let zombieHeartbeat = Date().timeIntervalSince1970 - 120
|
||||
defaults.set(zombieHeartbeat, forKey: FlowSessionKeys.flowHeartbeat)
|
||||
|
||||
@@ -387,7 +375,7 @@ final class FlowSessionBridgeTests: XCTestCase {
|
||||
let now = Date().timeIntervalSince1970
|
||||
FlowSessionBridge.rotateHostGeneration(defaults: defaults)
|
||||
let liveGeneration = FlowSessionBridge.currentHostGeneration(defaults: defaults)
|
||||
FlowSessionBridge.markSessionActive(duration: 60, sessionId: sessionId, defaults: defaults)
|
||||
FlowSessionBridge.markSessionActivePersistent(sessionId: sessionId, defaults: defaults)
|
||||
FlowSessionBridge.writeReadySnapshot(
|
||||
FlowReadySnapshot(
|
||||
sessionId: sessionId,
|
||||
@@ -412,7 +400,7 @@ final class FlowSessionBridgeTests: XCTestCase {
|
||||
|
||||
func testClearFlowStateOnHostLaunchPreservesPendingHost() {
|
||||
let defaults = makeDefaults()
|
||||
FlowSessionBridge.markSessionActive(duration: 3_600, defaults: defaults)
|
||||
FlowSessionBridge.markSessionActivePersistent(defaults: defaults)
|
||||
FlowSessionBridge.setHostReady(true, defaults: defaults)
|
||||
FlowSessionBridge.setPendingHostBundleId("com.example.host", defaults: defaults)
|
||||
|
||||
@@ -478,7 +466,7 @@ final class FlowSessionBridgeTests: XCTestCase {
|
||||
let defaults = makeDefaults()
|
||||
let sessionId = UUID()
|
||||
let now = Date().timeIntervalSince1970
|
||||
FlowSessionBridge.markSessionActive(duration: 60, sessionId: sessionId, defaults: defaults)
|
||||
FlowSessionBridge.markSessionActivePersistent(sessionId: sessionId, defaults: defaults)
|
||||
let snapshot = FlowReadySnapshot(
|
||||
sessionId: sessionId,
|
||||
ready: true,
|
||||
@@ -501,7 +489,7 @@ final class FlowSessionBridgeTests: XCTestCase {
|
||||
let defaults = makeDefaults()
|
||||
let sessionId = UUID()
|
||||
let now = Date().timeIntervalSince1970
|
||||
FlowSessionBridge.markSessionActive(duration: 60, sessionId: sessionId, defaults: defaults)
|
||||
FlowSessionBridge.markSessionActivePersistent(sessionId: sessionId, defaults: defaults)
|
||||
let skewed = FlowReadySnapshot(
|
||||
sessionId: sessionId,
|
||||
ready: true,
|
||||
|
||||
@@ -29,40 +29,20 @@ final class FlowSessionPolicyTests: XCTestCase {
|
||||
XCTAssertEqual(FlowInactivityDuration.tenMinutes.timeInterval, 10 * 60)
|
||||
}
|
||||
|
||||
func testKeepAliveModeDefaultsToPictureInPicture() {
|
||||
let defaults = makeDefaults()
|
||||
XCTAssertEqual(FlowSessionPolicy.keepAliveMode(defaults: defaults), .pictureInPicture)
|
||||
XCTAssertFalse(FlowSessionPolicy.usesInactivityExpiry(defaults: defaults))
|
||||
}
|
||||
|
||||
func testPiPSessionHasNoInactivityExpiry() {
|
||||
let defaults = makeDefaults()
|
||||
defaults.set(FlowKeepAliveMode.pictureInPicture.rawValue,
|
||||
forKey: AppGroupConfiguration.Keys.flowKeepAliveMode)
|
||||
FlowSessionBridge.markSessionActive(sessionId: UUID(), defaults: defaults)
|
||||
FlowSessionBridge.markSessionActivePersistent(sessionId: UUID(), defaults: defaults)
|
||||
|
||||
XCTAssertTrue(FlowSessionBridge.isSessionActive(defaults: defaults))
|
||||
XCTAssertNil(FlowSessionBridge.sessionExpiresAt(defaults: defaults))
|
||||
|
||||
FlowSessionBridge.touchLastActivity(defaults: defaults)
|
||||
XCTAssertNil(FlowSessionBridge.sessionExpiresAt(defaults: defaults))
|
||||
XCTAssertNil(defaults.object(forKey: FlowSessionKeys.flowSessionExpires))
|
||||
}
|
||||
|
||||
func testTouchLastActivityExtendsExpiry() {
|
||||
func testLegacyExpiryDoesNotInvalidatePersistentSession() {
|
||||
let defaults = makeDefaults()
|
||||
defaults.set(
|
||||
FlowKeepAliveMode.liveActivity.rawValue,
|
||||
forKey: AppGroupConfiguration.Keys.flowKeepAliveMode
|
||||
)
|
||||
defaults.set(FlowInactivityDuration.tenMinutes.rawValue, forKey: AppGroupConfiguration.Keys.flowInactivityDuration)
|
||||
FlowSessionBridge.markSessionActive(defaults: defaults)
|
||||
FlowSessionBridge.markSessionActivePersistent(defaults: defaults)
|
||||
defaults.set(Date().timeIntervalSince1970 - 30, forKey: FlowSessionKeys.flowSessionExpires)
|
||||
|
||||
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)
|
||||
XCTAssertTrue(FlowSessionBridge.isSessionActive(defaults: defaults))
|
||||
}
|
||||
|
||||
func testPendingHostBundleIdRoundTrip() {
|
||||
|
||||
@@ -64,7 +64,6 @@ final class SettingsCloudSyncTests: XCTestCase {
|
||||
activePolishStyleId: SyncedField(value: "builtin.light", updatedAt: stampA, deviceID: deviceA),
|
||||
llmThinkingEnabled: SyncedField(value: false, updatedAt: stampA, deviceID: deviceA),
|
||||
flowSkipAppSwitch: SyncedField(value: true, updatedAt: stampA, deviceID: deviceA),
|
||||
flowKeepAliveMode: SyncedField(value: .liveActivity, updatedAt: stampA, deviceID: deviceA),
|
||||
flowInactivityDuration: SyncedField(value: .twelveHours, updatedAt: stampA, deviceID: deviceA)
|
||||
)
|
||||
let remote = SyncedAppSettingsV2(
|
||||
@@ -87,7 +86,6 @@ final class SettingsCloudSyncTests: XCTestCase {
|
||||
activePolishStyleId: SyncedField(value: "builtin.formal", updatedAt: stampB, deviceID: deviceB),
|
||||
llmThinkingEnabled: SyncedField(value: true, updatedAt: stampB, deviceID: deviceB),
|
||||
flowSkipAppSwitch: SyncedField(value: false, updatedAt: stampB, deviceID: deviceB),
|
||||
flowKeepAliveMode: SyncedField(value: .pictureInPicture, updatedAt: stampB, deviceID: deviceB),
|
||||
flowInactivityDuration: SyncedField(value: .threeHours, updatedAt: stampB, deviceID: deviceB)
|
||||
)
|
||||
|
||||
@@ -100,6 +98,31 @@ final class SettingsCloudSyncTests: XCTestCase {
|
||||
XCTAssertEqual(merged.polishIntensity.value, .heavy)
|
||||
}
|
||||
|
||||
func testLegacyKeepAliveFieldDecodesButIsNotReencoded() throws {
|
||||
let payload = SyncedAppSettingsV2.seeded(
|
||||
from: AppGroupConfiguration.load(fromAvailable: defaults),
|
||||
deviceID: deviceA,
|
||||
updatedAt: Date(timeIntervalSince1970: 100)
|
||||
)
|
||||
let encoder = JSONEncoder()
|
||||
var object = try XCTUnwrap(
|
||||
JSONSerialization.jsonObject(with: encoder.encode(payload)) as? [String: Any]
|
||||
)
|
||||
object["flowKeepAliveMode"] = [
|
||||
"value": "liveActivity",
|
||||
"updatedAt": 0,
|
||||
"deviceID": "legacy-device",
|
||||
]
|
||||
let legacyData = try JSONSerialization.data(withJSONObject: object)
|
||||
|
||||
let decoded = try JSONDecoder().decode(SyncedAppSettingsV2.self, from: legacyData)
|
||||
let reencodedObject = try XCTUnwrap(
|
||||
JSONSerialization.jsonObject(with: encoder.encode(decoded)) as? [String: Any]
|
||||
)
|
||||
|
||||
XCTAssertNil(reencodedObject["flowKeepAliveMode"])
|
||||
}
|
||||
|
||||
func testLegacyV1PullDoesNotClearKeychain() async throws {
|
||||
try Keychain.setAPIKey("sk-local-openai", for: "openai", useICloudSync: false)
|
||||
store.setSettingsICloudSyncEnabled(true)
|
||||
|
||||
Reference in New Issue
Block a user