@mohasinac/appkit 4.42.1 → 4.42.3

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 (182) 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/components/AdminSupportTicketsView.js +27 -1
  13. package/dist/features/admin/constants/filter-tabs.d.ts +25 -0
  14. package/dist/features/admin/constants/filter-tabs.js +30 -0
  15. package/dist/features/admin/hooks/useAdminListingData.d.ts +35 -0
  16. package/dist/features/admin/hooks/useAdminListingData.js +45 -9
  17. package/dist/features/cart/schemas/index.d.ts +15 -0
  18. package/dist/features/cart/schemas/index.js +2 -0
  19. package/dist/features/cart/types/index.d.ts +22 -0
  20. package/dist/features/categories/components/CategoriesIndexListing.js +19 -1
  21. package/dist/features/categories/components/CategoriesIndexPageView.js +14 -1
  22. package/dist/features/classified/components/ClassifiedDetailPageView.d.ts +8 -0
  23. package/dist/features/classified/components/ClassifiedDetailPageView.js +1 -0
  24. package/dist/features/events/actions/event-actions.d.ts +13 -0
  25. package/dist/features/events/actions/event-actions.js +75 -1
  26. package/dist/features/events/repository/event-entry.repository.d.ts +17 -0
  27. package/dist/features/events/repository/event-entry.repository.js +36 -0
  28. package/dist/features/homepage/components/BrandsSection.js +1 -1
  29. package/dist/features/homepage/components/CustomerReviewsSection.js +1 -1
  30. package/dist/features/homepage/components/FeaturedBundlesSection.js +1 -1
  31. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  32. package/dist/features/media/upload/MediaUploadField.js +21 -3
  33. package/dist/features/orders/components/OrdersList.js +2 -1
  34. package/dist/features/orders/index.d.ts +1 -0
  35. package/dist/features/orders/index.js +1 -0
  36. package/dist/features/orders/types/index.d.ts +2 -0
  37. package/dist/features/orders/utils/order-ref.d.ts +45 -0
  38. package/dist/features/orders/utils/order-ref.js +58 -0
  39. package/dist/features/products/components/MakeOfferButton.d.ts +18 -1
  40. package/dist/features/products/components/MakeOfferButton.js +23 -1
  41. package/dist/features/products/components/ProductDetailPageView.d.ts +5 -0
  42. package/dist/features/products/components/ProductDetailPageView.js +3 -2
  43. package/dist/features/products/repository/products.repository.js +29 -2
  44. package/dist/features/seller/actions/offer-actions.js +32 -0
  45. package/dist/features/seller/components/QuickProductForm.js +15 -1
  46. package/dist/features/seller/components/SellerBidsView.js +3 -1
  47. package/dist/features/seller/components/SellerOffersView.js +2 -2
  48. package/dist/features/seller/components/SellerOrdersView.js +49 -6
  49. package/dist/features/seller/components/SellerProductsCards.js +23 -1
  50. package/dist/features/seller/components/SellerProductsView.js +27 -2
  51. package/dist/features/seller/hooks/useSellerListingData.d.ts +0 -1
  52. package/dist/features/seller/hooks/useSellerListingData.js +17 -19
  53. package/dist/features/shell/SectionForm.js +33 -2
  54. package/dist/features/tester/seed-data/_happy-path.d.ts +23 -0
  55. package/dist/features/tester/seed-data/_happy-path.js +710 -0
  56. package/dist/features/tester/seed-data/_money-flows.js +34 -23
  57. package/dist/features/tester/seed-data/authored/account-auth__profile-settings.js +22 -3
  58. package/dist/features/tester/seed-data/authored/addresses__address-filters.js +17 -12
  59. package/dist/features/tester/seed-data/authored/addresses__postal-lookup.js +7 -6
  60. package/dist/features/tester/seed-data/authored/addresses__postal-validation.js +10 -4
  61. package/dist/features/tester/seed-data/authored/addresses__state-picker.js +13 -8
  62. package/dist/features/tester/seed-data/authored/addresses__unban-request.js +49 -11
  63. package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +10 -6
  64. package/dist/features/tester/seed-data/authored/admin__bans-and-trust.js +71 -32
  65. package/dist/features/tester/seed-data/authored/admin__blog-faqs.js +80 -31
  66. package/dist/features/tester/seed-data/authored/admin__bug-hunter-rewards.js +62 -25
  67. package/dist/features/tester/seed-data/authored/admin__bulk-actions.js +61 -31
  68. package/dist/features/tester/seed-data/authored/admin__bundles.js +84 -37
  69. package/dist/features/tester/seed-data/authored/admin__buyer-data-admin.js +90 -57
  70. package/dist/features/tester/seed-data/authored/admin__catalog-listings.js +38 -26
  71. package/dist/features/tester/seed-data/authored/admin__category-brand-authoring.js +47 -28
  72. package/dist/features/tester/seed-data/authored/admin__classifieds-digitalcodes-live.js +118 -31
  73. package/dist/features/tester/seed-data/authored/admin__content-deletes.js +30 -24
  74. package/dist/features/tester/seed-data/authored/admin__content-marketing.js +83 -46
  75. package/dist/features/tester/seed-data/authored/admin__coupons.js +8 -8
  76. package/dist/features/tester/seed-data/authored/admin__events-raffles-spin.js +9 -9
  77. package/dist/features/tester/seed-data/authored/admin__firebase-function-effects.js +19 -4
  78. package/dist/features/tester/seed-data/authored/admin__media-watermark.js +77 -33
  79. package/dist/features/tester/seed-data/authored/admin__orders-fulfillment.js +5 -5
  80. package/dist/features/tester/seed-data/authored/admin__prize-draws-lotteries.js +29 -5
  81. package/dist/features/tester/seed-data/authored/admin__site-system.js +45 -23
  82. package/dist/features/tester/seed-data/authored/admin__uncovered-admin-pages.js +53 -28
  83. package/dist/features/tester/seed-data/authored/admin__users-trust.js +20 -20
  84. package/dist/features/tester/seed-data/authored/buying__bidding.js +19 -2
  85. package/dist/features/tester/seed-data/authored/buying__browsing-search.js +20 -5
  86. package/dist/features/tester/seed-data/authored/buying__buying-checkout.js +28 -10
  87. package/dist/features/tester/seed-data/authored/buying__cart.js +10 -3
  88. package/dist/features/tester/seed-data/authored/buying__image-tile-layout.js +0 -16
  89. package/dist/features/tester/seed-data/authored/buying__my-orders.js +27 -5
  90. package/dist/features/tester/seed-data/authored/buying__offers.js +22 -7
  91. package/dist/features/tester/seed-data/authored/buying__order-detail-actions.js +3 -3
  92. package/dist/features/tester/seed-data/authored/buying__product-detail.js +35 -4
  93. package/dist/features/tester/seed-data/authored/buying__return-request.js +19 -6
  94. package/dist/features/tester/seed-data/authored/buying__reviews-pagination.js +36 -18
  95. package/dist/features/tester/seed-data/authored/buying__reviews.js +16 -1
  96. package/dist/features/tester/seed-data/authored/buying__user-dashboard-navigation.js +2 -1
  97. package/dist/features/tester/seed-data/authored/buying__user-uncovered-pages.js +16 -1
  98. package/dist/features/tester/seed-data/authored/community-support__public-profile.js +3 -2
  99. package/dist/features/tester/seed-data/authored/community-support__support-tickets.js +4 -4
  100. package/dist/features/tester/seed-data/authored/content-discovery__blog.js +19 -2
  101. package/dist/features/tester/seed-data/authored/content-discovery__category-brand-relations.js +4 -4
  102. package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +5 -5
  103. package/dist/features/tester/seed-data/authored/content-discovery__coupons.js +41 -6
  104. package/dist/features/tester/seed-data/authored/content-discovery__event-detail-subroutes.js +63 -8
  105. package/dist/features/tester/seed-data/authored/content-discovery__event-participation.js +29 -29
  106. package/dist/features/tester/seed-data/authored/content-discovery__events.js +2 -2
  107. package/dist/features/tester/seed-data/authored/content-discovery__faq-help.js +23 -1
  108. package/dist/features/tester/seed-data/authored/content-discovery__notifications.js +6 -6
  109. package/dist/features/tester/seed-data/authored/content-discovery__search.js +1 -1
  110. package/dist/features/tester/seed-data/authored/content-discovery__store-reviews-aggregate.js +1 -1
  111. package/dist/features/tester/seed-data/authored/cta-layout__checkout-bottom-bar.js +31 -6
  112. package/dist/features/tester/seed-data/authored/cta-layout__dialog-footers.js +34 -10
  113. package/dist/features/tester/seed-data/authored/cta-layout__editor-action-bar.js +35 -8
  114. package/dist/features/tester/seed-data/authored/cta-layout__navbar-ctas.js +31 -9
  115. package/dist/features/tester/seed-data/authored/cta-layout__product-bottom-bar.js +29 -3
  116. package/dist/features/tester/seed-data/authored/design-ux__back-to-top-button.js +1 -1
  117. package/dist/features/tester/seed-data/authored/design-ux__carousel-arrow-bounds.js +10 -10
  118. package/dist/features/tester/seed-data/authored/design-ux__dashboard-layout.js +3 -3
  119. package/dist/features/tester/seed-data/authored/design-ux__footer-theme.js +2 -2
  120. package/dist/features/tester/seed-data/authored/design-ux__form-validation-errors.js +2 -2
  121. package/dist/features/tester/seed-data/authored/design-ux__general-design.js +4 -4
  122. package/dist/features/tester/seed-data/authored/design-ux__hand-mode-layout.js +1 -1
  123. package/dist/features/tester/seed-data/authored/design-ux__homepage-carousels.js +8 -8
  124. package/dist/features/tester/seed-data/authored/design-ux__status-badge-legibility.js +2 -2
  125. package/dist/features/tester/seed-data/authored/design-ux__sticky-cta-bar.js +2 -2
  126. package/dist/features/tester/seed-data/authored/page-wiring__data-loss.js +39 -16
  127. package/dist/features/tester/seed-data/authored/page-wiring__detail-pages.js +43 -8
  128. package/dist/features/tester/seed-data/authored/page-wiring__drawer-pages.js +40 -14
  129. package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +31 -14
  130. package/dist/features/tester/seed-data/authored/public-pages__auth-error-pages.js +7 -6
  131. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +14 -5
  132. package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.js +3 -3
  133. package/dist/features/tester/seed-data/authored/public-pages__help-how-it-works.js +1 -1
  134. package/dist/features/tester/seed-data/authored/public-pages__help-scams-guides-subpages.js +4 -2
  135. package/dist/features/tester/seed-data/authored/public-pages__legal-policy-pages.js +3 -3
  136. package/dist/features/tester/seed-data/authored/public-pages__newly-wired-browse-indexes.js +5 -5
  137. package/dist/features/tester/seed-data/authored/public-pages__store-tabs.js +5 -5
  138. package/dist/features/tester/seed-data/authored/public-pages__stores-sellers-directories.js +3 -3
  139. package/dist/features/tester/seed-data/authored/search-and-nav__employee-permissions.js +45 -11
  140. package/dist/features/tester/seed-data/authored/search-and-nav__header-search.js +57 -17
  141. package/dist/features/tester/seed-data/authored/search-and-nav__settings-deep-links.js +36 -5
  142. package/dist/features/tester/seed-data/authored/search-and-nav__sidebar-search.js +29 -4
  143. package/dist/features/tester/seed-data/authored/selling__become-seller.js +23 -0
  144. package/dist/features/tester/seed-data/authored/selling__final-sale-authoring.js +14 -13
  145. package/dist/features/tester/seed-data/authored/selling__listing-a-product.js +14 -9
  146. package/dist/features/tester/seed-data/authored/selling__listing-edit-roundtrip.js +34 -7
  147. package/dist/features/tester/seed-data/authored/selling__listing-lifecycle.js +17 -7
  148. package/dist/features/tester/seed-data/authored/selling__listing-type-fields-roundtrip.js +9 -9
  149. package/dist/features/tester/seed-data/authored/selling__media-limits.js +9 -4
  150. package/dist/features/tester/seed-data/authored/selling__product-upload-details.js +3 -3
  151. package/dist/features/tester/seed-data/authored/selling__quick-add-minimum-details.js +3 -3
  152. package/dist/features/tester/seed-data/authored/selling__sectionised-forms.js +25 -1
  153. package/dist/features/tester/seed-data/authored/selling__seller-analytics-payouts.js +27 -6
  154. package/dist/features/tester/seed-data/authored/selling__seller-bids-bundles-filters.js +19 -1
  155. package/dist/features/tester/seed-data/authored/selling__seller-catalog-org.js +63 -22
  156. package/dist/features/tester/seed-data/authored/selling__seller-custom-brands.js +9 -5
  157. package/dist/features/tester/seed-data/authored/selling__seller-guide.js +7 -6
  158. package/dist/features/tester/seed-data/authored/selling__seller-listing-types.js +3 -2
  159. package/dist/features/tester/seed-data/authored/selling__seller-marketing-extras.js +18 -18
  160. package/dist/features/tester/seed-data/authored/selling__seller-ops-comms.js +13 -12
  161. package/dist/features/tester/seed-data/authored/selling__seller-orders-at-scale.js +19 -3
  162. package/dist/features/tester/seed-data/authored/selling__seller-orders.js +22 -4
  163. package/dist/features/tester/seed-data/authored/selling__seller-shipping-payouts-setup.js +20 -8
  164. package/dist/features/tester/seed-data/authored/selling__store-dashboard-navigation.js +4 -4
  165. package/dist/features/tester/seed-data/authored/selling__store-uncovered-pages.js +2 -2
  166. package/dist/features/tester/seed-data/authored/seo__canonical-and-host.js +5 -5
  167. package/dist/features/tester/seed-data/authored/seo__og-images.js +1 -1
  168. package/dist/features/tester/seed-data/authored/seo__page-metadata.js +7 -7
  169. package/dist/features/tester/seed-data/authored/seo__sitemap-and-robots.js +3 -3
  170. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +123 -21
  171. package/dist/features/tester/utils/phases.d.ts +17 -0
  172. package/dist/features/tester/utils/phases.js +29 -2
  173. package/dist/index.d.ts +1 -0
  174. package/dist/index.js +2 -0
  175. package/dist/seo/json-ld.d.ts +23 -0
  176. package/dist/seo/json-ld.js +3 -2
  177. package/dist/server.d.ts +1 -0
  178. package/dist/server.js +2 -0
  179. package/dist/styles.css +1 -1
  180. package/dist/tailwind-utilities.css +1 -1
  181. package/dist/ui/components/PaginatedSelect.js +0 -0
  182. package/package.json +1 -1
