@ultimat3/cli 22.3.0 → 22.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +28 -28
- package/src/browser-launcher-fake.ts +4 -1
- package/src/cmd-shot-spec.ts +7 -1
- package/src/cmd-shot.ts +17 -0
- package/src/messages.ts +6 -0
- package/src/shot-verdict.ts +65 -2
- package/src/test-workers.ts +13 -0
- package/src/verify-run.ts +7 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cli",
|
|
3
|
-
"version": "22.3.
|
|
3
|
+
"version": "22.3.1",
|
|
4
4
|
"description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -38,33 +38,33 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@babel/core": "^8.0.1",
|
|
41
|
-
"@ultimat3/action": "22.3.
|
|
42
|
-
"@ultimat3/admin": "22.3.
|
|
43
|
-
"@ultimat3/ai": "22.3.
|
|
44
|
-
"@ultimat3/auth": "22.3.
|
|
45
|
-
"@ultimat3/cache": "22.3.
|
|
46
|
-
"@ultimat3/core": "22.3.
|
|
47
|
-
"@ultimat3/db": "22.3.
|
|
48
|
-
"@ultimat3/entity": "22.3.
|
|
49
|
-
"@ultimat3/flags": "22.3.
|
|
50
|
-
"@ultimat3/http": "22.3.
|
|
51
|
-
"@ultimat3/i18n": "22.3.
|
|
52
|
-
"@ultimat3/jobs": "22.3.
|
|
53
|
-
"@ultimat3/mail": "22.3.
|
|
54
|
-
"@ultimat3/manifest": "22.3.
|
|
55
|
-
"@ultimat3/mcp": "22.3.
|
|
56
|
-
"@ultimat3/money": "22.3.
|
|
57
|
-
"@ultimat3/notify": "22.3.
|
|
58
|
-
"@ultimat3/policy": "22.3.
|
|
59
|
-
"@ultimat3/pwa": "22.3.
|
|
60
|
-
"@ultimat3/query": "22.3.
|
|
61
|
-
"@ultimat3/realtime": "22.3.
|
|
62
|
-
"@ultimat3/render": "22.3.
|
|
63
|
-
"@ultimat3/schema": "22.3.
|
|
64
|
-
"@ultimat3/seo": "22.3.
|
|
65
|
-
"@ultimat3/storage": "22.3.
|
|
66
|
-
"@ultimat3/testing": "22.3.
|
|
67
|
-
"@ultimat3/time": "22.3.
|
|
41
|
+
"@ultimat3/action": "22.3.1",
|
|
42
|
+
"@ultimat3/admin": "22.3.1",
|
|
43
|
+
"@ultimat3/ai": "22.3.1",
|
|
44
|
+
"@ultimat3/auth": "22.3.1",
|
|
45
|
+
"@ultimat3/cache": "22.3.1",
|
|
46
|
+
"@ultimat3/core": "22.3.1",
|
|
47
|
+
"@ultimat3/db": "22.3.1",
|
|
48
|
+
"@ultimat3/entity": "22.3.1",
|
|
49
|
+
"@ultimat3/flags": "22.3.1",
|
|
50
|
+
"@ultimat3/http": "22.3.1",
|
|
51
|
+
"@ultimat3/i18n": "22.3.1",
|
|
52
|
+
"@ultimat3/jobs": "22.3.1",
|
|
53
|
+
"@ultimat3/mail": "22.3.1",
|
|
54
|
+
"@ultimat3/manifest": "22.3.1",
|
|
55
|
+
"@ultimat3/mcp": "22.3.1",
|
|
56
|
+
"@ultimat3/money": "22.3.1",
|
|
57
|
+
"@ultimat3/notify": "22.3.1",
|
|
58
|
+
"@ultimat3/policy": "22.3.1",
|
|
59
|
+
"@ultimat3/pwa": "22.3.1",
|
|
60
|
+
"@ultimat3/query": "22.3.1",
|
|
61
|
+
"@ultimat3/realtime": "22.3.1",
|
|
62
|
+
"@ultimat3/render": "22.3.1",
|
|
63
|
+
"@ultimat3/schema": "22.3.1",
|
|
64
|
+
"@ultimat3/seo": "22.3.1",
|
|
65
|
+
"@ultimat3/storage": "22.3.1",
|
|
66
|
+
"@ultimat3/testing": "22.3.1",
|
|
67
|
+
"@ultimat3/time": "22.3.1",
|
|
68
68
|
"babel-preset-solid": "^1.9.15"
|
|
69
69
|
}
|
|
70
70
|
}
|
|
@@ -26,6 +26,8 @@ export interface FakeShotPage {
|
|
|
26
26
|
readonly url: string;
|
|
27
27
|
readonly html: string;
|
|
28
28
|
readonly evaluate?: Readonly<Record<string, string>> | undefined;
|
|
29
|
+
/** The document's HTTP status. Absent is 200 — the page was recorded because it exists. */
|
|
30
|
+
readonly status?: number | undefined;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/** A PNG signature, and nothing behind it: deterministic bytes, never a render. */
|
|
@@ -76,7 +78,8 @@ function fakePage(byUrl: ReadonlyMap<string, FakeShotPage>, init: ShotSessionIni
|
|
|
76
78
|
network.push({
|
|
77
79
|
method: 'GET',
|
|
78
80
|
url: page.url,
|
|
79
|
-
|
|
81
|
+
// Data the page answers with, never a bound — a recorded page exists, so absent is a 200.
|
|
82
|
+
status: page.status === undefined ? 200 : page.status,
|
|
80
83
|
resourceType: 'document',
|
|
81
84
|
at: init.clock.now().getTime(),
|
|
82
85
|
});
|
package/src/cmd-shot-spec.ts
CHANGED
|
@@ -7,7 +7,7 @@ export const shotSpec: CommandSpec = {
|
|
|
7
7
|
name: 'shot',
|
|
8
8
|
summary: 'photograph one route, one island in a state it declares, or every island in the app',
|
|
9
9
|
usage:
|
|
10
|
-
'x shot <route> [--locale <l>] [--theme light|dark] | --matrix [<route>] | --island <name> [--state <id>] | --all-islands [--port 0] [--out <dir>] [--settle 2000] [--json]',
|
|
10
|
+
'x shot <route> [--locale <l>] [--theme light|dark] | --matrix [<route>] | --island <name> [--state <id>] | --all-islands [--expect-status 404] [--port 0] [--out <dir>] [--settle 2000] [--json]',
|
|
11
11
|
requiresApp: true,
|
|
12
12
|
flags: [
|
|
13
13
|
{ name: 'port', type: 'string', summary: 'dev port (0 lets the kernel pick a free one)' },
|
|
@@ -15,6 +15,12 @@ export const shotSpec: CommandSpec = {
|
|
|
15
15
|
{ name: 'full', type: 'boolean', summary: 'whole page, not the fold', default: true },
|
|
16
16
|
{ name: 'settle', type: 'string', summary: 'ms to wait after load before capturing' },
|
|
17
17
|
{ name: 'timeout', type: 'string', summary: 'ms one navigation may take' },
|
|
18
|
+
{
|
|
19
|
+
name: 'expect-status',
|
|
20
|
+
type: 'string',
|
|
21
|
+
summary:
|
|
22
|
+
'the document status this shot is ok with (e.g. 404); absent: any 2xx, else it fails',
|
|
23
|
+
},
|
|
18
24
|
{ name: 'browser', type: 'string', summary: 'Chrome or Chromium binary to launch' },
|
|
19
25
|
{
|
|
20
26
|
name: 'cdp-url',
|
package/src/cmd-shot.ts
CHANGED
|
@@ -45,6 +45,7 @@ import { readThemeFlag, themeChoiceExpression } from './shot-theme';
|
|
|
45
45
|
import type { IslandCount, ShotArtifacts } from './shot-verdict';
|
|
46
46
|
import {
|
|
47
47
|
buildVerdict,
|
|
48
|
+
documentStatus,
|
|
48
49
|
ISLAND_PROBE,
|
|
49
50
|
parseIslandProbe,
|
|
50
51
|
shotLines,
|
|
@@ -218,6 +219,11 @@ export interface ShotRun {
|
|
|
218
219
|
*/
|
|
219
220
|
readonly acceptLanguage?: string | undefined;
|
|
220
221
|
readonly now?: (() => Date) | undefined;
|
|
222
|
+
/**
|
|
223
|
+
* `--expect-status`: the document status this shot is ok with — `404` to photograph the not-found
|
|
224
|
+
* page on purpose. Absent means any 2xx, and anything else fails the verdict with the status.
|
|
225
|
+
*/
|
|
226
|
+
readonly expectStatus?: number | undefined;
|
|
221
227
|
/**
|
|
222
228
|
* Something to do with the page AFTER the islands settled and BEFORE the picture — `ui.inspect`
|
|
223
229
|
* reads the DOM here, on the one navigation the picture already paid for. `settle` re-runs the
|
|
@@ -265,6 +271,9 @@ export async function runShot(options: ShotRun): Promise<ShotArtifacts> {
|
|
|
265
271
|
if (choice !== undefined) await page.prepare(choice);
|
|
266
272
|
}
|
|
267
273
|
await page.goto(requestedUrl, { timeout: options.timeoutMs });
|
|
274
|
+
// Read NOW as well as at capture: the network ring is bounded, and a page that fires a few
|
|
275
|
+
// hundred requests while it settles evicts the document entry the verdict needs.
|
|
276
|
+
const landedStatus = documentStatus(page.network(), page.url());
|
|
268
277
|
if (options.settleMs > 0) await Bun.sleep(options.settleMs);
|
|
269
278
|
// The probe may legitimately answer nothing — a page that refuses evaluation, a driver with no
|
|
270
279
|
// JS engine. `null` says so; a `0` would read as "the route renders no islands", which is a
|
|
@@ -303,6 +312,8 @@ export async function runShot(options: ShotRun): Promise<ShotArtifacts> {
|
|
|
303
312
|
network: page.network(),
|
|
304
313
|
networkDropped: page.networkDropped(),
|
|
305
314
|
islands,
|
|
315
|
+
landedStatus,
|
|
316
|
+
...(options.expectStatus === undefined ? {} : { expectStatus: options.expectStatus }),
|
|
306
317
|
});
|
|
307
318
|
mkdirSync(options.outDir, { recursive: true });
|
|
308
319
|
const image = join(options.outDir, SHOT_IMAGE);
|
|
@@ -387,6 +398,11 @@ export const shotCommand: CliCommand = {
|
|
|
387
398
|
const port = intFlag(ctx.args, 'port', PORT_RANGE.min, DEFAULT_PORT, PORT_RANGE.max);
|
|
388
399
|
const settleMs = intFlag(ctx.args, 'settle', 0, DEFAULT_SETTLE_MS);
|
|
389
400
|
const timeoutMs = intFlag(ctx.args, 'timeout', 1, DEFAULT_PAGE_TIMEOUT_MS);
|
|
401
|
+
// Read only when given: its absence means "any 2xx", which no single default number can say.
|
|
402
|
+
const expectStatus =
|
|
403
|
+
flagString(ctx.args, 'expect-status') === undefined
|
|
404
|
+
? undefined
|
|
405
|
+
: intFlag(ctx.args, 'expect-status', 100, 200, 599);
|
|
390
406
|
// Which browser this run gets — start one here, or attach to one somebody else is running.
|
|
391
407
|
// Decided by `shot-browser.ts` over plain inputs, and decided HERE, before a dev server or a
|
|
392
408
|
// provider session exists to pay for a typo. It also PROBES for an installed Chrome and refuses
|
|
@@ -427,6 +443,7 @@ export const shotCommand: CliCommand = {
|
|
|
427
443
|
timeoutMs,
|
|
428
444
|
fullPage: flagBool(ctx.args, 'full'),
|
|
429
445
|
extraHosts: flagString(ctx.args, 'allow-hosts'),
|
|
446
|
+
...(expectStatus === undefined ? {} : { expectStatus }),
|
|
430
447
|
};
|
|
431
448
|
if (matrix) {
|
|
432
449
|
// `--locale` and `--theme` narrow the matrix to one value of their axis.
|
package/src/messages.ts
CHANGED
|
@@ -189,6 +189,12 @@ const CATALOG = {
|
|
|
189
189
|
'cli.shot.ok': '{route} clean — {islands} island(s) mounted, nothing logged and nothing threw',
|
|
190
190
|
'cli.shot.errors': '{route}: {errors} console error(s) — verdict.json names each one',
|
|
191
191
|
'cli.shot.redirected': '{route} redirected to {url} — the picture is not the route asked for',
|
|
192
|
+
// After the redirect and before everything else: a 404 page can be spotless, and a picture of
|
|
193
|
+
// "Not found" read as the route is the capture lying about its subject.
|
|
194
|
+
'cli.shot.status':
|
|
195
|
+
'{route} answered HTTP {status}, expected {expected} — the picture is that response, not the page',
|
|
196
|
+
'cli.shot.document': ' document HTTP {status}',
|
|
197
|
+
'cli.shot.documentUnknown': ' document status not seen — the driver recorded no response for it',
|
|
192
198
|
'cli.shot.server.booted': ' server booted for this shot on {url}',
|
|
193
199
|
'cli.shot.server.reused': ' server the x dev already running on {url}',
|
|
194
200
|
'cli.shot.canvas': ' canvas {width}x{height}',
|
package/src/shot-verdict.ts
CHANGED
|
@@ -25,6 +25,9 @@ export const SHOT_MESSAGE_KEYS = [
|
|
|
25
25
|
'cli.shot.ok',
|
|
26
26
|
'cli.shot.errors',
|
|
27
27
|
'cli.shot.redirected',
|
|
28
|
+
'cli.shot.status',
|
|
29
|
+
'cli.shot.document',
|
|
30
|
+
'cli.shot.documentUnknown',
|
|
28
31
|
'cli.shot.picture',
|
|
29
32
|
'cli.shot.verdict',
|
|
30
33
|
'cli.shot.server.booted',
|
|
@@ -146,6 +149,13 @@ export interface ShotInput {
|
|
|
146
149
|
readonly networkDropped: number;
|
|
147
150
|
/** `null` when the probe could not run or did not parse. Never a zero standing in for unknown. */
|
|
148
151
|
readonly islands: IslandCount | null;
|
|
152
|
+
/**
|
|
153
|
+
* The document's status as read right after navigation. Only a fallback: the network ring is
|
|
154
|
+
* bounded, so on a page of a few hundred requests the document entry may be gone by capture time.
|
|
155
|
+
*/
|
|
156
|
+
readonly landedStatus?: number | null | undefined;
|
|
157
|
+
/** `--expect-status`: the one status this shot is ok with. Absent means any 2xx. */
|
|
158
|
+
readonly expectStatus?: number | undefined;
|
|
149
159
|
}
|
|
150
160
|
|
|
151
161
|
export interface ShotVerdict extends ShotInput {
|
|
@@ -161,10 +171,44 @@ export interface ShotVerdict extends ShotInput {
|
|
|
161
171
|
* gating, like a console warning: a missing favicon would otherwise fail every shot of every app.
|
|
162
172
|
*/
|
|
163
173
|
readonly failed: number;
|
|
174
|
+
/**
|
|
175
|
+
* The HTTP status of the document photographed. `null` when no driver response carried one —
|
|
176
|
+
* "not seen" never gates, like an uncounted island, and is never reported as a 200.
|
|
177
|
+
*/
|
|
178
|
+
readonly status: number | null;
|
|
179
|
+
/** The status asked for with `--expect-status`, or `null` for "any 2xx". */
|
|
180
|
+
readonly expectedStatus: number | null;
|
|
164
181
|
/** What this verdict cannot see, stated every time — a `0` whose blind spots are named. */
|
|
165
182
|
readonly blind: readonly string[];
|
|
166
183
|
}
|
|
167
184
|
|
|
185
|
+
const withoutFragment = (url: string): string => {
|
|
186
|
+
const hash = url.indexOf('#');
|
|
187
|
+
return hash === -1 ? url : url.slice(0, hash);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The status of the LAST document response at `url` — the last, because a reload or a redirect
|
|
192
|
+
* back re-requests it and the picture is of the latest. The fragment is dropped: `location.href`
|
|
193
|
+
* keeps it and no request ever carries one.
|
|
194
|
+
*/
|
|
195
|
+
export function documentStatus(network: readonly NetworkEntry[], url: string): number | null {
|
|
196
|
+
const target = withoutFragment(url);
|
|
197
|
+
for (let index = network.length - 1; index >= 0; index -= 1) {
|
|
198
|
+
const entry = network[index];
|
|
199
|
+
if (entry === undefined || entry.resourceType !== 'document') continue;
|
|
200
|
+
if (withoutFragment(entry.url) !== target) continue;
|
|
201
|
+
if (entry.status !== undefined) return entry.status;
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** ok for a status: the one expected, or any 2xx when none was. Unseen is not a failure. */
|
|
207
|
+
const statusOk = (status: number | null, expected: number | null): boolean => {
|
|
208
|
+
if (expected !== null) return status === expected;
|
|
209
|
+
return status === null || (status >= 200 && status < 300);
|
|
210
|
+
};
|
|
211
|
+
|
|
168
212
|
/**
|
|
169
213
|
* What a shot is blind to, each naming the mechanism rather than apologising. Constant because
|
|
170
214
|
* these are properties of the browser port, not of a run — and in the artifact because `errors: 0`
|
|
@@ -183,7 +227,7 @@ const levelCount = (lines: readonly ConsoleLine[], level: ConsoleLine['level']):
|
|
|
183
227
|
lines.filter((line) => line.level === level).length;
|
|
184
228
|
|
|
185
229
|
/**
|
|
186
|
-
* `ok` is
|
|
230
|
+
* `ok` is five conditions, and every one is something a picture cannot show: nothing on the page
|
|
187
231
|
* logged an error, nothing THREW, no island's `mount()` REJECTED, and the document photographed is
|
|
188
232
|
* the route that was asked for.
|
|
189
233
|
*
|
|
@@ -195,16 +239,23 @@ const levelCount = (lines: readonly ConsoleLine[], level: ConsoleLine['level']):
|
|
|
195
239
|
* an uncounted probe (`null`) out of the verdict — "not counted" is not "none failed".
|
|
196
240
|
* A redirect is a failure of the CAPTURE rather than of the app: an agent that
|
|
197
241
|
* photographs the sign-in page and files "the island did not mount" is the outcome this prevents.
|
|
242
|
+
* A fifth (`As of 2026-09-26`): the document answered 2xx, or exactly `--expect-status`. A 404
|
|
243
|
+
* page is clean by every other clause, and `x shot /typo` reported it ok.
|
|
198
244
|
*/
|
|
199
245
|
export function buildVerdict(input: ShotInput): ShotVerdict {
|
|
200
246
|
const errors = levelCount(input.console, 'error');
|
|
247
|
+
const status = documentStatus(input.network, input.finalUrl) ?? input.landedStatus ?? null;
|
|
248
|
+
const expectedStatus = input.expectStatus ?? null;
|
|
201
249
|
return {
|
|
202
250
|
...input,
|
|
203
251
|
ok:
|
|
204
252
|
errors === 0 &&
|
|
205
253
|
input.pageErrors.length === 0 &&
|
|
206
254
|
(input.islands?.failed ?? 0) === 0 &&
|
|
207
|
-
input.requestedUrl === input.finalUrl
|
|
255
|
+
input.requestedUrl === input.finalUrl &&
|
|
256
|
+
statusOk(status, expectedStatus),
|
|
257
|
+
status,
|
|
258
|
+
expectedStatus,
|
|
208
259
|
redirected: input.requestedUrl !== input.finalUrl,
|
|
209
260
|
errors,
|
|
210
261
|
warnings: levelCount(input.console, 'warn'),
|
|
@@ -241,6 +292,8 @@ export function verdictJson(verdict: ShotVerdict): JsonValue {
|
|
|
241
292
|
requestedUrl: verdict.requestedUrl,
|
|
242
293
|
finalUrl: verdict.finalUrl,
|
|
243
294
|
redirected: verdict.redirected,
|
|
295
|
+
status: verdict.status,
|
|
296
|
+
expectedStatus: verdict.expectedStatus,
|
|
244
297
|
server: verdict.server,
|
|
245
298
|
capturedAt: verdict.capturedAt,
|
|
246
299
|
screenshot: verdict.screenshot,
|
|
@@ -301,6 +354,9 @@ export function shotLines(artifacts: ShotArtifacts): readonly string[] {
|
|
|
301
354
|
const canvas = verdict.canvas;
|
|
302
355
|
return [
|
|
303
356
|
msg(`cli.shot.server.${verdict.server}`, { url: verdict.finalUrl }),
|
|
357
|
+
verdict.status === null
|
|
358
|
+
? msg('cli.shot.documentUnknown')
|
|
359
|
+
: msg('cli.shot.document', { status: verdict.status }),
|
|
304
360
|
canvas === null
|
|
305
361
|
? msg('cli.shot.canvasUnreadable', { bytes: verdict.bytes.byteLength })
|
|
306
362
|
: msg('cli.shot.canvas', { width: canvas.width, height: canvas.height }),
|
|
@@ -338,6 +394,13 @@ export const shotSummary = (verdict: ShotVerdict): string => {
|
|
|
338
394
|
if (verdict.redirected) {
|
|
339
395
|
return msg('cli.shot.redirected', { route: verdict.route, url: verdict.finalUrl });
|
|
340
396
|
}
|
|
397
|
+
if (!statusOk(verdict.status, verdict.expectedStatus)) {
|
|
398
|
+
return msg('cli.shot.status', {
|
|
399
|
+
route: verdict.route,
|
|
400
|
+
status: verdict.status ?? 'unseen',
|
|
401
|
+
expected: verdict.expectedStatus ?? '2xx',
|
|
402
|
+
});
|
|
403
|
+
}
|
|
341
404
|
// Ahead of the console count, because a throw is the more severe fact AND the quieter one: an
|
|
342
405
|
// island that died can log nothing at all, so `errors` would report a clean page.
|
|
343
406
|
if (verdict.pageErrors.length > 0) {
|
package/src/test-workers.ts
CHANGED
|
@@ -90,6 +90,19 @@ export const defaultWorkers = (
|
|
|
90
90
|
return Math.max(WORKER_FLOOR, Math.min(byCpu, byMemory, WORKER_CEILING));
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* The width for a parallel suite that SHARES the machine — `x verify` runs the static steps beside
|
|
95
|
+
* `live`, `job`, `e2e` and `eval` (`verify-run.ts`), and each of those scans is a CPU-bound process
|
|
96
|
+
* of its own. `defaultWorkers()`' oversubscription fills a worker's own stalls when nothing else
|
|
97
|
+
* wants the cores; beside six other processes it only multiplies the contention. Measured on
|
|
98
|
+
* notificado.co, 8 vCPU (#537): at 1.5x, `errors` went 1.8s alone → 7.9s in the gate and
|
|
99
|
+
* `boundaries` 2.3s → 9.7s. So: one worker per core, still held to what memory can carry.
|
|
100
|
+
*/
|
|
101
|
+
export const sharedWorkers = (
|
|
102
|
+
available: number = availableCpus(),
|
|
103
|
+
freeBytes: number = availableMemory(),
|
|
104
|
+
): number => Math.min(defaultWorkers(available, freeBytes), Math.max(WORKER_FLOOR, available));
|
|
105
|
+
|
|
93
106
|
/**
|
|
94
107
|
* Which types run across worker processes, and why the other two cannot.
|
|
95
108
|
*
|
package/src/verify-run.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { ERROR_DOCS_URL, renderThrowable } from '@ultimat3/core';
|
|
6
6
|
import { msg } from './messages';
|
|
7
7
|
import type { CommandResult, Finding, StepResult } from './output';
|
|
8
|
+
import { sharedWorkers } from './test-workers';
|
|
8
9
|
import {
|
|
9
10
|
floorRequires,
|
|
10
11
|
readVerifyFloor,
|
|
@@ -40,6 +41,10 @@ export async function runVerify(
|
|
|
40
41
|
await pending;
|
|
41
42
|
pending = undefined;
|
|
42
43
|
};
|
|
44
|
+
// A parallel suite inside that window shares the cores with the static group, so its DEFAULT
|
|
45
|
+
// width is one per core (`sharedWorkers`); an explicit `--workers` is the caller's and stands.
|
|
46
|
+
const shared: VerifyContext =
|
|
47
|
+
ctx.workers === undefined && beside.length > 0 ? { ...ctx, workers: sharedWorkers() } : ctx;
|
|
43
48
|
for (const step of selected) {
|
|
44
49
|
if (beside.includes(step)) continue;
|
|
45
50
|
if (step.name === SERIAL_SUITES[0]) {
|
|
@@ -51,7 +56,8 @@ export async function runVerify(
|
|
|
51
56
|
} else if (!SERIAL_SUITES.includes(step.name)) {
|
|
52
57
|
await join();
|
|
53
58
|
}
|
|
54
|
-
|
|
59
|
+
const inWindow = pending !== undefined && SERIAL_SUITES.includes(step.name);
|
|
60
|
+
byName.set(step.name, await runStep(step, inWindow ? shared : ctx, floor));
|
|
55
61
|
}
|
|
56
62
|
await join();
|
|
57
63
|
// Reported in the declared order, whatever order the steps finished in: the table, `--json` and
|