fix(ios,android): close the acceptance gaps the review found, add Android CI
- T-iOS-34's stated acceptance is "最大字号不破版" and it was failing: the key bar froze its height at 52pt while an AX5 keycap needs 108.24pt, so caps clipped — recorded as a withKnownIssue rather than fixed. Height now derives from the content size category (and tracks live changes via registerForTraitChanges); the known-issue marker is gone, replaced by positive assertions including a 12-category no-clip sweep and a guard that stays red if anyone writes the constant back. Honest tradeoff: the keycap font is clamped at .accessibility2, the same policy the design system already applies to dense content, because an unclamped AX5 bar would eat the terminal. A test pins the clamp so the two cannot drift. - Thumbnails silently 401'd on a token-gated host: the pipeline built its own transport with no token source, and by design never throws, so every preview degraded to a placeholder with no signal. Assembled from AppEnvironment now. - Android had zero CI while the token wave shipped 24 files of secret-handling code. The instrumented leg is workflow_dispatch-only and says why in the file: no one has ever seen it green on a runner, and a required leg nobody trusts just produces a false green. - Android persisted a validated token before the host probe succeeded, stranding a secret for a host that never paired. App bundle 534 -> 550 on both simulators, zero known issues. Android 687 -> 691.
This commit is contained in:
@@ -22,6 +22,11 @@ import WireProtocol
|
||||
/// (AfterFirstUnlockThisDeviceOnly, hosted keychain test asserts it);
|
||||
/// UserDefaults carries only the non-secret per-host lastSessionId. A token
|
||||
/// never reaches a log line, a URL query, or an error description.
|
||||
/// - The one other `URLSession` in the app belongs to `thumbnailTransport()`
|
||||
/// below — assembled HERE, from these same providers, for the same reasons
|
||||
/// (F1: the session-thumbnail pipeline is built by a SwiftUI `@State`
|
||||
/// initializer that has no environment to read, and used to build a
|
||||
/// credential-less transport of its own).
|
||||
/// - No debug ATS overrides exist (project.yml declares NO
|
||||
/// NSAllowsArbitraryLoads in any configuration — only the five §5.2 CIDR
|
||||
/// exceptions), and no isSecureTextEntry-style screenshot hacks are used;
|
||||
@@ -73,10 +78,7 @@ struct AppEnvironment: Sendable {
|
||||
// missing cert is the normal pre-install state (→ nil); genuine faults
|
||||
// are logged, not fatal (`loadedIdentityOrNil`). mTLS challenges only
|
||||
// fire for tunnel hosts, so a `nil` result is inert for local hosts.
|
||||
let identityStore = KeychainClientIdentityStore()
|
||||
let identityProvider: @Sendable () -> ClientIdentity? = {
|
||||
identityStore.loadedIdentityOrNil()
|
||||
}
|
||||
let identityProvider = keychainIdentityProvider()
|
||||
let hostStore = KeychainHostStore()
|
||||
// C1 · ONE access-token source for the whole app, reading the same
|
||||
// Keychain records the pairing flow writes. Both transports consult it
|
||||
@@ -129,6 +131,52 @@ struct AppEnvironment: Sendable {
|
||||
)
|
||||
}
|
||||
|
||||
/// C-iOS-2 · The lazy, Keychain-backed device-identity provider: resolved
|
||||
/// per TLS client-certificate challenge (never snapshotted), so a certificate
|
||||
/// imported mid-run is presented on the NEXT handshake without a relaunch.
|
||||
/// ONE definition, shared by `production()` and `thumbnailTransport()`.
|
||||
static func keychainIdentityProvider() -> @Sendable () -> ClientIdentity? {
|
||||
let identityStore = KeychainClientIdentityStore()
|
||||
return { identityStore.loadedIdentityOrNil() }
|
||||
}
|
||||
|
||||
/// F1 · The HTTP transport `SessionThumbnailPipeline.live()` fetches
|
||||
/// `GET /live-sessions/:id/preview` over — assembled HERE, at the
|
||||
/// composition root, from the SAME two credential providers `production()`
|
||||
/// installs on the app-wide transport: the lazy mTLS identity AND the
|
||||
/// per-origin access token.
|
||||
///
|
||||
/// WHY IT EXISTS (the bug it fixes): the pipeline used to build its own
|
||||
/// `URLSessionHTTPTransport` with an identity provider but **no token
|
||||
/// source**, so on a `WEBTERM_TOKEN`-gated host every preview came back 401.
|
||||
/// The pipeline never throws by design — a failed preview IS a placeholder —
|
||||
/// so the whole session list silently degraded to placeholder thumbnails with
|
||||
/// nothing on screen saying why. It is assembled here rather than in the
|
||||
/// component because the pipeline is created by a SwiftUI `@State`
|
||||
/// initializer that has no `AppEnvironment` to read from.
|
||||
///
|
||||
/// It is a SEPARATE `URLSession` from `production().http` for the same reason
|
||||
/// (no environment at that call site), and that costs nothing: both are
|
||||
/// `.ephemeral` (preview bytes are raw ring-buffer terminal output and must
|
||||
/// never touch a disk cache — T-iOS-19) and both resolve their credentials
|
||||
/// per request, so neither can hold a stale token.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - hostStore: where the per-host tokens live — Keychain in production,
|
||||
/// an in-memory double in tests.
|
||||
/// - identityProvider: the mTLS identity source (see above).
|
||||
static func thumbnailTransport(
|
||||
hostStore: any HostStore = KeychainHostStore(),
|
||||
identityProvider: @escaping @Sendable () -> ClientIdentity?
|
||||
= AppEnvironment.keychainIdentityProvider()
|
||||
) -> URLSessionHTTPTransport {
|
||||
let tokens = AccessTokenSource(store: hostStore)
|
||||
return URLSessionHTTPTransport(
|
||||
identityProvider: identityProvider,
|
||||
tokenForOrigin: { origin in await tokens.token(forOrigin: origin) }
|
||||
)
|
||||
}
|
||||
|
||||
/// Away-digest source for a session engine: wraps `APIClient.events` per
|
||||
/// host (the engine never holds an HTTP client — plan §3.2). The token rides
|
||||
/// along at the transport, so this stays token-free.
|
||||
|
||||
Reference in New Issue
Block a user