@@ -14,6 +14,33 @@
14
14
  * was unbanned a moment ago — the fix is to re-read on navigation, not to widen
15
15
  * the refresh interval.
16
16
  *
17
+ * šŸ›‘ A SOFT BAN DOES NOT BLOCK SIGN-IN (corrected 2026-09-29). Three cases and
18
+ * one catalogue label said it did. It does not, and the code is unambiguous:
19
+ *
20
+ * soft ban POST /api/admin/users/{uid}/soft-ban appends to `user.softBans[]`
21
+ * as `{ action, reason, bannedBy, bannedAt, expiresAt }`, where
22
+ * `action` is one of EIGHT: write_reviews, write_blog_comments,
23
+ * join_events, place_bids, create_listings, send_messages,
24
+ * create_support_tickets, report_scammers. It is a PER-ACTION
25
+ * restriction. Sign-in is untouched.
26
+ * lift DELETE /api/admin/users/{uid}/soft-ban/{action} — one action at a
27
+ * time. `/unban` does NOT clear softBans.
28
+ * hard ban POST …/hard-ban sets `disabled: true` on the Auth record and
29
+ * enqueues the 8-stage `hardBanCascade` job. `/api/auth/login`
30
+ * refuses on `userRecord.disabled` alone — so THIS is the ban that
31
+ * blocks sign-in.
32
+ * unban POST …/unban sets `disabled: false` on Auth and the user doc and
33
+ * clears the hard-ban fields only.
34
+ *
35
+ * So the sign-in case now exercises a HARD ban for the sign-in half and a soft
36
+ * ban for the restriction half, and says plainly that a soft-banned account
37
+ * signing in normally is CORRECT — otherwise the first tester to run it reports
38
+ * a defect that is really the case being wrong.
39
+ *
40
+ * `reason` is `z.string().min(1)`, so a ONE-CHARACTER reason is accepted. The
41
+ * requires-reason case hedged on "if a minimum length applies"; it now states
42
+ * the contract so a one-character reason being accepted is not written up.
43
+ *
17
44
  * @tag domain:tester
