vibeview 0.39.0 → 0.41.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.
@@ -274,7 +274,7 @@ var ApiClient = class {
274
274
  }
275
275
  async getRunStatus(runId) {
276
276
  const data = await this.request("GET", `/runs/${runId}`);
277
- return { ...data, url: data.run_url ?? `${this.baseUrl}/tests/runs/${runId}` };
277
+ return { ...data, url: runResultUrl(data, `${this.baseUrl}/tests/runs/${runId}`) };
278
278
  }
279
279
  /** Request without /ci prefix — for non-CI endpoints like /apps, /tests */
280
280
  async requestRaw(method, path, body) {
@@ -772,6 +772,13 @@ var ApiClient = class {
772
772
  return this.requestRaw("POST", `/webhooks/${id}/test`);
773
773
  }
774
774
  };
775
+ function runResultUrl(data, fallback) {
776
+ const url = data.run_url ?? fallback;
777
+ if (!data.retry_run_id) return url;
778
+ const firstId = url.slice(url.lastIndexOf("/") + 1);
779
+ if (!firstId) return url;
780
+ return `${url.slice(0, url.length - firstId.length)}${encodeURIComponent(data.retry_run_id)}`;
781
+ }
775
782
  function parseContentDispositionFilename(header) {
776
783
  if (!header) return void 0;
777
784
  const extMatch = header.match(/filename\*\s*=\s*[^']*''([^;]+)/i);
@@ -870,6 +877,10 @@ function createSpinner(text, opts = {}) {
870
877
  var EXIT_PASS = 0;
871
878
  var EXIT_TEST_FAIL = 1;
872
879
  var EXIT_INFRA_ERROR = 2;
880
+ var RUN_COMMANDS = ["run-test", "run-suite", "run-suites"];
881
+ function runCommandParseExitCode(err) {
882
+ return err.exitCode === 0 ? EXIT_PASS : EXIT_INFRA_ERROR;
883
+ }
873
884
  var UsageError = class extends Error {
874
885
  constructor(message) {
875
886
  super(message);
@@ -1015,6 +1026,8 @@ export {
1015
1026
  EXIT_PASS,
1016
1027
  EXIT_TEST_FAIL,
1017
1028
  EXIT_INFRA_ERROR,
1029
+ RUN_COMMANDS,
1030
+ runCommandParseExitCode,
1018
1031
  UsageError,
1019
1032
  DEVICE_STATUS_FILTERS,
1020
1033
  queueReasonCopy,
@@ -13,7 +13,7 @@ import {
13
13
  resolveArtifact,
14
14
  resolveAuth,
15
15
  uploadAppBuild
16
- } from "./chunk-MYXUGCR5.js";
16
+ } from "./chunk-GDTLLJFL.js";
17
17
 
18
18
  // src/lib/formatters/not-checked.ts
19
19
  function notCheckedSteps(steps) {
package/dist/cli.js CHANGED
@@ -30,7 +30,7 @@ import {
30
30
  runAgentVerb,
31
31
  runStepsNotChecked,
32
32
  stepsNotCheckedLabel
33
- } from "./chunk-EQN3X62U.js";
33
+ } from "./chunk-MK3WDPVZ.js";
34
34
  import {
35
35
  ApiClient,
36
36
  ApiStatusError,
@@ -40,6 +40,7 @@ import {
40
40
  EXIT_TEST_FAIL,
41
41
  NetworkError,
42
42
  NotFoundError,
43
+ RUN_COMMANDS,
43
44
  UsageError,
44
45
  createSpinner,
45
46
  errorExitCode,
@@ -49,9 +50,10 @@ import {
49
50
  queueReasonCopy,
50
51
  reportCommandError,
51
52
  resolveAuth,
53
+ runCommandParseExitCode,
52
54
  saveConfig,
53
55
  uploadApp
54
- } from "./chunk-MYXUGCR5.js";
56
+ } from "./chunk-GDTLLJFL.js";
55
57
 
56
58
  // src/cli.ts
57
59
  import { createRequire } from "module";
@@ -125,9 +127,17 @@ function formatHuman(result, verbose) {
125
127
  const notCheckedCases = cases.filter((t) => outcomeOf(t) === "not_checked").length;
126
128
  const total = cases.length;
127
129
  const notCheckedNote = notCheckedCases > 0 ? `, ${notCheckedCases} not checked` : notChecked > 0 ? `, ${stepsNotCheckedLabel(notChecked)}` : "";
128
- lines.push(` ${chalk.dim(`${passed}/${total} cases passed${notCheckedNote}`)}`);
130
+ const quarantined = cases.filter((t) => t.status === "failed" && t.quarantined === true).length;
131
+ const quarantinedNote = quarantined > 0 && (result.test_runs ?? []).length > 0 ? `, ${quarantined} failed (quarantined)` : "";
132
+ lines.push(
133
+ ` ${chalk.dim(`${passed}/${total} cases passed${notCheckedNote}${quarantinedNote}`)}`
134
+ );
129
135
  if (result.url) {
130
- lines.push(` ${chalk.dim(result.url)}`);
136
+ lines.push(` ${chalk.dim(result.first_attempt ? `Retry: ${result.url}` : result.url)}`);
137
+ }
138
+ if (result.first_attempt) {
139
+ const first = result.first_attempt;
140
+ lines.push(` ${chalk.dim(`First attempt (${first.status ?? "unknown"}): ${first.url}`)}`);
131
141
  }
132
142
  const suiteCases = result.test_runs ?? [];
133
143
  if (suiteCases.length > 0) {
@@ -647,11 +657,13 @@ function formatToolCall(tc) {
647
657
  function runStartedLine(evt) {
648
658
  const steps = `${evt.total_steps} step(s)`;
649
659
  const model = chalk2.bold(evt.model || "default");
650
- if (evt.mode === "replay") return `${chalk2.cyan("\u25B6")} Replay started \u2014 ${steps} (no AI)`;
660
+ if (evt.mode === "replay") {
661
+ return `${chalk2.cyan("\u25B6")} No AI run started \u2014 ${steps}, replays only (no AI)`;
662
+ }
651
663
  if (evt.mode === "hybrid") {
652
- return `${chalk2.cyan("\u25B6")} Hybrid test started \u2014 ${steps}, replaying the recording, AI fallback model: ${model}`;
664
+ return `${chalk2.cyan("\u25B6")} Smart run started \u2014 ${steps}, replays recorded and learned paths, AI where needed (model: ${model})`;
653
665
  }
654
- return `${chalk2.cyan("\u25B6")} AI test started \u2014 ${steps}, model: ${model}`;
666
+ return `${chalk2.cyan("\u25B6")} AI only run started \u2014 ${steps}, model: ${model}`;
655
667
  }
656
668
  function displayProgressEvent(evt, ctx) {
657
669
  const { spinner } = ctx;
@@ -765,6 +777,37 @@ function parseRunPlatform(value) {
765
777
  }
766
778
  return value;
767
779
  }
780
+ var RUN_MODES = ["hybrid", "ai", "replay"];
781
+ function parseRunMode(value) {
782
+ if (value === void 0) return void 0;
783
+ if (!RUN_MODES.includes(value)) {
784
+ throw new Error(
785
+ `--mode must be one of ${RUN_MODES.join(", ")} (Smart, AI only, No AI), got "${value}"`
786
+ );
787
+ }
788
+ return value;
789
+ }
790
+ var MAX_TIMEOUT_S = 7200;
791
+ var POSITIVE_INT = /^[1-9][0-9]*$/;
792
+ function parseTimeoutSeconds(value) {
793
+ const n = POSITIVE_INT.test(value.trim()) ? Number(value.trim()) : NaN;
794
+ if (!Number.isSafeInteger(n) || n < 1 || n > MAX_TIMEOUT_S) {
795
+ throw new Error(
796
+ `--timeout must be a whole number of seconds from 1 to ${MAX_TIMEOUT_S}, got "${value}"`
797
+ );
798
+ }
799
+ return n;
800
+ }
801
+ function parseBuildId(value) {
802
+ if (value === void 0) return void 0;
803
+ const n = POSITIVE_INT.test(value.trim()) ? Number(value.trim()) : NaN;
804
+ if (!Number.isSafeInteger(n)) {
805
+ throw new Error(
806
+ `--build must be a build id (a positive whole number, see \`vibeview list-builds\`), got "${value}"`
807
+ );
808
+ }
809
+ return n;
810
+ }
768
811
  function refusePlatformWithApp(options) {
769
812
  if (options.platform && options.app) {
770
813
  throw new Error(
@@ -1181,15 +1224,16 @@ Aggregate: ${aggregate}${aggregateNote}
1181
1224
  for (const cr of sr.case_runs ?? []) {
1182
1225
  const outcome = outcomeOf(cr);
1183
1226
  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");
1227
+ const caseMark = neutral ? chalk5.gray("\u25CB") : cr.status === "passed" ? chalk5.green("\u2713") : cr.status === "cancelled" || cr.status === "pending" || cr.status === "running" ? chalk5.yellow("-") : cr.status === "failed" && cr.quarantined === true ? chalk5.yellow("\u2717") : chalk5.red("\u2717");
1185
1228
  const dur = cr.duration_ms ? chalk5.dim(` (${(cr.duration_ms / 1e3).toFixed(1)}s)`) : "";
1186
1229
  const unchecked = notCheckedSuffix(outcome, cr.steps_not_checked);
1187
- const label = neutral ? outcome.replace("_", " ") : cr.status ?? "?";
1230
+ const quarantined = cr.status === "failed" && cr.quarantined === true;
1231
+ const label = neutral ? outcome.replace("_", " ") : quarantined ? "failed (quarantined)" : cr.status ?? "?";
1188
1232
  process.stdout.write(
1189
1233
  ` ${caseMark} ${cr.case_name ?? "Case"} ${label}${unchecked ? chalk5.yellow(unchecked) : ""}${dur} ${chalk5.dim(cr.url)}
1190
1234
  `
1191
1235
  );
1192
- if (cr.error_message && neutral) {
1236
+ if (cr.error_message && (neutral || cr.status === "failed" && cr.quarantined === true)) {
1193
1237
  process.stdout.write(` ${chalk5.dim(cr.error_message)}
1194
1238
  `);
1195
1239
  } else if (cr.error_message && cr.status !== "passed") {
@@ -1205,7 +1249,15 @@ Aggregate: ${aggregate}${aggregateNote}
1205
1249
  }
1206
1250
  function suiteCounts(sr) {
1207
1251
  const nc = sr.not_checked_tests ?? 0;
1208
- return `${sr.passed} passed, ${sr.failed} failed${nc > 0 ? `, ${nc} not checked` : ""}`;
1252
+ const q = Math.min(
1253
+ sr.failed,
1254
+ sr.quarantined_failures ?? (sr.case_runs ?? []).filter((c) => c.status === "failed" && c.quarantined === true).length
1255
+ );
1256
+ const parts = [`${sr.passed} passed`];
1257
+ if (q === 0 || sr.failed > q) parts.push(`${sr.failed - q} failed`);
1258
+ if (q > 0) parts.push(`${q} failed (quarantined)`);
1259
+ if (nc > 0) parts.push(`${nc} not checked`);
1260
+ return parts.join(", ");
1209
1261
  }
1210
1262
 
1211
1263
  // src/commands/run-test.ts
@@ -4060,11 +4112,27 @@ function resolveMetadata(opts) {
4060
4112
  process.exit(2);
4061
4113
  }
4062
4114
  }
4115
+ function parseRunFlags(opts) {
4116
+ try {
4117
+ return {
4118
+ mode: parseRunMode(opts.mode),
4119
+ timeout: parseTimeoutSeconds(opts.timeout),
4120
+ build: parseBuildId(opts.build)
4121
+ };
4122
+ } catch (err) {
4123
+ process.stderr.write(`error: ${err.message}
4124
+ `);
4125
+ process.exit(2);
4126
+ }
4127
+ }
4063
4128
  var program = new Command();
4064
4129
  program.name("vibeview").description(
4065
4130
  "VibeView CLI -- run your app on cloud iOS simulators, Android emulators, Apple TV, Android TV and Roku devices: a React Native dev loop (dev), device control for you or your coding agent (ui-tree, tap, mcp), and tests from CI (run-test, run-suites)"
4066
4131
  ).version(pkg.version);
4067
- program.command("run-suite <suite-id>").description("Run all test cases in a suite").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--output <format>", "Output format: human, json, junit", "human").option("--verbose", "Show per-case breakdown", false).option("--timeout <seconds>", "Max wait time in seconds (queue wait plus execution)", "1800").option("--commit-sha <sha>", "Git commit SHA for GitHub status checks").option("--model <model>", "AI model to use").option("--provider <provider>", "AI provider").option("--mode <mode>", "Execution mode: hybrid, ai, replay").option(
4132
+ program.command("run-suite <suite-id>").description("Run all test cases in a suite").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--output <format>", "Output format: human, json, junit", "human").option("--verbose", "Show per-case breakdown", false).option("--timeout <seconds>", "Max wait time in seconds (queue wait plus execution)", "1800").option("--commit-sha <sha>", "Git commit SHA for GitHub status checks").option("--model <model>", "AI model to use").option("--provider <provider>", "AI provider").option(
4133
+ "--mode <mode>",
4134
+ "Execution mode: hybrid (Smart, the default), ai (AI only), replay (No AI)"
4135
+ ).option(
4068
4136
  "--device-category <category>",
4069
4137
  "Device category: phone, tablet, tv (default: tv for a TV platform, else phone)",
4070
4138
  parseDeviceCategory
@@ -4093,12 +4161,12 @@ program.command("run-suite <suite-id>").description("Run all test cases in a sui
4093
4161
  `);
4094
4162
  process.exit(2);
4095
4163
  }
4164
+ const runFlags = parseRunFlags(opts);
4096
4165
  const metadata = resolveMetadata(opts);
4097
4166
  return runSuite(suiteId, {
4098
4167
  ...opts,
4099
4168
  metadata,
4100
- timeout: parseInt(opts.timeout, 10),
4101
- build: opts.build ? parseInt(opts.build, 10) : void 0
4169
+ ...runFlags
4102
4170
  });
4103
4171
  });
4104
4172
  program.command("run-suites").description("Run all suites matching a tag filter").requiredOption(
@@ -4109,7 +4177,10 @@ program.command("run-suites").description("Run all suites matching a tag filter"
4109
4177
  ).option("--match <mode>", "any (default) or all", "any").option(
4110
4178
  "--platform <platform>",
4111
4179
  "Filter matched suites to one platform: ios, android, tvos, androidtv, roku (default: no filter)"
4112
- ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--output <format>", "Output format: human, json, junit", "human").option("--verbose", "Show per-suite progress", false).option("--timeout <seconds>", "Per-suite timeout, capped at 2 hours total", "1800").option("--commit-sha <sha>", "Git commit SHA propagated to every SuiteRun").option("--model <model>", "AI model").option("--provider <provider>", "AI provider").option("--mode <mode>", "Execution mode: hybrid, ai, replay").option(
4180
+ ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--output <format>", "Output format: human, json, junit", "human").option("--verbose", "Show per-suite progress", false).option("--timeout <seconds>", "Per-suite timeout, capped at 2 hours total", "1800").option("--commit-sha <sha>", "Git commit SHA propagated to every SuiteRun").option("--model <model>", "AI model").option("--provider <provider>", "AI provider").option(
4181
+ "--mode <mode>",
4182
+ "Execution mode: hybrid (Smart, the default), ai (AI only), replay (No AI)"
4183
+ ).option(
4113
4184
  "--device-category <category>",
4114
4185
  "Device category: phone, tablet, tv (default: tv for a TV platform, else phone)",
4115
4186
  parseDeviceCategory
@@ -4131,15 +4202,18 @@ program.command("run-suites").description("Run all suites matching a tag filter"
4131
4202
  `);
4132
4203
  process.exit(2);
4133
4204
  }
4205
+ const runFlags = parseRunFlags(opts);
4134
4206
  const metadata = resolveMetadata(opts);
4135
4207
  return runSuites({
4136
4208
  ...opts,
4137
4209
  metadata,
4138
- timeout: parseInt(opts.timeout, 10),
4139
- build: opts.build ? parseInt(opts.build, 10) : void 0
4210
+ ...runFlags
4140
4211
  });
4141
4212
  });
4142
- program.command("run-test <test-id>").description("Run a single test case").option("--token <token>", "API token").option("--output <format>", "Output format: human, json, junit", "human").option("--verbose", "Show step details", false).option("--timeout <seconds>", "Max wait time in seconds (queue wait plus execution)", "1800").option("--commit-sha <sha>", "Git commit SHA for GitHub status checks").option("--model <model>", "AI model to use").option("--provider <provider>", "AI provider").option("--mode <mode>", "Execution mode: hybrid, ai, replay").option(
4213
+ program.command("run-test <test-id>").description("Run a single test case").option("--token <token>", "API token").option("--output <format>", "Output format: human, json, junit", "human").option("--verbose", "Show step details", false).option("--timeout <seconds>", "Max wait time in seconds (queue wait plus execution)", "1800").option("--commit-sha <sha>", "Git commit SHA for GitHub status checks").option("--model <model>", "AI model to use").option("--provider <provider>", "AI provider").option(
4214
+ "--mode <mode>",
4215
+ "Execution mode: hybrid (Smart, the default), ai (AI only), replay (No AI)"
4216
+ ).option(
4143
4217
  "--device-category <category>",
4144
4218
  "Device category: phone, tablet, tv (default: tv for a TV platform, else phone)",
4145
4219
  parseDeviceCategory
@@ -4168,12 +4242,12 @@ program.command("run-test <test-id>").description("Run a single test case").opti
4168
4242
  `);
4169
4243
  process.exit(2);
4170
4244
  }
4245
+ const runFlags = parseRunFlags(opts);
4171
4246
  const metadata = resolveMetadata(opts);
4172
4247
  return runTest(testId, {
4173
4248
  ...opts,
4174
4249
  metadata,
4175
- timeout: parseInt(opts.timeout, 10),
4176
- build: opts.build ? parseInt(opts.build, 10) : void 0
4250
+ ...runFlags
4177
4251
  });
4178
4252
  });
4179
4253
  program.command("list-suites").description("List available test suites").option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").option(
@@ -4387,7 +4461,7 @@ program.command("dev-reload").description(
4387
4461
  program.command("mcp").description(
4388
4462
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
4389
4463
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
4390
- const { runMcpServer } = await import("./server-GEY4WZUH.js");
4464
+ const { runMcpServer } = await import("./server-435X37GD.js");
4391
4465
  await runMcpServer({ ...opts, version: pkg.version });
4392
4466
  });
4393
4467
  program.command("agent-setup").description(
@@ -4411,6 +4485,9 @@ ${pressButtonsHelp()}`);
4411
4485
  return runAgentVerb(cmd, positionals, opts);
4412
4486
  });
4413
4487
  }
4488
+ for (const name of RUN_COMMANDS) {
4489
+ program.commands.find((c) => c.name() === name)?.exitOverride((err) => process.exit(runCommandParseExitCode(err)));
4490
+ }
4414
4491
  if (updateCheckAllowed(process.argv, process.env, process.stderr.isTTY)) {
4415
4492
  void checkForUpdate(pkg.version, { print: (line) => process.stderr.write(`${line}
4416
4493
  `) });
@@ -23,18 +23,19 @@ import {
23
23
  outcomeOf,
24
24
  parseBuildId,
25
25
  parseIdleWarningSeconds,
26
+ platformLabel,
26
27
  platformWithArticle,
27
28
  readDevState,
28
29
  recordDevUse,
29
30
  renderEnvelope,
30
31
  resolveMetro,
31
32
  startDevSession
32
- } from "./chunk-EQN3X62U.js";
33
+ } from "./chunk-MK3WDPVZ.js";
33
34
  import {
34
35
  ApiClient,
35
36
  NotFoundError,
36
37
  resolveAuth
37
- } from "./chunk-MYXUGCR5.js";
38
+ } from "./chunk-GDTLLJFL.js";
38
39
 
39
40
  // src/mcp/server.ts
40
41
  import { writeFileSync } from "fs";
@@ -47,7 +48,7 @@ var SESSION_ID_PROPERTY = {
47
48
  };
48
49
  var AI_USE_PROPERTY = {
49
50
  type: "string",
50
- description: "smart (default): replays saved paths and uses AI only where the screen changed; no_ai: replays only, no usage credit; ai_only: the AI does every step.",
51
+ description: "smart (default): replays recorded and learned paths, and uses AI only where the screen changed or a step has no path yet; no_ai: replays only, no AI and no usage credit (a step with no path is not checked); ai_only: the AI does every step.",
51
52
  enum: ["smart", "no_ai", "ai_only"]
52
53
  };
53
54
  var AI_USE_MODE = { smart: "hybrid", no_ai: "replay", ai_only: "ai" };
@@ -59,7 +60,12 @@ var CREATE_TEST_DEF = {
59
60
  app: { type: "string", description: "App id (from list_apps)." },
60
61
  name: { type: "string", description: "Test name." },
61
62
  steps: { type: "string", description: "The steps, one per line." },
62
- group: { type: "string", description: "Group id to add the test to (optional)." }
63
+ group: { type: "string", description: "Group id to add the test to (optional)." },
64
+ if_exists: {
65
+ type: "string",
66
+ description: "When a test with this name already exists for the app: update replaces it, new creates another one beside it, fail refuses. Omitted: an identical test is reused, a different one is refused.",
67
+ enum: ["fail", "update", "new"]
68
+ }
63
69
  },
64
70
  required: ["app", "name", "steps"]
65
71
  };
@@ -72,7 +78,7 @@ var RUN_TEST_DEF = {
72
78
  session_id: { type: "string", description: "Run on this session instead of a new device." },
73
79
  device_model: {
74
80
  type: "string",
75
- description: "Device model for a new device (from list_device_models); omitted: any free one."
81
+ description: "Device model for a new device (from list_device_models); omitted: any free one. A model of another platform (same form factor) runs the test's app for that platform: the app linked in its app family, else the app with the same bundle id / package. The reply names the platform and app the run went to."
76
82
  },
77
83
  ai_use: AI_USE_PROPERTY,
78
84
  start_from_current_screen: {
@@ -94,7 +100,7 @@ var RUN_GROUP_DEF = {
94
100
  session_id: { type: "string", description: "Run on this session instead of a new device." },
95
101
  device_model: {
96
102
  type: "string",
97
- description: "Device model for a new device; omitted: any free one."
103
+ description: "Device model for a new device; omitted: any free one. A model of another platform (same form factor) runs the group's app for that platform: the app linked in its app family, else the app with the same bundle id / package. The reply names the platform and app the run went to."
98
104
  },
99
105
  ai_use: AI_USE_PROPERTY,
100
106
  start_from_current_screen: {
@@ -347,6 +353,14 @@ async function explainInactive(sessionId, deps) {
347
353
  function findCommand(toolName) {
348
354
  return AGENT_COMMANDS.find((c) => toolNameFor(c.verb) === toolName);
349
355
  }
356
+ function testToolErrorMessage(err) {
357
+ const message = err instanceof Error ? err.message : String(err);
358
+ if (err instanceof NotFoundError && message.startsWith("Not found: ")) {
359
+ const detail = message.slice("Not found: ".length);
360
+ if (/not found/i.test(detail)) return detail;
361
+ }
362
+ return message;
363
+ }
350
364
  async function handleCreateTest(args, deps) {
351
365
  const steps = String(args.steps ?? "").split("\n").map((s) => s.trim()).filter(Boolean).map((text) => ({ text }));
352
366
  if (!steps.length) return errorResult("create_test: `steps` needs at least one line");
@@ -355,7 +369,8 @@ async function handleCreateTest(args, deps) {
355
369
  name: String(args.name),
356
370
  app_id: String(args.app),
357
371
  steps,
358
- group_ids: typeof args.group === "string" && args.group ? [args.group] : void 0
372
+ group_ids: typeof args.group === "string" && args.group ? [args.group] : void 0,
373
+ if_exists: args.if_exists === "fail" || args.if_exists === "update" || args.if_exists === "new" ? args.if_exists : void 0
359
374
  });
360
375
  const verb = created.outcome === "unchanged" ? "Test already exists, unchanged" : created.outcome === "updated" ? "Updated existing test" : "Created test";
361
376
  return textResult(
@@ -363,9 +378,17 @@ async function handleCreateTest(args, deps) {
363
378
  created
364
379
  );
365
380
  } catch (err) {
366
- return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
381
+ return errorResult(`error: ${testToolErrorMessage(err)}`);
367
382
  }
368
383
  }
384
+ function runDestination(lanes) {
385
+ return lanes.map((l) => {
386
+ const label = platformLabel(l.platform);
387
+ const device = l.model ? ` (${l.model})` : "";
388
+ const app = l.app_name ? `: ${l.app_name}, the ${label} app` : "";
389
+ return ` Running on ${label}${device}${app}.`;
390
+ }).join("");
391
+ }
369
392
  async function handleRun(kind, args, deps) {
370
393
  const id = String(kind === "test" ? args.test : args.group);
371
394
  const mode = typeof args.ai_use === "string" ? AI_USE_MODE[args.ai_use] : void 0;
@@ -387,12 +410,13 @@ async function handleRun(kind, args, deps) {
387
410
  kind === "test" ? { target: "test", test_ids: [id], devices, mode } : { target: "group", group_id: id, devices, mode }
388
411
  );
389
412
  const queued = r.lanes.some((l) => l.status === "queued") ? " It is waiting for a free device." : "";
413
+ const where = runDestination(r.lanes);
390
414
  return textResult(
391
- `Started run ${r.batch_id}.${queued} Check it with get_run run_id=${r.batch_id}.`,
415
+ `Started run ${r.batch_id}.${where}${queued} Check it with get_run run_id=${r.batch_id}.`,
392
416
  r
393
417
  );
394
418
  } catch (err) {
395
- return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
419
+ return errorResult(`error: ${testToolErrorMessage(err)}`);
396
420
  }
397
421
  }
398
422
  function notCheckedNote(status, n) {
@@ -407,11 +431,25 @@ function agentStatus(r) {
407
431
  function testsNotChecked(n) {
408
432
  return typeof n === "number" && n > 0 ? `, ${n} not checked` : "";
409
433
  }
434
+ function quarantinedFailure(t) {
435
+ return t?.status === "failed" && t?.quarantined === true;
436
+ }
437
+ function quarantinedCount(n) {
438
+ return typeof n === "number" && n > 0 ? `, ${n} failed (quarantined)` : "";
439
+ }
410
440
  function formatRunForAgent(run) {
411
441
  const lines = [];
412
442
  if (run.lanes) {
443
+ const rowQuarantined = (r) => typeof r.tests_quarantined === "number" ? r.tests_quarantined : (r.tests ?? []).filter(quarantinedFailure).length;
444
+ let totalQuarantined = run.summary.tests_quarantined;
445
+ if (typeof totalQuarantined !== "number") {
446
+ totalQuarantined = 0;
447
+ for (const lane of run.lanes) {
448
+ for (const r of lane.runs ?? []) totalQuarantined += rowQuarantined(r);
449
+ }
450
+ }
413
451
  lines.push(
414
- `${run.name}: ${agentStatus(run)} \u2014 ${run.summary.tests_passed} of ${run.summary.tests_total} tests passed${testsNotChecked(run.summary.tests_not_checked)}`
452
+ `${run.name}: ${agentStatus(run)} \u2014 ${run.summary.tests_passed} of ${run.summary.tests_total} tests passed${testsNotChecked(run.summary.tests_not_checked)}${quarantinedCount(totalQuarantined)}`
415
453
  );
416
454
  for (const lane of run.lanes) {
417
455
  const detail = [lane.platform, lane.os_version].filter(Boolean).join(" ");
@@ -422,10 +460,32 @@ function formatRunForAgent(run) {
422
460
  if (lane.error_message) head += ` \u2014 ${lane.error_message}`;
423
461
  lines.push(head);
424
462
  for (const r of lane.runs ?? []) {
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)})` : "";
463
+ const count = r.kind === "group" && typeof r.tests_total === "number" ? ` (${r.tests_passed ?? 0}/${r.tests_total} tests passed${testsNotChecked(r.tests_not_checked)}${quarantinedCount(rowQuarantined(r))})` : "";
464
+ const cell = r.kind === "test" ? r.tests?.[0] ?? {} : {};
465
+ const q = r.kind === "test" && quarantinedFailure(cell) ? " (quarantined)" : "";
466
+ const why = r.error_message || cell.not_run_reason;
467
+ const unchecked = r.kind === "test" ? notCheckedNote(outcomeOf(cell), cell.steps_not_checked) : "";
426
468
  lines.push(
427
- ` ${r.name}: ${agentStatus(r)}${count}${r.error_message ? ` \u2014 ${r.error_message}` : ""}`
469
+ ` ${r.name}: ${agentStatus(r)}${q}${count}${why ? ` \u2014 ${why}` : ""}${unchecked}`
428
470
  );
471
+ if (r.kind !== "group") continue;
472
+ for (const t of r.tests ?? []) {
473
+ if (t.status === "not_run") {
474
+ const reason = t.not_run_reason || t.error_message;
475
+ lines.push(` ${t.name}: not run${reason ? ` \u2014 ${reason}` : ""}`);
476
+ continue;
477
+ }
478
+ const note = notCheckedNote(outcomeOf(t), t.steps_not_checked);
479
+ if (note) {
480
+ lines.push(` ${t.name}: ${agentStatus(t)}${note}`);
481
+ continue;
482
+ }
483
+ if (t.status !== "failed" && t.status !== "error") continue;
484
+ const tq = quarantinedFailure(t) ? " (quarantined)" : "";
485
+ lines.push(
486
+ ` ${t.name}: ${t.status}${tq}${t.error_message ? ` \u2014 ${t.error_message}` : ""}`
487
+ );
488
+ }
429
489
  }
430
490
  }
431
491
  } else if (Array.isArray(run.test_runs)) {
@@ -463,7 +523,7 @@ async function handleGetRun(args, deps) {
463
523
  const run = kind.kind === "batch" ? await client.getRunBatch(id) : await client.getRunStatus(id);
464
524
  return textResult(formatRunForAgent(run), run);
465
525
  } catch (err) {
466
- return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
526
+ return errorResult(`error: ${testToolErrorMessage(err)}`);
467
527
  }
468
528
  }
469
529
  async function dispatchToolCall(toolName, args, deps, extra = {}) {
@@ -882,7 +942,7 @@ async function handleUploadApp(args, deps) {
882
942
  }
883
943
  const name = typeof args.name === "string" && args.name ? args.name : void 0;
884
944
  const upload = deps.uploadApp ?? (async (f, n) => {
885
- const { uploadAppBuild } = await import("./upload-app-UHJLFQKZ.js");
945
+ const { uploadAppBuild } = await import("./upload-app-ZEQMGZIT.js");
886
946
  const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
887
947
  return result2;
888
948
  });
@@ -1278,6 +1338,23 @@ var MCP_INSTRUCTIONS = [
1278
1338
  "A session stopped elsewhere is reported as ended; call dev_start again for a new one.",
1279
1339
  'A session nobody uses ends after the idle timeout. A result that ends with "Session \u2026 ends in N s for inactivity" means act (tap, type, swipe, press) to keep it, or dev_stop if you are done; ui_tree, screenshot, logs, find and wait do not count as use.'
1280
1340
  ].join("\n");
1341
+ function toolInputSchema(def, z) {
1342
+ const shape = {};
1343
+ for (const [key, prop] of Object.entries(def.properties)) {
1344
+ const base = prop.enum ? z.enum(prop.enum) : z[prop.type]();
1345
+ const described = base.describe(prop.description);
1346
+ shape[key] = def.required.includes(key) ? described : described.optional();
1347
+ }
1348
+ const valid = Object.keys(def.properties);
1349
+ return z.strictObject(shape, {
1350
+ error: (issue) => {
1351
+ if (issue.code !== "unrecognized_keys") return void 0;
1352
+ const unknown = issue.keys.map((k) => `"${k}"`).join(", ");
1353
+ const takes = valid.length ? `${def.name} takes: ${valid.join(", ")}.` : `${def.name} takes no arguments.`;
1354
+ return `Unknown argument${issue.keys.length === 1 ? "" : "s"} ${unknown}. ${takes}`;
1355
+ }
1356
+ });
1357
+ }
1281
1358
  async function createMcpServer(opts) {
1282
1359
  const [{ McpServer }, { z }] = await Promise.all([
1283
1360
  import("@modelcontextprotocol/sdk/server/mcp.js"),
@@ -1296,21 +1373,12 @@ async function createMcpServer(opts) {
1296
1373
  { name: "vibeview", version: opts.version ?? "0.0.0" },
1297
1374
  { instructions: MCP_INSTRUCTIONS }
1298
1375
  );
1299
- function zodShape(def) {
1300
- const shape = {};
1301
- for (const [key, prop] of Object.entries(def.properties)) {
1302
- const base = prop.enum ? z.enum(prop.enum) : z[prop.type]();
1303
- const described = base.describe(prop.description);
1304
- shape[key] = def.required.includes(key) ? described : described.optional();
1305
- }
1306
- return shape;
1307
- }
1308
1376
  for (const def of buildToolDefinitions()) {
1309
1377
  server.registerTool(
1310
1378
  def.name,
1311
1379
  {
1312
1380
  description: def.description,
1313
- inputSchema: zodShape(def),
1381
+ inputSchema: toolInputSchema(def, z),
1314
1382
  annotations: { readOnlyHint: !def.mutating }
1315
1383
  },
1316
1384
  async (args, extra) => {
@@ -1397,7 +1465,10 @@ export {
1397
1465
  idleNoticeText,
1398
1466
  mcpText,
1399
1467
  progressReporter,
1468
+ runDestination,
1400
1469
  runMcpServer,
1401
1470
  startProgress,
1402
- startedDevice
1471
+ startedDevice,
1472
+ testToolErrorMessage,
1473
+ toolInputSchema
1403
1474
  };
@@ -3,7 +3,7 @@ import {
3
3
  describeBuild,
4
4
  uploadApp,
5
5
  uploadAppBuild
6
- } from "./chunk-MYXUGCR5.js";
6
+ } from "./chunk-GDTLLJFL.js";
7
7
  export {
8
8
  describeBuild,
9
9
  uploadApp,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeview",
3
- "version": "0.39.0",
3
+ "version": "0.41.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",