- App/HUD/HudStateBridge.swift (new): @MainActor public func busHudState(
from: HudState) -> Bus.HudState. RawValue round-trip with assertionFailure
on drift, .idle fallback in Release. Option (b) from the plan — avoids
name-collision between App.HudState and Bus.HudState via module-qualified
Bus.HudState in the helper's scope.
- App/AppDelegate.swift:
* New stored properties: hudStateCoordinator, dormantAgentContinuation,
dormantVoiceContinuation, dormantConfirmContinuation (all internal so
@testable import Jarvis can assert W5 retention), webviewEntryFilename
(internal test seam for W6).
* installBus(): constructs HudStateCoordinator with emit closure that
bridges App.HudState -> Bus.HudState via busHudState() and dispatches
to bridge.send(.hudState(...)) on the MainActor. Creates three dormant
AsyncStream producers (Phase 4/5/6 replace them) and retains their
continuations on self so coordinator.{agent,voice,confirm}Task stay
live. markReady() fires from onHandshakeArmed so the HUD promotes
.booting -> .idle as soon as the webview is responsive.
* Load target: bus-harness.html -> index.html (via
webviewEntryFilename property so W6 can assert post-install value).
XCTest-host short-circuit from 02-03 deviation #5 preserved, updated
to reference the property-driven filename.
- scripts/check-single-writer-hudstate.sh: allowlist extended to
{HudStateCoordinator.swift, HudStateBridge.swift, AppDelegate.swift} +
{BusOutbound.swift, Protocol.swift}. Rationale comment updated: Phase 4+
drives the coordinator via its three input AsyncStreams, not new
.hudState(...) call sites. Already wired as a preBuildScript by Task 1.
- App/Tests/AppTests/HudStateCoordinatorBusWiringTests.swift (new):
* W1 test_coordinatorConstructedInInstallBus — delegate.hudStateCoordinator
non-nil after applicationWillFinishLaunching.
* W2 test_emitClosureBridgesToBusSend — FakeJSEvaluator observes the
closure producing a decoded BusOutbound.hudState(.thinking) after
coordinator.markReady() + agent.yield(.thinking). Proves the exact
closure shape AppDelegate installs routes rawValues end-to-end.
* W3 test_markReadyCalledOnHandshakeArmed — coordinator pinned at .booting
pre-handshake, promotes to .idle after bridge.handleHelloAck.
* W4 test_appHudStateRawValueMatchesBusHudState — table-driven bidirectional
rawValue parity over HudState.allCases and Bus.HudState.allCases. Catches
drift before busHudState()'s assertion fires at runtime.
* W5 test_dormantStreamsHeldByDelegate — all three continuations non-nil.
* W6 test_installBusLoadsIndexNotHarness — webviewEntryFilename == "index"
post-installBus.
- App/Tests/AppTests/WebviewBundleLoadTests.swift (new):
* B1 test_indexHtmlInRepoWebviewDir — structural (#filePath traversal to
repo root), catches missing committed index.html.
* B3 test_indexHtmlReferencesRelativeAssets — content-grep for "./assets/"
present + "src=\"/assets/" absent. Catches Vite misconfig or absolute-
path drift.
* B-extra test_assetsDirectoryGitignored_documentedInSmokeScript — doc-only,
asserts the split of responsibility between XCTest (committed index.html)
and scripts/smoke-test-hud.sh (built-app assets/*.js).
Tests are repo-structural / evaluator-observation only; xcodebuild test
launch still blocked on Xcode 26 RunningBoard error 5 per 02-03 precedent.
Verification is xcodebuild build (succeeds) + scripts/smoke-test-hud.sh
(next task). This follows the 02-03 SUMMARY deviation #5 pattern.
xcodebuild build -configuration Debug: BUILD SUCCEEDED.
bash scripts/check-single-writer-hudstate.sh: PASS (allowlist active).
packages/Bus swift test: 47/47 green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
29 lines
1.3 KiB
Swift
29 lines
1.3 KiB
Swift
import Bus
|
|
|
|
/// Bridges the App-side `HudState` (UI concerns, voiceOverLabel, declaration
|
|
/// order tuned for menu-bar icon factory) to the Bus-side `HudState` (wire
|
|
/// format). The two enums are kept in lock-step via their shared String
|
|
/// rawValues — a drift between them is an internal bug.
|
|
///
|
|
/// Plan 03-05 wires this helper through `AppDelegate.installBus()`:
|
|
/// HudStateCoordinator emits App.HudState → busHudState(from:) → Bus.HudState
|
|
/// → WebviewBridge.send(.hudState(...))
|
|
///
|
|
/// Enum parity is assertion-guarded in Debug and table-enforced by
|
|
/// `HudStateCoordinatorBusWiringTests.test_appHudStateRawValueMatchesBusHudState`.
|
|
/// The Release fallback to `.idle` prevents a crash if someone ever adds an
|
|
/// App-only case without adding it to Bus — the HUD wedges on `.idle` rather
|
|
/// than terminating the user's session.
|
|
///
|
|
/// `@MainActor` is not strictly required (the function is a pure rawValue
|
|
/// round-trip), but it matches the caller expectation in
|
|
/// `HudStateCoordinator`'s emit closure which is @MainActor-isolated.
|
|
@MainActor
|
|
public func busHudState(from app: HudState) -> Bus.HudState {
|
|
guard let bus = Bus.HudState(rawValue: app.rawValue) else {
|
|
assertionFailure("HudState rawValue drift: \(app.rawValue) not in Bus.HudState")
|
|
return .idle
|
|
}
|
|
return bus
|
|
}
|