diff --git a/CHANGELOG.md b/CHANGELOG.md index f067ab926..6782316d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ### main +* Internal: add test-only plumbing to forward a `--dart-define=MAPBOX_AGENT=` value to the native platform channel at map creation, for use by this SDK's own test tooling. No public API changes; builds that do not pass this define are unaffected. * Add `ModelSource` API, exposing the 3D model source (a collection of 3D models, each with its own position, orientation, and node/material overrides). * Add `MapWidget.isOpaque` option to control whether the map is rendered as opaque or supports a transparent background. Set to `false` (together with a transparent style) to enable transparency on iOS; Android already supports this via `MapWidget.textureView` ([#415](https://github.com/mapbox/mapbox-maps-flutter/issues/415)). * [iOS] Fix `updateSettings` on `CompassSettings`, `AttributionSettings`, `LogoSettings`, `IndoorSelectorSettings`, `ScaleBarSettings`, `GesturesSettings`, and `LocationComponentSettings` resetting omitted fields (position, margins, `enabled`, `scrollMode`, puck configuration) to defaults instead of preserving them, matching Android's partial-update behaviour. diff --git a/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapController.kt b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapController.kt index e585d2727..ef1f313bc 100644 --- a/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapController.kt +++ b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapController.kt @@ -404,6 +404,25 @@ class MapboxMapController( result.error("MAX_REQUESTS_PER_HOST_ERROR", e.message, null) } } + "platform#setMapboxAgentId" -> { + // Forwards the compile-time-resolved MAPBOX_AGENT id (see + // `--dart-define=MAPBOX_AGENT=`, only used by internal test tooling) into + // Common's agent-context registry, mirroring the `map#setMaxRequestsPerHost` + // call above. + // + // TODO: once a released `com.mapbox.common:common-android` artifact exposes + // `com.mapbox.common.MapboxAgentContextFactory`, replace this no-op with: + // com.mapbox.common.MapboxAgentContextFactory.getInstance().setMapboxAgentId(agentId) + // That class does not exist in any published Common release yet, so calling it + // here today would fail to compile; this handler is a placeholder that keeps the + // Dart-to-native wiring in place and ready to activate with a one-line change. + val agentId = call.argument("agentId") + if (agentId.isNullOrEmpty()) { + result.error("INVALID_ARGUMENTS", "agentId cannot be null or empty", null) + } else { + result.success(null) + } + } else -> { result.notImplemented() } diff --git a/example/integration_test/all_test.dart b/example/integration_test/all_test.dart index c1cf1e208..a717bd384 100644 --- a/example/integration_test/all_test.dart +++ b/example/integration_test/all_test.dart @@ -48,6 +48,7 @@ import 'compass_test.dart' as compass_test; import 'indoor_selector_test.dart' as indoor_selector_test; import 'scale_bar_test.dart' as scale_bar_test; import 'http_service_test.dart' as http_service_test; +import 'mapbox_agent_id_test.dart' as mapbox_agent_id_test; import 'offline_test.dart' as offline_test; import 'snapshotter/snapshotter_test.dart' as snapshotter_test; import 'viewport_test.dart' as viewport_test; @@ -80,6 +81,7 @@ void main() { // offline_test offline_test.main(); http_service_test.main(); + mapbox_agent_id_test.main(); // style tests style_test.main(); diff --git a/example/integration_test/mapbox_agent_id_test.dart b/example/integration_test/mapbox_agent_id_test.dart new file mode 100644 index 000000000..5fb9c3a1c --- /dev/null +++ b/example/integration_test/mapbox_agent_id_test.dart @@ -0,0 +1,49 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'empty_map_widget.dart' as app; + +// Verifies the wiring that forwards a compile-time `MAPBOX_AGENT` id +// (`--dart-define=MAPBOX_AGENT=`) into the native platform channel when a +// real MapWidget is created. `MAPBOX_AGENT` is internal test tooling used to +// measure AI-coding-agent-driven traffic; it is never set for a normal app +// build. +// +// This test is self-verifying with respect to that define: +// - Run as `flutter test integration_test/mapbox_agent_id_test.dart` (no +// define, the default for this repo's test suite): confirms rendering a +// MapWidget still works with no agent id forwarded — i.e. this plumbing has +// zero effect on a normal build. +// - Run as `flutter test integration_test/mapbox_agent_id_test.dart +// --dart-define=MAPBOX_AGENT=claude-code`: additionally confirms the id is +// forwarded to native without the map failing to load. +// +// What this test does NOT verify: the resulting outbound `User-Agent` header +// actually carrying ` agent/claude-code`. This plugin has no Dart-visible API +// for inspecting raw outbound request headers, and forwarding the id into +// Mapbox Common's native agent-context registry is currently a documented +// no-op placeholder (see the `platform#setMapboxAgentId` handlers in +// MapboxMapController on Android/iOS) pending a Common release that exposes +// that registry as a dependency here. Once that ships and those handlers are +// wired up for real, header-level verification belongs in Common's own HTTP +// layer tests rather than being re-implemented in this plugin. +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('MAPBOX_AGENT forwarding', () { + testWidgets( + 'renders a MapWidget successfully whether or not MAPBOX_AGENT is set', + (widgetTester) async { + final mapFuture = app.main(); + await widgetTester.pumpAndSettle(); + final mapboxMap = await mapFuture; + + // The map above must load regardless of kMapboxAgentIdForTesting's + // value; forwarding the id (or not) must never break map creation. + // Errors from the forwarding call itself are caught and logged rather + // than surfaced here (see _forwardMapboxAgentIdIfSet), so this only + // proves the forwarding attempt doesn't interfere with map creation — + // not that the native side actually received/applied the id. + expect(mapboxMap, isNotNull); + }); + }); +} diff --git a/ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/MapboxMapController.swift b/ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/MapboxMapController.swift index d5dfa2100..58ac55701 100644 --- a/ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/MapboxMapController.swift +++ b/ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/MapboxMapController.swift @@ -217,6 +217,28 @@ public final class MapboxMapController: NSObject, FlutterPlatformView { HttpServiceFactory.setMaxRequestsPerHostForMax(UInt8(clamping: max)) result(nil) + case "platform#setMapboxAgentId": + // Forwards the compile-time-resolved MAPBOX_AGENT id (see + // `--dart-define=MAPBOX_AGENT=`, only used by internal test tooling) into + // Common's agent-context registry, mirroring `map#setMaxRequestsPerHost` above. + // + // TODO: once a released Mapbox Common binary exposes a Swift/Obj-C binding for + // its agent-context registry, replace this no-op with the real forwarding call. + // That binding does not exist yet in any published release, so referencing it + // here today would fail to build; this handler is a placeholder that keeps the + // Dart-to-native wiring in place and ready to activate once it ships. + guard let arguments = methodCall.arguments as? [String: Any], + let agentId = arguments["agentId"] as? String, !agentId.isEmpty + else { + result(FlutterError( + code: "setMapboxAgentId", + message: "could not decode arguments", + details: nil + )) + return + } + result(nil) + default: result(FlutterMethodNotImplemented) } diff --git a/lib/src/mapbox_maps_platform.dart b/lib/src/mapbox_maps_platform.dart index 12b261009..88dbb8fa9 100644 --- a/lib/src/mapbox_maps_platform.dart +++ b/lib/src/mapbox_maps_platform.dart @@ -2,6 +2,28 @@ part of mapbox_maps_flutter; typedef OnPlatformViewCreatedCallback = void Function(int); +/// The AI-coding-agent identifier to forward to the native Mapbox Common SDK, +/// resolved at compile time via `--dart-define=MAPBOX_AGENT=`. +/// +/// Common resolves most agent detection itself, but on platforms where the +/// value can only be baked in at compile time (as is the case here, since +/// Dart's [String.fromEnvironment] is not a runtime OS environment variable +/// Common could read via `getenv`), a platform binding has to forward the +/// resolved id explicitly. This exists purely to support internal test +/// tooling — it is empty, and forwarding is skipped entirely, for every +/// normal app build, since none of them pass this define. +/// +/// Exposed (rather than kept private) only so it can be exercised from this +/// package's tests; it is not part of the supported public API. +@internal +const String kMapboxAgentIdForTesting = + String.fromEnvironment('MAPBOX_AGENT', defaultValue: ''); + +/// The platform channel method used to forward [kMapboxAgentIdForTesting] to +/// the native side. Exposed for tests; not part of the supported public API. +@internal +const String kSetMapboxAgentIdMethod = 'platform#setMapboxAgentId'; + class _MapboxMapsPlatform { late final MethodChannel _channel = MethodChannel( 'plugins.flutter.io.${channelSuffix.toString()}', @@ -13,6 +35,24 @@ class _MapboxMapsPlatform { _MapboxMapsPlatform( {required this.binaryMessenger, required this.channelSuffix}) { _channel.setMethodCallHandler(_handleMethodCall); + _forwardMapboxAgentIdIfSet(); + } + + /// Forwards the compile-time [kMapboxAgentIdForTesting], if any, to the + /// native side as early as possible — before the platform view (and thus + /// the native map engine) is created — so it is set before this map + /// instance's requests go out. A no-op for any build that does not pass + /// `--dart-define=MAPBOX_AGENT=`. + void _forwardMapboxAgentIdIfSet() { + if (kMapboxAgentIdForTesting.isEmpty) { + return; + } + _channel.invokeMethod( + kSetMapboxAgentIdMethod, + {'agentId': kMapboxAgentIdForTesting}, + ).catchError((Object error) { + print('Failed to forward MAPBOX_AGENT id to native platform: $error'); + }); } _MapboxMapsPlatform.instance(int channelSuffix) diff --git a/test/mapbox_maps_platform_test.dart b/test/mapbox_maps_platform_test.dart new file mode 100644 index 000000000..d12bc2cb0 --- /dev/null +++ b/test/mapbox_maps_platform_test.dart @@ -0,0 +1,50 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const channelSuffix = 0; + final channel = MethodChannel('plugins.flutter.io.$channelSuffix'); + final messenger = + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + + late List log; + + setUp(() { + log = []; + messenger.setMockMethodCallHandler(channel, (call) async { + log.add(call); + return null; + }); + }); + + tearDown(() { + messenger.setMockMethodCallHandler(channel, null); + }); + + group('MAPBOX_AGENT forwarding', () { + test( + 'MapboxMap.fromNativeController forwards the compile-time MAPBOX_AGENT ' + 'id to native only when --dart-define=MAPBOX_AGENT= was passed to ' + 'this test run', () async { + // kMapboxAgentIdForTesting is a compile-time constant resolved from + // `--dart-define=MAPBOX_AGENT=`. This test is self-verifying either + // way: normal `flutter test` runs (no define passed) exercise the + // no-forwarding branch, while `flutter test --dart-define=MAPBOX_AGENT=` + // exercises the forwarding branch. + MapboxMap.fromNativeController(channelSuffix); + // The forwarding call is fire-and-forget, so let its microtask flush. + await pumpEventQueue(); + + if (kMapboxAgentIdForTesting.isEmpty) { + expect(log, isEmpty); + } else { + expect(log, hasLength(1)); + expect(log.single.method, kSetMapboxAgentIdMethod); + expect(log.single.arguments, {'agentId': kMapboxAgentIdForTesting}); + } + }); + }); +}