mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-20 16:50:08 +02:00
test(ios): real Swift compile + XCTest fixture; device-path probe; loopback bind fix
Closes the gap from prior commits where E2E tests stubbed the Swift StateServer in TypeScript. Now there's a real SwiftPM fixture at test/fixtures/ios-qa/FixtureApp/ that compiles the production templates and runs an XCTest suite against the actual StateServer implementation. Three new test layers: - swift build invariants (periodic-tier): debug-config build succeeds, XCTest suite passes (validates real Swift impl over Foundation + Network), release-config build has zero DebugBridge symbols (structural #if DEBUG gate works end-to-end). - Real-device probe (periodic-tier, GSTACK_HAS_IOS_DEVICE=1): devicectl can list + pair the connected iPhone. Surfaces actionable instructions when the trust dialog hasn't been confirmed yet. - Fixture sources copied from ios-qa/templates/ — Package.swift splits the bridge into DebugBridgeCore (Foundation+Network, cross-platform) and DebugBridgeUI (UIKit/SwiftUI, iOS-only) so swift build can validate the bulk of the production code on macOS without an iPhone or simulator. Also fixes a real bug the XCTest unit suite caught: NWListener with requiredLocalEndpoint on params silently fails to bind for listening (it's an outbound-connection concept). Replaced with .requiredInterfaceType=.loopback + .acceptLocalOnly=true + a per-connection peer-address check. The fork's inherited code had this bug; we shipped it untouched in v1.41.0.0 and the new XCTest suite caught it immediately.
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
// AUTO-GENERATED from gstack/ios-qa/templates/DebugOverlay.swift.template
|
||||
//
|
||||
// DebugOverlay — on-device visual presence. Animated brand-colored border +
|
||||
// agent attribution chip + (optional) recording watermark. Renders above
|
||||
// sheets, alerts, and modals via a dedicated UIWindow with high windowLevel.
|
||||
//
|
||||
// Everything in this file is gated #if DEBUG and gone in Release.
|
||||
|
||||
#if DEBUG && canImport(UIKit)
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
public final class DebugOverlayWindow {
|
||||
public static let shared = DebugOverlayWindow()
|
||||
|
||||
private var window: UIWindow?
|
||||
|
||||
public func install(recording: Bool = false) {
|
||||
guard window == nil else { return }
|
||||
guard let scene = UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).first else { return }
|
||||
|
||||
let w = PassThroughWindow(windowScene: scene)
|
||||
w.windowLevel = .alert + 1
|
||||
w.backgroundColor = .clear
|
||||
w.isUserInteractionEnabled = false
|
||||
|
||||
let host = UIHostingController(rootView: OverlayRoot(recording: recording))
|
||||
host.view.backgroundColor = .clear
|
||||
w.rootViewController = host
|
||||
w.isHidden = false
|
||||
|
||||
window = w
|
||||
}
|
||||
|
||||
public func setAttribution(_ identity: String) {
|
||||
OverlayAttributionState.shared.identity = identity
|
||||
}
|
||||
}
|
||||
|
||||
/// A window that lets touches pass through to underlying windows.
|
||||
private final class PassThroughWindow: UIWindow {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let view = super.hitTest(point, with: event)
|
||||
return view == rootViewController?.view ? nil : view
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class OverlayAttributionState: ObservableObject {
|
||||
static let shared = OverlayAttributionState()
|
||||
@Published var identity: String = "Claude Code (local)"
|
||||
}
|
||||
|
||||
private struct OverlayRoot: View {
|
||||
@StateObject private var attribution = OverlayAttributionState.shared
|
||||
@State private var phase: CGFloat = 0
|
||||
let recording: Bool
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Animated brand border
|
||||
BorderShape()
|
||||
.stroke(
|
||||
AngularGradient(
|
||||
gradient: Gradient(colors: [
|
||||
BrandColor.accent.opacity(0.0),
|
||||
BrandColor.accent.opacity(0.8),
|
||||
BrandColor.accent.opacity(0.0),
|
||||
]),
|
||||
center: .center,
|
||||
angle: .degrees(phase * 360)
|
||||
),
|
||||
lineWidth: 4
|
||||
)
|
||||
.ignoresSafeArea()
|
||||
.onAppear {
|
||||
withAnimation(.linear(duration: 2.0).repeatForever(autoreverses: false)) {
|
||||
phase = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
// Attribution chip (top safe area)
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("Driven by \(attribution.identity)")
|
||||
.font(.caption2.weight(.semibold))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 4)
|
||||
.background(
|
||||
Capsule().fill(BrandColor.accent.opacity(0.85))
|
||||
)
|
||||
.padding(.trailing, 12)
|
||||
.padding(.top, 8)
|
||||
Spacer().frame(width: 0)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
// Recording watermark (diagonal, bottom-right)
|
||||
if recording {
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("AGENT DEMO")
|
||||
.font(.system(size: 10, weight: .heavy, design: .monospaced))
|
||||
.foregroundColor(.red.opacity(0.7))
|
||||
.rotationEffect(.degrees(-30))
|
||||
.padding(.trailing, 16)
|
||||
.padding(.bottom, 30)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
private struct BorderShape: Shape {
|
||||
func path(in rect: CGRect) -> Path {
|
||||
var p = Path()
|
||||
p.addRoundedRect(in: rect.insetBy(dx: 2, dy: 2), cornerSize: CGSize(width: 16, height: 16))
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
private enum BrandColor {
|
||||
// gstack brand color — resolved from DESIGN.md when codegen runs.
|
||||
// Default falls back to a deep blue.
|
||||
static let accent = Color(red: 0.0, green: 0.46, blue: 1.0)
|
||||
}
|
||||
|
||||
#endif // DEBUG && canImport(UIKit)
|
||||
Reference in New Issue
Block a user