create-ailk 0.18.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/LICENSE +6 -5
  2. package/NOTICE +2 -0
  3. package/dist/add-module.js +11 -3
  4. package/dist/category-profile.d.ts +10 -0
  5. package/dist/category-profile.js +21 -0
  6. package/dist/derive-architecture.d.ts +5 -1
  7. package/dist/derive-architecture.js +8 -3
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/lib/license-key-files.d.ts +6 -3
  11. package/dist/lib/license-key-files.js +8 -2
  12. package/dist/lib/scaffold-render-sweep.d.ts +32 -0
  13. package/dist/lib/scaffold-render-sweep.js +76 -3
  14. package/dist/license-set.d.ts +1 -1
  15. package/dist/license-set.js +13 -8
  16. package/dist/module-architecture.js +128 -3
  17. package/dist/programmatic.js +11 -1
  18. package/dist/site-roster.js +21 -4
  19. package/dist/spoke-checkout.d.ts +11 -3
  20. package/dist/spoke-checkout.js +62 -12
  21. package/dist/starter-grid.d.ts +1 -1
  22. package/dist/surfaces.d.ts +14 -0
  23. package/dist/surfaces.js +170 -1
  24. package/dist/template-manifest.d.ts +4 -3
  25. package/dist/topology.d.ts +7 -2
  26. package/dist/topology.js +88 -3
  27. package/package.json +5 -4
  28. package/templates/.env.example +3 -3
  29. package/templates/.github/workflows/ci.yml +5 -0
  30. package/templates/apps/api/.env.example +17 -1
  31. package/templates/apps/api/CLAUDE.md +31 -7
  32. package/templates/apps/api/LICENSE +6 -5
  33. package/templates/apps/api/NOTICE +2 -0
  34. package/templates/apps/api/jest.config.cjs +4 -29
  35. package/templates/apps/api/package.json +1 -0
  36. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +19 -0
  37. package/templates/apps/api/src/__tests__/stripe-boot.test.ts +132 -0
  38. package/templates/apps/api/src/lib/__mocks__/prisma.ts +17 -0
  39. package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +237 -0
  40. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +334 -0
  41. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
  42. package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +217 -0
  43. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +7 -0
  44. package/templates/apps/api/src/lib/consent-store.ts +335 -0
  45. package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
  46. package/templates/apps/api/src/lib/stripe.ts +176 -3
  47. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +272 -39
  48. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +51 -0
  49. package/templates/apps/api/src/middleware/auth.ts +214 -46
  50. package/templates/apps/api/src/middleware/rate-limit.ts +5 -0
  51. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +167 -0
  52. package/templates/apps/api/src/routes/checkout/session.ts +3 -3
  53. package/templates/apps/api/src/routes/checkout/sessions.ts +6 -4
  54. package/templates/apps/api/src/routes/consent/cookie.ts +90 -0
  55. package/templates/apps/api/src/routes/consent/index.ts +18 -0
  56. package/templates/apps/api/src/routes/consent/unsubscribe.ts +127 -0
  57. package/templates/apps/api/src/routes/leads/index.ts +19 -0
  58. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +5 -0
  59. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +17 -4
  60. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +2 -0
  61. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +159 -0
  62. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +158 -0
  63. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +2 -0
  64. package/templates/apps/api/src/routes/webhooks/stripe.ts +17 -0
  65. package/templates/apps/api/src/server.ts +218 -16
  66. package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +503 -0
  67. package/templates/apps/api/src/services/__tests__/consent.test.ts +18 -3
  68. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +173 -4
  69. package/templates/apps/api/src/services/__tests__/person.test.ts +392 -0
  70. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +87 -0
  71. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +57 -17
  72. package/templates/apps/api/src/services/consent.ts +488 -3
  73. package/templates/apps/api/src/services/lead-routing.ts +135 -14
  74. package/templates/apps/api/src/services/person.ts +501 -1
  75. package/templates/apps/api/src/services/project-listings.ts +7 -2
  76. package/templates/apps/api/src/services/request-confirm.ts +52 -0
  77. package/templates/apps/api/src/services/revalidation.ts +11 -1
  78. package/templates/apps/api/vercel.json +2 -2
  79. package/templates/apps/mcp/.env.example +10 -0
  80. package/templates/apps/mcp/CLAUDE.md +9 -4
  81. package/templates/apps/mcp/LICENSE +6 -5
  82. package/templates/apps/mcp/NOTICE +2 -0
  83. package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
  84. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
  85. package/templates/apps/mcp/__tests__/boot.test.ts +78 -1
  86. package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
  87. package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
  88. package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
  89. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
  90. package/templates/apps/mcp/jest.config.cjs +3 -44
  91. package/templates/apps/mcp/src/abuse/index.ts +117 -21
  92. package/templates/apps/mcp/src/boot.ts +5 -23
  93. package/templates/apps/mcp/src/http-transport.ts +93 -73
  94. package/templates/apps/mcp/src/index.ts +2 -0
  95. package/templates/apps/mcp/src/request-context.ts +52 -0
  96. package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
  97. package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
  98. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
  99. package/templates/apps/web/LICENSE +6 -5
  100. package/templates/apps/web/NOTICE +2 -0
  101. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
  102. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/account-frame-sign-out.test.tsx +175 -0
  103. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +302 -0
  104. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +724 -0
  105. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +346 -41
  106. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
  107. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +146 -0
  108. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +232 -29
  109. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
  110. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
  111. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +140 -0
  112. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
  113. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
  114. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +409 -0
  115. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +65 -380
  116. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
  117. package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
  118. package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +188 -54
  119. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +24 -45
  120. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
  121. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
  122. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
  123. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
  124. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
  125. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
  126. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
  127. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
  128. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +174 -0
  129. package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
  130. package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
  131. package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
  132. package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
  133. package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
  134. package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
  135. package/templates/apps/web/app/[locale]/(authed)/account/general/contact-actions.ts +81 -0
  136. package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
  137. package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +193 -0
  138. package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
  139. package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
  140. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
  141. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
  142. package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +30 -4
  143. package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
  144. package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
  145. package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
  146. package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
  147. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
  148. package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
  149. package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +132 -0
  150. package/templates/apps/web/app/[locale]/(authed)/account/security/security-panel.tsx +102 -0
  151. package/templates/apps/web/app/[locale]/(authed)/account/security/security-source.ts +62 -0
  152. package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +13 -354
  153. package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +116 -0
  154. package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
  155. package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
  156. package/templates/apps/web/app/[locale]/layout.tsx +23 -4
  157. package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
  158. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
  159. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
  160. package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
  161. package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
  162. package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
  163. package/templates/apps/web/app/robots.txt/route.ts +10 -4
  164. package/templates/apps/web/auth.ts +25 -2
  165. package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
  166. package/templates/apps/web/lib/__tests__/auth-email.test.ts +84 -0
  167. package/templates/apps/web/lib/account-config.ts +188 -52
  168. package/templates/apps/web/lib/auth-email.ts +45 -10
  169. package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
  170. package/templates/apps/web/lib/hub-session/index.ts +1 -0
  171. package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
  172. package/templates/apps/web/lib/hub-session/seal.ts +13 -9
  173. package/templates/apps/web/lib/topology.generated.ts +33 -0
  174. package/templates/apps/web/messages/en.json +194 -33
  175. package/templates/database/CHANGELOG.md +37 -0
  176. package/templates/database/LICENSE +6 -5
  177. package/templates/database/NOTICE +2 -0
  178. package/templates/database/README.md +1 -1
  179. package/templates/database/__tests__/consent-ledger.db.test.ts +345 -0
  180. package/templates/database/__tests__/fence-fan-out.db.test.ts +6 -2
  181. package/templates/database/__tests__/fixtures/person-contact-backfill.json +182 -0
  182. package/templates/database/__tests__/member-area.db.test.ts +15 -3
  183. package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
  184. package/templates/database/__tests__/member-contract.db.test.ts +413 -0
  185. package/templates/database/__tests__/person-contact.db.test.ts +563 -0
  186. package/templates/database/__tests__/person.db.test.ts +3 -1
  187. package/templates/database/__tests__/seed.test.ts +54 -2
  188. package/templates/database/inbox/schema.prisma +166 -25
  189. package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
  190. package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
  191. package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +122 -0
  192. package/templates/database/migrations/20261014120000_person_contact/migration.sql +231 -0
  193. package/templates/database/package.json +3 -2
  194. package/templates/database/seed.ts +34 -2
  195. package/versions.json +36 -36
  196. package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
  197. package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
  198. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
  199. package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
  200. package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
  201. package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
  202. package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
  203. package/templates/apps/api/src/__tests__/server.test.ts +0 -591
  204. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
  205. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
  206. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
  207. package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
  208. package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
  209. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
  210. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
  211. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
  212. package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
  213. package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
  214. package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
  215. package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
  216. package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
  217. package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
  218. package/templates/database/__tests__/build.test.ts +0 -131
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Tests for /[locale]/account — the base account Inbox (#7430, spec 7423
3
- * U4 · D6a, D9, D12; AC-7 as amended 2026-10-01).
3
+ * U4 · D6a, D9, D12; AC-7 as amended 2026-10-01; #8231 D3, D5–D7).
4
4
  *
