@ultimat3/cli 23.0.0 → 24.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.
- package/CLAUDE.md +15 -7
- package/README.md +3 -3
- package/package.json +28 -28
- package/src/affected.ts +27 -35
- package/src/api-registration.ts +89 -0
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +5 -11
- package/src/app-manifest.ts +2 -8
- package/src/app-mcp.ts +10 -23
- package/src/app-permissions-borrowed.ts +129 -0
- package/src/app-permissions-site.ts +24 -0
- package/src/app-permissions.ts +7 -17
- package/src/budgets.ts +4 -35
- package/src/cdp-shot-driver.ts +3 -1
- package/src/cmd-auth-spec.ts +16 -0
- package/src/cmd-auth.ts +65 -0
- package/src/cmd-build.ts +64 -15
- package/src/cmd-db-backfill.ts +25 -12
- package/src/cmd-db.ts +6 -5
- package/src/cmd-deploy-helm.ts +7 -6
- package/src/cmd-deploy.ts +4 -4
- package/src/cmd-dev.ts +9 -8
- package/src/cmd-doctor-spec.ts +2 -9
- package/src/cmd-doctor.ts +41 -14
- package/src/cmd-env.ts +26 -2
- package/src/cmd-generate.ts +79 -32
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +10 -9
- package/src/cmd-jobs.ts +18 -6
- package/src/cmd-manifest-spec.ts +1 -1
- package/src/cmd-manifest.ts +6 -25
- package/src/cmd-mcp.ts +46 -15
- package/src/cmd-planned.ts +15 -8
- package/src/cmd-routes.ts +12 -2
- package/src/cmd-secrets.ts +47 -6
- package/src/cmd-shot-matrix.ts +1 -3
- package/src/cmd-shot.ts +12 -3
- package/src/cmd-tasks.ts +2 -2
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +13 -29
- package/src/cmd-verify.ts +4 -4
- package/src/db-subscribes.ts +12 -4
- package/src/dev-dashboard-guard.ts +76 -0
- package/src/dev-dashboard.ts +51 -29
- package/src/dev-lock.ts +4 -7
- package/src/dispatch.ts +3 -6
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -0
- package/src/drift-replica-identity.ts +42 -0
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +23 -3
- package/src/error-unthrown.ts +29 -11
- package/src/errors.ts +54 -83
- package/src/framework-schema-apply.ts +78 -0
- package/src/framework-schema-stamp.ts +96 -0
- package/src/framework-schema.ts +13 -2
- package/src/generate-files.ts +4 -3
- package/src/generate-kinds.ts +33 -1
- package/src/generate-shadow.ts +46 -0
- package/src/generate-write.ts +47 -13
- package/src/gh-target.ts +12 -7
- package/src/i18n-audit.ts +4 -1
- package/src/index.ts +1 -3
- package/src/island-bundle.ts +3 -15
- package/src/jobs-drain.ts +82 -15
- package/src/jobs-report.ts +4 -4
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +12 -3
- package/src/mcp-host.ts +16 -9
- package/src/mcp-test-run.ts +38 -0
- package/src/messages.ts +2 -0
- package/src/metrics-endpoint.ts +25 -1
- package/src/output.ts +6 -0
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +5 -20
- package/src/prerender-out.ts +34 -7
- package/src/prerender.ts +8 -5
- package/src/pwa-artifacts.ts +26 -94
- package/src/registry.ts +2 -0
- package/src/revalidated-response.ts +4 -3
- package/src/role-replicator.ts +16 -0
- package/src/role-start.ts +9 -4
- package/src/role-sync.ts +55 -20
- package/src/runtime-assets.ts +11 -7
- package/src/runtime-bindings.ts +4 -4
- package/src/runtime-cache.ts +38 -47
- package/src/runtime-idempotency-scope.ts +23 -0
- package/src/runtime-isr.ts +36 -0
- package/src/runtime-jobs.ts +8 -33
- package/src/runtime-mfa-warning.ts +29 -0
- package/src/runtime-notify-retention.ts +10 -46
- package/src/runtime-overrides.ts +1 -1
- package/src/runtime-purge.ts +38 -16
- package/src/runtime-queue.ts +31 -10
- package/src/runtime-realtime.ts +9 -45
- package/src/runtime-render.ts +62 -35
- package/src/runtime-services.ts +20 -17
- package/src/runtime-storage.ts +10 -5
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +34 -6
- package/src/serve-drain.ts +9 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-web.ts +12 -7
- package/src/shot-locale.ts +5 -23
- package/src/site-assets.ts +3 -5
- package/src/site-config.ts +9 -48
- package/src/site-seo.ts +1 -1
- package/src/sitemap-lastmod.ts +5 -0
- package/src/solid-loader.ts +12 -3
- package/src/source-files.ts +6 -0
- package/src/stored-object-headers.ts +43 -0
- package/src/sync-url.ts +2 -2
- package/src/tasks-facts.ts +3 -29
- package/src/templates/action.ts +3 -0
- package/src/templates/naming.ts +28 -0
- package/src/templates/route.ts +3 -6
- package/src/templates/scaffold-app.ts +6 -2
- package/src/templates/scaffold-container-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +4 -2
- package/src/templates/scaffold-db-client.ts +4 -2
- package/src/templates/scaffold-helm-fences.ts +76 -0
- package/src/templates/scaffold-helm-templates.ts +32 -7
- package/src/templates/scaffold-helm.ts +31 -1
- package/src/templates/scaffold-repo.ts +9 -1
- package/src/templates/scaffold-shell.ts +39 -36
- package/src/test-counts.ts +2 -6
- package/src/test-ignores.ts +25 -0
- package/src/test-passes.ts +0 -15
- package/src/test-select.ts +5 -14
- package/src/test-shards.ts +41 -93
- package/src/test-workers.ts +26 -4
- package/src/theme-boot.ts +6 -31
- package/src/trusted-hops-error.ts +17 -0
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +17 -46
- package/src/verify-deadline.ts +130 -33
- package/src/verify-merge.ts +60 -27
- package/src/verify-part-step.ts +90 -0
- package/src/verify-run.ts +29 -8
- package/src/verify-shard.ts +13 -2
- package/src/verify-stalled.ts +152 -0
- package/src/verify-tests.ts +5 -2
- package/src/build-errors.ts +0 -54
- package/src/verify-errors.ts +0 -58
package/src/test-shards.ts
CHANGED
|
@@ -39,12 +39,12 @@
|
|
|
39
39
|
// `@ultimat3/testing`'s preload clears the plugins after each file (`isolated-plugins.ts`). And the
|
|
40
40
|
// per-WORKER database survives untouched: `@ultimat3/testing`'s `workerId` already read
|
|
41
41
|
// `BUN_TEST_WORKER_ID` as its second key, which is exactly what Bun sets, 1..N, one per real
|
|
42
|
-
// process (probed on 1.4.0).
|
|
43
|
-
// still sets, so a single-shard rerun keeps naming its own database.
|
|
42
|
+
// process (probed on 1.4.0).
|
|
44
43
|
|
|
45
44
|
// why: Bun ships no path-join primitive; the timings cache lives under the run's root.
|
|
46
45
|
import { join } from 'node:path';
|
|
47
46
|
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
47
|
+
import { ISOLATED_ENV } from '@ultimat3/testing';
|
|
48
48
|
import type { AffectedSelection } from './affected';
|
|
49
49
|
import type { Runner } from './exec';
|
|
50
50
|
import { msg } from './messages';
|
|
@@ -52,8 +52,8 @@ import type { CommandResult, Finding, JsonValue, StepResult } from './output';
|
|
|
52
52
|
import { quoteArg } from './shell-quote';
|
|
53
53
|
import { testEnvOverrides } from './test-dotenv';
|
|
54
54
|
|
|
55
|
-
/** `@ultimat3/testing`'s `ISOLATED_ENV
|
|
56
|
-
export const ISOLATED_TEST_ENV =
|
|
55
|
+
/** `@ultimat3/testing`'s `ISOLATED_ENV` — the one spelling, which that package's preload reads. */
|
|
56
|
+
export const ISOLATED_TEST_ENV = ISOLATED_ENV;
|
|
57
57
|
|
|
58
58
|
/** Bun's per-file durations, cached under the run's root and refreshed by every parallel run. */
|
|
59
59
|
export const TEST_TIMINGS_FILE = '.x/test-timings.json';
|
|
@@ -68,24 +68,13 @@ import type { TestType } from './verify-tests';
|
|
|
68
68
|
* The argv for one run. An explicit file list, never a re-glob: discovery already decided which
|
|
69
69
|
* files belong to this type, and a child that globs again can pick up a file the selection removed.
|
|
70
70
|
*
|
|
71
|
-
* `--parallel=N`
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* to remove: measured on this repo, an 8-way split turned 0 failures into 36, every one
|
|
75
|
-
* `X_PERMISSION_UNKNOWN` in `@ultimat3/query` because the `packages/cli` file that had been
|
|
76
|
-
* declaring `feed:read` for it landed elsewhere. Half a dozen registries here are process-global by
|
|
77
|
-
* design — the permission set, the roles, the entity/action/query tables, the error-code titles,
|
|
78
|
-
* the fixture bag — and a serial `bun test` only passes because glob order happens to put every
|
|
79
|
-
* declaring file before every file that reads what it left behind.
|
|
80
|
-
*
|
|
81
|
-
* Bun's shard partition is round-robin over the list it is given, so the sorted list this hands it
|
|
82
|
-
* makes `--shard=2/8` the same 1/8 on CI and on a laptop (probed on 1.4.0).
|
|
71
|
+
* `--parallel=N` over the selection, sorted, so two runs over one corpus hand Bun the same list.
|
|
72
|
+
* One CI job's share of the corpus is `x verify --only unit --shard i/n` (`verify-shard.ts`), never
|
|
73
|
+
* a flag here: `x test --worker I` was a second, 0-based, unhashed split of the same suite.
|
|
83
74
|
*/
|
|
84
75
|
export function testArgs(input: {
|
|
85
76
|
readonly files: readonly string[];
|
|
86
77
|
readonly workers: number;
|
|
87
|
-
/** 0-based, matching `--worker`. Absent runs the whole selection across `workers` processes. */
|
|
88
|
-
readonly shard?: number;
|
|
89
78
|
/**
|
|
90
79
|
* Everything after a bare `--`, handed to `bun test` verbatim and BEFORE the file list, which is
|
|
91
80
|
* where bun reads its flags. `ParsedArgs.passthrough` had no reader anywhere until 2026-09, so
|
|
@@ -114,32 +103,23 @@ export function testArgs(input: {
|
|
|
114
103
|
const isolate =
|
|
115
104
|
extra.includes('--isolate') || (input.isolate === true && !extra.includes('--no-isolate'));
|
|
116
105
|
const bare = extra.filter((arg) => arg !== '--isolate' && arg !== '--no-isolate');
|
|
117
|
-
return
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
]
|
|
129
|
-
: [
|
|
130
|
-
'bun',
|
|
131
|
-
'test',
|
|
132
|
-
...(isolate ? ['--isolate'] : []),
|
|
133
|
-
`--shard=${String(input.shard + 1)}/${String(input.workers)}`,
|
|
134
|
-
...bare,
|
|
135
|
-
...files,
|
|
136
|
-
];
|
|
106
|
+
return [
|
|
107
|
+
'bun',
|
|
108
|
+
'test',
|
|
109
|
+
`--parallel=${String(input.workers)}`,
|
|
110
|
+
...(isolate ? [] : ['--no-isolate']),
|
|
111
|
+
...(input.timings === undefined || bare.some((arg) => arg.startsWith('--timings'))
|
|
112
|
+
? []
|
|
113
|
+
: [`--timings=${input.timings}`, '--update-timings']),
|
|
114
|
+
...bare,
|
|
115
|
+
...files,
|
|
116
|
+
];
|
|
137
117
|
}
|
|
138
118
|
|
|
139
119
|
/**
|
|
140
120
|
* The files an argv selects, flags stripped. `bun test` takes its file list positionally, so this
|
|
141
121
|
* is the inverse of `testArgs` and the one thing a test asserting "what did the child get?" needs
|
|
142
|
-
* — the flag COUNT is not fixed (`--
|
|
122
|
+
* — the flag COUNT is not fixed (`--no-isolate` and the timings pair come and go), and a
|
|
143
123
|
* test slicing a hardcoded prefix length reads a flag as a filename the day that changes.
|
|
144
124
|
*/
|
|
145
125
|
export const filesIn = (command: readonly string[]): readonly string[] =>
|
|
@@ -154,12 +134,9 @@ export interface ReproduceOptions {
|
|
|
154
134
|
readonly sample?: number;
|
|
155
135
|
/**
|
|
156
136
|
* The `--affected` narrowing, when there was one. The input most easily forgotten: `--affected`
|
|
157
|
-
* decides which files exist to run at all, so a rerun without it selects the WHOLE corpus
|
|
158
|
-
* its shard 2 is a different shard 2.
|
|
137
|
+
* decides which files exist to run at all, so a rerun without it selects the WHOLE corpus.
|
|
159
138
|
*/
|
|
160
139
|
readonly affected?: AffectedSelection;
|
|
161
|
-
/** 0-based, when reproducing ONE shard. Absent reproduces the whole selection. */
|
|
162
|
-
readonly shard?: number;
|
|
163
140
|
/** What the caller put after `--`. It reaches `bun test`, so a rerun without it runs differently. */
|
|
164
141
|
readonly passthrough?: readonly string[];
|
|
165
142
|
/** The run was isolated per file; the rerun must be too. */
|
|
@@ -185,7 +162,6 @@ export function reproduceFor(options: ReproduceOptions): string {
|
|
|
185
162
|
...(options.affected?.dirty === true ? ['--dirty'] : []),
|
|
186
163
|
'--workers',
|
|
187
164
|
String(options.workers),
|
|
188
|
-
...(options.shard === undefined ? [] : ['--worker', String(options.shard)]),
|
|
189
165
|
...(options.isolate === true ? ['--isolate'] : []),
|
|
190
166
|
// Last, and after a `--` of its own, because that is where the caller typed it and where the
|
|
191
167
|
// parser will find it again. Quoted for `shell-quote.ts`'s reason: a reproduce line is pasted.
|
|
@@ -200,14 +176,11 @@ export interface RunShardsOptions {
|
|
|
200
176
|
readonly runner: Runner;
|
|
201
177
|
readonly files: readonly TestFile[];
|
|
202
178
|
readonly workers: number;
|
|
203
|
-
/** Run exactly one shard of the same N-way split, not a one-worker run of everything. */
|
|
204
|
-
readonly only?: number;
|
|
205
179
|
readonly filter?: string;
|
|
206
180
|
readonly type?: TestType;
|
|
207
181
|
/**
|
|
208
182
|
* Set when `--sample` narrowed `files`: `kept` is what survived, `total` what discovery found.
|
|
209
|
-
*
|
|
210
|
-
* sample and would otherwise report that shard's size as the corpus.
|
|
183
|
+
* Carried rather than counted from what ran: a pass runs part of the sample, never the corpus.
|
|
211
184
|
*/
|
|
212
185
|
readonly sample?: { readonly kept: number; readonly total: number };
|
|
213
186
|
/** Passed straight to `reproduceFor`: see `ReproduceOptions.affected`. */
|
|
@@ -248,7 +221,6 @@ const planOf = (
|
|
|
248
221
|
...(pass.type === undefined ? {} : { type: pass.type }),
|
|
249
222
|
...(options.sample === undefined ? {} : { sample: options.sample.kept }),
|
|
250
223
|
...(options.affected === undefined ? {} : { affected: options.affected }),
|
|
251
|
-
...(options.only === undefined ? {} : { shard: options.only }),
|
|
252
224
|
...(options.isolate === true ? { isolate: true } : {}),
|
|
253
225
|
...(options.passthrough === undefined || options.passthrough.length === 0
|
|
254
226
|
? {}
|
|
@@ -256,17 +228,12 @@ const planOf = (
|
|
|
256
228
|
});
|
|
257
229
|
|
|
258
230
|
/**
|
|
259
|
-
* `
|
|
260
|
-
*
|
|
261
|
-
* codes already exist and both are already documented — a third would be a new name for a failed
|
|
262
|
-
* `bun test`.
|
|
231
|
+
* `X_TEST_FAILED`, whose `fix:` reruns exactly what failed. `X_TEST_SHARD_FAILED` was the
|
|
232
|
+
* `--worker` rerun's and went with it: the code stays registered (codes are forever) and unthrown.
|
|
263
233
|
*/
|
|
264
234
|
export const failureOf = (code: number, files: number, plan: ReproduceOptions): Finding => ({
|
|
265
|
-
code:
|
|
266
|
-
cause:
|
|
267
|
-
plan.shard === undefined
|
|
268
|
-
? `${plan.type ?? 'test'} run exited ${code} across ${plan.workers} worker(s) (${files} file(s))`
|
|
269
|
-
: `shard ${plan.shard} of ${plan.workers} exited ${code} (${files} file(s))`,
|
|
235
|
+
code: 'X_TEST_FAILED',
|
|
236
|
+
cause: `${plan.type ?? 'test'} run exited ${code} across ${plan.workers} worker(s) (${files} file(s))`,
|
|
270
237
|
fix: reproduceFor(plan),
|
|
271
238
|
docs: ERROR_DOCS_URL,
|
|
272
239
|
});
|
|
@@ -281,12 +248,10 @@ export const failureOf = (code: number, files: number, plan: ReproduceOptions):
|
|
|
281
248
|
* it. And every pass runs even after one fails — the caller asked for a suite, and a report that
|
|
282
249
|
* stops at the first red step hides the rest of the answer.
|
|
283
250
|
*
|
|
284
|
-
* `ULTIMATE_TEST_WORKER`
|
|
285
|
-
*
|
|
286
|
-
* numbers each with `BUN_TEST_WORKER_ID`, which `@ultimat3/testing`'s `workerId` already reads.
|
|
251
|
+
* No `ULTIMATE_TEST_WORKER` here: a `--parallel` run has N processes and Bun numbers each with
|
|
252
|
+
* `BUN_TEST_WORKER_ID`, which `@ultimat3/testing`'s `workerId` already reads.
|
|
287
253
|
*/
|
|
288
254
|
export async function runShards(options: RunShardsOptions): Promise<CommandResult> {
|
|
289
|
-
const only = options.only;
|
|
290
255
|
// Computed ONCE per invocation, never per pass: every pass spawns from the same `root` and the
|
|
291
256
|
// same parent env, so the leaked-key set cannot differ pass to pass.
|
|
292
257
|
const envOverrides: Record<string, string | undefined> = {
|
|
@@ -302,7 +267,6 @@ export async function runShards(options: RunShardsOptions): Promise<CommandResul
|
|
|
302
267
|
files: options.files,
|
|
303
268
|
workers: options.workers,
|
|
304
269
|
...(options.type === undefined ? {} : { type: options.type }),
|
|
305
|
-
...(only === undefined ? {} : { shard: only }),
|
|
306
270
|
});
|
|
307
271
|
const started = performance.now();
|
|
308
272
|
const steps: StepResult[] = [];
|
|
@@ -312,53 +276,38 @@ export async function runShards(options: RunShardsOptions): Promise<CommandResul
|
|
|
312
276
|
let exitCode = 0;
|
|
313
277
|
for (const pass of passes) {
|
|
314
278
|
const files = pass.files.map((file) => file.path);
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
isolatedRun(options) ? BATCH_FILES_PER_WORKER : SHARED_BATCH_FILES_PER_WORKER,
|
|
325
|
-
)
|
|
326
|
-
: [files];
|
|
279
|
+
// `-- --watch` is never split: it never exits, so a second batch would never start. Every
|
|
280
|
+
// other pass is spent in `test-batches.ts`' batches, one `bun test` after another.
|
|
281
|
+
const batches = !watching
|
|
282
|
+
? testBatches(
|
|
283
|
+
files,
|
|
284
|
+
pass.workers,
|
|
285
|
+
isolatedRun(options) ? BATCH_FILES_PER_WORKER : SHARED_BATCH_FILES_PER_WORKER,
|
|
286
|
+
)
|
|
287
|
+
: [files];
|
|
327
288
|
const result = await runBatches({
|
|
328
289
|
runner: options.runner,
|
|
329
290
|
batches,
|
|
330
291
|
stopOnFailure: bailing,
|
|
331
292
|
workers: pass.workers,
|
|
332
|
-
// A
|
|
333
|
-
|
|
334
|
-
...(only === undefined && pass.workers > 1 && options.lease !== undefined
|
|
335
|
-
? { lease: options.lease }
|
|
336
|
-
: {}),
|
|
293
|
+
// A serial pass is one worker: only a default-width parallel pass leases from the pool.
|
|
294
|
+
...(pass.workers > 1 && options.lease !== undefined ? { lease: options.lease } : {}),
|
|
337
295
|
argsFor: (batch, width) =>
|
|
338
296
|
testArgs({
|
|
339
297
|
files: batch,
|
|
340
|
-
workers:
|
|
341
|
-
...(only === undefined ? {} : { shard: only }),
|
|
298
|
+
workers: width,
|
|
342
299
|
...(options.passthrough === undefined ? {} : { passthrough: options.passthrough }),
|
|
343
300
|
...(options.isolate === undefined ? {} : { isolate: options.isolate }),
|
|
344
301
|
timings: join(options.root, TEST_TIMINGS_FILE),
|
|
345
302
|
}),
|
|
346
303
|
options: {
|
|
347
304
|
cwd: options.root,
|
|
348
|
-
...(Object.keys(envOverrides).length === 0
|
|
349
|
-
? {}
|
|
350
|
-
: {
|
|
351
|
-
env: {
|
|
352
|
-
...envOverrides,
|
|
353
|
-
...(only === undefined ? {} : { ULTIMATE_TEST_WORKER: String(only) }),
|
|
354
|
-
},
|
|
355
|
-
}),
|
|
305
|
+
...(Object.keys(envOverrides).length === 0 ? {} : { env: envOverrides }),
|
|
356
306
|
},
|
|
357
307
|
});
|
|
358
308
|
batchCount += batches.length;
|
|
359
309
|
const plan = planOf(options, pass);
|
|
360
|
-
const label =
|
|
361
|
-
only === undefined ? `${pass.workers} worker(s)` : `shard ${only} of ${pass.workers}`;
|
|
310
|
+
const label = `${pass.workers} worker(s)`;
|
|
362
311
|
steps.push({
|
|
363
312
|
name: `${pass.type === undefined ? label : `${pass.type} · ${label}`} · ${files.length} files${
|
|
364
313
|
batches.length > 1 ? msg('cli.test.batches', { batches: batches.length }) : ''
|
|
@@ -403,7 +352,6 @@ export async function runShards(options: RunShardsOptions): Promise<CommandResul
|
|
|
403
352
|
durationMs,
|
|
404
353
|
...(options.filter === undefined ? {} : { filter: options.filter }),
|
|
405
354
|
...(sample === undefined ? {} : { sample: { kept: sample.kept, total: sample.total } }),
|
|
406
|
-
...(only === undefined ? {} : { shard: only }),
|
|
407
355
|
// Only when the split made more than one, so a single-pass run's JSON is byte-identical to
|
|
408
356
|
// what it has always been — and a mixed one can never be read as if it were a single run.
|
|
409
357
|
...(spent.length > 1 ? { passes: spent } : {}),
|
package/src/test-workers.ts
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
// How wide a parallel test run goes, decided in one place. `x test --workers`, `x verify
|
|
2
2
|
// --workers` and every parallel step of the gate read this — a second default would split the same
|
|
3
|
-
// suite two different ways, and the `--
|
|
4
|
-
//
|
|
3
|
+
// suite two different ways, and the `--workers N` a failure's reproduction prints would then name
|
|
4
|
+
// a width the gate never ran.
|
|
5
5
|
|
|
6
6
|
// Bun ships no CPU-count or memory primitive: `cpus()` is the fallback when navigator cannot
|
|
7
7
|
// answer, and `totalmem()` is the only reader of the machine's RAM.
|
|
8
8
|
// why: Bun ships no CPU-count or total-memory primitive.
|
|
9
9
|
import { cpus, totalmem } from 'node:os';
|
|
10
|
+
import { UltimateError } from '@ultimat3/core';
|
|
10
11
|
import type { TestType } from '@ultimat3/testing';
|
|
11
|
-
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `ULTIMATE_TEST_MEMORY_BUDGET` or `ULTIMATE_TEST_MAX_WORKERS` set to something that does not
|
|
15
|
+
* parse. Refused rather than ignored: a typo read as "the default" is a run sized for a machine
|
|
16
|
+
* the caller said this is not.
|
|
17
|
+
*/
|
|
18
|
+
export class TestBudgetInvalidError extends UltimateError {
|
|
19
|
+
constructor(name: string, value: string) {
|
|
20
|
+
super({
|
|
21
|
+
code: 'X_TEST_BUDGET_INVALID',
|
|
22
|
+
cause: `${name}=${JSON.stringify(value)} does not parse — ${
|
|
23
|
+
name === 'ULTIMATE_TEST_MAX_WORKERS'
|
|
24
|
+
? 'expected a positive integer'
|
|
25
|
+
: 'expected a size such as 3g, 512m or 4GiB'
|
|
26
|
+
}`,
|
|
27
|
+
fix:
|
|
28
|
+
name === 'ULTIMATE_TEST_MAX_WORKERS'
|
|
29
|
+
? 'ULTIMATE_TEST_MAX_WORKERS=4 x verify'
|
|
30
|
+
: 'ULTIMATE_TEST_MEMORY_BUDGET=3g x verify',
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
12
34
|
|
|
13
35
|
/** navigator first: it is the runtime's own answer, and it respects a container's CPU limit. */
|
|
14
36
|
export function availableCpus(): number {
|
|
@@ -219,7 +241,7 @@ export const sharedWorkers = (
|
|
|
219
241
|
* Which types run across worker processes, and why the other two cannot.
|
|
220
242
|
*
|
|
221
243
|
* Parallel is safe when the only thing a test file shares with another file is the database, and
|
|
222
|
-
* the database is per worker by construction (`
|
|
244
|
+
* the database is per worker by construction (`BUN_TEST_WORKER_ID` → one clone of the migrated
|
|
223
245
|
* template, `@ultimat3/testing`'s `acquireWorkerDatabase`). Every other process-global in this
|
|
224
246
|
* framework — the permission set, the roles, the entity/action/query registries, the error-code
|
|
225
247
|
* titles, the fixture bag — is handled by `--isolate` giving each FILE its own module registry.
|
package/src/theme-boot.ts
CHANGED
|
@@ -1,40 +1,15 @@
|
|
|
1
|
-
// The no-flash theme script, inlined into every document a served or exported process writes,
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// scripts the framework exported was wired into a document: an app that wanted to open dark had to
|
|
5
|
-
// write its own boot script AND admit it to the CSP by hand — which every app forgot in one of the
|
|
6
|
-
// two places. Sibling of `app-auth.ts`'s `loadSignInPath` and imports the config for the same
|
|
7
|
-
// reason: a regex over the app's source is the pattern `app-load.ts` refuses.
|
|
1
|
+
// The no-flash theme script, inlined into every document a served or exported process writes, with
|
|
2
|
+
// `theme.defaultMode` (off the one loader, `app-config-load.ts`) as its fallback — and the CSP source
|
|
3
|
+
// that admits it, so no app writes its own boot script and forgets to admit it.
|
|
8
4
|
|
|
9
|
-
// why: Bun exposes no path-join primitive, and the config path is app-root-relative — the same
|
|
10
|
-
// necessity `app-auth.ts` records.
|
|
11
|
-
import { join } from 'node:path';
|
|
12
5
|
import type { ThemeMode } from '@ultimat3/core';
|
|
13
6
|
import { cspHashSource } from '@ultimat3/http';
|
|
14
7
|
import { renderHead, themeScript, themeScriptBody } from '@ultimat3/render';
|
|
15
|
-
import {
|
|
16
|
-
import { APP_CONFIG_FILE } from './app-root';
|
|
8
|
+
import { loadAppConfig } from './app-config-load';
|
|
17
9
|
|
|
18
|
-
|
|
19
|
-
typeof value === 'object' && value !== null;
|
|
20
|
-
|
|
21
|
-
const isThemeMode = (value: unknown): value is ThemeMode =>
|
|
22
|
-
value === 'light' || value === 'dark' || value === 'system';
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* `theme.defaultMode`, or `'system'` — the value `defineConfig` fills in — when the app declares
|
|
26
|
-
* none or the file is absent. Structural, never `instanceof`, for `loadSignInPath`'s reason.
|
|
27
|
-
*/
|
|
10
|
+
/** `theme.defaultMode`, or `'system'` — core's own default — for a root with no config file. */
|
|
28
11
|
export async function loadThemeMode(root: string): Promise<ThemeMode> {
|
|
29
|
-
|
|
30
|
-
if (!(await Bun.file(configPath).exists())) return 'system';
|
|
31
|
-
const module = (await import(configPath)) as Record<string, unknown>;
|
|
32
|
-
const config = module[APP_CONFIG_EXPORT];
|
|
33
|
-
if (!isRecord(config)) return 'system';
|
|
34
|
-
const theme = config['theme'];
|
|
35
|
-
if (!isRecord(theme)) return 'system';
|
|
36
|
-
const mode = theme['defaultMode'];
|
|
37
|
-
return isThemeMode(mode) ? mode : 'system';
|
|
12
|
+
return (await loadAppConfig(root))?.theme.defaultMode ?? 'system';
|
|
38
13
|
}
|
|
39
14
|
|
|
40
15
|
export interface ThemeBoot {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// The refusal of a malformed `TRUSTED_PROXY_HOPS` — its own code, never `X_PORT_INVALID`: a proxy
|
|
2
|
+
// count is not a port, and an operator told "is not a TCP port number" looked for the wrong setting.
|
|
3
|
+
|
|
4
|
+
import { UltimateError } from '@ultimat3/core';
|
|
5
|
+
import { MAX_PROXY_HOPS } from '@ultimat3/http';
|
|
6
|
+
import { quoteArg } from './shell-quote';
|
|
7
|
+
|
|
8
|
+
export class TrustedProxyHopsInvalidError extends UltimateError {
|
|
9
|
+
constructor(input: { readonly value: string }) {
|
|
10
|
+
super({
|
|
11
|
+
code: 'X_TRUSTED_PROXY_HOPS_INVALID',
|
|
12
|
+
cause: `TRUSTED_PROXY_HOPS=${quoteArg(input.value)} is not a whole number of proxies from 1 to ${MAX_PROXY_HOPS} — the count that append to x-forwarded-for in front of this process; unset means none`,
|
|
13
|
+
fix: 'TRUSTED_PROXY_HOPS=1 ROLE=web bun apps/web/server.ts',
|
|
14
|
+
meta: { value: input.value, max: MAX_PROXY_HOPS },
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The registered codes this repo no longer throws, as a list `error-unthrown.ts` reads — its own
|
|
2
|
+
// file, so the literals naming the waived codes are not mistaken for a throw site.
|
|
3
|
+
|
|
4
|
+
/** Where the list lives, so a finding can name the file to edit. */
|
|
5
|
+
export const UNTHROWN_CODES_FILE = 'packages/cli/src/unthrown-codes.ts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The codes this repo registers and no longer throws — each a decision, and each row on the
|
|
9
|
+
* reference page says so in words. A LIST, never a prose match: the waiver was any row containing
|
|
10
|
+
* "not thrown" or "thrown by nothing", so a sentence that happened to use either phrase silenced the
|
|
11
|
+
* rule for its code. A listing whose code is thrown again, or is not registered, is
|
|
12
|
+
* `X_ERROR_CODE_UNTHROWN_STALE`.
|
|
13
|
+
*/
|
|
14
|
+
export const UNTHROWN_CODES: ReadonlySet<string> = new Set([
|
|
15
|
+
// `x db studio` is planned and unreleased; it exits X_NOT_IMPLEMENTED.
|
|
16
|
+
'X_DB_STUDIO_FAILED',
|
|
17
|
+
// 21.0.0: there is no LiveClient to register (X_REALTIME_UNINSTALLED replaced it).
|
|
18
|
+
'X_LIVE_CLIENT_MISSING',
|
|
19
|
+
// 21.0.0: liveHookFor was deleted; useQuery reads any query.
|
|
20
|
+
'X_QUERY_NOT_SUBSCRIBABLE',
|
|
21
|
+
// 21.0.0: the worker no longer POSTs a flush endpoint nothing mounted.
|
|
22
|
+
'X_PWA_SYNC_FLUSH_FAILED',
|
|
23
|
+
'X_PWA_SYNC_INCOMPLETE',
|
|
24
|
+
// 24.0.0: `x test --worker` was deleted; `x verify --only unit --shard i/n` is the one split.
|
|
25
|
+
'X_TEST_SHARD_FAILED',
|
|
26
|
+
]);
|
package/src/verify-checks.ts
CHANGED
|
@@ -6,19 +6,13 @@ import { existsSync } from 'node:fs';
|
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
8
8
|
import type { Manifest } from '@ultimat3/manifest';
|
|
9
|
-
import {
|
|
10
|
-
AGENTS_MD_FILENAME,
|
|
11
|
-
assertNoDrift,
|
|
12
|
-
MANIFEST_FILENAME,
|
|
13
|
-
verifyContract,
|
|
14
|
-
} from '@ultimat3/manifest';
|
|
9
|
+
import { AGENTS_MD_FILENAME, MANIFEST_FILENAME, verifyContract } from '@ultimat3/manifest';
|
|
15
10
|
import type { MetaIssue } from '@ultimat3/seo';
|
|
16
11
|
import { validateMeta } from '@ultimat3/seo';
|
|
17
12
|
import { checkAgentsMd } from './app-agents-md';
|
|
18
13
|
import { readAppSources } from './app-boundaries';
|
|
19
14
|
import { envExampleFindings } from './app-env';
|
|
20
15
|
import { appManifest, readAppManifest } from './app-manifest';
|
|
21
|
-
import { OPENAPI_FILE, openApiStaleness } from './app-openapi';
|
|
22
16
|
import { policyFindings } from './app-permissions';
|
|
23
17
|
import { APP_CONFIG_FILE } from './app-root';
|
|
24
18
|
import { asyncPageFindings } from './async-pages';
|
|
@@ -33,6 +27,11 @@ import { catalogFindings } from './i18n-registration';
|
|
|
33
27
|
import { unregisteredJobFindings } from './job-registration';
|
|
34
28
|
import { liveRouteFindings } from './live-routes';
|
|
35
29
|
import { withLoadFindings, withoutLoadFindings } from './load-findings';
|
|
30
|
+
import {
|
|
31
|
+
hasCommittedContract,
|
|
32
|
+
manifestMissingFindings,
|
|
33
|
+
manifestStaleness,
|
|
34
|
+
} from './manifest-staleness';
|
|
36
35
|
import { msg } from './messages';
|
|
37
36
|
import type { Finding } from './output';
|
|
38
37
|
import { findingFrom } from './output';
|
|
@@ -198,10 +197,11 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
198
197
|
{
|
|
199
198
|
name: 'contract-diff',
|
|
200
199
|
summary: 'the published contract vs the committed manifest',
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
200
|
+
// A BREAKING change against the committed manifest, and nothing else. Whether the committed
|
|
201
|
+
// files are stale — `openapi.json` included — is the `manifest` step's, through the one check
|
|
202
|
+
// `x manifest --check` runs (`manifest-staleness.ts`): reported here as well it was two
|
|
203
|
+
// steps and one command answering one question, and the step named `manifest` saying green.
|
|
204
|
+
applies: async (ctx) => existsSync(join(ctx.root, MANIFEST_FILENAME)),
|
|
205
205
|
async run(ctx) {
|
|
206
206
|
const committed = await readAppManifest(ctx.root);
|
|
207
207
|
const { manifest, findings } = await appManifest(ctx.root);
|
|
@@ -209,14 +209,13 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
209
209
|
return withoutLoadFindings(ctx.root, [
|
|
210
210
|
...findings,
|
|
211
211
|
...(committed === undefined ? [] : contractFindings(committed, manifest)),
|
|
212
|
-
...(await openApiStaleness(ctx.root, manifest)),
|
|
213
212
|
]);
|
|
214
213
|
},
|
|
215
214
|
},
|
|
216
215
|
{
|
|
217
216
|
name: 'budgets',
|
|
218
217
|
summary:
|
|
219
|
-
'per-route JS bytes
|
|
218
|
+
'per-route JS bytes, the global style layer every document carries, and the routes that boot nothing to receive their live rows',
|
|
220
219
|
// The global-style assertion rides here rather than becoming a step of its own, because this
|
|
221
220
|
// step already asks the one question it asks: what does the document this build emits actually
|
|
222
221
|
// contain? It is also the same app load — `appManifest` fills render's stylesheet registry on
|
|
@@ -346,7 +345,6 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
346
345
|
const roleLoad = app ? roleLoadFindings(ctx.root, ctx.runner) : Promise.resolve([]);
|
|
347
346
|
const drift = await driftFindings(ctx.root);
|
|
348
347
|
const findings = [
|
|
349
|
-
...manifestMissingFindings(ctx.root),
|
|
350
348
|
...(app ? await withLoadFindings(ctx.root, drift) : drift),
|
|
351
349
|
// An app only: the framework monorepo declares no `defineApi`, so it has no list to be in.
|
|
352
350
|
...(existsSync(join(ctx.root, APP_CONFIG_FILE))
|
|
@@ -384,41 +382,14 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
384
382
|
];
|
|
385
383
|
|
|
386
384
|
/**
|
|
387
|
-
* The
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
* thing that writes it. Nothing ran it: after `x new`, `bin/setup` and all thirteen generators,
|
|
391
|
-
* `find . -name '*.manifest.json'` returned nothing while this step reported green (#F7).
|
|
392
|
-
*
|
|
393
|
-
* An app root, never this repo: the framework monorepo emits `framework.manifest.json` and has no
|
|
394
|
-
* `x.manifest.json` to be missing, so the `app.config.ts` is what decides — the same discriminator
|
|
395
|
-
* `drift`, `budgets`, `seo`, `i18n` and `policy` each use.
|
|
385
|
+
* The committed generated files against the code — `x manifest --check`'s check, through its
|
|
386
|
+
* function. With no committed contract there is nothing to load an app for: an app root is told
|
|
387
|
+
* its manifest is missing, and the framework monorepo (which has neither file) is told nothing.
|
|
396
388
|
*/
|
|
397
|
-
function manifestMissingFindings(root: string): readonly Finding[] {
|
|
398
|
-
if (!existsSync(join(root, APP_CONFIG_FILE))) return [];
|
|
399
|
-
if (existsSync(join(root, MANIFEST_FILENAME))) return [];
|
|
400
|
-
return [
|
|
401
|
-
{
|
|
402
|
-
code: 'X_MANIFEST_MISSING',
|
|
403
|
-
cause: `${MANIFEST_FILENAME} does not exist, so every fact an agent reads about this app — route table, action schemas, policies, error codes — is unavailable`,
|
|
404
|
-
fix: 'x manifest',
|
|
405
|
-
docs: ERROR_DOCS_URL,
|
|
406
|
-
at: MANIFEST_FILENAME,
|
|
407
|
-
},
|
|
408
|
-
];
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/** `assertNoDrift` throws `X_MANIFEST_DRIFT`; a step reports, so the error becomes a finding. */
|
|
412
389
|
async function driftFindings(root: string): Promise<readonly Finding[]> {
|
|
413
|
-
|
|
414
|
-
if (!existsSync(path)) return [];
|
|
390
|
+
if (!hasCommittedContract(root)) return manifestMissingFindings(root);
|
|
415
391
|
const { manifest, findings } = await appManifest(root);
|
|
416
|
-
|
|
417
|
-
await assertNoDrift({ manifest, path });
|
|
418
|
-
return findings;
|
|
419
|
-
} catch (error) {
|
|
420
|
-
return [...findings, { ...findingFrom(error), at: MANIFEST_FILENAME }];
|
|
421
|
-
}
|
|
392
|
+
return [...findings, ...(await manifestStaleness(root, manifest))];
|
|
422
393
|
}
|
|
423
394
|
|
|
424
395
|
/** A breaking change is allowed — with a major bump. `verifyContract` is the one that decides. */
|