@mohasinac/appkit 4.35.4 → 4.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/features/site-settings/adapters.d.ts +2 -2
- package/dist/_internal/server/features/site-settings/adapters.js +25 -1
- package/dist/_internal/server/jobs/handlers/onShipmentAllocationSync.js +73 -6
- package/dist/_internal/shared/features/site-settings/types.d.ts +12 -0
- package/dist/features/tester/schemas/firestore.d.ts +29 -0
- package/dist/features/tester/seed-data/_money-flows.js +13 -5
- package/dist/features/tester/seed-data/authored/_types.d.ts +24 -0
- package/dist/features/tester/seed-data/authored/account-auth__profile-settings.js +2 -0
- package/dist/features/tester/seed-data/authored/account-auth__signup-login.js +22 -0
- package/dist/features/tester/seed-data/authored/admin__bug-hunter-rewards.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__bug-hunter-rewards.js +106 -0
- package/dist/features/tester/seed-data/authored/admin__site-system.js +2 -0
- package/dist/features/tester/seed-data/authored/buying__my-orders.js +2 -0
- package/dist/features/tester/seed-data/authored/buying__offers.js +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__notifications.js +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__search.js +48 -19
- package/dist/features/tester/seed-data/authored/index.js +6 -0
- package/dist/features/tester/seed-data/authored/public-pages__auth-error-pages.js +4 -0
- package/dist/features/tester/seed-data/authored/selling__listing-a-product.js +0 -16
- package/dist/features/tester/seed-data/authored/selling__product-upload-details.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__product-upload-details.js +150 -0
- package/dist/features/tester/seed-data/authored/selling__quick-add-minimum-details.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__quick-add-minimum-details.js +154 -0
- package/dist/features/tester/seed-data/authored/selling__seller-orders.js +2 -0
- package/dist/features/tester/seed-data/index.d.ts +1 -0
- package/dist/features/tester/seed-data/index.js +1 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +111 -6
- package/dist/features/tester/seed-data/tester-responses-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/tester-responses-seed-data.js +95 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +797 -752
|
@@ -37,9 +37,9 @@ import type { PublicSiteSettings } from "../../../shared/features/site-settings/
|
|
|
37
37
|
* was traced to a call site; anything without one belongs in the private list,
|
|
38
38
|
* because an unused public field is pure attack surface.
|
|
39
39
|
*/
|
|
40
|
-
export declare const PUBLIC_SITE_SETTINGS_FIELDS: readonly ["contact", "payment", "listings", "notificationChannels", "announcementBar", "navConfig", "actionConfig", "background", "watermark"];
|
|
40
|
+
export declare const PUBLIC_SITE_SETTINGS_FIELDS: readonly ["contact", "payment", "listings", "notificationChannels", "announcementBar", "navConfig", "actionConfig", "background", "watermark", "disabledRoutes"];
|
|
41
41
|
/** Everything else, with the reason it stays server-side. */
|
|
42
|
-
export declare const PRIVATE_SITE_SETTINGS_FIELDS: readonly ["id", "createdAt", "updatedAt", "credentials", "emailSettings", "legalPages", "commissions", "emi", "gst", "laborRate", "adSettings", "integrations", "platformLimits", "auctionConfig", "
|
|
42
|
+
export declare const PRIVATE_SITE_SETTINGS_FIELDS: readonly ["id", "createdAt", "updatedAt", "credentials", "emailSettings", "legalPages", "commissions", "emi", "gst", "laborRate", "adSettings", "integrations", "platformLimits", "auctionConfig", "siteName", "tagline", "motto", "logo", "favicon", "seo", "socialLinks", "features", "shipping", "returns", "faq", "aboutContent", "navbarConfig", "footerConfig", "theme", "featuredResults", "messaging"];
|
|
43
43
|
export interface ToPublicSiteSettingsOptions {
|
|
44
44
|
/**
|
|
45
45
|
* The resolved marker → wordmark → text watermark chain. Injected because
|
|
@@ -13,6 +13,26 @@ export const PUBLIC_SITE_SETTINGS_FIELDS = [
|
|
|
13
13
|
"actionConfig", // useAuthGate
|
|
14
14
|
"background", // DashboardLayoutClient (background.light / .dark)
|
|
15
15
|
"watermark", // MediaVideo, as the fallback behind effectiveWatermark
|
|
16
|
+
/*
|
|
17
|
+
* src/proxy.ts — the admin "disable a nav item and that route 404s" gate.
|
|
18
|
+
*
|
|
19
|
+
* 🛑 It was PRIVATE until 2026-09-13, justified by "read by
|
|
20
|
+
* app/[locale]/layout.tsx (RSC)". That was true once and stopped being true in
|
|
21
|
+
* Root Cause #82, which moved the gate OUT of the root layout (a dynamic API
|
|
22
|
+
* there made every page on the site uncacheable) and into the proxy. The
|
|
23
|
+
* proxy cannot do a Firestore read on the Edge, so it fetches
|
|
24
|
+
* /api/site-settings over HTTP — where a private field simply does not exist.
|
|
25
|
+
*
|
|
26
|
+
* The gate has therefore been a silent no-op ever since: getDisabledRoutes()
|
|
27
|
+
* reads `body.data.disabledRoutes`, gets undefined, and returns []. It fails
|
|
28
|
+
* OPEN, so nothing 404s that should not; the admin capability just does
|
|
29
|
+
* nothing. The stale comment on the private entry was the only trace.
|
|
30
|
+
*
|
|
31
|
+
* Publishing it is the right call rather than reverting the move: it is a
|
|
32
|
+
* list of route paths an admin has switched off, not a secret, and the
|
|
33
|
+
* alternative is putting a per-request Firestore read back on the Edge.
|
|
34
|
+
*/
|
|
35
|
+
"disabledRoutes",
|
|
16
36
|
];
|
|
17
37
|
/** Everything else, with the reason it stays server-side. */
|
|
18
38
|
export const PRIVATE_SITE_SETTINGS_FIELDS = [
|
|
@@ -30,7 +50,6 @@ export const PRIVATE_SITE_SETTINGS_FIELDS = [
|
|
|
30
50
|
"integrations", // analytics/tag IDs are injected server-side into markup, not fetched by the client
|
|
31
51
|
"platformLimits", // server-enforced caps; no client reader
|
|
32
52
|
"auctionConfig", // read by AuctionDetailPageView, a Server Component doing its own settings read
|
|
33
|
-
"disabledRoutes", // read by app/[locale]/layout.tsx (RSC) to gate routing
|
|
34
53
|
"siteName", // delivered as an SSR prop / rendered server-side
|
|
35
54
|
"tagline", // admin chrome only
|
|
36
55
|
"motto", // rendered server-side
|
|
@@ -81,6 +100,11 @@ export function toPublicSiteSettings(doc, opts) {
|
|
|
81
100
|
actionConfig: doc.actionConfig,
|
|
82
101
|
background: doc.background,
|
|
83
102
|
watermark: doc.watermark,
|
|
103
|
+
// Defaulted to [] rather than passed through: src/proxy.ts fails OPEN on a
|
|
104
|
+
// missing value, so an absent field and an empty list already behave the
|
|
105
|
+
// same — but emitting `undefined` would drop the key from the JSON entirely
|
|
106
|
+
// and leave the next person wondering whether the projection ran at all.
|
|
107
|
+
disabledRoutes: doc.disabledRoutes ?? [],
|
|
84
108
|
effectiveWatermark: opts?.effectiveWatermark,
|
|
85
109
|
};
|
|
86
110
|
}
|
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
import { allocateShipmentCosts } from "../../../../features/shipments/utils/cost-allocation";
|
|
2
2
|
const DEFAULT_MAX_HOURS_PER_DAY = 4;
|
|
3
|
+
/*
|
|
4
|
+
* 🛑 THIS COMPARISON IS LOAD-BEARING. GETTING IT WRONG IS AN INFINITE LOOP THAT
|
|
5
|
+
* COSTS REAL MONEY, AND IT ALREADY DID.
|
|
6
|
+
*
|
|
7
|
+
* `onShipmentHeaderWrite` watches `procurementShipments/{shipmentId}` on
|
|
8
|
+
* documentWritten and writes BACK to that same document. The only thing stopping
|
|
9
|
+
* that from recurring forever is this equality check, so a false "changed" is not
|
|
10
|
+
* a wasted write — it is a permanent, self-sustaining cascade.
|
|
11
|
+
*
|
|
12
|
+
* It was `JSON.stringify(shipment.totals ?? {}) === JSON.stringify(totals)`.
|
|
13
|
+
* That is key-ORDER sensitive, and the two sides can never agree:
|
|
14
|
+
*
|
|
15
|
+
* computed — construction order from allocateShipmentCosts():
|
|
16
|
+
* lotsCost, customsTotal, shippingTotal, laborCost, …
|
|
17
|
+
* stored — Firestore returns map fields with keys sorted ALPHABETICALLY:
|
|
18
|
+
* customsTotal, estimatedProcessingDays, laborCost, lotCount, …
|
|
19
|
+
*
|
|
20
|
+
* So `totalsUnchanged` was ALWAYS false, every invocation wrote, and every write
|
|
21
|
+
* re-triggered the function. Measured 2026-09-11 in the Firebase console:
|
|
22
|
+
* `onShipmentHeaderWrite` 1,017,548 and `onShipmentDeleted` 1,017,372 invocations
|
|
23
|
+
* in 24 HOURS — 12M for the month against a 2M/month free quota, exceeded by
|
|
24
|
+
* 9.7M, while every other function in the project sat between 0 and 95.
|
|
25
|
+
*
|
|
26
|
+
* `onShipmentDeleted` was pure collateral: it is correctly guarded
|
|
27
|
+
* (`if (event.after) return`) but is bound to the same path, so it woke up a
|
|
28
|
+
* million times a day just to return.
|
|
29
|
+
*
|
|
30
|
+
* Two properties this replacement needs, and the old one had neither:
|
|
31
|
+
* 1. KEY ORDER MUST NOT MATTER — compare by key, never by serialised form.
|
|
32
|
+
* 2. FLOATS NEED TOLERANCE. `projectedMarginPercent` and `projectedRoiPercent`
|
|
33
|
+
* are unrounded divisions, and a Firestore round-trip can return a value
|
|
34
|
+
* that differs in the last bit. Exact `!==` on those is another way to make
|
|
35
|
+
* "unchanged" unreachable — the same bug with a different mechanism.
|
|
36
|
+
*/
|
|
37
|
+
const MONEY_EPSILON = 0.005; // half a paisa: below anything that can be displayed
|
|
38
|
+
function numbersEqual(a, b) {
|
|
39
|
+
const x = typeof a === "number" ? a : Number.NaN;
|
|
40
|
+
const y = typeof b === "number" ? b : Number.NaN;
|
|
41
|
+
if (Number.isNaN(x) && Number.isNaN(y))
|
|
42
|
+
return a === b; // both non-numeric: identity
|
|
43
|
+
return Math.abs(x - y) < MONEY_EPSILON;
|
|
44
|
+
}
|
|
45
|
+
/** Value equality over a flat numeric map, independent of key order. */
|
|
46
|
+
function totalsEqual(stored, computed) {
|
|
47
|
+
if (!stored || typeof stored !== "object" || Array.isArray(stored))
|
|
48
|
+
return false;
|
|
49
|
+
const a = stored;
|
|
50
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(computed)]);
|
|
51
|
+
for (const k of keys) {
|
|
52
|
+
if (!numbersEqual(a[k], computed[k]))
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
3
57
|
async function recomputeShipmentAllocation(shipmentId, ctx) {
|
|
4
58
|
const shipmentRef = ctx.db.collection("procurementShipments").doc(shipmentId);
|
|
5
59
|
const shipmentSnap = await shipmentRef.get();
|
|
@@ -34,11 +88,17 @@ async function recomputeShipmentAllocation(shipmentId, ctx) {
|
|
|
34
88
|
for (const lot of lots) {
|
|
35
89
|
const computed = perLot[lot.id];
|
|
36
90
|
const statusChanged = lot.shipmentStatus !== shipment.status;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
91
|
+
/*
|
|
92
|
+
* Same tolerance as the totals check, for the same reason: a lot write fires
|
|
93
|
+
* `onShipmentLotWrite`, which calls straight back into this function. Exact
|
|
94
|
+
* `!==` on a float that has round-tripped through Firestore is a second,
|
|
95
|
+
* independent path to the same infinite cascade.
|
|
96
|
+
*/
|
|
97
|
+
const valuesChanged = !numbersEqual(lot.customsAllocated, computed.customsAllocated) ||
|
|
98
|
+
!numbersEqual(lot.shippingAllocated, computed.shippingAllocated) ||
|
|
99
|
+
!numbersEqual(lot.totalLandedCost, computed.totalLandedCost) ||
|
|
100
|
+
!numbersEqual(lot.projectedRevenue, computed.projectedRevenue) ||
|
|
101
|
+
!numbersEqual(lot.projectedProfit, computed.projectedProfit);
|
|
42
102
|
if (statusChanged || valuesChanged) {
|
|
43
103
|
anyLotChanged = true;
|
|
44
104
|
batch.update(ctx.db.collection("shipmentLots").doc(lot.id), {
|
|
@@ -48,8 +108,15 @@ async function recomputeShipmentAllocation(shipmentId, ctx) {
|
|
|
48
108
|
});
|
|
49
109
|
}
|
|
50
110
|
}
|
|
51
|
-
const totalsUnchanged =
|
|
111
|
+
const totalsUnchanged = totalsEqual(shipment.totals, totals);
|
|
52
112
|
if (!totalsUnchanged) {
|
|
113
|
+
// trigger-self-write-ok: this writes to the collection onShipmentHeaderWrite
|
|
114
|
+
// watches, so it re-triggers itself — ONCE. The second pass recomputes the
|
|
115
|
+
// same `totals` from unchanged inputs, totalsEqual() reports them identical
|
|
116
|
+
// (by key, with a float tolerance — NOT by JSON.stringify, which is key-order
|
|
117
|
+
// sensitive and is what made this loop run 1,017,548 times in 24h), so no
|
|
118
|
+
// write happens and the cascade stops. Pinned by
|
|
119
|
+
// scripts/verify-shipment-allocation-guard.mjs.
|
|
53
120
|
batch.update(shipmentRef, { totals, totalsComputedAt: ctx.now, updatedAt: ctx.now });
|
|
54
121
|
}
|
|
55
122
|
if (anyLotChanged || !totalsUnchanged) {
|
|
@@ -44,6 +44,18 @@ export interface PublicSiteSettings {
|
|
|
44
44
|
actionConfig?: SiteSettingsDocument["actionConfig"];
|
|
45
45
|
background?: SiteSettingsDocument["background"];
|
|
46
46
|
watermark?: SiteSettingsDocument["watermark"];
|
|
47
|
+
/**
|
|
48
|
+
* Routes an admin has switched off. Read by `src/proxy.ts`, which rewrites a
|
|
49
|
+
* disabled path to the 404 page.
|
|
50
|
+
*
|
|
51
|
+
* 🛑 Not optional, and always emitted (defaulted to `[]`). The proxy fetches
|
|
52
|
+
* this over HTTP and fails OPEN on a missing value, so the difference between
|
|
53
|
+
* "no routes disabled" and "the field never arrived" is invisible at runtime —
|
|
54
|
+
* which is exactly how this capability sat dead after Root Cause #82 moved the
|
|
55
|
+
* gate out of the root layout. A key that is always present makes the two
|
|
56
|
+
* distinguishable from the wire alone.
|
|
57
|
+
*/
|
|
58
|
+
disabledRoutes: string[];
|
|
47
59
|
/** Derived, not a stored field — the resolved marker → wordmark → text chain. */
|
|
48
60
|
effectiveWatermark?: MediaVideoWatermark;
|
|
49
61
|
}
|
|
@@ -108,6 +108,35 @@ export interface TesterChecklistItemDocument extends BaseDocument {
|
|
|
108
108
|
* field to mean two different things.
|
|
109
109
|
*/
|
|
110
110
|
endResult?: string;
|
|
111
|
+
/**
|
|
112
|
+
* The case's own author was unsure of its intent — the report flags the verdict
|
|
113
|
+
* so nobody reads it with more confidence than it earned.
|
|
114
|
+
*
|
|
115
|
+
* 🛑 These were UNDECLARED here until 2026-09-13, which is why the flag had
|
|
116
|
+
* never once fired: `fetch-cases.mjs` read them off the document, SKILL.md
|
|
117
|
+
* documented them and `record-verdicts.mjs` had a line to render them, but
|
|
118
|
+
* `group()` did not forward them and this interface did not admit them. Four
|
|
119
|
+
* hops right, two hops missing, and not one error anywhere. Root Cause #38.
|
|
120
|
+
*/
|
|
121
|
+
needsReview?: boolean;
|
|
122
|
+
reviewNote?: string;
|
|
123
|
+
/**
|
|
124
|
+
* The case needs a channel an automated browser does not have — a real email
|
|
125
|
+
* inbox, or an interactive Google account. Always `null` in an automated run,
|
|
126
|
+
* by nature rather than by fault.
|
|
127
|
+
*
|
|
128
|
+
* 🛑 A PERMANENT FLOOR, not a backlog. Unflagged, these sit in the same
|
|
129
|
+
* "blocked" bucket as a case blocked by a genuine defect, so a raw blocked
|
|
130
|
+
* count mixes a constant with a signal and every run re-triages the same
|
|
131
|
+
* thirteen to rediscover none is a bug. Flagged, the report subtracts them.
|
|
132
|
+
*
|
|
133
|
+
* Never use it for a case that is merely hard, flaky or currently failing —
|
|
134
|
+
* that converts a defect into a documented non-test. See the three-kinds-of-
|
|
135
|
+
* null table in docs/TESTING-RIG-NOTES.md.
|
|
136
|
+
*/
|
|
137
|
+
requiresHumanChannel?: boolean;
|
|
138
|
+
/** Which channel is missing, e.g. "real email inbox". Rendered in the report. */
|
|
139
|
+
humanChannelReason?: string;
|
|
111
140
|
href?: string;
|
|
112
141
|
order: number;
|
|
113
142
|
phase: number;
|
|
@@ -155,16 +155,24 @@ export const moneyFlowsPages = [
|
|
|
155
155
|
{
|
|
156
156
|
pageKey: "auction-win-to-payment",
|
|
157
157
|
pageLabel: "Win an auction → get notified → pay",
|
|
158
|
-
href: "/auctions/auction-
|
|
158
|
+
href: "/auctions/auction-money-flows-closing",
|
|
159
159
|
cases: [
|
|
160
160
|
{
|
|
161
161
|
key: "winning-bid-recorded",
|
|
162
162
|
label: "Placing the highest bid on a closing auction records you as the winner",
|
|
163
163
|
roles: ["buyer"],
|
|
164
|
-
|
|
164
|
+
// Batch-scoped fixture, seeded by
|
|
165
|
+
// seed-data/fixtures/money-flows__auction-win-to-payment.mjs and closing
|
|
166
|
+
// ~3 minutes after this batch starts.
|
|
167
|
+
//
|
|
168
|
+
// It used to cite auction-tester-sandbox-cycle-1, which 46 OTHER
|
|
169
|
+
// citations need LIVE — one fixture with two contradictory
|
|
170
|
+
// requirements, so this case could never pass its final step and the
|
|
171
|
+
// rig notes recorded it as unpassable-by-design.
|
|
172
|
+
startPage: "/auctions/auction-money-flows-closing",
|
|
165
173
|
steps: [
|
|
166
174
|
STEP_SIGNIN_BUYER,
|
|
167
|
-
"Open /auctions/auction-
|
|
175
|
+
"Open /auctions/auction-money-flows-closing, which starts at ₹15,000 with a ₹1,000 increment and closes about three minutes from now.",
|
|
168
176
|
"Note the current bid and the end time.",
|
|
169
177
|
"Enter 16000 as the bid amount.",
|
|
170
178
|
"Click 'Place Bid'.",
|
|
@@ -180,7 +188,7 @@ export const moneyFlowsPages = [
|
|
|
180
188
|
roles: ["buyer"],
|
|
181
189
|
startPage: "/user/notifications",
|
|
182
190
|
steps: [
|
|
183
|
-
"Sign in as the buyer who won auction-
|
|
191
|
+
"Sign in as the buyer who won auction-money-flows-closing.",
|
|
184
192
|
"Open the notifications bell in the header.",
|
|
185
193
|
"Find the auction-won notification.",
|
|
186
194
|
"Read its text.",
|
|
@@ -197,7 +205,7 @@ export const moneyFlowsPages = [
|
|
|
197
205
|
roles: ["buyer"],
|
|
198
206
|
startPage: "/user/orders",
|
|
199
207
|
steps: [
|
|
200
|
-
"Sign in as the buyer who won auction-
|
|
208
|
+
"Sign in as the buyer who won auction-money-flows-closing.",
|
|
201
209
|
"Open /user/orders.",
|
|
202
210
|
"Locate the won auction.",
|
|
203
211
|
"Click its payment call to action.",
|
|
@@ -30,4 +30,28 @@ export interface AuthoredCase {
|
|
|
30
30
|
*/
|
|
31
31
|
needsReview?: boolean;
|
|
32
32
|
reviewNote?: string;
|
|
33
|
+
/**
|
|
34
|
+
* The case needs a channel an automated browser does not have — a real email
|
|
35
|
+
* inbox, or an interactive Google account. It is correct, it is worth keeping,
|
|
36
|
+
* and an automated run can never do anything with it but abstain.
|
|
37
|
+
*
|
|
38
|
+
* 🛑 This is a PERMANENT FLOOR, not a backlog. Without the flag those ~13 cases
|
|
39
|
+
* report `null` in every run forever and land in the same "blocked" bucket as a
|
|
40
|
+
* case blocked by a real defect — so a raw blocked count mixes a constant with
|
|
41
|
+
* a signal, and each run re-triages the same thirteen to rediscover that none
|
|
42
|
+
* of them is a bug. Flagged, the report can subtract them and say so.
|
|
43
|
+
*
|
|
44
|
+
* Deliberately distinct from the other two reasons a case yields no verdict —
|
|
45
|
+
* see the three-kinds-of-null table in docs/TESTING-RIG-NOTES.md. A missing
|
|
46
|
+
* fixture is a RIG fault and must be fixed; a case blocked by another defect is
|
|
47
|
+
* a PRODUCT fault and disappears when its cause does. Only this one is a
|
|
48
|
+
* permanent property of running headlessly, and collapsing the three is how a
|
|
49
|
+
* fixable gap hides inside an unfixable one.
|
|
50
|
+
*
|
|
51
|
+
* It must NEVER be used to excuse a case that is merely hard, flaky, or
|
|
52
|
+
* currently failing: that converts a defect into a documented non-test.
|
|
53
|
+
*/
|
|
54
|
+
requiresHumanChannel?: boolean;
|
|
55
|
+
/** Which channel is missing, e.g. "real email inbox" / "interactive Google account". */
|
|
56
|
+
humanChannelReason?: string;
|
|
33
57
|
}
|
|
@@ -148,6 +148,8 @@ export const authored = {
|
|
|
148
148
|
endResult: "All three links reach the same page. Set profile visibility back to Public afterwards, since other cases read this profile as another user.",
|
|
149
149
|
},
|
|
150
150
|
"checklist-account-auth-profile-settings-password-change-reset-link": {
|
|
151
|
+
requiresHumanChannel: true,
|
|
152
|
+
humanChannelReason: "real email inbox",
|
|
151
153
|
roles: ["buyer"],
|
|
152
154
|
startPage: "/user/settings",
|
|
153
155
|
steps: [
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
*/
|
|
29
29
|
export const authored = {
|
|
30
30
|
"checklist-account-auth-signup-login-auth-email-links-single-use": {
|
|
31
|
+
requiresHumanChannel: true,
|
|
32
|
+
humanChannelReason: "real email inbox",
|
|
31
33
|
roles: ["guest"],
|
|
32
34
|
startPage: "/auth/forgot-password",
|
|
33
35
|
steps: [
|
|
@@ -51,6 +53,8 @@ export const authored = {
|
|
|
51
53
|
endResult: "Account state after the second click is identical to after the first. divya.funko@gmail.com is back on TempPass123! by the final step — skipping that restore leaves the account unusable by every later case, since the seeder never resets an existing password.",
|
|
52
54
|
},
|
|
53
55
|
"checklist-account-auth-signup-login-auth-emails-sender-identity-and-inbox": {
|
|
56
|
+
requiresHumanChannel: true,
|
|
57
|
+
humanChannelReason: "real email inbox",
|
|
54
58
|
roles: ["guest"],
|
|
55
59
|
startPage: "/auth/register",
|
|
56
60
|
steps: [
|
|
@@ -92,6 +96,8 @@ export const authored = {
|
|
|
92
96
|
endResult: "Reloading /user shows 'QA Signup Tester' and the address used. The account persists — this case creates a real row that a later teardown does not remove, which is why the address is numbered rather than fixed.",
|
|
93
97
|
},
|
|
94
98
|
"checklist-account-auth-signup-login-email-verify": {
|
|
99
|
+
requiresHumanChannel: true,
|
|
100
|
+
humanChannelReason: "real email inbox",
|
|
95
101
|
roles: ["guest"],
|
|
96
102
|
startPage: "/auth/register",
|
|
97
103
|
steps: [
|
|
@@ -124,6 +130,8 @@ export const authored = {
|
|
|
124
130
|
endResult: "No email arrives for nobody-here-12345@example.com. The two on-screen responses were indistinguishable, which is the whole point — a difference of any kind is an account-enumeration leak.",
|
|
125
131
|
},
|
|
126
132
|
"checklist-account-auth-signup-login-google-link-confirmation-shown": {
|
|
133
|
+
requiresHumanChannel: true,
|
|
134
|
+
humanChannelReason: "interactive Google account",
|
|
127
135
|
roles: ["buyer"],
|
|
128
136
|
startPage: "/user",
|
|
129
137
|
steps: [
|
|
@@ -137,6 +145,8 @@ export const authored = {
|
|
|
137
145
|
endResult: "After reload the green confirmation is still there and the blue prompt has not returned. Needs a Google account to reach — answer null with that reason rather than guessing if none is available.",
|
|
138
146
|
},
|
|
139
147
|
"checklist-account-auth-signup-login-google-link-conflict-rejected": {
|
|
148
|
+
requiresHumanChannel: true,
|
|
149
|
+
humanChannelReason: "interactive Google account",
|
|
140
150
|
roles: ["buyer"],
|
|
141
151
|
startPage: "/user/settings",
|
|
142
152
|
steps: [
|
|
@@ -156,6 +166,8 @@ export const authored = {
|
|
|
156
166
|
endResult: "After reload vivaan.kapoor@gmail.com has no Google link and rehan.sheikh@gmail.com still has G. Unlink G from rehan.sheikh@gmail.com afterwards. Needs a real Google account — answer null if none is available.",
|
|
157
167
|
},
|
|
158
168
|
"checklist-account-auth-signup-login-google-link-different-email": {
|
|
169
|
+
requiresHumanChannel: true,
|
|
170
|
+
humanChannelReason: "interactive Google account",
|
|
159
171
|
roles: ["buyer"],
|
|
160
172
|
startPage: "/user/settings",
|
|
161
173
|
steps: [
|
|
@@ -174,6 +186,8 @@ export const authored = {
|
|
|
174
186
|
endResult: "After reload the link persists and the primary email is still vivaan.kapoor@gmail.com. Unlink afterwards. Needs a second Google account — answer null if none is available.",
|
|
175
187
|
},
|
|
176
188
|
"checklist-account-auth-signup-login-google-link-existing": {
|
|
189
|
+
requiresHumanChannel: true,
|
|
190
|
+
humanChannelReason: "interactive Google account",
|
|
177
191
|
roles: ["guest"],
|
|
178
192
|
startPage: "/auth/register",
|
|
179
193
|
steps: [
|
|
@@ -194,6 +208,8 @@ export const authored = {
|
|
|
194
208
|
endResult: "One account exists for that email, not two, and it kept its wishlist. Needs a Google account whose address you can also register with — answer null if none is available.",
|
|
195
209
|
},
|
|
196
210
|
"checklist-account-auth-signup-login-google-oauth": {
|
|
211
|
+
requiresHumanChannel: true,
|
|
212
|
+
humanChannelReason: "interactive Google account",
|
|
197
213
|
roles: ["guest"],
|
|
198
214
|
startPage: "/auth/login",
|
|
199
215
|
steps: [
|
|
@@ -208,6 +224,8 @@ export const authored = {
|
|
|
208
224
|
endResult: "Reloading /user keeps the session. Needs a Google account — answer null with that reason if none is available.",
|
|
209
225
|
},
|
|
210
226
|
"checklist-account-auth-signup-login-google-popup-blocked-fallback": {
|
|
227
|
+
requiresHumanChannel: true,
|
|
228
|
+
humanChannelReason: "interactive Google account",
|
|
211
229
|
roles: ["guest"],
|
|
212
230
|
startPage: "/auth/login",
|
|
213
231
|
steps: [
|
|
@@ -253,6 +271,8 @@ export const authored = {
|
|
|
253
271
|
endResult: "Navigating to /user lands on /auth/login and no data for Vivaan Kapoor is rendered at any point. A back-button press does not restore the dashboard.",
|
|
254
272
|
},
|
|
255
273
|
"checklist-account-auth-signup-login-password-reset": {
|
|
274
|
+
requiresHumanChannel: true,
|
|
275
|
+
humanChannelReason: "real email inbox",
|
|
256
276
|
roles: ["guest"],
|
|
257
277
|
startPage: "/auth/login",
|
|
258
278
|
steps: [
|
|
@@ -276,6 +296,8 @@ export const authored = {
|
|
|
276
296
|
endResult: "The password is back at TempPass123! by the final step. That restore is mandatory: the seeder never resets an existing password, so skipping it leaves this account permanently signed out of every later case.",
|
|
277
297
|
},
|
|
278
298
|
"checklist-account-auth-signup-login-signup-verification-email-arrives": {
|
|
299
|
+
requiresHumanChannel: true,
|
|
300
|
+
humanChannelReason: "real email inbox",
|
|
279
301
|
roles: ["guest"],
|
|
280
302
|
startPage: "/auth/register",
|
|
281
303
|
steps: [
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Authored six-part procedures for the admin/bug-hunter-rewards page.
|
|
3
|
+
* WHAT: 4 case(s), keyed by full checklist id.
|
|
4
|
+
*
|
|
5
|
+
* 🛑 THE LAST UNAUTHORED PAGE IN THE CATALOGUE. It was the sole remaining entry in
|
|
6
|
+
* R5's `UNAUTHORED_PAGES` ratchet (audit-tester-plugin-wiring.mjs), whose own
|
|
7
|
+
* header says the ratchet exists to be emptied. With this file it is.
|
|
8
|
+
*
|
|
9
|
+
* This page is awkward to test for a reason worth stating: confirming a bug
|
|
10
|
+
* DISABLES the case it was reported against, and credit is awarded to exactly one
|
|
11
|
+
* tester. Both effects are visible to every other tester, and the credit cannot be
|
|
12
|
+
* silently reassigned. So the cases below deliberately operate on the seeded demo
|
|
13
|
+
* fixture pair (v1 disabled + credited, v2 active) rather than on a live report:
|
|
14
|
+
*
|
|
15
|
+
* checklist-admin-bug-hunter-rewards-demo-fixture v1, isActive:false,
|
|
16
|
+
* bugConfirmed, credited to
|
|
17
|
+
* "Mock User 18"
|
|
18
|
+
* checklist-admin-bug-hunter-rewards-demo-fixture-v2 v2, isActive:true
|
|
19
|
+
*
|
|
20
|
+
* That pair exists precisely so this flow can be read without spending a real
|
|
21
|
+
* tester's report on it. Where a case must perform the irreversible half —
|
|
22
|
+
* confirming a bug — it does so against v2, which is the fixture reopened for
|
|
23
|
+
* exactly that purpose.
|
|
24
|
+
*
|
|
25
|
+
* @tag domain:tester
|
|
26
|
+
* @tag layer:seed
|
|
27
|
+
* @tag pattern:none
|
|
28
|
+
* @tag access:server-only
|
|
29
|
+
* @tag consumers:seed-data/authored/index.ts
|
|
30
|
+
* @tag sideEffects:none
|
|
31
|
+
*/
|
|
32
|
+
const SIGN_IN_ADMIN = "Sign in as admin@letitrip.in / TempPass123!.";
|
|
33
|
+
export const authored = {
|
|
34
|
+
"checklist-admin-bug-hunter-rewards-confirm-bug": {
|
|
35
|
+
roles: ["admin"],
|
|
36
|
+
startPage: "/admin/tester-feedback",
|
|
37
|
+
steps: [
|
|
38
|
+
SIGN_IN_ADMIN,
|
|
39
|
+
"Open /admin/tester-feedback and select the Main Issues tab.",
|
|
40
|
+
"Find the row for the case 'Demo fixture — reported bug, already confirmed and reopened (v2, active)' and read the tester name on it.",
|
|
41
|
+
"Click 'Mark as Bug' on that row.",
|
|
42
|
+
"Read the confirmation dialog in full before accepting, then accept it.",
|
|
43
|
+
"Read the badge that appears on the row afterwards.",
|
|
44
|
+
"Open /admin/tester-checklist, set the Status filter to 'All', and read the Status of that same case.",
|
|
45
|
+
],
|
|
46
|
+
inputs: { caseId: "checklist-admin-bug-hunter-rewards-demo-fixture-v2" },
|
|
47
|
+
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.",
|
|
49
|
+
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.",
|
|
51
|
+
},
|
|
52
|
+
"checklist-admin-bug-hunter-rewards-confirm-bug-idempotent": {
|
|
53
|
+
roles: ["admin"],
|
|
54
|
+
startPage: "/admin/tester-feedback",
|
|
55
|
+
steps: [
|
|
56
|
+
SIGN_IN_ADMIN,
|
|
57
|
+
"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.",
|
|
59
|
+
"Read the tester name currently credited on it.",
|
|
60
|
+
"Attempt 'Mark as Bug' on that row.",
|
|
61
|
+
"Read whatever the screen says in response.",
|
|
62
|
+
"Read the credited tester name again and compare it against the one noted earlier.",
|
|
63
|
+
],
|
|
64
|
+
inputs: { caseId: "checklist-admin-bug-hunter-rewards-demo-fixture", creditedTo: "Mock User 18" },
|
|
65
|
+
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.",
|
|
69
|
+
},
|
|
70
|
+
"checklist-admin-bug-hunter-rewards-reopen-case": {
|
|
71
|
+
roles: ["admin"],
|
|
72
|
+
startPage: "/admin/tester-checklist",
|
|
73
|
+
steps: [
|
|
74
|
+
SIGN_IN_ADMIN,
|
|
75
|
+
"Open /admin/tester-checklist and set the Status filter to 'Inactive' (or set Bug status to 'Bug Confirmed').",
|
|
76
|
+
"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.",
|
|
81
|
+
],
|
|
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.",
|
|
87
|
+
},
|
|
88
|
+
"checklist-admin-bug-hunter-rewards-catalog-default-active-filter": {
|
|
89
|
+
roles: ["admin"],
|
|
90
|
+
startPage: "/admin/tester-checklist",
|
|
91
|
+
steps: [
|
|
92
|
+
SIGN_IN_ADMIN,
|
|
93
|
+
"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.",
|
|
95
|
+
"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.",
|
|
97
|
+
"Set Status back to its default and set 'Bug status' to 'Bug Confirmed'.",
|
|
98
|
+
"Read which rows appear under that filter.",
|
|
99
|
+
],
|
|
100
|
+
inputs: { search: "Demo fixture — reported bug" },
|
|
101
|
+
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.",
|
|
103
|
+
expectedData: { defaultViewHidesBugConfirmed: true },
|
|
104
|
+
endResult: "Read-only — no case is modified.",
|
|
105
|
+
},
|
|
106
|
+
};
|
|
@@ -211,6 +211,8 @@ export const authored = {
|
|
|
211
211
|
endResult: "Read-only; send nothing.",
|
|
212
212
|
},
|
|
213
213
|
"checklist-admin-site-system-daily-digest-email-content": {
|
|
214
|
+
requiresHumanChannel: true,
|
|
215
|
+
humanChannelReason: "real email inbox",
|
|
214
216
|
roles: ["admin"],
|
|
215
217
|
startPage: "/admin/site",
|
|
216
218
|
steps: [
|
|
@@ -239,6 +239,8 @@ export const authored = {
|
|
|
239
239
|
endResult: "Read-only. If no rejected order exists, answer null rather than creating one — rejecting as fraud bans a real account for seven days.",
|
|
240
240
|
},
|
|
241
241
|
"checklist-buying-my-orders-order-lifecycle-emails-arrive": {
|
|
242
|
+
requiresHumanChannel: true,
|
|
243
|
+
humanChannelReason: "real email inbox",
|
|
242
244
|
roles: ["buyer"],
|
|
243
245
|
startPage: "/user/orders",
|
|
244
246
|
steps: [
|
|
@@ -384,6 +384,8 @@ export const authored = {
|
|
|
384
384
|
endResult: "All four listings keep their type-specific fields. Restore any title that was changed.",
|
|
385
385
|
},
|
|
386
386
|
"checklist-buying-offers-notification-email-is-a-real-email": {
|
|
387
|
+
requiresHumanChannel: true,
|
|
388
|
+
humanChannelReason: "real email inbox",
|
|
387
389
|
roles: ["buyer"],
|
|
388
390
|
startPage: "/user/settings",
|
|
389
391
|
steps: [
|
|
@@ -96,6 +96,8 @@ export const authored = {
|
|
|
96
96
|
endResult: "Read-only; nothing persists beyond the URL.",
|
|
97
97
|
},
|
|
98
98
|
"checklist-content-discovery-notifications-notification-email-actually-arrives": {
|
|
99
|
+
requiresHumanChannel: true,
|
|
100
|
+
humanChannelReason: "real email inbox",
|
|
99
101
|
roles: ["buyer"],
|
|
100
102
|
startPage: "/user/settings",
|
|
101
103
|
steps: [
|