@ultimat3/cli 20.1.0 → 20.1.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/cli",
3
- "version": "20.1.0",
3
+ "version": "20.1.1",
4
4
  "description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,34 +37,34 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@babel/core": "^7.28.4",
40
- "@ultimat3/action": "20.1.0",
41
- "@ultimat3/admin": "20.1.0",
42
- "@ultimat3/ai": "20.1.0",
43
- "@ultimat3/auth": "20.1.0",
44
- "@ultimat3/cache": "20.1.0",
45
- "@ultimat3/core": "20.1.0",
46
- "@ultimat3/db": "20.1.0",
47
- "@ultimat3/entity": "20.1.0",
48
- "@ultimat3/flags": "20.1.0",
49
- "@ultimat3/http": "20.1.0",
50
- "@ultimat3/i18n": "20.1.0",
51
- "@ultimat3/jobs": "20.1.0",
52
- "@ultimat3/mail": "20.1.0",
53
- "@ultimat3/manifest": "20.1.0",
54
- "@ultimat3/mcp": "20.1.0",
55
- "@ultimat3/money": "20.1.0",
56
- "@ultimat3/notify": "20.1.0",
57
- "@ultimat3/policy": "20.1.0",
58
- "@ultimat3/pwa": "20.1.0",
59
- "@ultimat3/query": "20.1.0",
60
- "@ultimat3/realtime": "20.1.0",
61
- "@ultimat3/render": "20.1.0",
62
- "@ultimat3/schema": "20.1.0",
63
- "@ultimat3/scraping": "20.1.0",
64
- "@ultimat3/seo": "20.1.0",
65
- "@ultimat3/storage": "20.1.0",
66
- "@ultimat3/testing": "20.1.0",
67
- "@ultimat3/time": "20.1.0",
40
+ "@ultimat3/action": "20.1.1",
41
+ "@ultimat3/admin": "20.1.1",
42
+ "@ultimat3/ai": "20.1.1",
43
+ "@ultimat3/auth": "20.1.1",
44
+ "@ultimat3/cache": "20.1.1",
45
+ "@ultimat3/core": "20.1.1",
46
+ "@ultimat3/db": "20.1.1",
47
+ "@ultimat3/entity": "20.1.1",
48
+ "@ultimat3/flags": "20.1.1",
49
+ "@ultimat3/http": "20.1.1",
50
+ "@ultimat3/i18n": "20.1.1",
51
+ "@ultimat3/jobs": "20.1.1",
52
+ "@ultimat3/mail": "20.1.1",
53
+ "@ultimat3/manifest": "20.1.1",
54
+ "@ultimat3/mcp": "20.1.1",
55
+ "@ultimat3/money": "20.1.1",
56
+ "@ultimat3/notify": "20.1.1",
57
+ "@ultimat3/policy": "20.1.1",
58
+ "@ultimat3/pwa": "20.1.1",
59
+ "@ultimat3/query": "20.1.1",
60
+ "@ultimat3/realtime": "20.1.1",
61
+ "@ultimat3/render": "20.1.1",
62
+ "@ultimat3/schema": "20.1.1",
63
+ "@ultimat3/scraping": "20.1.1",
64
+ "@ultimat3/seo": "20.1.1",
65
+ "@ultimat3/storage": "20.1.1",
66
+ "@ultimat3/testing": "20.1.1",
67
+ "@ultimat3/time": "20.1.1",
68
68
  "babel-preset-solid": "^1.9.15"
69
69
  }
70
70
  }
package/src/e2e-driver.ts CHANGED
@@ -70,7 +70,8 @@ export const e2eFixtures = (page: PageLike, browser: E2eBrowserPage): E2eFixture
70
70
  * declaration `driverFixtures()` registered — the ordinary way a driver arrives, last registration
71
71
  * wins — so every `test('…', async ({ page }) => …)` in the suite gets a browser. `useE2eDriver`
72
72
  * is the other half: it is what makes `hasE2eDriver()` answer true and stops `e2eTest` becoming a
73
- * `test.skip` a green gate reports over.
73
+ * `test.skip` — which the gate now reports as a SKIPPED step rather than the green check it
74
+ * printed until #434, and which a repo whose `x.verify.json` names `e2e` gets red for.
74
75
  *
