create-ailk 0.20.0 → 0.22.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 (222) hide show
  1. package/README.md +21 -1
  2. package/component-catalog.json +220 -8
  3. package/component-catalog.md +21 -9
  4. package/dist/add-module.js +9 -5
  5. package/dist/apply-design.d.ts +9 -0
  6. package/dist/apply-design.js +78 -31
  7. package/dist/derive-architecture.d.ts +8 -1
  8. package/dist/derive-architecture.js +38 -3
  9. package/dist/design-home.d.ts +54 -11
  10. package/dist/design-home.js +160 -22
  11. package/dist/design-package.js +7 -2
  12. package/dist/lib/design-checks.d.ts +6 -1
  13. package/dist/lib/design-checks.js +23 -2
  14. package/dist/lib/env-catalog.js +5 -1
  15. package/dist/module-architecture.js +4 -2
  16. package/dist/programmatic.js +63 -25
  17. package/dist/site-chrome.d.ts +8 -0
  18. package/dist/site-chrome.js +17 -1
  19. package/dist/site-roster.d.ts +4 -2
  20. package/dist/site-roster.js +7 -5
  21. package/dist/spoke-checkout.d.ts +39 -18
  22. package/dist/spoke-checkout.js +177 -236
  23. package/dist/starter-grid.js +16 -0
  24. package/dist/surfaces.js +30 -14
  25. package/dist/sync-routes.d.ts +28 -0
  26. package/dist/sync-routes.js +124 -1
  27. package/dist/template-record.d.ts +4 -3
  28. package/dist/template-record.js +6 -3
  29. package/dist/topology.js +25 -1
  30. package/dist/update.d.ts +6 -1
  31. package/dist/update.js +125 -18
  32. package/package.json +3 -3
  33. package/site-templates/_base/ailk-template.json +2 -2
  34. package/site-templates/_base/content/en/WorkingAgreementPage/working-agreement.mdx +78 -0
  35. package/site-templates/designs/minimalism/DESIGN.md +1 -1
  36. package/site-templates/designs/minimalism/contact/local-business-service.mdx +23 -0
  37. package/site-templates/designs/minimalism/contact/organization-product.mdx +23 -0
  38. package/site-templates/designs/minimalism/contact/organization-service.mdx +23 -0
  39. package/site-templates/designs/minimalism/contact/person-service.mdx +23 -0
  40. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +18 -1
  41. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +11 -0
  42. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +18 -1
  43. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +34 -23
  44. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +18 -1
  45. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +33 -23
  46. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +18 -1
  47. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +33 -23
  48. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +18 -1
  49. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +31 -21
  50. package/templates/README.md +26 -25
  51. package/templates/apps/api/CLAUDE.md +20 -7
  52. package/templates/apps/api/jest.config.cjs +1 -0
  53. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +19 -0
  54. package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +149 -0
  55. package/templates/apps/api/src/lib/consent-store.ts +28 -0
  56. package/templates/apps/api/src/lib/flow-completion-store.ts +136 -0
  57. package/templates/apps/api/src/lib/hub-sign-in-store.ts +38 -2
  58. package/templates/apps/api/src/lib/proposal-paid-store.ts +21 -4
  59. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +14 -3
  60. package/templates/apps/api/src/middleware/auth.ts +13 -6
  61. package/templates/apps/api/src/openapi/spec.ts +30 -3
  62. package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +49 -0
  63. package/templates/apps/api/src/routes/auth/hub-sign-in.ts +41 -0
  64. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +386 -0
  65. package/templates/apps/api/src/routes/consent/account.ts +266 -0
  66. package/templates/apps/api/src/routes/consent/index.ts +15 -7
  67. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +36 -0
  68. package/templates/apps/api/src/routes/content/get-page.ts +4 -0
  69. package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +515 -0
  70. package/templates/apps/api/src/routes/leads/index.ts +161 -7
  71. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +26 -0
  72. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +649 -0
  73. package/templates/apps/api/src/routes/webhooks/stripe.ts +154 -19
  74. package/templates/apps/api/src/server.ts +28 -0
  75. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +1038 -118
  76. package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +389 -0
  77. package/templates/apps/api/src/services/checkout-buyer.ts +100 -4
  78. package/templates/apps/api/src/services/consent.ts +95 -0
  79. package/templates/apps/api/src/services/lead-routing.ts +337 -8
  80. package/templates/apps/api/src/services/magnet-completion.ts +242 -0
  81. package/templates/apps/api/src/services/person.ts +40 -8
  82. package/templates/apps/api/src/services/plan-offerings.ts +59 -0
  83. package/templates/apps/api/src/services/project-listings.ts +6 -1
  84. package/templates/apps/api/src/services/public-site-resolver.ts +8 -1
  85. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +69 -61
  86. package/templates/apps/mcp/jest.config.cjs +1 -0
  87. package/templates/apps/web/.env.example +14 -0
  88. package/templates/apps/web/CLAUDE.md +139 -3
  89. package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +6 -2
  90. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +5 -4
  91. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +5 -1
  92. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +2 -0
  93. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +3 -14
  94. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +2 -0
  95. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +5 -1
  96. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +160 -8
  97. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +12 -9
  98. package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +105 -2
  99. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +36 -3
  100. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +47 -1
  101. package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +60 -22
  102. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +16 -8
  103. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +85 -31
  104. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +18 -4
  105. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +3 -1
  106. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +7 -4
  107. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +227 -207
  108. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +17 -1
  109. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +143 -1
  110. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +4 -0
  111. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +385 -89
  112. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +5 -0
  113. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +44 -5
  114. package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +68 -34
  115. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +0 -8
  116. package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +39 -151
  117. package/templates/apps/web/app/[locale]/(authed)/account/agreements-gate.ts +61 -0
  118. package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +143 -140
  119. package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +40 -6
  120. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +68 -54
  121. package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +5 -1
  122. package/templates/apps/web/app/[locale]/about/[slug]/page.tsx +46 -0
  123. package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +350 -0
  124. package/templates/apps/web/app/[locale]/about/page.tsx +83 -1
  125. package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +293 -0
  126. package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +115 -0
  127. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +146 -1
  128. package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +310 -0
  129. package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +143 -0
  130. package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +350 -0
  131. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +29 -3
  132. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
  133. package/templates/apps/web/app/[locale]/checkout/__tests__/pages.test.tsx +139 -0
  134. package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +136 -0
  135. package/templates/apps/web/app/[locale]/checkout/thank-you/page.tsx +114 -0
  136. package/templates/apps/web/app/[locale]/checkout/unavailable/page.tsx +51 -0
  137. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
  138. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +24 -1
  139. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +79 -4
  140. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-magnet.test.ts +138 -0
  141. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +17 -0
  142. package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +386 -0
  143. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +504 -24
  144. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +12 -3
  145. package/templates/apps/web/app/[locale]/contact/contact-form-magnet.ts +77 -0
  146. package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +2 -0
  147. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +184 -41
  148. package/templates/apps/web/app/[locale]/contact/intake-script.ts +26 -0
  149. package/templates/apps/web/app/[locale]/contact/page.tsx +120 -16
  150. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
  151. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +1 -1
  152. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +2 -2
  153. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +106 -1
  154. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.invite.test.tsx +257 -0
  155. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/account-invite.actions.test.ts +69 -0
  156. package/templates/apps/web/app/[locale]/flows/[slug]/account-invite.actions.ts +35 -0
  157. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +1 -1
  158. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  159. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
  160. package/templates/apps/web/app/[locale]/layout.tsx +23 -2
  161. package/templates/apps/web/app/[locale]/page.tsx +13 -1
  162. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
  163. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
  164. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +2 -2
  165. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
  166. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  167. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  168. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
  169. package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +89 -0
  170. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +104 -0
  171. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +38 -0
  172. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +9 -0
  173. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  174. package/templates/apps/web/app/[locale]/working-agreement/__tests__/page.test.tsx +63 -0
  175. package/templates/apps/web/app/[locale]/working-agreement/page.tsx +41 -0
  176. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +114 -0
  177. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +20 -3
  178. package/templates/apps/web/app/api/checkout/[offering]/route.ts +48 -0
  179. package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +120 -0
  180. package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +86 -0
  181. package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +44 -1
  182. package/templates/apps/web/app/api/hub-session/sign-in/route.ts +22 -3
  183. package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +92 -0
  184. package/templates/apps/web/app/api/webhooks/stripe/route.ts +31 -0
  185. package/templates/apps/web/auth.ts +28 -2
  186. package/templates/apps/web/jest.config.cjs +1 -0
  187. package/templates/apps/web/lib/__tests__/account-invite.test.ts +63 -0
  188. package/templates/apps/web/lib/__tests__/checkout.test.ts +243 -0
  189. package/templates/apps/web/lib/__tests__/consent-policies.test.ts +23 -0
  190. package/templates/apps/web/lib/__tests__/deck-page.test.ts +249 -0
  191. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -19
  192. package/templates/apps/web/lib/account-invite.ts +37 -0
  193. package/templates/apps/web/lib/checkout-config.ts +27 -0
  194. package/templates/apps/web/lib/checkout.ts +93 -0
  195. package/templates/apps/web/lib/consent-policies.ts +22 -0
  196. package/templates/apps/web/lib/deck-page.ts +192 -0
  197. package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +111 -0
  198. package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +88 -32
  199. package/templates/apps/web/lib/prisma-for-auth.ts +8 -0
  200. package/templates/apps/web/lib/sign-up-agreements.ts +447 -0
  201. package/templates/apps/web/lib/site-chrome.ts +2 -0
  202. package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
  203. package/templates/apps/web/messages/en.json +40 -12
  204. package/templates/apps/web/package.json +1 -0
  205. package/templates/apps/web/src/lib/__tests__/lead-write.test.ts +187 -0
  206. package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -0
  207. package/templates/apps/web/src/lib/lead-write.ts +90 -0
  208. package/templates/apps/web/src/lib/rate-limit.ts +79 -0
  209. package/templates/database/CHANGELOG.md +47 -0
  210. package/templates/database/__tests__/consent-source-checkout-migration.test.ts +79 -0
  211. package/templates/database/__tests__/flow-completion-migration.test.ts +115 -0
  212. package/templates/database/__tests__/flow-completion.db.test.ts +136 -0
  213. package/templates/database/__tests__/seed.test.ts +72 -2
  214. package/templates/database/inbox/schema.prisma +17 -2
  215. package/templates/database/jest.config.cjs +1 -0
  216. package/templates/database/migrations/20261018120000_flow_completion/migration.sql +52 -0
  217. package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +27 -0
  218. package/templates/database/package.json +1 -1
  219. package/templates/database/seed.ts +60 -0
  220. package/templates/tools/jest/pin-timezone.cjs +34 -0
  221. package/versions.json +34 -34
  222. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +0 -125
