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
170 lines
6.3 KiB
Swift
170 lines
6.3 KiB
Swift
// HomeAccountSurfacePolicyTests.swift
|
|
// OSGKeyboardTests
|
|
|
|
@testable import OSGKeyboard
|
|
import OSGKeyboardShared
|
|
import XCTest
|
|
|
|
final class HomeAccountSurfacePolicyTests: XCTestCase {
|
|
|
|
// MARK: - Readiness
|
|
|
|
func testCreditsNeedAnAccountButSurviveRestore() {
|
|
XCTAssertEqual(readiness(.managed, account: .signedIn), .credits)
|
|
XCTAssertEqual(readiness(.managed, account: .restoring), .credits)
|
|
XCTAssertEqual(readiness(.managed, account: .signedOut), .unavailable)
|
|
}
|
|
|
|
func testOwnKeyReadinessFollowsConfiguredKeys() {
|
|
XCTAssertEqual(readiness(.byok, asr: true, polish: true), .ownKey)
|
|
XCTAssertEqual(readiness(.byok, asr: true, polish: false), .dictationOnly)
|
|
XCTAssertEqual(readiness(.byok, asr: false, polish: true), .unavailable)
|
|
}
|
|
|
|
// MARK: - Surface rules, in priority order
|
|
|
|
func testMissingPermissionsOutrankEverything() {
|
|
XCTAssertEqual(
|
|
surface(needsPermissionSetup: true, account: .signedOut, pendingReferralCode: "K7M2Q9XA"),
|
|
.permissions
|
|
)
|
|
}
|
|
|
|
func testRestoringShowsNoAccountSurface() {
|
|
XCTAssertNil(surface(account: .restoring, readiness: .dictationOnly))
|
|
}
|
|
|
|
func testSavedInvitationLeadsUntilDeferred() {
|
|
XCTAssertEqual(
|
|
surface(account: .signedOut, pendingReferralCode: "K7M2Q9XA"),
|
|
.invitation(code: "K7M2Q9XA")
|
|
)
|
|
XCTAssertEqual(
|
|
surface(
|
|
account: .signedOut,
|
|
pendingReferralCode: "K7M2Q9XA",
|
|
isInvitationDeferred: true,
|
|
readiness: .dictationOnly
|
|
),
|
|
.signInSetup
|
|
)
|
|
}
|
|
|
|
func testSignedOutWithoutWorkingServiceAsksToSignIn() {
|
|
XCTAssertEqual(surface(account: .signedOut, readiness: .dictationOnly), .signInSetup)
|
|
XCTAssertEqual(surface(account: .signedOut, readiness: .unavailable), .signInSetup)
|
|
XCTAssertEqual(
|
|
surface(account: .signedOut, credentialSource: .managed, readiness: .unavailable),
|
|
.signInSetup
|
|
)
|
|
}
|
|
|
|
func testSignedOutWithWorkingKeysOnlyGetsQuietPromo() {
|
|
XCTAssertEqual(surface(account: .signedOut, readiness: .ownKey), .signInPromo)
|
|
}
|
|
|
|
func testCreditsConnectionFailureBecomesProblemCard() {
|
|
XCTAssertEqual(
|
|
surface(account: .signedIn, credentialSource: .managed, readiness: .credits, credits: .failed),
|
|
.creditsUnavailable
|
|
)
|
|
}
|
|
|
|
func testDepletedCreditsBecomeProblemCard() {
|
|
XCTAssertEqual(
|
|
surface(account: .signedIn, credentialSource: .managed, readiness: .credits, credits: .balance(0)),
|
|
.creditsDepleted
|
|
)
|
|
XCTAssertEqual(
|
|
surface(account: .signedIn, credentialSource: .managed, readiness: .credits, credits: .balance(-5)),
|
|
.creditsDepleted
|
|
)
|
|
}
|
|
|
|
func testHealthyOrLoadingCreditsStayQuiet() {
|
|
XCTAssertEqual(
|
|
surface(account: .signedIn, credentialSource: .managed, readiness: .credits, credits: .balance(1_500)),
|
|
.accountSummary
|
|
)
|
|
XCTAssertEqual(
|
|
surface(account: .signedIn, credentialSource: .managed, readiness: .credits, credits: .loading),
|
|
.accountSummary
|
|
)
|
|
}
|
|
|
|
func testSignedInWithoutKeysChoosesService() {
|
|
XCTAssertEqual(surface(account: .signedIn, readiness: .dictationOnly), .chooseService)
|
|
XCTAssertEqual(surface(account: .signedIn, readiness: .unavailable), .chooseService)
|
|
}
|
|
|
|
func testSignedInWithOwnKeysIgnoresCreditErrors() {
|
|
XCTAssertEqual(surface(account: .signedIn, readiness: .ownKey, credits: .failed), .accountSummary)
|
|
XCTAssertEqual(surface(account: .signedIn, readiness: .ownKey, credits: .balance(0)), .accountSummary)
|
|
}
|
|
|
|
func testSignedInIgnoresStalePendingInvitation() {
|
|
XCTAssertEqual(
|
|
surface(account: .signedIn, pendingReferralCode: "K7M2Q9XA", readiness: .ownKey),
|
|
.accountSummary
|
|
)
|
|
}
|
|
|
|
// MARK: - Layout
|
|
|
|
func testUnfoldedDuoUsesWideLayoutEvenWithCompactSizeClass() {
|
|
XCTAssertTrue(HomeLayoutPolicy.usesWideLayout(isRegularWidth: false, containerWidth: 669))
|
|
XCTAssertFalse(HomeLayoutPolicy.usesWideLayout(isRegularWidth: false, containerWidth: 466))
|
|
XCTAssertFalse(HomeLayoutPolicy.usesWideLayout(isRegularWidth: false, containerWidth: 402))
|
|
}
|
|
|
|
func testRegularSizeClassKeepsWideLayoutInsideSplitView() {
|
|
XCTAssertTrue(HomeLayoutPolicy.usesWideLayout(isRegularWidth: true, containerWidth: 504))
|
|
}
|
|
|
|
func testWideTopClearanceOnlyWhereNoStatusBarSitsAbove() {
|
|
// Landscape Duo inner display: status bar on the side, nothing above.
|
|
XCTAssertEqual(WideLayoutMetrics.topCornerClearance(topSafeAreaInset: 0), Spacing.md)
|
|
// iPad status bar, portrait Duo inner display (measured 82 pt).
|
|
XCTAssertEqual(WideLayoutMetrics.topCornerClearance(topSafeAreaInset: 24), 0)
|
|
XCTAssertEqual(WideLayoutMetrics.topCornerClearance(topSafeAreaInset: 82), 0)
|
|
// A thin inset only gets topped up to the minimum.
|
|
XCTAssertEqual(WideLayoutMetrics.topCornerClearance(topSafeAreaInset: 6), Spacing.md - 6)
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private func readiness(
|
|
_ source: CredentialSource,
|
|
account: HomeAccountPhase = .signedIn,
|
|
asr: Bool = true,
|
|
polish: Bool = true
|
|
) -> HomeServiceReadiness {
|
|
HomeAccountSurfacePolicy.readiness(
|
|
credentialSource: source,
|
|
account: account,
|
|
isASRConfigured: asr,
|
|
isPolishConfigured: polish
|
|
)
|
|
}
|
|
|
|
private func surface(
|
|
needsPermissionSetup: Bool = false,
|
|
account: HomeAccountPhase,
|
|
pendingReferralCode: String? = nil,
|
|
isInvitationDeferred: Bool = false,
|
|
credentialSource: CredentialSource = .byok,
|
|
readiness: HomeServiceReadiness = .ownKey,
|
|
credits: HomeCreditsState = .loading
|
|
) -> HomeAccountSurface? {
|
|
HomeAccountSurfacePolicy.surface(
|
|
needsPermissionSetup: needsPermissionSetup,
|
|
account: account,
|
|
pendingReferralCode: pendingReferralCode,
|
|
isInvitationDeferred: isInvitationDeferred,
|
|
credentialSource: credentialSource,
|
|
readiness: readiness,
|
|
credits: credits
|
|
)
|
|
}
|
|
}
|