75
76
  * `budget`, `signIn` and `deploy` are deliberately NOT registered here. Each needs something a
76
77
  * page cannot supply — byte counts off a built `dist/`, an app's own sign-in route, a second build
package/src/messages.ts CHANGED
@@ -279,6 +279,14 @@ const CATALOG = {
279
279
  // The `errors` step's own coverage, in `output`: a scan without a parser reads most fix lines
280
280
  // and not all of them, and a step that reports findings alone claims a completeness it lacks.
281
281
  'cli.verify.fixCoverage': 'checked {checked} fix line(s), could not read {unreadable}',
282
+ // Why a skipped step is a dash when the step can say so: the suite exists and not one of its
283
+ // tests ran. Each test's own name carries the reason it skipped, and `bun test` prints only the
284
+ // counts — so the line says how many and `x test <type>` prints the names.
285
+ 'cli.verify.allSkipped': 'found {skipped} test(s) and every one skipped itself',
286
+ // The other shape the same counts can take: a suite that reported no test at all, skipped ones
287
+ // included. Its own line and not `allSkipped` with a zero, because the repair differs — there is
288
+ // no skipped test to read a reason off, so the files the step selected hold no test.
289
+ 'cli.verify.ranNothing': 'found no test to run in the file(s) it selected',
282
290
  'cli.verify.serial': 'serial',
283
291
  'cli.verify.workers': '{workers} workers',
284
292
  'cli.env.checked': '{count} declared variable(s), all present and valid',
package/src/output.ts CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { ERROR_DOCS_URL, renderThrowable, singleLine, stringField } from '@ultimat3/core';
6
6
  import { msg } from './messages';
7
+ import type { TestCounts } from './test-counts';
7
8
 
8
9
  export interface Finding {
9
10
  readonly code: string;
@@ -24,6 +25,13 @@ export interface StepResult {
24
25
  readonly output?: string;
25
26
  /** Worker processes the step used; `1` means it ran serially. Absent for a non-test step. */
26
27
  readonly workers?: number;
28
+ /**
29
+ * What the step's suite executed. Absent for a step that spawned no test process, which is NOT
30
+ * the same state as `{ ran: 0 }` — a step with no suite and a suite whose every test skipped
31
+ * itself both report `skipped`, and this is the only thing that tells a reader which one it is
32
+ * looking at (#434).
33
+ */
34
+ readonly tests?: TestCounts;
27
35
  }
28
36
 
29
37
  export type JsonValue =
@@ -164,10 +172,27 @@ const width = (step: StepResult): string => {
164
172
  return ` ${step.workers === 1 ? msg('cli.verify.serial') : msg('cli.verify.workers', { workers: step.workers })}`;
165
173
  };
166
174
 
175
+ /**
176
+ * Why a step is a dash, when the step itself can say. `- roadmap` is "there is nothing here to
177
+ * check"; a step that spawned a suite and executed none of it is a different state, which is what
178
+ * issue #434 reported as a green check — so a step carrying counts always says which it is, in one
179
+ * of the two shapes its counts can take. The reason each test skipped lives in that test's own
180
+ * NAME — `bun test` prints only the counts — so the line points at the suite and `x test <type>`
181
+ * is what prints the names.
182
+ */
183
+ const why = (step: StepResult): string => {
184
+ const tests = step.tests;
185
+ if (step.skipped !== true || tests === undefined) return '';
186
+ if (tests.skipped === 0) return ` ${msg('cli.verify.ranNothing')}`;
187
+ return ` ${msg('cli.verify.allSkipped', { skipped: tests.skipped })}`;
188
+ };
189
+
167
190
  export function renderHuman(result: CommandResult, verbose = false): string {
168
191
  const out: string[] = [];
169
192
  for (const step of result.steps ?? []) {
170
- out.push(` ${mark(step)} ${step.name.padEnd(18)} ${step.durationMs}ms${width(step)}`);
193
+ out.push(
194
+ ` ${mark(step)} ${step.name.padEnd(18)} ${step.durationMs}ms${width(step)}${why(step)}`,
195
+ );
171
196
  for (const finding of step.findings) out.push(renderFinding(finding, ' '));
172
197
  // NOT escaped, and that is the one exception: `output` is this process's own captured
173
198
  // subprocess stdout — `bun test`'s colour is the reason a human reads it at all, and it is
@@ -196,6 +221,9 @@ export function renderJson(result: CommandResult): string {
196
221
  skipped: step.skipped === true,
197
222
  findings: step.findings,
198
223
  ...(step.workers === undefined ? {} : { workers: step.workers }),
224
+ // The counts the human line's `why()` renders, as numbers: a `--json` reader deciding whether
225
+ // a skipped lane is missing a suite or missing a prerequisite needs the same fact CI's log has.
226
+ ...(step.tests === undefined ? {} : { tests: step.tests }),
199
227
  // A FAILED step carries its captured stdout, exactly as the human renderer prints it. CI runs
200
228
  // `--json`, and without this the log said only "one or more unit tests failed" with a generic
201
229
  // fix line — the failing test's name and its assertion diff existed and were thrown away, so
@@ -295,7 +295,9 @@ const biome = (): string => `{
295
295
  *
296
296
  * Two remain absent. `e2e` has a scaffolded file and it is an `e2eTest` — `test.skip` until the app
297
297
  * registers a browser driver, so the step would run zero tests and fail the ratchet on the
298
- * scaffold's own placeholder. `contract-diff` needs a committed `x.manifest.json`, which
298
+ * scaffold's own placeholder. Its absence is visible rather than silent: a suite that executed
299
+ * nothing reports as a SKIPPED step, not the green check it printed until #434, so the app's own
300
+ * gate line says `- e2e` and names it in the summary until a driver makes it run. `contract-diff` needs a committed `x.manifest.json`, which
299
301
  * `x manifest` writes later. Each joins the list in the commit that makes the app's gate run it.
300
302
  */
301
303
  const SCAFFOLD_FLOOR: readonly VerifyStepName[] = [
package/src/verify-run.ts CHANGED
@@ -53,20 +53,29 @@ export async function runVerify(
53
53
  findings: [findingOf(error, step.name)],
54
54
  }),
55
55
  );
56
+ // A suite that executed nothing did not run, whatever its exit code says: `bun test` exits 0
57
+ // over an all-skipped file, so the counts are the only channel that can tell the two apart.
58
+ // ONE definition of "nothing ran", read twice, because the floor decides which of the two
59
+ // things it means — exactly as it already does for a step whose `applies` said no.
60
+ const tests = outcome.tests;
61
+ const nothingRan = tests !== undefined && tests.ran === 0;
62
+ const required = floorRequires(floor, step.name);
56
63
  // A step the floor requires whose suite executed nothing is the same vanished suite as a step
57
64
  // with no files at all — the run just had to finish before it could be seen. Appended to the
58
65
  // step's own findings so `data.failed`, the counts and every gate reading this table carry it.
59
- const vanished =
60
- floorRequires(floor, step.name) && outcome.tests !== undefined && outcome.tests.ran === 0
61
- ? [skippedSuiteFinding(step.name, outcome.tests.skipped)]
62
- : [];
66
+ const vanished = nothingRan && required ? [skippedSuiteFinding(step.name, tests.skipped)] : [];
63
67
  results.push({
64
68
  name: step.name,
65
69
  ok: outcome.ok && vanished.length === 0,
66
70
  durationMs: Math.round(performance.now() - started),
71
+ // Without a floor to require it, a suite that ran nothing is a SKIP and not a pass (#434):
72
+ // the `e2e` step printed `✓ e2e 46ms` over its one skipped test, which is the one thing a
73
+ // step table may never do — a reader cannot tell a lane that ran from a lane that did not.
74
+ skipped: nothingRan && !required,
67
75
  findings: [...outcome.findings, ...vanished],
68
76
  ...(outcome.output === undefined ? {} : { output: outcome.output }),
69
77
  ...(outcome.workers === undefined ? {} : { workers: outcome.workers }),
78
+ ...(tests === undefined ? {} : { tests }),
70
79
  });
71
80
  }
72
81
  const failedSteps = results.filter((step) => !step.ok).map((step) => step.name);