@@ -23,8 +23,11 @@
23
23
  * unchanged (the caller's own credential); 404 and an unverified 401/403
24
24
  * load `[]`, any other failure rejects (D12). On a cookie-session site
25
25
  * they call with the cookie too — no gate (#8231 D7).
26
+ * 8. #8765 D8 — the Inbox renders nothing about agreements: the card is
27
+ * retired, whatever `member.consents` answers (the layout's interstitial
28
+ * is the one place an agreement is asked).
26
29
  */
27
- import { fireEvent, render, screen, within } from "@testing-library/react";
30
+ import { act, fireEvent, render, screen, within } from "@testing-library/react";
28
31
 
29
32
  // The global setup mock resolves only top-level namespace keys against
30
33
  // en.json; the area reads nested keys (`inbox.tabs.all`, …) — a call
@@ -34,13 +37,20 @@ import { fireEvent, render, screen, within } from "@testing-library/react";
34
37
  jest.mock("next-intl/server", () => {
35
38
  const msgs: unknown = require("../../../../../messages/en.json");
36
39
  const dig = (obj: unknown, path: string): unknown =>
37
- path.split(".").reduce<unknown>(
38
- (acc, k) => (acc !== null && typeof acc === "object" ? (acc as Record<string, unknown>)[k] : undefined),
39
- obj,
40
- );
40
+ path
41
+ .split(".")
42
+ .reduce<unknown>(
43
+ (acc, k) =>
44
+ acc !== null && typeof acc === "object"
45
+ ? (acc as Record<string, unknown>)[k]
46
+ : undefined,
47
+ obj,
48
+ );
41
49
  return {
42
50
  __esModule: true,
43
- getTranslations: async (arg: string | { namespace?: string; locale?: string }) => {
51
+ getTranslations: async (
52
+ arg: string | { namespace?: string; locale?: string },
53
+ ) => {
44
54
  const ns = typeof arg === "string" ? arg : (arg.namespace ?? "");
45
55
  const messages = dig(msgs, ns);
46
56
  return (key: string, values?: Record<string, unknown>): string => {
@@ -64,7 +74,9 @@ const mockGetSession = jest.fn();
64
74
  // eslint-disable-next-line no-restricted-syntax -- #507: mock-internal rewrite pending; auth.ts's lazy Better-Auth singleton has no HTTP/DB boundary a unit test of the session fields can mock without standing up Prisma.
65
75
  jest.mock("../../../../../auth", () => ({
66
76
  __esModule: true,
67
- auth: { api: { getSession: (...args: unknown[]) => mockGetSession(...args) } },
77
+ auth: {
78
+ api: { getSession: (...args: unknown[]) => mockGetSession(...args) },
79
+ },
68
80
  }));
69
81
 
70
82
  const mockSignupMe = jest.fn();
@@ -73,8 +85,14 @@ const mockMemberPieces = jest.fn();
73
85
  const mockMemberMessages = jest.fn();
74
86
  const mockMemberPurchases = jest.fn();
75
87
  const mockMemberAppointments = jest.fn();
88
+ const mockMemberConsents = jest.fn();
89
+ const mockAccountAgreements = jest.fn();
76
90
  jest.mock("@working-theory/api-client", () => ({
77
91
  __esModule: true,
92
+ // #8765 D7 — the account-level agreements the layout and the page share.
93
+ consent: {
94
+ accountAgreements: (...args: unknown[]) => mockAccountAgreements(...args),
95
+ },
78
96
  waitlistSignups: { me: (...args: unknown[]) => mockSignupMe(...args) },
79
97
  subscription: { me: (...args: unknown[]) => mockSubscriptionMe(...args) },
80
98
  member: {
@@ -82,12 +100,22 @@ jest.mock("@working-theory/api-client", () => ({
82
100
  messages: (...args: unknown[]) => mockMemberMessages(...args),
83
101
  purchases: (...args: unknown[]) => mockMemberPurchases(...args),
84
102
  appointments: (...args: unknown[]) => mockMemberAppointments(...args),
103
+ // #8765 D8 — the retired card's read; the page must not depend on it.
104
+ consents: (...args: unknown[]) => mockMemberConsents(...args),
85
105
  // #8233 C4 — the New menu's availability reads; 404 here, so the Inbox
86
106
  // cases see no New action and no error row.
87
107
  meetingTypes: () =>
88
- Promise.resolve({ ok: false, status: 404, body: { ok: false, error: { code: "not_found", message: "Not found" } } }),
108
+ Promise.resolve({
109
+ ok: false,
110
+ status: 404,
111
+ body: { ok: false, error: { code: "not_found", message: "Not found" } },
112
+ }),
89
113
  requestForm: () =>
90
- Promise.resolve({ ok: false, status: 404, body: { ok: false, error: { code: "not_found", message: "Not found" } } }),
114
+ Promise.resolve({
115
+ ok: false,
116
+ status: 404,
117
+ body: { ok: false, error: { code: "not_found", message: "Not found" } },
118
+ }),
91
119
  },
92
120
  }));
93
121
 
@@ -142,14 +170,25 @@ const PURCHASE = {
142
170
  };
143
171
 
144
172
  function appointment(id: string, title: string, startTime: string) {
145
- return { id, title, startTime, endTime: startTime, status: "confirmed" as const };
173
+ return {
174
+ id,
175
+ title,
176
+ startTime,
177
+ endTime: startTime,
178
+ status: "confirmed" as const,
179
+ };
146
180
  }
147
181
 
148
182
  const NO_APPOINTMENTS = { ok: true, body: { upcoming: [], past: [] } };
149
183
 
150
184
  const NO_SUBSCRIPTION = {
151
185
  ok: true,
152
- body: { actionRequired: false, status: null, accessRevokesAt: null, portalAvailable: false },
186
+ body: {
187
+ actionRequired: false,
188
+ status: null,
189
+ accessRevokesAt: null,
190
+ portalAvailable: false,
191
+ },
153
192
  };
154
193
 
155
194
  const PIECE = {
@@ -189,9 +228,15 @@ const MESSAGE_WITH_FILE = {
189
228
  };
190
229
 
191
230
  /** The hub credential a spoke signed in through its hub forwards (#7894 D12). */
192
- const BEARER = { headers: { authorization: `Bearer ailk_hst_${"t".repeat(43)}` } };
231
+ const BEARER = {
232
+ headers: { authorization: `Bearer ailk_hst_${"t".repeat(43)}` },
233
+ };
193
234
 
194
- const notFound = { ok: false, status: 404, body: { ok: false, error: { code: "not_found", message: "Not found" } } };
235
+ const notFound = {
236
+ ok: false,
237
+ status: 404,
238
+ body: { ok: false, error: { code: "not_found", message: "Not found" } },
239
+ };
195
240
 
196
241
  /** The context a hub-mode spoke hands every contribution, signed in. */
197
242
  async function memberCtx(
@@ -200,10 +245,10 @@ async function memberCtx(
200
245
  const locale = overrides.locale ?? "en";
201
246
  // The contract's translator takes any key (account-data.ts's reasoning):
202
247
  // forward through the mock's runtime signature.
203
- const translate = (await getTranslations({ locale, namespace: "account" })) as (
204
- key: string,
205
- values?: Record<string, unknown>,
206
- ) => string;
248
+ const translate = (await getTranslations({
249
+ locale,
250
+ namespace: "account",
251
+ })) as (key: string, values?: Record<string, unknown>) => string;
207
252
  const t: AccountTranslate = (key, values) => translate(key, values);
208
253
  return {
209
254
  locale,
@@ -221,7 +266,9 @@ function kindOf(id: string): (typeof accountInboxKinds)[number] {
221
266
  }
222
267
 
223
268
  function needsClientSource(): (typeof accountForYouSources)[number] {
224
- const source = accountForYouSources.find((s) => s.id === "piece-needs-client");
269
+ const source = accountForYouSources.find(
270
+ (s) => s.id === "piece-needs-client",
271
+ );
225
272
  if (!source) throw new Error("no piece-needs-client source");
226
273
  return source;
227
274
  }
@@ -238,7 +285,12 @@ const UNVERIFIED_AND_PAST_DUE = (): void => {
238
285
  });
239
286
  mockSubscriptionMe.mockResolvedValue({
240
287
  ok: true,
241
- body: { actionRequired: true, status: "past_due", accessRevokesAt: null, portalAvailable: true },
288
+ body: {
289
+ actionRequired: true,
290
+ status: "past_due",
291
+ accessRevokesAt: null,
292
+ portalAvailable: true,
293
+ },
242
294
  });
243
295
  };
244
296
 
@@ -248,24 +300,154 @@ beforeEach(() => {
248
300
  mockGetSession.mockReset().mockResolvedValue(SESSION);
249
301
  mockSignupMe.mockReset().mockResolvedValue({ ok: true, body: SIGNUP });
250
302
  mockSubscriptionMe.mockReset().mockResolvedValue(NO_SUBSCRIPTION);
251
- mockMemberPurchases.mockReset().mockResolvedValue({ ok: true, body: { items: [PURCHASE] } });
303
+ mockMemberPurchases
304
+ .mockReset()
305
+ .mockResolvedValue({ ok: true, body: { items: [PURCHASE] } });
252
306
  mockMemberAppointments.mockReset().mockResolvedValue(NO_APPOINTMENTS);
253
307
  // The page's pieces and messages answer 404 unless a case fills them.
254
308
  mockMemberPieces.mockReset().mockResolvedValue(notFound);
255
309
  mockMemberMessages.mockReset().mockResolvedValue(notFound);
310
+ mockAccountAgreements
311
+ .mockReset()
312
+ .mockResolvedValue({ ok: true, body: { agreements: [], complete: true } });
256
313
  mockRefresh.mockReset();
257
314
  });
258
315
 
259
316
  /** The member kinds' own cases read rows from both member reads. */
260
317
  function memberRows(): void {
261
- mockMemberPieces.mockResolvedValue({ ok: true, body: { items: [PIECE, PIECE_NEEDS_CLIENT] } });
262
- mockMemberMessages.mockResolvedValue({ ok: true, body: { items: [MESSAGE, MESSAGE_WITH_FILE] } });
318
+ mockMemberPieces.mockResolvedValue({
319
+ ok: true,
320
+ body: { items: [PIECE, PIECE_NEEDS_CLIENT] },
321
+ });
322
+ mockMemberMessages.mockResolvedValue({
323
+ ok: true,
324
+ body: { items: [MESSAGE, MESSAGE_WITH_FILE] },
325
+ });
263
326
  }
264
327
 
265
328
  afterAll(() => {
266
329
  accountExperimentKeys.length = 0;
267
330
  });
268
331
 
332
+ describe("no agreements on the Inbox (#8765 D8, AC-3, AC-4)", () => {
333
+ /** A `signUp` agreement the old card would have shown: no grant, or one at an older version. */
334
+ function missingAgreement(
335
+ state: null | { granted: boolean; policyVersion: number },
336
+ ) {
337
+ return {
338
+ id: "account_terms",
339
+ kind: "agreement",
340
+ area: null,
341
+ required: true,
342
+ signUp: true,
343
+ description: "Account terms.",
344
+ statement: "I accept the [Terms](terms).",
345
+ policyHref: "/working-agreement",
346
+ policyVersion: 2,
347
+ state: state && {
348
+ ...state,
349
+ at: "2026-09-01T00:00:00.000Z",
350
+ source: "sign_up",
351
+ },
352
+ effective: false,
353
+ };
354
+ }
355
+
356
+ it.each([
357
+ ["no grant", { ok: true, body: { purposes: [missingAgreement(null)] } }],
358
+ [
359
+ "an older version",
360
+ {
361
+ ok: true,
362
+ body: {
363
+ purposes: [missingAgreement({ granted: true, policyVersion: 1 })],
364
+ },
365
+ },
366
+ ],
367
+ ["a refusal", { ok: false, status: 503, body: {} }],
368
+ ])(
369
+ "with member.consents answering %s, no account-agreements element renders",
370
+ async (_label, answer) => {
371
+ mockMemberConsents.mockResolvedValue(answer);
372
+ await renderPage();
373
+ // Let any mount-time effect settle — the retired card appeared only after one.
374
+ await act(async () => {
375
+ for (let i = 0; i < 10; i += 1)
376
+ await new Promise((resolve) => setTimeout(resolve, 0));
377
+ });
378
+ expect(screen.getByTestId("account-inbox")).toBeInTheDocument();
379
+ expect(
380
+ document.querySelector('[data-testid^="account-agreements"]'),
381
+ ).toBeNull();
382
+ },
383
+ );
384
+ });
385
+
386
+ describe("no Inbox loader runs behind the interstitial (#8765 D7)", () => {
387
+ const OUTSTANDING = {
388
+ ok: true,
389
+ body: {
390
+ agreements: [
391
+ {
392
+ id: "account_terms",
393
+ statement: "I accept the [Terms](terms).",
394
+ policyVersion: 2,
395
+ acceptedVersion: null,
396
+ acceptedAt: null,
397
+ current: false,
398
+ },
399
+ ],
400
+ complete: false,
401
+ },
402
+ };
403
+
404
+ const inboxLoaders = () => [
405
+ mockMemberPieces,
406
+ mockMemberMessages,
407
+ mockMemberPurchases,
408
+ mockMemberAppointments,
409
+ mockSubscriptionMe,
410
+ mockSignupMe,
411
+ ];
412
+
413
+ it.each([
414
+ [
415
+ "complete: false",
416
+ () => mockAccountAgreements.mockResolvedValue(OUTSTANDING),
417
+ ],
418
+ [
419
+ "a 503 (fails closed)",
420
+ () =>
421
+ mockAccountAgreements.mockResolvedValue({
422
+ ok: false,
423
+ status: 503,
424
+ body: {},
425
+ }),
426
+ ],
427
+ [
428
+ "a thrown read (fails closed)",
429
+ () => mockAccountAgreements.mockRejectedValue(new Error("down")),
430
+ ],
431
+ ])(
432
+ "with %s, the page renders nothing and calls no Inbox loader",
433
+ async (_label, arrange) => {
434
+ arrange();
435
+ const element = await Page({ params: Promise.resolve({ locale: "en" }) });
436
+ expect(element).toBeNull();
437
+ // member.messages stamps readAt on its first read: it must never run here.
438
+ for (const loader of inboxLoaders())
439
+ expect(loader).not.toHaveBeenCalled();
440
+ },
441
+ );
442
+
443
+ it("with an older hub's 404, the Inbox loads as before", async () => {
444
+ mockAccountAgreements.mockResolvedValue(notFound);
445
+ await renderPage();
446
+ expect(screen.getByTestId("account-inbox")).toBeInTheDocument();
447
+ expect(mockMemberMessages).toHaveBeenCalled();
448
+ });
449
+ });
450
+
269
451
  describe("the Inbox — tabs All · Purchases · Scheduled (#8231 D3, AC-3)", () => {
270
452
  /** The rows of one tab, by title, after selecting it. */
271
453
  function tabRows(id: string): string[] {
@@ -278,23 +460,42 @@ describe("the Inbox — tabs All · Purchases · Scheduled (#8231 D3, AC-3)", ()
278
460
  it("renders All · Purchases · Scheduled with counts, All selected — no Recents or Suggested", async () => {
279
461
  accountExperimentKeys.push("ailk-founder-diagnostic");
280
462
  mockMemberPieces.mockResolvedValue({ ok: true, body: { items: [PIECE] } });
281
- mockMemberMessages.mockResolvedValue({ ok: true, body: { items: [MESSAGE] } });
463
+ mockMemberMessages.mockResolvedValue({
464
+ ok: true,
465
+ body: { items: [MESSAGE] },
466
+ });
282
467
  mockMemberAppointments.mockResolvedValue({
283
468
  ok: true,
284
- body: { upcoming: [appointment("appt_1", "Kickoff call", "2026-09-10T15:00:00.000Z")], past: [] },
469
+ body: {
470
+ upcoming: [
471
+ appointment("appt_1", "Kickoff call", "2026-09-10T15:00:00.000Z"),
472
+ ],
473
+ past: [],
474
+ },
285
475
  });
286
476
  await renderPage();
287
- expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Inbox");
477
+ expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
478
+ "Inbox",
479
+ );
288
480
  const tabs = screen.getAllByRole("tab");
289
481
  expect(tabs.map((tab) => tab.getAttribute("data-testid"))).toEqual([
290
482
  "inbox-tab-all",
291
483
  "inbox-tab-purchases",
292
484
  "inbox-tab-scheduled",
293
485
  ]);
294
- expect(tabs.map((tab) => tab.textContent)).toEqual(["All5", "Purchases1", "Scheduled1"]);
295
- expect(screen.getByTestId("inbox-tab-all")).toHaveAttribute("aria-selected", "true");
486
+ expect(tabs.map((tab) => tab.textContent)).toEqual([
487
+ "All5",
488
+ "Purchases1",
489
+ "Scheduled1",
490
+ ]);
491
+ expect(screen.getByTestId("inbox-tab-all")).toHaveAttribute(
492
+ "aria-selected",
493
+ "true",
494
+ );
296
495
  // All: every row, newest first.
297
- const all = within(screen.getByTestId("inbox-all")).getAllByTestId("inbox-all-row");
496
+ const all = within(screen.getByTestId("inbox-all")).getAllByTestId(
497
+ "inbox-all-row",
498
+ );
298
499
  expect(all.map((row) => row.textContent)).toEqual([
299
500
  expect.stringContaining("Kickoff call"),
300
501
  expect.stringContaining("Kickoff notes"),
@@ -304,9 +505,13 @@ describe("the Inbox — tabs All · Purchases · Scheduled (#8231 D3, AC-3)", ()
304
505
  ]);
305
506
  // Purchases: the purchase row only. Scheduled: the appointment only.
306
507
  expect(tabRows("purchases")).toEqual([expect.stringContaining("pro")]);
307
- expect(tabRows("scheduled")).toEqual([expect.stringContaining("Kickoff call")]);
508
+ expect(tabRows("scheduled")).toEqual([
509
+ expect.stringContaining("Kickoff call"),
510
+ ]);
308
511
  // The result row is under All and in no other tab.
309
- expect(screen.queryByText("ailk-founder-diagnostic")).not.toBeInTheDocument();
512
+ expect(
513
+ screen.queryByText("ailk-founder-diagnostic"),
514
+ ).not.toBeInTheDocument();
310
515
  });
311
516
 
312
517
  it("each All row links to its item page, prefixed off the default locale", async () => {
@@ -319,7 +524,10 @@ describe("the Inbox — tabs All · Purchases · Scheduled (#8231 D3, AC-3)", ()
319
524
  );
320
525
  expect(
321
526
  within(all).getByText("ailk-founder-diagnostic").closest("a"),
322
- ).toHaveAttribute("href", "/fr/account/inbox/result/ailk-founder-diagnostic");
527
+ ).toHaveAttribute(
528
+ "href",
529
+ "/fr/account/inbox/result/ailk-founder-diagnostic",
530
+ );
323
531
  });
324
532
 
325
533
  it("Scheduled lists the upcoming soonest first, then the past newest first", async () => {
@@ -351,7 +559,14 @@ describe("the Inbox — tabs All · Purchases · Scheduled (#8231 D3, AC-3)", ()
351
559
  kind: "licence",
352
560
  label: "purchases.heading",
353
561
  section: "purchases",
354
- load: async () => [{ id: "lic_1", title: "Site licence", at: "2026-09-05T00:00:00.000Z", data: null }],
562
+ load: async () => [
563
+ {
564
+ id: "lic_1",
565
+ title: "Site licence",
566
+ at: "2026-09-05T00:00:00.000Z",
567
+ data: null,
568
+ },
569
+ ],
355
570
  renderDetail: () => ({}),
356
571
  };
357
572
  accountInboxKinds.push(licence);
@@ -375,7 +590,9 @@ describe("the Inbox — tabs All · Purchases · Scheduled (#8231 D3, AC-3)", ()
375
590
  "Purchases0",
376
591
  "Scheduled0",
377
592
  ]);
378
- expect(screen.getByTestId("inbox-empty")).toHaveTextContent("Nothing here yet.");
593
+ expect(screen.getByTestId("inbox-empty")).toHaveTextContent(
594
+ "Nothing here yet.",
595
+ );
379
596
  });
380
597
  });
381
598
 
@@ -386,7 +603,9 @@ describe("For-you items, not Inbox rows (AC-7, D9 as amended)", () => {
386
603
  const forYou = screen.getByTestId("inbox-panel-for-you");
387
604
  expect(within(forYou).getAllByTestId("for-you-item")).toHaveLength(2);
388
605
  const list = screen.getByTestId("inbox-list");
389
- expect(within(list).queryByText("Email not verified")).not.toBeInTheDocument();
606
+ expect(
607
+ within(list).queryByText("Email not verified"),
608
+ ).not.toBeInTheDocument();
390
609
  expect(
391
610
  within(list).queryByText("Your payment didn't go through"),
392
611
  ).not.toBeInTheDocument();
@@ -400,7 +619,9 @@ describe("For-you items, not Inbox rows (AC-7, D9 as amended)", () => {
400
619
  expect(item).toHaveAttribute("data-item-id", "add-name");
401
620
  expect(item).toHaveTextContent("Add your name");
402
621
  // DESIGN.md §5.4c — the tag is icon + label.
403
- expect(within(item).getByTestId("for-you-tag").querySelector("svg")).not.toBeNull();
622
+ expect(
623
+ within(item).getByTestId("for-you-tag").querySelector("svg"),
624
+ ).not.toBeNull();
404
625
  expect(within(item).getByTestId("for-you-open")).toHaveAttribute(
405
626
  "href",
406
627
  "/account/general",
@@ -424,15 +645,22 @@ describe("the phone Inbox tab (#7469, DESIGN.md §2b · §5.15)", () => {
424
645
  UNVERIFIED_AND_PAST_DUE();
425
646
  await renderPage();
426
647
  expect(screen.getByTestId("inbox-panel-inbox")).not.toHaveClass("hidden");
427
- expect(screen.getByTestId("inbox-panel-inbox")).not.toHaveClass("md:hidden");
648
+ expect(screen.getByTestId("inbox-panel-inbox")).not.toHaveClass(
649
+ "md:hidden",
650
+ );
428
651
  expect(screen.getByTestId("inbox-panel-for-you")).toHaveClass("hidden");
429
652
  });
430
653
 
431
654
  it("the phone has no large title or lead — both stay at md+", async () => {
432
655
  await renderPage();
433
- expect(screen.getByRole("heading", { level: 1 })).toHaveClass("max-md:sr-only");
656
+ expect(screen.getByRole("heading", { level: 1 })).toHaveClass(
657
+ "max-md:sr-only",
658
+ );
434
659
  expect(screen.getByRole("heading", { level: 1 })).not.toHaveClass("hidden");
435
- expect(screen.getByText("What changed, and what's coming.")).toHaveClass("hidden", "md:block");
660
+ expect(screen.getByText("What changed, and what's coming.")).toHaveClass(
661
+ "hidden",
662
+ "md:block",
663
+ );
436
664
  });
437
665
 
438
666
  it("the list sits in a bordered card, inset by the canvas on every side", async () => {
@@ -442,7 +670,9 @@ describe("the phone Inbox tab (#7469, DESIGN.md §2b · §5.15)", () => {
442
670
  // no full-bleed escape, and the list is RowGroup's bordered box.
443
671
  const page = screen.getByTestId("account-inbox");
444
672
  expect(page.className).not.toMatch(/-mx-|w-screen/);
445
- expect(screen.getByTestId("inbox-panel-inbox").className ?? "").not.toMatch(/-mx-|w-screen/);
673
+ expect(screen.getByTestId("inbox-panel-inbox").className ?? "").not.toMatch(
674
+ /-mx-|w-screen/,
675
+ );
446
676
  });
447
677
  });
448
678
 
@@ -458,20 +688,28 @@ describe("the phone For you tab (#7469, DESIGN.md §2b · §5.15)", () => {
458
688
  expect(panel).toHaveClass("md:hidden");
459
689
  expect(panel).not.toHaveClass("hidden");
460
690
  // The Inbox panel is hidden on the phone but still shows at md+.
461
- expect(screen.getByTestId("inbox-panel-inbox")).toHaveClass("hidden", "md:block");
691
+ expect(screen.getByTestId("inbox-panel-inbox")).toHaveClass(
692
+ "hidden",
693
+ "md:block",
694
+ );
462
695
  const card = within(panel).getByTestId("for-you-list-card");
463
696
  const items = within(card).getAllByTestId("for-you-item");
464
697
  expect(items).toHaveLength(3);
465
698
  for (const item of items) {
466
699
  expect(within(item).getByTestId("for-you-tag")).toBeInTheDocument();
467
700
  expect(
468
- within(item).queryByTestId("for-you-open") ?? within(item).queryByTestId("for-you-view"),
701
+ within(item).queryByTestId("for-you-open") ??
702
+ within(item).queryByTestId("for-you-view"),
469
703
  ).not.toBeNull();
470
- expect(within(item).getByTestId("for-you-dismiss")).toHaveAccessibleName(/^Dismiss/);
704
+ expect(within(item).getByTestId("for-you-dismiss")).toHaveAccessibleName(
705
+ /^Dismiss/,
706
+ );
471
707
  }
472
708
  expect(within(panel).getByText("Add your name")).toBeInTheDocument();
473
709
  expect(within(panel).getByText("Email not verified")).toBeInTheDocument();
474
- expect(within(panel).getByText("Your payment didn't go through")).toBeInTheDocument();
710
+ expect(
711
+ within(panel).getByText("Your payment didn't go through"),
712
+ ).toBeInTheDocument();
475
713
  });
476
714
 
477
715
  it("✕ dismisses an item — it leaves the list", async () => {
@@ -483,7 +721,9 @@ describe("the phone For you tab (#7469, DESIGN.md §2b · §5.15)", () => {
483
721
  fireEvent.click(within(first!).getByTestId("for-you-dismiss"));
484
722
  const left = within(panel).getAllByTestId("for-you-item");
485
723
  expect(left).toHaveLength(1);
486
- expect(left.map((item) => item.getAttribute("data-item-id"))).not.toContain(id);
724
+ expect(left.map((item) => item.getAttribute("data-item-id"))).not.toContain(
725
+ id,
726
+ );
487
727
  });
488
728
 
489
729
  it("the For you tab keeps the page's h1 (visually hidden on the phone)", async () => {
@@ -519,7 +759,9 @@ describe("Hub unavailable (D12)", () => {
519
759
  mockSubscriptionMe.mockResolvedValue({ ok: false, status: 502, body: {} });
520
760
  await renderPage();
521
761
  expect(
522
- within(screen.getByTestId("inbox-panel-inbox")).getByTestId("inbox-error"),
762
+ within(screen.getByTestId("inbox-panel-inbox")).getByTestId(
763
+ "inbox-error",
764
+ ),
523
765
  ).toBeInTheDocument();
524
766
  });
525
767
 
@@ -531,7 +773,9 @@ describe("Hub unavailable (D12)", () => {
531
773
  "We couldn't load everything in your Inbox.",
532
774
  );
533
775
  // Never a silently empty list (D12): the empty line gives way to the error row.
534
- expect(within(panel).queryByTestId("for-you-empty")).not.toBeInTheDocument();
776
+ expect(
777
+ within(panel).queryByTestId("for-you-empty"),
778
+ ).not.toBeInTheDocument();
535
779
  });
536
780
 
537
781
  it("a failed Inbox-kind read leaves the For you tab without an error row", async () => {
@@ -569,24 +813,36 @@ describe("the member kinds — piece and message (#7920, spec 7423 D8 · D12)",
569
813
  ]);
570
814
  });
571
815
 
572
- it.each(["piece", "message"])("%s loads [] on a 404 — nothing for this caller", async (id) => {
573
- mockMemberPieces.mockResolvedValue(notFound);
574
- mockMemberMessages.mockResolvedValue(notFound);
575
- await expect(kindOf(id).load(await memberCtx())).resolves.toEqual([]);
576
- });
816
+ it.each(["piece", "message"])(
817
+ "%s loads [] on a 404 — nothing for this caller",
818
+ async (id) => {
819
+ mockMemberPieces.mockResolvedValue(notFound);
820
+ mockMemberMessages.mockResolvedValue(notFound);
821
+ await expect(kindOf(id).load(await memberCtx())).resolves.toEqual([]);
822
+ },
823
+ );
577
824
 
578
825
  it.each([
579
826
  ["piece", 401],
580
827
  ["piece", 403],
581
828
  ["message", 401],
582
829
  ["message", 403],
583
- ])("%s loads [] on a %i while the email is unverified", async (id, status) => {
584
- const refusal = { ok: false, status, body: { ok: false, error: { code: "email_unverified" } } };
585
- mockMemberPieces.mockResolvedValue(refusal);
586
- mockMemberMessages.mockResolvedValue(refusal);
587
- const ctx = await memberCtx({ user: { name: "Ada", email: "ada@example.com", emailVerified: false } });
588
- await expect(kindOf(id).load(ctx)).resolves.toEqual([]);
589
- });
830
+ ])(
831
+ "%s loads [] on a %i while the email is unverified",
832
+ async (id, status) => {
833
+ const refusal = {
834
+ ok: false,
835
+ status,
836
+ body: { ok: false, error: { code: "email_unverified" } },
837
+ };
838
+ mockMemberPieces.mockResolvedValue(refusal);
839
+ mockMemberMessages.mockResolvedValue(refusal);
840
+ const ctx = await memberCtx({
841
+ user: { name: "Ada", email: "ada@example.com", emailVerified: false },
842
+ });
843
+ await expect(kindOf(id).load(ctx)).resolves.toEqual([]);
844
+ },
845
+ );
590
846
 
591
847
  it.each([
592
848
  ["piece", 401],
@@ -597,30 +853,51 @@ describe("the member kinds — piece and message (#7920, spec 7423 D8 · D12)",
597
853
  ["message", 403],
598
854
  ["message", 502],
599
855
  ["message", 0],
600
- ])("%s rejects on a %i for a verified email — never a partial list shown as whole", async (id, status) => {
601
- const failure = {
602
- ok: false,
603
- status,
604
- body: { ok: false, error: { code: status === 0 ? "insecure_transport" : "failed", message: "x" } },
605
- };
606
- mockMemberPieces.mockResolvedValue(failure);
607
- mockMemberMessages.mockResolvedValue(failure);
608
- await expect(kindOf(id).load(await memberCtx())).rejects.toMatchObject({ status });
609
- });
856
+ ])(
857
+ "%s rejects on a %i for a verified email — never a partial list shown as whole",
858
+ async (id, status) => {
859
+ const failure = {
860
+ ok: false,
861
+ status,
862
+ body: {
863
+ ok: false,
864
+ error: {
865
+ code: status === 0 ? "insecure_transport" : "failed",
866
+ message: "x",
867
+ },
868
+ },
869
+ };
870
+ mockMemberPieces.mockResolvedValue(failure);
871
+ mockMemberMessages.mockResolvedValue(failure);
872
+ await expect(kindOf(id).load(await memberCtx())).rejects.toMatchObject({
873
+ status,
874
+ });
875
+ },
876
+ );
610
877
 
611
878
  it("on a cookie-session site both kinds and the source call with the cookie and return rows (#8231 D7)", async () => {
612
879
  const cookie = { headers: { cookie: "session=abc" } };
613
880
  const ctx = await memberCtx({ init: cookie });
614
- expect((await kindOf("piece").load(ctx)).map((p) => p.id)).toEqual([PIECE.id, PIECE_NEEDS_CLIENT.id]);
615
- expect((await kindOf("message").load(ctx)).map((m) => m.id)).toEqual([MESSAGE.id, MESSAGE_WITH_FILE.id]);
616
- expect((await needsClientSource().load(ctx)).map((i) => i.id)).toEqual([`piece:${PIECE_NEEDS_CLIENT.id}`]);
881
+ expect((await kindOf("piece").load(ctx)).map((p) => p.id)).toEqual([
882
+ PIECE.id,
883
+ PIECE_NEEDS_CLIENT.id,
884
+ ]);
885
+ expect((await kindOf("message").load(ctx)).map((m) => m.id)).toEqual([
886
+ MESSAGE.id,
887
+ MESSAGE_WITH_FILE.id,
888
+ ]);
889
+ expect((await needsClientSource().load(ctx)).map((i) => i.id)).toEqual([
890
+ `piece:${PIECE_NEEDS_CLIENT.id}`,
891
+ ]);
617
892
  expect(mockMemberPieces).toHaveBeenCalledWith(cookie);
618
893
  expect(mockMemberMessages).toHaveBeenCalledWith(cookie);
619
894
  });
620
895
 
621
896
  it("the cookie-session Inbox lists the member rows under All, read with the forwarded cookie (#8231 D7)", async () => {
622
897
  await renderPage();
623
- const sent = expect.objectContaining({ headers: expect.objectContaining({ cookie: "session=abc" }) });
898
+ const sent = expect.objectContaining({
899
+ headers: expect.objectContaining({ cookie: "session=abc" }),
900
+ });
624
901
  expect(mockMemberPieces).toHaveBeenCalledWith(sent);
625
902
  expect(mockMemberMessages).toHaveBeenCalledWith(sent);
626
903
  expect(mockMemberPurchases).toHaveBeenCalledWith(sent);
@@ -634,14 +911,17 @@ describe("the member kinds — piece and message (#7920, spec 7423 D8 · D12)",
634
911
  it.each([
635
912
  ["pieces", mockMemberPieces],
636
913
  ["messages", mockMemberMessages],
637
- ] as const)("a 500 from member.%s shows the Inbox error row with Retry (#8231 D7)", async (_name, mock) => {
638
- mock.mockResolvedValue({ ok: false, status: 500, body: {} });
639
- await renderPage();
640
- const inbox = screen.getByTestId("inbox-panel-inbox");
641
- expect(within(inbox).getByTestId("inbox-error")).toBeInTheDocument();
642
- fireEvent.click(within(inbox).getByTestId("inbox-retry"));
643
- expect(mockRefresh).toHaveBeenCalledTimes(1);
644
- });
914
+ ] as const)(
915
+ "a 500 from member.%s shows the Inbox error row with Retry (#8231 D7)",
916
+ async (_name, mock) => {
917
+ mock.mockResolvedValue({ ok: false, status: 500, body: {} });
918
+ await renderPage();
919
+ const inbox = screen.getByTestId("inbox-panel-inbox");
920
+ expect(within(inbox).getByTestId("inbox-error")).toBeInTheDocument();
921
+ fireEvent.click(within(inbox).getByTestId("inbox-retry"));
922
+ expect(mockRefresh).toHaveBeenCalledTimes(1);
923
+ },
924
+ );
645
925
 
646
926
  it("a piece's detail shows its stage and dates", async () => {
647
927
  const ctx = await memberCtx();
@@ -649,7 +929,11 @@ describe("the member kinds — piece and message (#7920, spec 7423 D8 · D12)",
649
929
  const detail = kindOf("piece").renderDetail(first!, ctx);
650
930
  expect(detail.status).toBe("Drafting");
651
931
  expect(detail.fields?.heading).toBe("My pieces");
652
- expect(detail.fields?.items.map((i) => i.label)).toEqual(["Stage", "Started", "Updated"]);
932
+ expect(detail.fields?.items.map((i) => i.label)).toEqual([
933
+ "Stage",
934
+ "Started",
935
+ "Updated",
936
+ ]);
653
937
  });
654
938
 
655
939
  it("a message with an asset shows a file action that opens the member file read", async () => {
@@ -665,16 +949,19 @@ describe("the member kinds — piece and message (#7920, spec 7423 D8 · D12)",
665
949
  href: `/account/inbox/message/${MESSAGE_WITH_FILE.id}/file`,
666
950
  },
667
951
  ]);
668
- expect(detail.fields?.items).toContainEqual({ label: "File", value: "brand-guide.pdf" });
952
+ expect(detail.fields?.items).toContainEqual({
953
+ label: "File",
954
+ value: "brand-guide.pdf",
955
+ });
669
956
  });
670
957
 
671
958
  it("the file action carries the locale prefix off the default locale", async () => {
672
959
  const ctx = await memberCtx({ locale: "fr" });
673
960
  const items = await kindOf("message").load(ctx);
674
961
  const withFile = items.find((m) => m.id === MESSAGE_WITH_FILE.id)!;
675
- expect(kindOf("message").renderDetail(withFile, ctx).actions?.[0]?.href).toBe(
676
- `/fr/account/inbox/message/${MESSAGE_WITH_FILE.id}/file`,
677
- );
962
+ expect(
963
+ kindOf("message").renderDetail(withFile, ctx).actions?.[0]?.href,
964
+ ).toBe(`/fr/account/inbox/message/${MESSAGE_WITH_FILE.id}/file`);
678
965
  });
679
966
 
680
967
  it("a message without an asset has no file action", async () => {
@@ -697,7 +984,10 @@ describe("the needs-client For-you source (#7920)", () => {
697
984
  type: { id: "piece", label: "Piece", tone: "needs-you" },
698
985
  title: "About page",
699
986
  time: { kind: "at", at: PIECE_NEEDS_CLIENT.updatedAt },
700
- action: { kind: "open", href: `/account/inbox/piece/${PIECE_NEEDS_CLIENT.id}` },
987
+ action: {
988
+ kind: "open",
989
+ href: `/account/inbox/piece/${PIECE_NEEDS_CLIENT.id}`,
990
+ },
701
991
  dismissible: true,
702
992
  },
703
993
  ]);
@@ -705,7 +995,9 @@ describe("the needs-client For-you source (#7920)", () => {
705
995
  });
706
996
 
707
997
  it("off the default locale the Open href carries the prefix", async () => {
708
- const [item] = await needsClientSource().load(await memberCtx({ locale: "fr" }));
998
+ const [item] = await needsClientSource().load(
999
+ await memberCtx({ locale: "fr" }),
1000
+ );
709
1001
  expect(item?.action).toEqual({
710
1002
  kind: "open",
711
1003
  href: `/fr/account/inbox/piece/${PIECE_NEEDS_CLIENT.id}`,
@@ -714,8 +1006,12 @@ describe("the needs-client For-you source (#7920)", () => {
714
1006
 
715
1007
  it("loads [] on a 404 and rejects on any other failure", async () => {
716
1008
  mockMemberPieces.mockResolvedValue(notFound);
717
- await expect(needsClientSource().load(await memberCtx())).resolves.toEqual([]);
1009
+ await expect(needsClientSource().load(await memberCtx())).resolves.toEqual(
1010
+ [],
1011
+ );
718
1012
  mockMemberPieces.mockResolvedValue({ ok: false, status: 500, body: {} });
719
- await expect(needsClientSource().load(await memberCtx())).rejects.toMatchObject({ status: 500 });
1013
+ await expect(
1014
+ needsClientSource().load(await memberCtx()),
1015
+ ).rejects.toMatchObject({ status: 500 });
720
1016
  });
721
1017
  });