18
45
  * @tag layer:seed
19
46
  * @tag pattern:none
@@ -29,49 +56,58 @@ export const authored = {
29
56
  steps: [
30
57
  SIGN_IN_ADMIN,
31
58
  "Open /admin/users and find the account rohit.collect@gmail.com.",
32
- "Soft-ban the account with the reason 'QA ban probe — checklist case, will be lifted.'",
33
- "Sign out.",
34
- "Try to sign in as rohit.collect@gmail.com / TempPass123!.",
35
- "Read what is shown.",
36
- "Sign back in as the admin and lift the ban.",
59
+ "Soft-ban it from place_bids with the reason 'QA ban probe — checklist case, will be lifted.'",
60
+ "Sign out and sign in as rohit.collect@gmail.com / TempPass123!. This must SUCCEED — a soft ban restricts one action, it does not block sign-in.",
61
+ "Open any live auction and try to place a bid. Read the refusal and whether it quotes the reason the admin typed.",
62
+ "Sign back in as the admin and lift that soft ban.",
63
+ "Now HARD-ban the same account with the reason 'QA sign-in block probe — checklist case, will be lifted.'",
64
+ "Sign out and try to sign in as rohit.collect@gmail.com / TempPass123!. Read exactly what is shown.",
65
+ "Sign back in as the admin and unban the account, then confirm it signs in again.",
37
66
  ],
38
- inputs: { reason: "QA ban probe — checklist case, will be lifted." },
39
- expectedBehaviour: "A soft-banned account cannot sign in and is told why. A refusal with a generic credentials error is worse than a block — the user believes their password is wrong and resets it, repeatedly, against an account that would refuse them either way.",
40
- expectedUiState: "The sign-in is refused with a message naming the ban, not an invalid-credentials error. The reason the admin typed is visible to the user.",
41
- endResult: "The ban is lifted and the account signs in again.",
67
+ inputs: {
68
+ reason: "QA ban probe — checklist case, will be lifted.",
69
+ softBanAction: "place_bids",
70
+ hardBanReason: "QA sign-in block probe — checklist case, will be lifted.",
71
+ },
72
+ expectedBehaviour: "The two bans do different things and both must say why. A soft ban blocks ONE action and leaves sign-in alone; a hard ban disables the account outright and sign-in is refused. A refusal with a generic credentials error is worse than a block — the user believes their password is wrong and resets it, repeatedly, against an account that would refuse them either way.",
73
+ expectedUiState: "The soft-banned account SIGNS IN NORMALLY — that is correct and is not a finding — and is refused only at the bid, with the admin's reason quoted. The hard-banned account is refused at sign-in with a message naming the disabled account, not an invalid-credentials error. Record the exact wording of both refusals.",
74
+ expectedData: { softBannedCanSignIn: true, hardBannedCanSignIn: false },
75
+ endResult: "The soft ban is lifted, the hard ban is lifted, and the account signs in and can bid again. šŸ›‘ Both must be lifted separately — /unban clears the hard-ban fields only and leaves softBans untouched.",
42
76
  },
