@ultimat3/cli 22.15.0 → 23.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 (169) hide show
  1. package/CLAUDE.md +23 -8
  2. package/README.md +28 -5
  3. package/package.json +29 -28
  4. package/src/admin-catalog-keys.ts +16 -0
  5. package/src/admin-registration.ts +192 -0
  6. package/src/api-registration.ts +36 -11
  7. package/src/app-boundaries.ts +6 -20
  8. package/src/app-load.ts +55 -11
  9. package/src/app-manifest.ts +14 -0
  10. package/src/app-permissions.ts +0 -0
  11. package/src/app-reload-graph.ts +7 -30
  12. package/src/app-transport.ts +219 -0
  13. package/src/boundary-findings.ts +16 -4
  14. package/src/browser-transport.ts +160 -0
  15. package/src/budgets.ts +25 -1
  16. package/src/cmd-build-spec.ts +10 -3
  17. package/src/cmd-build.ts +56 -9
  18. package/src/cmd-db.ts +60 -11
  19. package/src/cmd-doctor.ts +35 -11
  20. package/src/cmd-generate.ts +118 -17
  21. package/src/cmd-jobs-spec.ts +19 -3
  22. package/src/cmd-jobs.ts +120 -3
  23. package/src/cmd-new.ts +15 -1
  24. package/src/cmd-routes.ts +15 -1
  25. package/src/cmd-secrets-ring.ts +76 -0
  26. package/src/cmd-secrets-spec.ts +10 -2
  27. package/src/cmd-secrets.ts +48 -12
  28. package/src/cmd-tasks.ts +84 -15
  29. package/src/cmd-verify.ts +10 -2
  30. package/src/comment-safe.ts +16 -0
  31. package/src/coverage-floor.ts +334 -0
  32. package/src/coverage-lcov.ts +193 -0
  33. package/src/coverage-source.ts +136 -0
  34. package/src/db-object-drift.ts +30 -0
  35. package/src/db-replay-engine.ts +115 -0
  36. package/src/db-schema-dump.ts +150 -0
  37. package/src/db-schema-refresh.ts +122 -0
  38. package/src/dev-dashboard.ts +2 -2
  39. package/src/dev-route-table.ts +9 -0
  40. package/src/doctor-coverage.ts +39 -0
  41. package/src/doctor-guards.ts +54 -0
  42. package/src/doctor-sealed.ts +132 -0
  43. package/src/document-graph.ts +82 -0
  44. package/src/error-codes.ts +32 -0
  45. package/src/framework-schema.ts +15 -0
  46. package/src/generate-files.ts +76 -10
  47. package/src/generate-grant-findings.ts +76 -0
  48. package/src/generate-grants.ts +77 -3
  49. package/src/generate-kinds.ts +19 -0
  50. package/src/generate-write.ts +15 -27
  51. package/src/generated-imports.ts +59 -0
  52. package/src/guard-sources.ts +104 -0
  53. package/src/guards.ts +23 -7
  54. package/src/handle-registration.ts +217 -0
  55. package/src/i18n-registration.ts +36 -0
  56. package/src/image-prepare.ts +51 -8
  57. package/src/import-closure.ts +165 -0
  58. package/src/index.ts +11 -2
  59. package/src/island-store.ts +24 -16
  60. package/src/jobs-drain.ts +14 -7
  61. package/src/jobs-json.ts +50 -0
  62. package/src/jobs-report.ts +3 -0
  63. package/src/json-merge.ts +46 -0
  64. package/src/mcp-errors.ts +29 -1
  65. package/src/messages.ts +33 -0
  66. package/src/metrics-endpoint.ts +71 -24
  67. package/src/migration-extensions.ts +29 -0
  68. package/src/module-imports.ts +47 -0
  69. package/src/output.ts +18 -9
  70. package/src/permission-grants.ts +41 -9
  71. package/src/publish-closure.ts +101 -0
  72. package/src/role-load.ts +143 -0
  73. package/src/role-start.ts +18 -1
  74. package/src/role-wake.ts +45 -0
  75. package/src/runtime-admin.ts +141 -0
  76. package/src/runtime-hooks.ts +5 -2
  77. package/src/runtime-queue.ts +8 -1
  78. package/src/runtime-render.ts +6 -0
  79. package/src/runtime-services.ts +2 -1
  80. package/src/scaffold-typecheck.ts +1 -0
  81. package/src/schema-dump-drift.ts +64 -0
  82. package/src/serve-boot.ts +85 -157
  83. package/src/serve-prebuilt-paths.ts +19 -0
  84. package/src/serve-prebuilt.ts +84 -0
  85. package/src/serve-web.ts +164 -0
  86. package/src/serve.ts +6 -9
  87. package/src/server-barrels.ts +54 -0
  88. package/src/source-edit.ts +49 -0
  89. package/src/tasks-facts.ts +4 -1
  90. package/src/templates/action.ts +144 -11
  91. package/src/templates/admin-catalog.ts +58 -0
  92. package/src/templates/admin-page.ts +36 -8
  93. package/src/templates/admin.ts +57 -17
  94. package/src/templates/backfill.ts +121 -87
  95. package/src/templates/entity.ts +140 -44
  96. package/src/templates/guard-animated-layout-property.ts +4 -11
  97. package/src/templates/guard-bare-error.ts +9 -13
  98. package/src/templates/guard-focus-visible.ts +4 -11
  99. package/src/templates/guard-image-dimensions.ts +6 -6
  100. package/src/templates/guard-island-without-states.ts +2 -4
  101. package/src/templates/guard-raw-breakpoint.ts +210 -0
  102. package/src/templates/guard-raw-colour.ts +4 -11
  103. package/src/templates/guard-raw-length.ts +375 -0
  104. package/src/templates/guard-raw-motion.ts +213 -0
  105. package/src/templates/guard-raw-shadow.ts +187 -0
  106. package/src/templates/guard-raw-z-index.ts +166 -0
  107. package/src/templates/guard-repo-raw-sql.ts +345 -0
  108. package/src/templates/guard-semantic-interactive.ts +6 -6
  109. package/src/templates/guard-undeclared-custom-property.ts +217 -0
  110. package/src/templates/guard-undefined-style-class.ts +215 -0
  111. package/src/templates/guard-untranslated-string.ts +6 -6
  112. package/src/templates/guard-unzoned-date.ts +9 -13
  113. package/src/templates/index.ts +3 -2
  114. package/src/templates/island.ts +34 -51
  115. package/src/templates/job-body-test.ts +121 -0
  116. package/src/templates/job.ts +23 -8
  117. package/src/templates/policy.ts +16 -9
  118. package/src/templates/query.ts +242 -26
  119. package/src/templates/resource-create.ts +62 -18
  120. package/src/templates/resource-form-island.ts +58 -80
  121. package/src/templates/resource-page.ts +269 -0
  122. package/src/templates/resource-service.ts +96 -0
  123. package/src/templates/resource.ts +86 -61
  124. package/src/templates/route.ts +38 -29
  125. package/src/templates/scaffold-api.ts +55 -1
  126. package/src/templates/scaffold-app.ts +171 -51
  127. package/src/templates/scaffold-auth.ts +83 -19
  128. package/src/templates/scaffold-browser-client.ts +90 -0
  129. package/src/templates/scaffold-container.ts +16 -0
  130. package/src/templates/scaffold-dashboard-bare.ts +64 -2
  131. package/src/templates/scaffold-dashboard-example.ts +124 -31
  132. package/src/templates/scaffold-dashboard-shared.ts +1 -20
  133. package/src/templates/scaffold-dashboard.ts +1 -2
  134. package/src/templates/scaffold-db-client.ts +142 -0
  135. package/src/templates/scaffold-db-package.ts +90 -8
  136. package/src/templates/scaffold-docs.ts +50 -5
  137. package/src/templates/scaffold-guards.ts +54 -11
  138. package/src/templates/scaffold-http.ts +9 -7
  139. package/src/templates/scaffold-repo.ts +52 -2
  140. package/src/templates/scaffold-roles.ts +56 -9
  141. package/src/templates/scaffold-shell.ts +36 -70
  142. package/src/templates/scaffold-site.ts +45 -19
  143. package/src/templates/scaffold-ui-package.ts +18 -0
  144. package/src/templates/slice-foundation.ts +46 -8
  145. package/src/test-counts.ts +18 -2
  146. package/src/transport-calls.ts +105 -0
  147. package/src/unscanned-admin.ts +54 -0
  148. package/src/verify-checks.ts +26 -7
  149. package/src/verify-coverage-run.ts +190 -0
  150. package/src/verify-deadline.ts +256 -0
  151. package/src/verify-errors.ts +15 -6
  152. package/src/verify-floor.ts +36 -9
  153. package/src/verify-merge.ts +117 -24
  154. package/src/verify-progress.ts +25 -0
  155. package/src/verify-role-load-probe.ts +54 -0
  156. package/src/verify-role-load.ts +148 -0
  157. package/src/verify-run.ts +93 -35
  158. package/src/verify-shard.ts +20 -9
  159. package/src/verify-step.ts +42 -1
  160. package/src/verify-tests.ts +78 -6
  161. package/src/workspace-checks.ts +50 -8
  162. package/src/workspace-dep-edit.ts +127 -0
  163. package/src/cmd-dev-fixture.ts +0 -204
  164. package/src/dev-live-fixture.ts +0 -122
  165. package/src/policy-fixture.ts +0 -78
  166. package/src/realtime-browser-probe-fixture.ts +0 -9
  167. package/src/role-start-fixture.ts +0 -78
  168. package/src/scaffold-fixture.ts +0 -160
  169. package/src/templates/island-fixture.ts +0 -76
