vibeview 0.38.0 → 0.39.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.
@@ -15,6 +15,31 @@ import {
15
15
  uploadAppBuild
16
16
  } from "./chunk-MYXUGCR5.js";
17
17
 
18
+ // src/lib/formatters/not-checked.ts
19
+ function notCheckedSteps(steps) {
20
+ return (steps ?? []).filter((s) => s.status === "not_checked");
21
+ }
22
+ function runStepsNotChecked(result) {
23
+ if (typeof result.steps_not_checked === "number") return result.steps_not_checked;
24
+ return notCheckedSteps(result.steps).length;
25
+ }
26
+ function stepsNotCheckedLabel(n) {
27
+ return `${n} step${n === 1 ? "" : "s"} not checked`;
28
+ }
29
+ function notCheckedSuffix(status, n) {
30
+ return status === "passed" && n && n > 0 ? ` (${stepsNotCheckedLabel(n)})` : "";
31
+ }
32
+ function outcomeOf(r) {
33
+ if (r.not_checked === true || r.status === "not_checked") return "not_checked";
34
+ if (r.not_run === true || r.status === "cancelled" && isNotRunMessage(r.error_message)) {
35
+ return "not_run";
36
+ }
37
+ return r.status ?? "";
38
+ }
39
+ function isNotRunMessage(message) {
40
+ return (message ?? "").startsWith("Not run:");
41
+ }
42
+
18
43
  // src/lib/dev-config.ts
19
44
  import { readFileSync, renameSync, rmSync, writeFileSync } from "fs";
20
45
  import { join } from "path";
@@ -5168,6 +5193,11 @@ async function runAgentVerb(cmd, positionals2, opts) {
5168
5193
  }
5169
5194
 
