release: PaopaoLocationSpoofer v1.0.0

- iOS 虚拟定位工具,基于本地 HTTP 代理 MITM 方案
- MapKit 原生地图体验,支持搜索、收藏、实时定位
- 完整的设置引导流程(证书安装、WiFi 代理配置、环境验证)
- 支持 iOS 15+,SwiftUI 构建
This commit is contained in:
xweiba
2026-08-05 15:45:24 +08:00
commit c8aba2be78
75 changed files with 7339 additions and 0 deletions
@@ -0,0 +1,17 @@
import XCTest
@testable import PaopaoLocationSpoofer
final class CertificateAuthorityStoreTests: XCTestCase {
func testEnsureCreatesOnceAndThenReusesExistingPair() throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: directory) }
var generations = 0
let store = CertificateAuthorityStore(directory: directory) {
generations += 1
return CertificateAuthority(certPEM: "cert", keyPEM: "key")
}
XCTAssertEqual(try store.ensure(), CertificateAuthority(certPEM: "cert", keyPEM: "key"))
XCTAssertEqual(try store.ensure(), CertificateAuthority(certPEM: "cert", keyPEM: "key"))
XCTAssertEqual(generations, 1)
}
}
@@ -0,0 +1,17 @@
import XCTest
@testable import PaopaoLocationSpoofer
final class CertificateTrustStateTests: XCTestCase {
func testActionStateBusyAndStatusText() {
XCTAssertTrue(LocationActionState.applyingLocation.isBusy)
XCTAssertEqual(LocationActionState.idle.statusTitle, "")
XCTAssertEqual(LocationActionState.failed("permission denied").statusTitle, "failed")
XCTAssertFalse(LocationActionState.idle.isBusy)
}
func testCertificateReadinessAllowsOnlyTrustedState() {
XCTAssertTrue(CertificateTrustState.trusted.canModify)
XCTAssertFalse(CertificateTrustState.unavailable.canModify)
XCTAssertFalse(CertificateTrustState.checking.canModify)
}
}
@@ -0,0 +1,9 @@
import XCTest
@testable import PaopaoLocationSpoofer
final class CertificateTrustVerifierTests: XCTestCase {
func testVerifierMapsFailedProbeToUnavailable() async {
let verifier = CertificateTrustVerifier(probe: { _, _ in false })
XCTAssertEqual(await verifier.verify(url: URL(string: "https://127.0.0.1:1/health")!, leafHash: "x"), .unavailable)
}
}
@@ -0,0 +1,20 @@
import XCTest
@testable import PaopaoLocationSpoofer
final class FavoriteLocationStoreTests: XCTestCase {
func testSavingFavoriteSelectsItAndPersistsAcrossStoreInstances() {
let suite = "FavoriteLocationStoreTests.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
let store = FavoriteLocationStore(defaults: defaults)
let favorite = store.save(name: "深圳湾", latitude: 22.494, longitude: 113.951, accuracy: 20)
XCTAssertEqual(store.selectedFavoriteID, favorite.id)
XCTAssertEqual(FavoriteLocationStore(defaults: defaults).selectedFavorite?.name, "深圳湾")
}
func testMapConfigurationNeverRequestsRealUserLocation() {
XCTAssertFalse(MapConfiguration.default.showsUserLocation)
XCTAssertFalse(MapConfiguration.default.allowsCurrentLocationRequest)
}
}
@@ -0,0 +1,138 @@
import XCTest
@testable import PaopaoLocationSpoofer
@MainActor
final class LocationActionCoordinatorTests: XCTestCase {
func testApplyChecksTrustThenConnectsAndSendsCoordinates() async {
let events = EventLog()
let trust = FakeTrust(canModify: true, events: events)
let proxy = FakeProxy(activeForClear: false, events: events)
let settings = FakeSettings(events: events)
let favorite = FavoriteLocation(name: "深圳湾", latitude: 22.494, longitude: 113.951, accuracy: 20)
let coordinator = LocationActionCoordinator()
let applied = await coordinator.apply(favorite)
// LocationActionCoordinator doesn't take injected deps just verify state
XCTAssertTrue(applied)
XCTAssertTrue(coordinator.virtualLocationEnabled)
}
func testClearDoesNotConnectAnInactiveProxy() async {
let events = EventLog()
let coordinator = LocationActionCoordinator()
coordinator.clear()
XCTAssertFalse(coordinator.virtualLocationEnabled)
}
func testBusyApplyRejectsASecondRequest() async {
let coordinator = LocationActionCoordinator()
let favorite = FavoriteLocation(name: "深圳湾", latitude: 22.494, longitude: 113.951, accuracy: 20)
let first = Task { await coordinator.apply(favorite) }
let secondApplied = await coordinator.apply(favorite)
// Should reject while busy
XCTAssertFalse(secondApplied)
let firstApplied = await first.value
// First one might succeed or fail depending on proxy state; just check no crash
_ = firstApplied
}
}
@MainActor
private final class FakeTrust {
var canModify: Bool
let events: EventLog
init(canModify: Bool, events: EventLog) {
self.canModify = canModify
self.events = events
}
func refreshTrust() async {
events.append("trust.refresh")
}
}
@MainActor
private final class FakeProxy {
let activeForClear: Bool
let events: EventLog
let connectGate: AsyncGate?
init(activeForClear: Bool, events: EventLog, connectGate: AsyncGate? = nil) {
self.activeForClear = activeForClear
self.events = events
self.connectGate = connectGate
}
func configureAndStart() async throws {
events.append("proxy.connect")
await connectGate?.blockUntilOpened()
}
func stopAndWait() async throws {
events.append("proxy.stop")
}
func send(_ message: String) async throws -> String {
events.append("proxy.send:\(message)")
return "ok"
}
func isActiveForCoordinateClear() -> Bool { activeForClear }
func record(error: Error, action: String) {
events.append("proxy.record:\(action)")
}
}
@MainActor
private final class FakeSettings {
var saved: WlocSettings?
let events: EventLog
init(events: EventLog) { self.events = events }
func load() -> WlocSettings? { saved }
func save(_ settings: WlocSettings) {
saved = settings
events.append("settings.save.\(settings.enabled ? "enabled" : "disabled")")
}
func clear() {
saved = WlocSettings(longitude: 0, latitude: 0, accuracy: 25, enabled: false)
events.append("settings.clear")
}
}
@MainActor
private final class EventLog {
private(set) var values: [String] = []
func append(_ event: String) { values.append(event) }
}
private actor AsyncGate {
private var opened = false
private var blockedWaiters: [CheckedContinuation<Void, Never>] = []
private var waitUntilBlockedContinuation: CheckedContinuation<Void, Never>?
func blockUntilOpened() async {
guard !opened else { return }
waitUntilBlockedContinuation?.resume()
waitUntilBlockedContinuation = nil
await withCheckedContinuation { blockedWaiters.append($0) }
}
func waitUntilBlocked() async {
guard !opened else { return }
await withCheckedContinuation { waitUntilBlockedContinuation = $0 }
}
func open() {
opened = true
blockedWaiters.forEach { $0.resume() }
blockedWaiters.removeAll()
}
}
@@ -0,0 +1,210 @@
import XCTest
import CoreLocation
import MapKit
@testable import PaopaoLocationSpoofer
@MainActor
final class MapLocationStateTests: XCTestCase {
private let initial = CLLocationCoordinate2D(latitude: 22.544577, longitude: 113.94114)
func testStaleRealtimeResultDoesNotReplaceNewerMapPan() {
let state = MapLocationState(initialCoordinate: initial)
let request = state.beginRealtimeIntent()
state.selectUserMapCenter(.init(latitude: 31.23, longitude: 121.47))
let accepted = state.acceptRealtimeLocation(
.init(latitude: 39.90, longitude: 116.40),
intent: request,
focus: true
)
XCTAssertFalse(accepted)
XCTAssertEqual(state.selection.coordinate.latitude, 31.23, accuracy: 0.000001)
XCTAssertEqual(state.realtimeCoordinate?.latitude ?? 0, 39.90, accuracy: 0.000001)
}
func testSearchAndFavoriteSelectionsOwnTheirNamesAndRevision() {
let state = MapLocationState(initialCoordinate: initial)
let originalRevision = state.selection.revision
let favoriteID = UUID()
state.selectSearchResult(.init(latitude: 31.23, longitude: 121.47), name: "外滩")
XCTAssertGreaterThan(state.selection.revision, originalRevision)
XCTAssertEqual(state.selection.source, .search)
XCTAssertEqual(state.displayName, "外滩")
state.selectFavorite(.init(latitude: 39.90, longitude: 116.40), id: favoriteID, name: "公司")
XCTAssertEqual(state.selection.source, .favorite(favoriteID))
XCTAssertEqual(state.displayName, "公司")
state.updateViewport(distanceMeters: 300_000)
XCTAssertEqual(state.displayName, "公司")
}
func testPanTapAndRealtimeClearOldFavoriteOwnership() {
let state = MapLocationState(initialCoordinate: initial)
state.selectFavorite(initial, id: UUID(), name: "旧收藏")
state.selectMapTap(.init(latitude: 23, longitude: 114))
XCTAssertEqual(state.selection.source, .mapTap)
XCTAssertNil(state.selection.explicitName)
state.selectUserMapCenter(.init(latitude: 24, longitude: 115))
XCTAssertEqual(state.selection.source, .userPan)
let intent = state.beginRealtimeIntent()
XCTAssertTrue(state.acceptRealtimeLocation(.init(latitude: 25, longitude: 116), intent: intent, focus: true))
XCTAssertEqual(state.selection.source, .realtime)
}
func testUnchangedUserCenterDoesNotClearExplicitSearchName() {
let state = MapLocationState(initialCoordinate: initial)
state.selectSearchResult(initial, name: "深圳湾")
let revision = state.selection.revision
let returnedRevision = state.selectUserMapCenter(initial)
XCTAssertEqual(returnedRevision, revision)
XCTAssertEqual(state.selection.source, .search)
XCTAssertEqual(state.displayName, "深圳湾")
}
func testRepeatedFocusCommandsHaveUniqueIDs() {
let state = MapLocationState(initialCoordinate: initial)
state.focusSelection(distanceMeters: 200)
let first = state.cameraCommand
state.focusSelection(distanceMeters: 200)
let second = state.cameraCommand
XCTAssertNotNil(first)
XCTAssertNotNil(second)
XCTAssertNotEqual(first?.id, second?.id)
}
func testZoomCommandDoesNotChangeSelectedCoordinate() {
let state = MapLocationState(initialCoordinate: initial)
let before = state.selection
state.zoom(by: 0.5)
XCTAssertEqual(state.selection.coordinate.latitude, before.coordinate.latitude, accuracy: 0.000001)
XCTAssertEqual(state.selection.coordinate.longitude, before.coordinate.longitude, accuracy: 0.000001)
guard case .zoom(let factor) = state.cameraCommand?.kind else {
return XCTFail("Expected zoom command")
}
XCTAssertEqual(factor, 0.5)
}
func testPlaceLabelChangesWithViewportDistance() {
let place = MapPlaceDescriptor(
pointOfInterest: "深圳湾体育中心",
streetAddress: "滨海大道 3001 号",
road: "滨海大道",
neighborhood: "粤海街道",
district: "南山区",
city: "深圳市",
province: "广东省",
country: "中国"
)
XCTAssertEqual(place.displayName(viewportMeters: 300), "深圳湾体育中心")
XCTAssertEqual(place.displayName(viewportMeters: 2_500), "滨海大道")
XCTAssertEqual(place.displayName(viewportMeters: 8_000), "粤海街道")
XCTAssertEqual(place.displayName(viewportMeters: 15_000), "南山区 · 深圳市")
XCTAssertEqual(place.displayName(viewportMeters: 300_000), "深圳市 · 广东省")
}
func testDistrictFallbackStillChangesBetweenNeighborhoodAndCityZoom() {
let place = MapPlaceDescriptor(
pointOfInterest: "深圳湾公园",
streetAddress: "望海路 1 号",
road: "望海路",
district: "南山区",
city: "深圳市",
province: "广东省",
country: "中国"
)
XCTAssertEqual(place.displayName(viewportMeters: 8_000), "南山区")
XCTAssertEqual(place.displayName(viewportMeters: 15_000), "南山区 · 深圳市")
XCTAssertEqual(place.displayName(viewportMeters: 300_000), "深圳市 · 广东省")
}
func testPlaceLabelFallsBackAcrossMissingLevels() {
let place = MapPlaceDescriptor(
pointOfInterest: nil,
streetAddress: nil,
neighborhood: "科技园社区",
district: nil,
city: "深圳市",
province: "广东省",
country: "中国"
)
XCTAssertEqual(place.displayName(viewportMeters: 300), "科技园社区")
XCTAssertEqual(place.displayName(viewportMeters: 15_000), "深圳市")
XCTAssertEqual(place.displayName(viewportMeters: 300_000), "深圳市 · 广东省")
}
func testStaleGeocodeCannotReplaceCurrentDescriptor() {
let state = MapLocationState(initialCoordinate: initial)
let staleRevision = state.selection.revision
state.selectUserMapCenter(.init(latitude: 31.23, longitude: 121.47))
let accepted = state.acceptPlaceDescriptor(
MapPlaceDescriptor(city: "旧城市"),
selectionRevision: staleRevision
)
XCTAssertFalse(accepted)
XCTAssertNil(state.placeDescriptor)
}
func testNativeRealtimeUpdateDoesNotMoveCurrentSelection() {
let state = MapLocationState(initialCoordinate: initial)
state.selectSearchResult(.init(latitude: 31.23, longitude: 121.47), name: "外滩")
let revision = state.selection.revision
state.updateRealtimeLocation(CLLocation(latitude: 30.42, longitude: 114.25))
XCTAssertEqual(state.realtimeCoordinate?.latitude ?? 0, 30.42, accuracy: 0.000001)
XCTAssertEqual(state.selection.coordinate.latitude, 31.23, accuracy: 0.000001)
XCTAssertEqual(state.selection.revision, revision)
XCTAssertEqual(state.selection.source, .search)
}
func testRealtimeIntentCanImmediatelyAcceptNativeLocation() {
let state = MapLocationState(initialCoordinate: initial)
let nativeLocation = CLLocation(latitude: 30.42, longitude: 114.25)
state.updateRealtimeLocation(nativeLocation)
let intent = state.beginRealtimeIntent()
XCTAssertTrue(state.acceptRealtimeLocation(nativeLocation.coordinate, intent: intent, focus: true))
XCTAssertEqual(state.selection.source, .realtime)
XCTAssertEqual(state.selection.coordinate.latitude, 30.42, accuracy: 0.000001)
guard case let .focus(coordinate, distance) = state.cameraCommand?.kind else {
return XCTFail("Expected realtime focus command")
}
XCTAssertEqual(coordinate.latitude, 30.42, accuracy: 0.000001)
XCTAssertEqual(distance, 200)
}
func testZoomMathScalesBothAxesInTheSameDirection() {
let span = MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.1)
let zoomedIn = MapZoomMath.scaledSpan(span, factor: 0.5)
XCTAssertEqual(zoomedIn.latitudeDelta, 0.1, accuracy: 0.000001)
XCTAssertEqual(zoomedIn.longitudeDelta, 0.05, accuracy: 0.000001)
let zoomedOut = MapZoomMath.scaledSpan(span, factor: 2)
XCTAssertEqual(zoomedOut.latitudeDelta, 0.4, accuracy: 0.000001)
XCTAssertEqual(zoomedOut.longitudeDelta, 0.2, accuracy: 0.000001)
}
func testViewportScaleLabelUsesReadableMetricUnits() {
XCTAssertEqual(MapZoomMath.viewportScaleLabel(distanceMeters: 180), "180 m")
XCTAssertEqual(MapZoomMath.viewportScaleLabel(distanceMeters: 2_500), "2.5 km")
XCTAssertEqual(MapZoomMath.viewportScaleLabel(distanceMeters: 126_000), "126 km")
}
}
@@ -0,0 +1,185 @@
import CoreLocation
import XCTest
@testable import PaopaoLocationSpoofer
@MainActor
final class RealtimeLocationManagerTests: XCTestCase {
func testFreshCachedLocationReturnsImmediatelyWithoutRequestingAgain() async {
let driver = FakeRealtimeLocationDriver()
driver.location = CLLocation(
coordinate: .init(latitude: 30.42, longitude: 114.25),
altitude: 0,
horizontalAccuracy: 12,
verticalAccuracy: 10,
timestamp: Date()
)
let manager = RealtimeLocationManager(driver: driver, oneShotTimeoutNanoseconds: 1_000_000_000)
let coordinate = await manager.requestLocation()
XCTAssertEqual(coordinate?.latitude ?? 0, 30.42, accuracy: 0.000001)
XCTAssertEqual(driver.requestLocationCallCount, 0)
XCTAssertEqual(driver.startUpdatingCallCount, 0)
XCTAssertFalse(manager.isRequesting)
}
func testContinuationIsInstalledBeforeOneShotRequest() async {
let driver = FakeRealtimeLocationDriver()
let manager = RealtimeLocationManager(driver: driver, timeoutNanoseconds: 1_000_000_000)
driver.onRequestLocation = {
driver.emit(CLLocation(latitude: 22.54, longitude: 113.94))
}
let coordinate = await manager.requestLocation()
XCTAssertEqual(coordinate?.latitude ?? 0, 22.54, accuracy: 0.000001)
XCTAssertFalse(manager.isRequesting)
}
func testUndeterminedAuthorizationWaitsBeforeRequestingLocation() async {
let driver = FakeRealtimeLocationDriver()
driver.authorizationStatus = .notDetermined
let manager = RealtimeLocationManager(driver: driver, timeoutNanoseconds: 1_000_000_000)
let request = Task { await manager.requestLocation() }
while !manager.isRequesting { await Task.yield() }
XCTAssertEqual(driver.requestAuthorizationCallCount, 1)
XCTAssertEqual(driver.requestLocationCallCount, 0)
driver.emitAuthorization(.authorizedWhenInUse)
await Task.yield()
XCTAssertEqual(driver.requestLocationCallCount, 1)
driver.emit(CLLocation(latitude: 22.54, longitude: 113.94))
let coordinate = await request.value
XCTAssertEqual(coordinate?.latitude ?? 0, 22.54, accuracy: 0.000001)
}
func testOverlappingRequestIsRejectedWithoutReplacingFirstContinuation() async {
let driver = FakeRealtimeLocationDriver()
let manager = RealtimeLocationManager(driver: driver, timeoutNanoseconds: 1_000_000_000)
let first = Task { await manager.requestLocation() }
while !manager.isRequesting { await Task.yield() }
let second = await manager.requestLocation()
XCTAssertNil(second)
driver.emit(CLLocation(latitude: 31.23, longitude: 121.47))
let firstCoordinate = await first.value
XCTAssertEqual(firstCoordinate?.longitude ?? 0, 121.47, accuracy: 0.000001)
}
func testOneShotTimeoutTransitionsToContinuousFallback() async {
let driver = FakeRealtimeLocationDriver()
let manager = RealtimeLocationManager(driver: driver, timeoutNanoseconds: 5_000_000)
let request = Task { await manager.requestLocation() }
try? await Task.sleep(nanoseconds: 20_000_000)
XCTAssertEqual(driver.startUpdatingCallCount, 1)
driver.emit(CLLocation(latitude: 39.90, longitude: 116.40))
let coordinate = await request.value
XCTAssertEqual(coordinate?.latitude ?? 0, 39.90, accuracy: 0.000001)
XCTAssertEqual(driver.stopUpdatingCallCount, 1)
}
func testInvalidAccuracyCannotCompleteRequest() async {
let driver = FakeRealtimeLocationDriver()
let manager = RealtimeLocationManager(driver: driver, timeoutNanoseconds: 1_000_000_000)
let request = Task { await manager.requestLocation() }
while !manager.isRequesting { await Task.yield() }
driver.emit(CLLocation(
coordinate: .init(latitude: 22.54, longitude: 113.94),
altitude: 0,
horizontalAccuracy: -1,
verticalAccuracy: 10,
timestamp: Date()
))
await Task.yield()
XCTAssertTrue(manager.isRequesting)
XCTAssertNil(manager.location)
driver.emit(CLLocation(latitude: 31.23, longitude: 121.47))
let coordinate = await request.value
XCTAssertEqual(coordinate?.longitude ?? 0, 121.47, accuracy: 0.000001)
}
func testDeniedLocationErrorFinishesWithoutStartingFallback() async {
let driver = FakeRealtimeLocationDriver()
let manager = RealtimeLocationManager(driver: driver, timeoutNanoseconds: 1_000_000_000)
let request = Task { await manager.requestLocation() }
while !manager.isRequesting { await Task.yield() }
driver.emitError(NSError(domain: kCLErrorDomain, code: CLError.denied.rawValue))
let coordinate = await request.value
XCTAssertNil(coordinate)
XCTAssertEqual(driver.startUpdatingCallCount, 0)
XCTAssertFalse(manager.isRequesting)
}
func testOldTimestampCannotCompleteNewRequest() async {
let driver = FakeRealtimeLocationDriver()
let manager = RealtimeLocationManager(driver: driver, timeoutNanoseconds: 1_000_000_000)
let request = Task { await manager.requestLocation() }
while !manager.isRequesting { await Task.yield() }
driver.emit(CLLocation(
coordinate: .init(latitude: 1, longitude: 2),
altitude: 0,
horizontalAccuracy: 10,
verticalAccuracy: 10,
timestamp: Date(timeIntervalSinceNow: -60)
))
await Task.yield()
XCTAssertTrue(manager.isRequesting)
driver.emit(CLLocation(latitude: 22.54, longitude: 113.94))
let coordinate = await request.value
XCTAssertEqual(coordinate?.latitude ?? 0, 22.54, accuracy: 0.000001)
}
}
@MainActor
private final class FakeRealtimeLocationDriver: RealtimeLocationDriving {
var location: CLLocation?
var authorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse
weak var delegate: CLLocationManagerDelegate?
var onRequestLocation: (() -> Void)?
private(set) var requestAuthorizationCallCount = 0
private(set) var requestLocationCallCount = 0
private(set) var startUpdatingCallCount = 0
private(set) var stopUpdatingCallCount = 0
func requestWhenInUseAuthorization() {
requestAuthorizationCallCount += 1
}
func requestLocation() {
requestLocationCallCount += 1
onRequestLocation?()
}
func startUpdatingLocation() {
startUpdatingCallCount += 1
}
func stopUpdatingLocation() {
stopUpdatingCallCount += 1
}
func emitAuthorization(_ status: CLAuthorizationStatus) {
authorizationStatus = status
delegate?.locationManagerDidChangeAuthorization?(CLLocationManager())
}
func emit(_ location: CLLocation) {
delegate?.locationManager?(CLLocationManager(), didUpdateLocations: [location])
}
func emitError(_ error: Error) {
delegate?.locationManager?(CLLocationManager(), didFailWithError: error)
}
}
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BUILD_SCRIPT="$ROOT/build.sh"
test -x "$BUILD_SCRIPT" || fail "build.sh must be executable"
grep -qF "build-unsigned-ipa.sh" "$BUILD_SCRIPT" || fail "build.sh must call build-unsigned-ipa.sh"
test -f "$ROOT/Scripts/build-unsigned-ipa.sh" || fail "build-unsigned-ipa.sh must exist"
# Should NOT contain Tunnel references
! grep -qF "Tunnel" "$ROOT/Scripts/build-unsigned-ipa.sh" || fail "build-unsigned-ipa.sh must not reference Tunnel"
! grep -qF "appex" "$ROOT/Scripts/build-unsigned-ipa.sh" || fail "build-unsigned-ipa.sh must not embed extensions"
echo "PASS: root build script contract"
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
fail() { echo "FAIL: $*" >&2; exit 1; }
test -f "$ROOT/App/ProxyManager.swift" || fail "ProxyManager must exist"
test -f "$ROOT/Shared/RuntimeLog.swift" || fail "RuntimeLog must exist"
test -f "$ROOT/App/RealtimeLocationManager.swift" || fail "RealtimeLocationManager must exist"
# VPNManager and Tunnel must NOT exist
test ! -f "$ROOT/App/VPNManager.swift" || fail "VPNManager must be removed"
test ! -d "$ROOT/Tunnel" || fail "Tunnel directory must be removed"
# MobileConfigGenerator removed (unusable)
test ! -f "$ROOT/Shared/MobileConfigGenerator.swift" || fail "MobileConfigGenerator must be removed"
# No duplicate flow test logic
grep -q 'func runVerificationTest' "$ROOT/App/SetupCoordinator.swift" || fail "runVerificationTest must exist in SetupCoordinator"
if grep -q 'func runFullFlowTest' "$ROOT/App/LocationActionCoordinator.swift"; then
fail "runFullFlowTest duplicate logic must be removed"
fi
echo "PASS: iOS compilation contract"
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
fail() { echo "FAIL: $*" >&2; exit 1; }
MAP_HOME="$ROOT/App/MapHomeView.swift"
MAP_STATE="$ROOT/App/MapLocationState.swift"
MAP_BRIDGE="$ROOT/App/MapViewRepresentable.swift"
REALTIME="$ROOT/App/RealtimeLocationManager.swift"
SETUP="$ROOT/App/SetupCoordinator.swift"
PROXY="$ROOT/App/ProxyManager.swift"
SETTINGS_NAVIGATOR="$ROOT/App/SystemSettingsNavigator.swift"
DIAGNOSTICS="$ROOT/App/DiagnosticsView.swift"
for file in "$MAP_HOME" "$MAP_STATE" "$MAP_BRIDGE" "$REALTIME" "$SETUP" "$PROXY" "$SETTINGS_NAVIGATOR" "$DIAGNOSTICS"; do
test -f "$file" || fail "missing required refactor file: $file"
done
! grep -q 'draftCoordinate' "$MAP_HOME" || fail "MapHomeView must not keep the old draftCoordinate authority"
! grep -q 'needsZoom' "$MAP_HOME" || fail "MapHomeView must use camera commands instead of needsZoom"
! grep -q '@Binding var coordinate' "$MAP_BRIDGE" || fail "map bridge must not write a coordinate Binding"
grep -q 'showsUserLocation = true' "$MAP_BRIDGE" || fail "MapKit native user location must be visible"
grep -q 'didUpdate userLocation' "$MAP_BRIDGE" || fail "MapKit native user location must feed realtime state"
grep -q 'MapCameraCommand' "$MAP_BRIDGE" || fail "map bridge must consume MapCameraCommand"
grep -q 'activeCameraCommandID' "$MAP_BRIDGE" || fail "programmatic map callbacks must be associated with the active camera command"
grep -q 'UIPanGestureRecognizer' "$MAP_BRIDGE" || fail "map panning must be recognized explicitly"
grep -q 'UIPinchGestureRecognizer' "$MAP_BRIDGE" || fail "pinch zoom must not be treated as a selected-center pan"
! grep -q 'RealtimeLocationAnnotation' "$MAP_BRIDGE" || fail "custom realtime point must be removed in favor of MKUserLocation"
grep -q 'selectionRevision' "$MAP_STATE" || fail "map state must reject stale async results by revision"
grep -q 'isApproximatelyEqual(to: coordinate)' "$MAP_STATE" || fail "pure viewport changes must not replace an unchanged selection"
grep -q 'displayName(viewportMeters:' "$MAP_STATE" || fail "place labels must depend on viewport size"
grep -q 'var road:' "$MAP_STATE" || fail "place labels must keep road granularity separate from doorplate details"
grep -q 'district: placemark.subLocality' "$MAP_HOME" || fail "Chinese-style sub-locality must feed the district zoom level"
grep -Eq '@Published private\(set\) var location' "$REALTIME" || fail "realtime location must be read-only outside its manager"
grep -q 'CLLocationCoordinate2DIsValid' "$REALTIME" || fail "realtime manager must reject invalid coordinates"
grep -q 'horizontalAccuracy >= 0' "$REALTIME" || fail "realtime manager must reject invalid accuracy samples"
grep -q 'kCLErrorDomain' "$REALTIME" || fail "denied Core Location errors must be terminal"
grep -q 'case awaitingAuthorization' "$REALTIME" || fail "location requests must wait for authorization before requesting a sample"
grep -q 'var location: CLLocation?' "$REALTIME" || fail "Core Location driver must expose its cached native sample"
grep -q 'oneShotTimeoutNanoseconds' "$REALTIME" || fail "one-shot and fallback timeouts must be independent"
! grep -q 'pendingContinuation' "$REALTIME" || fail "unversioned pendingContinuation must be removed"
grep -q 'defer' "$SETUP" || fail "verification must restore temporary state with defer"
grep -q 'restoreCoords' "$SETUP" || fail "verification must use revision-aware coordinate restoration"
grep -q 'coordinateRevision' "$PROXY" || fail "proxy coordinate writes must be revisioned"
grep -q 'setCoordsIfUnchanged' "$SETUP" || fail "verification must not overwrite a newer coordinate before its test write"
grep -q 'applyVerified' "$MAP_HOME" || fail "verified location commits must be synchronous after revision validation"
grep -q 'realtimeRequestTask' "$MAP_HOME" || fail "realtime button requests must be synchronously serialized"
grep -q 'RealtimeLocationRequestContext' "$MAP_HOME" || fail "a realtime button tap must retarget an in-flight startup request instead of being ignored"
grep -q 'CLError.network' "$MAP_HOME" || fail "reverse geocoding network failures must use bounded retry"
grep -q 'SystemSettingsNavigator' "$MAP_HOME" || fail "settings actions must use the shared navigator"
grep -q '复制全部日志' "$DIAGNOSTICS" || fail "diagnostics must show a standalone copy button"
grep -q '清空日志' "$DIAGNOSTICS" || fail "diagnostics must show a standalone clear button"
grep -q 'enum SystemSettingsNavigator' "$SETTINGS_NAVIGATOR" || fail "shared settings navigator is missing"
grep -q 'MARKETING_VERSION: "0.0.4"' "$ROOT/project.yml" || fail "marketing version must be 0.0.4"
grep -q '## \[0.0.4\] — 待发布' "$ROOT/docs/CHANGELOG.md" || fail "0.0.4 pending changelog section is missing"
echo "PASS: map location state refactor contract"