chore(release): bump version to 2.1.6 (build 131)
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
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
// DebugAccountSessionHandoffTests.swift
|
||||
// OSGKeyboardTests
|
||||
//
|
||||
// DEBUG-only device → Simulator session handoff: the device must forget the
|
||||
// session it hands off, and the Simulator must never keep the handoff file.
|
||||
|
||||
#if DEBUG
|
||||
@testable import OSGKeyboard
|
||||
@testable import OSGKeyboardHostSupport
|
||||
import XCTest
|
||||
|
||||
final class DebugAccountSessionHandoffTests: XCTestCase {
|
||||
private var directory: URL!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
directory = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
try? FileManager.default.removeItem(at: directory)
|
||||
}
|
||||
|
||||
// MARK: - AccountAPIClient
|
||||
|
||||
func testRelinquishReturnsSessionAndForgetsItEverywhere() async throws {
|
||||
let session = makeAccountSession()
|
||||
let store = InMemoryAccountSecurityStore(session: session)
|
||||
let client = makeClient(store: store)
|
||||
// Warm the in-memory cache: clearing Keychain alone would not stop it.
|
||||
_ = try await client.currentSession()
|
||||
|
||||
let relinquished = try await client.debugRelinquishSession()
|
||||
|
||||
XCTAssertEqual(relinquished, session)
|
||||
let stored = await store.session
|
||||
XCTAssertNil(stored)
|
||||
let current = try await client.currentSession()
|
||||
XCTAssertNil(current)
|
||||
}
|
||||
|
||||
func testRelinquishWithoutSessionThrows() async {
|
||||
let client = makeClient(store: InMemoryAccountSecurityStore())
|
||||
|
||||
do {
|
||||
_ = try await client.debugRelinquishSession()
|
||||
XCTFail("Expected sessionUnavailable")
|
||||
} catch {
|
||||
XCTAssertEqual(error as? AccountAPIError, .sessionUnavailable)
|
||||
}
|
||||
}
|
||||
|
||||
func testAdoptReplacesCachedSession() async throws {
|
||||
let store = InMemoryAccountSecurityStore(session: makeAccountSession())
|
||||
let client = makeClient(store: store)
|
||||
_ = try await client.currentSession()
|
||||
let handedOff = makeAccountSession(accessToken: "access-device", refreshToken: "refresh-device")
|
||||
|
||||
try await client.debugAdoptSession(handedOff)
|
||||
|
||||
let current = try await client.currentSession()
|
||||
XCTAssertEqual(current, handedOff)
|
||||
let stored = await store.session
|
||||
XCTAssertEqual(stored, handedOff)
|
||||
}
|
||||
|
||||
// MARK: - Handoff file
|
||||
|
||||
func testExportFileRoundTripsThroughImportAndIsDeleted() throws {
|
||||
let session = makeAccountSession()
|
||||
let url = try DebugAccountSessionHandoff.writeExportFile(session, directory: directory)
|
||||
|
||||
let imported = DebugAccountSessionHandoff.consumePendingImport(directory: directory)
|
||||
|
||||
XCTAssertEqual(imported, session)
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: url.path))
|
||||
}
|
||||
|
||||
func testMalformedImportIsRejectedAndStillDeleted() throws {
|
||||
let url = directory.appendingPathComponent(DebugAccountSessionHandoff.fileName)
|
||||
try Data(#"{"format":"something-else","session":{}}"#.utf8).write(to: url)
|
||||
|
||||
XCTAssertNil(DebugAccountSessionHandoff.consumePendingImport(directory: directory))
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: url.path))
|
||||
}
|
||||
|
||||
func testParkedSessionIsPendingUntilConsumed() throws {
|
||||
XCTAssertFalse(DebugAccountSessionHandoff.hasPendingImport(directory: directory))
|
||||
_ = try DebugAccountSessionHandoff.writeExportFile(makeAccountSession(), directory: directory)
|
||||
|
||||
XCTAssertTrue(DebugAccountSessionHandoff.hasPendingImport(directory: directory))
|
||||
_ = DebugAccountSessionHandoff.consumePendingImport(directory: directory)
|
||||
XCTAssertFalse(DebugAccountSessionHandoff.hasPendingImport(directory: directory))
|
||||
}
|
||||
|
||||
func testMissingImportReturnsNil() {
|
||||
XCTAssertNil(DebugAccountSessionHandoff.consumePendingImport(directory: directory))
|
||||
}
|
||||
|
||||
func testImportedMarkerMatchesOnlyTheImportedAccount() throws {
|
||||
let suite = "DebugAccountSessionHandoffTests.\(UUID().uuidString)"
|
||||
let defaults = try XCTUnwrap(UserDefaults(suiteName: suite))
|
||||
defer { defaults.removePersistentDomain(forName: suite) }
|
||||
let imported = makeAccountSession().accountId
|
||||
|
||||
DebugAccountSessionHandoff.markImported(accountID: imported, defaults: defaults)
|
||||
|
||||
XCTAssertTrue(DebugAccountSessionHandoff.isImported(accountID: imported, defaults: defaults))
|
||||
XCTAssertFalse(DebugAccountSessionHandoff.isImported(accountID: UUID(), defaults: defaults))
|
||||
}
|
||||
|
||||
private func makeClient(store: InMemoryAccountSecurityStore) -> AccountAPIClient {
|
||||
AccountAPIClient(
|
||||
baseURL: URL(string: "https://account.test")!,
|
||||
transport: QueueAccountTransport([]),
|
||||
sessionVault: store
|
||||
)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user