@mohasinac/appkit 4.42.1 → 4.42.2

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 (181) hide show
  1. package/dist/_internal/server/features/offers/adapters.d.ts +29 -7
  2. package/dist/_internal/server/features/offers/adapters.js +14 -3
  3. package/dist/_internal/server/features/orders/adapters.js +54 -1
  4. package/dist/client.d.ts +1 -0
  5. package/dist/client.js +1 -0
  6. package/dist/features/account/components/UserOrdersView.js +30 -2
  7. package/dist/features/account/hooks/useAddresses.d.ts +21 -6
  8. package/dist/features/account/hooks/useAddresses.js +1 -1
  9. package/dist/features/addresses/hooks/usePostalLookup.js +84 -12
  10. package/dist/features/admin/components/AdminCategoriesView.js +37 -2
  11. package/dist/features/admin/components/AdminOffersView.js +2 -2
  12. package/dist/features/admin/constants/filter-tabs.d.ts +25 -0
  13. package/dist/features/admin/constants/filter-tabs.js +30 -0
  14. package/dist/features/admin/hooks/useAdminListingData.d.ts +35 -0
  15. package/dist/features/admin/hooks/useAdminListingData.js +45 -9
  16. package/dist/features/cart/schemas/index.d.ts +15 -0
  17. package/dist/features/cart/schemas/index.js +2 -0
  18. package/dist/features/cart/types/index.d.ts +22 -0
  19. package/dist/features/categories/components/CategoriesIndexListing.js +19 -1
  20. package/dist/features/categories/components/CategoriesIndexPageView.js +14 -1
  21. package/dist/features/classified/components/ClassifiedDetailPageView.d.ts +8 -0
  22. package/dist/features/classified/components/ClassifiedDetailPageView.js +1 -0
  23. package/dist/features/events/actions/event-actions.d.ts +13 -0
  24. package/dist/features/events/actions/event-actions.js +75 -1
  25. package/dist/features/events/repository/event-entry.repository.d.ts +17 -0
  26. package/dist/features/events/repository/event-entry.repository.js +36 -0
  27. package/dist/features/homepage/components/BrandsSection.js +1 -1
  28. package/dist/features/homepage/components/CustomerReviewsSection.js +1 -1
  29. package/dist/features/homepage/components/FeaturedBundlesSection.js +1 -1
  30. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  31. package/dist/features/media/upload/MediaUploadField.js +21 -3
  32. package/dist/features/orders/components/OrdersList.js +2 -1
  33. package/dist/features/orders/index.d.ts +1 -0
  34. package/dist/features/orders/index.js +1 -0
  35. package/dist/features/orders/types/index.d.ts +2 -0
  36. package/dist/features/orders/utils/order-ref.d.ts +45 -0
  37. package/dist/features/orders/utils/order-ref.js +58 -0
  38. package/dist/features/products/components/MakeOfferButton.d.ts +18 -1
  39. package/dist/features/products/components/MakeOfferButton.js +23 -1
  40. package/dist/features/products/components/ProductDetailPageView.d.ts +5 -0
  41. package/dist/features/products/components/ProductDetailPageView.js +3 -2
  42. package/dist/features/products/repository/products.repository.js +29 -2
  43. package/dist/features/seller/actions/offer-actions.js +32 -0
  44. package/dist/features/seller/components/QuickProductForm.js +15 -1
  45. package/dist/features/seller/components/SellerBidsView.js +3 -1
  46. package/dist/features/seller/components/SellerOffersView.js +2 -2
  47. package/dist/features/seller/components/SellerOrdersView.js +49 -6
  48. package/dist/features/seller/components/SellerProductsCards.js +23 -1
  49. package/dist/features/seller/components/SellerProductsView.js +27 -2
  50. package/dist/features/seller/hooks/useSellerListingData.d.ts +0 -1
  51. package/dist/features/seller/hooks/useSellerListingData.js +17 -19
  52. package/dist/features/shell/SectionForm.js +33 -2
  53. package/dist/features/tester/seed-data/_happy-path.d.ts +23 -0
  54. package/dist/features/tester/seed-data/_happy-path.js +710 -0
  55. package/dist/features/tester/seed-data/_money-flows.js +34 -23
  56. package/dist/features/tester/seed-data/authored/account-auth__profile-settings.js +22 -3
  57. package/dist/features/tester/seed-data/authored/addresses__address-filters.js +17 -12
  58. package/dist/features/tester/seed-data/authored/addresses__postal-lookup.js +7 -6
  59. package/dist/features/tester/seed-data/authored/addresses__postal-validation.js +10 -4
  60. package/dist/features/tester/seed-data/authored/addresses__state-picker.js +13 -8
  61. package/dist/features/tester/seed-data/authored/addresses__unban-request.js +49 -11
  62. package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +10 -6
  63. package/dist/features/tester/seed-data/authored/admin__bans-and-trust.js +71 -32
  64. package/dist/features/tester/seed-data/authored/admin__blog-faqs.js +80 -31
  65. package/dist/features/tester/seed-data/authored/admin__bug-hunter-rewards.js +62 -25
  66. package/dist/features/tester/seed-data/authored/admin__bulk-actions.js +61 -31
  67. package/dist/features/tester/seed-data/authored/admin__bundles.js +84 -37
  68. package/dist/features/tester/seed-data/authored/admin__buyer-data-admin.js +90 -57
  69. package/dist/features/tester/seed-data/authored/admin__catalog-listings.js +38 -26
  70. package/dist/features/tester/seed-data/authored/admin__category-brand-authoring.js +47 -28
  71. package/dist/features/tester/seed-data/authored/admin__classifieds-digitalcodes-live.js +118 -31
  72. package/dist/features/tester/seed-data/authored/admin__content-deletes.js +30 -24
  73. package/dist/features/tester/seed-data/authored/admin__content-marketing.js +83 -46
  74. package/dist/features/tester/seed-data/authored/admin__coupons.js +8 -8
  75. package/dist/features/tester/seed-data/authored/admin__events-raffles-spin.js +9 -9
  76. package/dist/features/tester/seed-data/authored/admin__firebase-function-effects.js +19 -4
  77. package/dist/features/tester/seed-data/authored/admin__media-watermark.js +77 -33
  78. package/dist/features/tester/seed-data/authored/admin__orders-fulfillment.js +5 -5
  79. package/dist/features/tester/seed-data/authored/admin__prize-draws-lotteries.js +29 -5
  80. package/dist/features/tester/seed-data/authored/admin__site-system.js +45 -23
  81. package/dist/features/tester/seed-data/authored/admin__uncovered-admin-pages.js +53 -28
  82. package/dist/features/tester/seed-data/authored/admin__users-trust.js +20 -20
  83. package/dist/features/tester/seed-data/authored/buying__bidding.js +19 -2
  84. package/dist/features/tester/seed-data/authored/buying__browsing-search.js +20 -5
  85. package/dist/features/tester/seed-data/authored/buying__buying-checkout.js +28 -10
  86. package/dist/features/tester/seed-data/authored/buying__cart.js +10 -3
  87. package/dist/features/tester/seed-data/authored/buying__image-tile-layout.js +0 -16
  88. package/dist/features/tester/seed-data/authored/buying__my-orders.js +27 -5
  89. package/dist/features/tester/seed-data/authored/buying__offers.js +22 -7
  90. package/dist/features/tester/seed-data/authored/buying__order-detail-actions.js +3 -3
  91. package/dist/features/tester/seed-data/authored/buying__product-detail.js +35 -4
  92. package/dist/features/tester/seed-data/authored/buying__return-request.js +19 -6
  93. package/dist/features/tester/seed-data/authored/buying__reviews-pagination.js +36 -18
  94. package/dist/features/tester/seed-data/authored/buying__reviews.js +16 -1
  95. package/dist/features/tester/seed-data/authored/buying__user-dashboard-navigation.js +2 -1
  96. package/dist/features/tester/seed-data/authored/buying__user-uncovered-pages.js +16 -1
  97. package/dist/features/tester/seed-data/authored/community-support__public-profile.js +3 -2
  98. package/dist/features/tester/seed-data/authored/community-support__support-tickets.js +4 -4
  99. package/dist/features/tester/seed-data/authored/content-discovery__blog.js +19 -2
  100. package/dist/features/tester/seed-data/authored/content-discovery__category-brand-relations.js +4 -4
  101. package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +5 -5
  102. package/dist/features/tester/seed-data/authored/content-discovery__coupons.js +41 -6
  103. package/dist/features/tester/seed-data/authored/content-discovery__event-detail-subroutes.js +63 -8
  104. package/dist/features/tester/seed-data/authored/content-discovery__event-participation.js +29 -29
  105. package/dist/features/tester/seed-data/authored/content-discovery__events.js +2 -2
  106. package/dist/features/tester/seed-data/authored/content-discovery__faq-help.js +23 -1
  107. package/dist/features/tester/seed-data/authored/content-discovery__notifications.js +6 -6
  108. package/dist/features/tester/seed-data/authored/content-discovery__search.js +1 -1
  109. package/dist/features/tester/seed-data/authored/content-discovery__store-reviews-aggregate.js +1 -1
  110. package/dist/features/tester/seed-data/authored/cta-layout__checkout-bottom-bar.js +31 -6
  111. package/dist/features/tester/seed-data/authored/cta-layout__dialog-footers.js +34 -10
  112. package/dist/features/tester/seed-data/authored/cta-layout__editor-action-bar.js +35 -8
  113. package/dist/features/tester/seed-data/authored/cta-layout__navbar-ctas.js +31 -9
  114. package/dist/features/tester/seed-data/authored/cta-layout__product-bottom-bar.js +29 -3
  115. package/dist/features/tester/seed-data/authored/design-ux__back-to-top-button.js +1 -1
  116. package/dist/features/tester/seed-data/authored/design-ux__carousel-arrow-bounds.js +10 -10
  117. package/dist/features/tester/seed-data/authored/design-ux__dashboard-layout.js +3 -3
  118. package/dist/features/tester/seed-data/authored/design-ux__footer-theme.js +2 -2
  119. package/dist/features/tester/seed-data/authored/design-ux__form-validation-errors.js +2 -2
  120. package/dist/features/tester/seed-data/authored/design-ux__general-design.js +4 -4
  121. package/dist/features/tester/seed-data/authored/design-ux__hand-mode-layout.js +1 -1
  122. package/dist/features/tester/seed-data/authored/design-ux__homepage-carousels.js +8 -8
  123. package/dist/features/tester/seed-data/authored/design-ux__status-badge-legibility.js +2 -2
  124. package/dist/features/tester/seed-data/authored/design-ux__sticky-cta-bar.js +2 -2
  125. package/dist/features/tester/seed-data/authored/page-wiring__data-loss.js +39 -16
  126. package/dist/features/tester/seed-data/authored/page-wiring__detail-pages.js +43 -8
  127. package/dist/features/tester/seed-data/authored/page-wiring__drawer-pages.js +40 -14
  128. package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +31 -14
  129. package/dist/features/tester/seed-data/authored/public-pages__auth-error-pages.js +7 -6
  130. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +14 -5
  131. package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.js +3 -3
  132. package/dist/features/tester/seed-data/authored/public-pages__help-how-it-works.js +1 -1
  133. package/dist/features/tester/seed-data/authored/public-pages__help-scams-guides-subpages.js +4 -2
  134. package/dist/features/tester/seed-data/authored/public-pages__legal-policy-pages.js +3 -3
  135. package/dist/features/tester/seed-data/authored/public-pages__newly-wired-browse-indexes.js +5 -5
  136. package/dist/features/tester/seed-data/authored/public-pages__store-tabs.js +5 -5
  137. package/dist/features/tester/seed-data/authored/public-pages__stores-sellers-directories.js +3 -3
  138. package/dist/features/tester/seed-data/authored/search-and-nav__employee-permissions.js +45 -11
  139. package/dist/features/tester/seed-data/authored/search-and-nav__header-search.js +57 -17
  140. package/dist/features/tester/seed-data/authored/search-and-nav__settings-deep-links.js +36 -5
  141. package/dist/features/tester/seed-data/authored/search-and-nav__sidebar-search.js +29 -4
  142. package/dist/features/tester/seed-data/authored/selling__become-seller.js +23 -0
  143. package/dist/features/tester/seed-data/authored/selling__final-sale-authoring.js +14 -13
  144. package/dist/features/tester/seed-data/authored/selling__listing-a-product.js +14 -9
  145. package/dist/features/tester/seed-data/authored/selling__listing-edit-roundtrip.js +34 -7
  146. package/dist/features/tester/seed-data/authored/selling__listing-lifecycle.js +17 -7
  147. package/dist/features/tester/seed-data/authored/selling__listing-type-fields-roundtrip.js +9 -9
  148. package/dist/features/tester/seed-data/authored/selling__media-limits.js +9 -4
  149. package/dist/features/tester/seed-data/authored/selling__product-upload-details.js +3 -3
  150. package/dist/features/tester/seed-data/authored/selling__quick-add-minimum-details.js +3 -3
  151. package/dist/features/tester/seed-data/authored/selling__sectionised-forms.js +25 -1
  152. package/dist/features/tester/seed-data/authored/selling__seller-analytics-payouts.js +27 -6
  153. package/dist/features/tester/seed-data/authored/selling__seller-bids-bundles-filters.js +19 -1
  154. package/dist/features/tester/seed-data/authored/selling__seller-catalog-org.js +63 -22
  155. package/dist/features/tester/seed-data/authored/selling__seller-custom-brands.js +9 -5
  156. package/dist/features/tester/seed-data/authored/selling__seller-guide.js +7 -6
  157. package/dist/features/tester/seed-data/authored/selling__seller-listing-types.js +3 -2
  158. package/dist/features/tester/seed-data/authored/selling__seller-marketing-extras.js +18 -18
  159. package/dist/features/tester/seed-data/authored/selling__seller-ops-comms.js +13 -12
  160. package/dist/features/tester/seed-data/authored/selling__seller-orders-at-scale.js +19 -3
  161. package/dist/features/tester/seed-data/authored/selling__seller-orders.js +22 -4
  162. package/dist/features/tester/seed-data/authored/selling__seller-shipping-payouts-setup.js +20 -8
  163. package/dist/features/tester/seed-data/authored/selling__store-dashboard-navigation.js +4 -4
  164. package/dist/features/tester/seed-data/authored/selling__store-uncovered-pages.js +2 -2
  165. package/dist/features/tester/seed-data/authored/seo__canonical-and-host.js +5 -5
  166. package/dist/features/tester/seed-data/authored/seo__og-images.js +1 -1
  167. package/dist/features/tester/seed-data/authored/seo__page-metadata.js +7 -7
  168. package/dist/features/tester/seed-data/authored/seo__sitemap-and-robots.js +3 -3
  169. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +123 -21
  170. package/dist/features/tester/utils/phases.d.ts +17 -0
  171. package/dist/features/tester/utils/phases.js +29 -2
  172. package/dist/index.d.ts +1 -0
  173. package/dist/index.js +2 -0
  174. package/dist/seo/json-ld.d.ts +23 -0
  175. package/dist/seo/json-ld.js +3 -2
  176. package/dist/server.d.ts +1 -0
  177. package/dist/server.js +2 -0
  178. package/dist/styles.css +1 -1
  179. package/dist/tailwind-utilities.css +1 -1
  180. package/dist/ui/components/PaginatedSelect.js +0 -0
  181. package/package.json +1 -1
