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.
- package/dist/{chunk-MYXUGCR5.js → chunk-GDTLLJFL.js} +14 -1
- package/dist/{chunk-EQN3X62U.js → chunk-MK3WDPVZ.js} +1 -1
- package/dist/cli.js +98 -21
- package/dist/{server-GEY4WZUH.js → server-435X37GD.js} +97 -26
- 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,
|
package/dist/cli.js
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
runAgentVerb,
|
|
31
31
|
runStepsNotChecked,
|
|
32
32
|
stepsNotCheckedLabel
|
|
33
|
-
} from "./chunk-
|
|
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-
|
|
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
|
-
|
|
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")
|
|
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")}
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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-
|
|
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-
|
|
33
|
+
} from "./chunk-MK3WDPVZ.js";
|
|
33
34
|
import {
|
|
34
35
|
ApiClient,
|
|
35
36
|
NotFoundError,
|
|
36
37
|
resolveAuth
|
|
37
|
-
} from "./chunk-
|
|
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
|
|
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: ${
|
|
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: ${
|
|
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}${
|
|
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: ${
|
|
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-
|
|
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:
|
|
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
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibeview",
|
|
3
|
-
"version": "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",
|