@volter/twin-googlemaps 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +96 -0
  3. package/dist/src/cli.d.ts +2 -0
  4. package/dist/src/cli.js +23 -0
  5. package/dist/src/googlemaps-auth.d.ts +31 -0
  6. package/dist/src/googlemaps-auth.js +37 -0
  7. package/dist/src/googlemaps-budget.d.ts +61 -0
  8. package/dist/src/googlemaps-budget.js +142 -0
  9. package/dist/src/googlemaps-capabilities.d.ts +3 -0
  10. package/dist/src/googlemaps-capabilities.js +648 -0
  11. package/dist/src/googlemaps-conformance.d.ts +8 -0
  12. package/dist/src/googlemaps-conformance.js +18 -0
  13. package/dist/src/googlemaps-connector.d.ts +94 -0
  14. package/dist/src/googlemaps-connector.js +243 -0
  15. package/dist/src/googlemaps-data.d.ts +87 -0
  16. package/dist/src/googlemaps-data.js +370 -0
  17. package/dist/src/googlemaps-perform-harness.d.ts +5 -0
  18. package/dist/src/googlemaps-perform-harness.js +17 -0
  19. package/dist/src/googlemaps-server.d.ts +14 -0
  20. package/dist/src/googlemaps-server.js +25 -0
  21. package/dist/src/googlemaps-twin.d.ts +2 -0
  22. package/dist/src/googlemaps-twin.js +1116 -0
  23. package/dist/src/googlemaps-types.d.ts +45 -0
  24. package/dist/src/googlemaps-types.js +1 -0
  25. package/dist/src/index.d.ts +11 -0
  26. package/dist/src/index.js +65 -0
  27. package/package.json +52 -0
  28. package/src/cli.ts +22 -0
  29. package/src/googlemaps-auth.ts +52 -0
  30. package/src/googlemaps-budget.ts +168 -0
  31. package/src/googlemaps-capabilities.ts +766 -0
  32. package/src/googlemaps-conformance.ts +24 -0
  33. package/src/googlemaps-connector.ts +250 -0
  34. package/src/googlemaps-data.ts +399 -0
  35. package/src/googlemaps-perform-harness.ts +17 -0
  36. package/src/googlemaps-server.ts +33 -0
  37. package/src/googlemaps-twin.ts +1110 -0
  38. package/src/googlemaps-types.ts +47 -0
  39. package/src/index.ts +124 -0
