vibeview 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-MYXUGCR5.js → chunk-GDTLLJFL.js} +14 -1
- package/dist/{chunk-AV674D7T.js → chunk-MK3WDPVZ.js} +31 -1
- package/dist/cli.js +130 -70
- package/dist/{server-FMU3HQWF.js → server-56MR4KJW.js} +95 -29
- package/dist/{upload-app-UHJLFQKZ.js → upload-app-ZEQMGZIT.js} +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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,32 @@ import {
|
|
|
13
13
|
resolveArtifact,
|
|
14
14
|
resolveAuth,
|
|
15
15
|
uploadAppBuild
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-GDTLLJFL.js";
|
|
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
|
+
}
|
|
17
42
|
|
|
18
43
|
// src/lib/dev-config.ts
|
|
19
44
|
import { readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
@@ -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
|
-
|
|
30
|
+
runAgentVerb,
|
|
31
|
+
runStepsNotChecked,
|
|
32
|
+
stepsNotCheckedLabel
|
|
33
|
+
} from "./chunk-MK3WDPVZ.js";
|
|
29
34
|
import {
|
|
30
35
|
ApiClient,
|
|
31
36
|
ApiStatusError,
|
|
@@ -35,6 +40,7 @@ import {
|
|
|
35
40
|
EXIT_TEST_FAIL,
|
|
36
41
|
NetworkError,
|
|
37
42
|
NotFoundError,
|
|
43
|
+
RUN_COMMANDS,
|
|
38
44
|
UsageError,
|
|
39
45
|
createSpinner,
|
|
40
46
|
errorExitCode,
|
|
@@ -44,9 +50,10 @@ import {
|
|
|
44
50
|
queueReasonCopy,
|
|
45
51
|
reportCommandError,
|
|
46
52
|
resolveAuth,
|
|
53
|
+
runCommandParseExitCode,
|
|
47
54
|
saveConfig,
|
|
48
55
|
uploadApp
|
|
49
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-GDTLLJFL.js";
|
|
50
57
|
|
|
51
58
|
// src/cli.ts
|
|
52
59
|
import { createRequire } from "module";
|
|
@@ -61,21 +68,6 @@ import chalk2 from "chalk";
|
|
|
61
68
|
// src/lib/formatters/human.ts
|
|
62
69
|
import chalk from "chalk";
|
|
63
70
|
|
|
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
71
|
// src/lib/formatters/cases.ts
|
|
80
72
|
function resultCases(result) {
|
|
81
73
|
if (Array.isArray(result.test_runs)) return result.test_runs;
|
|
@@ -91,6 +83,8 @@ function resultCases(result) {
|
|
|
91
83
|
screenshot_signed_url: failedStep?.screenshot_signed_url ?? null,
|
|
92
84
|
url: result.url,
|
|
93
85
|
quarantined: result.quarantined,
|
|
86
|
+
not_checked: result.not_checked,
|
|
87
|
+
not_run: result.not_run,
|
|
94
88
|
steps_not_checked: runStepsNotChecked(result),
|
|
95
89
|
not_checked_steps: notCheckedSteps(result.steps).map((s) => ({
|
|
96
90
|
step: s.step,
|
|
@@ -118,37 +112,50 @@ function failureLines(failure) {
|
|
|
118
112
|
}
|
|
119
113
|
function formatHuman(result, verbose) {
|
|
120
114
|
const lines = [];
|
|
121
|
-
const
|
|
115
|
+
const outcome = outcomeOf(result);
|
|
116
|
+
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
117
|
const duration = result.duration_ms ? `${(result.duration_ms / 1e3).toFixed(1)}s` : "-";
|
|
123
118
|
lines.push("");
|
|
124
119
|
const title = result.suite_name || result.case_name || result.run_id;
|
|
125
120
|
const cases = resultCases(result);
|
|
126
121
|
const notChecked = cases.reduce((n, t) => n + (t.steps_not_checked ?? 0), 0);
|
|
127
|
-
const runSuffix = notCheckedSuffix(
|
|
122
|
+
const runSuffix = notCheckedSuffix(outcome, notChecked);
|
|
128
123
|
lines.push(
|
|
129
124
|
` ${icon}${runSuffix ? chalk.yellow(runSuffix) : ""} ${chalk.bold(title)} ${chalk.dim(duration)}`
|
|
130
125
|
);
|
|
131
|
-
const passed = cases.filter((t) => t
|
|
126
|
+
const passed = cases.filter((t) => outcomeOf(t) === "passed").length;
|
|
127
|
+
const notCheckedCases = cases.filter((t) => outcomeOf(t) === "not_checked").length;
|
|
132
128
|
const total = cases.length;
|
|
133
|
-
const notCheckedNote = notChecked > 0 ? `, ${stepsNotCheckedLabel(notChecked)}` : "";
|
|
134
|
-
|
|
129
|
+
const notCheckedNote = notCheckedCases > 0 ? `, ${notCheckedCases} not checked` : notChecked > 0 ? `, ${stepsNotCheckedLabel(notChecked)}` : "";
|
|
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
|
+
);
|
|
135
135
|
if (result.url) {
|
|
136
|
-
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}`)}`);
|
|
137
141
|
}
|
|
138
142
|
const suiteCases = result.test_runs ?? [];
|
|
139
143
|
if (suiteCases.length > 0) {
|
|
140
144
|
lines.push("");
|
|
141
145
|
for (const tr of suiteCases) {
|
|
142
|
-
const
|
|
146
|
+
const caseOutcome = outcomeOf(tr);
|
|
147
|
+
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
148
|
const caseDuration = tr.duration_ms ? `${(tr.duration_ms / 1e3).toFixed(1)}s` : "-";
|
|
144
|
-
const caseSuffix = notCheckedSuffix(
|
|
149
|
+
const caseSuffix = notCheckedSuffix(caseOutcome, tr.steps_not_checked);
|
|
145
150
|
lines.push(
|
|
146
151
|
` ${caseIcon}${caseSuffix ? chalk.yellow(caseSuffix) : ""} ${tr.case_name} ${chalk.dim(caseDuration)}`
|
|
147
152
|
);
|
|
148
153
|
if (tr.url) {
|
|
149
154
|
lines.push(` ${chalk.dim(tr.url)}`);
|
|
150
155
|
}
|
|
151
|
-
if (
|
|
156
|
+
if (caseOutcome === "not_checked" || caseOutcome === "not_run") {
|
|
157
|
+
if (tr.error_message) lines.push(` ${chalk.dim(tr.error_message)}`);
|
|
158
|
+
} else if (tr.status !== "passed") {
|
|
152
159
|
lines.push(...failureLines(tr));
|
|
153
160
|
}
|
|
154
161
|
}
|
|
@@ -174,7 +181,10 @@ function formatHuman(result, verbose) {
|
|
|
174
181
|
}
|
|
175
182
|
}
|
|
176
183
|
}
|
|
177
|
-
if (result.error_message) {
|
|
184
|
+
if (result.error_message && outcome === "not_checked") {
|
|
185
|
+
lines.push(`
|
|
186
|
+
${chalk.dim(result.error_message)}`);
|
|
187
|
+
} else if (result.error_message) {
|
|
178
188
|
lines.push(`
|
|
179
189
|
${chalk.red("Error:")} ${result.error_message}`);
|
|
180
190
|
}
|
|
@@ -216,13 +226,18 @@ function caseDetailLines(c) {
|
|
|
216
226
|
return lines;
|
|
217
227
|
}
|
|
218
228
|
var isQuarantinedFailure = (t) => t.status === "failed" && t.quarantined === true;
|
|
229
|
+
var NEUTRAL = /* @__PURE__ */ new Set(["not_checked", "not_run"]);
|
|
230
|
+
function neutralMessage(outcome, message) {
|
|
231
|
+
if (outcome === "not_checked") return `not checked: ${message || "no step was checked"}`;
|
|
232
|
+
return message || "Not run";
|
|
233
|
+
}
|
|
219
234
|
function formatJunit(result) {
|
|
220
235
|
const suiteName = escapeXml(result.suite_name || result.case_name || result.run_id);
|
|
221
236
|
const cases = resultCases(result);
|
|
222
237
|
const tests = cases.length;
|
|
223
238
|
const failures = cases.filter((t) => t.status === "failed" && !isQuarantinedFailure(t)).length;
|
|
224
239
|
const errors = cases.filter((t) => t.status === "error").length;
|
|
225
|
-
const skipped = cases.filter(isQuarantinedFailure).length;
|
|
240
|
+
const skipped = cases.filter((t) => isQuarantinedFailure(t) || NEUTRAL.has(outcomeOf(t))).length;
|
|
226
241
|
const skippedAttr = skipped > 0 ? ` skipped="${skipped}"` : "";
|
|
227
242
|
const totalTime = result.duration_ms ? (result.duration_ms / 1e3).toFixed(1) : "0";
|
|
228
243
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -249,8 +264,9 @@ function formatJunit(result) {
|
|
|
249
264
|
for (const tr of cases) {
|
|
250
265
|
const caseName = escapeXml(tr.case_name);
|
|
251
266
|
const caseTime = tr.duration_ms ? (tr.duration_ms / 1e3).toFixed(1) : "0";
|
|
252
|
-
const
|
|
253
|
-
|
|
267
|
+
const outcome = outcomeOf(tr);
|
|
268
|
+
const detail = caseDetailLines({ ...tr, status: outcome });
|
|
269
|
+
if (outcome === "passed" && detail.length === 0) {
|
|
254
270
|
lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}" />`);
|
|
255
271
|
continue;
|
|
256
272
|
}
|
|
@@ -258,6 +274,10 @@ function formatJunit(result) {
|
|
|
258
274
|
if (isQuarantinedFailure(tr)) {
|
|
259
275
|
const msg = escapeXml(tr.error_message || "Test failed");
|
|
260
276
|
lines.push(` <skipped message="quarantined: ${msg}" />`);
|
|
277
|
+
} else if (NEUTRAL.has(outcome)) {
|
|
278
|
+
lines.push(
|
|
279
|
+
` <skipped message="${escapeXml(neutralMessage(outcome, tr.error_message))}" />`
|
|
280
|
+
);
|
|
261
281
|
} else if (tr.status === "failed") {
|
|
262
282
|
const msg = escapeXml(tr.error_message || "Test failed");
|
|
263
283
|
lines.push(` <failure message="${msg}" type="TestFailure">${msg}</failure>`);
|
|
@@ -294,7 +314,7 @@ function formatJunitError(name, message, url) {
|
|
|
294
314
|
lines.push("</testsuites>");
|
|
295
315
|
return lines.join("\n");
|
|
296
316
|
}
|
|
297
|
-
var MULTI_TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
|
|
317
|
+
var MULTI_TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled", "not_checked"]);
|
|
298
318
|
function caseLines(c, classname) {
|
|
299
319
|
const name = escapeXml(c.name);
|
|
300
320
|
const head = ` <testcase name="${name}" classname="${classname}" time="${c.time}"`;
|
|
@@ -331,6 +351,14 @@ function formatMultiSuiteJunit(result, opts = {}) {
|
|
|
331
351
|
url: c.url || void 0,
|
|
332
352
|
steps_not_checked: c.steps_not_checked
|
|
333
353
|
};
|
|
354
|
+
const outcome = outcomeOf(c);
|
|
355
|
+
if (NEUTRAL.has(outcome)) {
|
|
356
|
+
return {
|
|
357
|
+
...base,
|
|
358
|
+
verdict: "skipped",
|
|
359
|
+
message: neutralMessage(outcome, c.error_message)
|
|
360
|
+
};
|
|
361
|
+
}
|
|
334
362
|
if (c.status === "failed" && c.quarantined === true) {
|
|
335
363
|
return {
|
|
336
364
|
...base,
|
|
@@ -409,6 +437,7 @@ function formatMultiSuiteJunit(result, opts = {}) {
|
|
|
409
437
|
}
|
|
410
438
|
|
|
411
439
|
// src/lib/poll.ts
|
|
440
|
+
var TERMINAL_STATUSES = ["passed", "failed", "error", "cancelled", "not_checked"];
|
|
412
441
|
function resumeSpinner(spinner, text) {
|
|
413
442
|
if (spinner.isEnabled === false) {
|
|
414
443
|
if (text !== void 0) spinner.text = text;
|
|
@@ -506,20 +535,26 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
|
|
|
506
535
|
`${chalk2.cyan("\u25B6")} ${chalk2.bold(tr.case_name || "Case")} started`
|
|
507
536
|
);
|
|
508
537
|
}
|
|
509
|
-
const isTerminal =
|
|
510
|
-
const wasTerminal = prev &&
|
|
538
|
+
const isTerminal = TERMINAL_STATUSES.includes(cur);
|
|
539
|
+
const wasTerminal = prev && TERMINAL_STATUSES.includes(prev);
|
|
511
540
|
if (isTerminal && !wasTerminal) {
|
|
512
|
-
const
|
|
541
|
+
const outcome = outcomeOf(tr);
|
|
542
|
+
const neutral = outcome === "not_checked" || outcome === "not_run";
|
|
543
|
+
const icon = neutral ? chalk2.gray("\u25CB") : cur === "passed" ? chalk2.green("\u2713") : cur === "cancelled" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
|
|
544
|
+
const label = neutral ? outcome.replace("_", " ") : cur;
|
|
513
545
|
const dur = tr.duration_ms ? ` (${(tr.duration_ms / 1e3).toFixed(1)}s)` : "";
|
|
514
|
-
printBelowSpinner(
|
|
515
|
-
|
|
546
|
+
printBelowSpinner(
|
|
547
|
+
spinner,
|
|
548
|
+
`${icon} ${chalk2.bold(tr.case_name || "Case")} ${label}${dur}`
|
|
549
|
+
);
|
|
550
|
+
if (tr.error_message && cur !== "passed" && !neutral) {
|
|
516
551
|
printBelowSpinner(spinner, ` ${chalk2.red(tr.error_message)}`);
|
|
517
552
|
}
|
|
518
553
|
}
|
|
519
554
|
caseStateBefore.set(tr.public_id, cur);
|
|
520
555
|
}
|
|
521
556
|
}
|
|
522
|
-
if (
|
|
557
|
+
if (TERMINAL_STATUSES.includes(result.status)) {
|
|
523
558
|
if (printedSteps.size > 0 && Array.isArray(result.steps)) {
|
|
524
559
|
spinner.stop();
|
|
525
560
|
result.steps.forEach((st, i) => {
|
|
@@ -569,7 +604,7 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
|
|
|
569
604
|
if (result.type === "suite_run" && Array.isArray(result.test_runs)) {
|
|
570
605
|
const total = result.total_cases ?? result.test_runs.length;
|
|
571
606
|
const done = result.test_runs.filter(
|
|
572
|
-
(tr) =>
|
|
607
|
+
(tr) => TERMINAL_STATUSES.includes(tr.status ?? "")
|
|
573
608
|
).length;
|
|
574
609
|
spinner.text = `Running suite \u2014 ${done}/${total} cases complete...`;
|
|
575
610
|
} else {
|
|
@@ -666,10 +701,10 @@ function displayProgressEvent(evt, ctx) {
|
|
|
666
701
|
break;
|
|
667
702
|
}
|
|
668
703
|
case "run_completed": {
|
|
669
|
-
const icon = evt.status === "passed" ? chalk2.green("\u2713") : evt.status === "cancelled" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
|
|
704
|
+
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
705
|
spinner.stop();
|
|
671
706
|
process.stderr.write(
|
|
672
|
-
`${icon} Run ${evt.status}${evt.duration_ms ? ` (${(evt.duration_ms / 1e3).toFixed(1)}s)` : ""}
|
|
707
|
+
`${icon} Run ${evt.status === "not_checked" ? "not checked" : evt.status}${evt.duration_ms ? ` (${(evt.duration_ms / 1e3).toFixed(1)}s)` : ""}
|
|
673
708
|
`
|
|
674
709
|
);
|
|
675
710
|
resumeSpinner(spinner);
|
|
@@ -709,7 +744,7 @@ function outputResult(result, options) {
|
|
|
709
744
|
}
|
|
710
745
|
}
|
|
711
746
|
function mapExitCode(status) {
|
|
712
|
-
if (status === "passed") return EXIT_PASS;
|
|
747
|
+
if (status === "passed" || status === "not_checked") return EXIT_PASS;
|
|
713
748
|
if (status === "failed") return EXIT_TEST_FAIL;
|
|
714
749
|
return EXIT_INFRA_ERROR;
|
|
715
750
|
}
|
|
@@ -832,7 +867,8 @@ ${signal} received.
|
|
|
832
867
|
device: options.device,
|
|
833
868
|
build: options.build,
|
|
834
869
|
metadata: options.metadata,
|
|
835
|
-
|
|
870
|
+
// Opt-in: always sent, so an older server's CI default never retries.
|
|
871
|
+
retryFailed: options.retry === true,
|
|
836
872
|
platform: options.platform,
|
|
837
873
|
app: options.app
|
|
838
874
|
});
|
|
@@ -960,7 +996,8 @@ ${signal} received.
|
|
|
960
996
|
device_id: opts.device,
|
|
961
997
|
build_id: opts.build,
|
|
962
998
|
metadata: opts.metadata,
|
|
963
|
-
|
|
999
|
+
// Opt-in: always sent, so an older server's CI default never retries.
|
|
1000
|
+
retry_failed: opts.retry === true
|
|
964
1001
|
});
|
|
965
1002
|
} catch (err) {
|
|
966
1003
|
spinner.stop();
|
|
@@ -1021,7 +1058,7 @@ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
|
|
|
1021
1058
|
pending = [];
|
|
1022
1059
|
if (nextText !== null) spinner.start(nextText);
|
|
1023
1060
|
};
|
|
1024
|
-
const TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
|
|
1061
|
+
const TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled", "not_checked"]);
|
|
1025
1062
|
let last = null;
|
|
1026
1063
|
const abandon = async (reason) => {
|
|
1027
1064
|
flush(null);
|
|
@@ -1080,10 +1117,11 @@ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
|
|
|
1080
1117
|
}
|
|
1081
1118
|
const wasTerminal = prev !== void 0 && TERMINAL.has(prev);
|
|
1082
1119
|
if (TERMINAL.has(cur) && !wasTerminal) {
|
|
1083
|
-
const
|
|
1120
|
+
const nothingChecked = outcomeOf(sr) === "not_checked";
|
|
1121
|
+
const mark = nothingChecked ? chalk5.gray("\u25CB") : cur === "passed" ? chalk5.green("\u2713") : cur === "cancelled" ? chalk5.yellow("\u26A0") : chalk5.red("\u2717");
|
|
1084
1122
|
const link = sr.url ? chalk5.dim(" " + sr.url) : "";
|
|
1085
1123
|
print(
|
|
1086
|
-
`${mark} ${chalk5.bold(sr.suite_name)} ${cur}: ${sr
|
|
1124
|
+
`${mark} ${chalk5.bold(sr.suite_name)} ${nothingChecked ? "not checked" : cur}: ${suiteCounts(sr)}${link}`
|
|
1087
1125
|
);
|
|
1088
1126
|
if (sr.error_message) {
|
|
1089
1127
|
print(` ${chalk5.red(sr.error_message)}`);
|
|
@@ -1137,39 +1175,57 @@ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
|
|
|
1137
1175
|
0
|
|
1138
1176
|
);
|
|
1139
1177
|
const aggregateNote = totalNotChecked > 0 ? chalk5.yellow(` (${stepsNotCheckedLabel(totalNotChecked)})`) : "";
|
|
1178
|
+
const aggregate = outcomeOf(last) === "not_checked" ? "not checked" : last.status;
|
|
1140
1179
|
process.stdout.write(`
|
|
1141
|
-
Aggregate: ${
|
|
1180
|
+
Aggregate: ${aggregate}${aggregateNote}
|
|
1142
1181
|
`);
|
|
1143
1182
|
for (const sr of last.suite_runs) {
|
|
1144
|
-
const mark = sr.status === "passed" ? chalk5.green("\u2713") : chalk5.red("\u2717");
|
|
1183
|
+
const mark = outcomeOf(sr) === "not_checked" ? chalk5.gray("\u25CB") : sr.status === "passed" ? chalk5.green("\u2713") : chalk5.red("\u2717");
|
|
1145
1184
|
const link = sr.url ? chalk5.dim(` ${sr.url}`) : "";
|
|
1146
|
-
process.stdout.write(
|
|
1147
|
-
|
|
1148
|
-
`
|
|
1149
|
-
);
|
|
1185
|
+
process.stdout.write(` ${mark} ${sr.suite_name}: ${suiteCounts(sr)}${link}
|
|
1186
|
+
`);
|
|
1150
1187
|
if (sr.error_message) {
|
|
1151
1188
|
process.stdout.write(` ${chalk5.red(sr.error_message)}
|
|
1152
1189
|
`);
|
|
1153
1190
|
}
|
|
1154
1191
|
for (const cr of sr.case_runs ?? []) {
|
|
1155
|
-
const
|
|
1192
|
+
const outcome = outcomeOf(cr);
|
|
1193
|
+
const neutral = outcome === "not_checked" || outcome === "not_run";
|
|
1194
|
+
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");
|
|
1156
1195
|
const dur = cr.duration_ms ? chalk5.dim(` (${(cr.duration_ms / 1e3).toFixed(1)}s)`) : "";
|
|
1157
|
-
const unchecked = notCheckedSuffix(
|
|
1196
|
+
const unchecked = notCheckedSuffix(outcome, cr.steps_not_checked);
|
|
1197
|
+
const quarantined = cr.status === "failed" && cr.quarantined === true;
|
|
1198
|
+
const label = neutral ? outcome.replace("_", " ") : quarantined ? "failed (quarantined)" : cr.status ?? "?";
|
|
1158
1199
|
process.stdout.write(
|
|
1159
|
-
` ${caseMark} ${cr.case_name ?? "Case"} ${
|
|
1200
|
+
` ${caseMark} ${cr.case_name ?? "Case"} ${label}${unchecked ? chalk5.yellow(unchecked) : ""}${dur} ${chalk5.dim(cr.url)}
|
|
1160
1201
|
`
|
|
1161
1202
|
);
|
|
1162
|
-
if (cr.error_message && cr.status
|
|
1203
|
+
if (cr.error_message && (neutral || cr.status === "failed" && cr.quarantined === true)) {
|
|
1204
|
+
process.stdout.write(` ${chalk5.dim(cr.error_message)}
|
|
1205
|
+
`);
|
|
1206
|
+
} else if (cr.error_message && cr.status !== "passed") {
|
|
1163
1207
|
process.stdout.write(` ${chalk5.red(cr.error_message)}
|
|
1164
1208
|
`);
|
|
1165
1209
|
}
|
|
1166
1210
|
}
|
|
1167
1211
|
}
|
|
1168
1212
|
}
|
|
1169
|
-
if (last.status === "passed") process.exit(EXIT_PASS);
|
|
1213
|
+
if (last.status === "passed" || last.status === "not_checked") process.exit(EXIT_PASS);
|
|
1170
1214
|
if (last.status === "failed") process.exit(EXIT_TEST_FAIL);
|
|
1171
1215
|
process.exit(EXIT_INFRA_ERROR);
|
|
1172
1216
|
}
|
|
1217
|
+
function suiteCounts(sr) {
|
|
1218
|
+
const nc = sr.not_checked_tests ?? 0;
|
|
1219
|
+
const q = Math.min(
|
|
1220
|
+
sr.failed,
|
|
1221
|
+
sr.quarantined_failures ?? (sr.case_runs ?? []).filter((c) => c.status === "failed" && c.quarantined === true).length
|
|
1222
|
+
);
|
|
1223
|
+
const parts = [`${sr.passed} passed`];
|
|
1224
|
+
if (q === 0 || sr.failed > q) parts.push(`${sr.failed - q} failed`);
|
|
1225
|
+
if (q > 0) parts.push(`${q} failed (quarantined)`);
|
|
1226
|
+
if (nc > 0) parts.push(`${nc} not checked`);
|
|
1227
|
+
return parts.join(", ");
|
|
1228
|
+
}
|
|
1173
1229
|
|
|
1174
1230
|
// src/commands/run-test.ts
|
|
1175
1231
|
import chalk6 from "chalk";
|
|
@@ -1238,7 +1294,8 @@ ${signal} received.
|
|
|
1238
1294
|
device: options.device,
|
|
1239
1295
|
build: options.build,
|
|
1240
1296
|
metadata: options.metadata,
|
|
1241
|
-
|
|
1297
|
+
// Opt-in: always sent, so an older server's CI default never retries.
|
|
1298
|
+
retryFailed: options.retry === true,
|
|
1242
1299
|
platform: options.platform,
|
|
1243
1300
|
app: options.app
|
|
1244
1301
|
});
|
|
@@ -1641,7 +1698,7 @@ function formatImport(result, baseUrl) {
|
|
|
1641
1698
|
}
|
|
1642
1699
|
for (const f of result.flags) lines.push(flagLine(f));
|
|
1643
1700
|
for (const t of [...result.tests, ...result.setups]) {
|
|
1644
|
-
const meta = [`${t.steps.length} steps`];
|
|
1701
|
+
const meta = [`${t.steps.length} ${t.steps.length === 1 ? "step" : "steps"}`];
|
|
1645
1702
|
if (t.setup_keys.length) meta.push(`before this test: ${t.setup_keys.join(", ")}`);
|
|
1646
1703
|
if (t.start_state) meta.push(`start: ${START_STATE_LABEL[t.start_state] ?? t.start_state}`);
|
|
1647
1704
|
const head = t.importable ? t.name : chalk11.red(`${t.name} (not imported)`);
|
|
@@ -4040,9 +4097,9 @@ program.command("run-suite <suite-id>").description("Run all test cases in a sui
|
|
|
4040
4097
|
"--app <app-id>",
|
|
4041
4098
|
"Run the suite against this app instead of its own (e.g. another platform build with a different bundle id)"
|
|
4042
4099
|
).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
|
-
"--
|
|
4044
|
-
"
|
|
4045
|
-
).action((suiteId, opts) => {
|
|
4100
|
+
"--retry",
|
|
4101
|
+
"Retry a failed test once, after a fresh start of the app (off by default); a pass on the retry marks the test flaky"
|
|
4102
|
+
).option("--no-retry", "Deprecated: retries are off unless --retry is given (accepted, no effect)").action((suiteId, opts) => {
|
|
4046
4103
|
if (opts.device && opts.deviceCategory) {
|
|
4047
4104
|
process.stderr.write("error: --device cannot be combined with --device-category\n");
|
|
4048
4105
|
process.exit(2);
|
|
@@ -4079,9 +4136,9 @@ program.command("run-suites").description("Run all suites matching a tag filter"
|
|
|
4079
4136
|
"--device <device_id>",
|
|
4080
4137
|
"Pin run to a specific device (use `vibeview list-devices` to find one). Conflicts with --device-category."
|
|
4081
4138
|
).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
|
-
"--
|
|
4083
|
-
"
|
|
4084
|
-
).action((opts) => {
|
|
4139
|
+
"--retry",
|
|
4140
|
+
"Retry a failed test once, after a fresh start of the app (off by default); a pass on the retry marks the test flaky"
|
|
4141
|
+
).option("--no-retry", "Deprecated: retries are off unless --retry is given (accepted, no effect)").action((opts) => {
|
|
4085
4142
|
if (opts.device && opts.deviceCategory) {
|
|
4086
4143
|
process.stderr.write("error: --device cannot be combined with --device-category\n");
|
|
4087
4144
|
process.exit(2);
|
|
@@ -4115,9 +4172,9 @@ program.command("run-test <test-id>").description("Run a single test case").opti
|
|
|
4115
4172
|
"--app <app-id>",
|
|
4116
4173
|
"Run the test against this app instead of its own (e.g. another platform build with a different bundle id)"
|
|
4117
4174
|
).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
|
-
"--
|
|
4119
|
-
"
|
|
4120
|
-
).action((testId, opts) => {
|
|
4175
|
+
"--retry",
|
|
4176
|
+
"Retry a failed test once, after a fresh start of the app (off by default); a pass on the retry marks the test flaky"
|
|
4177
|
+
).option("--no-retry", "Deprecated: retries are off unless --retry is given (accepted, no effect)").action((testId, opts) => {
|
|
4121
4178
|
if (opts.device && opts.deviceCategory) {
|
|
4122
4179
|
process.stderr.write("error: --device cannot be combined with --device-category\n");
|
|
4123
4180
|
process.exit(2);
|
|
@@ -4349,7 +4406,7 @@ program.command("dev-reload").description(
|
|
|
4349
4406
|
program.command("mcp").description(
|
|
4350
4407
|
"Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
|
|
4351
4408
|
).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
|
|
4352
|
-
const { runMcpServer } = await import("./server-
|
|
4409
|
+
const { runMcpServer } = await import("./server-56MR4KJW.js");
|
|
4353
4410
|
await runMcpServer({ ...opts, version: pkg.version });
|
|
4354
4411
|
});
|
|
4355
4412
|
program.command("agent-setup").description(
|
|
@@ -4373,6 +4430,9 @@ ${pressButtonsHelp()}`);
|
|
|
4373
4430
|
return runAgentVerb(cmd, positionals, opts);
|
|
4374
4431
|
});
|
|
4375
4432
|
}
|
|
4433
|
+
for (const name of RUN_COMMANDS) {
|
|
4434
|
+
program.commands.find((c) => c.name() === name)?.exitOverride((err) => process.exit(runCommandParseExitCode(err)));
|
|
4435
|
+
}
|
|
4376
4436
|
if (updateCheckAllowed(process.argv, process.env, process.stderr.isTTY)) {
|
|
4377
4437
|
void checkForUpdate(pkg.version, { print: (line) => process.stderr.write(`${line}
|
|
4378
4438
|
`) });
|
|
@@ -20,20 +20,22 @@ import {
|
|
|
20
20
|
isDevPlatform,
|
|
21
21
|
loadDevConfig,
|
|
22
22
|
loadLocalDevConfig,
|
|
23
|
+
outcomeOf,
|
|
23
24
|
parseBuildId,
|
|
24
25
|
parseIdleWarningSeconds,
|
|
26
|
+
platformLabel,
|
|
25
27
|
platformWithArticle,
|
|
26
28
|
readDevState,
|
|
27
29
|
recordDevUse,
|
|
28
30
|
renderEnvelope,
|
|
29
31
|
resolveMetro,
|
|
30
32
|
startDevSession
|
|
31
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-MK3WDPVZ.js";
|
|
32
34
|
import {
|
|
33
35
|
ApiClient,
|
|
34
36
|
NotFoundError,
|
|
35
37
|
resolveAuth
|
|
36
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-GDTLLJFL.js";
|
|
37
39
|
|
|
38
40
|
// src/mcp/server.ts
|
|
39
41
|
import { writeFileSync } from "fs";
|
|
@@ -58,7 +60,12 @@ var CREATE_TEST_DEF = {
|
|
|
58
60
|
app: { type: "string", description: "App id (from list_apps)." },
|
|
59
61
|
name: { type: "string", description: "Test name." },
|
|
60
62
|
steps: { type: "string", description: "The steps, one per line." },
|
|
61
|
-
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
|
+
}
|
|
62
69
|
},
|
|
63
70
|
required: ["app", "name", "steps"]
|
|
64
71
|
};
|
|
@@ -71,7 +78,7 @@ var RUN_TEST_DEF = {
|
|
|
71
78
|
session_id: { type: "string", description: "Run on this session instead of a new device." },
|
|
72
79
|
device_model: {
|
|
73
80
|
type: "string",
|
|
74
|
-
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."
|
|
75
82
|
},
|
|
76
83
|
ai_use: AI_USE_PROPERTY,
|
|
77
84
|
start_from_current_screen: {
|
|
@@ -93,7 +100,7 @@ var RUN_GROUP_DEF = {
|
|
|
93
100
|
session_id: { type: "string", description: "Run on this session instead of a new device." },
|
|
94
101
|
device_model: {
|
|
95
102
|
type: "string",
|
|
96
|
-
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."
|
|
97
104
|
},
|
|
98
105
|
ai_use: AI_USE_PROPERTY,
|
|
99
106
|
start_from_current_screen: {
|
|
@@ -346,6 +353,14 @@ async function explainInactive(sessionId, deps) {
|
|
|
346
353
|
function findCommand(toolName) {
|
|
347
354
|
return AGENT_COMMANDS.find((c) => toolNameFor(c.verb) === toolName);
|
|
348
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
|
+
}
|
|
349
364
|
async function handleCreateTest(args, deps) {
|
|
350
365
|
const steps = String(args.steps ?? "").split("\n").map((s) => s.trim()).filter(Boolean).map((text) => ({ text }));
|
|
351
366
|
if (!steps.length) return errorResult("create_test: `steps` needs at least one line");
|
|
@@ -354,7 +369,8 @@ async function handleCreateTest(args, deps) {
|
|
|
354
369
|
name: String(args.name),
|
|
355
370
|
app_id: String(args.app),
|
|
356
371
|
steps,
|
|
357
|
-
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
|
|
358
374
|
});
|
|
359
375
|
const verb = created.outcome === "unchanged" ? "Test already exists, unchanged" : created.outcome === "updated" ? "Updated existing test" : "Created test";
|
|
360
376
|
return textResult(
|
|
@@ -362,9 +378,17 @@ async function handleCreateTest(args, deps) {
|
|
|
362
378
|
created
|
|
363
379
|
);
|
|
364
380
|
} catch (err) {
|
|
365
|
-
return errorResult(`error: ${
|
|
381
|
+
return errorResult(`error: ${testToolErrorMessage(err)}`);
|
|
366
382
|
}
|
|
367
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
|
+
}
|
|
368
392
|
async function handleRun(kind, args, deps) {
|
|
369
393
|
const id = String(kind === "test" ? args.test : args.group);
|
|
370
394
|
const mode = typeof args.ai_use === "string" ? AI_USE_MODE[args.ai_use] : void 0;
|
|
@@ -386,12 +410,13 @@ async function handleRun(kind, args, deps) {
|
|
|
386
410
|
kind === "test" ? { target: "test", test_ids: [id], devices, mode } : { target: "group", group_id: id, devices, mode }
|
|
387
411
|
);
|
|
388
412
|
const queued = r.lanes.some((l) => l.status === "queued") ? " It is waiting for a free device." : "";
|
|
413
|
+
const where = runDestination(r.lanes);
|
|
389
414
|
return textResult(
|
|
390
|
-
`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}.`,
|
|
391
416
|
r
|
|
392
417
|
);
|
|
393
418
|
} catch (err) {
|
|
394
|
-
return errorResult(`error: ${
|
|
419
|
+
return errorResult(`error: ${testToolErrorMessage(err)}`);
|
|
395
420
|
}
|
|
396
421
|
}
|
|
397
422
|
function notCheckedNote(status, n) {
|
|
@@ -399,33 +424,63 @@ function notCheckedNote(status, n) {
|
|
|
399
424
|
return ` \u2014 ${n} step${n === 1 ? "" : "s"} not checked (no recorded actions; not verified)`;
|
|
400
425
|
}
|
|
401
426
|
var RUN_IN_PROGRESS = /* @__PURE__ */ new Set(["queued", "pending", "running"]);
|
|
427
|
+
function agentStatus(r) {
|
|
428
|
+
const outcome = outcomeOf(r);
|
|
429
|
+
return outcome === "not_checked" || outcome === "not_run" ? outcome.replace("_", " ") : outcome;
|
|
430
|
+
}
|
|
431
|
+
function testsNotChecked(n) {
|
|
432
|
+
return typeof n === "number" && n > 0 ? `, ${n} not checked` : "";
|
|
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
|
+
}
|
|
402
440
|
function formatRunForAgent(run) {
|
|
403
441
|
const lines = [];
|
|
404
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
|
+
}
|
|
405
451
|
lines.push(
|
|
406
|
-
`${run.name}: ${run
|
|
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)}`
|
|
407
453
|
);
|
|
408
454
|
for (const lane of run.lanes) {
|
|
409
455
|
const detail = [lane.platform, lane.os_version].filter(Boolean).join(" ");
|
|
410
|
-
let head = `${lane.model ? `${lane.model} (${detail})` : detail}: ${lane
|
|
456
|
+
let head = `${lane.model ? `${lane.model} (${detail})` : detail}: ${agentStatus(lane)}`;
|
|
411
457
|
if (lane.status === "queued" && typeof lane.queue?.position === "number") {
|
|
412
458
|
head += ` (position ${lane.queue.position} in the queue)`;
|
|
413
459
|
}
|
|
414
460
|
if (lane.error_message) head += ` \u2014 ${lane.error_message}`;
|
|
415
461
|
lines.push(head);
|
|
416
462
|
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)` : "";
|
|
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 q = r.kind === "test" && quarantinedFailure(r.tests?.[0] ?? {}) ? " (quarantined)" : "";
|
|
418
465
|
lines.push(
|
|
419
|
-
` ${r.name}: ${r
|
|
466
|
+
` ${r.name}: ${agentStatus(r)}${q}${count}${r.error_message ? ` \u2014 ${r.error_message}` : ""}`
|
|
420
467
|
);
|
|
468
|
+
if (r.kind !== "group") continue;
|
|
469
|
+
for (const t of r.tests ?? []) {
|
|
470
|
+
if (t.status !== "failed" && t.status !== "error") continue;
|
|
471
|
+
const tq = quarantinedFailure(t) ? " (quarantined)" : "";
|
|
472
|
+
lines.push(
|
|
473
|
+
` ${t.name}: ${t.status}${tq}${t.error_message ? ` \u2014 ${t.error_message}` : ""}`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
421
476
|
}
|
|
422
477
|
}
|
|
423
478
|
} else if (Array.isArray(run.test_runs)) {
|
|
424
|
-
lines.push(`${run.suite_name ?? "Group"}: ${run
|
|
479
|
+
lines.push(`${run.suite_name ?? "Group"}: ${agentStatus(run)}`);
|
|
425
480
|
for (const t of run.test_runs) {
|
|
426
481
|
const q = t.quarantined && t.status === "failed" ? " (quarantined)" : "";
|
|
427
482
|
lines.push(
|
|
428
|
-
` ${t.case_name}: ${t
|
|
483
|
+
` ${t.case_name}: ${agentStatus(t)}${q}${t.error_message ? ` \u2014 ${t.error_message}` : ""}${notCheckedNote(outcomeOf(t), t.steps_not_checked)}`
|
|
429
484
|
);
|
|
430
485
|
}
|
|
431
486
|
} else {
|
|
@@ -434,7 +489,7 @@ function formatRunForAgent(run) {
|
|
|
434
489
|
const notChecked = typeof run.steps_not_checked === "number" ? run.steps_not_checked : (run.steps ?? []).filter((s) => s.status === "not_checked").length;
|
|
435
490
|
const q = run.quarantined && run.status === "failed" ? " (quarantined)" : "";
|
|
436
491
|
lines.push(
|
|
437
|
-
`${name}: ${run
|
|
492
|
+
`${name}: ${agentStatus(run)}${q}${run.error_message ? ` \u2014 ${run.error_message}` : ""}${notCheckedNote(outcomeOf(run), notChecked)}`
|
|
438
493
|
);
|
|
439
494
|
if (failed) {
|
|
440
495
|
lines.push(
|
|
@@ -455,7 +510,7 @@ async function handleGetRun(args, deps) {
|
|
|
455
510
|
const run = kind.kind === "batch" ? await client.getRunBatch(id) : await client.getRunStatus(id);
|
|
456
511
|
return textResult(formatRunForAgent(run), run);
|
|
457
512
|
} catch (err) {
|
|
458
|
-
return errorResult(`error: ${
|
|
513
|
+
return errorResult(`error: ${testToolErrorMessage(err)}`);
|
|
459
514
|
}
|
|
460
515
|
}
|
|
461
516
|
async function dispatchToolCall(toolName, args, deps, extra = {}) {
|
|
@@ -874,7 +929,7 @@ async function handleUploadApp(args, deps) {
|
|
|
874
929
|
}
|
|
875
930
|
const name = typeof args.name === "string" && args.name ? args.name : void 0;
|
|
876
931
|
const upload = deps.uploadApp ?? (async (f, n) => {
|
|
877
|
-
const { uploadAppBuild } = await import("./upload-app-
|
|
932
|
+
const { uploadAppBuild } = await import("./upload-app-ZEQMGZIT.js");
|
|
878
933
|
const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
|
|
879
934
|
return result2;
|
|
880
935
|
});
|
|
@@ -1270,6 +1325,23 @@ var MCP_INSTRUCTIONS = [
|
|
|
1270
1325
|
"A session stopped elsewhere is reported as ended; call dev_start again for a new one.",
|
|
1271
1326
|
'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.'
|
|
1272
1327
|
].join("\n");
|
|
1328
|
+
function toolInputSchema(def, z) {
|
|
1329
|
+
const shape = {};
|
|
1330
|
+
for (const [key, prop] of Object.entries(def.properties)) {
|
|
1331
|
+
const base = prop.enum ? z.enum(prop.enum) : z[prop.type]();
|
|
1332
|
+
const described = base.describe(prop.description);
|
|
1333
|
+
shape[key] = def.required.includes(key) ? described : described.optional();
|
|
1334
|
+
}
|
|
1335
|
+
const valid = Object.keys(def.properties);
|
|
1336
|
+
return z.strictObject(shape, {
|
|
1337
|
+
error: (issue) => {
|
|
1338
|
+
if (issue.code !== "unrecognized_keys") return void 0;
|
|
1339
|
+
const unknown = issue.keys.map((k) => `"${k}"`).join(", ");
|
|
1340
|
+
const takes = valid.length ? `${def.name} takes: ${valid.join(", ")}.` : `${def.name} takes no arguments.`;
|
|
1341
|
+
return `Unknown argument${issue.keys.length === 1 ? "" : "s"} ${unknown}. ${takes}`;
|
|
1342
|
+
}
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1273
1345
|
async function createMcpServer(opts) {
|
|
1274
1346
|
const [{ McpServer }, { z }] = await Promise.all([
|
|
1275
1347
|
import("@modelcontextprotocol/sdk/server/mcp.js"),
|
|
@@ -1288,21 +1360,12 @@ async function createMcpServer(opts) {
|
|
|
1288
1360
|
{ name: "vibeview", version: opts.version ?? "0.0.0" },
|
|
1289
1361
|
{ instructions: MCP_INSTRUCTIONS }
|
|
1290
1362
|
);
|
|
1291
|
-
function zodShape(def) {
|
|
1292
|
-
const shape = {};
|
|
1293
|
-
for (const [key, prop] of Object.entries(def.properties)) {
|
|
1294
|
-
const base = prop.enum ? z.enum(prop.enum) : z[prop.type]();
|
|
1295
|
-
const described = base.describe(prop.description);
|
|
1296
|
-
shape[key] = def.required.includes(key) ? described : described.optional();
|
|
1297
|
-
}
|
|
1298
|
-
return shape;
|
|
1299
|
-
}
|
|
1300
1363
|
for (const def of buildToolDefinitions()) {
|
|
1301
1364
|
server.registerTool(
|
|
1302
1365
|
def.name,
|
|
1303
1366
|
{
|
|
1304
1367
|
description: def.description,
|
|
1305
|
-
inputSchema:
|
|
1368
|
+
inputSchema: toolInputSchema(def, z),
|
|
1306
1369
|
annotations: { readOnlyHint: !def.mutating }
|
|
1307
1370
|
},
|
|
1308
1371
|
async (args, extra) => {
|
|
@@ -1389,7 +1452,10 @@ export {
|
|
|
1389
1452
|
idleNoticeText,
|
|
1390
1453
|
mcpText,
|
|
1391
1454
|
progressReporter,
|
|
1455
|
+
runDestination,
|
|
1392
1456
|
runMcpServer,
|
|
1393
1457
|
startProgress,
|
|
1394
|
-
startedDevice
|
|
1458
|
+
startedDevice,
|
|
1459
|
+
testToolErrorMessage,
|
|
1460
|
+
toolInputSchema
|
|
1395
1461
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibeview",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.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",
|