@ultimat3/cli 22.6.2 → 22.7.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/package.json +28 -28
- package/src/app-load.ts +88 -47
- package/src/app-reload-graph.ts +228 -0
- package/src/cmd-build-spec.ts +8 -1
- package/src/cmd-build.ts +23 -8
- package/src/cmd-dev-fixture.ts +26 -0
- package/src/cmd-dev.ts +10 -0
- package/src/cmd-test-spec.ts +9 -10
- package/src/cmd-test.ts +25 -2
- package/src/cmd-verify-spec.ts +37 -13
- package/src/cmd-verify.ts +81 -5
- package/src/dev-route-table.ts +7 -0
- package/src/error-codes.ts +8 -0
- package/src/mcp-errors.ts +5 -0
- package/src/messages.ts +1 -0
- package/src/output.ts +26 -0
- package/src/templates/scaffold-docs.ts +3 -1
- package/src/test-batches.ts +35 -4
- package/src/test-shards.ts +74 -10
- package/src/test-slots.ts +168 -0
- package/src/test-workers.ts +169 -80
- package/src/verify-errors.ts +49 -0
- package/src/verify-floor.ts +28 -0
- package/src/verify-merge.ts +235 -0
- package/src/verify-run.ts +16 -9
- package/src/verify-shard.ts +145 -0
- package/src/verify-step.ts +20 -1
- package/src/verify-test-run.ts +47 -6
- package/src/verify-tests.ts +39 -4
package/src/verify-test-run.ts
CHANGED
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
// files belong to a type and this one owns what happens to them once selected — a wrong file list
|
|
3
3
|
// is never a race, and a race is never a selection bug.
|
|
4
4
|
|
|
5
|
+
// why: Bun ships no path-join primitive; the timings cache lives under the run's root.
|
|
6
|
+
import { join } from 'node:path';
|
|
5
7
|
import type { Runner } from './exec';
|
|
6
8
|
import type { Finding } from './output';
|
|
7
9
|
import { runBatches, testBatches } from './test-batches';
|
|
8
10
|
import { countsOf } from './test-counts';
|
|
9
11
|
import { testEnvOverrides } from './test-dotenv';
|
|
10
12
|
import type { TestFile } from './test-select';
|
|
11
|
-
import { failureOf, testArgs } from './test-shards';
|
|
13
|
+
import { failureOf, ISOLATED_TEST_ENV, TEST_TIMINGS_FILE, testArgs } from './test-shards';
|
|
14
|
+
import { BATCH_FILES_PER_WORKER, SHARED_BATCH_FILES_PER_WORKER } from './test-workers';
|
|
12
15
|
import type { StepOutcome } from './verify-step';
|
|
13
16
|
// Type-only, so nothing here evaluates verify-tests.ts and the two files cannot form a cycle.
|
|
14
17
|
import type { TestType } from './verify-tests';
|
|
@@ -23,6 +26,12 @@ export interface ParallelRunOptions {
|
|
|
23
26
|
readonly type: TestType;
|
|
24
27
|
/** This process's own environment. Optional and defaulted to `Bun.env` — see `VerifyContext.env`. */
|
|
25
28
|
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
29
|
+
/** A fresh global per file — `x.verify.json` `"isolate": true` or `--isolate`. Default off. */
|
|
30
|
+
readonly isolate?: boolean;
|
|
31
|
+
/** Why the width is what it is — printed on the step line. */
|
|
32
|
+
readonly widthReason?: string;
|
|
33
|
+
/** The machine-pool lease (`test-slots.ts`), for a default-width run. */
|
|
34
|
+
readonly lease?: (want: number) => Promise<{ readonly count: number; release(): void }>;
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
/**
|
|
@@ -42,14 +51,29 @@ export interface ParallelRunOptions {
|
|
|
42
51
|
export async function runParallel(options: ParallelRunOptions): Promise<StepOutcome> {
|
|
43
52
|
const files = options.files.map((file) => file.path);
|
|
44
53
|
const workers = Math.max(1, Math.min(Math.trunc(options.workers), files.length || 1));
|
|
45
|
-
const envOverrides =
|
|
54
|
+
const envOverrides = {
|
|
55
|
+
...testEnvOverrides(options.root, options.env ?? Bun.env),
|
|
56
|
+
...(options.isolate === true ? { [ISOLATED_TEST_ENV]: '1' } : {}),
|
|
57
|
+
};
|
|
46
58
|
// Batched exactly as `x test` batches the same selection at the same width, so the gate's
|
|
47
59
|
// `fix:` line — `x test <type> --workers N` — reruns the very processes that failed.
|
|
48
|
-
const batches = testBatches(
|
|
60
|
+
const batches = testBatches(
|
|
61
|
+
files,
|
|
62
|
+
workers,
|
|
63
|
+
options.isolate === true ? BATCH_FILES_PER_WORKER : SHARED_BATCH_FILES_PER_WORKER,
|
|
64
|
+
);
|
|
49
65
|
const result = await runBatches({
|
|
50
66
|
runner: options.runner,
|
|
51
67
|
batches,
|
|
52
|
-
|
|
68
|
+
workers,
|
|
69
|
+
...(options.lease === undefined || workers === 1 ? {} : { lease: options.lease }),
|
|
70
|
+
argsFor: (batch, width) =>
|
|
71
|
+
testArgs({
|
|
72
|
+
files: batch,
|
|
73
|
+
workers: width,
|
|
74
|
+
isolate: options.isolate === true,
|
|
75
|
+
timings: join(options.root, TEST_TIMINGS_FILE),
|
|
76
|
+
}),
|
|
53
77
|
options: {
|
|
54
78
|
cwd: options.root,
|
|
55
79
|
...(Object.keys(envOverrides).length === 0 ? {} : { env: envOverrides }),
|
|
@@ -60,11 +84,28 @@ export async function runParallel(options: ParallelRunOptions): Promise<StepOutc
|
|
|
60
84
|
// to drift — and the one that drifts is the gate's, which is the one an agent reads first.
|
|
61
85
|
const findings: readonly Finding[] = result.ok
|
|
62
86
|
? []
|
|
63
|
-
: [
|
|
87
|
+
: [
|
|
88
|
+
failureOf(result.code, files.length, {
|
|
89
|
+
workers,
|
|
90
|
+
type: options.type,
|
|
91
|
+
...(options.isolate === true ? { isolate: true } : {}),
|
|
92
|
+
}),
|
|
93
|
+
];
|
|
94
|
+
// The width the run REACHED: a batch the machine pool narrowed ran narrower than planned, and a
|
|
95
|
+
// step line that printed the plan would name a width that never ran.
|
|
96
|
+
const reached = Math.max(1, ...result.widths);
|
|
97
|
+
const narrowed = reached < workers;
|
|
64
98
|
return {
|
|
65
99
|
ok: findings.length === 0,
|
|
66
100
|
findings,
|
|
67
|
-
workers,
|
|
101
|
+
workers: reached,
|
|
102
|
+
...(options.widthReason === undefined
|
|
103
|
+
? {}
|
|
104
|
+
: {
|
|
105
|
+
widthReason: narrowed
|
|
106
|
+
? `${String(reached)} of ${options.widthReason} — machine slots held by another run`
|
|
107
|
+
: options.widthReason,
|
|
108
|
+
}),
|
|
68
109
|
// Per batch: each process printed its own summary, and one parse of the joined text would
|
|
69
110
|
// read only the last one.
|
|
70
111
|
tests: countsOf(result.results),
|
package/src/verify-tests.ts
CHANGED
|
@@ -18,8 +18,11 @@ import { countsOf } from './test-counts';
|
|
|
18
18
|
import { testEnvOverrides } from './test-dotenv';
|
|
19
19
|
import type { TestFile } from './test-select';
|
|
20
20
|
import { discoverTests } from './test-select';
|
|
21
|
-
import {
|
|
21
|
+
import { machineLease } from './test-slots';
|
|
22
|
+
import type { WorkerPlan } from './test-workers';
|
|
23
|
+
import { availableCpus, SERIAL_TYPES, totalMemory, workerPlan } from './test-workers';
|
|
22
24
|
import { withE2eApp } from './verify-e2e';
|
|
25
|
+
import { shardFiles } from './verify-shard';
|
|
23
26
|
import type { StepOutcome, VerifyContext, VerifyStep } from './verify-step';
|
|
24
27
|
import { fromExec, fromFindings } from './verify-step';
|
|
25
28
|
import { runParallel } from './verify-test-run';
|
|
@@ -212,16 +215,48 @@ const runSerial = async (ctx: VerifyContext, type: TestType): Promise<StepOutcom
|
|
|
212
215
|
|
|
213
216
|
const runType = async (ctx: VerifyContext, type: TestType): Promise<StepOutcome> => {
|
|
214
217
|
if (isSerial(type)) return runSerial(ctx, type);
|
|
215
|
-
const
|
|
218
|
+
const discoveredFiles = await filesFor(ctx.root, type);
|
|
219
|
+
// Under `--shard`, this job runs its slice and says which — `x verify merge` checks the slices.
|
|
220
|
+
// An EMPTY slice is a pass with zero tests, never the serial fallback below (which would run
|
|
221
|
+
// the whole type): the zero-tests floor is `merge`'s to apply, on the summed counts.
|
|
222
|
+
const shard =
|
|
223
|
+
ctx.shard === undefined
|
|
224
|
+
? undefined
|
|
225
|
+
: shardFiles(
|
|
226
|
+
discoveredFiles.map((file) => file.path),
|
|
227
|
+
ctx.shard,
|
|
228
|
+
ctx.shard.timings,
|
|
229
|
+
);
|
|
230
|
+
if (shard !== undefined && shard.files.length === 0) {
|
|
231
|
+
return { ok: true, findings: [], workers: 0, tests: { ran: 0, skipped: 0 }, shard };
|
|
232
|
+
}
|
|
233
|
+
const files =
|
|
234
|
+
shard === undefined
|
|
235
|
+
? discoveredFiles
|
|
236
|
+
: discoveredFiles.filter((file) => shard.files.includes(file.path));
|
|
216
237
|
if (files.length === 0) return runSerial(ctx, type);
|
|
217
|
-
|
|
238
|
+
const env = ctx.env ?? Bun.env;
|
|
239
|
+
const plan =
|
|
240
|
+
ctx.workers === undefined ? workerPlan(availableCpus(), totalMemory(), env) : undefined;
|
|
241
|
+
const workers = ctx.workers ?? (plan as WorkerPlan).workers;
|
|
242
|
+
const lease = plan === undefined ? undefined : machineLease(plan.workers, env);
|
|
243
|
+
const outcome = await runParallel({
|
|
218
244
|
root: ctx.root,
|
|
219
245
|
runner: ctx.runner,
|
|
220
246
|
files,
|
|
221
|
-
workers
|
|
247
|
+
workers,
|
|
222
248
|
type,
|
|
249
|
+
isolate: ctx.isolate === true,
|
|
250
|
+
widthReason:
|
|
251
|
+
plan === undefined
|
|
252
|
+
? `${String(Math.min(workers, files.length))} workers (--workers)`
|
|
253
|
+
: files.length < plan.workers
|
|
254
|
+
? `${String(files.length)} of ${plan.reason}`
|
|
255
|
+
: plan.reason,
|
|
256
|
+
...(lease === undefined ? {} : { lease }),
|
|
223
257
|
...(ctx.env === undefined ? {} : { env: ctx.env }),
|
|
224
258
|
});
|
|
259
|
+
return shard === undefined ? outcome : { ...outcome, shard };
|
|
225
260
|
};
|
|
226
261
|
|
|
227
262
|
const isApp = (root: string): boolean => existsSync(join(root, APP_CONFIG_FILE));
|