@@ -0,0 +1,648 @@
1
+ import { mkdtempSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { projectResources } from '@volter/world-core';
5
+ import { checkCapabilities, verifyBoundary } from '@volter/world-tooling';
6
+ import { pullGoogleMapsGeocodes, } from "./googlemaps-connector.js";
7
+ import { performPending } from "./googlemaps-perform-harness.js";
8
+ import { handleGoogleMapsTwinRequest } from "./googlemaps-twin.js";
9
+ async function withRoot(fn) {
10
+ const root = mkdtempSync(join(tmpdir(), 'googlemaps-cap-'));
11
+ const h = (step) => handleGoogleMapsTwinRequest({
12
+ method: step.m,
13
+ path: step.p,
14
+ body: step.b === undefined ? undefined : JSON.stringify(step.b),
15
+ root,
16
+ readOnly: step.readOnly,
17
+ occurredAt: '1970-01-01T00:00:00.000Z',
18
+ ...(step.headers ? { headers: step.headers } : {}),
19
+ });
20
+ try {
21
+ return await verifyBoundary('googlemaps.withRoot', () => fn(h, root));
22
+ }
23
+ finally {
24
+ rmSync(root, { recursive: true, force: true });
25
+ }
26
+ }
27
+ const done = (id, area, title, dimension, tier, verify) => ({ id, area, title, dimension, tier, expected: 'done', verify });
28
+ const todo = (id, area, title, dimension, tier) => ({ id, area, title, dimension, tier, expected: 'todo' });
29
+ export const GOOGLEMAPS_CAPABILITIES = [
30
+ // ---- image bytes the twin does not serve yet ----
31
+ todo('googlemaps.staticmap.served_bytes', 'staticmap', 'Static Maps: serve deterministic placeholder image bytes at the /maps/api/staticmap URL (today only image metadata is returned)', 'api', 'common'),
32
+ todo('googlemaps.streetview.served_bytes', 'streetview', 'Street View: serve deterministic placeholder image bytes for a pano (today only metadata and a deterministic pano id are returned)', 'api', 'common'),
33
+ todo('googlemaps.places.photo_bytes', 'places', 'Place Photos: serve deterministic placeholder image bytes at the photo URL the twin returns (today the photo reference resolves to nothing)', 'api', 'common'),
34
+ // ---- Geocoding ----
35
+ done('googlemaps.geocode.ok', 'geocoding', 'Geocoding returns OK with a location for a known place', 'api', 'core', () => withRoot(async (h) => {
36
+ const r = await h({ m: 'GET', p: '/maps/api/geocode/json?address=Los%20Angeles' });
37
+ const res = r.body.results?.[0];
38
+ return r.status === 200 && r.body.status === 'OK'
39
+ && res?.geometry?.location?.lat === 34.0522342 && res?.geometry?.location?.lng === -118.2436849
40
+ && res?.formatted_address === 'Los Angeles, CA, USA' && typeof res?.place_id === 'string';
41
+ })),
42
+ done('googlemaps.geocode.full_result_shape', 'geocoding', 'Geocoding result models the full real shape', 'api', 'core', () => withRoot(async (h) => {
43
+ const r = await h({ m: 'GET', p: '/maps/api/geocode/json?address=1600%20Amphitheatre%20Pkwy%2C%20Mountain%20View' });
44
+ const res = r.body.results?.[0];
45
+ const comp = res?.address_components?.find((c) => c.types?.includes('administrative_area_level_1'));
46
+ return r.status === 200
47
+ && Array.isArray(res?.address_components) && res.address_components.length > 0
48
+ && comp?.short_name === 'CA' && comp?.long_name === 'California'
49
+ && ['ROOFTOP', 'RANGE_INTERPOLATED', 'GEOMETRIC_CENTER', 'APPROXIMATE'].includes(res?.geometry?.location_type)
50
+ && typeof res?.geometry?.viewport?.northeast?.lat === 'number'
51
+ && Array.isArray(res?.types);
52
+ })),
53
+ done('googlemaps.geocode.zero_results', 'geocoding', 'Geocoding returns ZERO_RESULTS for an unmatched query', 'api', 'core', () => withRoot(async (h) => {
54
+ const r = await h({ m: 'GET', p: '/maps/api/geocode/json?address=zzzznoresult-nowhere' });
55
+ return r.status === 200 && r.body.status === 'ZERO_RESULTS' && Array.isArray(r.body.results) && r.body.results.length === 0;
56
+ })),
57
+ done('googlemaps.geocode.invalid_request', 'geocoding', 'Geocoding rejects a missing address with INVALID_REQUEST', 'api', 'core', () => withRoot(async (h) => {
58
+ const r = await h({ m: 'GET', p: '/maps/api/geocode/json' });
59
+ return r.status === 200 && r.body.status === 'INVALID_REQUEST' && typeof r.body.error_message === 'string';
60
+ })),
61
+ done('googlemaps.geocode.by_place_id', 'geocoding', 'Geocoding by place_id resolves a known place', 'api', 'common', () => withRoot(async (h) => {
62
+ const r = await h({ m: 'GET', p: '/maps/api/geocode/json?place_id=ChIJOwg_06VPwokRYv534QaPC8g' });
63
+ const res = r.body.results?.[0];
64
+ return r.body.status === 'OK' && res?.formatted_address === 'New York, NY, USA';
65
+ })),
66
+ done('googlemaps.geocode.place_id_not_found', 'geocoding', 'Geocoding by unknown place_id returns ZERO_RESULTS', 'api', 'common', () => withRoot(async (h) => {
67
+ const r = await h({ m: 'GET', p: '/maps/api/geocode/json?place_id=ChIJ_does_not_exist' });
68
+ return r.body.status === 'ZERO_RESULTS';
69
+ })),
70
+ done('googlemaps.geocode.reverse', 'geocoding', 'Reverse geocoding resolves a lat/lng to an address', 'api', 'core', () => withRoot(async (h) => {
71
+ const r = await h({ m: 'GET', p: '/maps/api/geocode/json?latlng=40.714224,-73.961452' });
72
+ const res = r.body.results?.[0];
73
+ return r.body.status === 'OK'
74
+ && res?.geometry?.location?.lat === 40.714224 && res?.geometry?.location?.lng === -73.961452
75
+ && res?.geometry?.location_type === 'APPROXIMATE' && typeof res?.formatted_address === 'string';
76
+ })),
77
+ done('googlemaps.geocode.reverse_invalid', 'geocoding', 'Reverse geocoding rejects an invalid latlng', 'api', 'common', () => withRoot(async (h) => {
78
+ const r = await h({ m: 'GET', p: '/maps/api/geocode/json?latlng=not-a-coord' });
79
+ return r.body.status === 'INVALID_REQUEST';
80
+ })),
81
+ done('googlemaps.geocode.deterministic', 'geocoding', 'Geocoding is deterministic for arbitrary valid addresses', 'api', 'common', () => withRoot(async (h) => {
82
+ const a = await h({ m: 'GET', p: '/maps/api/geocode/json?address=42%20Twin%20Street' });
83
+ const b = await h({ m: 'GET', p: '/maps/api/geocode/json?address=42%20Twin%20Street' });
84
+ const la = a.body.results?.[0]?.geometry?.location;
85
+ const lb = b.body.results?.[0]?.geometry?.location;
86
+ return a.body.status === 'OK' && la?.lat === lb?.lat && la?.lng === lb?.lng;
87
+ })),
88
+ done('googlemaps.geocode.stateful_override', 'geocoding', 'Geocoding reads connector-seeded override state', 'api', 'common', () => withRoot(async (h) => {
89
+ const seeded = { address_components: [], formatted_address: 'Seeded HQ, Seedville', geometry: { location: { lat: 1.5, lng: 2.5 }, location_type: 'ROOFTOP', viewport: { northeast: { lat: 1.6, lng: 2.6 }, southwest: { lat: 1.4, lng: 2.4 } } }, place_id: 'ChIJseeded', types: ['premise'] };
90
+ await h({ m: 'POST', p: '/twin/geocode', b: { address: 'Seeded HQ', result: seeded } });
91
+ const r = await h({ m: 'GET', p: '/maps/api/geocode/json?address=Seeded%20HQ' });
92
+ const res = r.body.results?.[0];
93
+ return r.body.status === 'OK' && res?.formatted_address === 'Seeded HQ, Seedville' && res?.geometry?.location?.lat === 1.5;
94
+ })),
95
+ // ---- Distance Matrix ----
96
+ done('googlemaps.distancematrix.ok', 'distance_matrix', 'Distance Matrix returns OK rows with duration + distance', 'api', 'core', () => withRoot(async (h) => {
97
+ const r = await h({ m: 'GET', p: '/maps/api/distancematrix/json?origins=34.05,-118.24&destinations=34.10,-118.30' });
98
+ const el = r.body.rows?.[0]?.elements?.[0];
99
+ return r.status === 200 && r.body.status === 'OK'
100
+ && el?.status === 'OK'
101
+ && typeof el?.duration?.value === 'number' && typeof el?.duration?.text === 'string'
102
+ && typeof el?.distance?.value === 'number' && typeof el?.distance?.text === 'string'
103
+ && Array.isArray(r.body.origin_addresses) && Array.isArray(r.body.destination_addresses);
104
+ })),
105
+ done('googlemaps.distancematrix.matrix', 'distance_matrix', 'Distance Matrix returns a full M×N matrix', 'api', 'common', () => withRoot(async (h) => {
106
+ const r = await h({ m: 'GET', p: '/maps/api/distancematrix/json?origins=34.05,-118.24|40.71,-74.0&destinations=51.5,-0.12|35.68,139.65' });
107
+ const rows = r.body.rows;
108
+ return r.body.status === 'OK' && rows?.length === 2 && rows[0].elements.length === 2 && rows[1].elements.length === 2
109
+ && rows[0].elements[0].status === 'OK';
110
+ })),
111
+ done('googlemaps.distancematrix.element_not_found', 'distance_matrix', 'Distance Matrix marks an unresolvable element NOT_FOUND', 'api', 'common', () => withRoot(async (h) => {
112
+ const r = await h({ m: 'GET', p: '/maps/api/distancematrix/json?origins=34.05,-118.24&destinations=nowhere-zero_results' });
113
+ return r.body.status === 'OK' && r.body.rows?.[0]?.elements?.[0]?.status === 'NOT_FOUND';
114
+ })),
115
+ done('googlemaps.distancematrix.invalid_request', 'distance_matrix', 'Distance Matrix rejects missing origins/destinations', 'api', 'core', () => withRoot(async (h) => {
116
+ const r = await h({ m: 'GET', p: '/maps/api/distancematrix/json?origins=34.05,-118.24' });
117
+ return r.body.status === 'INVALID_REQUEST';
118
+ })),
119
+ done('googlemaps.distancematrix.units', 'distance_matrix', 'Distance Matrix honors metric vs imperial units', 'api', 'common', () => withRoot(async (h) => {
120
+ const imp = await h({ m: 'GET', p: '/maps/api/distancematrix/json?origins=34.05,-118.24&destinations=34.50,-118.80' });
121
+ const met = await h({ m: 'GET', p: '/maps/api/distancematrix/json?origins=34.05,-118.24&destinations=34.50,-118.80&units=metric' });
122
+ const impText = imp.body.rows?.[0]?.elements?.[0]?.distance?.text;
123
+ const metText = met.body.rows?.[0]?.elements?.[0]?.distance?.text;
124
+ return /mi$/.test(impText) && /km$/.test(metText);
125
+ })),
126
+ // ---- Directions ----
127
+ done('googlemaps.directions.ok', 'directions', 'Directions returns OK with a route, legs and polyline', 'api', 'core', () => withRoot(async (h) => {
128
+ const r = await h({ m: 'GET', p: '/maps/api/directions/json?origin=Los%20Angeles&destination=New%20York' });
129
+ const route = r.body.routes?.[0];
130
+ const leg = route?.legs?.[0];
131
+ return r.status === 200 && r.body.status === 'OK'
132
+ && typeof route?.overview_polyline?.points === 'string'
133
+ && typeof leg?.distance?.value === 'number' && typeof leg?.duration?.value === 'number'
134
+ && leg?.start_location && leg?.end_location && Array.isArray(leg?.steps) && leg.steps.length > 0;
135
+ })),
136
+ done('googlemaps.directions.geocoded_waypoints', 'directions', 'Directions includes geocoded_waypoints and bounds', 'api', 'common', () => withRoot(async (h) => {
137
+ const r = await h({ m: 'GET', p: '/maps/api/directions/json?origin=London&destination=Tokyo' });
138
+ const wp = r.body.geocoded_waypoints;
139
+ const route = r.body.routes?.[0];
140
+ return r.body.status === 'OK' && Array.isArray(wp) && wp.length === 2 && wp[0].geocoder_status === 'OK'
141
+ && route?.bounds?.northeast && route?.copyrights && Array.isArray(route?.warnings);
142
+ })),
143
+ done('googlemaps.directions.zero_results', 'directions', 'Directions returns ZERO_RESULTS when no route exists', 'api', 'common', () => withRoot(async (h) => {
144
+ const r = await h({ m: 'GET', p: '/maps/api/directions/json?origin=nowhere-zero_results&destination=London' });
145
+ return r.body.status === 'ZERO_RESULTS' && r.body.routes?.length === 0;
146
+ })),
147
+ done('googlemaps.directions.invalid_request', 'directions', 'Directions rejects a missing origin/destination', 'api', 'core', () => withRoot(async (h) => {
148
+ const r = await h({ m: 'GET', p: '/maps/api/directions/json?origin=London' });
149
+ return r.body.status === 'INVALID_REQUEST';
150
+ })),
151
+ // ---- Places (legacy) ----
152
+ done('googlemaps.places.find_place', 'places', 'Find Place from Text returns a candidate', 'api', 'common', () => withRoot(async (h) => {
153
+ const r = await h({ m: 'GET', p: '/maps/api/place/findplacefromtext/json?input=Tokyo&inputtype=textquery' });
154
+ const c = r.body.candidates?.[0];
155
+ return r.body.status === 'OK' && c?.place_id === 'ChIJ51cu8IcbXWARiRtXIothAS4' && c?.geometry?.location?.lat === 35.6761919;
156
+ })),
157
+ done('googlemaps.places.find_place_invalid', 'places', 'Find Place rejects a missing input', 'api', 'common', () => withRoot(async (h) => {
158
+ const r = await h({ m: 'GET', p: '/maps/api/place/findplacefromtext/json?inputtype=textquery' });
159
+ return r.body.status === 'INVALID_REQUEST';
160
+ })),
161
+ done('googlemaps.places.text_search', 'places', 'Text Search returns results with rating', 'api', 'common', () => withRoot(async (h) => {
162
+ const r = await h({ m: 'GET', p: '/maps/api/place/textsearch/json?query=London' });
163
+ const res = r.body.results?.[0];
164
+ return r.body.status === 'OK' && res?.place_id === 'ChIJdd4hrwug2EcRmSrV3Vo6llI' && typeof res?.rating === 'number';
165
+ })),
166
+ done('googlemaps.places.text_search_zero', 'places', 'Text Search returns ZERO_RESULTS for unmatched query', 'api', 'common', () => withRoot(async (h) => {
167
+ const r = await h({ m: 'GET', p: '/maps/api/place/textsearch/json?query=zzzznoresult' });
168
+ return r.body.status === 'ZERO_RESULTS' && r.body.results?.length === 0;
169
+ })),
170
+ done('googlemaps.places.nearby_search', 'places', 'Nearby Search returns results near a location', 'api', 'common', () => withRoot(async (h) => {
171
+ const r = await h({ m: 'GET', p: '/maps/api/place/nearbysearch/json?location=34.05,-118.24&radius=1500' });
172
+ const res = r.body.results?.[0];
173
+ return r.body.status === 'OK' && typeof res?.place_id === 'string' && typeof res?.vicinity === 'string';
174
+ })),
175
+ done('googlemaps.places.nearby_requires_radius', 'places', 'Nearby Search requires radius or rankby=distance', 'api', 'common', () => withRoot(async (h) => {
176
+ const bad = await h({ m: 'GET', p: '/maps/api/place/nearbysearch/json?location=34.05,-118.24' });
177
+ const ok = await h({ m: 'GET', p: '/maps/api/place/nearbysearch/json?location=34.05,-118.24&rankby=distance' });
178
+ return bad.body.status === 'INVALID_REQUEST' && ok.body.status === 'OK';
179
+ })),
180
+ done('googlemaps.places.details', 'places', 'Place Details returns a place with components', 'api', 'core', () => withRoot(async (h) => {
181
+ const r = await h({ m: 'GET', p: '/maps/api/place/details/json?place_id=ChIJj61dQgK6j4AR4GeTYWZsKWw' });
182
+ const res = r.body.result;
183
+ return r.body.status === 'OK' && res?.place_id === 'ChIJj61dQgK6j4AR4GeTYWZsKWw' && Array.isArray(res?.address_components) && typeof res?.url === 'string';
184
+ })),
185
+ done('googlemaps.places.details_not_found', 'places', 'Place Details returns NOT_FOUND for an unknown place', 'api', 'common', () => withRoot(async (h) => {
186
+ const r = await h({ m: 'GET', p: '/maps/api/place/details/json?place_id=ChIJ_unknown' });
187
+ return r.body.status === 'NOT_FOUND';
188
+ })),
189
+ done('googlemaps.places.autocomplete', 'places', 'Autocomplete returns predictions for a prefix', 'api', 'core', () => withRoot(async (h) => {
190
+ const r = await h({ m: 'GET', p: '/maps/api/place/autocomplete/json?input=Lon' });
191
+ const pred = r.body.predictions?.[0];
192
+ return r.body.status === 'OK' && typeof pred?.description === 'string' && typeof pred?.place_id === 'string'
193
+ && pred?.structured_formatting?.main_text === 'London';
194
+ })),
195
+ done('googlemaps.places.autocomplete_invalid', 'places', 'Autocomplete rejects a missing input', 'api', 'common', () => withRoot(async (h) => {
196
+ const r = await h({ m: 'GET', p: '/maps/api/place/autocomplete/json' });
197
+ return r.body.status === 'INVALID_REQUEST';
198
+ })),
199
+ done('googlemaps.places.query_autocomplete', 'places', 'Query Autocomplete returns predictions', 'api', 'niche', () => withRoot(async (h) => {
200
+ const r = await h({ m: 'GET', p: '/maps/api/place/queryautocomplete/json?input=pizza' });
201
+ return r.body.status === 'OK' && typeof r.body.predictions?.[0]?.description === 'string';
202
+ })),
203
+ // ---- Places API (New) ----
204
+ done('googlemaps.places_new.text_search', 'places_new', 'Places API (New) searchText returns places', 'api', 'common', () => withRoot(async (h) => {
205
+ const r = await h({ m: 'POST', p: '/v1/places:searchText', b: { textQuery: 'Tokyo' }, headers: { 'x-goog-api-key': 'k', 'x-goog-fieldmask': 'places.displayName,places.location' } });
206
+ const p = r.body.places?.[0];
207
+ return r.status === 200 && p?.displayName?.text === 'Tokyo' && p?.location?.latitude === 35.6761919 && p?.id === 'ChIJ51cu8IcbXWARiRtXIothAS4';
208
+ })),
209
+ done('googlemaps.places_new.text_search_validation', 'places_new', 'searchText rejects an empty textQuery with INVALID_ARGUMENT', 'api', 'common', () => withRoot(async (h) => {
210
+ const r = await h({ m: 'POST', p: '/v1/places:searchText', b: {}, headers: { 'x-goog-api-key': 'k' } });
211
+ return r.status === 400 && r.body.error?.status === 'INVALID_ARGUMENT';
212
+ })),
213
+ done('googlemaps.places_new.text_search_zero', 'places_new', 'searchText returns an empty object for an unmatched query', 'api', 'common', () => withRoot(async (h) => {
214
+ const zero = await h({ m: 'POST', p: '/v1/places:searchText', b: { textQuery: 'zzzznoresult' }, headers: { 'x-goog-api-key': 'k' } });
215
+ // a companion MATCHING query on the same handler must actually return place data —
216
+ // otherwise "places === undefined" for the zero-result query is indistinguishable from
217
+ // an always-empty saboteur response.
218
+ const found = await h({ m: 'POST', p: '/v1/places:searchText', b: { textQuery: 'coffee shop' }, headers: { 'x-goog-api-key': 'k' } });
219
+ const places = found.body.places;
220
+ return zero.status === 200 && zero.body.places === undefined
221
+ && found.status === 200 && Array.isArray(places) && places.length > 0 && typeof places[0]?.id === 'string';
222
+ })),
223
+ done('googlemaps.places_new.nearby_search', 'places_new', 'Places API (New) searchNearby returns places', 'api', 'common', () => withRoot(async (h) => {
224
+ const r = await h({ m: 'POST', p: '/v1/places:searchNearby', b: { locationRestriction: { circle: { center: { latitude: 34.05, longitude: -118.24 }, radius: 1000 } } }, headers: { 'x-goog-api-key': 'k' } });
225
+ return r.status === 200 && typeof r.body.places?.[0]?.id === 'string';
226
+ })),
227
+ done('googlemaps.places_new.nearby_validation', 'places_new', 'searchNearby rejects missing locationRestriction', 'api', 'common', () => withRoot(async (h) => {
228
+ const r = await h({ m: 'POST', p: '/v1/places:searchNearby', b: {}, headers: { 'x-goog-api-key': 'k' } });
229
+ return r.status === 400 && r.body.error?.status === 'INVALID_ARGUMENT';
230
+ })),
231
+ done('googlemaps.places_new.details', 'places_new', 'Places API (New) GET place details', 'api', 'common', () => withRoot(async (h) => {
232
+ const r = await h({ m: 'GET', p: '/v1/places/ChIJOwg_06VPwokRYv534QaPC8g', headers: { 'x-goog-api-key': 'k', 'x-goog-fieldmask': 'displayName' } });
233
+ return r.status === 200 && r.body.id === 'ChIJOwg_06VPwokRYv534QaPC8g' && r.body.displayName?.text === 'New York' && r.body.name === 'places/ChIJOwg_06VPwokRYv534QaPC8g';
234
+ })),
235
+ done('googlemaps.places_new.details_not_found', 'places_new', 'Places API (New) details returns 404 for unknown place', 'api', 'common', () => withRoot(async (h) => {
236
+ const r = await h({ m: 'GET', p: '/v1/places/ChIJ_unknown', headers: { 'x-goog-api-key': 'k' } });
237
+ return r.status === 404 && r.body.error?.status === 'NOT_FOUND';
238
+ })),
239
+ done('googlemaps.places_new.autocomplete', 'places_new', 'Places API (New) autocomplete returns suggestions', 'api', 'common', () => withRoot(async (h) => {
240
+ const r = await h({ m: 'POST', p: '/v1/places:autocomplete', b: { input: 'Lon' }, headers: { 'x-goog-api-key': 'k' } });
241
+ const s = r.body.suggestions?.[0]?.placePrediction;
242
+ return r.status === 200 && s?.placeId === 'ChIJdd4hrwug2EcRmSrV3Vo6llI' && s?.structuredFormat?.mainText?.text === 'London';
243
+ })),
244
+ // ---- Roads ----
245
+ done('googlemaps.roads.snap_to_roads', 'roads', 'Roads snapToRoads returns snapped points', 'api', 'common', () => withRoot(async (h) => {
246
+ const r = await h({ m: 'GET', p: '/v1/snapToRoads?path=34.05,-118.24|34.06,-118.25', headers: { 'x-goog-api-key': 'k' } });
247
+ const sp = r.body.snappedPoints;
248
+ return r.status === 200 && sp?.length === 2 && sp[0].location.latitude === 34.05 && sp[0].originalIndex === 0 && typeof sp[0].placeId === 'string';
249
+ })),
250
+ done('googlemaps.roads.snap_invalid', 'roads', 'Roads snapToRoads rejects a missing path with 400', 'api', 'common', () => withRoot(async (h) => {
251
+ const r = await h({ m: 'GET', p: '/v1/snapToRoads', headers: { 'x-goog-api-key': 'k' } });
252
+ return r.status === 400 && r.body.error?.status === 'INVALID_ARGUMENT';
253
+ })),
254
+ done('googlemaps.roads.nearest_roads', 'roads', 'Roads nearestRoads returns snapped points', 'api', 'common', () => withRoot(async (h) => {
255
+ const r = await h({ m: 'GET', p: '/v1/nearestRoads?points=60.17,24.94|60.17,24.95', headers: { 'x-goog-api-key': 'k' } });
256
+ return r.status === 200 && r.body.snappedPoints?.length === 2;
257
+ })),
258
+ done('googlemaps.roads.speed_limits', 'roads', 'Roads speedLimits returns speed limits', 'api', 'niche', () => withRoot(async (h) => {
259
+ const r = await h({ m: 'GET', p: '/v1/speedLimits?path=34.05,-118.24', headers: { 'x-goog-api-key': 'k' } });
260
+ const sl = r.body.speedLimits?.[0];
261
+ return r.status === 200 && typeof sl?.speedLimit === 'number' && sl?.units === 'KPH' && typeof sl?.placeId === 'string';
262
+ })),
263
+ // ---- Time Zone / Elevation / Geolocation ----
264
+ done('googlemaps.timezone.ok', 'timezone', 'Time Zone returns a faithful tz envelope', 'api', 'common', () => withRoot(async (h) => {
265
+ const r = await h({ m: 'GET', p: '/maps/api/timezone/json?location=34.05,-118.24&timestamp=1331161200' });
266
+ return r.status === 200 && r.body.status === 'OK'
267
+ && typeof r.body.timeZoneId === 'string' && typeof r.body.rawOffset === 'number'
268
+ && typeof r.body.timeZoneName === 'string';
269
+ })),
270
+ done('googlemaps.timezone.invalid', 'timezone', 'Time Zone rejects missing location/timestamp', 'api', 'common', () => withRoot(async (h) => {
271
+ const r = await h({ m: 'GET', p: '/maps/api/timezone/json?location=34.05,-118.24' });
272
+ return r.body.status === 'INVALID_REQUEST';
273
+ })),
274
+ done('googlemaps.elevation.locations', 'elevation', 'Elevation returns elevation for locations', 'api', 'common', () => withRoot(async (h) => {
275
+ const r = await h({ m: 'GET', p: '/maps/api/elevation/json?locations=39.73,-104.98|36.45,-116.86' });
276
+ const res = r.body.results;
277
+ return r.body.status === 'OK' && res?.length === 2 && typeof res[0].elevation === 'number' && typeof res[0].resolution === 'number';
278
+ })),
279
+ done('googlemaps.elevation.path_samples', 'elevation', 'Elevation path sampling honors `samples`', 'api', 'niche', () => withRoot(async (h) => {
280
+ const ok = await h({ m: 'GET', p: '/maps/api/elevation/json?path=36.578,-118.291|36.23,-116.83&samples=3' });
281
+ const bad = await h({ m: 'GET', p: '/maps/api/elevation/json?path=36.578,-118.291' });
282
+ return ok.body.status === 'OK' && bad.body.status === 'INVALID_REQUEST';
283
+ })),
284
+ done('googlemaps.elevation.invalid', 'elevation', 'Elevation rejects missing locations/path', 'api', 'common', () => withRoot(async (h) => {
285
+ const r = await h({ m: 'GET', p: '/maps/api/elevation/json' });
286
+ return r.body.status === 'INVALID_REQUEST';
287
+ })),
288
+ done('googlemaps.geolocation.ok', 'geolocation', 'Geolocation returns a location + accuracy', 'api', 'common', () => withRoot(async (h) => {
289
+ const r = await h({ m: 'POST', p: '/maps/api/geolocation/json', b: { considerIp: true } });
290
+ return r.status === 200 && typeof r.body.location?.lat === 'number' && typeof r.body.accuracy === 'number';
291
+ })),
292
+ done('googlemaps.geolocation.deterministic', 'geolocation', 'Geolocation is deterministic for identical input', 'api', 'niche', () => withRoot(async (h) => {
293
+ const a = await h({ m: 'POST', p: '/maps/api/geolocation/json', b: { wifiAccessPoints: [{ macAddress: '00:11' }] } });
294
+ const b = await h({ m: 'POST', p: '/maps/api/geolocation/json', b: { wifiAccessPoints: [{ macAddress: '00:11' }] } });
295
+ // a DIFFERENT input must yield a DIFFERENT location — otherwise "same lat for same
296
+ // input" is trivially true of an always-undefined saboteur response too.
297
+ const c = await h({ m: 'POST', p: '/maps/api/geolocation/json', b: { wifiAccessPoints: [{ macAddress: 'ff:ee' }] } });
298
+ const latA = a.body.location?.lat;
299
+ return typeof latA === 'number' && typeof a.body.location?.lng === 'number' && typeof a.body.accuracy === 'number'
300
+ && latA === b.body.location?.lat
301
+ && latA !== c.body.location?.lat;
302
+ })),
303
+ // ---- Address Validation ----
304
+ done('googlemaps.address_validation.ok', 'address_validation', 'Address Validation returns a verdict + geocode', 'api', 'common', () => withRoot(async (h) => {
305
+ const r = await h({ m: 'POST', p: '/v1:validateAddress', b: { address: { addressLines: ['1600 Amphitheatre Pkwy, Mountain View'] } }, headers: { 'x-goog-api-key': 'k' } });
306
+ const result = r.body.result;
307
+ return r.status === 200 && result?.verdict?.addressComplete === true
308
+ && typeof result?.geocode?.location?.latitude === 'number' && typeof r.body.responseId === 'string';
309
+ })),
310
+ done('googlemaps.address_validation.unconfirmed', 'address_validation', 'Address Validation flags unconfirmed components', 'api', 'niche', () => withRoot(async (h) => {
311
+ const r = await h({ m: 'POST', p: '/v1:validateAddress', b: { address: { addressLines: ['some unknown route 9999'] } }, headers: { 'x-goog-api-key': 'k' } });
312
+ return r.status === 200 && r.body.result?.verdict?.hasUnconfirmedComponents === true;
313
+ })),
314
+ done('googlemaps.address_validation.invalid', 'address_validation', 'Address Validation rejects empty addressLines', 'api', 'common', () => withRoot(async (h) => {
315
+ const r = await h({ m: 'POST', p: '/v1:validateAddress', b: { address: { addressLines: [] } }, headers: { 'x-goog-api-key': 'k' } });
316
+ return r.status === 400 && r.body.error?.status === 'INVALID_ARGUMENT';
317
+ })),
318
+ // ---- Static Maps / Street View ----
319
+ done('googlemaps.staticmap.metadata', 'staticmap', 'Static Maps returns image metadata for a valid request', 'api', 'common', () => withRoot(async (h) => {
320
+ const r = await h({ m: 'GET', p: '/maps/api/staticmap?center=34.05,-118.24&zoom=12&size=600x400' });
321
+ return r.status === 200 && r.body.contentType === 'image/png' && r.body.size === '600x400';
322
+ })),
323
+ done('googlemaps.staticmap.invalid', 'staticmap', 'Static Maps rejects a missing size', 'api', 'common', () => withRoot(async (h) => {
324
+ const r = await h({ m: 'GET', p: '/maps/api/staticmap?center=34.05,-118.24' });
325
+ return r.status === 400 && r.body.error?.status === 'INVALID_ARGUMENT';
326
+ })),
327
+ done('googlemaps.streetview.metadata_endpoint', 'streetview', 'Street View metadata returns OK with a pano id', 'api', 'common', () => withRoot(async (h) => {
328
+ const r = await h({ m: 'GET', p: '/maps/api/streetview/metadata?location=34.05,-118.24' });
329
+ return r.status === 200 && r.body.status === 'OK' && typeof r.body.pano_id === 'string';
330
+ })),
331
+ done('googlemaps.streetview.metadata_zero', 'streetview', 'Street View metadata returns ZERO_RESULTS where no pano exists', 'api', 'niche', () => withRoot(async (h) => {
332
+ const r = await h({ m: 'GET', p: '/maps/api/streetview/metadata?location=zzzznoresult-nowhere' });
333
+ return r.body.status === 'ZERO_RESULTS';
334
+ })),
335
+ done('googlemaps.streetview.image_metadata', 'streetview', 'Street View image endpoint validates size', 'api', 'common', () => withRoot(async (h) => {
336
+ const ok = await h({ m: 'GET', p: '/maps/api/streetview?location=34.05,-118.24&size=600x400' });
337
+ const bad = await h({ m: 'GET', p: '/maps/api/streetview?location=34.05,-118.24' });
338
+ return ok.body.contentType === 'image/jpeg' && bad.status === 400;
339
+ })),
340
+ // ---- Auth / errors / honesty ----
341
+ done('googlemaps.auth.request_denied', 'errors', 'Missing API key yields REQUEST_DENIED on a legacy service', 'api', 'core', () => withRoot(async (h) => {
342
+ const r = await h({ m: 'GET', p: '/maps/api/geocode/json?address=Los%20Angeles', headers: {} });
343
+ return r.status === 200 && r.body.status === 'REQUEST_DENIED' && typeof r.body.error_message === 'string';
344
+ })),
345
+ done('googlemaps.auth.valid_key', 'errors', 'A valid key passes the gate', 'api', 'core', () => withRoot(async (h) => {
346
+ const r = await h({ m: 'GET', p: '/maps/api/geocode/json?address=Los%20Angeles&key=AIzaTwinKey', headers: {} });
347
+ return r.status === 200 && r.body.status === 'OK';
348
+ })),
349
+ done('googlemaps.auth.over_query_limit', 'errors', 'Sentinel key yields OVER_QUERY_LIMIT', 'api', 'common', () => withRoot(async (h) => {
350
+ const r = await h({ m: 'GET', p: '/maps/api/geocode/json?address=Los%20Angeles&key=over-query-limit', headers: {} });
351
+ return r.body.status === 'OVER_QUERY_LIMIT';
352
+ })),
353
+ done('googlemaps.auth.over_daily_limit', 'errors', 'Sentinel key yields OVER_DAILY_LIMIT', 'api', 'niche', () => withRoot(async (h) => {
354
+ const r = await h({ m: 'GET', p: '/maps/api/geocode/json?address=Los%20Angeles&key=over-daily-limit', headers: {} });
355
+ return r.body.status === 'OVER_DAILY_LIMIT';
356
+ })),
357
+ done('googlemaps.auth.v1_permission_denied', 'errors', 'Missing key yields a 403 PERMISSION_DENIED on a v1 service', 'api', 'common', () => withRoot(async (h) => {
358
+ const r = await h({ m: 'POST', p: '/v1/places:searchText', b: { textQuery: 'Tokyo' }, headers: {} });
359
+ return r.status === 403 && r.body.error?.status === 'PERMISSION_DENIED';
360
+ })),
361
+ done('googlemaps.honesty.unmodeled_legacy', 'honesty', 'Unmodeled legacy op fails like the vendor (INVALID_REQUEST)', 'api', 'core', () => withRoot(async (h) => {
362
+ const r = await h({ m: 'GET', p: '/maps/api/unknown/json?key=k' });
363
+ return r.body.status === 'INVALID_REQUEST';
364
+ })),
365
+ done('googlemaps.honesty.unmodeled_v1', 'honesty', 'Unmodeled v1 op fails with a 404 NOT_FOUND', 'api', 'common', () => withRoot(async (h) => {
366
+ const r = await h({ m: 'GET', p: '/v1/unknownThing', headers: { 'x-goog-api-key': 'k' } });
367
+ return r.status === 404 && r.body.error?.status === 'NOT_FOUND';
368
+ })),
369
+ done('googlemaps.honesty.readonly_rejects_seed', 'honesty', 'readOnly forbids twin write endpoints (405)', 'api', 'common', () => withRoot(async (h) => {
370
+ const r = await h({ m: 'POST', p: '/twin/geocode', b: { address: 'x', result: {} }, readOnly: true });
371
+ return r.status === 405;
372
+ })),
373
+ done('googlemaps.audit.request_logging', 'audit', 'Local audit log records requests; read-only is not logged', 'api', 'niche', () => withRoot(async (h) => {
374
+ await h({ m: 'GET', p: '/maps/api/geocode/json?address=Los%20Angeles' });
375
+ await h({ m: 'GET', p: '/maps/api/directions/json?origin=London&destination=Tokyo' });
376
+ await h({ m: 'GET', p: '/maps/api/geocode/json?address=Los%20Angeles', readOnly: true });
377
+ const log = await h({ m: 'GET', p: '/twin/audit' });
378
+ const entries = log.body.entries;
379
+ return entries?.length === 2 && entries.some((e) => e.api === 'geocode' && e.status === 'OK') && entries.some((e) => e.api === 'directions');
380
+ })),
381
+ // ---- Connector ----
382
+ done('googlemaps.connector.pull', 'connector', 'Connector pulls geocode overrides through an injected client idempotently', 'connector', 'common', async () => {
383
+ const root = mkdtempSync(join(tmpdir(), 'googlemaps-conn-'));
384
+ try {
385
+ const execute = async () => ({ status: 200, data: { status: 'OK', results: [{ formatted_address: 'Pulled, Earth', geometry: { location: { lat: 9, lng: 9 } } }] } });
386
+ const n1 = await pullGoogleMapsGeocodes(execute, ['Pulled Place'], { root });
387
+ await pullGoogleMapsGeocodes(execute, ['Pulled Place'], { root });
388
+ const rows = projectResources('googlemaps', root).filter((r) => r.type === 'geocode');
389
+ return n1 === 1 && rows.length === 1 && rows[0].result?.formatted_address === 'Pulled, Earth';
390
+ }
391
+ finally {
392
+ rmSync(root, { recursive: true, force: true });
393
+ }
394
+ }),
395
+ done('googlemaps.connector.push', 'connector', 'Connector pushes pending geocode seeds through an injected client idempotently', 'connector', 'common', async () => {
396
+ const root = mkdtempSync(join(tmpdir(), 'googlemaps-conn-'));
397
+ try {
398
+ // A local seed creates a pending geocode.upsert action over a forked base.
399
+ const { applyTwinWrite } = await import('@volter/world-core');
400
+ await applyTwinWrite('googlemaps', {
401
+ operation: 'geocode.upsert',
402
+ subjectType: 'geocode',
403
+ subjectId: 'geocode_pushcap',
404
+ fields: { id: 'geocode_pushcap', normalized: 'push hq', address: 'Push HQ', result: { formatted_address: 'Push HQ' } },
405
+ occurredAt: '1970-01-01T00:00:00.000Z',
406
+ actor: { kind: 'system' },
407
+ }, root);
408
+ const { pendingActions } = await import('@volter/world-core');
409
+ const paths = [];
410
+ const first = await performPending(async (req) => { paths.push(req.path); return { status: 200, data: {} }; }, { root });
411
+ const second = await performPending(async () => ({ status: 200, data: {} }), { root });
412
+ return first === 1 && second === 0 && pendingActions('googlemaps', root).length === 0 && paths[0] === '/twin/geocode';
413
+ }
414
+ finally {
415
+ rmSync(root, { recursive: true, force: true });
416
+ }
417
+ }),
418
+ // ---- Geocoding niche ----
419
+ done('googlemaps.geocode.plus_codes', 'geocoding', 'Plus Codes (global_code / compound_code) in geocode results', 'api', 'niche', () => withRoot(async (h) => {
420
+ const r = await h({ m: 'GET', p: '/maps/api/geocode/json?address=Los%20Angeles' });
421
+ const pc = r.body.results?.[0]?.plus_code;
422
+ return r.body.status === 'OK'
423
+ && typeof pc?.global_code === 'string' && /\+/.test(pc.global_code) && pc.global_code.length >= 8
424
+ && typeof pc?.compound_code === 'string' && pc.compound_code.includes('Los Angeles');
425
+ })),
426
+ done('googlemaps.geocode.components_filter', 'geocoding', 'Geocoding components filter (country constraint → ZERO_RESULTS on mismatch)', 'api', 'niche', () => withRoot(async (h) => {
427
+ const ok = await h({ m: 'GET', p: '/maps/api/geocode/json?address=London&components=country:GB' });
428
+ const miss = await h({ m: 'GET', p: '/maps/api/geocode/json?address=London&components=country:JP' });
429
+ return ok.body.status === 'OK' && ok.body.results?.[0]?.place_id === 'ChIJdd4hrwug2EcRmSrV3Vo6llI'
430
+ && miss.body.status === 'ZERO_RESULTS' && miss.body.results?.length === 0;
431
+ })),
432
+ done('googlemaps.geocode.region_biasing', 'geocoding', 'Geocoding bounds biasing snaps a synthesized result into the requested viewport', 'api', 'niche', () => withRoot(async (h) => {
433
+ // A non-canned address synthesizes a location; bounds must steer it into range.
434
+ const free = await h({ m: 'GET', p: '/maps/api/geocode/json?address=42%20Twin%20Street' });
435
+ const freeLoc = free.body.results?.[0]?.geometry?.location;
436
+ const biased = await h({ m: 'GET', p: '/maps/api/geocode/json?address=42%20Twin%20Street&region=us&bounds=40.0,-75.0|41.0,-73.0' });
437
+ const bLoc = biased.body.results?.[0]?.geometry?.location;
438
+ // Free location is somewhere on the globe; biased one is clamped into the box.
439
+ return biased.body.status === 'OK'
440
+ && bLoc?.lat >= 40.0 && bLoc?.lat <= 41.0 && bLoc?.lng >= -75.0 && bLoc?.lng <= -73.0
441
+ && !(freeLoc?.lat >= 40.0 && freeLoc?.lat <= 41.0 && freeLoc?.lng >= -75.0 && freeLoc?.lng <= -73.0);
442
+ })),
443
+ done('googlemaps.geocode.language', 'geocoding', 'Geocoding language parameter localizes formatted_address', 'api', 'niche', () => withRoot(async (h) => {
444
+ const en = await h({ m: 'GET', p: '/maps/api/geocode/json?address=Tokyo&language=en' });
445
+ const ja = await h({ m: 'GET', p: '/maps/api/geocode/json?address=Tokyo&language=ja' });
446
+ const enAddr = en.body.results?.[0]?.formatted_address;
447
+ const jaAddr = ja.body.results?.[0]?.formatted_address;
448
+ return enAddr === 'Tokyo, Japan' && jaAddr === '日本、東京都' && jaAddr !== enAddr
449
+ && en.body.results?.[0]?.place_id === ja.body.results?.[0]?.place_id;
450
+ })),
451
+ // ---- Directions ----
452
+ done('googlemaps.directions.waypoints', 'directions', 'Directions waypoints add legs; optimize:true reorders waypoint_order', 'api', 'common', () => withRoot(async (h) => {
453
+ const plain = await h({ m: 'GET', p: '/maps/api/directions/json?origin=Los%20Angeles&destination=New%20York&waypoints=Tokyo|London' });
454
+ const pr = plain.body.routes?.[0];
455
+ const opt = await h({ m: 'GET', p: '/maps/api/directions/json?origin=Los%20Angeles&destination=New%20York&waypoints=optimize:true|Tokyo|London' });
456
+ const or = opt.body.routes?.[0];
457
+ return plain.body.status === 'OK' && pr?.legs?.length === 3 && JSON.stringify(pr.waypoint_order) === '[0,1]'
458
+ && or?.legs?.length === 3 && Array.isArray(or.waypoint_order) && or.waypoint_order.length === 2
459
+ && JSON.stringify(or.waypoint_order) !== '[0,1]';
460
+ })),
461
+ done('googlemaps.directions.alternatives', 'directions', 'Directions alternatives=true returns multiple distinct routes', 'api', 'common', () => withRoot(async (h) => {
462
+ const r = await h({ m: 'GET', p: '/maps/api/directions/json?origin=Los%20Angeles&destination=New%20York&alternatives=true' });
463
+ const routes = r.body.routes;
464
+ return r.body.status === 'OK' && routes?.length === 2
465
+ && routes[1].legs[0].duration.value > routes[0].legs[0].duration.value;
466
+ })),
467
+ done('googlemaps.directions.travel_modes', 'directions', 'Directions walking/bicycling/transit modes scale duration + set travel_mode', 'api', 'common', () => withRoot(async (h) => {
468
+ const drive = await h({ m: 'GET', p: '/maps/api/directions/json?origin=Los%20Angeles&destination=New%20York&mode=driving' });
469
+ const walk = await h({ m: 'GET', p: '/maps/api/directions/json?origin=Los%20Angeles&destination=New%20York&mode=walking' });
470
+ const dStep = drive.body.routes?.[0]?.legs?.[0]?.steps?.[0];
471
+ const wStep = walk.body.routes?.[0]?.legs?.[0]?.steps?.[0];
472
+ return dStep?.travel_mode === 'DRIVING' && wStep?.travel_mode === 'WALKING'
473
+ && wStep.duration.value > dStep.duration.value;
474
+ })),
475
+ done('googlemaps.directions.departure_traffic', 'directions', 'Directions departure_time adds duration_in_traffic to each leg', 'api', 'common', () => withRoot(async (h) => {
476
+ const r = await h({ m: 'GET', p: '/maps/api/directions/json?origin=Los%20Angeles&destination=New%20York&departure_time=now' });
477
+ const leg = r.body.routes?.[0]?.legs?.[0];
478
+ return r.body.status === 'OK' && typeof leg?.duration_in_traffic?.value === 'number'
479
+ && leg.duration_in_traffic.value > leg.duration.value && typeof leg.duration_in_traffic.text === 'string';
480
+ })),
481
+ done('googlemaps.directions.avoid', 'directions', 'Directions avoid=tolls|highways records route warnings', 'api', 'niche', () => withRoot(async (h) => {
482
+ const r = await h({ m: 'GET', p: '/maps/api/directions/json?origin=Los%20Angeles&destination=New%20York&avoid=tolls|ferries' });
483
+ const w = r.body.routes?.[0]?.warnings;
484
+ return r.body.status === 'OK' && Array.isArray(w) && w.some((m) => /tolls/.test(m)) && w.some((m) => /ferries/.test(m));
485
+ })),
486
+ done('googlemaps.directions.transit_details', 'directions', 'Directions transit mode adds transit_details (line, stops)', 'api', 'niche', () => withRoot(async (h) => {
487
+ const r = await h({ m: 'GET', p: '/maps/api/directions/json?origin=London&destination=Tokyo&mode=transit' });
488
+ const td = r.body.routes?.[0]?.legs?.[0]?.steps?.[0]?.transit_details;
489
+ return r.body.status === 'OK' && td?.line?.vehicle?.type === 'HEAVY_RAIL'
490
+ && typeof td?.num_stops === 'number' && typeof td?.departure_stop?.name === 'string';
491
+ })),
492
+ // ---- Distance Matrix ----
493
+ done('googlemaps.distancematrix.travel_modes', 'distance_matrix', 'Distance Matrix travel modes + departure_time traffic', 'api', 'common', () => withRoot(async (h) => {
494
+ const walk = await h({ m: 'GET', p: '/maps/api/distancematrix/json?origins=34.05,-118.24&destinations=34.50,-118.80&mode=walking' });
495
+ const drive = await h({ m: 'GET', p: '/maps/api/distancematrix/json?origins=34.05,-118.24&destinations=34.50,-118.80&mode=driving&departure_time=now' });
496
+ const wEl = walk.body.rows?.[0]?.elements?.[0];
497
+ const dEl = drive.body.rows?.[0]?.elements?.[0];
498
+ return wEl?.status === 'OK' && dEl?.status === 'OK'
499
+ && wEl.duration.value > dEl.duration.value
500
+ && typeof dEl.duration_in_traffic?.value === 'number' && dEl.duration_in_traffic.value > dEl.duration.value;
501
+ })),
502
+ done('googlemaps.distancematrix.max_elements', 'distance_matrix', 'Distance Matrix enforces MAX_ELEMENTS_EXCEEDED over 100 elements', 'api', 'niche', () => withRoot(async (h) => {
503
+ const origins = Array.from({ length: 11 }, (_v, i) => `34.${i},-118.2`).join('|');
504
+ const dests = Array.from({ length: 11 }, (_v, i) => `35.${i},-118.3`).join('|');
505
+ const r = await h({ m: 'GET', p: `/maps/api/distancematrix/json?origins=${origins}&destinations=${dests}` });
506
+ const ok = await h({ m: 'GET', p: '/maps/api/distancematrix/json?origins=34.05,-118.24&destinations=34.50,-118.80' });
507
+ return r.body.status === 'MAX_ELEMENTS_EXCEEDED' && ok.body.status === 'OK';
508
+ })),
509
+ // ---- Places (legacy) ----
510
+ done('googlemaps.places.photos', 'places', 'Place Details exposes photo_reference; Place Photo endpoint returns metadata', 'api', 'common', () => withRoot(async (h) => {
511
+ const d = await h({ m: 'GET', p: '/maps/api/place/details/json?place_id=ChIJj61dQgK6j4AR4GeTYWZsKWw&fields=photos' });
512
+ const ref = d.body.result?.photos?.[0]?.photo_reference;
513
+ const photo = await h({ m: 'GET', p: `/maps/api/place/photo?maxwidth=400&photo_reference=${ref}` });
514
+ const bad = await h({ m: 'GET', p: '/maps/api/place/photo?photo_reference=x' });
515
+ return typeof ref === 'string' && photo.body.contentType === 'image/jpeg' && photo.body.photo_reference === ref
516
+ && bad.body.status === 'INVALID_REQUEST';
517
+ })),
518
+ done('googlemaps.places.pagination', 'places', 'Text/Nearby Search next_page_token paginates deterministically', 'api', 'common', () => withRoot(async (h) => {
519
+ const p1 = await h({ m: 'GET', p: '/maps/api/place/textsearch/json?query=London' });
520
+ const tok = p1.body.next_page_token;
521
+ const p2 = await h({ m: 'GET', p: `/maps/api/place/textsearch/json?pagetoken=${encodeURIComponent(tok)}` });
522
+ return p1.body.status === 'OK' && p1.body.results?.length === 20 && typeof tok === 'string'
523
+ && p2.body.results?.length === 3
524
+ && p1.body.results[0].place_id !== p2.body.results[0].place_id;
525
+ })),
526
+ done('googlemaps.places.opening_hours', 'places', 'Place Details returns opening_hours / current_opening_hours', 'api', 'common', () => withRoot(async (h) => {
527
+ const r = await h({ m: 'GET', p: '/maps/api/place/details/json?place_id=ChIJj61dQgK6j4AR4GeTYWZsKWw' });
528
+ const oh = r.body.result?.opening_hours;
529
+ return r.body.status === 'OK' && oh?.open_now === true
530
+ && Array.isArray(oh?.weekday_text) && oh.weekday_text.length === 7
531
+ && oh.periods?.[0]?.open?.time === '0900' && r.body.result?.current_opening_hours?.open_now === true;
532
+ })),
533
+ done('googlemaps.places.reviews', 'places', 'Place Details returns reviews + editorial_summary', 'api', 'niche', () => withRoot(async (h) => {
534
+ const r = await h({ m: 'GET', p: '/maps/api/place/details/json?place_id=ChIJj61dQgK6j4AR4GeTYWZsKWw' });
535
+ const rev = r.body.result?.reviews?.[0];
536
+ return r.body.status === 'OK' && typeof rev?.author_name === 'string' && typeof rev?.rating === 'number'
537
+ && typeof rev?.text === 'string' && typeof r.body.result?.editorial_summary?.overview === 'string';
538
+ })),
539
+ done('googlemaps.places.field_masks', 'places', 'Place Details `fields` masks the response to requested fields only', 'api', 'common', () => withRoot(async (h) => {
540
+ const r = await h({ m: 'GET', p: '/maps/api/place/details/json?place_id=ChIJj61dQgK6j4AR4GeTYWZsKWw&fields=place_id,name' });
541
+ const res = r.body.result;
542
+ return r.body.status === 'OK' && res?.place_id === 'ChIJj61dQgK6j4AR4GeTYWZsKWw' && res?.name === 'Mountain View'
543
+ && res?.formatted_address === undefined && res?.reviews === undefined && Object.keys(res).length === 2;
544
+ })),
545
+ // ---- Places API (New) ----
546
+ done('googlemaps.places_new.field_mask_enforcement', 'places_new', 'X-Goog-FieldMask restricts searchText / details response fields', 'api', 'common', () => withRoot(async (h) => {
547
+ const s = await h({ m: 'POST', p: '/v1/places:searchText', b: { textQuery: 'Tokyo' }, headers: { 'x-goog-api-key': 'k', 'x-goog-fieldmask': 'places.id,places.displayName' } });
548
+ const pl = s.body.places?.[0];
549
+ const d = await h({ m: 'GET', p: '/v1/places/ChIJOwg_06VPwokRYv534QaPC8g', headers: { 'x-goog-api-key': 'k', 'x-goog-fieldmask': 'id' } });
550
+ return pl?.id === 'ChIJ51cu8IcbXWARiRtXIothAS4' && pl?.displayName?.text === 'Tokyo' && pl?.location === undefined && pl?.types === undefined
551
+ && d.body.id === 'ChIJOwg_06VPwokRYv534QaPC8g' && d.body.displayName === undefined;
552
+ })),
553
+ done('googlemaps.places_new.photos', 'places_new', 'Places API (New) place exposes photos; media endpoint returns photoUri', 'api', 'common', () => withRoot(async (h) => {
554
+ const d = await h({ m: 'GET', p: '/v1/places/ChIJOwg_06VPwokRYv534QaPC8g', headers: { 'x-goog-api-key': 'k', 'x-goog-fieldmask': 'photos' } });
555
+ const name = d.body.photos?.[0]?.name;
556
+ const ref = name.split('/').pop();
557
+ const media = await h({ m: 'GET', p: `/v1/places/ChIJOwg_06VPwokRYv534QaPC8g/photos/${ref}/media?maxWidthPx=400`, headers: { 'x-goog-api-key': 'k' } });
558
+ const bad = await h({ m: 'GET', p: `/v1/places/ChIJOwg_06VPwokRYv534QaPC8g/photos/${ref}/media`, headers: { 'x-goog-api-key': 'k' } });
559
+ return typeof name === 'string' && typeof media.body.photoUri === 'string' && media.body.contentType === 'image/jpeg'
560
+ && bad.status === 400 && bad.body.error?.status === 'INVALID_ARGUMENT';
561
+ })),
562
+ done('googlemaps.places_new.pagination', 'places_new', 'Places API (New) pageToken/pageSize pagination', 'api', 'niche', () => withRoot(async (h) => {
563
+ const p1 = await h({ m: 'POST', p: '/v1/places:searchText', b: { textQuery: 'London', pageSize: 10 }, headers: { 'x-goog-api-key': 'k' } });
564
+ const tok = p1.body.nextPageToken;
565
+ const p2 = await h({ m: 'POST', p: '/v1/places:searchText', b: { textQuery: 'London', pageToken: tok }, headers: { 'x-goog-api-key': 'k' } });
566
+ return p1.body.places?.length === 10 && typeof tok === 'string'
567
+ && Array.isArray(p2.body.places) && p2.body.places.length > 0
568
+ && p1.body.places[0].id !== p2.body.places[0].id;
569
+ })),
570
+ // ---- Roads / TimeZone ----
571
+ done('googlemaps.roads.interpolate', 'roads', 'Roads snapToRoads interpolate=true densifies with index-less points', 'api', 'niche', () => withRoot(async (h) => {
572
+ const plain = await h({ m: 'GET', p: '/v1/snapToRoads?path=34.05,-118.24|34.06,-118.25', headers: { 'x-goog-api-key': 'k' } });
573
+ const inter = await h({ m: 'GET', p: '/v1/snapToRoads?interpolate=true&path=34.05,-118.24|34.06,-118.25', headers: { 'x-goog-api-key': 'k' } });
574
+ const ip = inter.body.snappedPoints;
575
+ return plain.body.snappedPoints?.length === 2 && ip?.length === 3
576
+ && ip[0].originalIndex === 0 && ip[1].originalIndex === undefined && ip[2].originalIndex === 1;
577
+ })),
578
+ done('googlemaps.timezone.dst', 'timezone', 'Time Zone computes a non-zero dstOffset for a summer timestamp in a DST zone', 'api', 'niche', () => withRoot(async (h) => {
579
+ // Find a location whose deterministic zone observes DST, then probe summer vs winter.
580
+ for (const loc of ['34.05,-118.24', '40.71,-74.0', '51.5,-0.12', '48.0,2.0', '45.0,-100.0']) {
581
+ const summer = await h({ m: 'GET', p: `/maps/api/timezone/json?location=${loc}&timestamp=1656633600` }); // 2022-07-01
582
+ const winter = await h({ m: 'GET', p: `/maps/api/timezone/json?location=${loc}&timestamp=1640995200` }); // 2022-01-01
583
+ if (summer.body.dstOffset === 3600 && winter.body.dstOffset === 0
584
+ && summer.body.timeZoneName !== winter.body.timeZoneName)
585
+ return true;
586
+ }
587
+ return false;
588
+ })),
589
+ // ---- Address Validation / Static Maps ----
590
+ done('googlemaps.address_validation.components', 'address_validation', 'Address Validation returns per-component confirmationLevel', 'api', 'niche', () => withRoot(async (h) => {
591
+ const ok = await h({ m: 'POST', p: '/v1:validateAddress', b: { address: { addressLines: ['1600 Amphitheatre Pkwy, Mountain View'] } }, headers: { 'x-goog-api-key': 'k' } });
592
+ const comp = ok.body.result?.address?.addressComponents?.[0];
593
+ const bad = await h({ m: 'POST', p: '/v1:validateAddress', b: { address: { addressLines: ['unknown route 9999'] } }, headers: { 'x-goog-api-key': 'k' } });
594
+ const badComp = bad.body.result?.address?.addressComponents?.[0];
595
+ return comp?.confirmationLevel === 'CONFIRMED' && typeof comp?.componentType === 'string'
596
+ && badComp?.confirmationLevel === 'UNCONFIRMED_BUT_PLAUSIBLE';
597
+ })),
598
+ done('googlemaps.address_validation.usps', 'address_validation', 'Address Validation populates USPS CASS metadata for US addresses only', 'api', 'niche', () => withRoot(async (h) => {
599
+ const us = await h({ m: 'POST', p: '/v1:validateAddress', b: { address: { addressLines: ['1600 Amphitheatre Pkwy, Mountain View'] } }, headers: { 'x-goog-api-key': 'k' } });
600
+ const nonUs = await h({ m: 'POST', p: '/v1:validateAddress', b: { address: { addressLines: ['London'] } }, headers: { 'x-goog-api-key': 'k' } });
601
+ return typeof us.body.result?.uspsData?.standardizedAddress?.zipCode === 'string'
602
+ && us.body.result?.uspsData?.dpvConfirmation === 'Y'
603
+ && nonUs.body.result?.uspsData === undefined;
604
+ })),
605
+ done('googlemaps.staticmap.styling', 'staticmap', 'Static Maps parses style/markers/path/maptype; rejects an invalid maptype', 'api', 'niche', () => withRoot(async (h) => {
606
+ const r = await h({ m: 'GET', p: '/maps/api/staticmap?center=34.05,-118.24&zoom=12&size=600x400&maptype=satellite&markers=color:red|34.05,-118.24&style=feature:road|color:0x000000' });
607
+ const bad = await h({ m: 'GET', p: '/maps/api/staticmap?center=34.05,-118.24&size=600x400&maptype=bogus' });
608
+ return r.body.maptype === 'satellite' && r.body.markers?.length === 1 && r.body.style?.length === 1
609
+ && bad.status === 400 && bad.body.error?.status === 'INVALID_ARGUMENT';
610
+ })),
611
+ // ---- Routes API (New) ----
612
+ done('googlemaps.routes_api.compute_routes', 'routes_api', 'Routes API (New) computeRoutes returns routes with distance/duration/polyline', 'api', 'common', () => withRoot(async (h) => {
613
+ const r = await h({
614
+ m: 'POST', p: '/directions/v2:computeRoutes',
615
+ b: { origin: { location: { latLng: { latitude: 34.05, longitude: -118.24 } } }, destination: { location: { latLng: { latitude: 40.71, longitude: -74.0 } } }, travelMode: 'DRIVE', computeAlternativeRoutes: true },
616
+ headers: { 'x-goog-api-key': 'k', 'x-goog-fieldmask': 'routes.distanceMeters,routes.duration,routes.polyline.encodedPolyline' },
617
+ });
618
+ const routes = r.body.routes;
619
+ const bad = await h({ m: 'POST', p: '/directions/v2:computeRoutes', b: { origin: { location: { latLng: { latitude: 34.05, longitude: -118.24 } } } }, headers: { 'x-goog-api-key': 'k' } });
620
+ return r.status === 200 && routes?.length === 2 && typeof routes[0].distanceMeters === 'number'
621
+ && /^\d+s$/.test(routes[0].duration) && typeof routes[0].polyline?.encodedPolyline === 'string'
622
+ && routes[0].legs === undefined // masked out
623
+ && bad.status === 400 && bad.body.error?.status === 'INVALID_ARGUMENT';
624
+ })),
625
+ done('googlemaps.routes_api.compute_route_matrix', 'routes_api', 'Routes API (New) computeRouteMatrix returns indexed elements with condition', 'api', 'common', () => withRoot(async (h) => {
626
+ const r = await h({
627
+ m: 'POST', p: '/distanceMatrix/v2:computeRouteMatrix',
628
+ b: {
629
+ origins: [{ waypoint: { location: { latLng: { latitude: 34.05, longitude: -118.24 } } } }, { waypoint: { location: { latLng: { latitude: 40.71, longitude: -74.0 } } } }],
630
+ destinations: [{ waypoint: { location: { latLng: { latitude: 51.5, longitude: -0.12 } } } }],
631
+ travelMode: 'DRIVE',
632
+ },
633
+ headers: { 'x-goog-api-key': 'k', 'x-goog-fieldmask': 'originIndex,destinationIndex,duration,distanceMeters,condition' },
634
+ });
635
+ const arr = r.body;
636
+ const bad = await h({ m: 'POST', p: '/distanceMatrix/v2:computeRouteMatrix', b: { origins: [] }, headers: { 'x-goog-api-key': 'k' } });
637
+ return r.status === 200 && Array.isArray(arr) && arr.length === 2
638
+ && arr[0].originIndex === 0 && arr[0].destinationIndex === 0 && arr[0].condition === 'ROUTE_EXISTS'
639
+ && typeof arr[0].distanceMeters === 'number' && /^\d+s$/.test(arr[0].duration)
640
+ && bad.status === 400 && bad.body.error?.status === 'INVALID_ARGUMENT';
641
+ })),
642
+ // ---- still genuinely-unbuilt (live denominator) ----
643
+ todo('googlemaps.places.session_tokens', 'places', 'Autocomplete sessiontoken billing semantics', 'api', 'niche'),
644
+ todo('googlemaps.geolocation.cell_wifi_trilateration', 'geolocation', 'Geolocation cell/wifi trilateration fidelity', 'api', 'niche'),
645
+ ];
646
+ export async function googlemapsCapabilities() {
647
+ return checkCapabilities('googlemaps', GOOGLEMAPS_CAPABILITIES);
648
+ }