@ultimat3/cli 23.0.0 → 25.0.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 (296) hide show
  1. package/CLAUDE.md +28 -13
  2. package/README.md +32 -7
  3. package/package.json +29 -29
  4. package/src/admin-catalog-keys.ts +2 -2
  5. package/src/affected.ts +30 -36
  6. package/src/api-registration.ts +89 -0
  7. package/src/app-artifacts.ts +1 -1
  8. package/src/app-auth.ts +4 -27
  9. package/src/app-boundaries.ts +17 -4
  10. package/src/app-config-load.ts +51 -0
  11. package/src/app-env.ts +26 -25
  12. package/src/app-load.ts +38 -14
  13. package/src/app-manifest.ts +4 -10
  14. package/src/app-mcp.ts +31 -46
  15. package/src/app-name.ts +52 -0
  16. package/src/app-permissions-borrowed.ts +123 -0
  17. package/src/app-permissions-site.ts +48 -0
  18. package/src/app-permissions.ts +7 -17
  19. package/src/app-root.ts +9 -5
  20. package/src/bin.ts +3 -7
  21. package/src/browser-launcher.ts +4 -2
  22. package/src/budgets-charged.ts +60 -0
  23. package/src/budgets-scan.ts +76 -0
  24. package/src/budgets-unmeasured.ts +91 -0
  25. package/src/budgets.ts +117 -154
  26. package/src/cdp-shot-driver.ts +20 -1
  27. package/src/cdp-shot-watch.ts +35 -31
  28. package/src/ci-log.ts +0 -0
  29. package/src/cmd-auth-spec.ts +16 -0
  30. package/src/cmd-auth.ts +65 -0
  31. package/src/cmd-build-spec.ts +7 -1
  32. package/src/cmd-build.ts +151 -22
  33. package/src/cmd-db-backfill.ts +26 -13
  34. package/src/cmd-db-branch.ts +2 -2
  35. package/src/cmd-db.ts +10 -6
  36. package/src/cmd-deploy-compose.ts +94 -0
  37. package/src/cmd-deploy-drain.ts +48 -0
  38. package/src/cmd-deploy-helm.ts +7 -6
  39. package/src/cmd-deploy-start-first.ts +178 -0
  40. package/src/cmd-deploy.ts +158 -36
  41. package/src/cmd-dev.ts +18 -356
  42. package/src/cmd-doctor-spec.ts +2 -9
  43. package/src/cmd-doctor.ts +57 -80
  44. package/src/cmd-env.ts +55 -16
  45. package/src/cmd-generate.ts +89 -32
  46. package/src/cmd-help.ts +19 -6
  47. package/src/cmd-i18n.ts +21 -17
  48. package/src/cmd-jobs-spec.ts +7 -22
  49. package/src/cmd-jobs.ts +33 -140
  50. package/src/cmd-manifest-spec.ts +1 -1
  51. package/src/cmd-manifest.ts +6 -25
  52. package/src/cmd-mcp.ts +71 -22
  53. package/src/cmd-new.ts +36 -8
  54. package/src/cmd-planned.ts +24 -8
  55. package/src/cmd-routes.ts +15 -5
  56. package/src/cmd-secrets.ts +47 -6
  57. package/src/cmd-shot-matrix.ts +22 -7
  58. package/src/cmd-shot-spec.ts +10 -2
  59. package/src/cmd-shot.ts +46 -41
  60. package/src/cmd-tasks.ts +2 -2
  61. package/src/cmd-test-spec.ts +1 -7
  62. package/src/cmd-test.ts +14 -30
  63. package/src/cmd-verify.ts +4 -4
  64. package/src/coverage-lcov.ts +4 -2
  65. package/src/db-backfill.ts +2 -2
  66. package/src/db-replay-engine.ts +4 -10
  67. package/src/db-subscribes.ts +27 -16
  68. package/src/dev-boot.ts +290 -0
  69. package/src/dev-child-watch.ts +63 -12
  70. package/src/dev-dashboard-guard.ts +76 -0
  71. package/src/dev-dashboard.ts +56 -30
  72. package/src/dev-lock.ts +100 -31
  73. package/src/dev-rebuild.ts +80 -0
  74. package/src/dev-restart-retire.ts +46 -0
  75. package/src/dev-route-table.ts +30 -8
  76. package/src/dev-supervisor.ts +114 -22
  77. package/src/dev-traces.ts +53 -20
  78. package/src/dispatch.ts +17 -13
  79. package/src/doctor-app-url.ts +39 -0
  80. package/src/doctor-auth.ts +95 -0
  81. package/src/doctor-offline.ts +6 -5
  82. package/src/document-options.ts +7 -0
  83. package/src/drift-replica-identity.ts +42 -0
  84. package/src/drift.ts +29 -6
  85. package/src/error-catalog.ts +6 -9
  86. package/src/error-codes.ts +29 -3
  87. package/src/error-page-csp.ts +14 -15
  88. package/src/error-unthrown.ts +29 -11
  89. package/src/errors.ts +54 -83
  90. package/src/exec.ts +8 -0
  91. package/src/favicon.ts +2 -2
  92. package/src/fix-command.ts +9 -1
  93. package/src/framework-env.ts +148 -0
  94. package/src/framework-schema-apply.ts +78 -0
  95. package/src/framework-schema-stamp.ts +96 -0
  96. package/src/framework-schema.ts +25 -3
  97. package/src/generate-catalog-locales.ts +79 -0
  98. package/src/generate-files.ts +4 -3
  99. package/src/generate-kinds.ts +18 -0
  100. package/src/generate-plural.ts +50 -0
  101. package/src/generate-shadow.ts +95 -0
  102. package/src/generate-write.ts +47 -13
  103. package/src/gh-target.ts +12 -7
  104. package/src/hold.ts +56 -1
  105. package/src/i18n-audit.ts +25 -9
  106. package/src/i18n-index.ts +5 -6
  107. package/src/i18n-registration.ts +7 -9
  108. package/src/import-scan.ts +28 -3
  109. package/src/index.ts +2 -6
  110. package/src/island-bundle.ts +57 -79
  111. package/src/island-capture.ts +1 -1
  112. package/src/island-duplicate-refusal.ts +57 -0
  113. package/src/island-duplicates.ts +221 -0
  114. package/src/island-entry-missing.ts +64 -0
  115. package/src/island-harness-route.ts +20 -4
  116. package/src/island-harness-script.ts +23 -2
  117. package/src/island-identity.ts +17 -14
  118. package/src/island-package-dedupe.ts +150 -0
  119. package/src/island-realtime.ts +36 -16
  120. package/src/island-routes.ts +2 -2
  121. package/src/island-runtime.ts +241 -0
  122. package/src/island-sources.ts +105 -0
  123. package/src/island-states-load.ts +6 -0
  124. package/src/island-store.ts +90 -6
  125. package/src/jobs-json.ts +0 -26
  126. package/src/jobs-report.ts +36 -20
  127. package/src/local-cli-handoff.ts +90 -0
  128. package/src/loopback-host.ts +13 -0
  129. package/src/manifest-staleness.ts +62 -0
  130. package/src/mcp-errors.ts +20 -4
  131. package/src/mcp-host.ts +22 -14
  132. package/src/mcp-test-run.ts +38 -0
  133. package/src/mcp-ui-inspect.ts +3 -2
  134. package/src/mcp-ui-interact.ts +3 -2
  135. package/src/mcp-ui.ts +8 -7
  136. package/src/measure-scope.ts +15 -10
  137. package/src/messages.ts +29 -16
  138. package/src/metrics-endpoint.ts +28 -3
  139. package/src/migration-extensions.ts +1 -1
  140. package/src/otlp-export.ts +2 -2
  141. package/src/output.ts +24 -0
  142. package/src/page-navigation.ts +7 -23
  143. package/src/page-speculation.ts +19 -31
  144. package/src/parse.ts +0 -8
  145. package/src/permission-grants.ts +3 -3
  146. package/src/posix-path.ts +30 -0
  147. package/src/prerender-locales.ts +6 -4
  148. package/src/prerender-out.ts +34 -7
  149. package/src/prerender-report.ts +58 -0
  150. package/src/prerender-served.ts +52 -0
  151. package/src/prerender.ts +71 -103
  152. package/src/pwa-artifacts.ts +33 -94
  153. package/src/pwa-manifest-locales.ts +9 -9
  154. package/src/registry.ts +2 -0
  155. package/src/retry-memo.ts +1 -1
  156. package/src/revalidated-response.ts +4 -3
  157. package/src/role-realtime.ts +7 -1
  158. package/src/role-replicator.ts +124 -29
  159. package/src/role-start.ts +69 -85
  160. package/src/role-sync.ts +119 -25
  161. package/src/root-env.ts +157 -28
  162. package/src/route-islands.ts +69 -0
  163. package/src/runtime-admin.ts +4 -1
  164. package/src/runtime-assets.ts +26 -9
  165. package/src/runtime-bindings.ts +27 -7
  166. package/src/runtime-cache.ts +46 -55
  167. package/src/runtime-hooks.ts +2 -1
  168. package/src/runtime-idempotency-scope.ts +23 -0
  169. package/src/runtime-isr-outcome.ts +100 -0
  170. package/src/runtime-isr.ts +36 -0
  171. package/src/runtime-jobs.ts +76 -41
  172. package/src/runtime-live-feed.ts +1 -1
  173. package/src/runtime-mail.ts +26 -0
  174. package/src/runtime-mfa-warning.ts +29 -0
  175. package/src/runtime-notify-retention.ts +10 -46
  176. package/src/runtime-overrides.ts +4 -4
  177. package/src/runtime-purge.ts +38 -16
  178. package/src/runtime-queue.ts +43 -22
  179. package/src/runtime-realtime.ts +11 -45
  180. package/src/runtime-render.ts +136 -114
  181. package/src/runtime-replica.ts +3 -8
  182. package/src/runtime-services.ts +49 -26
  183. package/src/runtime-storage-upload.ts +2 -2
  184. package/src/runtime-storage.ts +13 -8
  185. package/src/schema-diff.ts +16 -0
  186. package/src/schema-drift.ts +45 -15
  187. package/src/secrets-rotation.ts +8 -9
  188. package/src/seo-routes.ts +80 -9
  189. package/src/serve-boot.ts +51 -12
  190. package/src/serve-drain.ts +11 -33
  191. package/src/serve-env.ts +9 -6
  192. package/src/serve-retire.ts +95 -0
  193. package/src/serve-web.ts +31 -15
  194. package/src/serve.ts +27 -10
  195. package/src/server-barrels.ts +6 -3
  196. package/src/shot-browser.ts +1 -1
  197. package/src/shot-locale.ts +6 -33
  198. package/src/shot-settle.ts +3 -3
  199. package/src/shot-theme.ts +1 -1
  200. package/src/shot-viewport.ts +152 -0
  201. package/src/signal-shred.ts +26 -12
  202. package/src/site-asset-routes.ts +2 -2
  203. package/src/site-assets.ts +3 -5
  204. package/src/site-config.ts +9 -48
  205. package/src/site-seo.ts +7 -5
  206. package/src/sitemap-lastmod.ts +5 -0
  207. package/src/solid-loader.ts +12 -3
  208. package/src/source-files.ts +19 -3
  209. package/src/static-report.ts +5 -0
  210. package/src/stored-object-headers.ts +43 -0
  211. package/src/style-routes.ts +2 -2
  212. package/src/sw-artifacts.ts +8 -8
  213. package/src/sw-precache-plan.ts +34 -4
  214. package/src/sync-authenticator.ts +3 -3
  215. package/src/sync-url.ts +3 -3
  216. package/src/tasks-facts.ts +3 -29
  217. package/src/templates/action.ts +7 -4
  218. package/src/templates/backfill.ts +4 -4
  219. package/src/templates/entity.ts +2 -2
  220. package/src/templates/github/ci.yml.ts +28 -18
  221. package/src/templates/github/image.yml.ts +92 -0
  222. package/src/templates/github/runner.ts +18 -0
  223. package/src/templates/guard-bare-error.ts +16 -2
  224. package/src/templates/guard-unzoned-date.ts +56 -6
  225. package/src/templates/index.ts +1 -1
  226. package/src/templates/job-body-test.ts +2 -2
  227. package/src/templates/job.ts +6 -6
  228. package/src/templates/naming.ts +28 -0
  229. package/src/templates/query.ts +9 -8
  230. package/src/templates/resource-page.ts +9 -7
  231. package/src/templates/resource-service.ts +2 -2
  232. package/src/templates/route.ts +26 -14
  233. package/src/templates/scaffold-api.ts +2 -3
  234. package/src/templates/scaffold-app.ts +8 -3
  235. package/src/templates/scaffold-auth.ts +205 -70
  236. package/src/templates/scaffold-bin.ts +122 -0
  237. package/src/templates/scaffold-claude-commands.ts +11 -11
  238. package/src/templates/scaffold-container-compose.ts +10 -1
  239. package/src/templates/scaffold-container.ts +16 -8
  240. package/src/templates/scaffold-dashboard-bare.ts +8 -4
  241. package/src/templates/scaffold-dashboard-example-tests.ts +208 -0
  242. package/src/templates/scaffold-dashboard-example.ts +112 -172
  243. package/src/templates/scaffold-dashboard-shared.ts +62 -1
  244. package/src/templates/scaffold-dashboard.ts +9 -5
  245. package/src/templates/scaffold-db-client.ts +4 -2
  246. package/src/templates/scaffold-db-package.ts +2 -2
  247. package/src/templates/scaffold-docs.ts +20 -77
  248. package/src/templates/scaffold-domain-package.ts +0 -4
  249. package/src/templates/scaffold-entries.ts +4 -3
  250. package/src/templates/scaffold-env.ts +6 -3
  251. package/src/templates/scaffold-gitattributes.ts +59 -0
  252. package/src/templates/scaffold-helm-fences.ts +76 -0
  253. package/src/templates/scaffold-helm-templates.ts +100 -19
  254. package/src/templates/scaffold-helm.ts +62 -5
  255. package/src/templates/scaffold-http.ts +1 -1
  256. package/src/templates/scaffold-i18n.ts +15 -2
  257. package/src/templates/scaffold-icon.ts +2 -2
  258. package/src/templates/scaffold-repo.ts +27 -16
  259. package/src/templates/scaffold-shell.ts +55 -37
  260. package/src/test-counts.ts +2 -6
  261. package/src/test-ignores.ts +25 -0
  262. package/src/test-only-shipped.ts +193 -0
  263. package/src/test-passes.ts +0 -15
  264. package/src/test-select.ts +7 -15
  265. package/src/test-shards.ts +40 -95
  266. package/src/test-workers.ts +26 -4
  267. package/src/theme-boot.ts +8 -33
  268. package/src/theme-brand.ts +66 -0
  269. package/src/trusted-hops-error.ts +17 -0
  270. package/src/trusted-hops.ts +49 -0
  271. package/src/unscanned-admin.ts +2 -2
  272. package/src/unthrown-codes.ts +26 -0
  273. package/src/verify-checks.ts +29 -48
  274. package/src/verify-coverage-run.ts +3 -2
  275. package/src/verify-deadline.ts +207 -33
  276. package/src/verify-merge.ts +108 -43
  277. package/src/verify-part-step.ts +98 -0
  278. package/src/verify-role-load-probe.ts +12 -2
  279. package/src/verify-role-load.ts +9 -4
  280. package/src/verify-run.ts +32 -8
  281. package/src/verify-shard.ts +13 -2
  282. package/src/verify-stalled.ts +152 -0
  283. package/src/verify-step.ts +2 -0
  284. package/src/verify-test-run.ts +3 -2
  285. package/src/verify-tests.ts +5 -3
  286. package/src/worker-bundle.ts +2 -2
  287. package/src/workspace-checks.ts +53 -7
  288. package/src/workspace-graph.ts +3 -2
  289. package/src/write-line.ts +82 -7
  290. package/src/browser-launcher-fake-html.ts +0 -80
  291. package/src/browser-launcher-fake.ts +0 -168
  292. package/src/build-errors.ts +0 -54
  293. package/src/jobs-drain.ts +0 -163
  294. package/src/scaffold-typecheck.ts +0 -227
  295. package/src/thrown-by.ts +0 -24
  296. package/src/verify-errors.ts +0 -58
