@mohasinac/appkit 4.39.1 → 4.40.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/jobs/core/countersReconcile.js +21 -0
- package/dist/_internal/server/jobs/core/onProductWrite.js +35 -1
- package/dist/features/categories/components/CategoryDetailPageView.js +17 -5
- package/dist/features/categories/repository/categories.repository.d.ts +65 -1
- package/dist/features/categories/repository/categories.repository.js +264 -78
- package/dist/features/categories/types/index.d.ts +2 -2
- package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +145 -0
- package/dist/features/tester/seed-data/authored/admin__firebase-function-effects.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__firebase-function-effects.js +234 -0
- package/dist/features/tester/seed-data/authored/buying__buying-checkout.js +32 -0
- package/dist/features/tester/seed-data/authored/buying__cart.js +60 -0
- package/dist/features/tester/seed-data/authored/buying__order-detail-actions.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__order-detail-actions.js +138 -0
- package/dist/features/tester/seed-data/authored/buying__wishlist-history.js +30 -0
- package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +145 -0
- package/dist/features/tester/seed-data/authored/content-discovery__event-detail-subroutes.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__event-detail-subroutes.js +154 -0
- package/dist/features/tester/seed-data/authored/content-discovery__store-reviews-aggregate.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__store-reviews-aggregate.js +88 -0
- package/dist/features/tester/seed-data/authored/index.js +22 -0
- package/dist/features/tester/seed-data/authored/public-pages__help-how-it-works.js +134 -7
- package/dist/features/tester/seed-data/authored/public-pages__help-scams-guides-subpages.js +89 -0
- package/dist/features/tester/seed-data/authored/public-pages__newly-wired-browse-indexes.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/public-pages__newly-wired-browse-indexes.js +141 -0
- package/dist/features/tester/seed-data/authored/public-pages__store-tabs.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/public-pages__store-tabs.js +115 -0
- package/dist/features/tester/seed-data/authored/selling__digital-content-delivery.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__digital-content-delivery.js +148 -0
- package/dist/features/tester/seed-data/authored/selling__listing-edit-roundtrip.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__listing-edit-roundtrip.js +118 -0
- package/dist/features/tester/seed-data/authored/selling__seller-orders-at-scale.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__seller-orders-at-scale.js +69 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +471 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/tabs-overflow.js +30 -7
- package/package.json +1 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Authored six-part procedures for admin/admin-detail-round-trips.
|
|
3
|
+
* WHAT: 7 cases, keyed by full checklist id.
|
|
4
|
+
*
|
|
5
|
+
* 🛑 THERE ARE 33 `[id]` DIRECTORIES UNDER `admin/` AND ALMOST ALL SAT AT ZERO.
|
|
6
|
+
*
|
|
7
|
+
* Every case here is a ROUND TRIP: open, change ONE field, save, RELOAD, then
|
|
8
|
+
* check the fields you did NOT touch. That shape is what catches an editor
|
|
9
|
+
* seeded from the wrong object — a form that opens blank and then saves its own
|
|
10
|
+
* emptiness passes any case that merely asks whether the page loads. It is
|
|
11
|
+
* exactly how the 2026-09-14 data-loss bug survived (Root Cause #98), and it is
|
|
12
|
+
* also Root Cause #38's shape, where saving one field silently stripped tester
|
|
13
|
+
* flags and un-verified a store because the list row never carried them.
|
|
14
|
+
*
|
|
15
|
+
* @tag domain:tester
|
|
16
|
+
* @tag layer:seed
|
|
17
|
+
* @tag pattern:none
|
|
18
|
+
* @tag access:server-only
|
|
19
|
+
* @tag consumers:seed-data/authored/index.ts
|
|
20
|
+
* @tag sideEffects:none
|
|
21
|
+
*/
|
|
22
|
+
export const authored = {
|
|
23
|
+
"checklist-admin-admin-detail-round-trips-coupon-edit-round-trip": {
|
|
24
|
+
roles: ["admin"],
|
|
25
|
+
startPage: "/admin/coupons",
|
|
26
|
+
steps: [
|
|
27
|
+
"Sign in as admin@letitrip.in / TempPass123!.",
|
|
28
|
+
"Open /admin/coupons and find the coupon with code ARENA25.",
|
|
29
|
+
"Open its Edit action and write down its discount value, its usage limits and its validity dates.",
|
|
30
|
+
"Change ONLY the description — append the word roundtrip.",
|
|
31
|
+
"Press Save.",
|
|
32
|
+
"Reload the page and open the same coupon again.",
|
|
33
|
+
"Read the discount value, the usage limits and the validity dates.",
|
|
34
|
+
],
|
|
35
|
+
inputs: { couponCode: "ARENA25", descriptionSuffix: "roundtrip" },
|
|
36
|
+
expectedBehaviour: "Editing one field writes that field and leaves the rest as they were. A handler that merges a partial validity or restrictions object wholesale wipes whatever the caller did not resend — and a handler that never calls the repository at all returns a 200 that echoes the submission back, which is indistinguishable from a save.",
|
|
37
|
+
expectedUiState: "The description carries the appended word. The discount value, both usage limits and both validity dates are exactly as written down.",
|
|
38
|
+
expectedData: { unrelatedFieldsChanged: 0 },
|
|
39
|
+
endResult: "All of it survives a second reload — the change was stored, not just rendered.",
|
|
40
|
+
},
|
|
41
|
+
"checklist-admin-admin-detail-round-trips-faq-edit-preserves-answer-shape": {
|
|
42
|
+
roles: ["admin"],
|
|
43
|
+
startPage: "/admin/faqs",
|
|
44
|
+
steps: [
|
|
45
|
+
"Sign in as admin@letitrip.in / TempPass123!.",
|
|
46
|
+
"Open /admin/faqs and pick the FAQ whose slug is coupons-and-discounts.",
|
|
47
|
+
"Open its Edit action and read how the answer renders in the editor.",
|
|
48
|
+
"Append the sentence: Edited during a round-trip check.",
|
|
49
|
+
"Press Save.",
|
|
50
|
+
"Reload and reopen the same FAQ.",
|
|
51
|
+
"Open the public /faqs page and find the same question.",
|
|
52
|
+
],
|
|
53
|
+
inputs: { faqSlug: "coupons-and-discounts", appended: "Edited during a round-trip check." },
|
|
54
|
+
expectedBehaviour: "The answer is a structured value, not a bare string. A create path that wraps it and an update path that writes it flat produce a document every reader expects to be an object and finds is a string — which renders as nothing, or as raw markup, on the public page.",
|
|
55
|
+
expectedUiState: "The editor still shows formatted rich text after the reload, and the public FAQ renders the answer as prose rather than as visible markup or an empty block.",
|
|
56
|
+
expectedData: { answerRendersAsRichText: true },
|
|
57
|
+
endResult: "The appended sentence is present on both the admin editor and the public page.",
|
|
58
|
+
},
|
|
59
|
+
"checklist-admin-admin-detail-round-trips-category-edit-keeps-hierarchy": {
|
|
60
|
+
roles: ["admin"],
|
|
61
|
+
startPage: "/admin/categories",
|
|
62
|
+
steps: [
|
|
63
|
+
"Sign in as admin@letitrip.in / TempPass123!.",
|
|
64
|
+
"Open /categories/x-starters as a visitor in a second tab and write down its product count.",
|
|
65
|
+
"In the first tab open /admin/categories and find X Starters.",
|
|
66
|
+
"Write down its parent and how many children it has.",
|
|
67
|
+
"Open its Edit action, change ONLY its description, and press Save.",
|
|
68
|
+
"Reload the admin list and reopen it.",
|
|
69
|
+
"Reload the public category page in the second tab.",
|
|
70
|
+
],
|
|
71
|
+
inputs: { categorySlug: "x-starters" },
|
|
72
|
+
expectedBehaviour: "A rename or a description edit touches that field alone. The hierarchy fields — parent, children, ancestors, tier — are derived and must never be rewritten from form state, and the product count is derived from a trigger rather than typed.",
|
|
73
|
+
expectedUiState: "The parent and the child count are unchanged, and the public page shows the same product count as before.",
|
|
74
|
+
expectedData: { hierarchyChanged: false, countChanged: false },
|
|
75
|
+
endResult: "Everything except the description is identical after a reload.",
|
|
76
|
+
},
|
|
77
|
+
"checklist-admin-admin-detail-round-trips-user-edit-keeps-tester-flags": {
|
|
78
|
+
roles: ["admin"],
|
|
79
|
+
startPage: "/admin/users",
|
|
80
|
+
steps: [
|
|
81
|
+
"Sign in as admin@letitrip.in / TempPass123!.",
|
|
82
|
+
"Open /admin/users and find the account claude-tester@letitrip.in.",
|
|
83
|
+
"Open its Edit action and write down whether the tester flags are on.",
|
|
84
|
+
"Change ONLY its bio — append the word roundtrip.",
|
|
85
|
+
"Press Save.",
|
|
86
|
+
"Reload the list and reopen the same account.",
|
|
87
|
+
"Read the tester flags again.",
|
|
88
|
+
],
|
|
89
|
+
inputs: { account: "claude-tester@letitrip.in", bioSuffix: "roundtrip" },
|
|
90
|
+
expectedBehaviour: "An editor seeded from a LIST row can only send back what that row carried. If the list serializer omits a field the update schema accepts, the editor state defaults to false and the save rewrites it — so editing any other field silently strips it. That is why the check is on a field the form did not touch.",
|
|
91
|
+
expectedUiState: "Both tester flags are still on after the reload, and the bio carries the appended word.",
|
|
92
|
+
expectedData: { testerFlagsPreserved: true },
|
|
93
|
+
endResult: "The flags survive a second reload. If this fails, restore them before moving on — the bot needs them to run at all.",
|
|
94
|
+
},
|
|
95
|
+
"checklist-admin-admin-detail-round-trips-store-edit-keeps-verified": {
|
|
96
|
+
roles: ["admin"],
|
|
97
|
+
startPage: "/admin/stores",
|
|
98
|
+
steps: [
|
|
99
|
+
"Sign in as admin@letitrip.in / TempPass123!.",
|
|
100
|
+
"Open /admin/stores and find Beyblade Arena.",
|
|
101
|
+
"Note whether it is marked Verified and whether it is Featured.",
|
|
102
|
+
"Open its Edit action and change ONLY the admin notes — append the word roundtrip.",
|
|
103
|
+
"Press Save.",
|
|
104
|
+
"Reload the list.",
|
|
105
|
+
"Read the Verified and Featured markers again.",
|
|
106
|
+
],
|
|
107
|
+
inputs: { store: "store-beyblade-arena", notesSuffix: "roundtrip" },
|
|
108
|
+
expectedBehaviour: "Same failure mode as the tester flags, with a worse consequence: un-verifying a store is visible to shoppers. A store's capabilities can be reset to a two-item default by the same mechanism, which is invisible until a seller reports that a feature disappeared.",
|
|
109
|
+
expectedUiState: "Verified and Featured are unchanged, and the admin notes carry the appended word.",
|
|
110
|
+
expectedData: { verifiedPreserved: true, featuredPreserved: true },
|
|
111
|
+
endResult: "Both markers are still correct after a reload.",
|
|
112
|
+
},
|
|
113
|
+
"checklist-admin-admin-detail-round-trips-order-view-matches-drawer": {
|
|
114
|
+
roles: ["admin"],
|
|
115
|
+
startPage: "/admin/orders",
|
|
116
|
+
steps: [
|
|
117
|
+
"Sign in as admin@letitrip.in / TempPass123!.",
|
|
118
|
+
"Open /admin/orders and open the row drawer for order-1-20260721-11joon.",
|
|
119
|
+
"Write down the item titles, the shipping address, the payment method and the status.",
|
|
120
|
+
"Close the drawer and use that row's Open full page action.",
|
|
121
|
+
"Read the same four things on the full page.",
|
|
122
|
+
],
|
|
123
|
+
inputs: { orderId: "order-1-20260721-11joon" },
|
|
124
|
+
expectedBehaviour: "The drawer and the full page are two renderings of one record, so they must agree. Where they differ, one of them is reading a field the other is not — and the full page is the bookmarkable one an admin will send to somebody else.",
|
|
125
|
+
expectedUiState: "Item titles, address, payment method and status are identical in both, and the full page is at a real bookmarkable URL rather than a modal over the list.",
|
|
126
|
+
expectedData: { drawerAndPageAgree: true },
|
|
127
|
+
endResult: "Nothing is changed; this case only reads.",
|
|
128
|
+
},
|
|
129
|
+
"checklist-admin-admin-detail-round-trips-admin-detail-missing-id-404s": {
|
|
130
|
+
roles: ["admin"],
|
|
131
|
+
startPage: "/admin/coupons",
|
|
132
|
+
steps: [
|
|
133
|
+
"Sign in as admin@letitrip.in / TempPass123!.",
|
|
134
|
+
"Navigate to /admin/coupons/zzzznope-not-a-coupon-42/edit — a deliberately invented id, not a fixture.",
|
|
135
|
+
"Read what the page shows.",
|
|
136
|
+
"Navigate to /admin/orders/zzzznope-not-an-order-42/view.",
|
|
137
|
+
"Read that page too.",
|
|
138
|
+
],
|
|
139
|
+
inputs: { couponId: "zzzznope-not-a-coupon-42", orderId: "zzzznope-not-an-order-42" },
|
|
140
|
+
expectedBehaviour: "An id that resolves to nothing must 404. An empty editor here is the tell that the page never checked what it received — and an empty editor with a Save button is how a record gets created or overwritten from a typo.",
|
|
141
|
+
expectedUiState: "Both show a not-found page. Neither shows an editable form, and neither shows a Save button.",
|
|
142
|
+
expectedData: { showsEditForm: false },
|
|
143
|
+
endResult: "Nothing is created and nothing is saved.",
|
|
144
|
+
},
|
|
145
|
+
};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Authored six-part procedures for the admin/firebase-function-effects page.
|
|
3
|
+
* WHAT: 14 cases, keyed by full checklist id.
|
|
4
|
+
*
|
|
5
|
+
* 🛑 EVERY CASE HERE TESTS AN EFFECT, NEVER A FUNCTION.
|
|
6
|
+
*
|
|
7
|
+
* 55 Firebase functions are deployed — 28 scheduled, 20 Firestore triggers, 7
|
|
8
|
+
* HTTPS — and none had a checklist case. The reason is that the obvious way to
|
|
9
|
+
* write one is untestable: "trigger the cron and confirm it ran" cannot be done
|
|
10
|
+
* from a browser in a session, so every such case would be answered null
|
|
11
|
+
* forever, which reads as coverage while providing none.
|
|
12
|
+
*
|
|
13
|
+
* So each case instead reads the thing the function is RESPONSIBLE FOR. A nightly
|
|
14
|
+
* reconciler is tested by recounting what it reconciles; a settlement job by
|
|
15
|
+
* whether the winner can pay; a self-triggering write-back by whether its
|
|
16
|
+
* timestamp moves when nothing changed. All answerable in seconds, all able to
|
|
17
|
+
* fail.
|
|
18
|
+
*
|
|
19
|
+
* Three of these are written against real, recorded incidents and would have
|
|
20
|
+
* caught them: #92 (a trigger that self-triggered 1,017,548 times in 24h),
|
|
21
|
+
* #60 (a settlement that produced an order nobody could pay), #102 (a reconciler
|
|
22
|
+
* that could not express what it reconciled).
|
|
23
|
+
*
|
|
24
|
+
* @tag domain:tester
|
|
25
|
+
* @tag layer:seed
|
|
26
|
+
* @tag pattern:none
|
|
27
|
+
* @tag access:server-only
|
|
28
|
+
* @tag consumers:seed-data/authored/index.ts
|
|
29
|
+
* @tag sideEffects:none
|
|
30
|
+
*/
|
|
31
|
+
const ADMIN = "Sign in as admin@letitrip.in / TempPass123!.";
|
|
32
|
+
export const authored = {
|
|
33
|
+
"checklist-admin-firebase-function-effects-function-invocation-health-per-function": {
|
|
34
|
+
roles: ["admin"],
|
|
35
|
+
startPage: "/admin/maintenance",
|
|
36
|
+
steps: [
|
|
37
|
+
ADMIN,
|
|
38
|
+
"Open the Firebase console for this project and go to Functions.",
|
|
39
|
+
"Sort the function list by invocation count over the last 24 hours, highest first.",
|
|
40
|
+
"Write down the top entry's count and the count of the tenth entry.",
|
|
41
|
+
"Divide the first by the second.",
|
|
42
|
+
],
|
|
43
|
+
expectedBehaviour: "No single function is running away from the rest. This is the case that would have caught Root Cause #92, where onShipmentHeaderWrite self-triggered 1,017,548 times in 24 hours against a 2,000,000/month free quota while every other function in the project sat between 0 and 95 — and the overage then blocked deploys and degraded production through the sibling Firestore read quota.",
|
|
44
|
+
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
|
+
expectedData: { runawayFunctions: 0 },
|
|
46
|
+
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.",
|
|
47
|
+
},
|
|
48
|
+
"checklist-admin-firebase-function-effects-function-errors-page-has-no-producer": {
|
|
49
|
+
roles: ["admin"],
|
|
50
|
+
startPage: "/admin/maintenance/function-errors",
|
|
51
|
+
steps: [
|
|
52
|
+
ADMIN,
|
|
53
|
+
"Open /admin/maintenance/function-errors and read what it shows.",
|
|
54
|
+
"Open /admin/maintenance/server-errors in another tab and read what it shows.",
|
|
55
|
+
"Compare the two: note whether server-errors has rows while function-errors does not.",
|
|
56
|
+
],
|
|
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.",
|
|
58
|
+
expectedUiState: "function-errors renders an empty list or empty state. server-errors renders actual rows. Screenshot both, side by side if possible.",
|
|
59
|
+
expectedData: { functionErrorRows: 0 },
|
|
60
|
+
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.",
|
|
61
|
+
},
|
|
62
|
+
"checklist-admin-firebase-function-effects-https-function-401-is-healthy-500-is-not": {
|
|
63
|
+
roles: ["admin"],
|
|
64
|
+
startPage: "/admin/maintenance",
|
|
65
|
+
steps: [
|
|
66
|
+
ADMIN,
|
|
67
|
+
"Open the Firebase console's Functions list and copy the trigger URL of adminAnalytics.",
|
|
68
|
+
"Open that URL in a new tab with no auth header and read the status and body.",
|
|
69
|
+
"Repeat for storeAnalytics, promotionsApi, listingProcessor, triggerEventRaffle, assignSpinPrize and gateway.",
|
|
70
|
+
"Write down the status code each one returned.",
|
|
71
|
+
],
|
|
72
|
+
expectedBehaviour: "Every one of the seven answers 401. A 401 means the module LOADED and the auth gate ran — that is healthy, and it is the expected answer, not a failure. A 500 means the module failed at cold start, which is how Root Cause #69 served 500 on every route while the build reported READY.",
|
|
73
|
+
expectedUiState: "Seven status codes are recorded. All are 401 (or 403). Any 500 is quoted with the function's name and whatever body it returned.",
|
|
74
|
+
expectedData: { httpsFunctions: 7, fiveHundreds: 0 },
|
|
75
|
+
endResult: "Read-only; nothing persists. 🛑 Do not record a 401 as a failure — that inversion is the single most likely way this case is answered wrongly.",
|
|
76
|
+
},
|
|
77
|
+
"checklist-admin-firebase-function-effects-counters-reconcile-effect-category-counts": {
|
|
78
|
+
roles: ["admin"],
|
|
79
|
+
startPage: "/categories",
|
|
80
|
+
steps: [
|
|
81
|
+
ADMIN,
|
|
82
|
+
"Open /categories and write down the product count shown for the root category Spinning Tops.",
|
|
83
|
+
"Open that category's own page and count the listings it actually shows, paging through if there is more than one page.",
|
|
84
|
+
"Do the same for one leaf category further down the tree.",
|
|
85
|
+
"Compare each displayed count against your recount.",
|
|
86
|
+
],
|
|
87
|
+
expectedBehaviour: "The displayed count equals the recount, for both the root and the leaf. countersReconcile runs nightly and maintains two DIFFERENT numbers — metrics.productCount, the items filed directly under a row, and metrics.totalProductCount, that row plus every descendant. Root Cause 102: setMetrics took one pair of numbers and wrote it to both fields, so on any ancestor the row's own items were lost from its own rollup. 19 of 65 rows disagreed with a recount and every one was LOW.",
|
|
88
|
+
expectedUiState: "Two categories are recounted and both match. Any mismatch is reported with three numbers: the displayed count, the recount, and which category.",
|
|
89
|
+
endResult: "Read-only; nothing persists. A wrong number here never errors — it is simply a number — so the recount is the only thing that can detect it.",
|
|
90
|
+
},
|
|
91
|
+
"checklist-admin-firebase-function-effects-revenue-rollup-effect-dashboard-reads-one-doc": {
|
|
92
|
+
roles: ["admin"],
|
|
93
|
+
startPage: "/admin",
|
|
94
|
+
steps: [
|
|
95
|
+
ADMIN,
|
|
96
|
+
"Open /admin and read the revenue figures on the dashboard.",
|
|
97
|
+
"Open /admin/orders, filter to delivered orders, and note roughly how many there are and their rough total.",
|
|
98
|
+
"Compare the dashboard's figure against that rough total.",
|
|
99
|
+
"Reload /admin once and confirm the figure is the same and appears without a long delay.",
|
|
100
|
+
],
|
|
101
|
+
expectedBehaviour: "The dashboard shows a plausible revenue figure derived from a pre-computed rollup rather than a live scan of every order. revenueRollup exists specifically so this page is a single-document read — that is the pattern protecting the 50,000/day Firestore read quota, and a dashboard that scans instead is how that quota gets spent.",
|
|
102
|
+
expectedUiState: "A revenue figure is present and is of the same order as the delivered orders suggest. Zero revenue while real delivered orders exist means the rollup did not run or wrote a shape the page cannot read.",
|
|
103
|
+
endResult: "Read-only; nothing persists.",
|
|
104
|
+
},
|
|
105
|
+
"checklist-admin-firebase-function-effects-auction-settlement-effect-winner-gets-payable-line": {
|
|
106
|
+
roles: ["admin"],
|
|
107
|
+
startPage: "/admin/products",
|
|
108
|
+
steps: [
|
|
109
|
+
ADMIN,
|
|
110
|
+
"Open /admin/products and filter to auctions, then find one that has already ended with bids on it.",
|
|
111
|
+
"Open it and note the winning bidder.",
|
|
112
|
+
"Open that winner's account view from /admin/users and look for what the win produced.",
|
|
113
|
+
"Establish whether the win left something the winner could actually pay for — a locked cart line reachable from checkout — or only a record that renders nowhere.",
|
|
114
|
+
],
|
|
115
|
+
expectedBehaviour: "A settled auction leaves the winner a LOCKED cart line that reaches checkout through the auction lane. Root Cause #60: auctionSettlement used to write a document into the orders collection that was not an OrderDocument — flat productId/unitPrice fields instead of buyerId and items[], no paymentMethod, a Firestore auto-id instead of a semantic one. It rendered correctly in no orders list, no checkout accepted it, and the sweep that cleans up unpaid orders never saw it. There was no way, anywhere in the product, for a winner to pay.",
|
|
116
|
+
expectedUiState: "The win is visible to the winner as something actionable. If it appears only as a row that cannot be opened or paid, that is the finding, and the auction id is quoted.",
|
|
117
|
+
endResult: "Read-only — do not pay or cancel anything. The point is whether a payable path EXISTS, not whether it completes.",
|
|
118
|
+
},
|
|
119
|
+
"checklist-admin-firebase-function-effects-offer-expiry-effect-lapsed-offer-frees-the-cart": {
|
|
120
|
+
roles: ["admin"],
|
|
121
|
+
startPage: "/admin/offers",
|
|
122
|
+
steps: [
|
|
123
|
+
ADMIN,
|
|
124
|
+
"Open /admin/offers and find an offer whose status is expired, or one whose deadline has clearly passed.",
|
|
125
|
+
"Open it and note the buyer.",
|
|
126
|
+
"Open that buyer's cart from the admin user view, or sign in as them if the fixture allows.",
|
|
127
|
+
"Check whether a locked line for that offer is still sitting in their cart.",
|
|
128
|
+
],
|
|
129
|
+
expectedBehaviour: "A lapsed offer is no longer actionable AND has left no locked line behind. runOfferExpiry sweeps three things in one pass — pending offers past expiresAt, ACCEPTED offers past checkoutDeadline, and unpaid auction wins. The third and second were added later than the first, and the function deliberately has no early return when the first query is empty, because the other two sweeps must still run.",
|
|
130
|
+
expectedUiState: "The offer reads as expired and the buyer's cart has no leftover locked line for it.",
|
|
131
|
+
expectedData: { leftoverLockedLines: 0 },
|
|
132
|
+
endResult: "Read-only; nothing persists. 🛑 A leftover locked line is worse than it looks: the offer lane OUTRANKS the standard lane, so it blocks the buyer's entire cart, not merely that one offer.",
|
|
133
|
+
},
|
|
134
|
+
"checklist-admin-firebase-function-effects-payment-window-timeout-effect-expired-proof-window": {
|
|
135
|
+
roles: ["admin"],
|
|
136
|
+
startPage: "/admin/orders",
|
|
137
|
+
steps: [
|
|
138
|
+
ADMIN,
|
|
139
|
+
"Open /admin/orders and filter to manual-payment orders awaiting payment.",
|
|
140
|
+
"Find one whose payment deadline has already passed.",
|
|
141
|
+
"Open it and read its status and its payment panel.",
|
|
142
|
+
"Note whether it has been resolved by a sweep or is still sitting pending with an expired deadline.",
|
|
143
|
+
],
|
|
144
|
+
expectedBehaviour: "An order past its 15-minute payment window has been resolved rather than left pending indefinitely. paymentWindowTimeout and paymentReviewAutoApprove are the two sweeps that stop a manual payment stalling forever — one cancels an unpaid window, the other auto-approves an unreviewed proof after two hours.",
|
|
145
|
+
expectedUiState: "The order shows a resolved state consistent with its deadline having passed. An order still pending with a deadline hours in the past is the finding, with its id and its deadline quoted.",
|
|
146
|
+
endResult: "Read-only — do not verify or reject the payment yourself; that would destroy the evidence.",
|
|
147
|
+
},
|
|
148
|
+
"checklist-admin-firebase-function-effects-rtdb-event-channels-are-pruned": {
|
|
149
|
+
roles: ["admin"],
|
|
150
|
+
startPage: "/admin/maintenance",
|
|
151
|
+
steps: [
|
|
152
|
+
ADMIN,
|
|
153
|
+
"Open the Firebase console's Realtime Database for this project.",
|
|
154
|
+
"Look at the auth_events, payment_events and bulk_events nodes and note roughly how many children each holds.",
|
|
155
|
+
"Note the oldest timestamp you can see under each.",
|
|
156
|
+
"Compare that age against the prune interval each is supposed to have.",
|
|
157
|
+
],
|
|
158
|
+
expectedBehaviour: "The signal channels are being pruned rather than accumulating. cleanupRtdbEvents prunes auth_events at 3 minutes and payment_events and bulk_events at 15, and email_events at 1 hour. The email channel is deliberately the longest: a batch can take ten minutes between triggering a send and asserting on it, and a ping deleted mid-batch reads as \"no email was sent\" — a false negative in exactly the mechanism meant to prevent one.",
|
|
159
|
+
expectedUiState: "Each channel holds only recent children. A node holding entries hours or days old means the prune is not running for it, and that node's name is quoted.",
|
|
160
|
+
endResult: "Read-only — do not delete anything by hand. Note that auction-bids is deliberately NOT pruned and is not part of this case.",
|
|
161
|
+
},
|
|
162
|
+
"checklist-admin-firebase-function-effects-media-tmp-cleanup-effect-aborted-uploads-removed": {
|
|
163
|
+
roles: ["admin"],
|
|
164
|
+
startPage: "/admin/media",
|
|
165
|
+
steps: [
|
|
166
|
+
ADMIN,
|
|
167
|
+
"Open /admin/media and begin uploading an image.",
|
|
168
|
+
"Abandon the upload before it finalises — close the dialog or navigate away mid-upload.",
|
|
169
|
+
"Return to /admin/media and look for the abandoned file.",
|
|
170
|
+
"Reload once and look again.",
|
|
171
|
+
],
|
|
172
|
+
expectedBehaviour: "An abandoned upload does not appear as a real, usable asset. Bytes land under a tmp/ prefix and are moved to a permanent path only by finalize; mediaTmpCleanup removes whatever never finalised. An un-finalised object showing up in the media library is the finding, because it can then be attached to a listing and will later vanish.",
|
|
173
|
+
expectedUiState: "The media library shows no entry for the abandoned upload, either immediately or after the reload.",
|
|
174
|
+
endResult: "If a stray entry was created, note its name so it can be cleaned up; do not leave it attached to any listing.",
|
|
175
|
+
},
|
|
176
|
+
"checklist-admin-firebase-function-effects-product-write-trigger-effect-stock-and-groups": {
|
|
177
|
+
roles: ["admin"],
|
|
178
|
+
startPage: "/admin/products",
|
|
179
|
+
steps: [
|
|
180
|
+
ADMIN,
|
|
181
|
+
"Open /admin/grouped-listings and pick a group, noting its member count and how many members it says are active.",
|
|
182
|
+
"Note one member product's id.",
|
|
183
|
+
"Open that product in /admin/products and set its stock quantity to 0, then save.",
|
|
184
|
+
"Return to /admin/grouped-listings and reload, then read the same group's active member count.",
|
|
185
|
+
"Set the product's stock back to its original value and confirm the count returns.",
|
|
186
|
+
],
|
|
187
|
+
expectedBehaviour: "Changing one product's stock updates the derived state on the OTHER records that depend on it, without anyone editing them. onProductWrite / onProductStockChange recompute a grouped listing's activeMemberCount and visibilityStatus and keep bundles in sync. The effect is visible on the group, never on the product you edited — which is why a case that only re-reads the product would pass against a broken trigger.",
|
|
188
|
+
expectedUiState: "The group's active member count drops by one after the stock change and returns after it is restored. A count that never moves is the finding.",
|
|
189
|
+
endResult: "🛑 RESTORE THE STOCK. Leaving a seeded product at zero silently removes it from every public listing for every later case.",
|
|
190
|
+
},
|
|
191
|
+
"checklist-admin-firebase-function-effects-order-create-trigger-effect-staff-signal": {
|
|
192
|
+
roles: ["admin"],
|
|
193
|
+
startPage: "/admin/orders",
|
|
194
|
+
steps: [
|
|
195
|
+
ADMIN,
|
|
196
|
+
"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.",
|
|
198
|
+
"Note whether the signal exists, and whether there is exactly one rather than one per employee.",
|
|
199
|
+
],
|
|
200
|
+
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.",
|
|
201
|
+
expectedUiState: "A staff-visible signal exists for the recent order. If several identical signals exist, one per employee, that is a finding in its own right.",
|
|
202
|
+
endResult: "Read-only; nothing persists.",
|
|
203
|
+
},
|
|
204
|
+
"checklist-admin-firebase-function-effects-shipment-trigger-no-op-guard-holds": {
|
|
205
|
+
roles: ["admin"],
|
|
206
|
+
startPage: "/admin/shipments",
|
|
207
|
+
steps: [
|
|
208
|
+
ADMIN,
|
|
209
|
+
"Open /admin/shipments and open one shipment.",
|
|
210
|
+
"Note its computed totals and the timestamp showing when those totals were last computed.",
|
|
211
|
+
"Press Save without changing any field.",
|
|
212
|
+
"Reload the shipment and read that timestamp again.",
|
|
213
|
+
"Press Save unchanged a second time, reload, and read it once more.",
|
|
214
|
+
],
|
|
215
|
+
expectedBehaviour: "Saving an unchanged shipment does NOT recompute its totals — the timestamp is identical all three times. This is Root Cause #92's exact mechanism: onShipmentHeaderWrite watches the collection it writes back to, and its only brake is a no-op guard. That guard compared with JSON.stringify, which is key-order sensitive, while Firestore returns map fields alphabetically and the code built them in construction order. The two strings could never match, so every write re-triggered — 1,017,548 invocations in 24 hours, which then blocked deploys and degraded production.",
|
|
216
|
+
expectedUiState: "The totals-computed timestamp is byte-identical across all three readings. Any movement on an unchanged save means the guard is not holding, and that is a billing incident, not a cosmetic one.",
|
|
217
|
+
expectedData: { timestampMovementsOnUnchangedSave: 0 },
|
|
218
|
+
endResult: "No field is changed. 🛑 If the timestamp DOES move, stop and report immediately rather than saving again — each save is another round of the loop.",
|
|
219
|
+
},
|
|
220
|
+
"checklist-admin-firebase-function-effects-scheduled-job-count-matches-registry": {
|
|
221
|
+
roles: ["admin"],
|
|
222
|
+
startPage: "/admin/maintenance",
|
|
223
|
+
steps: [
|
|
224
|
+
ADMIN,
|
|
225
|
+
"Open the Google Cloud console's Cloud Scheduler page for this project.",
|
|
226
|
+
"Count the jobs listed and write the number down.",
|
|
227
|
+
"Open the Firebase console's Functions list and count the entries with a scheduled trigger.",
|
|
228
|
+
"Compare the two numbers.",
|
|
229
|
+
],
|
|
230
|
+
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
|
+
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.",
|
|
233
|
+
},
|
|
234
|
+
};
|
|
@@ -530,4 +530,36 @@ export const authored = {
|
|
|
530
530
|
needsReview: true,
|
|
531
531
|
reviewNote: "The step 2 add-ons screen did not show a shipping method selector in the session observed — it may only appear when the seller has configured multiple shipping options. Mark as needs-review until confirmed.",
|
|
532
532
|
},
|
|
533
|
+
/* ── Added 2026-09-14 (F3d) — the SIGNED-OUT checkout ────────────────────────
|
|
534
|
+
* 30 checkout cases existed and NONE was signed-out, so the refusal path — the
|
|
535
|
+
* single most important thing checkout does for a guest — had no coverage at
|
|
536
|
+
* all. Only roles exactly ["guest"] reaches the guest identity slice.
|
|
537
|
+
*/
|
|
538
|
+
"checklist-buying-buying-checkout-checkout-guest-redirected-to-signin": {
|
|
539
|
+
roles: ["guest"],
|
|
540
|
+
startPage: "/checkout",
|
|
541
|
+
steps: [
|
|
542
|
+
"While signed out, open /checkout directly by URL.",
|
|
543
|
+
"Note exactly what renders, and where the URL ends up.",
|
|
544
|
+
"If any form is shown, note which fields it asks for before refusing.",
|
|
545
|
+
],
|
|
546
|
+
expectedBehaviour: "The visitor is sent to sign in. Checkout does not render its address form to a signed-out visitor and only refuse at the end — that wastes the buyer's typing and reads as a bug in the payment step rather than an auth requirement.",
|
|
547
|
+
expectedUiState: "Either /auth/login or an explicit sign-in prompt. No address fields, no payment step, and no partially-rendered checkout shell with a spinner that never resolves.",
|
|
548
|
+
endResult: "No order and no draft address are created. Nothing persists.",
|
|
549
|
+
},
|
|
550
|
+
"checklist-buying-buying-checkout-checkout-guest-returns-after-signin": {
|
|
551
|
+
roles: ["guest"],
|
|
552
|
+
startPage: "/cart",
|
|
553
|
+
steps: [
|
|
554
|
+
"While signed out, add product-beyblade-burst-valkyrie to the cart.",
|
|
555
|
+
"Press the checkout CTA and follow the sign-in prompt.",
|
|
556
|
+
"Sign in as rehan.sheikh@gmail.com / TempPass123!.",
|
|
557
|
+
"Note which page you land on once signed in.",
|
|
558
|
+
"Note whether product-beyblade-burst-valkyrie is still in the cart.",
|
|
559
|
+
],
|
|
560
|
+
expectedBehaviour: "BEFORE: a signed-out visitor with one cart line is stopped at checkout. AFTER signing in they are returned to checkout with that same line still present — the guest cart has merged into the account cart. A redirect that forgets where it came from, or that loses the line, is the same as losing the sale.",
|
|
561
|
+
expectedUiState: "The landing page is checkout or the cart, not the homepage. The line for product-beyblade-burst-valkyrie is present with its original quantity, and prices are now visible because the viewer is signed in.",
|
|
562
|
+
expectedData: { cartLinesAfterSignIn: 1 },
|
|
563
|
+
endResult: "The line now lives in the ACCOUNT cart and will persist across sessions — remove it afterwards so later cases start clean. Place no order.",
|
|
564
|
+
},
|
|
533
565
|
};
|
|
@@ -1168,4 +1168,64 @@ export const authored = {
|
|
|
1168
1168
|
expectedUiState: "No add-on flags appear in the request bodies. The recorded fees match the cart's checkboxes. A body carrying add-on flags is the finding even if the totals happen to agree today.",
|
|
1169
1169
|
endResult: "Two orders exist with fees matching the cart.",
|
|
1170
1170
|
},
|
|
1171
|
+
/* ── Added 2026-09-14 (F3d) — the SIGNED-OUT cart ───────────────────────────
|
|
1172
|
+
* 71 cart cases existed and 3 were signed-out. Only a case whose `roles` is
|
|
1173
|
+
* exactly ["guest"] is routed into the guest identity slice, which copies an
|
|
1174
|
+
* EMPTY storage state over the fixed session.json path; a two-role case runs
|
|
1175
|
+
* signed in and cannot test any of this.
|
|
1176
|
+
*/
|
|
1177
|
+
"checklist-buying-cart-cart-guest-add-and-persist": {
|
|
1178
|
+
roles: ["guest"],
|
|
1179
|
+
startPage: "/products/product-beyblade-burst-valkyrie",
|
|
1180
|
+
steps: [
|
|
1181
|
+
"Open /products/product-beyblade-burst-valkyrie while signed out.",
|
|
1182
|
+
"Press Add to cart.",
|
|
1183
|
+
"Open /cart and note the line that is there.",
|
|
1184
|
+
"Press the browser reload button and wait for the page to settle.",
|
|
1185
|
+
"Note whether the same line is still there.",
|
|
1186
|
+
],
|
|
1187
|
+
expectedBehaviour: "BEFORE: a signed-out visitor adds an item and the guest cart holds it. AFTER a full reload the same line is still present. The guest cart is localStorage-backed, so a cart that does not survive a reload loses the visitor's selection between any two navigations — and that is invisible to a signed-in tester, which is why this case exists.",
|
|
1188
|
+
expectedUiState: "The cart shows exactly one line for product-beyblade-burst-valkyrie both before and after the reload, with the same quantity. An empty cart after reload is the failure.",
|
|
1189
|
+
expectedData: { cartLinesAfterReload: 1 },
|
|
1190
|
+
endResult: "The line persists in localStorage until cleared. Remove it at the end so the next signed-out case starts clean.",
|
|
1191
|
+
},
|
|
1192
|
+
"checklist-buying-cart-cart-guest-prices-gated": {
|
|
1193
|
+
roles: ["guest"],
|
|
1194
|
+
startPage: "/cart",
|
|
1195
|
+
steps: [
|
|
1196
|
+
"While signed out, add product-beyblade-burst-valkyrie to the cart and open /cart.",
|
|
1197
|
+
"Read the price area of the cart line.",
|
|
1198
|
+
"Read the order summary's total area.",
|
|
1199
|
+
"Reload the page once and watch the price area during the first second after load.",
|
|
1200
|
+
],
|
|
1201
|
+
expectedBehaviour: "A signed-out visitor is shown a sign-in prompt where a price would be, not an amount. The gate has three states and the middle one renders a neutral placeholder, so a signed-in buyer must never see the prompt flash on load — but a signed-out visitor must never see a real amount at all.",
|
|
1202
|
+
expectedUiState: "The cart line and the summary both show the sign-in prompt in place of the money, and the row keeps its height rather than collapsing. No rupee amount for the item price is visible anywhere on the page.",
|
|
1203
|
+
endResult: "Read-only apart from the cart line; remove it afterwards.",
|
|
1204
|
+
},
|
|
1205
|
+
"checklist-buying-cart-cart-guest-checkout-prompts-signin": {
|
|
1206
|
+
roles: ["guest"],
|
|
1207
|
+
startPage: "/cart",
|
|
1208
|
+
steps: [
|
|
1209
|
+
"While signed out, add product-beyblade-burst-valkyrie to the cart and open /cart.",
|
|
1210
|
+
"Press the checkout CTA.",
|
|
1211
|
+
"Note exactly where you land and what is shown.",
|
|
1212
|
+
],
|
|
1213
|
+
expectedBehaviour: "The visitor is asked to sign in. They are not dropped onto a checkout form that will refuse them later, and not left on the cart with a button that appears to do nothing.",
|
|
1214
|
+
expectedUiState: "A sign-in prompt, modal or /auth/login page appears, and it is clear that signing in will continue the purchase. A silent no-op is the failure, and so is an empty checkout page.",
|
|
1215
|
+
endResult: "No order is created. Remove the cart line afterwards.",
|
|
1216
|
+
},
|
|
1217
|
+
"checklist-buying-cart-cart-guest-group-line-refused": {
|
|
1218
|
+
roles: ["guest"],
|
|
1219
|
+
startPage: "/bundles",
|
|
1220
|
+
steps: [
|
|
1221
|
+
"While signed out, open /bundles and pick any bundle.",
|
|
1222
|
+
"Try to add the bundle to the cart.",
|
|
1223
|
+
"Note exactly what happens.",
|
|
1224
|
+
"Open /cart and note whether anything was added.",
|
|
1225
|
+
],
|
|
1226
|
+
expectedBehaviour: "The visitor is told to sign in, explicitly. A grouped or bundle line cannot exist in the guest cart at all — it is keyed on productId and quantity only, so such a line could not survive the login merge — which means a half-added line would be silently lost rather than merged.",
|
|
1227
|
+
expectedUiState: "A sign-in prompt appears. The cart afterwards contains no partial bundle line and no single stray member product.",
|
|
1228
|
+
expectedData: { guestCartBundleLines: 0 },
|
|
1229
|
+
endResult: "Nothing is added to the guest cart. Nothing persists.",
|
|
1230
|
+
},
|
|
1171
1231
|
};
|