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