@@ -32,7 +32,7 @@ export const authored = {
32
32
  "Open /admin/sections and open a section for edit.",
33
33
  "Change its heading to 'QA Section Heading Probe' and save.",
34
34
  "Reload the admin page and read the heading.",
35
- "Open the public homepage and find that section.",
35
+ "Sign out, open /, and find that section on the homepage.",
36
36
  "Read its heading there.",
37
37
  "Restore the original heading.",
38
38
  ],
@@ -79,7 +79,7 @@ export const authored = {
79
79
  steps: [
80
80
  SIGN_IN_ADMIN,
81
81
  "Open /admin/sections and disable a section rather than deleting it.",
82
- "Open the public homepage and check the section is gone from it.",
82
+ "Sign out, open /, and check the section is gone from the homepage.",
83
83
  "Return to the admin page and check the section is still listed, marked as disabled.",
84
84
  "Re-enable it and check it returns publicly.",
85
85
  "Read the two controls side by side and check which is which is obvious without trying them.",
@@ -128,27 +128,30 @@ export const authored = {
128
128
  startPage: "/admin/carousel",
129
129
  steps: [
130
130
  SIGN_IN_ADMIN,
131
- "Open /admin/carousel and count how many slides are currently active.",
132
- "Read whether the page states a maximum.",
133
- "Activate slides until you exceed that maximum.",
134
- "Read what happens on the attempt that would exceed it.",
135
- "Open the homepage and count the slides in the rotation.",
136
- "Restore the original active set.",
131
+ "Open /admin/carousel and write down which slides are active — five of the six seeded slides are, and one is inactive.",
132
+ "Read whether the page states a maximum anywhere.",
133
+ "Activate the sixth slide and save.",
134
+ "Read what happens: a refusal naming the maximum, or a successful save.",
135
+ "Reload /admin/carousel and count the active slides again.",
136
+ "Sign out, open /, and count the slides actually in the homepage rotation.",
137
+ "Sign back in and restore the original active set from step 2.",
137
138
  ],
138
- expectedBehaviour: "Exceeding the active-slide maximum is refused with a reason. Silently accepting the extra slide and then rendering only the first few is worse than refusing — the admin sees their slide saved and never learns it is not being shown.",
139
- expectedUiState: "The attempt is refused with a message naming the maximum, and the homepage's rotation count matches the active count. A saved-but-not-rendered slide is the finding.",
140
- endResult: "The original active set is restored.",
139
+ inputs: { maxActiveSlides: 5, seededSlides: 6 },
140
+ expectedBehaviour: "Exceeding the active-slide maximum should be refused with a reason. Silently accepting the extra slide and then rendering only the first few is worse than refusing — the admin sees their slide saved and never learns it is not being shown.",
141
+ expectedUiState: "🛑 EXPECT THE SAVE TO SUCCEED, AND RECORD IT AS A FAILURE. The limit is enforced in the appkit handlers mounted at /api/carousel, and the admin editor posts to /api/admin/carousel, whose `active` field is a bare optional boolean with no count check. So expect six active slides in the admin list and FIVE in the homepage rotation — the public read slices to five. Report both numbers; the gap between them is the finding, and it is exactly the saved-but-not-rendered slide this case names.",
142
+ expectedData: { activeSlidesInAdmin: 6, slidesOnHomepage: 5 },
143
+ endResult: "The original active set is restored, so the homepage rotates the same five slides as before.",
141
144
  },
142
145
  "checklist-admin-content-deletes-blog-edit-persists": {
143
146
  roles: ["admin"],
144
147
  startPage: "/admin/blog",
145
148
  steps: [
146
149
  SIGN_IN_ADMIN,
147
- "Open /admin/blog and open a published post for edit.",
150
+ "Open /admin/blog and open blog-spot-genuine-takara-tomy-beyblade for edit, writing down its Slug.",
148
151
  "Change its title to 'QA Blog Title Probe', edit a sentence of its body, and replace its cover image with /test-media/sample-image.png.",
149
152
  "Save.",
150
153
  "Reload the admin editor and check all three changes are there.",
151
- "Open the post's public page and check all three again.",
154
+ "Sign out, open /blog/blog-spot-genuine-takara-tomy-beyblade, and check all three again.",
152
155
  "Restore the original title, sentence and cover image.",
153
156
  ],
154
157
  inputs: { title: "QA Blog Title Probe", coverImage: "/test-media/sample-image.png" },
@@ -162,7 +165,7 @@ export const authored = {
162
165
  steps: [
163
166
  SIGN_IN_ADMIN,
164
167
  "Open /admin/blog and create a post titled 'QA Disposable Post', publish it.",
165
- "Open its public page and note the URL.",
168
+ "Open /blog/qa-disposable-post — the slug is the title slugified — and note that it renders.",
166
169
  "Return to /admin/blog and delete it, confirming.",
167
170
  "Reload the public URL with the network panel open.",
168
171
  "Read the response status.",
@@ -200,7 +203,7 @@ export const authored = {
200
203
  "Return to /admin/products and delete it, confirming.",
201
204
  "Reload the public URL with the network panel open and read the status.",
202
205
  "Open /products and search for it.",
203
- "Open the search page and search for it there too.",
206
+ "Open /search and search for it there too.",
204
207
  ],
205
208
  inputs: { title: "QA Disposable Listing", price: 299 },
206
209
  expectedBehaviour: "A deleted listing leaves every grid it appeared in, not only its own page. Search is the one most likely to lag, because its index is written on save — a listing removed from the collection but left in the index renders a result card that 404s when clicked.",
@@ -213,18 +216,21 @@ export const authored = {
213
216
  startPage: "/admin/products",
214
217
  steps: [
215
218
  SIGN_IN_ADMIN,
216
- "Open /admin/orders and find an order, noting one product it contains.",
217
- "Open /admin/products and attempt to delete that product.",
218
- "Read whether the interface refuses, warns, or deletes.",
219
- "If it deletes, open that order and read whether its rows still render.",
220
- "Click the product link on the order's row and read where it lands.",
221
- "Record which of the three behaviours occurred.",
219
+ "Open /admin/orders, open any order, and write down its id and the exact title of one product it contains.",
220
+ "Open /admin/products, find that product, and delete it.",
221
+ "Read whether the interface refuses, warns about the order, or simply deletes.",
222
+ "Reload /admin/products and confirm whether the product is gone.",
223
+ "Reopen the order from step 2 and read whether its rows still render with that title and price.",
224
+ "Click the product link on the order's row and read exactly where it lands — a product page, a 404, or nothing.",
225
+ "Repeat the link check as the BUYER on /user/orders for the same order, since that is where a real person would meet it.",
222
226
  ],
227
+ inputs: { deleteIsHard: true },
223
228
  expectedBehaviour: "An order's rows denormalise the title, price and image they display, so an existing order survives the product's deletion — but the buyer's link into that product breaks. Either outcome can be defensible; what matters is that it is deliberate and stated rather than discovered by a buyer opening an old order.",
224
- expectedUiState: "Record exactly what happened: a refusal naming the orders, an archive instead of a delete, or a hard delete. If it deleted, record whether the order still renders and where its product link goes.",
225
- endResult: "If a product was hard-deleted, re-seed rather than attempting a manual repair.",
229
+ expectedUiState: "🛑 EXPECT A HARD DELETE. `adminDeleteProduct` checks only that a prize draw is not locked and then deletes the document; there is no order check anywhere, and no archive path. So expect no refusal and no warning. The order should still render its rows from the denormalised title and price — if it does NOT, that is a far more serious finding than the broken link. Record where the product link lands, on both the admin and the buyer surfaces.",
230
+ expectedData: { deleteRefused: false, orderRowsStillRender: true },
231
+ endResult: "A seeded product has been destroyed. `products` is seed-owned and is re-seeded around every run, so this is recoverable — but re-seed rather than attempting a manual repair, and do not run this case against a product another case in the same batch depends on.",
226
232
  needsReview: true,
227
- reviewNote: "The intended behaviour is not settled. The case exists to record which of refuse / archive / hard-delete actually happens, and what it does to an existing order.",
233
+ reviewNote: "What the code does is now settled — a hard delete with no order guard. What is NOT settled is whether that is the intended behaviour, so the case records the consequence for a real buyer rather than asserting a verdict on the design.",
228
234
  },
229
235
  "checklist-admin-content-deletes-delete-confirmations-name-the-record": {
230
236
  roles: ["admin"],
@@ -13,6 +13,36 @@
13
13
  * checking the interface. Two cases here read the PUBLIC response rather than the
14
14
  * admin form.
15
15
  *
16
+ * VERIFIED FIXED 2026-09-29 before asserting it: `adSettings` is now in
17
+ * `PRIVATE_SITE_SETTINGS_FIELDS`, and `/api/ads` serves the public view behind
18
+ * `if (String(item.status || "") !== "active") return false`. So the ads case
19
+ * should PASS; it is kept because the leak was invisible from the admin seat.
20
+ *
21
+ * 🛑 THE NAVIGATION EDITOR REACHES NO PUBLIC SURFACE.
22
+ *
23
+ * `AdminNavigationView` reads and writes `siteSettings.navbarConfig.navItems`
24
+ * through /api/admin/navigation. The public navbar renders `MAIN_NAV_ITEMS`
25
+ * from `src/constants/navigation.tsx` — a static array, filtered only by the
26
+ * listing-type toggles. A repo-wide grep for `navbarConfig` finds the two admin
27
+ * routes, the `/api/admin/site` field list, the seed and the projection, and
28
+ * **no renderer at all**. So it is a complete CRUD surface over data nothing
29
+ * displays, and the projection lists `navbarConfig` as private with the reason
30
+ * "rendered server-side" — which is not true of anything. Recorded in
31
+ * OUTOFSCOPE; the case below now expects the public half to fail.
32
+ *
33
+ * 🛑 THERE IS NO FEATURE-FLAGS SURFACE. The `featureFlags` group was DELETED on
34
+ * 2026-08-29 — "11 of its 14 keys had zero readers, and the survivors were not
35
+ * flags at all". The survivors moved: listing and category types to the
36
+ * `listings` tab (a PRODUCT CONTROL, per that schema comment), and
37
+ * `smsVerification` / `adminCheckoutBypass` to the `payment` group. The last
38
+ * case is rewritten onto the control that actually exists and actually reaches
39
+ * public behaviour.
40
+ *
41
+ * /admin/site has TWENTY tabs, not nineteen: about, branding, appearance,
42
+ * themes, announcement, seo, contact, watermark, fees, procurement, emi, gst,
43
+ * listings, integrations, shipping, auction, limits, whatsapp, notifications,
44
+ * legal.
45
+ *
16
46
  * @tag domain:tester
17
47
  * @tag layer:seed
18
48
  * @tag pattern:none
@@ -26,18 +56,21 @@ export const authored = {
26
56
  startPage: "/admin/ads",
27
57
  steps: [
28
58
  "Sign in as admin@letitrip.in / TempPass123!.",
29
- "Open the admin ads surface and read every existing ad, its status and its provider settings.",
30
- "Create an ad named 'QA Ad crud-preview' in a DRAFT state, with a placement and any provider credential field filled in.",
31
- "Save, RELOAD, and read every field including how the credential is displayed.",
59
+ "Open /admin/ads and read every existing ad, its status and its placement.",
60
+ "In the provider settings, read how 'AdSense client id' and 'Third-party script URL' are presented — whether a stored value is shown or only a masked placeholder.",
61
+ "Create an ad named 'QA Ad crud-preview' in a DRAFT state with a placement selected.",
62
+ "Save, RELOAD, and read every field.",
32
63
  "Use the preview and read what it renders.",
33
- "Open / in a private window and check the draft ad is NOT rendered anywhere.",
34
- "Open that page's source and search for the ad's name and for the credential value.",
35
- "Delete the ad.",
64
+ "Sign out, open /, and check the draft ad is NOT rendered anywhere.",
65
+ "View that page's source and search it for 'QA Ad crud-preview' and for any AdSense client id.",
66
+ "Open /api/ads signed out and check the draft is absent from the response.",
67
+ "Sign back in and delete the ad.",
36
68
  ],
37
- inputs: { name: "QA Ad crud-preview", status: "draft" },
69
+ // audit-hardcoded-api-routes-ok: a URL the tester opens by hand, not a call site.
70
+ inputs: { name: "QA Ad crud-preview", status: "draft", publicEndpoint: "/api/ads" },
38
71
  expectedBehaviour: "A draft ad is invisible publicly and its provider credentials never leave the server. The ads group was written by the admin form while undeclared on the settings type, so it was published wholesale by an unauthenticated endpoint — including drafts and unmasked credentials — and nothing type-driven could see it.",
39
- expectedUiState: "The ad round-trips with its credential MASKED in the admin form. The public page does not render it and its source contains neither the ad's name nor the credential value. A draft appearing publicly, or a credential in the source, is a leak rather than a display bug.",
40
- expectedData: { credentialInPublicSource: 0 },
72
+ expectedUiState: "The ad round-trips. Provider credentials appear only as masked placeholders in the admin form, never as editable stored values. The public page does not render the draft, its source contains neither the ad's name nor an AdSense id, and /api/ads omits it — that endpoint keeps only `status === \"active\"`. A draft appearing publicly, or a credential in the source, is a leak rather than a display bug.",
73
+ expectedData: { credentialInPublicSource: 0, draftInPublicAdsResponse: false },
41
74
  endResult: "The ad is deleted by the final step.",
42
75
  },
43
76
  "checklist-admin-content-marketing-newsletter-export-admin": {
@@ -45,13 +78,14 @@ export const authored = {
45
78
  startPage: "/admin/newsletter",
46
79
  steps: [
47
80
  "Sign in as admin@letitrip.in / TempPass123!.",
48
- "Open the admin newsletter surface and read the subscriber count and the rows.",
81
+ "Open /admin/newsletter and read the subscriber count and the rows.",
49
82
  "Read whether subscriber email addresses are shown in full or masked.",
50
- "Trigger the export and read what the page does immediately.",
51
- "Navigate away and back, then read whether the export's progress is still reported.",
83
+ "Click 'Export CSV' and read what the page does IMMEDIATELY — whether it returns at once with a job accepted, or blocks.",
84
+ "Navigate to /admin and back to /admin/newsletter, then read whether the export's progress is still reported.",
52
85
  "Wait for it to finish and open the exported file.",
53
86
  "Compare the file's row count against the subscriber count shown.",
54
87
  ],
88
+ inputs: { action: "Export CSV" },
55
89
  expectedBehaviour: "The export runs as a background job rather than inside the request — an unbounded scan of every subscriber is exactly the shape that dies at the ten-second function ceiling in production while a small local list passes. The page subscribes to the job's progress instead of waiting on a response.",
56
90
  expectedUiState: "The export returns immediately with a job accepted rather than freezing the page. Progress survives navigating away and back. The finished file's row count matches the subscriber count. A request that hangs and then times out is the failure.",
57
91
  endResult: "An export file exists. Note whether subscriber emails are exposed in the LIST as well as in the file — the file is expected to carry them, the list is a judgement.",
@@ -60,12 +94,12 @@ export const authored = {
60
94
  roles: ["admin", "guest"],
61
95
  startPage: "/contact",
62
96
  steps: [
63
- "Open /contact in a private window and submit a message: name 'QA Contact submissions-admin', email qa-contact-admin@mailnull.com, subject 'Checklist submission', message 'Sent by the tester checklist.'.",
64
- "Sign in as admin@letitrip.in / TempPass123!.",
65
- "Open the admin contact submissions surface and find that message.",
66
- "Read every field of it and check the full message body is readable from the list or a detail view.",
67
- "Read the row actions offered and check at least one lets you VIEW rather than only act.",
68
- "Use any status control to mark it handled, then RELOAD and read the status.",
97
+ "Signed out, open /contact and submit a message: name 'QA Contact submissions-admin', email qa-contact-admin@mailnull.com, subject 'Checklist submission', message 'Sent by the tester checklist.'.",
98
+ "Sign in as admin@letitrip.in / TempPass123! and open /admin/contact.",
99
+ "Find that message and read its status — a new submission reads 'new'.",
100
+ "Open the row menu and use its View action, then read every field in the drawer and confirm the full message body is there rather than a truncated preview.",
101
+ "Use the row menu's mark-read action, RELOAD, and read the status.",
102
+ "Use the row menu's archive action, RELOAD, and read the status again.",
69
103
  "Search zzzznope and read the result count.",
70
104
  ],
71
105
  inputs: {
@@ -74,7 +108,7 @@ export const authored = {
74
108
  message: "Sent by the tester checklist.",
75
109
  },
76
110
  expectedBehaviour: "A submitted message arrives in the admin surface with its full body readable. This is the round trip the contact form's own case cannot prove — a form that shows a confirmation and delivers nothing looks identical to one that works, from the sender's side.",
77
- expectedUiState: "The submission appears with its name, email, subject and full body. A view affordance exists alongside any actions. The status change survives a reload. 'zzzznope' returns zero rows rather than the full list.",
111
+ expectedUiState: "The submission appears with its name, email, subject and full body. The View action opens a drawer rather than only offering mutations — a row you can act on but not read is the acting-blind shape. Status moves new → read → resolved and each survives a reload. 'zzzznope' returns zero rows rather than the full list.",
78
112
  expectedData: { nonsenseResultCount: 0 },
79
113
  endResult: "The submission remains, marked handled. Its arrival is the assertion — a missing message means the contact form delivers nowhere.",
80
114
  },
@@ -83,7 +117,7 @@ export const authored = {
83
117
  startPage: "/admin/media",
84
118
  steps: [
85
119
  "Sign in as admin@letitrip.in / TempPass123!.",
86
- "Open the admin media library and read the grid.",
120
+ "Open /admin/media and read the grid.",
87
121
  "Check every tile renders its image rather than an empty bordered box.",
88
122
  "Read the filter and sort controls and use each, confirming the results change.",
89
123
  "Search for a known filename, then search zzzznope and read the count.",
@@ -101,33 +135,36 @@ export const authored = {
101
135
  startPage: "/admin/navigation",
102
136
  steps: [
103
137
  "Sign in as admin@letitrip.in / TempPass123!.",
104
- "Open /admin/navigation and read every nav item, its label and its destination.",
138
+ "Open /admin/navigation and write down every nav item, its label, its destination and its order.",
105
139
  "Create one labelled 'QA Nav navigation-editor' pointing at /about, and save.",
106
- "RELOAD and read every field of it.",
107
- "Open / in a private window and find the new item in the public navigation.",
108
- "Click it and confirm it lands on /about.",
109
- "Reorder two items, save, reload, and check the new order holds publicly.",
110
- "Delete the item and confirm it disappears from both the editor and the public nav.",
140
+ "RELOAD the editor and read every field of it — this half is the editor's own round trip.",
141
+ "Sign out and open /. Read the public navbar and record every item it shows.",
142
+ "Compare that list against the editor's: is 'QA Nav navigation-editor' there, and do the two lists agree at all?",
143
+ "Sign back in, reorder two items, save, reload, then sign out and check whether the public navbar order followed.",
144
+ "Sign back in, delete the item, restore the original order, and confirm the editor matches what was written down in step 2.",
111
145
  ],
112
146
  inputs: { label: "QA Nav navigation-editor", href: "/about" },
113
- expectedBehaviour: "The editor is the source of the public navigation, so a create, a reorder and a delete all reach it. A nav entry pointing at a page that does not exist is blocked elsewhere as a hard failure; this case is the other half — an entry that exists and never renders.",
114
- expectedUiState: "The new item appears in the public navigation and resolves to /about. The reorder holds after a reload, publicly as well as in the editor. After the delete it is gone from both.",
115
- endResult: "The item is deleted and the original order restored. An item saved in the editor but absent from the public nav is the failure.",
147
+ expectedBehaviour: "An editor that saves is not an editor that reaches anything. The claim under test is that /admin/navigation is the source of the public navbar — a create, a reorder and a delete should all show up there.",
148
+ expectedUiState: "🛑 EXPECT THE PUBLIC HALF TO FAIL, AND RECORD IT AS A FAILURE. The editor round trip should work: the item saves and survives a reload. But the public navbar is `MAIN_NAV_ITEMS`, a static array in src/constants/navigation.tsx filtered only by the listing-type toggles, and nothing anywhere reads `navbarConfig.navItems` — so expect the new item to be absent from /, the reorder to change nothing publicly, and the two lists to disagree. Record both lists side by side; that comparison is the finding.",
149
+ expectedData: { savedInEditor: true, appearsInPublicNavbar: false },
150
+ endResult: "The item is deleted and the original order restored, so the editor matches step 2 again. Keep the halves apart: the editor persisting correctly and reaching nothing is a different defect from an editor that fails to save.",
116
151
  },
117
152
  "checklist-admin-content-marketing-settings-navigation-actions": {
118
153
  roles: ["admin"],
119
154
  startPage: "/admin/site",
120
155
  steps: [
121
156
  "Sign in as admin@letitrip.in / TempPass123!.",
122
- "Open /admin/site and read every tab in the strip.",
123
- "Open the header search and type a settings term such as fees.",
157
+ "Open /admin/site and read every tab in the strip — there should be twenty: about, branding, appearance, themes, announcement, seo, contact, watermark, fees, procurement, emi, gst, listings, integrations, shipping, auction, limits, whatsapp, notifications, legal.",
158
+ "Open the header search and type 'fees'.",
124
159
  "Click the result and read which tab opens.",
125
- "Read the URL and check it names the tab.",
126
- "Copy that URL, open it in a new tab, and confirm it opens on the same tab.",
127
- "Open /admin/site with an unknown tab value and read what happens.",
160
+ "Read the URL and check it names the tab rather than being bare /admin/site.",
161
+ "Copy that URL, open it in a new browser tab, and confirm it opens on the same settings tab.",
162
+ "Open /admin/site?tab=zzzznope and read what happens.",
128
163
  ],
129
- expectedBehaviour: "Settings are reachable by deep link and by search, and search reaches individual tabs rather than only the page. Landing on the first of nineteen tabs and leaving the admin to hunt is a fail rather than a partial pass, and an unknown tab value falls back quietly because the realistic cause is a stale bookmark.",
164
+ inputs: { searchTerm: "fees", expectedTab: "fees", unknownTab: "zzzznope", tabCount: 20 },
165
+ expectedBehaviour: "Settings are reachable by deep link and by search, and search reaches individual tabs rather than only the page. Landing on the first of twenty tabs and leaving the admin to hunt is a fail rather than a partial pass, and an unknown tab value falls back quietly because the realistic cause is a stale bookmark.",
130
166
  expectedUiState: "The search result opens the named tab, the URL carries it, and the copied URL reopens the same tab. An unknown value opens the default with no error banner and no blank panel.",
167
+ expectedData: { tabCount: 20 },
131
168
  endResult: "Read-only; nothing is saved.",
132
169
  },
133
170
  "checklist-admin-content-marketing-features-feature-flags-admin": {
@@ -135,17 +172,17 @@ export const authored = {
135
172
  startPage: "/admin/site",
136
173
  steps: [
137
174
  "Sign in as admin@letitrip.in / TempPass123!.",
138
- "Open the feature-flags surface and read every flag and its state.",
139
- "Write down the state of each.",
140
- "Turn ONE flag off and save.",
141
- "RELOAD and check only that flag changed.",
142
- "Open the public surface that flag controls and confirm the behaviour changed.",
143
- "Turn it back on, save, and confirm the behaviour returns.",
144
- "Open /api/site-settings in a private window and read whether any flag is present in the response.",
175
+ "Open /admin/site, then its 'Listings' tab, and write down the state of every listing-type and category-type toggle.",
176
+ "Turn the auction listing type OFF and save.",
177
+ "RELOAD the tab and check that ONLY that toggle changed.",
178
+ "Sign out and open /: the Auctions item must be gone from the main navbar. Then open /auctions and read what is shown.",
179
+ "Sign back in, turn the auction type back ON, save, sign out, and confirm the navbar item and /auctions both return.",
180
+ "Signed out, open /api/site-settings and search the response for 'adminCheckoutBypass', 'smsVerification' and 'commissions'.",
145
181
  ],
146
- expectedBehaviour: "A flag reaches the behaviour it names, and a save of one leaves the others alone. The public settings response is an ALLOW-LIST projection: a flag such as the admin checkout bypass has no business being readable by an anonymous caller, and it was published for a long time because the response was built by spreading the document and deleting three keys.",
147
- expectedUiState: "Only the toggled flag changes after the reload. The public behaviour follows it in both directions. The public settings response carries no operational flag — specifically nothing naming a checkout bypass or an admin capability.",
148
- expectedData: { operationalFlagsInPublicResponse: 0 },
149
- endResult: "Every flag is back in its original state. A flag visible in the public response is a projection leak rather than a settings bug.",
182
+ inputs: { tab: "listings", toggled: "auction" },
183
+ expectedBehaviour: "A product control reaches the behaviour it names, in both directions, and saving one leaves the others alone — a disabled listing type is hidden from every listing surface and rejected on create and add-to-cart. The public settings response is an ALLOW-LIST projection: an operational setting such as the admin checkout bypass has no business being readable by an anonymous caller, and such settings were published for a long time because the response was built by spreading the document and deleting three keys.",
184
+ expectedUiState: "Only the toggled type changes after the reload. With auctions off the navbar drops its Auctions item; with them on it returns. The public settings response contains no operational setting — nothing naming a checkout bypass, an SMS-verification switch or the commission economics.",
185
+ expectedData: { operationalSettingsInPublicResponse: 0 },
186
+ endResult: "Every toggle is back in its original state and the navbar shows Auctions again. A setting visible in the public response is a projection leak rather than a settings bug. Note this page has NO 'feature flags' surface: that group was deleted in favour of this one, because 11 of its 14 keys had no readers at all.",
150
187
  },
151
188
  };
@@ -63,9 +63,9 @@ export const authored = {
63
63
  steps: [
64
64
  "Sign in as admin@letitrip.in / TempPass123!.",
65
65
  "Open /admin/coupons, find ARENAVIP, and read its per-user limit (1), total limit (50) and current usage (12).",
66
- "Sign in as vivaan.kapoor@gmail.com / TempPass123! and place an order of 2 × product-beyblade-original-dranzer-s (₹2,998, above ARENAVIP's ₹2,000 minimum) applying ARENAVIP.",
66
+ "Swap identity before this step: this case lists admin among its roles, so the harness resolves the whole batch to the ADMIN session. Copy tester/.tester-runs/session-buyer.json over session.json and CLOSE THE BROWSER — the MCP reads that file only at context creation. An admin can apply a coupon perfectly well, so without this the case passes while testing the wrong person. Then sign in as vivaan.kapoor@gmail.com / TempPass123! and place an order of 2 × product-beyblade-original-dranzer-s (₹2,998, above ARENAVIP's ₹2,000 minimum) applying ARENAVIP.",
67
67
  "Attempt a second order applying it again and read the refusal.",
68
- "Sign in as rehan.sheikh@gmail.com / TempPass123! and apply ARENAVIP at checkout on the same two items.",
68
+ "Swap identity before this step: this case lists admin among its roles, so the harness resolves the whole batch to the ADMIN session. Copy tester/.tester-runs/session-buyer.json over session.json and CLOSE THE BROWSER — the MCP reads that file only at context creation. An admin can apply a coupon perfectly well, so without this the case passes while testing the wrong person. Then sign in as rehan.sheikh@gmail.com / TempPass123! and apply ARENAVIP at checkout on the same two items.",
69
69
  "Sign back in as admin and read the usage count.",
70
70
  ],
71
71
  inputs: { code: "ARENAVIP", perUserLimit: 1 },
@@ -79,7 +79,7 @@ export const authored = {
79
79
  startPage: "/admin/coupons",
80
80
  steps: [
81
81
  "Sign in as admin@letitrip.in / TempPass123! and confirm QAADMIN15 has a minimum purchase of 500 and a maximum discount of 200.",
82
- "Sign in as vivaan.kapoor@gmail.com / TempPass123!.",
82
+ "Swap identity before this step: this case lists admin among its roles, so the harness resolves the whole batch to the ADMIN session. Copy tester/.tester-runs/session-buyer.json over session.json and CLOSE THE BROWSER — the MCP reads that file only at context creation. An admin can apply a coupon perfectly well, so without this the case passes while testing the wrong person. Then sign in as vivaan.kapoor@gmail.com / TempPass123!.",
83
83
  "Add product-beyblade-metal-storm-pegasus (₹99) to the cart alone and apply QAADMIN15 at checkout.",
84
84
  "Read the refusal and whether it names the required amount.",
85
85
  "Add product-beyblade-original-driger-v (₹1,799) so the subtotal clears the minimum, and apply again.",
@@ -88,7 +88,7 @@ export const authored = {
88
88
  ],
89
89
  inputs: { code: "QAADMIN15", minPurchase: 500, maxDiscount: 200 },
90
90
  expectedBehaviour: "The minimum gates whether the coupon applies at all and the maximum caps what it gives. The minimum is measured against the items the coupon could actually discount rather than the whole cart, and money is decimal rupees throughout — a validator rejecting 799.50 as a non-integer is a leftover from the old whole-number storage.",
91
- expectedUiState: "Below the minimum the coupon is refused with a message naming the threshold, not a bare 'Invalid coupon'. Above it the discount is capped at ₹200.00. A decimal amount is accepted in the coupon form.",
91
+ expectedUiState: "Below the minimum the coupon is refused with a message naming the REASON — today 'Minimum purchase requirement not met', which names the reason but NOT the amount, and the missing amount is a known gap recorded against content-discovery/coupons rather than a fresh finding here. What must not appear is a bare 'Invalid coupon', which would tell the buyer nothing about how much more to add. Above it the discount is capped at ₹200.00. A decimal amount is accepted in the coupon form.",
92
92
  endResult: "Nothing is ordered; leave checkout without placing.",
93
93
  },
94
94
  "checklist-admin-coupons-coupon-scope-admin-vs-seller": {
@@ -113,7 +113,7 @@ export const authored = {
113
113
  "Sign in as admin@letitrip.in / TempPass123!.",
114
114
  "Open /admin/coupons, open SEALED20, and write down its end date.",
115
115
  "Set its end date to yesterday and LEAVE the Active toggle ON, then save — it is now past its end date while still flagged active.",
116
- "Sign in as vivaan.kapoor@gmail.com / TempPass123! and apply SEALED20 at checkout.",
116
+ "Swap identity before this step: this case lists admin among its roles, so the harness resolves the whole batch to the ADMIN session. Copy tester/.tester-runs/session-buyer.json over session.json and CLOSE THE BROWSER — the MCP reads that file only at context creation. An admin can apply a coupon perfectly well, so without this the case passes while testing the wrong person. Then sign in as vivaan.kapoor@gmail.com / TempPass123! and apply SEALED20 at checkout.",
117
117
  "Read the exact wording of the refusal.",
118
118
  "Sign back in as admin, deactivate BLADER50 without touching its dates, and apply that one as the buyer.",
119
119
  "Compare the two refusal messages.",
@@ -129,7 +129,7 @@ export const authored = {
129
129
  startPage: "/admin/coupons",
130
130
  steps: [
131
131
  "Sign in as admin@letitrip.in / TempPass123! and confirm QAADMIN15 is admin-scoped with no store.",
132
- "Sign in as vivaan.kapoor@gmail.com / TempPass123!.",
132
+ "Swap identity before this step: this case lists admin among its roles, so the harness resolves the whole batch to the ADMIN session. Copy tester/.tester-runs/session-buyer.json over session.json and CLOSE THE BROWSER — the MCP reads that file only at context creation. An admin can apply a coupon perfectly well, so without this the case passes while testing the wrong person. Then sign in as vivaan.kapoor@gmail.com / TempPass123!.",
133
133
  "Add product-beyblade-burst-regalia-genesis (₹1,399, Beyblade Arena) to the cart.",
134
134
  "Add prizedraw-beyblade-original-vintage-vault (₹299, LetItRip Official) and set its quantity to 2.",
135
135
  "Apply QAADMIN15 at checkout and read the discount.",
@@ -163,7 +163,7 @@ export const authored = {
163
163
  "Sign in as admin@letitrip.in / TempPass123!.",
164
164
  "Create a coupon coded QACATADMIN20 at 20%, restricted to the Beyblade Burst category, no minimum.",
165
165
  "Save, RELOAD, and confirm the restriction persisted.",
166
- "Sign in as vivaan.kapoor@gmail.com / TempPass123!.",
166
+ "Swap identity before this step: this case lists admin among its roles, so the harness resolves the whole batch to the ADMIN session. Copy tester/.tester-runs/session-buyer.json over session.json and CLOSE THE BROWSER — the MCP reads that file only at context creation. An admin can apply a coupon perfectly well, so without this the case passes while testing the wrong person. Then sign in as vivaan.kapoor@gmail.com / TempPass123!.",
167
167
  "Add product-beyblade-metal-storm-pegasus (₹1,299, Metal Fight) alone and apply QACATADMIN20 — read the refusal.",
168
168
  "Add product-beyblade-burst-regalia-genesis (₹1,399, Burst) and apply again.",
169
169
  "Read the discount and compare it against 20% of ₹1,399.",
@@ -201,7 +201,7 @@ export const authored = {
201
201
  "Sign in as admin@letitrip.in / TempPass123!.",
202
202
  "Open /admin/coupons and read the usage figure on every row.",
203
203
  "Note the figure for ARENA25.",
204
- "Sign in as vivaan.kapoor@gmail.com / TempPass123! and place an order applying ARENA25.",
204
+ "Swap identity before this step: this case lists admin among its roles, so the harness resolves the whole batch to the ADMIN session. Copy tester/.tester-runs/session-buyer.json over session.json and CLOSE THE BROWSER — the MCP reads that file only at context creation. An admin can apply a coupon perfectly well, so without this the case passes while testing the wrong person. Then sign in as vivaan.kapoor@gmail.com / TempPass123! and place an order applying ARENA25.",
205
205
  "Sign back in as admin and read ARENA25's usage figure again.",
206
206
  "Open the coupon's detail and read whether it shows total usage, per-user usage, or both.",
207
207
  "Reload and confirm the figure persists.",
@@ -30,7 +30,7 @@ export const authored = {
30
30
  "Sign in as admin@letitrip.in / TempPass123!.",
31
31
  "Open /admin/events/new and read every option in the event type picker.",
32
32
  "Write the list down and compare it against the real event types the catalogue supports.",
33
- "Create an event titled 'QA Event raffle-open' with an open raffle, a prize, a start date of today and an end date a week out.",
33
+ "Create an event titled 'QA Event raffle-open' with a raffle of type 'Open raffle (all confirmed entries)' — the full dropdown label, a prize, a start date of today and an end date a week out.",
34
34
  "Save, RELOAD, and read the raffle type, the prize and the dates.",
35
35
  "Sign out, sign in as rehan.sheikh@gmail.com / TempPass123!, and enter the raffle from its public page.",
36
36
  "Sign back in as admin and read the entry count on the event.",
@@ -45,10 +45,10 @@ export const authored = {
45
45
  startPage: "/admin/events/new",
46
46
  steps: [
47
47
  "Sign in as admin@letitrip.in / TempPass123!.",
48
- "Create an event titled 'QA Event raffle-scorers' with a top-N-scorers raffle, N set to 3, and a prize.",
48
+ "Create an event titled 'QA Event raffle-scorers' with the raffle type 'Top N scorers' — that exact dropdown label — N set to 3, and a prize.",
49
49
  "Save, RELOAD, and read the raffle type and the N value.",
50
- "Open the public page and read how the Overview tab describes who can win.",
51
- "Sign in as rehan.sheikh@gmail.com / TempPass123! and enter, then read the Leaderboard tab.",
50
+ "Open the event's public page at /events/{its id} and read how the Overview tab describes who can win.",
51
+ "Swap identity before this step: this case lists admin among its roles, so the harness resolves the whole batch to the ADMIN session. Copy tester/.tester-runs/session-buyer.json over session.json and CLOSE THE BROWSER — the MCP reads it only at context creation. Then sign in as rehan.sheikh@gmail.com / TempPass123!, enter the event, and read the Leaderboard tab. Entering as the ADMIN would still populate a leaderboard, so this would pass while measuring the wrong account.",
52
52
  "Read whether the leaderboard is ordered by score and whether the qualifying cut-off is stated.",
53
53
  ],
54
54
  inputs: { title: "QA Event raffle-scorers", raffleType: "top_n_scorers", topN: 3 },
@@ -61,9 +61,9 @@ export const authored = {
61
61
  startPage: "/admin/events/new",
62
62
  steps: [
63
63
  "Sign in as admin@letitrip.in / TempPass123!.",
64
- "Create an event titled 'QA Event raffle-participants' with a top-N-participants raffle, N set to 3, and a prize.",
64
+ "Create an event titled 'QA Event raffle-participants' with the raffle type 'Top N earliest participants' — note the word 'earliest', which a tester scanning for 'Top N participants' would not match, N set to 3, and a prize.",
65
65
  "Save, RELOAD, and read the raffle type and N.",
66
- "Open the public page and read how the Overview tab describes who can win.",
66
+ "Open the event's public page at /events/{its id} and read how the Overview tab describes who can win.",
67
67
  "Compare that description against the scorers event's.",
68
68
  "Enter as rehan.sheikh@gmail.com / TempPass123! and compare the two leaderboards' ordering.",
69
69
  ],
@@ -97,7 +97,7 @@ export const authored = {
97
97
  "Create an event titled 'QA Event spin-create' of the spin-wheel type.",
98
98
  "Define three prizes with labels and weights, set a maximum of 2 spins per user, and set a spin window covering today.",
99
99
  "Save, RELOAD, and read every prize, weight, the maximum and the window.",
100
- "Open the public page and read the wheel and its segments.",
100
+ "Open the event's public page at /events/{its id} and read the wheel and its segments.",
101
101
  "Check the wheel shows the prize labels and does NOT disclose their weights.",
102
102
  ],
103
103
  inputs: { title: "QA Event spin-create", prizeCount: 3, maxPerUser: 2 },
@@ -129,7 +129,7 @@ export const authored = {
129
129
  startPage: "/admin/events",
130
130
  steps: [
131
131
  "Sign in as admin@letitrip.in / TempPass123!.",
132
- "Open an event's entries page and read every column.",
132
+ "Open /admin/events/event-favourite-blader-poll/entries — an ACTIVE poll carrying 3 seeded entries, so the table is not empty — and read every column.",
133
133
  "Read every status filter offered and select each, noting any that returns nothing.",
134
134
  "For each empty filter, check the unfiltered list for rows of that status.",
135
135
  "Read the row actions offered and check at least one lets you VIEW an entry.",
@@ -147,7 +147,7 @@ export const authored = {
147
147
  startPage: "/admin/events",
148
148
  steps: [
149
149
  "Sign in as admin@letitrip.in / TempPass123!.",
150
- "Open the entries page for an event that collects responses — a poll, survey or feedback event.",
150
+ "Open /admin/events/event-favourite-blader-poll/entries. That is the poll, and it has 3 seeded entries; picking an event with none would leave nothing to read and is indistinguishable from a broken table.",
151
151
  "Find a row offering Confirm, Waitlist or Cancel.",
152
152
  "WITHOUT using any of those, look for a way to read the entry's actual content first.",
153
153
  "Open the entry and read the participant's response in full.",
@@ -4,12 +4,23 @@
4
4
  *
5
5
  * 🛑 EVERY CASE HERE TESTS AN EFFECT, NEVER A FUNCTION.
6
6
  *
7
- * 55 Firebase functions are deployed — 28 scheduled, 20 Firestore triggers, 7
7
+ * 53 Firebase functions are defined — 27 scheduled, 20 Firestore triggers, 6
8
8
  * HTTPS — and none had a checklist case. The reason is that the obvious way to
9
9
  * write one is untestable: "trigger the cron and confirm it ran" cannot be done
10
10
  * from a browser in a session, so every such case would be answered null
11
11
  * forever, which reads as coverage while providing none.
12
12
  *
13
+ * 🛑 THOSE FOUR NUMBERS WERE 55 / 28 / 20 / 7 UNTIL 2026-09-29, and three of them
14
+ * were wrong. Recounted from the registry itself:
15
+ *
16
+ * grep -c "defineFunction(" appkit/src/_internal/server/functions/scheduled.ts
17
+ * firestore.ts
18
+ * https.ts
19
+ *
20
+ * CLAUDE.md warns that the scheduled figure has drifted four separate times and
21
+ * says to recount rather than quote it; this file quoted it and drifted a fifth.
22
+ * Recount before trusting any of them — including these.
23
+ *
13
24
  * So each case instead reads the thing the function is RESPONSIBLE FOR. A nightly
14
25
  * reconciler is tested by recounting what it reconciles; a settlement job by
15
26
  * whether the winner can pay; a self-triggering write-back by whether its
@@ -44,6 +55,8 @@ export const authored = {
44
55
  expectedUiState: "The per-function breakdown is on screen, sorted. The top function's count is of the same order as the rest of the active ones. A ratio in the thousands is the finding, and the function's name is quoted.",
45
56
  expectedData: { runawayFunctions: 0 },
46
57
  endResult: "Read-only; nothing persists. 🛑 Read the PER-FUNCTION breakdown, never the aggregate — the aggregate never names the culprit, which is exactly why #92 was found from a billing page rather than from monitoring.",
58
+ requiresHumanChannel: true,
59
+ humanChannelReason: "The per-function invocation breakdown lives in the Firebase console, which needs an interactive Google sign-in the automated browser does not have. There is no in-product surface for it — /admin/maintenance/cloud-logs shows raw log lines, not per-function counts. A human runs this one; the automated answer is null, naming the console as the missing channel.",
47
60
  },
48
61
  "checklist-admin-firebase-function-effects-function-errors-page-has-no-producer": {
49
62
  roles: ["admin"],
@@ -54,7 +67,7 @@ export const authored = {
54
67
  "Open /admin/maintenance/server-errors in another tab and read what it shows.",
55
68
  "Compare the two: note whether server-errors has rows while function-errors does not.",
56
69
  ],
57
- expectedBehaviour: "BEFORE: the page is expected to list Cloud Function failures. AFTER: it is empty, and it will remain empty no matter what breaks, because NOTHING IN PRODUCTION WRITES a serverErrors row with source \"function\". The only producer is wrapJobHandler, which is referenced solely by its own test. The sibling server-errors page has a real producer and should show rows, which is what makes the contrast legible.",
70
+ expectedBehaviour: "BEFORE: the page is expected to list Cloud Function failures. AFTER: it is empty, and it will remain empty no matter what breaks, because NOTHING IN PRODUCTION WRITES a serverErrors row with source \"function\". `wrapJobHandler` is the only thing that would, and as of 2026-09-29 it has NO CALL SITES AT ALL — it is defined, re-exported from the server barrel, and invoked nowhere. (This case used to say \"referenced solely by its own test\", which stopped being true when both test suites were deleted; the claim is now stronger, not weaker.) The sibling server-errors page has a real producer and should show rows, which is what makes the contrast legible.",
58
71
  expectedUiState: "function-errors renders an empty list or empty state. server-errors renders actual rows. Screenshot both, side by side if possible.",
59
72
  expectedData: { functionErrorRows: 0 },
60
73
  endResult: "🛑 ANSWER NO. This case is authored to FAIL against today's product, and that is deliberate: a case reading \"check that errors appear here\" would pass vacuously against a surface that can never populate. A red case with a screenshot is a tracked defect; a note in a document is not.",
@@ -194,7 +207,7 @@ export const authored = {
194
207
  steps: [
195
208
  ADMIN,
196
209
  "Open /admin/orders and note the most recent order's id and the time it was created.",
197
- "Open the admin notifications or inbox surface and look for a signal corresponding to that order.",
210
+ "Open /admin/admin-notifications — the staff inbox, which is a different page from /admin/notifications, the per-user notification list — and look for a signal corresponding to that order.",
198
211
  "Note whether the signal exists, and whether there is exactly one rather than one per employee.",
199
212
  ],
200
213
  expectedBehaviour: "A new order leaves something an operator can notice, as ONE durable staff-visible record rather than a personal notification fanned out to every employee. The fan-out shape is what nearly exhausted a 100/day email allowance from a single event, so one row read by the admin inbox and counted by the daily digest is the correct shape.",
@@ -229,6 +242,8 @@ export const authored = {
229
242
  ],
230
243
  expectedBehaviour: "The two counts match. Scheduler bills per REGISTERED job rather than per invocation, so a job with no matching function is paid for and dead, and a scheduled function with no job simply never runs — and nothing in the product reports either.",
231
244
  expectedUiState: "Two counts are written down and are equal. If they differ, name the jobs or functions that appear on only one side.",
232
- endResult: "Read-only; nothing persists. 🛑 RECOUNT rather than quoting a remembered number — this figure has drifted four separate times, which is exactly why the case asks for a count instead of stating one.",
245
+ endResult: "Read-only; nothing persists. 🛑 RECOUNT rather than quoting a remembered number — this figure has drifted five separate times now, including once inside this very file, which is exactly why the case asks for a count instead of stating one. As of 2026-09-29 the registry defines 27 scheduled functions; if Scheduler shows a different number, the difference is the finding.",
246
+ requiresHumanChannel: true,
247
+ humanChannelReason: "Both counts are read from Google consoles — Cloud Scheduler and the Firebase Functions list — which need an interactive Google sign-in the automated browser does not have. The registry side can be counted from source, but the billed side cannot, and the whole case is the comparison. A human runs this one.",
233
248
  },
234
249
  };