5
5
  * Covers:
6
- * 1. The D6a tabs Recents · Suggested · Scheduled · All, each with its
7
- * count (zeros included), Recents selected.
8
- * 2. `result` and `purchase` rows sit under Recents and All, newest first,
9
- * each linking to its item page.
6
+ * 1. The tabs All · Purchases · Scheduled, each with its count (zeros
7
+ * included), All selected (#8231 D3).
8
+ * 2. All holds every row newest first, each linking to its item page;
9
+ * Purchases holds the kinds whose section is `purchases`; Scheduled the
10
+ * `upcoming` kinds, upcoming soonest first, then past newest first.
10
11
  * 3. `email-unverified` and `payment-due` are For-you items (the phone
11
12
  * For you tab), never Inbox rows.
12
13
  * 4. A member with no name gets an "Add your name" For-you item (needs-you,
@@ -20,13 +21,13 @@
20
21
  * 7. #7920 — the member area's `piece` and `message` kinds and the
21
22
  * needs-client For-you source. Each reads the hub with `ctx.init`
22
23
  * unchanged (the caller's own credential); 404 and an unverified 401/403
23
- * load `[]`, any other failure rejects (D12). With no hub credential on
24
- * `ctx.init` — a cookie-session site — they make no call and load `[]`.
24
+ * load `[]`, any other failure rejects (D12). On a cookie-session site
25
+ * they call with the cookie too — no gate (#8231 D7).
25
26
  */
26
27
  import { fireEvent, render, screen, within } from "@testing-library/react";
27
28
 
28
29
  // The global setup mock resolves only top-level namespace keys against
29
- // en.json; the area reads nested keys (`inbox.tabs.recents`, …) — a call
30
+ // en.json; the area reads nested keys (`inbox.tabs.all`, …) — a call
30
31
  // shape real next-intl supports — so resolve dotted namespaces AND dotted
31
32
  // keys here against the same catalog. `{name}` placeholders are filled from
32
33
  // `values` the way next-intl does.
@@ -67,18 +68,26 @@ jest.mock("../../../../../auth", () => ({
67
68
  }));
68
69
 
69
70
  const mockSignupMe = jest.fn();
70
- const mockCheckoutsMe = jest.fn();
71
71
  const mockSubscriptionMe = jest.fn();
72
72
  const mockMemberPieces = jest.fn();
73
73
  const mockMemberMessages = jest.fn();
74
+ const mockMemberPurchases = jest.fn();
75
+ const mockMemberAppointments = jest.fn();
74
76
  jest.mock("@working-theory/api-client", () => ({
75
77
  __esModule: true,
76
78
  waitlistSignups: { me: (...args: unknown[]) => mockSignupMe(...args) },
77
- flowCheckouts: { me: (...args: unknown[]) => mockCheckoutsMe(...args) },
78
79
  subscription: { me: (...args: unknown[]) => mockSubscriptionMe(...args) },
79
80
  member: {
80
81
  pieces: (...args: unknown[]) => mockMemberPieces(...args),
81
82
  messages: (...args: unknown[]) => mockMemberMessages(...args),
83
+ purchases: (...args: unknown[]) => mockMemberPurchases(...args),
84
+ appointments: (...args: unknown[]) => mockMemberAppointments(...args),
85
+ // #8233 C4 — the New menu's availability reads; 404 here, so the Inbox
86
+ // cases see no New action and no error row.
87
+ meetingTypes: () =>
88
+ Promise.resolve({ ok: false, status: 404, body: { ok: false, error: { code: "not_found", message: "Not found" } } }),
89
+ requestForm: () =>
90
+ Promise.resolve({ ok: false, status: 404, body: { ok: false, error: { code: "not_found", message: "Not found" } } }),
82
91
  },
83
92
  }));
84
93
 
@@ -123,19 +132,21 @@ const SIGNUP = {
123
132
  answers: { q3: null },
124
133
  };
125
134
 
126
- const CHECKOUT = {
127
- id: "fc_done",
128
- flowSlug: "founder-flow",
129
- experimentKey: "ailk-founder-diagnostic",
130
- offerKey: "pro",
131
- status: "completed",
135
+ const PURCHASE = {
136
+ id: "pur_done",
137
+ offering: "pro",
132
138
  amountMinor: 4900,
133
139
  currency: "usd",
134
140
  period: "monthly",
135
- completedAt: "2026-09-04T10:00:00.000Z",
136
- createdAt: "2026-09-04T09:58:00.000Z",
141
+ purchasedAt: "2026-09-04T10:00:00.000Z",
137
142
  };
138
143
 
144
+ function appointment(id: string, title: string, startTime: string) {
145
+ return { id, title, startTime, endTime: startTime, status: "confirmed" as const };
146
+ }
147
+
148
+ const NO_APPOINTMENTS = { ok: true, body: { upcoming: [], past: [] } };
149
+
139
150
  const NO_SUBSCRIPTION = {
140
151
  ok: true,
141
152
  body: { actionRequired: false, status: null, accessRevokesAt: null, portalAvailable: false },
@@ -236,74 +247,133 @@ beforeEach(() => {
236
247
  accountExperimentKeys.length = 0;
237
248
  mockGetSession.mockReset().mockResolvedValue(SESSION);
238
249
  mockSignupMe.mockReset().mockResolvedValue({ ok: true, body: SIGNUP });
239
- mockCheckoutsMe.mockReset().mockResolvedValue({ ok: true, body: [CHECKOUT] });
240
250
  mockSubscriptionMe.mockReset().mockResolvedValue(NO_SUBSCRIPTION);
241
- mockMemberPieces.mockReset().mockResolvedValue({ ok: true, body: { items: [PIECE, PIECE_NEEDS_CLIENT] } });
242
- mockMemberMessages.mockReset().mockResolvedValue({ ok: true, body: { items: [MESSAGE, MESSAGE_WITH_FILE] } });
251
+ mockMemberPurchases.mockReset().mockResolvedValue({ ok: true, body: { items: [PURCHASE] } });
252
+ mockMemberAppointments.mockReset().mockResolvedValue(NO_APPOINTMENTS);
253
+ // The page's pieces and messages answer 404 unless a case fills them.
254
+ mockMemberPieces.mockReset().mockResolvedValue(notFound);
255
+ mockMemberMessages.mockReset().mockResolvedValue(notFound);
243
256
  mockRefresh.mockReset();
244
257
  });
245
258
 
259
+ /** The member kinds' own cases read rows from both member reads. */
260
+ 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] } });
263
+ }
264
+
246
265
  afterAll(() => {
247
266
  accountExperimentKeys.length = 0;
248
267
  });
249
268
 
250
- describe("the Inbox — D6a tabs (AC-7)", () => {
251
- it("renders Recents · Suggested · Scheduled · All with counts, Recents selected", async () => {
269
+ describe("the Inbox — tabs All · Purchases · Scheduled (#8231 D3, AC-3)", () => {
270
+ /** The rows of one tab, by title, after selecting it. */
271
+ function tabRows(id: string): string[] {
272
+ fireEvent.mouseDown(screen.getByTestId(`inbox-tab-${id}`));
273
+ return within(screen.getByTestId(`inbox-${id}`))
274
+ .queryAllByTestId(`inbox-${id}-row`)
275
+ .map((row) => within(row).getByRole("link").textContent ?? "");
276
+ }
277
+
278
+ it("renders All · Purchases · Scheduled with counts, All selected — no Recents or Suggested", async () => {
252
279
  accountExperimentKeys.push("ailk-founder-diagnostic");
280
+ mockMemberPieces.mockResolvedValue({ ok: true, body: { items: [PIECE] } });
281
+ mockMemberMessages.mockResolvedValue({ ok: true, body: { items: [MESSAGE] } });
282
+ mockMemberAppointments.mockResolvedValue({
283
+ ok: true,
284
+ body: { upcoming: [appointment("appt_1", "Kickoff call", "2026-09-10T15:00:00.000Z")], past: [] },
285
+ });
253
286
  await renderPage();
254
287
  expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Inbox");
255
288
  const tabs = screen.getAllByRole("tab");
256
289
  expect(tabs.map((tab) => tab.getAttribute("data-testid"))).toEqual([
257
- "inbox-tab-recents",
258
- "inbox-tab-suggested",
259
- "inbox-tab-scheduled",
260
290
  "inbox-tab-all",
291
+ "inbox-tab-purchases",
292
+ "inbox-tab-scheduled",
261
293
  ]);
262
- expect(tabs.map((tab) => tab.textContent)).toEqual([
263
- "Recents2",
264
- "Suggested0",
265
- "Scheduled0",
266
- "All2",
294
+ expect(tabs.map((tab) => tab.textContent)).toEqual(["All5", "Purchases1", "Scheduled1"]);
295
+ expect(screen.getByTestId("inbox-tab-all")).toHaveAttribute("aria-selected", "true");
296
+ // All: every row, newest first.
297
+ const all = within(screen.getByTestId("inbox-all")).getAllByTestId("inbox-all-row");
298
+ expect(all.map((row) => row.textContent)).toEqual([
299
+ expect.stringContaining("Kickoff call"),
300
+ expect.stringContaining("Kickoff notes"),
301
+ expect.stringContaining("Spring newsletter"),
302
+ expect.stringContaining("pro"),
303
+ expect.stringContaining("ailk-founder-diagnostic"),
267
304
  ]);
268
- expect(screen.getByTestId("inbox-tab-recents")).toHaveAttribute("aria-selected", "true");
305
+ // Purchases: the purchase row only. Scheduled: the appointment only.
306
+ expect(tabRows("purchases")).toEqual([expect.stringContaining("pro")]);
307
+ expect(tabRows("scheduled")).toEqual([expect.stringContaining("Kickoff call")]);
308
+ // The result row is under All and in no other tab.
309
+ expect(screen.queryByText("ailk-founder-diagnostic")).not.toBeInTheDocument();
269
310
  });
270
311
 
271
- it("result and purchase rows sit under Recents, newest first, each linking to its item page", async () => {
312
+ it("each All row links to its item page, prefixed off the default locale", async () => {
272
313
  accountExperimentKeys.push("ailk-founder-diagnostic");
273
- await renderPage();
274
- const recents = screen.getByTestId("inbox-recents");
275
- const rows = within(recents).getAllByTestId("inbox-recents-row");
276
- expect(rows).toHaveLength(2);
277
- // The purchase (Sep 4) is newer than the result (Sep 3).
278
- expect(rows[0]).toHaveTextContent("pro");
279
- expect(rows[1]).toHaveTextContent("ailk-founder-diagnostic");
280
- expect(within(recents).getByText("pro").closest("a")).toHaveAttribute(
281
- "href",
282
- "/account/inbox/purchase/fc_done",
283
- );
284
- expect(
285
- within(recents).getByText("ailk-founder-diagnostic").closest("a"),
286
- ).toHaveAttribute("href", "/account/inbox/result/ailk-founder-diagnostic");
287
- });
288
-
289
- it("the same rows are in All; a non-default locale prefixes the item href", async () => {
290
314
  await renderPage("fr");
291
- fireEvent.mouseDown(screen.getByTestId("inbox-tab-all"));
292
315
  const all = screen.getByTestId("inbox-all");
293
316
  expect(within(all).getByText("pro").closest("a")).toHaveAttribute(
294
317
  "href",
295
- "/fr/account/inbox/purchase/fc_done",
318
+ "/fr/account/inbox/purchase/pur_done",
296
319
  );
320
+ expect(
321
+ within(all).getByText("ailk-founder-diagnostic").closest("a"),
322
+ ).toHaveAttribute("href", "/fr/account/inbox/result/ailk-founder-diagnostic");
323
+ });
324
+
325
+ it("Scheduled lists the upcoming soonest first, then the past newest first", async () => {
326
+ mockMemberAppointments.mockResolvedValue({
327
+ ok: true,
328
+ body: {
329
+ // Out of order on purpose: the page orders them, not the read.
330
+ upcoming: [
331
+ appointment("u1", "U1", "2026-10-25T15:00:00.000Z"),
332
+ appointment("u2", "U2", "2026-10-20T15:00:00.000Z"),
333
+ ],
334
+ past: [
335
+ appointment("p1", "P1", "2026-09-01T15:00:00.000Z"),
336
+ appointment("p2", "P2", "2026-09-05T15:00:00.000Z"),
337
+ ],
338
+ },
339
+ });
340
+ await renderPage();
341
+ expect(tabRows("scheduled")).toEqual([
342
+ expect.stringContaining("U2"),
343
+ expect.stringContaining("U1"),
344
+ expect.stringContaining("P2"),
345
+ expect.stringContaining("P1"),
346
+ ]);
297
347
  });
298
348
 
299
- it("with no rows, every tab still renders and Recents shows the empty line", async () => {
300
- mockCheckoutsMe.mockResolvedValue({ ok: true, body: [] });
349
+ it("a kind declaring section purchases lands under Purchases, not Scheduled", async () => {
350
+ const licence: (typeof accountInboxKinds)[number] = {
351
+ kind: "licence",
352
+ label: "purchases.heading",
353
+ section: "purchases",
354
+ load: async () => [{ id: "lic_1", title: "Site licence", at: "2026-09-05T00:00:00.000Z", data: null }],
355
+ renderDetail: () => ({}),
356
+ };
357
+ accountInboxKinds.push(licence);
358
+ try {
359
+ await renderPage();
360
+ expect(tabRows("purchases")).toEqual([
361
+ expect.stringContaining("Site licence"),
362
+ expect.stringContaining("pro"),
363
+ ]);
364
+ expect(tabRows("scheduled")).toEqual([]);
365
+ } finally {
366
+ accountInboxKinds.splice(accountInboxKinds.indexOf(licence), 1);
367
+ }
368
+ });
369
+
370
+ it("with no rows, every tab still renders and All shows the empty line", async () => {
371
+ mockMemberPurchases.mockResolvedValue({ ok: true, body: { items: [] } });
301
372
  await renderPage();
302
373
  expect(screen.getAllByRole("tab").map((tab) => tab.textContent)).toEqual([
303
- "Recents0",
304
- "Suggested0",
305
- "Scheduled0",
306
374
  "All0",
375
+ "Purchases0",
376
+ "Scheduled0",
307
377
  ]);
308
378
  expect(screen.getByTestId("inbox-empty")).toHaveTextContent("Nothing here yet.");
309
379
  });
@@ -333,7 +403,7 @@ describe("For-you items, not Inbox rows (AC-7, D9 as amended)", () => {
333
403
  expect(within(item).getByTestId("for-you-tag").querySelector("svg")).not.toBeNull();
334
404
  expect(within(item).getByTestId("for-you-open")).toHaveAttribute(
335
405
  "href",
336
- "/account/settings#general",
406
+ "/account/general",
337
407
  );
338
408
  });
339
409
  });
@@ -435,7 +505,7 @@ describe("the phone For you tab (#7469, DESIGN.md §2b · §5.15)", () => {
435
505
 
436
506
  describe("Hub unavailable (D12)", () => {
437
507
  it("a failed read renders an error row with Retry, never an empty Inbox", async () => {
438
- mockCheckoutsMe.mockResolvedValue({ ok: false, status: 500, body: {} });
508
+ mockMemberPurchases.mockResolvedValue({ ok: false, status: 500, body: {} });
439
509
  await renderPage();
440
510
  expect(screen.getByTestId("inbox-error")).toHaveTextContent(
441
511
  "We couldn't load everything in your Inbox.",
@@ -465,7 +535,7 @@ describe("Hub unavailable (D12)", () => {
465
535
  });
466
536
 
467
537
  it("a failed Inbox-kind read leaves the For you tab without an error row", async () => {
468
- mockCheckoutsMe.mockResolvedValue({ ok: false, status: 500, body: {} });
538
+ mockMemberPurchases.mockResolvedValue({ ok: false, status: 500, body: {} });
469
539
  await renderPage("en", "for-you");
470
540
  const panel = screen.getByTestId("inbox-panel-for-you");
471
541
  expect(within(panel).queryByTestId("inbox-error")).not.toBeInTheDocument();
@@ -473,6 +543,8 @@ describe("Hub unavailable (D12)", () => {
473
543
  });
474
544
 
475
545
  describe("the member kinds — piece and message (#7920, spec 7423 D8 · D12)", () => {
546
+ beforeEach(memberRows);
547
+
476
548
  it("declares piece and message as Inbox kinds in the updates section, labelled from the account namespace", async () => {
477
549
  const ctx = await memberCtx();
478
550
  expect(kindOf("piece").section).toBe("updates");
@@ -536,22 +608,39 @@ describe("the member kinds — piece and message (#7920, spec 7423 D8 · D12)",
536
608
  await expect(kindOf(id).load(await memberCtx())).rejects.toMatchObject({ status });
537
609
  });
538
610
 
539
- it("with no hub credential on ctx.init (a cookie-session site) both kinds and the source load [] without a call", async () => {
540
- const ctx = await memberCtx({ init: { headers: { cookie: "session=abc" } } });
541
- await expect(kindOf("piece").load(ctx)).resolves.toEqual([]);
542
- await expect(kindOf("message").load(ctx)).resolves.toEqual([]);
543
- await expect(needsClientSource().load(ctx)).resolves.toEqual([]);
544
- expect(mockMemberPieces).not.toHaveBeenCalled();
545
- expect(mockMemberMessages).not.toHaveBeenCalled();
611
+ it("on a cookie-session site both kinds and the source call with the cookie and return rows (#8231 D7)", async () => {
612
+ const cookie = { headers: { cookie: "session=abc" } };
613
+ 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}`]);
617
+ expect(mockMemberPieces).toHaveBeenCalledWith(cookie);
618
+ expect(mockMemberMessages).toHaveBeenCalledWith(cookie);
546
619
  });
547
620
 
548
- it("the cookie-session Inbox stays as it was: no member rows, no error row, no member call", async () => {
621
+ it("the cookie-session Inbox lists the member rows under All, read with the forwarded cookie (#8231 D7)", async () => {
549
622
  await renderPage();
623
+ const sent = expect.objectContaining({ headers: expect.objectContaining({ cookie: "session=abc" }) });
624
+ expect(mockMemberPieces).toHaveBeenCalledWith(sent);
625
+ expect(mockMemberMessages).toHaveBeenCalledWith(sent);
626
+ expect(mockMemberPurchases).toHaveBeenCalledWith(sent);
627
+ expect(mockMemberAppointments).toHaveBeenCalledWith(sent);
628
+ const all = screen.getByTestId("inbox-all");
629
+ expect(within(all).getByText("Spring newsletter")).toBeInTheDocument();
630
+ expect(within(all).getByText("Kickoff notes")).toBeInTheDocument();
550
631
  expect(screen.queryByTestId("inbox-error")).not.toBeInTheDocument();
551
- expect(screen.queryByText("Spring newsletter")).not.toBeInTheDocument();
552
- expect(screen.queryByText("Kickoff notes")).not.toBeInTheDocument();
553
- expect(mockMemberPieces).not.toHaveBeenCalled();
554
- expect(mockMemberMessages).not.toHaveBeenCalled();
632
+ });
633
+
634
+ it.each([
635
+ ["pieces", mockMemberPieces],
636
+ ["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);
555
644
  });
556
645
 
557
646
  it("a piece's detail shows its stage and dates", async () => {
@@ -598,6 +687,8 @@ describe("the member kinds — piece and message (#7920, spec 7423 D8 · D12)",
598
687
  });
599
688
 
600
689
  describe("the needs-client For-you source (#7920)", () => {
690
+ beforeEach(memberRows);
691
+
601
692
  it("a needsClient piece is a needs-you For-you item with an Open action to its Inbox row", async () => {
602
693
  const items = await needsClientSource().load(await memberCtx());
603
694
  expect(items).toEqual([