@@ -10,7 +10,7 @@ import { SECRETS_KEY_FILE } from '@ultimat3/core';
10
10
  import { PREBUILT_COMMAND } from '../serve-prebuilt-paths';
11
11
  import type { GeneratedFile, NameSet } from './naming';
12
12
  import { composeProdFile, PROD_ENV_FILE } from './scaffold-container-compose';
13
- import { helmFiles } from './scaffold-helm';
13
+ import { helmFiles, RUNTIME_UID } from './scaffold-helm';
14
14
 
15
15
  const dockerfile = (
16
16
  app: NameSet,
@@ -30,14 +30,18 @@ const dockerfile = (
30
30
  # install refuses. \`bunfig.toml\` survives because its \`[install]\` section is input to the install,
31
31
  # and so does a \`.tgz\`: a \`file:\` tarball dependency (a vendored or private package) is resolved
32
32
  # from the file itself, and without it the frozen install cannot find what the lockfile names.
33
- FROM oven/bun:1.4-alpine AS manifests
33
+ # The tag names the series for the reader; the digest is what builds — a tag is a pointer its
34
+ # publisher can move. Dependabot's docker ecosystem moves the digest together with the tag.
35
+ # The digest is \`oven/bun:1.4.2-alpine\`'s, the patch the framework that wrote this file was
36
+ # tested on — the same patch as \`engines.bun\` in package.json.
37
+ FROM oven/bun:1.4-alpine@sha256:d888c0ae6c86d7866ff10c5aafdd9077b36aee6455b33dd270fb93c0dd5cef6f AS manifests
34
38
  WORKDIR /app
35
39
  COPY . .
36
40
  RUN find . ! -type d ! -name package.json ! -name bunfig.toml ! -name '*.tgz' -delete \\
37
41
  && find . -type d -empty -delete
38
42
 
39
43
  # ---------- deps: runtime dependencies only, cached on the lockfile and the manifests ----------
40
- FROM oven/bun:1.4-alpine AS deps
44
+ FROM oven/bun:1.4-alpine@sha256:d888c0ae6c86d7866ff10c5aafdd9077b36aee6455b33dd270fb93c0dd5cef6f AS deps
41
45
  WORKDIR /app
42
46
  COPY --from=manifests /app ./
43
47
  COPY bun.lock ./
@@ -90,9 +94,11 @@ HEALTHCHECK --interval=10s --timeout=3s --start-period=30s --retries=3 CMD \\
90
94
  # \`.x/\` is where any binding that is still embedded keeps its state — the local storage disk, and
91
95
  # PGlite if DATABASE_URL is unset. Owned by the runtime user, because /app is not: the alternative
92
96
  # is a non-root process failing at boot on a directory it is the only one that ever writes.
93
- RUN mkdir -p /app/.x && chown -R bun:bun /app/.x
97
+ # Numeric, and the same number docker/helm/values.yaml runs every pod as: ${RUNTIME_UID} is the base
98
+ # image's \`bun\` user, and a numeric USER is one a kubelet can check against runAsNonRoot.
99
+ RUN mkdir -p /app/.x && chown -R ${RUNTIME_UID}:${RUNTIME_UID} /app/.x
94
100
 
95
- USER bun
101
+ USER ${RUNTIME_UID}:${RUNTIME_UID}
96
102
  # apps/web/server.ts reads ROLE and PORT and nothing else. \`migrate\` applies the migrations and
97
103
  # exits; every other role serves until SIGTERM.
98
104
  ENTRYPOINT ["bun", "apps/web/server.ts"]
@@ -188,9 +194,11 @@ x build --target static --out dist/static # one HTML file per \`render: 'stati
188
194
  x build --target binary --out dist/app # a single executable, no Bun install needed
189
195
  \`\`\`
190
196
 
191
- The binary bundles the framework, not the app: the registries are filled by scanning
192
- \`apps/*/{site,app,api,shared}\` at boot, so it is a launcher that must be **started from the app
193
- root**, with the source tree beside it. The image is the self-contained artifact.
197
+ The binary bundles neither the framework nor the app: it is a launcher. \`@ultimat3/*\` stays
198
+ external and loads from the app's own \`node_modules\`, the app's \`package.json\` and
199
+ \`tsconfig.json\` are read at run time, and the registries are filled by scanning
200
+ \`apps/*/{site,app,api,shared}\` at boot. So it must be **started from the app root**, with the
201
+ source tree and an installed \`node_modules\` beside it. The image is the self-contained artifact.
194
202
 
195
203
  ## A PaaS (Heroku, Render, Fly, Railway, Cloud Run, App Runner…)
196
204
 
@@ -224,10 +224,14 @@ unitTest('the tiles are the framework registries, counted', async () => {
224
224
  unitTest('the table is the route table: path, surface and render mode per route', async () => {
225
225
  const t = useT();
226
226
  const view = await renderRoute(page, { url });
227
- expect(view.html.match(/<tr data-row=/g)).toHaveLength(1);
228
- expect(view.html).toMatch(
229
- /<tr data-row="\\/dashboard"><td>\\/dashboard<\\/td><td>app<\\/td><td>ssr<\\/td>/,
230
- );
227
+ expect(view.html.match(/<tr[^>]*\\sdata-row=/g)).toHaveLength(1);
228
+ // Each cell ends in its value; on a phone a card label rides in front of it, hidden from AT.
229
+ const row = view.html.match(/<tr[^>]*data-row="\\/dashboard">([\\s\\S]*?)<\\/tr>/)?.[1] ?? '';
230
+ expect([...row.matchAll(/([^>]*)<\\/td>/g)].map((cell) => cell[1])).toEqual([
231
+ '/dashboard',
232
+ 'app',
233
+ 'ssr',
234
+ ]);
231
235
  expect(view.text).toContain(t('app.dashboard.routesCaption'));
232
236
  expect(view.meta.title).toBe(t('app.dashboard.title'));
233
237
  // The sidebar knows where it is.
@@ -0,0 +1,208 @@
1
+ // The tests the `--example` dashboard ships beside its page and its view module — split from
2
+ // `scaffold-dashboard-example.ts` to keep each file under the ceiling. Emitted, never run here: the
3
+ // generated app's own `bun test` runs them, and the scaffold gate holds that run to green.
4
+
5
+ import { sortedImports } from './imports';
6
+ import type { NameSet } from './naming';
7
+
8
+ export const exampleViewTest =
9
+ (): string => `// The numbers the dashboard shows, pinned against a fixed clock. The interesting cases are the
10
+ // window edges: a post from eight days ago is in the prior week, not this one, and a day with no
11
+ // posts is still a bar.
12
+ import { expect, unitTest } from '@ultimat3/testing';
13
+ import {
14
+ bucketByDay,
15
+ formatCount,
16
+ type PostRow,
17
+ postStats,
18
+ priceCeilings,
19
+ priceMix,
20
+ toPostRow,
21
+ } from './dashboard-view';
22
+
23
+ const NOW = new Date('2026-03-15T12:00:00.000Z');
24
+ const DAY_MS = 86_400_000;
25
+
26
+ const row = (id: string, daysAgo: number, minor = 0, currency = 'USD'): PostRow => ({
27
+ id,
28
+ title: \`post \${id}\`,
29
+ price: { minor, currency },
30
+ createdAt: new Date(NOW.getTime() - daysAgo * DAY_MS).toISOString(),
31
+ });
32
+
33
+ const rows = [row('a', 1), row('b', 3), row('c', 8)];
34
+
35
+ unitTest('postStats counts the week, compares it with the prior one, and counts today', () => {
36
+ const stats = postStats(rows, NOW);
37
+ expect(stats.total).toBe(3);
38
+ expect(stats.lastWeek).toBe(2);
39
+ // Two this week against one the week before: +100%, and the chip says so.
40
+ expect(stats.delta).toEqual({ text: '+100%', trend: 'up' });
41
+ // 'a' is yesterday and nothing was created on 03-15 itself.
42
+ expect(stats.today).toBe(0);
43
+ expect(postStats([...rows, row('d', 0.25)], NOW).today).toBe(1);
44
+ });
45
+
46
+ unitTest('a zero baseline yields no delta, and no rows count nothing', () => {
47
+ expect(postStats([row('a', 1)], NOW).delta).toBeUndefined();
48
+ expect(postStats([], NOW)).toEqual({ total: 0, lastWeek: 0, delta: undefined, today: 0 });
49
+ });
50
+
51
+ unitTest('bucketByDay is one bar per day, oldest first, quiet days included', () => {
52
+ const points = bucketByDay(rows, NOW, 14);
53
+ expect(points).toHaveLength(14);
54
+ expect(points.at(-1)?.key).toBe('03-15');
55
+ expect(points[0]?.key).toBe('03-02');
56
+ expect(points.reduce((sum, point) => sum + point.value, 0)).toBe(3);
57
+ // Eight days ago is 03-07: inside a 14-day window, so the bar is there.
58
+ expect(points.find((point) => point.key === '03-07')?.value).toBe(1);
59
+ expect(bucketByDay([], NOW, 3).map((point) => point.value)).toEqual([0, 0, 0]);
60
+ });
61
+
62
+ unitTest('toPostRow serialises a Date, passes ISO text through, and keeps the price whole', () => {
63
+ const at = new Date('2026-01-02T03:04:05.000Z');
64
+ const price = { minor: 1900, currency: 'USD' };
65
+ expect(toPostRow({ id: 'x', title: 't', price, createdAt: at }).createdAt).toBe(at.toISOString());
66
+ expect(toPostRow({ id: 'x', title: 't', price, createdAt: '2026-01-01' })).toEqual({
67
+ id: 'x',
68
+ title: 't',
69
+ price,
70
+ createdAt: '2026-01-01',
71
+ });
72
+ });
73
+
74
+ unitTest('priceMix splits free from paid, and priceCeilings is the dearest per currency', () => {
75
+ const priced = [row('a', 1, 0), row('b', 1, 1900), row('c', 1, 500), row('d', 1, 300, 'JPY')];
76
+ expect(priceMix(priced)).toEqual({ free: 1, paid: 3 });
77
+ expect(priceMix([])).toEqual({ free: 0, paid: 0 });
78
+ // 300 yen is never measured against 1,900 cents: each currency has its own ceiling.
79
+ expect([...priceCeilings(priced)]).toEqual([
80
+ ['USD', 1900],
81
+ ['JPY', 300],
82
+ ]);
83
+ });
84
+
85
+ unitTest('formatCount follows the locale', () => {
86
+ expect(formatCount(1234, 'en')).toBe('1,234');
87
+ expect(formatCount(1234, 'de')).toBe('1.234');
88
+ });
89
+ `;
90
+
91
+ export const examplePageTest = (
92
+ app: NameSet,
93
+ ): string => `// The dashboard, rendered as a request renders it: \`load\` reads the actor's org through the
94
+ // slice's query — the in-memory driver under test — and the page turns the rows into tiles, a chart
95
+ // and a table. Losing a policy is the other regression worth a test: the page's own, and the read's
96
+ // — \`dashboard:read\` opens the page, and only \`post:read\` shows its posts.
97
+ ${sortedImports([
98
+ `import { driver } from '@${app.kebab}/db';`,
99
+ `import { useT } from '@${app.kebab}/i18n';`,
100
+ "import { ctxOf, frozenClock, isUltimateError, runWithContext } from '@ultimat3/core';",
101
+ "import { testActor } from '@ultimat3/policy';",
102
+ "import { dbUnavailable } from '@ultimat3/db';",
103
+ "import { afterEach, expect, renderRoute, unitTest } from '@ultimat3/testing';",
104
+ ])}
105
+ // The app's API, as boot loads it: \`defineApi\` is what names the query the page reads, and a
106
+ // query with no name is refused (X_QUERY_UNREGISTERED) before it reads a row.
107
+ import '../../api';
108
+ import { DEMO_ORG_ID } from '../../shared/demo-org';
109
+ import * as repo from '../post/repo';
110
+ import * as page from './page';
111
+
112
+ const url = 'https://example.test/dashboard';
113
+ // The read grant, a direct one: what a \`member\` holds in apps/web/shared/roles.ts.
114
+ const read = 'post:read';
115
+ const viewer = testActor('viewer', { orgId: DEMO_ORG_ID, permissions: [read] }).actor;
116
+ const elsewhere = '00000000-0000-4000-8000-000000000009';
117
+ const stranger = testActor('stranger', { orgId: elsewhere, permissions: [read] }).actor;
118
+
119
+ const DAY_MS = 86_400_000;
120
+
121
+ /**
122
+ * One post in the viewer's org, written the way the app writes one — \`daysAgo\` days before now.
123
+ * The request's clock is what stamps \`createdAt\`, so the test chooses the instant.
124
+ */
125
+ const post = (title: string, daysAgo: number, minor = 0) => {
126
+ const clock = frozenClock(new Date(Date.now() - daysAgo * DAY_MS));
127
+ const draft = { orgId: DEMO_ORG_ID, title, price: { minor, currency: 'USD' } };
128
+ return runWithContext(ctxOf({ actor: viewer, clock }), () => repo.insert(draft));
129
+ };
130
+
131
+ /** The figure a tile shows, read off the markup by the \`stat\` the page gave it. */
132
+ const stat = (html: string, name: string): string | undefined =>
133
+ html.match(new RegExp(\`data-stat="\${name}">([^<]*)<\`))?.[1];
134
+
135
+ // One store per process: without this, one test's rows are the next test's fixtures.
136
+ afterEach(() => {
137
+ driver.reset?.();
138
+ });
139
+
140
+ unitTest('the dashboard is gated, renders per request, and hydrates one island', async () => {
141
+ const view = await renderRoute(page, { url, actor: viewer });
142
+ expect(page.config.render).toBe('ssr');
143
+ expect(page.config.policy?.permission).toBe('dashboard:read');
144
+ expect(page.config.offline).toBe('runtime');
145
+ expect(page.config.budget.js).toBe('60kb');
146
+ // What the render emitted, beside what the config declares: the theme toggle, and only it.
147
+ expect(view.islands.map((island) => [island.moduleId, island.strategy])).toEqual([
148
+ ['shared-theme-toggle', 'visible'],
149
+ ]);
150
+ });
151
+
152
+ unitTest('it counts and lists the posts of the org that is looking', async () => {
153
+ await post('Last month', 30);
154
+ await post('This week', 3, 1900);
155
+ await post('Today', 0);
156
+ const view = await renderRoute(page, { url, actor: viewer });
157
+ // Newest first, as the query orders them — and each tile counts its own window.
158
+ expect(view.data.rows.map((row) => row.title)).toEqual(['Today', 'This week', 'Last month']);
159
+ expect(stat(view.html, 'posts-total')).toBe('3');
160
+ expect(stat(view.html, 'posts-week')).toBe('2');
161
+ expect(stat(view.html, 'posts-today')).toBe('1');
162
+ expect(stat(view.html, 'posts-paid')).toBe('1');
163
+ // One table row per post, each price with its in-cell bar.
164
+ expect(view.html.match(/<tr[^>]*\\sdata-row=/g)).toHaveLength(3);
165
+ expect(view.html.match(/data-inline-bar-value/g)).toHaveLength(3);
166
+ expect(view.text).toContain('$19.00');
167
+ // The trend is one filled series; the ring draws free and paid; the dial is a meter.
168
+ expect(view.html.match(/data-area="true"/g)).toHaveLength(1);
169
+ expect(view.html.match(/data-segment="/g)).toHaveLength(2);
170
+ expect(view.html).toContain('role="meter"');
171
+ expect(view.text).toContain('This week');
172
+ // The sidebar knows where it is.
173
+ expect(view.html).toMatch(/<a href="\\/dashboard" aria-current="page"/);
174
+ });
175
+
176
+ unitTest('another org sees none of them: the page says so instead of an empty table', async () => {
177
+ const t = useT();
178
+ await post('Not theirs', 0);
179
+ const view = await renderRoute(page, { url, actor: stranger });
180
+ expect(view.data.rows).toEqual([]);
181
+ expect(stat(view.html, 'posts-total')).toBe('0');
182
+ expect(view.html).not.toMatch(/<tr[^>]*\\sdata-row=/);
183
+ expect(view.text).toContain(t('app.dashboard.emptyTitle'));
184
+ expect(view.meta.title).toBe(t('app.dashboard.title'));
185
+ expect(view.meta.description).toBe(t('app.dashboard.description'));
186
+ });
187
+
188
+ unitTest('a viewer the post read refuses is refused, never shown the org’s posts', async () => {
189
+ await post('Not for them', 0);
190
+ // In the org, onto the page, without \`post:read\`: the query's policy decides, not the page's.
191
+ const outsider = testActor('outsider', { orgId: DEMO_ORG_ID }).actor;
192
+ const rendering = renderRoute(page, { url, actor: outsider });
193
+ const refused = await rendering.catch((error: unknown) => error);
194
+ expect(isUltimateError(refused) && refused.code).toBe('X_FORBIDDEN');
195
+ });
196
+
197
+ unitTest('the build measures the empty page: no database is not a failure there', async () => {
198
+ const measured = await page.dashboardData(() => Promise.reject(dbUnavailable('no database')));
199
+ // The clock is frozen under test, so the instant the windows are cut at is this one.
200
+ expect(measured).toEqual({ rows: [], now: new Date().toISOString() });
201
+ });
202
+
203
+ unitTest('any other failure of the read propagates', async () => {
204
+ const cause = new TypeError('the read broke');
205
+ const read = (): Promise<never> => Promise.reject(cause);
206
+ expect(await page.dashboardData(read).catch((error: unknown) => error)).toBe(cause);
207
+ });
208
+ `;