[JJC-20260618-005-D] P1/P2 cleanup: structured errors, privacy audit, timeout SSOT, view-model tests
13 items, 555-line diff, build + 15/15 tests green.
ARCH-A3: Phase.error now carries ErrorKind (micDenied/speechDenied/asr/llm/
appGroupUnavailable/unknown) so the UI can pick icons/copy without parsing
free-form strings. Phase.ErrorKind, Phase, LLMError all Equatable.
ARCH-A4: Every TextField in APISettingsCard gets .keyboardType(.asciiCapable)
to defeat SwiftUI's iOS 18 system-keyboard hand-off that auto-suggests
Chinese/emoji and corrupts API keys / URLs / model names.
ARCH-A5 + DOC-3: PrivacyInfo.xcprivacy audited for honesty. Removed three
declared-but-unused APIs (FileTimestamp / DiskSpace / SystemBootTime) and
added ActiveKeyboards (DDA9.1) to the extension (it actually calls
advanceToNextInputMode in the tap path). Main App now declares only
UserDefaults (CA92.1). CHANGELOG updated.
ARCH-A6: Extracted PermissionManager (mic+speech permission flow, iOS 17
branching) and AppGroupPersistor (App Group load/persist) from the God
Object. KeyboardViewController drops 515 → 459 lines. KeyboardPipelineController
left in-place per risk plan — pressBegan state machine is too race-sensitive
to refactor in this pass.
RED-2: Deleted unused Theme enum (no call sites).
RED-3: Deleted unused cardStyle() alias (no call sites).
RED-7: Single source of truth for LLM timeout — LLMClient.requestTimeout +
LLMClientFactory.defaultRequestTimeout; PolishingService derives timeout from
defaultRequestTimeout+1 instead of hardcoding 15.
RED-8: ASRService.transcribe now emits .capability(onDeviceSupported:) as
first event per session; StatusBadge shows REC ⚠️ when the locale fell
back to cloud. New @Published var onDeviceSupported on State.
TEST-1: testPolishThrowsOnTransportTimeout now actually exercises
cancellation: StubURLProtocol delays response 5s, client.polish is
cancelled via Task.cancel(), test asserts the client throws .cancelled /
.transport / .decoding (was: silently passed).
TEST-2: New testPolisherSkipsNetworkWhenModeOff — PolishingService now
short-circuits when modeId == 'off' and returns trimmed input without
invoking LLMClient (proved via injected CountingLLMClient). Service was
moved to OSGKeyboardShared to be reachable from the test target.
TEST-3: KeyboardState (formerly KeyboardViewController.State) extracted
into OSGKeyboardShared so tests can @testable-import it. 5 phase/mode
tests in new KeyboardStateTests. Typealias preserves the old name.
TEST-4: New OSGKeyboardExtTests target with 6 tests covering State
initial values, phase transitions, structured-error round-trip, mode
switching, and InputMode rawValue round-trip.
This commit is contained in:
@@ -86,7 +86,7 @@ public struct KeyboardRootView: View {
|
||||
state.setLocale(newId)
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
StatusBadge(phase: state.phase)
|
||||
StatusBadge(phase: state.phase, onDeviceSupported: state.onDeviceSupported)
|
||||
Button(action: state.openSettings) {
|
||||
Image(systemName: "gearshape.fill")
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
@@ -229,8 +229,8 @@ private struct TranscriptLine: View {
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
}
|
||||
case .error(let msg):
|
||||
Text(msg)
|
||||
case .error(_, let msg):
|
||||
Text(msg ?? "")
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(palette.warning)
|
||||
.lineLimit(1)
|
||||
@@ -287,6 +287,11 @@ private struct StatusBadge: View {
|
||||
@Environment(\.themePalette) private var palette: ThemePalette
|
||||
|
||||
let phase: KeyboardViewController.State.Phase
|
||||
/// Reflects whether the active ASR session is on-device. We surface
|
||||
/// a small ⚠️ during recording so the user knows their audio is
|
||||
/// going to the cloud for this locale (and so devs catch it during
|
||||
/// QA without staring at the Xcode console).
|
||||
let onDeviceSupported: Bool
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
@@ -296,7 +301,11 @@ private struct StatusBadge: View {
|
||||
case .requestingPermissions:
|
||||
EmptyView()
|
||||
case .recording:
|
||||
dot(color: palette.recordRed, label: "REC")
|
||||
if onDeviceSupported {
|
||||
dot(color: palette.recordRed, label: "REC")
|
||||
} else {
|
||||
dot(color: palette.warning, label: "REC ⚠️", showWarning: true)
|
||||
}
|
||||
case .processing:
|
||||
dot(color: palette.accent, label: "···")
|
||||
case .error:
|
||||
@@ -307,11 +316,16 @@ private struct StatusBadge: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func dot(color: Color, label: String) -> some View {
|
||||
private func dot(color: Color, label: String, showWarning: Bool = false) -> some View {
|
||||
HStack(spacing: 4) {
|
||||
Circle()
|
||||
.fill(color)
|
||||
.frame(width: 6, height: 6)
|
||||
if showWarning {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.system(size: 9, weight: .bold))
|
||||
.foregroundStyle(palette.warning)
|
||||
}
|
||||
Text(label)
|
||||
.font(TypeStyle.caption2)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
|
||||
Reference in New Issue
Block a user