@@ -0,0 +1,127 @@
1
+ // One workspace declaring another in its `package.json`, as a TEXT edit. A generator that makes
2
+ // `packages/db` import the web app owes the manifest line `package-shape` would otherwise demand
3
+ // (`X_WORKSPACE_DEP_UNDECLARED`), and re-serialising the whole file would reformat arrays and
4
+ // spacing the author — or their formatter — chose.
5
+
6
+ import { containedPath } from './generate-write';
7
+
8
+ const MANIFEST = 'package.json';
9
+
10
+ /** The fields a manifest edit reads. Anything else in the file is carried through untouched. */
11
+ interface Manifest {
12
+ readonly name?: unknown;
13
+ readonly version?: unknown;
14
+ readonly dependencies?: unknown;
15
+ readonly devDependencies?: unknown;
16
+ readonly peerDependencies?: unknown;
17
+ }
18
+
19
+ function parseManifest(text: string): Manifest | undefined {
20
+ try {
21
+ const parsed: unknown = JSON.parse(text);
22
+ return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
23
+ ? (parsed as Manifest)
24
+ : undefined;
25
+ } catch {
26
+ // A manifest that does not parse is `bun install`'s finding, and `package-shape`'s.
27
+ return undefined;
28
+ }
29
+ }
30
+
31
+ const stringRecord = (value: unknown): Readonly<Record<string, string>> =>
32
+ typeof value === 'object' && value !== null
33
+ ? Object.fromEntries(
34
+ Object.entries(value).filter(
35
+ (entry): entry is [string, string] => typeof entry[1] === 'string',
36
+ ),
37
+ )
38
+ : {};
39
+
40
+ const declares = (manifest: Manifest, name: string): boolean =>
41
+ [manifest.dependencies, manifest.devDependencies, manifest.peerDependencies].some((block) =>
42
+ Object.hasOwn(stringRecord(block), name),
43
+ );
44
+
45
+ /**
46
+ * `manifest` with `name` declared under `dependencies`, or `undefined` when it already is — or
47
+ * when the text is not a manifest this can edit safely. The block is rewritten from its parsed
48
+ * entries, sorted the way `bun add` sorts them; every byte outside it is the author's.
49
+ */
50
+ export function withDependency(
51
+ manifest: string,
52
+ name: string,
53
+ version: string,
54
+ ): string | undefined {
55
+ const parsed = parseManifest(manifest);
56
+ if (parsed === undefined || declares(parsed, name)) return undefined;
57
+ const block = /^([ \t]*)"dependencies":\s*\{/m.exec(manifest);
58
+ if (block === null) {
59
+ const close = manifest.lastIndexOf('}');
60
+ const body = manifest.slice(0, close).trimEnd();
61
+ // `{}` has no property to put a comma after; nothing a scaffold writes is that.
62
+ if (close === -1 || body.endsWith('{')) return undefined;
63
+ return `${body},\n "dependencies": {\n "${name}": "${version}"\n }\n}\n`;
64
+ }
65
+ const indent = block[1] ?? '';
66
+ const open = block.index + block[0].length;
67
+ const close = manifest.indexOf('}', open);
68
+ if (close === -1) return undefined;
69
+ const entries = Object.entries({ ...stringRecord(parsed.dependencies), [name]: version })
70
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
71
+ .map(([key, value]) => `${indent} ${JSON.stringify(key)}: ${JSON.stringify(value)}`);
72
+ return `${manifest.slice(0, open)}\n${entries.join(',\n')}\n${indent}${manifest.slice(close)}`;
73
+ }
74
+
75
+ /** A workspace's `name` and `version`, or `undefined` when its manifest supplies no name. */
76
+ export async function readWorkspace(
77
+ root: string,
78
+ dir: string,
79
+ ): Promise<{ readonly name: string; readonly version: string } | undefined> {
80
+ const file = Bun.file(containedPath(root, `${dir}/${MANIFEST}`));
81
+ if (!(await file.exists())) return undefined;
82
+ const parsed = parseManifest(await file.text());
83
+ if (parsed === undefined || typeof parsed.name !== 'string' || parsed.name === '') {
84
+ return undefined;
85
+ }
86
+ return {
87
+ name: parsed.name,
88
+ version: typeof parsed.version === 'string' ? parsed.version : '0.0.0',
89
+ };
90
+ }
91
+
92
+ /** One file a caller is about to write: its app-root-relative path and its whole new text. */
93
+ export interface PlannedEdit {
94
+ readonly path: string;
95
+ readonly contents: string;
96
+ }
97
+
98
+ /**
99
+ * The edit that declares the workspace at `toDir` in `fromDir`'s manifest, NOT yet written — a
100
+ * registrar plans every file it touches and writes all of them or none. `undefined` when the edge
101
+ * is already declared or either manifest is unreadable; the `package-shape` step then names the
102
+ * line, which is the same edit by hand.
103
+ */
104
+ export async function planWorkspaceDependency(
105
+ root: string,
106
+ fromDir: string,
107
+ toDir: string,
108
+ ): Promise<PlannedEdit | undefined> {
109
+ const target = await readWorkspace(root, toDir);
110
+ const path = `${fromDir}/${MANIFEST}`;
111
+ const file = Bun.file(containedPath(root, path));
112
+ if (target === undefined || !(await file.exists())) return undefined;
113
+ const contents = withDependency(await file.text(), target.name, target.version);
114
+ return contents === undefined ? undefined : { path, contents };
115
+ }
116
+
117
+ /** Declares it now. Answers the path it rewrote, or `undefined` when there was nothing to write. */
118
+ export async function declareWorkspaceDependency(
119
+ root: string,
120
+ fromDir: string,
121
+ toDir: string,
122
+ ): Promise<string | undefined> {
123
+ const edit = await planWorkspaceDependency(root, fromDir, toDir);
124
+ if (edit === undefined) return undefined;
125
+ await Bun.write(containedPath(root, edit.path), edit.contents);
126
+ return edit.path;
127
+ }
@@ -1,204 +0,0 @@
1
- // The app `x dev` boots in its tests, and the one place it is declared: every assertion that
2
- // needs a booted app lives in `cmd-dev.test.ts`, because a process has ONE lifecycle — the second
3
- // in-process boot is refused (X_LIFECYCLE_DRAINED) — and the coverage gate runs a package in one
4
- // process. So the fixture is a module of its own, and that file keeps its line budget for tests.
5
- //
6
- // What the app declares, and what each declaration is here to prove:
7
- // app.config.ts the root marker a real `x dev` cannot start without; `ai.mcp` by default
8
- // apps/web/mcp.ts the app's own MCP endpoint, mounted by the web role
9
- // apps/web/runtime.ts the app's middleware and disks, reaching a development process
10
- // .git/HEAD the fixture is its own repository — see the entry below
11
- // apps/web/app/hello/* an SSR page with a component, edited on disk while `x dev` runs
12
- // apps/web/app/notes/* a memory-backed entity and a live query, fed by the in-process bridge
13
- // apps/web/app/posts/* an action, a policy and a query, mounted as HTTP routes
14
- // apps/web/site/pricing/* a static page with its own stylesheet, under the CSP `x dev` sends
15
- import { resetRegistry as resetActions } from '@ultimat3/action';
16
- import { clearRegistry as clearEntities } from '@ultimat3/entity';
17
- import { resetJobs, resetTasks } from '@ultimat3/jobs';
18
- import { clearPermissions, clearRoles } from '@ultimat3/policy';
19
- import { resetRegistry as resetQueries } from '@ultimat3/query';
20
- import type { Frame } from '@ultimat3/realtime';
21
- import { decode } from '@ultimat3/realtime';
22
- import type { WsLike } from '@ultimat3/realtime/server';
23
- import { clearRoutes } from '@ultimat3/render';
24
- import { resetAppLoad } from './app-load';
25
-
26
- export const DEV_FIXTURE_FILES: Readonly<Record<string, string>> = {
27
- 'package.json': JSON.stringify({ name: 'dev-fixture', version: '1.4.0' }),
28
-
29
- // Its own repository, so the ignore walk stops HERE. The framework's root `.gitignore` lists
30
- // `packages/cli/.dev-fixture/`, and `devIgnore` honours every ancestor up to a `.git` — without
31
- // this marker the watcher admitted the fixture root and nothing under it, so no save in this
32
- // tree ever reached `rebuild`, and the reload path was booted by every run and exercised by none.
33
- '.git/HEAD': 'ref: refs/heads/main\n',
34
-
35
- // The root marker a real `x dev` cannot start without, and where `ai.mcp` is declared — by
36
- // default `{ expose: true, path: '/mcp' }`, which is what the MCP mount reads.
37
- 'app.config.ts': `import { defineConfig } from '@ultimat3/core';
38
- export const config = defineConfig({ name: 'dev-fixture' });
39
- `,
40
-
41
- // The app's own MCP endpoint, in the contract `app-mcp.ts` reads. `resolveToken` answering
42
- // `null` rejects every bearer, which is enough to prove the ROUTE is mounted (401, not 404).
43
- 'apps/web/mcp.ts': `import { defineAppMcp } from '@ultimat3/mcp';
44
- export const mcp = defineAppMcp({ include: 'exposed', resolveToken: () => null });
45
- `,
46
-
47
- // The app's own middleware, in the contract `app-runtime.ts` reads: a header on every response
48
- // is the cheapest proof that the chain `x dev` composed is the app's and not only the replica's.
49
- // Its disks too: `x dev` built its own embedded disk and ignored this one until 22.4.
50
- 'apps/web/runtime.ts': `import { defineStorage, localDriver } from '@ultimat3/storage';
51
- const stamp = async (request, ctx, next) => {
52
- const response = await next(request, ctx);
53
- const headers = new Headers(response.headers);
54
- headers.set('x-dev-runtime', 'app');
55
- return new Response(response.body, { status: response.status, headers });
56
- };
57
- const storage = defineStorage({
58
- disks: { appdisk: localDriver({ root: new URL('../../.app-disk', import.meta.url).pathname }) },
59
- default: 'appdisk',
60
- });
61
- export const runtime = { middleware: [stamp], storage };
62
- `,
63
-
64
- // A memory-backed entity, so the fixture needs no migration: the row observer sits on
65
- // `database()`'s repo wrapper, the same seam a Postgres-backed repo writes through.
66
- 'apps/web/app/notes/entity.ts': `import { database, entity, memoryDriver, text, uuid } from '@ultimat3/entity';
67
- export const notes = entity('notes', { columns: { id: uuid().primaryKey(), title: text() } });
68
- export const db = database({ notes }, { driver: memoryDriver() });
69
- `,
70
- 'apps/web/app/notes/live.ts': `import { allow } from '@ultimat3/policy';
71
- import { from, query, t } from '@ultimat3/query';
72
- import { db } from './entity';
73
- export const liveNotes = query({
74
- input: t.object({}),
75
- policy: allow('public'),
76
- live: true,
77
- subscribes: ['notes'],
78
- sql: () =>
79
- from<{ id: string; title: string }>('notes', () => db.notes.where({}).all())
80
- .orderBy('id')
81
- .limit(50),
82
- });
83
- `,
84
-
85
- 'apps/web/app/posts/policy.ts': `import { allow, can, definePermissions, defineRoles } from '@ultimat3/policy';
86
- export const permissions = definePermissions(['post:publish'] as const);
87
- export const roles = defineRoles({
88
- author: { grants: ['post:publish'] },
89
- reader: { grants: [] },
90
- });
91
- export const canPostWrite = can('post:publish');
92
- export const anyone = allow();
93
- `,
94
-
95
- 'apps/web/app/posts/actions.ts': `import { action, t } from '@ultimat3/action';
96
- import { anyone, canPostWrite } from './policy';
97
-
98
- export const publishPost = action({
99
- input: t.object({ id: t.uuid }),
100
- output: t.object({ id: t.uuid }),
101
- policy: canPostWrite,
102
- async handle({ input }) {
103
- return { id: input.id };
104
- },
105
- });
106
-
107
- export const echoPost = action({
108
- input: t.object({ word: t.string }),
109
- output: t.object({ word: t.string }),
110
- policy: anyone,
111
- async handle({ input }) {
112
- return { word: input.word };
113
- },
114
- });
115
- `,
116
-
117
- // A page with a body, on the surface an author edits all day. What the reload test rewrites: the
118
- // module must be imported again for the new body to be served, which `import()` alone never does.
119
- 'apps/web/app/hello/page.tsx': `import { defineRoute } from '@ultimat3/render';
120
-
121
- export const config = defineRoute({
122
- render: 'ssr',
123
- hydrate: 'visible',
124
- offline: 'runtime',
125
- budget: { js: '60kb' },
126
- meta: () => ({ title: 'Hello', description: 'A page that is edited while x dev runs' }),
127
- });
128
-
129
- export function Page() {
130
- return <p>generation one</p>;
131
- }
132
- `,
133
-
134
- // An ISR page whose body is a COMPONENT in another file — the shape every real page has. What the
135
- // component-reload test rewrites: the page module's own bytes never change, so only evicting what
136
- // imports the component re-evaluates it, and only clearing the ISR store stops the first render
137
- // from answering for the rest of the process (notificado.co, 2026-09-27).
138
- 'apps/web/site/news/headline.tsx': `export function Headline() {
139
- return <h1>headline one</h1>;
140
- }
141
- `,
142
-
143
- 'apps/web/site/news/page.tsx': `import { defineRoute } from '@ultimat3/render';
144
- import { Headline } from './headline';
145
-
146
- export const config = defineRoute({
147
- render: 'isr',
148
- revalidate: { ttl: '1h' },
149
- offline: 'runtime',
150
- hydrate: 'never',
151
- budget: { js: '0kb' },
152
- meta: () => ({ title: 'News', description: 'A page whose component is edited while x dev runs' }),
153
- });
154
-
155
- export function Page() {
156
- return <main><Headline /></main>;
157
- }
158
- `,
159
-
160
- // A stylesheet the page imports, because that import is what registers it — and the document's
161
- // inline `<style>` is what the CSP has to name. Without one this file served no styled page and
162
- // could not have caught the policy that blanked every deployed app.
163
- 'apps/web/site/pricing/page.module.scss': `.price { color: #123456; }
164
- `,
165
-
166
- 'apps/web/site/pricing/page.tsx': `import { defineRoute } from '@ultimat3/render';
167
- import './page.module.scss';
168
-
169
- export const config = defineRoute({
170
- render: 'static',
171
- offline: 'precache',
172
- hydrate: 'never',
173
- budget: { js: '0kb' },
174
- meta: () => ({ title: 'Pricing', description: 'What it costs' }),
175
- });
176
- `,
177
- };
178
-
179
- export const resetRegistries = (): void => {
180
- resetActions();
181
- resetQueries();
182
- clearEntities();
183
- clearRoutes();
184
- resetJobs();
185
- resetTasks();
186
- clearPermissions();
187
- clearRoles();
188
- resetAppLoad();
189
- };
190
-
191
- /** A `WsLike` that keeps every frame the node sends, decoded — what a live assertion reads. */
192
- export class FakeWs implements WsLike {
193
- readonly frames: Frame[] = [];
194
- send(data: string): number {
195
- this.frames.push(decode(data));
196
- return data.length;
197
- }
198
- close(): void {}
199
- subscribe(): void {}
200
- unsubscribe(): void {}
201
- getBufferedAmount(): number {
202
- return 0;
203
- }
204
- }
@@ -1,122 +0,0 @@
1
- // The `x dev` live tests' process discipline, in one place: spawn the supervisor, read its two
2
- // streams, and — on every path, a failing one included — reap every process it started and prove
3
- // none is left. Two children of `cmd-dev-restart.live.test.ts` ran for six hours with ppid 1 and a
4
- // deleted cwd because the test's `finally` SIGKILLed the supervisor, which cannot forward a signal
5
- // it never gets, and nothing then looked for what it had started (2026-09-29).
6
-
7
- import { readdirSync, readlinkSync } from 'node:fs'; // why: /proc is read synchronously; Bun has no readlink.
8
-
9
- /** One pump per stream — reading a stream twice closes its reader under the first `for await`. */
10
- export function pump(stream: ReadableStream<Uint8Array>): { seen: () => string } {
11
- const decoder = new TextDecoder();
12
- let seen = '';
13
- void (async () => {
14
- for await (const chunk of stream) seen += decoder.decode(chunk, { stream: true });
15
- })();
16
- return { seen: () => seen };
17
- }
18
-
19
- /** Poll the buffer until `marker` shows up. The caller's own timeout is the deadline. */
20
- export async function waitFor(output: { seen: () => string }, marker: string): Promise<string> {
21
- for (;;) {
22
- const seen = output.seen();
23
- if (seen.includes(marker)) return seen;
24
- await Bun.sleep(25);
25
- }
26
- }
27
-
28
- /** Whether `pid` is still a process — signal 0 delivers nothing and only asks. */
29
- export function alive(pid: number): boolean {
30
- try {
31
- process.kill(pid, 0);
32
- return true;
33
- } catch {
34
- return false;
35
- }
36
- }
37
-
38
- /** Every descendant of `pid`, depth first — asked while `pid` lives, or its children are init's. */
39
- export function descendantsOf(pid: number): readonly number[] {
40
- const out = Bun.spawnSync(['pgrep', '-P', String(pid)]).stdout.toString();
41
- const children = out
42
- .split('\n')
43
- .map((line) => Number(line.trim()))
44
- .filter((child) => Number.isInteger(child) && child > 0);
45
- return children.flatMap((child) => [child, ...descendantsOf(child)]);
46
- }
47
-
48
- /**
49
- * Every process whose cwd is `dir` — a deleted one included, which Linux spells `<dir> (deleted)`.
50
- * The one question that finds an orphan after its parent is gone, when no process tree reaches it.
51
- * Empty where there is no `/proc` (macOS): the descendant walk is then the whole answer.
52
- */
53
- export function processesIn(dir: string): readonly number[] {
54
- let pids: readonly string[];
55
- try {
56
- pids = readdirSync('/proc').filter((name) => /^\d+$/.test(name));
57
- } catch {
58
- return [];
59
- }
60
- const found: number[] = [];
61
- for (const pid of pids) {
62
- try {
63
- const cwd = readlinkSync(`/proc/${pid}/cwd`);
64
- if (cwd === dir || cwd === `${dir} (deleted)`) found.push(Number(pid));
65
- } catch {
66
- // Gone between the listing and the read, or not ours to read.
67
- }
68
- }
69
- return found;
70
- }
71
-
72
- /** Whether anything still answers on `port` at the loopback address `x dev` binds. */
73
- export async function listening(port: number): Promise<boolean> {
74
- try {
75
- const socket = await Bun.connect({
76
- hostname: '127.0.0.1',
77
- port,
78
- socket: { data: () => undefined },
79
- });
80
- socket.end();
81
- return true;
82
- } catch {
83
- return false;
84
- }
85
- }
86
-
87
- /**
88
- * SIGKILL `pids`, then wait until each is gone. A test's last resort, run in `finally` — by then
89
- * every graceful stop the test meant to prove has had its chance. `performance.now()`, never
90
- * `Date.now()`: the test preload freezes the wall clock, and a deadline read off it never arrives.
91
- */
92
- export async function reap(pids: readonly number[], timeoutMs = 10_000): Promise<void> {
93
- for (const pid of pids) {
94
- try {
95
- process.kill(pid, 'SIGKILL');
96
- } catch {
97
- // Already gone.
98
- }
99
- }
100
- const until = performance.now() + timeoutMs;
101
- while (pids.some(alive) && performance.now() < until) await Bun.sleep(25);
102
- }
103
-
104
- /** What was left: the pids still in `dir` and whether `port` still answers. Both empty is clean. */
105
- export async function leftovers(
106
- dir: string,
107
- port: number | undefined,
108
- ): Promise<{ readonly pids: readonly number[]; readonly port: boolean }> {
109
- return {
110
- pids: processesIn(dir),
111
- port: port === undefined ? false : await listening(port),
112
- };
113
- }
114
-
115
- /**
116
- * The net under a TIMED-OUT test: bun abandons its body without running `finally`, so an
117
- * `afterEach` reaps whatever still runs in the fixture directory — the only handle left on an
118
- * orphan whose parent is gone.
119
- */
120
- export async function reapIn(dir: string): Promise<void> {
121
- await reap(processesIn(dir));
122
- }
@@ -1,78 +0,0 @@
1
- // The one policy declaration set both `x policy` test files run against. Shared rather than
2
- // copied, for the reason `thrown-by.ts` gives: every count in both files' assertions is read off
3
- // THIS set, so a second copy drifts and each file keeps passing while they stop agreeing.
4
-
5
- import { action, registerActions, t } from '@ultimat3/action';
6
- import { and, can, definePermissions, defineRoles } from '@ultimat3/policy';
7
- import { from, query, registerQuery } from '@ultimat3/query';
8
-
9
- /**
10
- * The row the fixture's two reads return, over an empty source: `x policy` describes a query and
11
- * evaluates its policy, it never executes one, so the rows are the one fact that may be missing.
12
- */
13
- interface PostRow {
14
- readonly id: string;
15
- }
16
-
17
- /**
18
- * Four permissions, three roles, two actions and two queries — each fact earning its place.
19
- *
20
- * `archivePost` is guarded by a COMPOSITE, and that is the point of it: its display capability is
21
- * the label `and(post:publish, post:read)`, which is not any permission, so a report that matched
22
- * on the label counted this action as enforcing nothing and printed both of its permissions as
23
- * dead grants. It enforces two, and the facts say so.
24
- *
25
- * `post:delete` is the control: declared, granted to `admin`, enforced by nothing at all. It is
26
- * what a genuinely dead grant looks like, and it is what keeps `unenforced` a claim worth making
27
- * now that a composite no longer lands in it by accident.
28
- *
29
- * `post:publish` is enforced by two actions AND a query, so the aggregation across declarations
30
- * has something to aggregate.
31
- *
32
- * Registers only; the registries are process-global, so the caller clears them first.
33
- */
34
- export function registerPolicyFixture(): void {
35
- definePermissions(['post:publish', 'post:read', 'post:delete', 'feed:read'] as const);
36
- defineRoles({
37
- admin: { grants: ['post:publish', 'post:read', 'post:delete', 'feed:read'] },
38
- editor: { grants: ['post:publish', 'post:read'] },
39
- reader: { grants: ['post:read', 'feed:read'] },
40
- });
41
- registerActions({
42
- publishPost: action({
43
- input: t.object({}),
44
- output: t.object({}),
45
- policy: can('post:publish'),
46
- async handle() {
47
- return {};
48
- },
49
- }),
50
- archivePost: action({
51
- input: t.object({}),
52
- output: t.object({}),
53
- policy: and(
54
- can('post:publish'),
55
- can('post:read', ({ actor }) => actor?.id === 'admin'),
56
- ),
57
- async handle() {
58
- return {};
59
- },
60
- }),
61
- });
62
- registerQuery(
63
- 'postFeed',
64
- query({
65
- input: t.object({}),
66
- policy: can('feed:read'),
67
- sql: () => from<PostRow>('posts', []).orderBy('id').limit(10),
68
- }),
69
- );
70
- registerQuery(
71
- 'publishedPosts',
72
- query({
73
- input: t.object({}),
74
- policy: can('post:publish'),
75
- sql: () => from<PostRow>('posts', []).orderBy('id').limit(10),
76
- }),
77
- );
78
- }
@@ -1,9 +0,0 @@
1
- // The browser island `wiki/Realtime.md` promises: one live hook and nothing else. It is a real
2
- // module rather than a string a test writes to a temp path, because module resolution is the thing
3
- // under test — `@ultimat3/realtime`'s client entry must reach neither the bus nor the WAL decoder.
4
- // Bundled AND imported by `realtime-browser-barrel.test.ts` — the import is what gives it an lcov
5
- // record, since `Bun.build()` reads this file without evaluating it.
6
-
7
- import { useQuery } from '@ultimat3/realtime';
8
-
9
- export const probeUseQuery = useQuery;
@@ -1,78 +0,0 @@
1
- // The one `RunningServices` every `role-start` test file boots roles against, and the one reset
2
- // between them. Shared rather than copied, for the reason `policy-fixture.ts` gives: three files
3
- // start the same roles, and a second copy of the runtime drifts while each file keeps passing.
4
- //
5
- // Its own module rather than a `.test.ts` neighbours import, because `tsconfig.json` excludes
6
- // `*.test.ts` — a fixture written there is one `tsc` never reads.
7
-
8
- import { noopPurgeDriver } from '@ultimat3/cache';
9
- import { type RealtimeConfig, resetLifecycle } from '@ultimat3/core';
10
- import { resetHttpConfig } from '@ultimat3/http';
11
- import {
12
- createMemoryDriver,
13
- createMemoryEventBus,
14
- createMemoryOutboxStore,
15
- resetJobs,
16
- resetJobsFacade,
17
- resetTasks,
18
- } from '@ultimat3/jobs';
19
- import { createMemoryDriver as createMemoryMailDriver } from '@ultimat3/mail';
20
- import { DEFAULT_PRESENCE_TTL_MS, InProcessTransport } from '@ultimat3/realtime/server';
21
- import { defineStorage, localDriver } from '@ultimat3/storage';
22
- import { resolveServices } from './runtime-bindings';
23
- import type { RunningServices } from './runtime-services';
24
-
25
- /**
26
- * Every service a role touches, embedded but real — no PGlite boot for a role-wiring test.
27
- *
28
- * `root` is a parameter and not a constant: each test file owns its own directory and deletes it,
29
- * so two files sharing one on-disk storage root cannot leave the other's fixture half-removed.
30
- */
31
- /** Enabled, in-process: the section every role test before 22.0.0 was implicitly booting. */
32
- const REALTIME_ON: RealtimeConfig = { enabled: true, transport: 'memory', urlEnv: undefined };
33
-
34
- export function fixtureRuntime(root: string, realtime = REALTIME_ON): RunningServices {
35
- const services = resolveServices(root, {});
36
- const transport = new InProcessTransport();
37
- return {
38
- services,
39
- db: { async ping() {}, async close() {} } as unknown as RunningServices['db'],
40
- jobs: createMemoryDriver(),
41
- // A real store, not a stub: the `worker` role starts the outbox relay against it, and a relay
42
- // whose `claim()` rejects on the first 200ms tick is an unhandled rejection in whichever test
43
- // happens to still be running.
44
- outbox: createMemoryOutboxStore(),
45
- events: createMemoryEventBus(),
46
- transport,
47
- realtime,
48
- transportDetail: 'in-process fanout',
49
- // The sync role reads this to build its `PresenceRegistry`; the default is what a boot with no
50
- // `NATS_URL` resolves to, so the fixture is the real number rather than a rounder one.
51
- presenceTtlMs: DEFAULT_PRESENCE_TTL_MS,
52
- storage: defineStorage({ disks: { local: localDriver({ root: `${root}/storage` }) } }),
53
- mail: createMemoryMailDriver(),
54
- mailDetail: 'embedded',
55
- purge: noopPurgeDriver(),
56
- purgeDetail: 'none',
57
- stop: async () => transport.close(),
58
- };
59
- }
60
-
61
- /**
62
- * The process-global state one started role leaves behind. `resetLifecycle` is the load-bearing
63
- * one: core's lifecycle is process-wide and a stopped server leaves it drained, so without it the
64
- * SECOND web role in a file answers every request `X_DRAINING` — a suite that only passes when its
65
- * tests are run one at a time. `@ultimat3/http`'s own server suite resets it for the same reason.
66
- */
67
- export function resetDevRolesState(): void {
68
- resetJobs();
69
- resetJobsFacade();
70
- resetTasks();
71
- resetLifecycle();
72
- // `configureHttp()` is a process-global registration made at MODULE scope, so one file that
73
- // loads an app leaves that app's CORS origins, body limit and buckets standing for every later
74
- // file in the same `bun test` process — and a scaffolded app now ships such a module
75
- // (`apps/web/app/http.ts`). Nothing is broken today; this is the same rule the four resets above
76
- // already follow, applied to the fifth global before it is the one that costs an afternoon.
77
- resetHttpConfig();
78
- }