5170
5195
  export {
5196
+ notCheckedSteps,
5197
+ runStepsNotChecked,
5198
+ stepsNotCheckedLabel,
5199
+ notCheckedSuffix,
5200
+ outcomeOf,
5171
5201
  DEV_PLATFORMS,
5172
5202
  PLATFORM_NEEDS_METRO,
5173
5203
  isDevPlatform,
package/dist/cli.js CHANGED
@@ -15,6 +15,9 @@ import {
15
15
  listBuilds,
16
16
  loadDevConfig,
17
17
  newTransitionState,
18
+ notCheckedSteps,
19
+ notCheckedSuffix,
20
+ outcomeOf,
18
21
  platformLabel,
19
22
  platformWithArticle,
20
23
  pressButtonsHelp,
@@ -24,8 +27,10 @@ import {
24
27
  readDevStateFile,
25
28
  readMetroState,
26
29
  recordDevUse,
27
- runAgentVerb
28
- } from "./chunk-AV674D7T.js";
30
+ runAgentVerb,
31
+ runStepsNotChecked,
32
+ stepsNotCheckedLabel
33
+ } from "./chunk-EQN3X62U.js";
29
34
  import {
30
35
  ApiClient,
31
36
  ApiStatusError,
@@ -61,21 +66,6 @@ import chalk2 from "chalk";
61
66
  // src/lib/formatters/human.ts
62
67
  import chalk from "chalk";
63
68
 
64
- // src/lib/formatters/not-checked.ts
65
- function notCheckedSteps(steps) {
66
- return (steps ?? []).filter((s) => s.status === "not_checked");
67
- }
68
- function runStepsNotChecked(result) {
69
- if (typeof result.steps_not_checked === "number") return result.steps_not_checked;
70
- return notCheckedSteps(result.steps).length;
71
- }
72
- function stepsNotCheckedLabel(n) {
73
- return `${n} step${n === 1 ? "" : "s"} not checked`;
74
- }
75
- function notCheckedSuffix(status, n) {
76
- return status === "passed" && n && n > 0 ? ` (${stepsNotCheckedLabel(n)})` : "";
77
- }
78
-
79
69
  // src/lib/formatters/cases.ts
80
70
  function resultCases(result) {
81
71
  if (Array.isArray(result.test_runs)) return result.test_runs;
@@ -91,6 +81,8 @@ function resultCases(result) {
91
81
  screenshot_signed_url: failedStep?.screenshot_signed_url ?? null,
92
82
  url: result.url,
93
83
  quarantined: result.quarantined,
84
+ not_checked: result.not_checked,
85
+ not_run: result.not_run,
94
86
  steps_not_checked: runStepsNotChecked(result),
95
87
  not_checked_steps: notCheckedSteps(result.steps).map((s) => ({
96
88
  step: s.step,
@@ -118,19 +110,21 @@ function failureLines(failure) {
118
110
  }
119
111
  function formatHuman(result, verbose) {
120
112
  const lines = [];
121
- const icon = result.status === "passed" ? chalk.green("PASS") : result.status === "failed" && result.quarantined === true ? chalk.yellow("FAIL (quarantined)") : chalk.red("FAIL");
113
+ const outcome = outcomeOf(result);
114
+ const icon = outcome === "not_checked" ? chalk.gray("NOT CHECKED") : result.status === "passed" ? chalk.green("PASS") : result.status === "failed" && result.quarantined === true ? chalk.yellow("FAIL (quarantined)") : chalk.red("FAIL");
122
115
  const duration = result.duration_ms ? `${(result.duration_ms / 1e3).toFixed(1)}s` : "-";
123
116
  lines.push("");
124
117
  const title = result.suite_name || result.case_name || result.run_id;
125
118
  const cases = resultCases(result);
126
119
  const notChecked = cases.reduce((n, t) => n + (t.steps_not_checked ?? 0), 0);
127
- const runSuffix = notCheckedSuffix(result.status, notChecked);
120
+ const runSuffix = notCheckedSuffix(outcome, notChecked);
128
121
  lines.push(
129
122
  ` ${icon}${runSuffix ? chalk.yellow(runSuffix) : ""} ${chalk.bold(title)} ${chalk.dim(duration)}`
130
123
  );
131
- const passed = cases.filter((t) => t.status === "passed").length;
124
+ const passed = cases.filter((t) => outcomeOf(t) === "passed").length;
125
+ const notCheckedCases = cases.filter((t) => outcomeOf(t) === "not_checked").length;
132
126
  const total = cases.length;
133
- const notCheckedNote = notChecked > 0 ? `, ${stepsNotCheckedLabel(notChecked)}` : "";
127
+ const notCheckedNote = notCheckedCases > 0 ? `, ${notCheckedCases} not checked` : notChecked > 0 ? `, ${stepsNotCheckedLabel(notChecked)}` : "";
134
128
  lines.push(` ${chalk.dim(`${passed}/${total} cases passed${notCheckedNote}`)}`);
135
129
  if (result.url) {
136
130
  lines.push(` ${chalk.dim(result.url)}`);
@@ -139,16 +133,19 @@ function formatHuman(result, verbose) {
139
133
  if (suiteCases.length > 0) {
140
134
  lines.push("");
141
135
  for (const tr of suiteCases) {
142
- const caseIcon = tr.status === "passed" ? chalk.green(" PASS") : tr.status === "failed" && tr.quarantined === true ? chalk.yellow(" FAIL (quarantined)") : tr.status === "cancelled" || tr.status === "pending" || tr.status === "running" ? chalk.yellow(" SKIP") : chalk.red(" FAIL");
136
+ const caseOutcome = outcomeOf(tr);
137
+ const caseIcon = caseOutcome === "not_checked" ? chalk.gray(" NOT CHECKED") : caseOutcome === "not_run" ? chalk.gray(" NOT RUN") : tr.status === "passed" ? chalk.green(" PASS") : tr.status === "failed" && tr.quarantined === true ? chalk.yellow(" FAIL (quarantined)") : tr.status === "cancelled" || tr.status === "pending" || tr.status === "running" ? chalk.yellow(" SKIP") : chalk.red(" FAIL");
143
138
  const caseDuration = tr.duration_ms ? `${(tr.duration_ms / 1e3).toFixed(1)}s` : "-";
144
- const caseSuffix = notCheckedSuffix(tr.status, tr.steps_not_checked);
139
+ const caseSuffix = notCheckedSuffix(caseOutcome, tr.steps_not_checked);
145
140
  lines.push(
146
141
  ` ${caseIcon}${caseSuffix ? chalk.yellow(caseSuffix) : ""} ${tr.case_name} ${chalk.dim(caseDuration)}`
147
142
  );
148
143
  if (tr.url) {
149
144
  lines.push(` ${chalk.dim(tr.url)}`);
150
145
  }
151
- if (tr.status !== "passed") {
146
+ if (caseOutcome === "not_checked" || caseOutcome === "not_run") {
147
+ if (tr.error_message) lines.push(` ${chalk.dim(tr.error_message)}`);
148
+ } else if (tr.status !== "passed") {
152
149
  lines.push(...failureLines(tr));
153
150
  }
154
151
  }
@@ -174,7 +171,10 @@ function formatHuman(result, verbose) {
174
171
  }
175
172
  }
176
173
  }
177
- if (result.error_message) {
174
+ if (result.error_message && outcome === "not_checked") {
175
+ lines.push(`
176
+ ${chalk.dim(result.error_message)}`);
177
+ } else if (result.error_message) {
178
178
  lines.push(`
179
179
  ${chalk.red("Error:")} ${result.error_message}`);
180
180
  }
@@ -216,13 +216,18 @@ function caseDetailLines(c) {
216
216
  return lines;
217
217
  }
218
218
  var isQuarantinedFailure = (t) => t.status === "failed" && t.quarantined === true;
219
+ var NEUTRAL = /* @__PURE__ */ new Set(["not_checked", "not_run"]);
220
+ function neutralMessage(outcome, message) {
221
+ if (outcome === "not_checked") return `not checked: ${message || "no step was checked"}`;
222
+ return message || "Not run";
223
+ }
219
224
  function formatJunit(result) {
220
225
  const suiteName = escapeXml(result.suite_name || result.case_name || result.run_id);
221
226
  const cases = resultCases(result);
222
227
  const tests = cases.length;
223
228
  const failures = cases.filter((t) => t.status === "failed" && !isQuarantinedFailure(t)).length;
224
229
  const errors = cases.filter((t) => t.status === "error").length;
225
- const skipped = cases.filter(isQuarantinedFailure).length;
230
+ const skipped = cases.filter((t) => isQuarantinedFailure(t) || NEUTRAL.has(outcomeOf(t))).length;
226
231
  const skippedAttr = skipped > 0 ? ` skipped="${skipped}"` : "";
227
232
  const totalTime = result.duration_ms ? (result.duration_ms / 1e3).toFixed(1) : "0";
228
233
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
@@ -249,8 +254,9 @@ function formatJunit(result) {
249
254
  for (const tr of cases) {
250
255
  const caseName = escapeXml(tr.case_name);
251
256
  const caseTime = tr.duration_ms ? (tr.duration_ms / 1e3).toFixed(1) : "0";
252
- const detail = caseDetailLines(tr);
253
- if (tr.status === "passed" && detail.length === 0) {
257
+ const outcome = outcomeOf(tr);
258
+ const detail = caseDetailLines({ ...tr, status: outcome });
259
+ if (outcome === "passed" && detail.length === 0) {
254
260
  lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}" />`);
255
261
  continue;
256
262
  }
@@ -258,6 +264,10 @@ function formatJunit(result) {
258
264
  if (isQuarantinedFailure(tr)) {
259
265
  const msg = escapeXml(tr.error_message || "Test failed");
260
266
  lines.push(` <skipped message="quarantined: ${msg}" />`);
267
+ } else if (NEUTRAL.has(outcome)) {
268
+ lines.push(
269
+ ` <skipped message="${escapeXml(neutralMessage(outcome, tr.error_message))}" />`
270
+ );
261
271
  } else if (tr.status === "failed") {
262
272
  const msg = escapeXml(tr.error_message || "Test failed");
263
273
  lines.push(` <failure message="${msg}" type="TestFailure">${msg}</failure>`);
@@ -294,7 +304,7 @@ function formatJunitError(name, message, url) {
294
304
  lines.push("</testsuites>");
295
305
  return lines.join("\n");
296
306
  }
297
- var MULTI_TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
307
+ var MULTI_TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled", "not_checked"]);
298
308
  function caseLines(c, classname) {
299
309
  const name = escapeXml(c.name);
300
310
  const head = ` <testcase name="${name}" classname="${classname}" time="${c.time}"`;
@@ -331,6 +341,14 @@ function formatMultiSuiteJunit(result, opts = {}) {
331
341
  url: c.url || void 0,
332
342
  steps_not_checked: c.steps_not_checked
333
343
  };
344
+ const outcome = outcomeOf(c);
345
+ if (NEUTRAL.has(outcome)) {
346
+ return {
347
+ ...base,
348
+ verdict: "skipped",
349
+ message: neutralMessage(outcome, c.error_message)
350
+ };
351
+ }
334
352
  if (c.status === "failed" && c.quarantined === true) {
335
353
  return {
336
354
  ...base,
@@ -409,6 +427,7 @@ function formatMultiSuiteJunit(result, opts = {}) {
409
427
  }
410
428
 
411
429
  // src/lib/poll.ts
430
+ var TERMINAL_STATUSES = ["passed", "failed", "error", "cancelled", "not_checked"];
412
431
  function resumeSpinner(spinner, text) {
413
432
  if (spinner.isEnabled === false) {
414
433
  if (text !== void 0) spinner.text = text;
@@ -506,20 +525,26 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
506
525
  `${chalk2.cyan("\u25B6")} ${chalk2.bold(tr.case_name || "Case")} started`
507
526
  );
508
527
  }
509
- const isTerminal = ["passed", "failed", "error", "cancelled"].includes(cur);
510
- const wasTerminal = prev && ["passed", "failed", "error", "cancelled"].includes(prev);
528
+ const isTerminal = TERMINAL_STATUSES.includes(cur);
529
+ const wasTerminal = prev && TERMINAL_STATUSES.includes(prev);
511
530
  if (isTerminal && !wasTerminal) {
512
- const icon = cur === "passed" ? chalk2.green("\u2713") : cur === "cancelled" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
531
+ const outcome = outcomeOf(tr);
532
+ const neutral = outcome === "not_checked" || outcome === "not_run";
533
+ const icon = neutral ? chalk2.gray("\u25CB") : cur === "passed" ? chalk2.green("\u2713") : cur === "cancelled" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
534
+ const label = neutral ? outcome.replace("_", " ") : cur;
513
535
  const dur = tr.duration_ms ? ` (${(tr.duration_ms / 1e3).toFixed(1)}s)` : "";
514
- printBelowSpinner(spinner, `${icon} ${chalk2.bold(tr.case_name || "Case")} ${cur}${dur}`);
515
- if (tr.error_message && cur !== "passed") {
536
+ printBelowSpinner(
537
+ spinner,
538
+ `${icon} ${chalk2.bold(tr.case_name || "Case")} ${label}${dur}`
539
+ );
540
+ if (tr.error_message && cur !== "passed" && !neutral) {
516
541
  printBelowSpinner(spinner, ` ${chalk2.red(tr.error_message)}`);
517
542
  }
518
543
  }
519
544
  caseStateBefore.set(tr.public_id, cur);
520
545
  }
521
546
  }
522
- if (["passed", "failed", "error", "cancelled"].includes(result.status)) {
547
+ if (TERMINAL_STATUSES.includes(result.status)) {
523
548
  if (printedSteps.size > 0 && Array.isArray(result.steps)) {
524
549
  spinner.stop();
525
550
  result.steps.forEach((st, i) => {
@@ -569,7 +594,7 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
569
594
  if (result.type === "suite_run" && Array.isArray(result.test_runs)) {
570
595
  const total = result.total_cases ?? result.test_runs.length;
571
596
  const done = result.test_runs.filter(
572
- (tr) => ["passed", "failed", "error", "cancelled"].includes(tr.status ?? "")
597
+ (tr) => TERMINAL_STATUSES.includes(tr.status ?? "")
573
598
  ).length;
574
599
  spinner.text = `Running suite \u2014 ${done}/${total} cases complete...`;
575
600
  } else {
@@ -666,10 +691,10 @@ function displayProgressEvent(evt, ctx) {
666
691
  break;
667
692
  }
668
693
  case "run_completed": {
669
- const icon = evt.status === "passed" ? chalk2.green("\u2713") : evt.status === "cancelled" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
694
+ const icon = evt.status === "not_checked" ? chalk2.gray("\u25CB") : evt.status === "passed" ? chalk2.green("\u2713") : evt.status === "cancelled" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
670
695
  spinner.stop();
671
696
  process.stderr.write(
672
- `${icon} Run ${evt.status}${evt.duration_ms ? ` (${(evt.duration_ms / 1e3).toFixed(1)}s)` : ""}
697
+ `${icon} Run ${evt.status === "not_checked" ? "not checked" : evt.status}${evt.duration_ms ? ` (${(evt.duration_ms / 1e3).toFixed(1)}s)` : ""}
673
698
  `
674
699
  );
675
700
  resumeSpinner(spinner);
@@ -709,7 +734,7 @@ function outputResult(result, options) {
709
734
  }
710
735
  }
711
736
  function mapExitCode(status) {
712
- if (status === "passed") return EXIT_PASS;
737
+ if (status === "passed" || status === "not_checked") return EXIT_PASS;
713
738
  if (status === "failed") return EXIT_TEST_FAIL;
714
739
  return EXIT_INFRA_ERROR;
715
740
  }
@@ -832,7 +857,8 @@ ${signal} received.
832
857
  device: options.device,
833
858
  build: options.build,
834
859
  metadata: options.metadata,
835
- retryFailed: options.retry === false ? false : void 0,
860
+ // Opt-in: always sent, so an older server's CI default never retries.
861
+ retryFailed: options.retry === true,
836
862
  platform: options.platform,
837
863
  app: options.app
838
864
  });
@@ -960,7 +986,8 @@ ${signal} received.
960
986
  device_id: opts.device,
961
987
  build_id: opts.build,
962
988
  metadata: opts.metadata,
963
- retry_failed: opts.retry === false ? false : void 0
989
+ // Opt-in: always sent, so an older server's CI default never retries.
990
+ retry_failed: opts.retry === true
964
991
  });
965
992
  } catch (err) {
966
993
  spinner.stop();
@@ -1021,7 +1048,7 @@ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
1021
1048
  pending = [];
1022
1049
  if (nextText !== null) spinner.start(nextText);
1023
1050
  };
1024
- const TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
1051
+ const TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled", "not_checked"]);
1025
1052
  let last = null;
1026
1053
  const abandon = async (reason) => {
1027
1054
  flush(null);
@@ -1080,10 +1107,11 @@ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
1080
1107
  }
1081
1108
  const wasTerminal = prev !== void 0 && TERMINAL.has(prev);
1082
1109
  if (TERMINAL.has(cur) && !wasTerminal) {
1083
- const mark = cur === "passed" ? chalk5.green("\u2713") : cur === "cancelled" ? chalk5.yellow("\u26A0") : chalk5.red("\u2717");
1110
+ const nothingChecked = outcomeOf(sr) === "not_checked";
1111
+ const mark = nothingChecked ? chalk5.gray("\u25CB") : cur === "passed" ? chalk5.green("\u2713") : cur === "cancelled" ? chalk5.yellow("\u26A0") : chalk5.red("\u2717");
1084
1112
  const link = sr.url ? chalk5.dim(" " + sr.url) : "";
1085
1113
  print(
1086
- `${mark} ${chalk5.bold(sr.suite_name)} ${cur}: ${sr.passed} passed, ${sr.failed} failed${link}`
1114
+ `${mark} ${chalk5.bold(sr.suite_name)} ${nothingChecked ? "not checked" : cur}: ${suiteCounts(sr)}${link}`
1087
1115
  );
1088
1116
  if (sr.error_message) {
1089
1117
  print(` ${chalk5.red(sr.error_message)}`);
@@ -1137,39 +1165,48 @@ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
1137
1165
  0
1138
1166
  );
1139
1167
  const aggregateNote = totalNotChecked > 0 ? chalk5.yellow(` (${stepsNotCheckedLabel(totalNotChecked)})`) : "";
1168
+ const aggregate = outcomeOf(last) === "not_checked" ? "not checked" : last.status;
1140
1169
  process.stdout.write(`
1141
- Aggregate: ${last.status}${aggregateNote}
1170
+ Aggregate: ${aggregate}${aggregateNote}
1142
1171
  `);
1143
1172
  for (const sr of last.suite_runs) {
1144
- const mark = sr.status === "passed" ? chalk5.green("\u2713") : chalk5.red("\u2717");
1173
+ const mark = outcomeOf(sr) === "not_checked" ? chalk5.gray("\u25CB") : sr.status === "passed" ? chalk5.green("\u2713") : chalk5.red("\u2717");
1145
1174
  const link = sr.url ? chalk5.dim(` ${sr.url}`) : "";
1146
- process.stdout.write(
1147
- ` ${mark} ${sr.suite_name}: ${sr.passed} passed, ${sr.failed} failed${link}
1148
- `
1149
- );
1175
+ process.stdout.write(` ${mark} ${sr.suite_name}: ${suiteCounts(sr)}${link}
1176
+ `);
1150
1177
  if (sr.error_message) {
1151
1178
  process.stdout.write(` ${chalk5.red(sr.error_message)}
1152
1179
  `);
1153
1180
  }
1154
1181
  for (const cr of sr.case_runs ?? []) {
1155
- const caseMark = cr.status === "passed" ? chalk5.green("\u2713") : cr.status === "cancelled" || cr.status === "pending" || cr.status === "running" ? chalk5.yellow("-") : chalk5.red("\u2717");
1182
+ const outcome = outcomeOf(cr);
1183
+ const neutral = outcome === "not_checked" || outcome === "not_run";
1184
+ const caseMark = neutral ? chalk5.gray("\u25CB") : cr.status === "passed" ? chalk5.green("\u2713") : cr.status === "cancelled" || cr.status === "pending" || cr.status === "running" ? chalk5.yellow("-") : chalk5.red("\u2717");
1156
1185
  const dur = cr.duration_ms ? chalk5.dim(` (${(cr.duration_ms / 1e3).toFixed(1)}s)`) : "";
1157
- const unchecked = notCheckedSuffix(cr.status, cr.steps_not_checked);
1186
+ const unchecked = notCheckedSuffix(outcome, cr.steps_not_checked);
1187
+ const label = neutral ? outcome.replace("_", " ") : cr.status ?? "?";
1158
1188
  process.stdout.write(
1159
- ` ${caseMark} ${cr.case_name ?? "Case"} ${cr.status ?? "?"}${unchecked ? chalk5.yellow(unchecked) : ""}${dur} ${chalk5.dim(cr.url)}
1189
+ ` ${caseMark} ${cr.case_name ?? "Case"} ${label}${unchecked ? chalk5.yellow(unchecked) : ""}${dur} ${chalk5.dim(cr.url)}
1160
1190
  `
1161
1191
  );
1162
- if (cr.error_message && cr.status !== "passed") {
1192
+ if (cr.error_message && neutral) {
1193
+ process.stdout.write(` ${chalk5.dim(cr.error_message)}
1194
+ `);
1195
+ } else if (cr.error_message && cr.status !== "passed") {
1163
1196
  process.stdout.write(` ${chalk5.red(cr.error_message)}
1164
1197
  `);
1165
1198
  }
1166
1199
  }
1167
1200
  }
1168
1201
  }
1169
- if (last.status === "passed") process.exit(EXIT_PASS);
1202
+ if (last.status === "passed" || last.status === "not_checked") process.exit(EXIT_PASS);
1170
1203
  if (last.status === "failed") process.exit(EXIT_TEST_FAIL);
1171
1204
  process.exit(EXIT_INFRA_ERROR);
1172
1205
  }
1206
+ function suiteCounts(sr) {
1207
+ const nc = sr.not_checked_tests ?? 0;
1208
+ return `${sr.passed} passed, ${sr.failed} failed${nc > 0 ? `, ${nc} not checked` : ""}`;
1209
+ }
1173
1210
 
1174
1211
  // src/commands/run-test.ts
1175
1212
  import chalk6 from "chalk";
@@ -1238,7 +1275,8 @@ ${signal} received.
1238
1275
  device: options.device,
1239
1276
  build: options.build,
1240
1277
  metadata: options.metadata,
1241
- retryFailed: options.retry === false ? false : void 0,
1278
+ // Opt-in: always sent, so an older server's CI default never retries.
1279
+ retryFailed: options.retry === true,
1242
1280
  platform: options.platform,
1243
1281
  app: options.app
1244
1282
  });
@@ -1641,7 +1679,7 @@ function formatImport(result, baseUrl) {
1641
1679
  }
1642
1680
  for (const f of result.flags) lines.push(flagLine(f));
1643
1681
  for (const t of [...result.tests, ...result.setups]) {
1644
- const meta = [`${t.steps.length} steps`];
1682
+ const meta = [`${t.steps.length} ${t.steps.length === 1 ? "step" : "steps"}`];
1645
1683
  if (t.setup_keys.length) meta.push(`before this test: ${t.setup_keys.join(", ")}`);
1646
1684
  if (t.start_state) meta.push(`start: ${START_STATE_LABEL[t.start_state] ?? t.start_state}`);
1647
1685
  const head = t.importable ? t.name : chalk11.red(`${t.name} (not imported)`);
@@ -4040,9 +4078,9 @@ program.command("run-suite <suite-id>").description("Run all test cases in a sui
4040
4078
  "--app <app-id>",
4041
4079
  "Run the suite against this app instead of its own (e.g. another platform build with a different bundle id)"
4042
4080
  ).option("--build <id>", "Specific AppBuild id to run against (defaults to latest)").option("--metadata <json>", "JSON object passed through to webhooks (e.g. PR context)").option("--metadata-file <path>", "Read JSON object from a file (use for nested JSON in CI)").option(
4043
- "--no-retry",
4044
- "Do not retry a failed test once (CI retries a failed test once by default)"
4045
- ).action((suiteId, opts) => {
4081
+ "--retry",
4082
+ "Retry a failed test once, after a fresh start of the app (off by default); a pass on the retry marks the test flaky"
4083
+ ).option("--no-retry", "Deprecated: retries are off unless --retry is given (accepted, no effect)").action((suiteId, opts) => {
4046
4084
  if (opts.device && opts.deviceCategory) {
4047
4085
  process.stderr.write("error: --device cannot be combined with --device-category\n");
4048
4086
  process.exit(2);
@@ -4079,9 +4117,9 @@ program.command("run-suites").description("Run all suites matching a tag filter"
4079
4117
  "--device <device_id>",
4080
4118
  "Pin run to a specific device (use `vibeview list-devices` to find one). Conflicts with --device-category."
4081
4119
  ).option("--build <id>", "AppBuild id (validated against every matched suite)").option("--metadata <json>", "JSON object passed through to webhooks (e.g. PR context)").option("--metadata-file <path>", "Read JSON object from a file (use for nested JSON in CI)").option(
4082
- "--no-retry",
4083
- "Do not retry a failed test once (CI retries a failed test once by default)"
4084
- ).action((opts) => {
4120
+ "--retry",
4121
+ "Retry a failed test once, after a fresh start of the app (off by default); a pass on the retry marks the test flaky"
4122
+ ).option("--no-retry", "Deprecated: retries are off unless --retry is given (accepted, no effect)").action((opts) => {
4085
4123
  if (opts.device && opts.deviceCategory) {
4086
4124
  process.stderr.write("error: --device cannot be combined with --device-category\n");
4087
4125
  process.exit(2);
@@ -4115,9 +4153,9 @@ program.command("run-test <test-id>").description("Run a single test case").opti
4115
4153
  "--app <app-id>",
4116
4154
  "Run the test against this app instead of its own (e.g. another platform build with a different bundle id)"
4117
4155
  ).option("--build <id>", "Specific AppBuild id to run against (defaults to latest)").option("--metadata <json>", "JSON object passed through to webhooks (e.g. PR context)").option("--metadata-file <path>", "Read JSON object from a file (use for nested JSON in CI)").option(
4118
- "--no-retry",
4119
- "Do not retry a failed test once (CI retries a failed test once by default)"
4120
- ).action((testId, opts) => {
4156
+ "--retry",
4157
+ "Retry a failed test once, after a fresh start of the app (off by default); a pass on the retry marks the test flaky"
4158
+ ).option("--no-retry", "Deprecated: retries are off unless --retry is given (accepted, no effect)").action((testId, opts) => {
4121
4159
  if (opts.device && opts.deviceCategory) {
4122
4160
  process.stderr.write("error: --device cannot be combined with --device-category\n");
4123
4161
  process.exit(2);
@@ -4349,7 +4387,7 @@ program.command("dev-reload").description(
4349
4387
  program.command("mcp").description(
4350
4388
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
4351
4389
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
4352
- const { runMcpServer } = await import("./server-FMU3HQWF.js");
4390
+ const { runMcpServer } = await import("./server-GEY4WZUH.js");
4353
4391
  await runMcpServer({ ...opts, version: pkg.version });
4354
4392
  });
4355
4393
  program.command("agent-setup").description(
@@ -20,6 +20,7 @@ import {
20
20
  isDevPlatform,
21
21
  loadDevConfig,
22
22
  loadLocalDevConfig,
23
+ outcomeOf,
23
24
  parseBuildId,
24
25
  parseIdleWarningSeconds,
25
26
  platformWithArticle,
@@ -28,7 +29,7 @@ import {
28
29
  renderEnvelope,
29
30
  resolveMetro,
30
31
  startDevSession
31
- } from "./chunk-AV674D7T.js";
32
+ } from "./chunk-EQN3X62U.js";
32
33
  import {
33
34
  ApiClient,
34
35
  NotFoundError,
@@ -399,33 +400,40 @@ function notCheckedNote(status, n) {
399
400
  return ` \u2014 ${n} step${n === 1 ? "" : "s"} not checked (no recorded actions; not verified)`;
400
401
  }
401
402
  var RUN_IN_PROGRESS = /* @__PURE__ */ new Set(["queued", "pending", "running"]);
403
+ function agentStatus(r) {
404
+ const outcome = outcomeOf(r);
405
+ return outcome === "not_checked" || outcome === "not_run" ? outcome.replace("_", " ") : outcome;
406
+ }
407
+ function testsNotChecked(n) {
408
+ return typeof n === "number" && n > 0 ? `, ${n} not checked` : "";
409
+ }
402
410
  function formatRunForAgent(run) {
403
411
  const lines = [];
404
412
  if (run.lanes) {
405
413
  lines.push(
406
- `${run.name}: ${run.status} \u2014 ${run.summary.tests_passed} of ${run.summary.tests_total} tests passed`
414
+ `${run.name}: ${agentStatus(run)} \u2014 ${run.summary.tests_passed} of ${run.summary.tests_total} tests passed${testsNotChecked(run.summary.tests_not_checked)}`
407
415
  );
408
416
  for (const lane of run.lanes) {
409
417
  const detail = [lane.platform, lane.os_version].filter(Boolean).join(" ");
410
- let head = `${lane.model ? `${lane.model} (${detail})` : detail}: ${lane.status}`;
418
+ let head = `${lane.model ? `${lane.model} (${detail})` : detail}: ${agentStatus(lane)}`;
411
419
  if (lane.status === "queued" && typeof lane.queue?.position === "number") {
412
420
  head += ` (position ${lane.queue.position} in the queue)`;
413
421
  }
414
422
  if (lane.error_message) head += ` \u2014 ${lane.error_message}`;
415
423
  lines.push(head);
416
424
  for (const r of lane.runs ?? []) {
417
- const count = r.kind === "group" && typeof r.tests_total === "number" ? ` (${r.tests_passed ?? 0}/${r.tests_total} tests passed)` : "";
425
+ const count = r.kind === "group" && typeof r.tests_total === "number" ? ` (${r.tests_passed ?? 0}/${r.tests_total} tests passed${testsNotChecked(r.tests_not_checked)})` : "";
418
426
  lines.push(
419
- ` ${r.name}: ${r.status}${count}${r.error_message ? ` \u2014 ${r.error_message}` : ""}`
427
+ ` ${r.name}: ${agentStatus(r)}${count}${r.error_message ? ` \u2014 ${r.error_message}` : ""}`
420
428
  );
421
429
  }
422
430
  }
423
431
  } else if (Array.isArray(run.test_runs)) {
424
- lines.push(`${run.suite_name ?? "Group"}: ${run.status}`);
432
+ lines.push(`${run.suite_name ?? "Group"}: ${agentStatus(run)}`);
425
433
  for (const t of run.test_runs) {
426
434
  const q = t.quarantined && t.status === "failed" ? " (quarantined)" : "";
427
435
  lines.push(
428
- ` ${t.case_name}: ${t.status}${q}${t.error_message ? ` \u2014 ${t.error_message}` : ""}${notCheckedNote(t.status, t.steps_not_checked)}`
436
+ ` ${t.case_name}: ${agentStatus(t)}${q}${t.error_message ? ` \u2014 ${t.error_message}` : ""}${notCheckedNote(outcomeOf(t), t.steps_not_checked)}`
429
437
  );
430
438
  }
431
439
  } else {
@@ -434,7 +442,7 @@ function formatRunForAgent(run) {
434
442
  const notChecked = typeof run.steps_not_checked === "number" ? run.steps_not_checked : (run.steps ?? []).filter((s) => s.status === "not_checked").length;
435
443
  const q = run.quarantined && run.status === "failed" ? " (quarantined)" : "";
436
444
  lines.push(
437
- `${name}: ${run.status}${q}${run.error_message ? ` \u2014 ${run.error_message}` : ""}${notCheckedNote(run.status, notChecked)}`
445
+ `${name}: ${agentStatus(run)}${q}${run.error_message ? ` \u2014 ${run.error_message}` : ""}${notCheckedNote(outcomeOf(run), notChecked)}`
438
446
  );
439
447
  if (failed) {
440
448
  lines.push(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeview",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "VibeView CLI -- run your app on cloud iOS simulators, Android emulators, Apple TV, Android TV and Roku devices: a React Native dev loop, device control for you or your coding agent (CLI or MCP), and tests from CI",
5
5
  "mcpName": "io.github.vibeview/vibeview",
6
6
  "type": "module",