@admoai/sdk-scenarios 0.0.0-stage → 1.0.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ Versioning: a scenario added or an `expect` tightened is a **minor** release; a scenario
4
+ removed, or a `request`/`expect` key renamed (every SDK's interpreter must change), is a
5
+ **major** one. Each SDK vendors one version and its own CI checks the vendored bytes against it.
6
+
7
+ ## 1.0.0 — 2026-10-20
8
+
9
+ The 39 scenarios the three SDKs already carried, byte for byte (sha256 `a55f68eb…`):
10
+ L1 L5 L6 L7 L8 · M1 M2 M3 M5 M6 M7 M8 · P1 P2 P3 P6 P8 · V1 V3 V4 V5 V6 V9 · Z1 Z2 · SP1–SP14.
11
+ First release from adhub, the source of truth since admoai/adhub#4158; before it the file was
12
+ mirrored into each SDK repo and held equal by a hash in each and a parity check in adhub.
package/README.md CHANGED
@@ -1,3 +1,27 @@
1
- # Temporary Holding Version
1
+ # @admoai/sdk-scenarios
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The scenarios every AdMoai mobile SDK (iOS, Android, Flutter) must pass against a real
4
+ decision-engine: given a request, the outcome a publisher's app observes. One definition,
5
+ executed by a thin interpreter in each SDK, so the three cannot make different claims.
6
+
7
+ This package is **data only**: `scenarios.json` (the manifest, `schemaVersion` inside). It
8
+ carries no URLs, hosts, credentials or tenant identifiers — `check.sh` refuses a release that
9
+ does.
10
+
11
+ ## For an SDK
12
+
13
+ Vendor one version and pin it; your CI verifies the vendored bytes against the published
14
+ tarball, and your tests read the vendored file (they never touch the network):
15
+
16
+ ```bash
17
+ curl -sSfL "https://registry.npmjs.org/@admoai/sdk-scenarios/-/sdk-scenarios-$VERSION.tgz" \
18
+ | tar -xzO package/scenarios.json | cmp - path/to/vendored/scenarios.json
19
+ ```
20
+
21
+ ## For adhub (the source)
22
+
23
+ The source of truth is `sdk/scenarios/` in admoai/adhub. To change a scenario: edit
24
+ `scenarios.json`, bump `version` in `package.json` (minor: added or tightened; major: removed
25
+ or a key renamed), add a `CHANGELOG.md` entry, and run `sdk/scenarios/check.sh`. Merging to
26
+ `main` publishes the new version (`.github/workflows/sdk-scenarios-release.yml`); each SDK then
27
+ moves to it in its own PR. See `docs/guides/sdk-ci.md`.
package/package.json CHANGED
@@ -1,6 +1,16 @@
1
1
  {
2
2
  "name": "@admoai/sdk-scenarios",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "The cross-SDK decision & tracking scenarios every AdMoai SDK asserts against a real decision-engine. Data only.",
5
+ "license": "MIT",
6
+ "files": [
7
+ "scenarios.json",
8
+ "CHANGELOG.md",
9
+ "README.md"
10
+ ],
11
+ "keywords": [
12
+ "admoai",
13
+ "sdk",
14
+ "contract-tests"
15
+ ]
16
+ }
package/scenarios.json ADDED
@@ -0,0 +1,946 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "description": "Shared cross-SDK E2E scenario manifest. ONE definition, executed by a thin interpreter in each SDK, so the three suites cannot drift. Declarative only: given a request, expect an outcome. Sponsored Pin tracking scenarios add `pinTracking`: the point-tracking calls an app makes on the served pins, as data, and the beacons that must reach the engine. Procedural scenarios (log capture, concurrency, retry, TTL waits) stay hand-written per SDK. Mirrored byte-identically into all three repos; a checksum test fails the build if the copies diverge. Every expected value here was verified against a live local decision-engine before being written.",
4
+ "scenarios": [
5
+ {
6
+ "id": "L1",
7
+ "title": "a normal native ad serves with complete, well-formed metadata",
8
+ "request": {
9
+ "placements": [
10
+ {
11
+ "key": "home"
12
+ }
13
+ ]
14
+ },
15
+ "expect": {
16
+ "outcome": "served",
17
+ "decisions": 1,
18
+ "creative": {
19
+ "requireMetadata": true,
20
+ "journey": false,
21
+ "formatEquals": "native",
22
+ "priorityIn": [
23
+ "standard",
24
+ "sponsorship",
25
+ "house"
26
+ ]
27
+ }
28
+ }
29
+ },
30
+ {
31
+ "id": "L5",
32
+ "title": "a placement with no eligible ad returns a clean no-ad, not an error",
33
+ "request": {
34
+ "placements": [
35
+ {
36
+ "key": "postroll"
37
+ }
38
+ ]
39
+ },
40
+ "expect": {
41
+ "outcome": "noAd",
42
+ "decisions": 1
43
+ }
44
+ },
45
+ {
46
+ "id": "L6",
47
+ "title": "the advertiser block is complete (name, legalName, logoUrl)",
48
+ "request": {
49
+ "placements": [
50
+ {
51
+ "key": "home"
52
+ }
53
+ ]
54
+ },
55
+ "expect": {
56
+ "outcome": "served",
57
+ "creative": {
58
+ "requireAdvertiser": true
59
+ }
60
+ }
61
+ },
62
+ {
63
+ "id": "L7",
64
+ "title": "the template block carries a non-empty key",
65
+ "request": {
66
+ "placements": [
67
+ {
68
+ "key": "home"
69
+ }
70
+ ]
71
+ },
72
+ "expect": {
73
+ "outcome": "served",
74
+ "creative": {
75
+ "requireTemplate": true
76
+ }
77
+ }
78
+ },
79
+ {
80
+ "id": "L8",
81
+ "title": "a served native ad exposes an impression URL, a click URL and non-empty contents",
82
+ "request": {
83
+ "placements": [
84
+ {
85
+ "key": "home"
86
+ }
87
+ ]
88
+ },
89
+ "expect": {
90
+ "outcome": "served",
91
+ "creative": {
92
+ "impressions": "required",
93
+ "clicksAtLeast": 1,
94
+ "requireContents": true
95
+ }
96
+ }
97
+ },
98
+ {
99
+ "id": "M1",
100
+ "title": "count=2 returns at most two creatives, and they are distinct",
101
+ "request": {
102
+ "placements": [
103
+ {
104
+ "key": "promotions",
105
+ "count": 2
106
+ }
107
+ ]
108
+ },
109
+ "expect": {
110
+ "outcome": "served",
111
+ "decisions": 1,
112
+ "creativesAtMost": 2,
113
+ "creativesDistinct": true
114
+ }
115
+ },
116
+ {
117
+ "id": "M2",
118
+ "title": "three placements in one request each get their own correctly-keyed decision",
119
+ "request": {
120
+ "placements": [
121
+ {
122
+ "key": "home"
123
+ },
124
+ {
125
+ "key": "promotions"
126
+ },
127
+ {
128
+ "key": "search"
129
+ }
130
+ ]
131
+ },
132
+ "expect": {
133
+ "outcome": "served",
134
+ "decisions": 3,
135
+ "decisionsMatchRequest": true
136
+ }
137
+ },
138
+ {
139
+ "id": "M3",
140
+ "title": "an advertiserId filter still serves that advertiser's ad",
141
+ "request": {
142
+ "placements": [
143
+ {
144
+ "key": "promotions",
145
+ "advertiserId": "advertiser_01JEPGMJXARVYSJA0CAM9FQ79Y"
146
+ }
147
+ ]
148
+ },
149
+ "expect": {
150
+ "outcome": "served",
151
+ "creative": {
152
+ "requireAdvertiser": true
153
+ }
154
+ }
155
+ },
156
+ {
157
+ "id": "M5",
158
+ "title": "format=native on a mixed placement returns a native ad",
159
+ "request": {
160
+ "placements": [
161
+ {
162
+ "key": "promotions",
163
+ "format": "native"
164
+ }
165
+ ]
166
+ },
167
+ "expect": {
168
+ "outcome": "served",
169
+ "creative": {
170
+ "formatEquals": "native"
171
+ }
172
+ }
173
+ },
174
+ {
175
+ "id": "M6",
176
+ "title": "format=video on the same mixed placement returns a video ad instead",
177
+ "request": {
178
+ "placements": [
179
+ {
180
+ "key": "promotions",
181
+ "format": "video"
182
+ }
183
+ ]
184
+ },
185
+ "expect": {
186
+ "outcome": "served",
187
+ "creative": {
188
+ "formatEquals": "video"
189
+ }
190
+ }
191
+ },
192
+ {
193
+ "id": "M7",
194
+ "title": "omitting format still serves \u2014 the engine chooses",
195
+ "request": {
196
+ "placements": [
197
+ {
198
+ "key": "promotions"
199
+ }
200
+ ]
201
+ },
202
+ "expect": {
203
+ "outcome": "served"
204
+ }
205
+ },
206
+ {
207
+ "id": "M8",
208
+ "title": "a journey-owned placement and a normal placement resolve independently in ONE request",
209
+ "request": {
210
+ "placements": [
211
+ {
212
+ "key": "promotions"
213
+ },
214
+ {
215
+ "key": "home"
216
+ }
217
+ ],
218
+ "session": "auto",
219
+ "journeyOpt": "in"
220
+ },
221
+ "expect": {
222
+ "outcome": "served",
223
+ "decisions": 2
224
+ }
225
+ },
226
+ {
227
+ "id": "P1",
228
+ "title": "a non-skippable JSON video exposes json delivery and five video events (no skip)",
229
+ "request": {
230
+ "placements": [
231
+ {
232
+ "key": "json_none"
233
+ }
234
+ ]
235
+ },
236
+ "expect": {
237
+ "outcome": "served",
238
+ "creative": {
239
+ "deliveryEquals": "json",
240
+ "formatEquals": "video",
241
+ "videoEventCount": 5,
242
+ "videoEventKeys": [
243
+ "start",
244
+ "first_quartile",
245
+ "midpoint",
246
+ "third_quartile",
247
+ "complete"
248
+ ]
249
+ }
250
+ }
251
+ },
252
+ {
253
+ "id": "P2",
254
+ "title": "a vast_tag ad exposes its tag URL and surfaces NO engine-side beacons (no double count)",
255
+ "request": {
256
+ "placements": [
257
+ {
258
+ "key": "vasttag_none"
259
+ }
260
+ ]
261
+ },
262
+ "expect": {
263
+ "outcome": "served",
264
+ "creative": {
265
+ "deliveryEquals": "vast_tag",
266
+ "requireVastTagUrl": true,
267
+ "videoEventCount": 0,
268
+ "impressions": "forbidden"
269
+ }
270
+ }
271
+ },
272
+ {
273
+ "id": "P3",
274
+ "title": "a vast_xml ad exposes base64 that decodes to a VAST document, and no beacons",
275
+ "request": {
276
+ "placements": [
277
+ {
278
+ "key": "vastxml_none"
279
+ }
280
+ ]
281
+ },
282
+ "expect": {
283
+ "outcome": "served",
284
+ "creative": {
285
+ "deliveryEquals": "vast_xml",
286
+ "requireVastXmlDecodes": true,
287
+ "videoEventCount": 0,
288
+ "impressions": "forbidden"
289
+ }
290
+ }
291
+ },
292
+ {
293
+ "id": "P6",
294
+ "title": "a skippable video resolves skippability from ENGINE METADATA, plus a sixth skip event",
295
+ "request": {
296
+ "placements": [
297
+ {
298
+ "key": "json_none_skippable"
299
+ }
300
+ ]
301
+ },
302
+ "expect": {
303
+ "outcome": "served",
304
+ "creative": {
305
+ "metadataIsSkippable": true,
306
+ "skipOffsetSecondsEquals": 5,
307
+ "helperIsSkippable": true,
308
+ "helperSkipOffsetEquals": "5",
309
+ "videoEventCount": 6
310
+ }
311
+ }
312
+ },
313
+ {
314
+ "id": "P8",
315
+ "title": "an end-card video reports its configured endCardMode",
316
+ "request": {
317
+ "placements": [
318
+ {
319
+ "key": "json_native_endcard"
320
+ }
321
+ ]
322
+ },
323
+ "expect": {
324
+ "outcome": "served",
325
+ "creative": {
326
+ "endCardModeEquals": "native_endcard"
327
+ }
328
+ }
329
+ },
330
+ {
331
+ "id": "V1",
332
+ "title": "an unknown placement key raises a typed validation error, not a silent no-fill",
333
+ "request": {
334
+ "placements": [
335
+ {
336
+ "key": "definitely_not_a_real_placement"
337
+ }
338
+ ]
339
+ },
340
+ "expect": {
341
+ "outcome": "error",
342
+ "error": {
343
+ "kind": "validation",
344
+ "status": 422,
345
+ "code": 10017
346
+ }
347
+ }
348
+ },
349
+ {
350
+ "id": "V3",
351
+ "title": "an unknown custom-targeting key is rejected by the engine",
352
+ "request": {
353
+ "placements": [
354
+ {
355
+ "key": "home"
356
+ }
357
+ ],
358
+ "custom": [
359
+ {
360
+ "key": "not_a_configured_preset",
361
+ "value": "x"
362
+ }
363
+ ]
364
+ },
365
+ "expect": {
366
+ "outcome": "error",
367
+ "error": {
368
+ "kind": "validation",
369
+ "status": 422,
370
+ "code": 10028
371
+ }
372
+ }
373
+ },
374
+ {
375
+ "id": "V4",
376
+ "title": "the SDK deduplicates a repeated custom key, so the engine never sees the duplicate that would fail the whole request",
377
+ "comment": "Sent raw over HTTP this same payload returns 422 code 10031, and the engine aborts validation for the ENTIRE request \u2014 every placement, not just the offending target. All three SDKs dedupe by key (last write wins), so a publisher cannot reach that state through the public API. This scenario is the live proof of that guard: it must SERVE, and both placements must come back.",
378
+ "request": {
379
+ "placements": [
380
+ {
381
+ "key": "home"
382
+ },
383
+ {
384
+ "key": "promotions"
385
+ }
386
+ ],
387
+ "custom": [
388
+ {
389
+ "key": "category",
390
+ "value": "sports"
391
+ },
392
+ {
393
+ "key": "category",
394
+ "value": "news"
395
+ }
396
+ ]
397
+ },
398
+ "expect": {
399
+ "outcome": "served",
400
+ "decisions": 2,
401
+ "decisionsMatchRequest": true
402
+ }
403
+ },
404
+ {
405
+ "id": "V5",
406
+ "title": "a single valid custom target does not break serving",
407
+ "request": {
408
+ "placements": [
409
+ {
410
+ "key": "home"
411
+ }
412
+ ],
413
+ "custom": [
414
+ {
415
+ "key": "category",
416
+ "value": "sports"
417
+ }
418
+ ]
419
+ },
420
+ "expect": {
421
+ "outcome": "served"
422
+ }
423
+ },
424
+ {
425
+ "id": "V6",
426
+ "title": "a count above the documented maximum is rejected",
427
+ "request": {
428
+ "placements": [
429
+ {
430
+ "key": "home",
431
+ "count": 99
432
+ }
433
+ ]
434
+ },
435
+ "expect": {
436
+ "outcome": "error",
437
+ "error": {
438
+ "kind": "validation",
439
+ "status": 422,
440
+ "code": 10016
441
+ }
442
+ }
443
+ },
444
+ {
445
+ "id": "V9",
446
+ "title": "a request with no placements fails locally, before any network call",
447
+ "request": {
448
+ "placements": []
449
+ },
450
+ "expect": {
451
+ "outcome": "error",
452
+ "error": {
453
+ "kind": "local"
454
+ }
455
+ }
456
+ },
457
+ {
458
+ "id": "Z1",
459
+ "title": "API version 2025-01-01 still serves a native ad and never a journey block",
460
+ "request": {
461
+ "placements": [
462
+ {
463
+ "key": "home"
464
+ }
465
+ ],
466
+ "apiVersion": "2025-01-01"
467
+ },
468
+ "expect": {
469
+ "outcome": "served",
470
+ "creative": {
471
+ "journey": false,
472
+ "requireMetadata": true
473
+ }
474
+ }
475
+ },
476
+ {
477
+ "id": "Z2",
478
+ "title": "omitting the version header still serves (engine falls back to the oldest active version)",
479
+ "request": {
480
+ "placements": [
481
+ {
482
+ "key": "home"
483
+ }
484
+ ],
485
+ "apiVersion": "none"
486
+ },
487
+ "expect": {
488
+ "outcome": "served",
489
+ "creative": {
490
+ "journey": false
491
+ }
492
+ }
493
+ },
494
+ {
495
+ "id": "SP1",
496
+ "title": "a radius search returns the campaign's own shops, nearest first",
497
+ "request": {
498
+ "apiVersion": "2025-11-01",
499
+ "placements": [
500
+ {
501
+ "key": "poi"
502
+ }
503
+ ],
504
+ "distance": {
505
+ "latitude": -33.4175,
506
+ "longitude": -70.6065,
507
+ "radius": 20000
508
+ }
509
+ },
510
+ "expect": {
511
+ "outcome": "served",
512
+ "creative": {
513
+ "matchedPointsAtLeast": 2,
514
+ "matchedPointsNearestFirst": true,
515
+ "matchedPointsWellFormed": true
516
+ }
517
+ }
518
+ },
519
+ {
520
+ "id": "SP2",
521
+ "title": "a bounds search returns them too \u2014 the other shape a map can ask with",
522
+ "request": {
523
+ "apiVersion": "2025-11-01",
524
+ "placements": [
525
+ {
526
+ "key": "poi"
527
+ }
528
+ ],
529
+ "distance": {
530
+ "latitude": -33.4175,
531
+ "longitude": -70.6065,
532
+ "bounds": {
533
+ "north": -33.3,
534
+ "south": -33.55,
535
+ "east": -70.45,
536
+ "west": -70.65
537
+ }
538
+ }
539
+ },
540
+ "expect": {
541
+ "outcome": "served",
542
+ "creative": {
543
+ "matchedPointsAtLeast": 2,
544
+ "matchedPointsWellFormed": true
545
+ }
546
+ }
547
+ },
548
+ {
549
+ "id": "SP3",
550
+ "title": "every returned shop carries its own view, tap and click beacons",
551
+ "request": {
552
+ "apiVersion": "2025-11-01",
553
+ "placements": [
554
+ {
555
+ "key": "poi"
556
+ }
557
+ ],
558
+ "distance": {
559
+ "latitude": -33.4175,
560
+ "longitude": -70.6065,
561
+ "radius": 20000
562
+ }
563
+ },
564
+ "expect": {
565
+ "outcome": "served",
566
+ "creative": {
567
+ "matchedPointsHaveBeacons": true
568
+ }
569
+ }
570
+ },
571
+ {
572
+ "id": "SP4",
573
+ "title": "every returned shop carries a resolved destination, its own or the creative's",
574
+ "request": {
575
+ "apiVersion": "2025-11-01",
576
+ "placements": [
577
+ {
578
+ "key": "poi"
579
+ }
580
+ ],
581
+ "distance": {
582
+ "latitude": -33.4175,
583
+ "longitude": -70.6065,
584
+ "radius": 20000
585
+ }
586
+ },
587
+ "expect": {
588
+ "outcome": "served",
589
+ "creative": {
590
+ "matchedPointsHaveClickUrl": true
591
+ }
592
+ }
593
+ },
594
+ {
595
+ "id": "SP5",
596
+ "title": "the request's limit narrows how many shops come back",
597
+ "request": {
598
+ "apiVersion": "2025-11-01",
599
+ "placements": [
600
+ {
601
+ "key": "poi"
602
+ }
603
+ ],
604
+ "distance": {
605
+ "latitude": -33.4175,
606
+ "longitude": -70.6065,
607
+ "radius": 20000,
608
+ "limit": 2
609
+ }
610
+ },
611
+ "expect": {
612
+ "outcome": "served",
613
+ "creative": {
614
+ "matchedPointsEquals": 2
615
+ }
616
+ }
617
+ },
618
+ {
619
+ "id": "SP6",
620
+ "title": "a tight radius excludes the shops outside it",
621
+ "request": {
622
+ "apiVersion": "2025-11-01",
623
+ "placements": [
624
+ {
625
+ "key": "poi"
626
+ }
627
+ ],
628
+ "distance": {
629
+ "latitude": -33.4175,
630
+ "longitude": -70.6065,
631
+ "radius": 1500
632
+ }
633
+ },
634
+ "expect": {
635
+ "outcome": "served",
636
+ "creative": {
637
+ "matchedPointsEquals": 1
638
+ }
639
+ }
640
+ },
641
+ {
642
+ "id": "SP7",
643
+ "title": "without a distance search a Sponsored Pin campaign does not serve at all",
644
+ "request": {
645
+ "apiVersion": "2025-11-01",
646
+ "placements": [
647
+ {
648
+ "key": "poi"
649
+ }
650
+ ]
651
+ },
652
+ "expect": {
653
+ "outcome": "noAd"
654
+ }
655
+ },
656
+ {
657
+ "id": "SP8",
658
+ "title": "on the older API version the ad still serves, silently carrying no pins \u2014 the failure an integrator must recognise",
659
+ "request": {
660
+ "apiVersion": "none",
661
+ "placements": [
662
+ {
663
+ "key": "poi"
664
+ }
665
+ ],
666
+ "distance": {
667
+ "latitude": -33.4175,
668
+ "longitude": -70.6065,
669
+ "radius": 20000
670
+ }
671
+ },
672
+ "expect": {
673
+ "outcome": "served",
674
+ "creative": {
675
+ "matchedPointsNone": true
676
+ }
677
+ }
678
+ },
679
+ {
680
+ "id": "SP9",
681
+ "title": "a map's first render sends every shop's view beacon exactly once",
682
+ "request": {
683
+ "apiVersion": "2025-11-01",
684
+ "placements": [
685
+ {
686
+ "key": "poi"
687
+ }
688
+ ],
689
+ "distance": {
690
+ "latitude": -33.4175,
691
+ "longitude": -70.6065,
692
+ "radius": 20000
693
+ }
694
+ },
695
+ "expect": {
696
+ "outcome": "served",
697
+ "creative": {
698
+ "matchedPointsAtLeast": 1,
699
+ "matchedPointsHaveBeacons": true
700
+ }
701
+ },
702
+ "pinTracking": {
703
+ "steps": [
704
+ {
705
+ "views": "all"
706
+ }
707
+ ],
708
+ "expect": {
709
+ "viewsOncePerResponse": true,
710
+ "engineAcceptsEveryBeacon": true
711
+ }
712
+ }
713
+ },
714
+ {
715
+ "id": "SP10",
716
+ "title": "re-rendering the same response sends no view beacon again",
717
+ "request": {
718
+ "apiVersion": "2025-11-01",
719
+ "placements": [
720
+ {
721
+ "key": "poi"
722
+ }
723
+ ],
724
+ "distance": {
725
+ "latitude": -33.4175,
726
+ "longitude": -70.6065,
727
+ "radius": 20000
728
+ }
729
+ },
730
+ "expect": {
731
+ "outcome": "served",
732
+ "creative": {
733
+ "matchedPointsAtLeast": 1,
734
+ "matchedPointsHaveBeacons": true
735
+ }
736
+ },
737
+ "pinTracking": {
738
+ "steps": [
739
+ {
740
+ "views": "all"
741
+ },
742
+ {
743
+ "views": "all"
744
+ },
745
+ {
746
+ "views": "all"
747
+ }
748
+ ],
749
+ "expect": {
750
+ "viewsOncePerResponse": true,
751
+ "engineAcceptsEveryBeacon": true
752
+ }
753
+ }
754
+ },
755
+ {
756
+ "id": "SP11",
757
+ "title": "a render that brings new shops into view sends views only for the new ones",
758
+ "request": {
759
+ "apiVersion": "2025-11-01",
760
+ "placements": [
761
+ {
762
+ "key": "poi"
763
+ }
764
+ ],
765
+ "distance": {
766
+ "latitude": -33.4175,
767
+ "longitude": -70.6065,
768
+ "radius": 20000
769
+ }
770
+ },
771
+ "expect": {
772
+ "outcome": "served",
773
+ "creative": {
774
+ "matchedPointsAtLeast": 4,
775
+ "matchedPointsHaveBeacons": true
776
+ }
777
+ },
778
+ "pinTracking": {
779
+ "steps": [
780
+ {
781
+ "views": {
782
+ "from": 0,
783
+ "to": 2
784
+ }
785
+ },
786
+ {
787
+ "views": {
788
+ "from": 1,
789
+ "to": 4
790
+ }
791
+ },
792
+ {
793
+ "views": {
794
+ "from": 0,
795
+ "to": 4
796
+ }
797
+ }
798
+ ],
799
+ "expect": {
800
+ "viewsOncePerResponse": true,
801
+ "engineAcceptsEveryBeacon": true
802
+ }
803
+ }
804
+ },
805
+ {
806
+ "id": "SP12",
807
+ "title": "a new decision response reports the same shops again, once",
808
+ "request": {
809
+ "apiVersion": "2025-11-01",
810
+ "placements": [
811
+ {
812
+ "key": "poi"
813
+ }
814
+ ],
815
+ "distance": {
816
+ "latitude": -33.4175,
817
+ "longitude": -70.6065,
818
+ "radius": 20000
819
+ }
820
+ },
821
+ "expect": {
822
+ "outcome": "served",
823
+ "creative": {
824
+ "matchedPointsAtLeast": 1,
825
+ "matchedPointsHaveBeacons": true
826
+ }
827
+ },
828
+ "pinTracking": {
829
+ "steps": [
830
+ {
831
+ "views": "all"
832
+ },
833
+ {
834
+ "newResponse": true
835
+ },
836
+ {
837
+ "views": "all"
838
+ },
839
+ {
840
+ "views": "all"
841
+ }
842
+ ],
843
+ "expect": {
844
+ "viewsOncePerResponse": true,
845
+ "newResponseHasNewViewUrls": true,
846
+ "engineAcceptsEveryBeacon": true
847
+ }
848
+ }
849
+ },
850
+ {
851
+ "id": "SP13",
852
+ "title": "single-pin views follow the same once-per-response rule as the bulk call",
853
+ "request": {
854
+ "apiVersion": "2025-11-01",
855
+ "placements": [
856
+ {
857
+ "key": "poi"
858
+ }
859
+ ],
860
+ "distance": {
861
+ "latitude": -33.4175,
862
+ "longitude": -70.6065,
863
+ "radius": 20000
864
+ }
865
+ },
866
+ "expect": {
867
+ "outcome": "served",
868
+ "creative": {
869
+ "matchedPointsAtLeast": 1,
870
+ "matchedPointsHaveBeacons": true
871
+ }
872
+ },
873
+ "pinTracking": {
874
+ "steps": [
875
+ {
876
+ "view": 0
877
+ },
878
+ {
879
+ "view": 0
880
+ },
881
+ {
882
+ "views": "all"
883
+ },
884
+ {
885
+ "view": 0
886
+ }
887
+ ],
888
+ "expect": {
889
+ "viewsOncePerResponse": true,
890
+ "engineAcceptsEveryBeacon": true
891
+ }
892
+ }
893
+ },
894
+ {
895
+ "id": "SP14",
896
+ "title": "taps and clicks are sent on every call, never de-duplicated",
897
+ "request": {
898
+ "apiVersion": "2025-11-01",
899
+ "placements": [
900
+ {
901
+ "key": "poi"
902
+ }
903
+ ],
904
+ "distance": {
905
+ "latitude": -33.4175,
906
+ "longitude": -70.6065,
907
+ "radius": 20000
908
+ }
909
+ },
910
+ "expect": {
911
+ "outcome": "served",
912
+ "creative": {
913
+ "matchedPointsAtLeast": 1,
914
+ "matchedPointsHaveBeacons": true
915
+ }
916
+ },
917
+ "pinTracking": {
918
+ "steps": [
919
+ {
920
+ "views": "all"
921
+ },
922
+ {
923
+ "tap": 0
924
+ },
925
+ {
926
+ "tap": 0
927
+ },
928
+ {
929
+ "click": 0
930
+ },
931
+ {
932
+ "click": 0
933
+ },
934
+ {
935
+ "views": "all"
936
+ }
937
+ ],
938
+ "expect": {
939
+ "viewsOncePerResponse": true,
940
+ "interactionsEveryCall": true,
941
+ "engineAcceptsEveryBeacon": true
942
+ }
943
+ }
944
+ }
945
+ ]
946
+ }