Files
OSGKeyboard/OSGKeyboardTests/DebugAccountSessionHandoffTests.swift
Rocky 4dce67df84 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
2026-10-29 11:54:13 +08:00

122 lines
4.7 KiB
Swift

// 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