43
77
  "checklist-admin-bans-and-trust-soft-ban-requires-reason": {
44
78
  roles: ["admin"],
45
79
  startPage: "/admin/users",
46
80
  steps: [
47
81
  SIGN_IN_ADMIN,
48
- "Open /admin/users and start banning the account rohit.collect@gmail.com.",
82
+ "Open /admin/users and start a soft ban on the account rohit.collect@gmail.com.",
49
83
  "Leave the reason field empty and submit.",
50
- "Read what happens.",
51
- "Type a one-character reason and submit.",
52
- "Read whether that is accepted.",
53
- "Cancel without banning.",
84
+ "Read what happens and WHERE the error appears — on the field, or as a banner.",
85
+ "Type a single character as the reason and submit.",
86
+ "Read whether that is accepted — the schema is a minimum of one character, so it should be.",
87
+ "If it was accepted, lift that soft ban immediately.",
54
88
  ],
89
+ inputs: { minimumReasonLength: 1 },
55
90
  expectedBehaviour: "A ban carries a reason, enforced rather than requested. The reason is what the banned user reads and what the audit log records — a ban with none leaves both the user and the next admin with an action and no explanation.",
56
- expectedUiState: "The empty submission is refused with an inline error on the reason field, not a banner. A one-character reason is refused too if a minimum length applies.",
57
- endResult: "Nobody is banned.",
91
+ expectedUiState: "The empty submission is refused with an inline error ON THE REASON FIELD, not a banner. A one-character reason IS accepted — the contract is `z.string().min(1)` — so do not record that as a finding; record instead whether a reason that short is useful to the person who reads it. What would be a finding is an empty reason being accepted, or the refusal appearing only as a banner with nothing marked on the field.",
92
+ expectedData: { emptyReasonRejected: true, oneCharReasonAccepted: true },
93
+ endResult: "Nobody is left banned — if the one-character ban applied, it is lifted.",
58
94
  },
59
95
  "checklist-admin-bans-and-trust-unban-restores-access": {
60
96
  roles: ["admin", "buyer"],
61
97
  startPage: "/admin/users",
62
98
  steps: [
63
99
  SIGN_IN_ADMIN,
64
- "Soft-ban the account rohit.collect@gmail.com with the reason 'QA unban probe — checklist case.'",
65
- "In a second browser, sign in as rohit.collect@gmail.com / TempPass123! and confirm the block.",
66
- "As the admin, lift the ban.",
67
- "In the second browser, WITHOUT signing out or reloading, navigate to another page.",
68
- "Read whether access is restored.",
69
- "Note how long it took, if it was not immediate.",
100
+ "In a second browser, sign in as rohit.collect@gmail.com / TempPass123! and leave that session open on a signed-in page.",
101
+ "As the admin, soft-ban that account from place_bids with the reason 'QA unban probe — checklist case.'",
102
+ "In the second browser, WITHOUT signing out or reloading, open a live auction and confirm bidding is refused.",
103
+ "As the admin, lift that soft ban.",
104
+ "In the second browser, WITHOUT signing out or reloading, navigate to another page and try to bid again.",
105
+ "Read whether access is restored, and note how long it took if it was not immediate.",
70
106
  ],
71
- inputs: { reason: "QA unban probe — checklist case." },
107
+ inputs: { reason: "QA unban probe — checklist case.", softBanAction: "place_bids" },
72
108
  expectedBehaviour: "A lifted ban takes effect on the next navigation. Role and status fields on a session refresh only periodically, so a guard reading that cached snapshot keeps blocking for minutes after the ban is gone — which reads to the user as the unban not having worked. The guard re-reads on navigation into a gated area for exactly this reason.",
73
- expectedUiState: "Access is restored on the next navigation, without a sign-out or a hard reload. A block persisting for minutes is the finding, recorded with how long.",
74
- endResult: "The account is unbanned and working.",
109
+ expectedUiState: "Bidding is restored on the next navigation, without a sign-out or a hard reload. A block persisting for minutes is the finding, recorded with how long — that is the cached-session-snapshot failure this case exists for, and it reads to a user as the unban not having worked.",
110
+ endResult: "The soft ban is lifted and the account can bid again. Note this case uses a SOFT ban deliberately: a hard ban ends the session outright, so there would be no live session left to observe the restoration on.",
75
111
  },
