@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.
Files changed (31) hide show
  1. package/dist/_internal/server/features/site-settings/adapters.d.ts +2 -2
  2. package/dist/_internal/server/features/site-settings/adapters.js +25 -1
  3. package/dist/_internal/server/jobs/handlers/onShipmentAllocationSync.js +73 -6
  4. package/dist/_internal/shared/features/site-settings/types.d.ts +12 -0
  5. package/dist/features/tester/schemas/firestore.d.ts +29 -0
  6. package/dist/features/tester/seed-data/_money-flows.js +13 -5
  7. package/dist/features/tester/seed-data/authored/_types.d.ts +24 -0
  8. package/dist/features/tester/seed-data/authored/account-auth__profile-settings.js +2 -0
  9. package/dist/features/tester/seed-data/authored/account-auth__signup-login.js +22 -0
  10. package/dist/features/tester/seed-data/authored/admin__bug-hunter-rewards.d.ts +2 -0
  11. package/dist/features/tester/seed-data/authored/admin__bug-hunter-rewards.js +106 -0
  12. package/dist/features/tester/seed-data/authored/admin__site-system.js +2 -0
  13. package/dist/features/tester/seed-data/authored/buying__my-orders.js +2 -0
  14. package/dist/features/tester/seed-data/authored/buying__offers.js +2 -0
  15. package/dist/features/tester/seed-data/authored/content-discovery__notifications.js +2 -0
  16. package/dist/features/tester/seed-data/authored/content-discovery__search.js +48 -19
  17. package/dist/features/tester/seed-data/authored/index.js +6 -0
  18. package/dist/features/tester/seed-data/authored/public-pages__auth-error-pages.js +4 -0
  19. package/dist/features/tester/seed-data/authored/selling__listing-a-product.js +0 -16
  20. package/dist/features/tester/seed-data/authored/selling__product-upload-details.d.ts +2 -0
  21. package/dist/features/tester/seed-data/authored/selling__product-upload-details.js +150 -0
  22. package/dist/features/tester/seed-data/authored/selling__quick-add-minimum-details.d.ts +2 -0
  23. package/dist/features/tester/seed-data/authored/selling__quick-add-minimum-details.js +154 -0
  24. package/dist/features/tester/seed-data/authored/selling__seller-orders.js +2 -0
  25. package/dist/features/tester/seed-data/index.d.ts +1 -0
  26. package/dist/features/tester/seed-data/index.js +1 -0
  27. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +111 -6
  28. package/dist/features/tester/seed-data/tester-responses-seed-data.d.ts +2 -0
  29. package/dist/features/tester/seed-data/tester-responses-seed-data.js +95 -0
  30. package/package.json +1 -1
  31. 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", "disabledRoutes", "siteName", "tagline", "motto", "logo", "favicon", "seo", "socialLinks", "features", "shipping", "returns", "faq", "aboutContent", "navbarConfig", "footerConfig", "theme", "featuredResults", "messaging"];
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
- const valuesChanged = lot.customsAllocated !== computed.customsAllocated ||
38
- lot.shippingAllocated !== computed.shippingAllocated ||
39
- lot.totalLandedCost !== computed.totalLandedCost ||
40
- lot.projectedRevenue !== computed.projectedRevenue ||
41
- lot.projectedProfit !== computed.projectedProfit;
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 = JSON.stringify(shipment.totals ?? {}) === JSON.stringify(totals);
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-tester-sandbox-cycle-1",
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
- startPage: "/auctions/auction-tester-sandbox-cycle-1",
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-tester-sandbox-cycle-1, which starts at ₹15,000 with a ₹1,000 increment.",
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-tester-sandbox-cycle-1.",
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-tester-sandbox-cycle-1.",
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,2 @@
1
+ import type { AuthoredCase } from "./_types";
2
+ export declare const authored: Record<string, AuthoredCase>;
@@ -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: [