76
112
  "checklist-admin-bans-and-trust-hard-ban-cascade-runs": {
77
113
  roles: ["admin"],
@@ -79,16 +115,19 @@ export const authored = {
79
115
  steps: [
80
116
  SIGN_IN_ADMIN,
81
117
  "Open /admin/users and hard-ban the account arjun.builder@gmail.com with the reason 'QA hard-ban cascade probe — checklist case.'",
82
- "Read what the interface says immediately after submitting.",
83
- "Check it reports that work was queued rather than claiming completion.",
118
+ "Read what the interface says IMMEDIATELY after submitting — the route returns a job id and a token, so it should report started work rather than a finished cascade.",
84
119
  "Wait for the completion signal and read what it reports.",
85
- "Open the account and read its state.",
86
- "Lift the ban afterwards.",
120
+ "Open the account and read its state — it should now be disabled.",
121
+ "Unban the account and confirm it is enabled again.",
87
122
  ],
88
- inputs: { reason: "QA hard-ban cascade probe — checklist case." },
123
+ inputs: {
124
+ reason: "QA hard-ban cascade probe — checklist case.",
125
+ jobType: "hardBanCascade",
126
+ cascadeStages: 8,
127
+ },
89
128
  expectedBehaviour: "A hard ban runs a multi-stage cascade that exceeds a single request's time budget, so it is queued as a background job and its completion is reported back. A screen that claims success at submit time is claiming an outcome it cannot know — the job may still fail.",
90
- expectedUiState: "The submit reports queued work, a completion signal follows, and the account's state reflects the finished cascade. An immediate unqualified success message is the finding.",
91
- endResult: "The ban is lifted.",
129
+ expectedUiState: "The submit reports started or queued work, a completion signal follows separately, and only then does the account's state reflect the finished cascade. An immediate unqualified 'user banned' with no later signal is the finding — the eight-stage cascade runs in a background job and the request cannot know its outcome.",
130
+ endResult: "The ban is lifted and the account is enabled again. A hard ban left in place outlives every reseed — users is PRESERVE tier — and silently breaks whichever other page signs in as this persona.",
92
131
  },
93
132
  "checklist-admin-bans-and-trust-hard-ban-recorded-in-audit-log": {
94
133
  roles: ["admin"],
@@ -9,8 +9,42 @@
9
9
  * šŸ›‘ A FAQ'S ANSWER IS NOT A STRING. The create path wraps a flat answer into a
10
10
  * text-plus-format object and writes the slug to a nested path; an update path
11
11
  * that spreads the body straight through writes a bare string into a field every
12
- * reader expects to be an object. That transform asymmetry is what the FAQ case
13
- * below is really testing, and it only shows after a reload.
12
+ * reader expects to be an object.
13
+ *
14
+ * VERIFIED FIXED 2026-09-29 before asserting anything about it: the PATCH at
15
+ * `src/app/api/admin/faqs/[id]/route.ts:53-57` destructures `answer` and `slug`
16
+ * out and applies the same transform as POST. So that half of the FAQ case
17
+ * should PASS, and a pass is the expected result rather than a surprise. It is
18
+ * kept because the failure is invisible in the admin form and only shows on the
19
+ * public page — exactly the kind that returns.
20
+ *
21
+ * šŸ›‘ THE FAQ CATEGORY PICKER OFFERS VALUES `FAQCategory` DOES NOT HAVE.
22
+ *
23
+ * `AdminFaqEditorView`'s CATEGORY_OPTIONS is shipping / returns / payments /
24
+ * auctions / pre-orders / general. The real union is orders_payment /
25
+ * shipping_delivery / returns_refunds / product_information / account_security /
26
+ * technical_support / general / scam_awareness. **Only `general` overlaps.**
27
+ * The API types category as a bare `z.string().min(1)` and then casts
28
+ * `as FAQCategory`, with a comment acknowledging it, so the wrong value is
29
+ * persisted unvalidated — and `FAQPageContent` counts a FAQ only
30
+ * `if (faq.category in categoryCounts)`, so an admin-created FAQ is in no
31
+ * sidebar bucket and no category filter ever returns it. All 63 seeded FAQs use
32
+ * the real values, which is why this has never shown up. Root Cause #33/#34's
33
+ * shape. Recorded in OUTOFSCOPE; the case below now expects it.
34
+ *
35
+ * (CLAUDE.md's Seed Data Reference lists FAQ categories as
36
+ * "Shipping/Returns/Payments/Auctions/Pre-orders" — it is describing this
37
+ * picker, not the schema, and is wrong about the data.)
38
+ *
39
+ * šŸ›‘ THE BLOG SLUG IS EDITABLE, and this file used to claim the opposite.
40
+ * `updateBlogPostSchema` accepts `slug` and the editor's Content section has a
41
+ * 'Slug' field. What is true is narrower: the slug is derived from the title at
42
+ * creation and is never RECOMPUTED when the title changes. Those are different
43
+ * claims and only the second one holds.
44
+ *
45
+ * Blog vocabulary, all closed enums: category is news | tips | guides | updates
46
+ * | community; status is draft | published | archived. Editor sections are
47
+ * Content, Media, 'SEO & Tags' and Publish.
14
48
  *
15
49
  * @tag domain:tester
16
50
  * @tag layer:seed
@@ -25,48 +59,62 @@ export const authored = {
25
59
  startPage: "/admin/blog",
26
60
  steps: [
27
61
  "Sign in as admin@letitrip.in / TempPass123!.",
28
- "Open the admin blog list and create a post titled 'QA Blog create-edit-publish' with an excerpt, a category, two tags and body content.",
29
- "Save it as a DRAFT and read the status shown.",
30
- "Open /blog in a private window and check the draft is NOT listed.",
31
- "Return to the admin editor, change the body, publish, and RELOAD the editor.",
62
+ "Open /admin/blog/new and create a post titled 'QA Blog create-edit-publish' with an excerpt, category Guides, tags 'qa' and 'checklist', and body content in the rich-text editor. Write down the Slug the Content section shows.",
63
+ "Save it as a DRAFT and read the status shown in the Publish section.",
64
+ "Sign out and open /blog, confirming the draft is NOT listed and its slug URL does not render it.",
65
+ "Sign back in, open the post from /admin/blog, change ONLY the body, set status to Published, save, and RELOAD the editor.",
32
66
  "Read every field to confirm only the body changed and the status is published.",
33
- "Open /blog in the private window and open the post.",
34
- "Delete the post and confirm it disappears from both the admin list and /blog.",
67
+ "Change ONLY the Title to 'QA Blog renamed', save, RELOAD, and read the Slug field: it must still be the one written down in step 2.",
68
+ "Sign out, open /blog and open the post at that original slug.",
69
+ "Sign back in, delete the post, and confirm it disappears from /admin/blog and from /blog.",
35
70
  ],
36
- inputs: { title: "QA Blog create-edit-publish" },
37
- expectedBehaviour: "Draft and published are genuinely different: a draft is invisible publicly. The slug is derived from the title at creation and is then immutable even though the title stays editable — that is the deliberate convention across this codebase, because recomputing it would break existing links.",
38
- expectedUiState: "The draft does not appear on /blog. After publishing it does, with the edited body. The editor's other fields are unchanged after the edit. The URL keeps its original slug after any title change.",
39
- endResult: "The post is deleted from both surfaces. A draft visible publicly is the most serious failure here.",
71
+ inputs: {
72
+ title: "QA Blog create-edit-publish",
73
+ titleAfter: "QA Blog renamed",
74
+ category: "Guides",
75
+ tags: "qa, checklist",
76
+ },
77
+ expectedBehaviour: "Draft and published are genuinely different: a draft is invisible publicly. The slug is derived from the title at creation and is never RECOMPUTED when the title changes, because recomputing it would break every existing link — it stays directly editable in the Slug field, which is the deliberate escape hatch rather than an oversight.",
78
+ expectedUiState: "The draft appears nowhere on /blog and its URL does not render it. After publishing it does, carrying the edited body. After the title change every other field — including Slug — is untouched, and the post is still reachable at the original slug.",
79
+ expectedData: { statusAfterPublish: "published", slugUnchangedAfterRename: true },
80
+ endResult: "The post is deleted from /admin/blog and /blog. A draft visible publicly is the most serious failure here; a slug that silently followed the title is the second.",
40
81
  },
41
82
  "checklist-admin-blog-faqs-blog-media-step-save": {
42
83
  roles: ["admin", "guest"],
43
84
  startPage: "/admin/blog",
44
85
  steps: [
45
86
  "Sign in as admin@letitrip.in / TempPass123!.",
46
- "Open the admin blog editor for an existing published post and note its current cover image.",
47
- "Upload public/test-media/sample-image.png as the cover image.",
48
- "Insert a second image into the body content itself.",
49
- "Save and RELOAD the editor, reading both the cover and the body.",
50
- "Open the post on /blog in a private window and read both images.",
51
- "Restore the original cover image and remove the inserted body image.",
87
+ "Open /admin/blog, open blog-spot-genuine-takara-tomy-beyblade for editing, and write down its current Cover Image and YouTube Video ID from the Media section (the id may be empty — no seeded post sets one).",
88
+ "Upload /test-media/sample-image.png as the Cover Image.",
89
+ "Set the YouTube Video ID to dQw4w9WgXcQ.",
90
+ "In the Content section's rich-text editor, insert /test-media/sample-image-2.png into the body itself.",
91
+ "Save, then RELOAD the editor and read the cover, the YouTube id and the body image.",
92
+ "Sign out, open the post on /blog, and read whether the cover, the video embed and the inline image all render.",
93
+ "Sign back in and restore the Cover Image and YouTube Video ID written down in step 1, and remove the inserted body image.",
52
94
  ],
53
- inputs: { coverImage: "public/test-media/sample-image.png" },
54
- expectedBehaviour: "The cover image and inline body images both persist and both render publicly. A blog post has NO gallery field — extra images live inside the body HTML — so a case looking for a separate image array is looking for something the document has never had. Newly uploaded media must also be finalised on update, not only on create, or the file is orphaned in temporary storage.",
55
- expectedUiState: "After the reload the cover and the inline image are both present in the editor, and both render on the public post. An image that previews during editing and is missing after the reload was never finalised.",
56
- endResult: "The post is restored to its original media. Record any image that vanished on reload — that is the orphaned-upload failure.",
95
+ inputs: {
96
+ coverImage: "/test-media/sample-image.png",
97
+ bodyImage: "/test-media/sample-image-2.png",
98
+ youtubeId: "dQw4w9WgXcQ",
99
+ post: "blog-spot-genuine-takara-tomy-beyblade",
100
+ },
101
+ expectedBehaviour: "The cover image, the YouTube id and inline body images all persist and all render publicly. Extra images belong INSIDE the body HTML: `BlogPostDocument` does declare `contentImages` and `additionalImages` arrays and the PATCH accepts them, but no renderer anywhere reads either one, no mounted editor exposes them (only `BlogPostForm`, which has no consumer), and no seeded post sets them — so a separate image array is not a path to anything visible. Newly uploaded media must also be finalised on update, not only on create, or the file is orphaned in temporary storage.",
102
+ expectedUiState: "After the reload the cover, the YouTube id and the inline image are all present in the editor, and all three render on the public post. An image that previews while editing and is gone after the reload was never finalised — that is the orphaned-upload failure and is the thing this case is for.",
103
+ expectedData: { coverPersisted: true, youtubeIdPersisted: true, bodyImagePersisted: true },
104
+ endResult: "The post carries its original cover and YouTube id again and the inserted body image is gone. Record any image that vanished on reload, and whether it vanished in the editor, on the public post, or both.",
57
105
  },
58
106
  "checklist-admin-blog-faqs-faq-create-edit-category": {
59
107
  roles: ["admin", "guest"],
60
108
  startPage: "/admin/faqs",
61
109
  steps: [
62
110
  "Sign in as admin@letitrip.in / TempPass123!.",
63
- "Open the admin FAQs list and create one: question 'QA FAQ create-edit-category?', answer 'Created by the tester checklist.', category Shipping.",
64
- "Save, RELOAD, and read the answer as it is displayed.",
65
- "Open /faqs in a private window and find the question under Shipping.",
66
- "Return to the admin editor, change ONLY the answer to 'Edited by the tester checklist.', and save.",
111
+ "Open /admin/faqs/new and create one: question 'QA FAQ create-edit-category?', answer 'Created by the tester checklist.', and pick Shipping from the Category select.",
112
+ "Save, RELOAD the editor, and read the answer as it is displayed.",
113
+ "Sign out, open /faqs, and look for the question — first under the category sidebar, then under All.",
114
+ "Sign back in, change ONLY the answer to 'Edited by the tester checklist.', and save.",
67
115
  "RELOAD the editor and read the answer, then open /faqs again and read it there.",
68
- "Change the category to Payments, save, reload, and check it moved category on /faqs.",
69
- "Delete the FAQ and confirm it disappears from both surfaces.",
116
+ "Change the Category to Payments, save, reload, and check whether it moved category on /faqs.",
117
+ "Delete the FAQ and confirm it disappears from /admin/faqs and /faqs.",
70
118
  ],
71
119
  inputs: {
72
120
  question: "QA FAQ create-edit-category?",
@@ -75,8 +123,9 @@ export const authored = {
75
123
  categoryBefore: "Shipping",
76
124
  categoryAfter: "Payments",
77
125
  },
78
- expectedBehaviour: "The EDIT is the case. Creating a FAQ wraps the flat answer into an object and writes the slug to a nested path; an update that spreads the submitted body straight into storage writes a bare string into the field every reader expects to be an object. The admin form may still render it — the public page is where the wrong shape surfaces.",
79
- expectedUiState: "After the edit and reload the answer reads as prose in BOTH the admin editor and on /faqs. What must not appear is raw markup, an object rendered as text, or a blank answer on the public page while the admin form looks fine.",
80
- endResult: "The FAQ is deleted from both surfaces. Checking /faqs after the edit — not only the admin form — is what makes this case worth running.",
126
+ expectedBehaviour: "Two independent things, and they have different answers today. (1) The ANSWER round-trip: creating a FAQ wraps the flat answer into a text-plus-format object, and the update must apply the same transform rather than spreading a bare string into a field every reader expects to be an object. (2) The CATEGORY: whatever the picker offers must be a value the rest of the product can represent, or the FAQ lands in a category nothing renders.",
127
+ expectedUiState: "The answer half should PASS — verified in source, the PATCH applies the same transform as create, so the answer reads as prose in both the admin editor and on /faqs, with no raw markup and no blank public answer.\n\nšŸ›‘ EXPECT THE CATEGORY HALF TO FAIL, AND RECORD IT AS A FAILURE. The picker's 'Shipping' writes the string 'shipping', and FAQCategory has no such value — it has 'shipping_delivery'. The API types category as a bare z.string() and casts it, so it is stored unvalidated, and /faqs counts a FAQ only if its category is a known one. So expect the question to be absent from every category in the sidebar while still appearing under All, and the move to Payments to change nothing visible. Say which categories you tried.",
128
+ expectedData: { answerRendersAsProse: true, appearsUnderAnyCategory: false },
129
+ endResult: "The FAQ is deleted from both surfaces. Keep the two halves apart in the comment: one mechanism works and one writes a value no reader understands.",
81
130
  },
82
131
  };
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * checklist-admin-bug-hunter-rewards-demo-fixture v1, isActive:false,
16
16
  * bugConfirmed, credited to
17
- * "Mock User 18"
17
+ * "Mock User 3"
18
18
  * checklist-admin-bug-hunter-rewards-demo-fixture-v2 v2, isActive:true
19
19
  *
20
20
  * That pair exists precisely so this flow can be read without spending a real
@@ -22,6 +22,24 @@
22
22
  * confirming a bug — it does so against v2, which is the fixture reopened for
23
23
  * exactly that purpose.
24
24
  *
25
+ * šŸ›‘ THE CREDIT IS "Mock User 3", NOT "Mock User 18" (fixed 2026-09-29). The seed
26
+ * sets `bugHunterId: "user-yugi-muto"` / `bugHunterName: "Mock User 3"`, and the
27
+ * responses seed uses the same pair. The idempotency case asserted
28
+ * byte-identity against "Mock User 18", so it would have failed on a correct
29
+ * system — the worst shape a case can have, because the failure looks like a
30
+ * product defect.
31
+ *
32
+ * šŸ›‘ v1 CANNOT BE REOPENED AGAIN. It already carries `supersededByItemId`, and
33
+ * the route returns 409 "This case has already been reopened" on exactly that.
34
+ * The reopen case pointed at v1 and expected a new version to appear; it now
35
+ * asserts the 409 on v1 and performs the real reopen on **v2**, which the
36
+ * confirm-bug case above it has just made bug-confirmed. That is a deliberate
37
+ * dependency between two cases in one batch, which is why they run in order.
38
+ *
39
+ * Both response rows ARE seeded (`tester-responses-seed-data.ts`), the v2 one
40
+ * deliberately left `status: "new"` and unconfirmed so confirm-bug has
41
+ * something to do. So no case here has to file a report first.
42
+ *
25
43
  * @tag domain:tester
26
44
  * @tag layer:seed
27
45
  * @tag pattern:none
@@ -39,15 +57,19 @@ export const authored = {
39
57
  "Open /admin/tester-feedback and select the Main Issues tab.",
40
58
  "Find the row for the case 'Demo fixture — reported bug, already confirmed and reopened (v2, active)' and read the tester name on it.",
41
59
  "Click 'Mark as Bug' on that row.",
42
- "Read the confirmation dialog in full before accepting, then accept it.",
60
+ "Read the confirmation dialog before accepting: it should be titled 'Confirm this bug?' and say the credit is awarded and the case disabled for other testers. Then accept it.",
43
61
  "Read the badge that appears on the row afterwards.",
44
62
  "Open /admin/tester-checklist, set the Status filter to 'All', and read the Status of that same case.",
45
63
  ],
46
- inputs: { caseId: "checklist-admin-bug-hunter-rewards-demo-fixture-v2" },
64
+ inputs: {
65
+ caseId: "checklist-admin-bug-hunter-rewards-demo-fixture-v2",
66
+ dialogTitle: "Confirm this bug?",
67
+ creditedTo: "Mock User 3",
68
+ },
47
69
  expectedBehaviour: "Confirming a bug does two things at once and both are irreversible from this screen: it credits exactly one tester, and it takes the case out of circulation so no second tester can answer it. A confirmation dialog must stand in front of it — an accidental click here spends a credit on the wrong person and silently shrinks the catalogue.",
48
- expectedUiState: "A dialog appears BEFORE anything commits. After accepting, the row shows a confirmed-bug badge naming the crediting tester — the same name that was on the row beforehand, not a different one. In the catalog the case's Status reads as bug-confirmed rather than Active.",
70
+ expectedUiState: "A dialog titled 'Confirm this bug?' appears BEFORE anything commits. After accepting, the row shows a confirmed-bug badge naming the crediting tester — the same name that was on the row beforehand, 'Mock User 3', not a different one. In the catalog the case's Status reads 'Bug Confirmed' rather than 'Active'.",
49
71
  expectedData: { creditedTesterUnchanged: true },
50
- endResult: "Survives a reload: the badge and the credited name are still there, and the case is still out of the Active view.",
72
+ endResult: "Survives a reload: the badge and the credited name are still there, and the case is still out of the Active view. šŸ›‘ Leave v2 confirmed — the reopen case below needs a bug-confirmed case that has NOT already been superseded, and v2 is the only one a fresh seed can provide.",
51
73
  },
52
74
  "checklist-admin-bug-hunter-rewards-confirm-bug-idempotent": {
53
75
  roles: ["admin"],
@@ -55,35 +77,50 @@ export const authored = {
55
77
  steps: [
56
78
  SIGN_IN_ADMIN,
57
79
  "Open /admin/tester-feedback and select the All Submissions tab.",
58
- "Find a row whose case is ALREADY bug-confirmed — the demo fixture 'reported bug, already confirmed and reopened (v1, disabled)' is credited to Mock User 18.",
80
+ "Find a row whose case is ALREADY bug-confirmed — the demo fixture 'reported bug, already confirmed and reopened (v1, disabled)' is credited to Mock User 3.",
59
81
  "Read the tester name currently credited on it.",
60
82
  "Attempt 'Mark as Bug' on that row.",
61
83
  "Read whatever the screen says in response.",
62
84
  "Read the credited tester name again and compare it against the one noted earlier.",
63
85
  ],
64
- inputs: { caseId: "checklist-admin-bug-hunter-rewards-demo-fixture", creditedTo: "Mock User 18" },
86
+ inputs: {
87
+ caseId: "checklist-admin-bug-hunter-rewards-demo-fixture",
88
+ creditedTo: "Mock User 3",
89
+ expectedRefusal: "This case has already been confirmed as a bug",
90
+ },
65
91
  expectedBehaviour: "A second confirmation on an already-confirmed case is REFUSED, and refused loudly. The failure mode this guards is silent re-crediting: two testers report the same case, an admin confirms twice, and the second confirmation quietly moves the credit off the person who actually found it. Nothing about that is visible unless the screen says so.",
66
- expectedUiState: "Either the action is absent/disabled on an already-confirmed row, or it is refused with a message naming the existing credit. What must NOT happen is a success message. The credited name is byte-identical to the one read at the start.",
67
- expectedData: { creditedTo: "Mock User 18" },
68
- endResult: "Reload the page: the credit still reads Mock User 18, and the case has not gained a second confirmation.",
92
+ expectedUiState: "Either the action is absent/disabled on an already-confirmed row, or it is refused — the server answers 409 'This case has already been confirmed as a bug'. What must NOT happen is a success message. The credited name is byte-identical to the one read at the start.",
93
+ expectedData: { creditedTo: "Mock User 3" },
94
+ endResult: "Reload the page: the credit still reads Mock User 3, and the case has not gained a second confirmation.",
69
95
  },
70
96
  "checklist-admin-bug-hunter-rewards-reopen-case": {
71
97
  roles: ["admin"],
72
98
  startPage: "/admin/tester-checklist",
73
99
  steps: [
74
100
  SIGN_IN_ADMIN,
75
- "Open /admin/tester-checklist and set the Status filter to 'Inactive' (or set Bug status to 'Bug Confirmed').",
101
+ "Open /admin/tester-checklist and set 'Bug status' to 'Bug Confirmed'.",
76
102
  "Find 'Demo fixture — reported bug, already confirmed and reopened (v1, disabled)' and read its Status and its bug-hunter credit.",
77
- "Use its 'Reopen as New Test Case' row action.",
78
- "Read the new case that appears — its label, its Status, and its version.",
79
- "Go back to the v1 case and read its Status and credit again.",
80
- "Open the Tester Hub as the tester and search for the new case.",
103
+ "Use its 'Reopen as New Test Case' row action and read what happens — v1 has already been superseded once, so expect a refusal rather than a new case.",
104
+ "Now find the v2 fixture, which the confirm-bug case above has just made bug-confirmed, and use 'Reopen as New Test Case' on it.",
105
+ "Read the new case that appears — its label, its Status, its version, and whether it carries any bug-hunter credit.",
106
+ "Go back to the v1 and v2 cases and read their Status and credit again.",
107
+ "Open /tester and search for the new case to confirm it is answerable.",
81
108
  ],
82
- inputs: { v1Id: "checklist-admin-bug-hunter-rewards-demo-fixture" },
83
- expectedBehaviour: "Reopening creates a NEW version rather than resurrecting the old one. The original must keep both its disabled state and its bug-hunter credit — that credit is a record of who found the defect, and a retest of the same behaviour does not undo the finding. Mutating v1 in place would erase the history the whole feature exists to keep.",
84
- expectedUiState: "Two rows exist afterwards: the old one still disabled and still credited, and a new active one answerable from the Tester Hub. The new case does NOT carry the old case's bug-hunter credit.",
85
- expectedData: { originalStaysDisabled: true, originalKeepsCredit: true },
86
- endResult: "Survives a reload: v1 disabled with its credit, the reopened version active and answerable.",
109
+ inputs: {
110
+ v1Id: "checklist-admin-bug-hunter-rewards-demo-fixture",
111
+ v2Id: "checklist-admin-bug-hunter-rewards-demo-fixture-v2",
112
+ newIdPattern: "{v2Id}-v3",
113
+ v1Refusal: "This case has already been reopened",
114
+ },
115
+ expectedBehaviour: "Reopening creates a NEW version rather than resurrecting the old one, and a case can only be reopened ONCE — a second reopen would fork the chain into two live retests of the same finding. The original must keep both its disabled state and its bug-hunter credit: that credit records who found the defect, and a retest of the same behaviour does not undo the finding. Mutating the old row in place would erase the history the whole feature exists to keep.",
116
+ expectedUiState: "v1 refuses the reopen — 409 'This case has already been reopened' — and is unchanged by the attempt. v2 reopens into a new row whose id ends '-v3' and whose version reads 3, Active and answerable from /tester, carrying NO bug-hunter credit of its own. Both older rows stay disabled and stay credited. A new case that inherited the credit, or an old row that lost it, is the failure.",
117
+ expectedData: {
118
+ originalStaysDisabled: true,
119
+ originalKeepsCredit: true,
120
+ newVersion: 3,
121
+ newCaseHasCredit: false,
122
+ },
123
+ endResult: "Survives a reload: v1 and v2 disabled with their credits, the reopened v3 active and answerable.",
87
124
  },
88
125
  "checklist-admin-bug-hunter-rewards-catalog-default-active-filter": {
89
126
  roles: ["admin"],
@@ -91,15 +128,15 @@ export const authored = {
91
128
  steps: [
92
129
  SIGN_IN_ADMIN,
93
130
  "Open /admin/tester-checklist without touching any filter and read the total count shown.",
94
- "Search for 'Demo fixture — reported bug' and read how many rows match.",
131
+ "Search for 'Demo fixture' and write down every matching row with its Status.",
95
132
  "Switch the Status filter to 'All' and read the total count and the matching rows again.",
96
- "Switch the Status filter to 'Inactive' and read which of the two demo fixtures appears.",
133
+ "Switch the Status filter to 'Inactive' and read which of the demo fixtures appear.",
97
134
  "Set Status back to its default and set 'Bug status' to 'Bug Confirmed'.",
98
- "Read which rows appear under that filter.",
135
+ "Read which rows appear under that filter, and confirm every one of them was absent from the default view.",
99
136
  ],
100
- inputs: { search: "Demo fixture — reported bug" },
137
+ inputs: { search: "Demo fixture" },
101
138
  expectedBehaviour: "The default view is the answerable catalogue, not everything ever written. A bug-confirmed case is deliberately out of circulation, so showing it by default would invite an admin to treat a closed finding as open work. Switching the filter must reveal it — hidden is not the same as gone.",
102
- expectedUiState: "On the default view only the v2 (active) demo fixture matches the search. Under Status 'All' both appear and the total is higher. Under 'Inactive' the v1 disabled one appears. Under Bug status 'Bug Confirmed' the v1 one appears. The counts move in the direction the filter implies.",
139
+ expectedUiState: "Every bug-confirmed row is absent from the default view and present under Status 'All', under 'Inactive', and under Bug status 'Bug Confirmed'. The totals move in the direction each filter implies. v1 is bug-confirmed on any fresh seed, so it is the row to anchor on.\n\nšŸ›‘ Do NOT assert an absolute count of demo fixtures. The three cases above this one deliberately confirm v2 and reopen it as v3, so how many demo rows exist depends on whether they ran. The assertion is the FILTER SEMANTICS — hidden is not gone — not the arithmetic. If v2 and v3 exist, they must obey the same rule, which makes the case stronger rather than fragile.",
103
140
  expectedData: { defaultViewHidesBugConfirmed: true },
104
141
  endResult: "Read-only — no case is modified.",
105
142
  },