@yoqa/cli 0.3.27 → 0.3.29
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/README.md +2 -2
- package/dist/main.js +648 -24
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @yoqa/cli
|
|
2
2
|
|
|
3
|
-
CLI for [Yoqa](https://yoqa.
|
|
3
|
+
CLI for [Yoqa](https://yoqa.arisn.dev/docs/overview) — drive a local runner over HTTP from your terminal or CI.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install -g @yoqa/cli
|
|
@@ -18,4 +18,4 @@ yoqa serve
|
|
|
18
18
|
|
|
19
19
|
Disable auto-start with `YOQA_NO_AUTOSTART=1`. Override the URL with `YOQA_RUNNER_HOST` / `YOQA_RUNNER_PORT` / `YOQA_RUNNER_URL`.
|
|
20
20
|
|
|
21
|
-
See [CLI docs](https://yoqa.
|
|
21
|
+
See [CLI docs](https://yoqa.arisn.dev/docs/cli) and [GitHub Actions](https://yoqa.arisn.dev/docs/github-actions).
|
package/dist/main.js
CHANGED
|
@@ -2122,7 +2122,7 @@ var require_commander = __commonJS((exports) => {
|
|
|
2122
2122
|
});
|
|
2123
2123
|
|
|
2124
2124
|
// src/program.ts
|
|
2125
|
-
import { readFile as readFile2 } from "node:fs/promises";
|
|
2125
|
+
import { readFile as readFile2, writeFile as writeFile3 } from "node:fs/promises";
|
|
2126
2126
|
|
|
2127
2127
|
// ../../node_modules/zod/v3/external.js
|
|
2128
2128
|
var exports_external = {};
|
|
@@ -6105,6 +6105,7 @@ var healthResponseSchema = exports_external.object({
|
|
|
6105
6105
|
uptimeMs: exports_external.number().nonnegative()
|
|
6106
6106
|
});
|
|
6107
6107
|
var devicePlatformSchema = exports_external.union([exports_external.literal("ios"), exports_external.literal("android")]);
|
|
6108
|
+
var laneNameSchema = exports_external.union([exports_external.literal("appium"), exports_external.literal("direct")]);
|
|
6108
6109
|
var deviceKindSchema = exports_external.union([
|
|
6109
6110
|
exports_external.literal("physical"),
|
|
6110
6111
|
exports_external.literal("simulator"),
|
|
@@ -6263,6 +6264,7 @@ var catalogAppSchema = exports_external.object({
|
|
|
6263
6264
|
name: exports_external.string().min(1),
|
|
6264
6265
|
prefix: exports_external.string().min(1),
|
|
6265
6266
|
context: exports_external.string(),
|
|
6267
|
+
knowledge: exports_external.string(),
|
|
6266
6268
|
iosBundleId: exports_external.string(),
|
|
6267
6269
|
iosAppStoreId: exports_external.string(),
|
|
6268
6270
|
androidApplicationId: exports_external.string(),
|
|
@@ -6278,6 +6280,7 @@ var updateAppRequestSchema = exports_external.object({
|
|
|
6278
6280
|
name: exports_external.string().min(1).optional(),
|
|
6279
6281
|
prefix: exports_external.string().min(1).optional(),
|
|
6280
6282
|
context: exports_external.string().optional(),
|
|
6283
|
+
knowledge: exports_external.string().optional(),
|
|
6281
6284
|
iosBundleId: exports_external.string().optional(),
|
|
6282
6285
|
iosAppStoreId: exports_external.string().optional(),
|
|
6283
6286
|
androidApplicationId: exports_external.string().optional(),
|
|
@@ -6591,7 +6594,8 @@ var probeProviderResponseSchema = exports_external.object({
|
|
|
6591
6594
|
var providerModelSchema = exports_external.object({
|
|
6592
6595
|
id: exports_external.string().min(1),
|
|
6593
6596
|
name: exports_external.string().min(1),
|
|
6594
|
-
provider: exports_external.string().min(1).optional()
|
|
6597
|
+
provider: exports_external.string().min(1).optional(),
|
|
6598
|
+
vision: exports_external.boolean().optional()
|
|
6595
6599
|
});
|
|
6596
6600
|
var listProviderModelsResponseSchema = exports_external.object({
|
|
6597
6601
|
models: exports_external.array(providerModelSchema),
|
|
@@ -6620,6 +6624,7 @@ var runExecutionModeSchema = exports_external.union([
|
|
|
6620
6624
|
exports_external.literal("script"),
|
|
6621
6625
|
exports_external.literal("agent")
|
|
6622
6626
|
]);
|
|
6627
|
+
var runScreenModeSchema = exports_external.union([exports_external.literal("vision"), exports_external.literal("tree")]);
|
|
6623
6628
|
var createRunRequestSchema = exports_external.object({
|
|
6624
6629
|
appId: exports_external.string().min(1),
|
|
6625
6630
|
caseIds: exports_external.array(exports_external.string().min(1)).min(1),
|
|
@@ -6627,7 +6632,18 @@ var createRunRequestSchema = exports_external.object({
|
|
|
6627
6632
|
platform: devicePlatformSchema,
|
|
6628
6633
|
buildId: exports_external.string().min(1).optional(),
|
|
6629
6634
|
buildPath: exports_external.string().min(1).optional(),
|
|
6630
|
-
executionMode: runExecutionModeSchema.optional()
|
|
6635
|
+
executionMode: runExecutionModeSchema.optional(),
|
|
6636
|
+
screenMode: runScreenModeSchema.optional(),
|
|
6637
|
+
lane: exports_external.union([laneNameSchema, exports_external.literal("auto")]).optional()
|
|
6638
|
+
});
|
|
6639
|
+
var stepPhasesSchema = exports_external.object({
|
|
6640
|
+
captureMs: exports_external.number().nonnegative(),
|
|
6641
|
+
screenMs: exports_external.number().nonnegative(),
|
|
6642
|
+
prepareMs: exports_external.number().nonnegative(),
|
|
6643
|
+
decideMs: exports_external.number().nonnegative(),
|
|
6644
|
+
actionMs: exports_external.number().nonnegative(),
|
|
6645
|
+
settleMs: exports_external.number().nonnegative(),
|
|
6646
|
+
decideRetries: exports_external.number().int().nonnegative()
|
|
6631
6647
|
});
|
|
6632
6648
|
var runStepSchema = exports_external.object({
|
|
6633
6649
|
id: exports_external.string().min(1),
|
|
@@ -6637,6 +6653,7 @@ var runStepSchema = exports_external.object({
|
|
|
6637
6653
|
screenshotUri: exports_external.string().nullable(),
|
|
6638
6654
|
ok: exports_external.boolean(),
|
|
6639
6655
|
latencyMs: exports_external.number().nonnegative(),
|
|
6656
|
+
phases: stepPhasesSchema.nullable().optional(),
|
|
6640
6657
|
detail: exports_external.string().nullable(),
|
|
6641
6658
|
command: exports_external.string().nullable().optional(),
|
|
6642
6659
|
createdAt: exports_external.number().int().nonnegative()
|
|
@@ -6661,6 +6678,9 @@ var runSchema = exports_external.object({
|
|
|
6661
6678
|
buildId: exports_external.string().nullable(),
|
|
6662
6679
|
status: runStatusSchema,
|
|
6663
6680
|
executionMode: runExecutionModeSchema,
|
|
6681
|
+
screenMode: runScreenModeSchema.optional(),
|
|
6682
|
+
lane: laneNameSchema.optional(),
|
|
6683
|
+
laneWarning: exports_external.string().min(1).optional(),
|
|
6664
6684
|
error: exports_external.string().nullable(),
|
|
6665
6685
|
createdAt: exports_external.number().int().nonnegative(),
|
|
6666
6686
|
startedAt: exports_external.number().int().nonnegative().nullable(),
|
|
@@ -6678,7 +6698,8 @@ var connectDeviceRequestSchema = exports_external.object({
|
|
|
6678
6698
|
deviceId: exports_external.string().min(1),
|
|
6679
6699
|
platform: devicePlatformSchema,
|
|
6680
6700
|
bundleId: exports_external.string().min(1).optional(),
|
|
6681
|
-
appPackage: exports_external.string().min(1).optional()
|
|
6701
|
+
appPackage: exports_external.string().min(1).optional(),
|
|
6702
|
+
lane: exports_external.union([laneNameSchema, exports_external.literal("auto")]).optional()
|
|
6682
6703
|
});
|
|
6683
6704
|
var activeDeviceResponseSchema = exports_external.object({
|
|
6684
6705
|
deviceId: exports_external.string().min(1),
|
|
@@ -6687,7 +6708,9 @@ var activeDeviceResponseSchema = exports_external.object({
|
|
|
6687
6708
|
mjpegPort: exports_external.number().int().positive().optional(),
|
|
6688
6709
|
streamReady: exports_external.boolean().optional(),
|
|
6689
6710
|
streamUrl: exports_external.string().min(1).optional(),
|
|
6690
|
-
heldByRun: exports_external.boolean().optional()
|
|
6711
|
+
heldByRun: exports_external.boolean().optional(),
|
|
6712
|
+
lane: laneNameSchema.optional(),
|
|
6713
|
+
laneWarning: exports_external.string().min(1).optional()
|
|
6691
6714
|
});
|
|
6692
6715
|
var controlPointerMessageSchema = exports_external.object({
|
|
6693
6716
|
type: exports_external.literal("pointer"),
|
|
@@ -6746,7 +6769,24 @@ var actionRequestSchema = exports_external.object({
|
|
|
6746
6769
|
appId: exports_external.string().optional(),
|
|
6747
6770
|
url: exports_external.string().optional(),
|
|
6748
6771
|
alertAction: exports_external.union([exports_external.literal("accept"), exports_external.literal("dismiss")]).optional(),
|
|
6749
|
-
seconds: exports_external.number().optional()
|
|
6772
|
+
seconds: exports_external.number().optional(),
|
|
6773
|
+
screenshot: exports_external.boolean().optional(),
|
|
6774
|
+
settleMs: exports_external.number().int().min(0).max(1e4).optional(),
|
|
6775
|
+
fullImage: exports_external.boolean().optional(),
|
|
6776
|
+
imageScale: exports_external.number().min(0.01).max(1).optional()
|
|
6777
|
+
});
|
|
6778
|
+
var actionResultScreenshotSchema = exports_external.object({
|
|
6779
|
+
path: exports_external.string().min(1),
|
|
6780
|
+
rawPath: exports_external.string().min(1).optional(),
|
|
6781
|
+
annotatedPath: exports_external.string().min(1).optional(),
|
|
6782
|
+
settled: exports_external.boolean(),
|
|
6783
|
+
waitedMs: exports_external.number(),
|
|
6784
|
+
changed: exports_external.boolean().nullable()
|
|
6785
|
+
});
|
|
6786
|
+
var actionPhasesSchema = exports_external.object({
|
|
6787
|
+
captureMs: exports_external.number().nonnegative(),
|
|
6788
|
+
actionMs: exports_external.number().nonnegative(),
|
|
6789
|
+
settleMs: exports_external.number().nonnegative()
|
|
6750
6790
|
});
|
|
6751
6791
|
var actionResponseSchema = exports_external.object({
|
|
6752
6792
|
ok: exports_external.literal(true),
|
|
@@ -6754,7 +6794,24 @@ var actionResponseSchema = exports_external.object({
|
|
|
6754
6794
|
resolved: exports_external.object({
|
|
6755
6795
|
x: exports_external.number().optional(),
|
|
6756
6796
|
y: exports_external.number().optional()
|
|
6757
|
-
}).optional()
|
|
6797
|
+
}).optional(),
|
|
6798
|
+
screenshot: actionResultScreenshotSchema.optional(),
|
|
6799
|
+
phases: actionPhasesSchema.optional()
|
|
6800
|
+
});
|
|
6801
|
+
var actionBatchRequestSchema = exports_external.object({
|
|
6802
|
+
steps: exports_external.array(actionRequestSchema).min(1),
|
|
6803
|
+
settleMs: exports_external.number().int().min(0).max(1e4).optional(),
|
|
6804
|
+
fullImage: exports_external.boolean().optional(),
|
|
6805
|
+
imageScale: exports_external.number().min(0.01).max(1).optional()
|
|
6806
|
+
});
|
|
6807
|
+
var actionBatchResponseSchema = exports_external.object({
|
|
6808
|
+
ok: exports_external.boolean(),
|
|
6809
|
+
completed: exports_external.number().int().nonnegative(),
|
|
6810
|
+
total: exports_external.number().int().nonnegative(),
|
|
6811
|
+
failedIndex: exports_external.number().int().nonnegative().optional(),
|
|
6812
|
+
error: exports_external.string().optional(),
|
|
6813
|
+
screenshot: actionResultScreenshotSchema.optional(),
|
|
6814
|
+
steps: exports_external.array(actionResponseSchema)
|
|
6758
6815
|
});
|
|
6759
6816
|
var yoqaStatusResponseSchema = exports_external.object({
|
|
6760
6817
|
runner: exports_external.object({
|
|
@@ -7309,10 +7366,28 @@ async function runYoqaShellScript(client, text, options = {}) {
|
|
|
7309
7366
|
}
|
|
7310
7367
|
|
|
7311
7368
|
// ../runner-client/src/run-report.ts
|
|
7312
|
-
function
|
|
7313
|
-
if (!action || typeof action !== "object")
|
|
7314
|
-
return
|
|
7315
|
-
const
|
|
7369
|
+
function readStepCycle(action) {
|
|
7370
|
+
if (!action || typeof action !== "object" || !("cycle" in action))
|
|
7371
|
+
return null;
|
|
7372
|
+
const cycle = action.cycle;
|
|
7373
|
+
if (!cycle || typeof cycle !== "object")
|
|
7374
|
+
return null;
|
|
7375
|
+
const record = cycle;
|
|
7376
|
+
const asRecord = (value) => value && typeof value === "object" ? value : null;
|
|
7377
|
+
const verify = asRecord(record.verify);
|
|
7378
|
+
if (!verify || typeof verify.type !== "string" || !verify.type)
|
|
7379
|
+
return null;
|
|
7380
|
+
return {
|
|
7381
|
+
action: asRecord(record.action),
|
|
7382
|
+
decision: asRecord(record.decision),
|
|
7383
|
+
verify: {
|
|
7384
|
+
type: verify.type,
|
|
7385
|
+
reason: typeof verify.reason === "string" ? verify.reason : null,
|
|
7386
|
+
thoughts: typeof verify.thoughts === "string" ? verify.thoughts : null
|
|
7387
|
+
}
|
|
7388
|
+
};
|
|
7389
|
+
}
|
|
7390
|
+
function summarizeActionRecord(record) {
|
|
7316
7391
|
const type = typeof record.type === "string" ? record.type : "step";
|
|
7317
7392
|
if (type === "tap") {
|
|
7318
7393
|
if (typeof record.label === "string" && record.label.trim())
|
|
@@ -7382,6 +7457,16 @@ function actionSummary(action) {
|
|
|
7382
7457
|
}
|
|
7383
7458
|
return type.charAt(0).toUpperCase() + type.slice(1);
|
|
7384
7459
|
}
|
|
7460
|
+
function actionSummary(action) {
|
|
7461
|
+
if (!action || typeof action !== "object")
|
|
7462
|
+
return "Step";
|
|
7463
|
+
const cycle = readStepCycle(action);
|
|
7464
|
+
if (cycle?.action)
|
|
7465
|
+
return summarizeActionRecord(cycle.action);
|
|
7466
|
+
if (cycle?.decision)
|
|
7467
|
+
return summarizeActionRecord(cycle.decision);
|
|
7468
|
+
return summarizeActionRecord(action);
|
|
7469
|
+
}
|
|
7385
7470
|
function optionalTrimmedString(value) {
|
|
7386
7471
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
7387
7472
|
}
|
|
@@ -7489,6 +7574,20 @@ function toReportStatus(status) {
|
|
|
7489
7574
|
return status;
|
|
7490
7575
|
return null;
|
|
7491
7576
|
}
|
|
7577
|
+
function formatStepPhases(phases) {
|
|
7578
|
+
const parts = [
|
|
7579
|
+
`capture ${Math.round(phases.captureMs)}ms`,
|
|
7580
|
+
`screen ${Math.round(phases.screenMs)}ms`,
|
|
7581
|
+
`image ${Math.round(phases.prepareMs)}ms`,
|
|
7582
|
+
`decide ${Math.round(phases.decideMs)}ms`,
|
|
7583
|
+
`action ${Math.round(phases.actionMs)}ms`,
|
|
7584
|
+
`settle ${Math.round(phases.settleMs)}ms`
|
|
7585
|
+
];
|
|
7586
|
+
if (phases.decideRetries > 0) {
|
|
7587
|
+
parts.push(`retries ${phases.decideRetries}`);
|
|
7588
|
+
}
|
|
7589
|
+
return parts.join(" · ");
|
|
7590
|
+
}
|
|
7492
7591
|
function mapCatalogStep(step, screenshotsByStepId) {
|
|
7493
7592
|
const { reason, thoughts } = stepReasoning(step);
|
|
7494
7593
|
return {
|
|
@@ -7497,6 +7596,7 @@ function mapCatalogStep(step, screenshotsByStepId) {
|
|
|
7497
7596
|
summary: actionSummary(step.action),
|
|
7498
7597
|
ok: step.ok,
|
|
7499
7598
|
latencyMs: step.latencyMs,
|
|
7599
|
+
phases: step.phases ?? null,
|
|
7500
7600
|
detail: step.detail,
|
|
7501
7601
|
reason,
|
|
7502
7602
|
thoughts,
|
|
@@ -7530,6 +7630,9 @@ function buildRunReportFromCatalogRun(run, meta = {}, screenshotsByStepId = {})
|
|
|
7530
7630
|
deviceLabel: meta.deviceLabel ?? null,
|
|
7531
7631
|
platform: run.platform,
|
|
7532
7632
|
executionMode: run.executionMode,
|
|
7633
|
+
screenMode: run.screenMode ?? null,
|
|
7634
|
+
lane: run.lane ?? null,
|
|
7635
|
+
laneWarning: run.laneWarning ?? null,
|
|
7533
7636
|
error: run.error,
|
|
7534
7637
|
createdAt: run.createdAt,
|
|
7535
7638
|
startedAt: run.startedAt,
|
|
@@ -7577,6 +7680,11 @@ function pngDataUri(base64) {
|
|
|
7577
7680
|
}
|
|
7578
7681
|
function formatRunReportHtml(doc) {
|
|
7579
7682
|
const accent = statusAccent(doc.status);
|
|
7683
|
+
const screenRows = [
|
|
7684
|
+
...doc.screenMode ? [["Screen", doc.screenMode]] : [],
|
|
7685
|
+
...doc.lane ? [["Lane", doc.lane]] : [],
|
|
7686
|
+
...doc.laneWarning ? [["Lane warning", doc.laneWarning]] : []
|
|
7687
|
+
];
|
|
7580
7688
|
const metaRows = [
|
|
7581
7689
|
["Status", accent.label],
|
|
7582
7690
|
["Source", doc.source === "catalog" ? "Catalog run" : "Manual Inspector"],
|
|
@@ -7584,6 +7692,7 @@ function formatRunReportHtml(doc) {
|
|
|
7584
7692
|
["Device", doc.deviceLabel ?? "—"],
|
|
7585
7693
|
["Platform", doc.platform ?? "—"],
|
|
7586
7694
|
["Mode", doc.executionMode ?? "—"],
|
|
7695
|
+
...screenRows,
|
|
7587
7696
|
["Started", formatWhen(doc.startedAt ?? doc.createdAt)],
|
|
7588
7697
|
["Finished", formatWhen(doc.finishedAt)],
|
|
7589
7698
|
["Duration", formatDuration(doc.startedAt ?? doc.createdAt, doc.finishedAt)],
|
|
@@ -7599,6 +7708,7 @@ function formatRunReportHtml(doc) {
|
|
|
7599
7708
|
const detailBlock = step.detail && step.detail !== step.reason ? `<p class="detail">${escapeHtml(step.detail)}</p>` : "";
|
|
7600
7709
|
const shot = step.screenshotBase64 ? `<img class="shot" alt="Step ${step.index} screenshot" src="${pngDataUri(step.screenshotBase64)}" />` : `<p class="muted">No screenshot</p>`;
|
|
7601
7710
|
const latency = step.latencyMs != null ? `<span class="muted">${step.latencyMs}ms</span>` : "";
|
|
7711
|
+
const phases = step.phases ? `<p class="muted phases">Phases: ${escapeHtml(formatStepPhases(step.phases))}</p>` : "";
|
|
7602
7712
|
const commandBlock = step.command ? `<pre class="command"><code>${escapeHtml(step.command)}</code></pre>` : "";
|
|
7603
7713
|
return `
|
|
7604
7714
|
<article class="step ${step.ok ? "ok" : "fail"}">
|
|
@@ -7610,6 +7720,7 @@ function formatRunReportHtml(doc) {
|
|
|
7610
7720
|
</div>
|
|
7611
7721
|
</header>
|
|
7612
7722
|
${commandBlock}
|
|
7723
|
+
${phases}
|
|
7613
7724
|
${reasonBlock}
|
|
7614
7725
|
${detailBlock}
|
|
7615
7726
|
${thoughtsBlock}
|
|
@@ -7707,6 +7818,9 @@ function formatRunReportMarkdown(doc) {
|
|
|
7707
7818
|
`| Device | ${escapeMd(doc.deviceLabel ?? "—")} |`,
|
|
7708
7819
|
`| Platform | ${escapeMd(doc.platform ?? "—")} |`,
|
|
7709
7820
|
`| Mode | ${escapeMd(doc.executionMode ?? "—")} |`,
|
|
7821
|
+
...doc.screenMode ? [`| Screen | ${escapeMd(doc.screenMode)} |`] : [],
|
|
7822
|
+
...doc.lane ? [`| Lane | ${escapeMd(doc.lane)} |`] : [],
|
|
7823
|
+
...doc.laneWarning ? [`| Lane warning | ${escapeMd(doc.laneWarning)} |`] : [],
|
|
7710
7824
|
`| Started | ${escapeMd(formatWhen(doc.startedAt ?? doc.createdAt))} |`,
|
|
7711
7825
|
`| Finished | ${escapeMd(formatWhen(doc.finishedAt))} |`,
|
|
7712
7826
|
`| Duration | ${escapeMd(formatDuration(doc.startedAt ?? doc.createdAt, doc.finishedAt))} |`,
|
|
@@ -7735,6 +7849,8 @@ function formatRunReportMarkdown(doc) {
|
|
|
7735
7849
|
lines.push(`- Command: \`${step.command.replace(/`/g, "'")}\``);
|
|
7736
7850
|
if (step.latencyMs != null)
|
|
7737
7851
|
lines.push(`- Latency: ${step.latencyMs}ms`);
|
|
7852
|
+
if (step.phases)
|
|
7853
|
+
lines.push(`- Phases: ${formatStepPhases(step.phases)}`);
|
|
7738
7854
|
if (step.reason)
|
|
7739
7855
|
lines.push(`- Reason: ${step.reason}`);
|
|
7740
7856
|
if (step.detail && step.detail !== step.reason)
|
|
@@ -7771,6 +7887,9 @@ function formatRunReportGithubSummary(doc) {
|
|
|
7771
7887
|
`| Device | ${escapeMd(doc.deviceLabel ?? "—")} |`,
|
|
7772
7888
|
`| Platform | ${escapeMd(doc.platform ?? "—")} |`,
|
|
7773
7889
|
`| Mode | ${escapeMd(doc.executionMode ?? "—")} |`,
|
|
7890
|
+
...doc.screenMode ? [`| Screen | ${escapeMd(doc.screenMode)} |`] : [],
|
|
7891
|
+
...doc.lane ? [`| Lane | ${escapeMd(doc.lane)} |`] : [],
|
|
7892
|
+
...doc.laneWarning ? [`| Lane warning | ${escapeMd(doc.laneWarning)} |`] : [],
|
|
7774
7893
|
`| Duration | ${escapeMd(formatDuration(doc.startedAt ?? doc.createdAt, doc.finishedAt))} |`,
|
|
7775
7894
|
`| Run ID | \`${doc.id}\` |`,
|
|
7776
7895
|
""
|
|
@@ -8226,6 +8345,15 @@ class RunnerClient {
|
|
|
8226
8345
|
}, "Action failed");
|
|
8227
8346
|
return actionResponseSchema.parse(json);
|
|
8228
8347
|
}
|
|
8348
|
+
async performActionBatch(request) {
|
|
8349
|
+
const body = actionBatchRequestSchema.parse(request);
|
|
8350
|
+
const json = await this.requestJson("/action/batch", {
|
|
8351
|
+
method: "POST",
|
|
8352
|
+
headers: { "Content-Type": "application/json" },
|
|
8353
|
+
body: JSON.stringify(body)
|
|
8354
|
+
}, "Action batch failed");
|
|
8355
|
+
return actionBatchResponseSchema.parse(json);
|
|
8356
|
+
}
|
|
8229
8357
|
async getStatus() {
|
|
8230
8358
|
const json = await this.requestJson("/status", undefined, "Status failed");
|
|
8231
8359
|
return yoqaStatusResponseSchema.parse(json);
|
|
@@ -8274,10 +8402,369 @@ var {
|
|
|
8274
8402
|
Option,
|
|
8275
8403
|
Help
|
|
8276
8404
|
} = import__.default;
|
|
8405
|
+
|
|
8406
|
+
// ../../services/runner/src/domains/benchmark/accuracy.ts
|
|
8407
|
+
function pointInElement(element, x, y) {
|
|
8408
|
+
return x >= element.x && x <= element.x + element.width && y >= element.y && y <= element.y + element.height;
|
|
8409
|
+
}
|
|
8410
|
+
function tapHitsElement(elements, x, y, expect) {
|
|
8411
|
+
const intended = expect?.id ? elements.filter((el) => el.id === expect.id) : expect?.label ? elements.filter((el) => el.label === expect.label) : elements;
|
|
8412
|
+
return intended.some((el) => pointInElement(el, x, y));
|
|
8413
|
+
}
|
|
8414
|
+
|
|
8415
|
+
// ../../services/runner/src/domains/benchmark/drivers.ts
|
|
8416
|
+
function yoqaBenchmarkDriver(client, options) {
|
|
8417
|
+
return {
|
|
8418
|
+
name: "yoqa",
|
|
8419
|
+
connect: async () => {
|
|
8420
|
+
await client.connectDevice({
|
|
8421
|
+
deviceId: options.deviceId,
|
|
8422
|
+
platform: options.platform,
|
|
8423
|
+
lane: options.lane
|
|
8424
|
+
});
|
|
8425
|
+
},
|
|
8426
|
+
screen: async () => {
|
|
8427
|
+
await client.getScreen();
|
|
8428
|
+
},
|
|
8429
|
+
tapToResult: async (x, y, expect) => {
|
|
8430
|
+
const result = await client.performAction({ kind: "tap", x, y, screenshot: true });
|
|
8431
|
+
const tree = await client.getScreen().catch(() => ({ elements: [] }));
|
|
8432
|
+
const hit = tapHitsElement(tree.elements ?? [], x, y, expect);
|
|
8433
|
+
return {
|
|
8434
|
+
hit,
|
|
8435
|
+
phases: {
|
|
8436
|
+
capture: result.phases?.captureMs ?? 0,
|
|
8437
|
+
action: result.phases?.actionMs ?? 0,
|
|
8438
|
+
settle: result.phases?.settleMs ?? result.screenshot?.waitedMs ?? 0
|
|
8439
|
+
}
|
|
8440
|
+
};
|
|
8441
|
+
},
|
|
8442
|
+
runCase: client.createRun && client.waitForRun && options.appId ? async (caseId) => {
|
|
8443
|
+
const created = await client.createRun?.({
|
|
8444
|
+
appId: options.appId ?? "",
|
|
8445
|
+
caseIds: [caseId],
|
|
8446
|
+
deviceId: options.deviceId,
|
|
8447
|
+
platform: options.platform,
|
|
8448
|
+
executionMode: "agent",
|
|
8449
|
+
screenMode: options.screenMode,
|
|
8450
|
+
lane: options.lane
|
|
8451
|
+
});
|
|
8452
|
+
if (!created)
|
|
8453
|
+
return { passed: false };
|
|
8454
|
+
const finished = await client.waitForRun?.(created.id);
|
|
8455
|
+
const steps = finished?.tests?.reduce((n, test) => n + (test.steps?.length ?? 0), 0);
|
|
8456
|
+
return { passed: finished?.status === "passed", steps };
|
|
8457
|
+
} : undefined,
|
|
8458
|
+
disconnect: async () => {
|
|
8459
|
+
await client.disconnectDevice();
|
|
8460
|
+
}
|
|
8461
|
+
};
|
|
8462
|
+
}
|
|
8463
|
+
|
|
8464
|
+
// ../../services/runner/src/domains/benchmark/scenario.ts
|
|
8465
|
+
async function runLatencyScenario(driver, options) {
|
|
8466
|
+
const clock = options.clock ?? { now: () => Date.now() };
|
|
8467
|
+
const taps = options.suite?.taps ?? [{ x: options.x ?? 500, y: options.y ?? 500 }];
|
|
8468
|
+
const tapToResult = [];
|
|
8469
|
+
const screenRead = [];
|
|
8470
|
+
const coldStart = [];
|
|
8471
|
+
const tapHits = [];
|
|
8472
|
+
const casePasses = [];
|
|
8473
|
+
const stepCounts = [];
|
|
8474
|
+
const captures = [];
|
|
8475
|
+
const actions = [];
|
|
8476
|
+
const settles = [];
|
|
8477
|
+
const t0 = clock.now();
|
|
8478
|
+
await driver.connect();
|
|
8479
|
+
options.advance?.(50);
|
|
8480
|
+
coldStart.push(clock.now() - t0);
|
|
8481
|
+
const tScreen = clock.now();
|
|
8482
|
+
await driver.screen();
|
|
8483
|
+
options.advance?.(20);
|
|
8484
|
+
screenRead.push(clock.now() - tScreen);
|
|
8485
|
+
for (let i = 0;i < options.repeats; i++) {
|
|
8486
|
+
const tap = taps[i % taps.length];
|
|
8487
|
+
if (!tap)
|
|
8488
|
+
continue;
|
|
8489
|
+
const tTap = clock.now();
|
|
8490
|
+
const result = await driver.tapToResult(tap.x, tap.y, {
|
|
8491
|
+
id: tap.id,
|
|
8492
|
+
label: tap.label
|
|
8493
|
+
});
|
|
8494
|
+
options.advance?.(30);
|
|
8495
|
+
tapToResult.push(clock.now() - tTap);
|
|
8496
|
+
if (result.hit != null)
|
|
8497
|
+
tapHits.push(result.hit);
|
|
8498
|
+
if (result.phases?.capture != null)
|
|
8499
|
+
captures.push(result.phases.capture);
|
|
8500
|
+
if (result.phases?.action != null)
|
|
8501
|
+
actions.push(result.phases.action);
|
|
8502
|
+
if (result.phases?.settle != null)
|
|
8503
|
+
settles.push(result.phases.settle);
|
|
8504
|
+
}
|
|
8505
|
+
if (driver.runCase && options.suite) {
|
|
8506
|
+
const times = options.suite.passRepeats;
|
|
8507
|
+
for (let i = 0;i < times; i++) {
|
|
8508
|
+
for (const item of options.suite.cases) {
|
|
8509
|
+
const outcome = await driver.runCase(item.caseId);
|
|
8510
|
+
const passed = typeof outcome === "boolean" ? outcome : outcome.passed;
|
|
8511
|
+
casePasses.push(passed);
|
|
8512
|
+
if (typeof outcome !== "boolean" && outcome.steps != null) {
|
|
8513
|
+
stepCounts.push(outcome.steps);
|
|
8514
|
+
}
|
|
8515
|
+
}
|
|
8516
|
+
}
|
|
8517
|
+
}
|
|
8518
|
+
await driver.disconnect();
|
|
8519
|
+
const phases = driver.name === "yoqa" && (captures.length || actions.length || settles.length) ? {
|
|
8520
|
+
...captures.length ? { capture: captures } : {},
|
|
8521
|
+
...actions.length ? { action: actions } : {},
|
|
8522
|
+
...settles.length ? { settle: settles } : {}
|
|
8523
|
+
} : undefined;
|
|
8524
|
+
return {
|
|
8525
|
+
tool: driver.name,
|
|
8526
|
+
platform: options.platform ?? "ios",
|
|
8527
|
+
...options.screenMode ? { screenMode: options.screenMode } : {},
|
|
8528
|
+
tapToResult,
|
|
8529
|
+
screenRead,
|
|
8530
|
+
coldStart,
|
|
8531
|
+
...tapHits.length ? { tapHits } : {},
|
|
8532
|
+
...casePasses.length ? { casePasses } : {},
|
|
8533
|
+
...stepCounts.length ? { stepCounts } : {},
|
|
8534
|
+
...phases ? { phases } : {}
|
|
8535
|
+
};
|
|
8536
|
+
}
|
|
8537
|
+
|
|
8538
|
+
// ../../services/runner/src/domains/benchmark/suite.ts
|
|
8539
|
+
var BENCHMARK_SUITE_VERSION = 1;
|
|
8540
|
+
var DEFAULT_BENCHMARK_SUITE = {
|
|
8541
|
+
version: BENCHMARK_SUITE_VERSION,
|
|
8542
|
+
repeats: 5,
|
|
8543
|
+
taps: [{ x: 500, y: 500 }],
|
|
8544
|
+
cases: [],
|
|
8545
|
+
passRepeats: 3,
|
|
8546
|
+
arms: ["vision"]
|
|
8547
|
+
};
|
|
8548
|
+
function parseBenchmarkSuite(raw) {
|
|
8549
|
+
if (!raw || typeof raw !== "object") {
|
|
8550
|
+
throw new Error("Benchmark suite must be an object");
|
|
8551
|
+
}
|
|
8552
|
+
const value = raw;
|
|
8553
|
+
const version = Number(value.version);
|
|
8554
|
+
const repeats = Number(value.repeats);
|
|
8555
|
+
const passRepeats = Number(value.passRepeats ?? 3);
|
|
8556
|
+
if (!Number.isInteger(version) || version < 1) {
|
|
8557
|
+
throw new Error("Benchmark suite version must be a positive integer");
|
|
8558
|
+
}
|
|
8559
|
+
if (!Number.isInteger(repeats) || repeats < 1) {
|
|
8560
|
+
throw new Error("Benchmark suite repeats must be a positive integer");
|
|
8561
|
+
}
|
|
8562
|
+
if (!Array.isArray(value.taps) || value.taps.length === 0) {
|
|
8563
|
+
throw new Error("Benchmark suite needs at least one tap");
|
|
8564
|
+
}
|
|
8565
|
+
const taps = value.taps.map((tap, i) => {
|
|
8566
|
+
if (!tap || typeof tap !== "object") {
|
|
8567
|
+
throw new Error(`Benchmark suite tap ${i} is invalid`);
|
|
8568
|
+
}
|
|
8569
|
+
const row = tap;
|
|
8570
|
+
const x = Number(row.x);
|
|
8571
|
+
const y = Number(row.y);
|
|
8572
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
8573
|
+
throw new Error(`Benchmark suite tap ${i} needs x and y`);
|
|
8574
|
+
}
|
|
8575
|
+
return {
|
|
8576
|
+
x,
|
|
8577
|
+
y,
|
|
8578
|
+
...typeof row.id === "string" ? { id: row.id } : {},
|
|
8579
|
+
...typeof row.label === "string" ? { label: row.label } : {}
|
|
8580
|
+
};
|
|
8581
|
+
});
|
|
8582
|
+
const cases = Array.isArray(value.cases) ? value.cases.flatMap((item) => {
|
|
8583
|
+
if (!item || typeof item !== "object")
|
|
8584
|
+
return [];
|
|
8585
|
+
const caseId = item.caseId;
|
|
8586
|
+
return typeof caseId === "string" && caseId.length > 0 ? [{ caseId }] : [];
|
|
8587
|
+
}) : [];
|
|
8588
|
+
return {
|
|
8589
|
+
version,
|
|
8590
|
+
repeats,
|
|
8591
|
+
taps,
|
|
8592
|
+
cases,
|
|
8593
|
+
passRepeats: Number.isInteger(passRepeats) ? passRepeats : 3,
|
|
8594
|
+
arms: parseBenchmarkArms(value.arms)
|
|
8595
|
+
};
|
|
8596
|
+
}
|
|
8597
|
+
function parseBenchmarkArms(raw) {
|
|
8598
|
+
if (raw == null)
|
|
8599
|
+
return ["vision"];
|
|
8600
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
8601
|
+
throw new Error("Benchmark suite arms must be a non-empty list of vision | tree");
|
|
8602
|
+
}
|
|
8603
|
+
const arms = [];
|
|
8604
|
+
for (const item of raw) {
|
|
8605
|
+
if (item !== "vision" && item !== "tree") {
|
|
8606
|
+
throw new Error("Benchmark suite arms must be vision or tree");
|
|
8607
|
+
}
|
|
8608
|
+
if (!arms.includes(item))
|
|
8609
|
+
arms.push(item);
|
|
8610
|
+
}
|
|
8611
|
+
return arms;
|
|
8612
|
+
}
|
|
8613
|
+
|
|
8614
|
+
// ../../services/runner/src/domains/benchmark/summarize.ts
|
|
8615
|
+
function percentile(samples, p) {
|
|
8616
|
+
if (samples.length === 0)
|
|
8617
|
+
return 0;
|
|
8618
|
+
const sorted = [...samples].sort((a, b) => a - b);
|
|
8619
|
+
const rank = Math.ceil(p / 100 * sorted.length) - 1;
|
|
8620
|
+
return sorted[Math.max(0, Math.min(sorted.length - 1, rank))] ?? 0;
|
|
8621
|
+
}
|
|
8622
|
+
function summarizeSamples(samples) {
|
|
8623
|
+
return {
|
|
8624
|
+
p50: percentile(samples, 50),
|
|
8625
|
+
p95: percentile(samples, 95),
|
|
8626
|
+
n: samples.length,
|
|
8627
|
+
samples: [...samples]
|
|
8628
|
+
};
|
|
8629
|
+
}
|
|
8630
|
+
function mean(samples) {
|
|
8631
|
+
if (!samples || samples.length === 0)
|
|
8632
|
+
return;
|
|
8633
|
+
return samples.reduce((sum, n) => sum + n, 0) / samples.length;
|
|
8634
|
+
}
|
|
8635
|
+
function summarizeRate(hits) {
|
|
8636
|
+
if (!hits || hits.length === 0)
|
|
8637
|
+
return;
|
|
8638
|
+
const n = hits.length;
|
|
8639
|
+
const count = hits.filter(Boolean).length;
|
|
8640
|
+
return { hits: count, n, rate: count / n };
|
|
8641
|
+
}
|
|
8642
|
+
function summarizeLatency(input) {
|
|
8643
|
+
return {
|
|
8644
|
+
recordedAt: input.recordedAt,
|
|
8645
|
+
repeats: input.repeats,
|
|
8646
|
+
...input.suiteVersion != null ? { suiteVersion: input.suiteVersion } : {},
|
|
8647
|
+
results: input.runs.map((run) => {
|
|
8648
|
+
const capture = mean(run.phases?.capture);
|
|
8649
|
+
const action = mean(run.phases?.action);
|
|
8650
|
+
const settle = mean(run.phases?.settle);
|
|
8651
|
+
const phases = capture != null || action != null || settle != null ? {
|
|
8652
|
+
...capture != null ? { capture } : {},
|
|
8653
|
+
...action != null ? { action } : {},
|
|
8654
|
+
...settle != null ? { settle } : {}
|
|
8655
|
+
} : undefined;
|
|
8656
|
+
const accuracy = summarizeRate(run.tapHits);
|
|
8657
|
+
const passRate = summarizeRate(run.casePasses);
|
|
8658
|
+
const steps = run.stepCounts?.length ? summarizeSamples(run.stepCounts) : undefined;
|
|
8659
|
+
return {
|
|
8660
|
+
tool: run.tool,
|
|
8661
|
+
platform: run.platform,
|
|
8662
|
+
...run.screenMode ? { screenMode: run.screenMode } : {},
|
|
8663
|
+
metrics: {
|
|
8664
|
+
tapToResult: summarizeSamples(run.tapToResult),
|
|
8665
|
+
screenRead: summarizeSamples(run.screenRead),
|
|
8666
|
+
coldStart: summarizeSamples(run.coldStart)
|
|
8667
|
+
},
|
|
8668
|
+
...accuracy ? { accuracy } : {},
|
|
8669
|
+
...passRate ? { passRate } : {},
|
|
8670
|
+
...steps ? { steps } : {},
|
|
8671
|
+
...phases ? { phases } : {}
|
|
8672
|
+
};
|
|
8673
|
+
})
|
|
8674
|
+
};
|
|
8675
|
+
}
|
|
8676
|
+
function formatLatencyTable(report) {
|
|
8677
|
+
const header = [
|
|
8678
|
+
"tool".padEnd(12),
|
|
8679
|
+
"platform".padEnd(10),
|
|
8680
|
+
"metric".padEnd(14),
|
|
8681
|
+
"p50".padStart(8),
|
|
8682
|
+
"p95".padStart(8),
|
|
8683
|
+
"n".padStart(4)
|
|
8684
|
+
].join(" ");
|
|
8685
|
+
const lines = [header];
|
|
8686
|
+
for (const row of report.results) {
|
|
8687
|
+
const tool = row.screenMode ? `${row.tool}/${row.screenMode}` : row.tool;
|
|
8688
|
+
for (const metric of ["tapToResult", "screenRead", "coldStart"]) {
|
|
8689
|
+
const s = row.metrics[metric];
|
|
8690
|
+
lines.push([
|
|
8691
|
+
tool.padEnd(12),
|
|
8692
|
+
row.platform.padEnd(10),
|
|
8693
|
+
metric.padEnd(14),
|
|
8694
|
+
String(Math.round(s.p50)).padStart(8),
|
|
8695
|
+
String(Math.round(s.p95)).padStart(8),
|
|
8696
|
+
String(s.n).padStart(4)
|
|
8697
|
+
].join(" "));
|
|
8698
|
+
}
|
|
8699
|
+
if (row.accuracy) {
|
|
8700
|
+
lines.push([
|
|
8701
|
+
tool.padEnd(12),
|
|
8702
|
+
row.platform.padEnd(10),
|
|
8703
|
+
"tapAccuracy".padEnd(14),
|
|
8704
|
+
row.accuracy.rate.toFixed(2).padStart(8),
|
|
8705
|
+
"".padStart(8),
|
|
8706
|
+
String(row.accuracy.n).padStart(4)
|
|
8707
|
+
].join(" "));
|
|
8708
|
+
}
|
|
8709
|
+
if (row.passRate) {
|
|
8710
|
+
lines.push([
|
|
8711
|
+
tool.padEnd(12),
|
|
8712
|
+
row.platform.padEnd(10),
|
|
8713
|
+
"casePassRate".padEnd(14),
|
|
8714
|
+
row.passRate.rate.toFixed(2).padStart(8),
|
|
8715
|
+
"".padStart(8),
|
|
8716
|
+
String(row.passRate.n).padStart(4)
|
|
8717
|
+
].join(" "));
|
|
8718
|
+
}
|
|
8719
|
+
if (row.steps) {
|
|
8720
|
+
lines.push([
|
|
8721
|
+
tool.padEnd(12),
|
|
8722
|
+
row.platform.padEnd(10),
|
|
8723
|
+
"stepCount".padEnd(14),
|
|
8724
|
+
String(Math.round(row.steps.p50)).padStart(8),
|
|
8725
|
+
String(Math.round(row.steps.p95)).padStart(8),
|
|
8726
|
+
String(row.steps.n).padStart(4)
|
|
8727
|
+
].join(" "));
|
|
8728
|
+
}
|
|
8729
|
+
if (row.phases) {
|
|
8730
|
+
const parts = Object.entries(row.phases).map(([k, v]) => `${k}=${Math.round(v)}`);
|
|
8731
|
+
lines.push(` phases ${parts.join(" ")}`);
|
|
8732
|
+
}
|
|
8733
|
+
}
|
|
8734
|
+
return `${lines.join(`
|
|
8735
|
+
`)}
|
|
8736
|
+
`;
|
|
8737
|
+
}
|
|
8738
|
+
function recommendScreenDefault(vision, tree) {
|
|
8739
|
+
if (!vision || !tree) {
|
|
8740
|
+
return {
|
|
8741
|
+
screenMode: "vision",
|
|
8742
|
+
reason: "Need both vision and tree arms before flipping the default"
|
|
8743
|
+
};
|
|
8744
|
+
}
|
|
8745
|
+
if (!vision.passRate || !tree.passRate) {
|
|
8746
|
+
return {
|
|
8747
|
+
screenMode: "vision",
|
|
8748
|
+
reason: "No Case pass-rate samples; keep vision-first"
|
|
8749
|
+
};
|
|
8750
|
+
}
|
|
8751
|
+
const visionAcc = vision.accuracy?.rate ?? 0;
|
|
8752
|
+
const treeAcc = tree.accuracy?.rate ?? 0;
|
|
8753
|
+
if (tree.passRate.rate > vision.passRate.rate && treeAcc >= visionAcc) {
|
|
8754
|
+
return {
|
|
8755
|
+
screenMode: "tree",
|
|
8756
|
+
reason: "Tree arm won Case pass rate without losing tap accuracy"
|
|
8757
|
+
};
|
|
8758
|
+
}
|
|
8759
|
+
return {
|
|
8760
|
+
screenMode: "vision",
|
|
8761
|
+
reason: "Tree arm did not beat vision on pass rate and accuracy; keep vision-first"
|
|
8762
|
+
};
|
|
8763
|
+
}
|
|
8277
8764
|
// package.json
|
|
8278
8765
|
var package_default = {
|
|
8279
8766
|
name: "@yoqa/cli",
|
|
8280
|
-
version: "0.3.
|
|
8767
|
+
version: "0.3.29",
|
|
8281
8768
|
description: "Yoqa CLI — drive local mobile QA from the terminal or CI (talks to the Yoqa runner over HTTP)",
|
|
8282
8769
|
license: "UNLICENSED",
|
|
8283
8770
|
type: "module",
|
|
@@ -8295,7 +8782,7 @@ var package_default = {
|
|
|
8295
8782
|
url: "git+https://github.com/Aris-ngoy/qa-agent.git",
|
|
8296
8783
|
directory: "packages/cli"
|
|
8297
8784
|
},
|
|
8298
|
-
homepage: "https://yoqa.
|
|
8785
|
+
homepage: "https://yoqa.arisn.dev/docs/cli",
|
|
8299
8786
|
bugs: {
|
|
8300
8787
|
url: "https://github.com/Aris-ngoy/qa-agent/issues"
|
|
8301
8788
|
},
|
|
@@ -8315,7 +8802,7 @@ var package_default = {
|
|
|
8315
8802
|
health: "bun run src/main.ts health"
|
|
8316
8803
|
},
|
|
8317
8804
|
dependencies: {
|
|
8318
|
-
"@yoqa/runner": "0.3.
|
|
8805
|
+
"@yoqa/runner": "0.3.29"
|
|
8319
8806
|
},
|
|
8320
8807
|
devDependencies: {
|
|
8321
8808
|
"@types/node": "^22.15.30",
|
|
@@ -9166,23 +9653,30 @@ for (const platform of ["ios", "android"]) {
|
|
|
9166
9653
|
}
|
|
9167
9654
|
});
|
|
9168
9655
|
}
|
|
9169
|
-
devices.command("connect").description("Open
|
|
9656
|
+
devices.command("connect").description("Open a Device Session on a device").argument("<deviceId>", "Device UDID / serial").requiredOption("--platform <platform>", "ios | android").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--bundle-id <id>", "iOS bundle id to launch").option("--app-package <id>", "Android application id to launch").option("--lane <lane>", "appium | direct | auto (Android / iOS sim auto → Direct)").option("--json", "Print raw JSON").action(async (deviceId, options) => {
|
|
9170
9657
|
try {
|
|
9171
9658
|
const platform = options.platform;
|
|
9172
9659
|
if (platform !== "ios" && platform !== "android") {
|
|
9173
9660
|
throw new Error("--platform must be ios or android");
|
|
9174
9661
|
}
|
|
9662
|
+
const lane = options.lane;
|
|
9663
|
+
if (lane != null && lane !== "appium" && lane !== "direct" && lane !== "auto") {
|
|
9664
|
+
throw new Error("--lane must be appium, direct, or auto");
|
|
9665
|
+
}
|
|
9175
9666
|
const body = await client(options.baseUrl).connectDevice({
|
|
9176
9667
|
deviceId,
|
|
9177
9668
|
platform,
|
|
9178
9669
|
bundleId: options.bundleId,
|
|
9179
|
-
appPackage: options.appPackage
|
|
9670
|
+
appPackage: options.appPackage,
|
|
9671
|
+
lane
|
|
9180
9672
|
});
|
|
9181
9673
|
if (options.json) {
|
|
9182
9674
|
console.log(JSON.stringify(body, null, 2));
|
|
9183
9675
|
return;
|
|
9184
9676
|
}
|
|
9185
|
-
console.log(`connected ${body.platform} ${body.deviceId}`);
|
|
9677
|
+
console.log(`connected ${body.platform} ${body.deviceId} lane ${body.lane ?? "appium"}`);
|
|
9678
|
+
if (body.laneWarning)
|
|
9679
|
+
console.log(`lane warning: ${body.laneWarning}`);
|
|
9186
9680
|
} catch (error) {
|
|
9187
9681
|
fail("devices connect", error);
|
|
9188
9682
|
}
|
|
@@ -9199,7 +9693,9 @@ devices.command("active").description("Show the currently connected device").opt
|
|
|
9199
9693
|
console.log(JSON.stringify(body, null, 2));
|
|
9200
9694
|
return;
|
|
9201
9695
|
}
|
|
9202
|
-
console.log(`${body.platform} ${body.deviceId}`);
|
|
9696
|
+
console.log(`${body.platform} ${body.deviceId} lane ${body.lane ?? "appium"}`);
|
|
9697
|
+
if (body.laneWarning)
|
|
9698
|
+
console.log(`lane warning: ${body.laneWarning}`);
|
|
9203
9699
|
} catch (error) {
|
|
9204
9700
|
fail("devices active", error);
|
|
9205
9701
|
}
|
|
@@ -9244,7 +9740,19 @@ program2.command("screenshot").description("Capture a screenshot from the active
|
|
|
9244
9740
|
});
|
|
9245
9741
|
var action = program2.command("action").description("Perform an action on the active device");
|
|
9246
9742
|
function addActionOptions(cmd) {
|
|
9247
|
-
return cmd.option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("-d, --description <text>", "Ground target by natural language").option("--label <text>", "Tap/focus by accessibility label (cleaned screen tree)").option("--id <text>", "Tap/focus by resource-id / accessibility id").option("--x <n>", "X in 0–1000", (v) => Number(v)).option("--y <n>", "Y in 0–1000", (v) => Number(v)).option("--x2 <n>", "End X in 0–1000", (v) => Number(v)).option("--y2 <n>", "End Y in 0–1000", (v) => Number(v)).option("--duration <ms>", "Gesture duration ms (long-press for tap)", (v) => Number(v)).option("--double", "Double-tap (tap only)").option("--text <text>", "Text to type").option("--app-id <id>", "Bundle id / application id").option("--url <url>", "URL to open").option("--seconds <n>", "Background seconds", (v) => Number(v));
|
|
9743
|
+
return cmd.option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("-d, --description <text>", "Ground target by natural language").option("--label <text>", "Tap/focus by accessibility label (cleaned screen tree)").option("--id <text>", "Tap/focus by resource-id / accessibility id").option("--x <n>", "X in 0–1000", (v) => Number(v)).option("--y <n>", "Y in 0–1000", (v) => Number(v)).option("--x2 <n>", "End X in 0–1000", (v) => Number(v)).option("--y2 <n>", "End Y in 0–1000", (v) => Number(v)).option("--duration <ms>", "Gesture duration ms (long-press for tap)", (v) => Number(v)).option("--double", "Double-tap (tap only)").option("--text <text>", "Text to type").option("--app-id <id>", "Bundle id / application id").option("--url <url>", "URL to open").option("--seconds <n>", "Background seconds", (v) => Number(v)).option("--screenshot", "Wait for the screen to settle, then return the result screenshot (and a copy marking where the tap or swipe landed)").option("--settle <ms>", "With --screenshot: longest wait for the screen to settle (default 1500)", (v) => Number(v)).option("--full", "With --screenshot: return the raw Result screenshot instead of the Agent image").option("--scale <n>", "With --screenshot: Agent image scale 0.01–1 (default: long edge ~1000 px)", (v) => Number(v));
|
|
9744
|
+
}
|
|
9745
|
+
function printResultScreenshot(shot) {
|
|
9746
|
+
console.log(`screenshot ${shot.path}`);
|
|
9747
|
+
if (shot.rawPath)
|
|
9748
|
+
console.log(`raw ${shot.rawPath}`);
|
|
9749
|
+
if (shot.annotatedPath)
|
|
9750
|
+
console.log(`marked ${shot.annotatedPath}`);
|
|
9751
|
+
console.log(`settled ${shot.settled} (${Math.round(shot.waitedMs)}ms)`);
|
|
9752
|
+
console.log(`changed ${shot.changed === null ? "unknown (screen still animating)" : shot.changed}`);
|
|
9753
|
+
}
|
|
9754
|
+
function printActionPhases(phases) {
|
|
9755
|
+
console.log(`phases capture=${phases.captureMs} action=${phases.actionMs} settle=${phases.settleMs}`);
|
|
9248
9756
|
}
|
|
9249
9757
|
for (const kind of [
|
|
9250
9758
|
"tap",
|
|
@@ -9273,7 +9781,11 @@ for (const kind of [
|
|
|
9273
9781
|
description: options.description,
|
|
9274
9782
|
appId: options.appId,
|
|
9275
9783
|
url: options.url,
|
|
9276
|
-
seconds: options.seconds
|
|
9784
|
+
seconds: options.seconds,
|
|
9785
|
+
screenshot: options.screenshot === true ? true : undefined,
|
|
9786
|
+
settleMs: options.settle,
|
|
9787
|
+
fullImage: options.full === true ? true : undefined,
|
|
9788
|
+
imageScale: options.scale
|
|
9277
9789
|
});
|
|
9278
9790
|
if (options.json) {
|
|
9279
9791
|
console.log(JSON.stringify(body, null, 2));
|
|
@@ -9283,6 +9795,10 @@ for (const kind of [
|
|
|
9283
9795
|
if (body.resolved?.x != null && body.resolved?.y != null) {
|
|
9284
9796
|
console.log(`resolved ${body.resolved.x},${body.resolved.y}`);
|
|
9285
9797
|
}
|
|
9798
|
+
if (body.screenshot)
|
|
9799
|
+
printResultScreenshot(body.screenshot);
|
|
9800
|
+
if (body.phases)
|
|
9801
|
+
printActionPhases(body.phases);
|
|
9286
9802
|
} catch (error) {
|
|
9287
9803
|
fail(`action ${kind}`, error);
|
|
9288
9804
|
}
|
|
@@ -9292,17 +9808,61 @@ addActionOptions(action.command("alert").description("Accept or dismiss a system
|
|
|
9292
9808
|
try {
|
|
9293
9809
|
const body = await client(String(options.baseUrl)).performAction({
|
|
9294
9810
|
kind: "alert",
|
|
9295
|
-
alertAction: options.dismiss ? "dismiss" : "accept"
|
|
9811
|
+
alertAction: options.dismiss ? "dismiss" : "accept",
|
|
9812
|
+
screenshot: options.screenshot === true ? true : undefined,
|
|
9813
|
+
settleMs: options.settle,
|
|
9814
|
+
fullImage: options.full === true ? true : undefined,
|
|
9815
|
+
imageScale: options.scale
|
|
9296
9816
|
});
|
|
9297
9817
|
if (options.json) {
|
|
9298
9818
|
console.log(JSON.stringify(body, null, 2));
|
|
9299
9819
|
return;
|
|
9300
9820
|
}
|
|
9301
9821
|
console.log(`ok ${body.kind}`);
|
|
9822
|
+
if (body.screenshot)
|
|
9823
|
+
printResultScreenshot(body.screenshot);
|
|
9302
9824
|
} catch (error) {
|
|
9303
9825
|
fail("action alert", error);
|
|
9304
9826
|
}
|
|
9305
9827
|
});
|
|
9828
|
+
addActionOptions(action.command("batch").description("Run known Actions from a JSON file or stdin (one Settle and Result screenshot at the end). Description-grounded steps are rejected.").argument("[file]", "JSON file of { steps: Action[] } or an Action array. Omit to read stdin.")).action(async (file, options) => {
|
|
9829
|
+
try {
|
|
9830
|
+
const raw = file ? await readFile2(file, "utf8") : await new Promise((resolve, reject) => {
|
|
9831
|
+
const chunks = [];
|
|
9832
|
+
process.stdin.on("data", (chunk) => {
|
|
9833
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
9834
|
+
});
|
|
9835
|
+
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
9836
|
+
process.stdin.on("error", reject);
|
|
9837
|
+
});
|
|
9838
|
+
const parsed = JSON.parse(raw);
|
|
9839
|
+
const steps = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && ("steps" in parsed) ? parsed.steps : null;
|
|
9840
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
9841
|
+
throw new Error("Batch JSON must be an Action array or { steps: Action[] }");
|
|
9842
|
+
}
|
|
9843
|
+
const body = await client(String(options.baseUrl)).performActionBatch({
|
|
9844
|
+
steps,
|
|
9845
|
+
settleMs: options.settle,
|
|
9846
|
+
fullImage: options.full === true ? true : undefined,
|
|
9847
|
+
imageScale: options.scale
|
|
9848
|
+
});
|
|
9849
|
+
if (options.json) {
|
|
9850
|
+
console.log(JSON.stringify(body, null, 2));
|
|
9851
|
+
return;
|
|
9852
|
+
}
|
|
9853
|
+
console.log(`batch ${body.ok ? "ok" : "failed"} ${body.completed}/${body.total}`);
|
|
9854
|
+
if (body.failedIndex != null)
|
|
9855
|
+
console.log(`failedIndex ${body.failedIndex}`);
|
|
9856
|
+
if (body.error)
|
|
9857
|
+
console.log(body.error);
|
|
9858
|
+
if (body.screenshot)
|
|
9859
|
+
printResultScreenshot(body.screenshot);
|
|
9860
|
+
if (!body.ok)
|
|
9861
|
+
process.exitCode = 1;
|
|
9862
|
+
} catch (error) {
|
|
9863
|
+
fail("action batch", error);
|
|
9864
|
+
}
|
|
9865
|
+
});
|
|
9306
9866
|
var setup = program2.command("setup").description("Install Appium and the platform driver (xcuitest / uiautomator2)");
|
|
9307
9867
|
setup.command("ios").description("Ensure Appium + xcuitest; optionally build and install WDA on a physical device").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("--device <udid>", "Physical device UDID to install WebDriverAgent on").option("--kind <kind>", "Device kind: physical | simulator", "physical").option("--xcode <path>", "Xcode Contents/Developer path (DEVELOPER_DIR)").option("--team <teamId>", "Apple Development team ID").option("--identity <name>", 'Codesigning identity name, e.g. "Apple Development: …"').option("--force", "Force a full WebDriverAgent rebuild even when prep is reusable").action(async (options) => {
|
|
9308
9868
|
try {
|
|
@@ -9588,13 +10148,14 @@ appsCmd.command("get").argument("<app>", "App prefix or id").option("--base-url
|
|
|
9588
10148
|
console.log(`prefix: ${app.prefix}`);
|
|
9589
10149
|
console.log(`name: ${app.name}`);
|
|
9590
10150
|
console.log(`app_context: ${app.context || "(empty)"}`);
|
|
10151
|
+
console.log(`app_knowledge: ${app.knowledge || "(empty)"}`);
|
|
9591
10152
|
console.log(`ios_bundle_id: ${app.iosBundleId || "(empty)"}`);
|
|
9592
10153
|
console.log(`android_application_id: ${app.androidApplicationId || "(empty)"}`);
|
|
9593
10154
|
} catch (error) {
|
|
9594
10155
|
fail("apps get", error);
|
|
9595
10156
|
}
|
|
9596
10157
|
});
|
|
9597
|
-
appsCmd.command("update").argument("<app>", "App prefix or id").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--name <name>", "Display name").option("--prefix <prefix>", "CLI prefix").option("--context <text>", "App context / shared rules").option("--ios-bundle-id <id>", "iOS bundle id").option("--android-application-id <id>", "Android application id").option("--json", "Print raw JSON").action(async (appArg, options) => {
|
|
10158
|
+
appsCmd.command("update").argument("<app>", "App prefix or id").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--name <name>", "Display name").option("--prefix <prefix>", "CLI prefix").option("--context <text>", "App context / shared rules").option("--knowledge <text>", "App knowledge notes injected into agent runs").option("--ios-bundle-id <id>", "iOS bundle id").option("--android-application-id <id>", "Android application id").option("--json", "Print raw JSON").action(async (appArg, options) => {
|
|
9598
10159
|
try {
|
|
9599
10160
|
const c = client(options.baseUrl);
|
|
9600
10161
|
const resolved = await resolveAppId(c, appArg);
|
|
@@ -9602,6 +10163,7 @@ appsCmd.command("update").argument("<app>", "App prefix or id").option("--base-u
|
|
|
9602
10163
|
name: options.name,
|
|
9603
10164
|
prefix: options.prefix,
|
|
9604
10165
|
context: options.context,
|
|
10166
|
+
knowledge: options.knowledge,
|
|
9605
10167
|
iosBundleId: options.iosBundleId,
|
|
9606
10168
|
androidApplicationId: options.androidApplicationId
|
|
9607
10169
|
});
|
|
@@ -9927,7 +10489,7 @@ runsCmd.command("delete").argument("<runId>", "Run id").option("--base-url <url>
|
|
|
9927
10489
|
fail("runs delete", error);
|
|
9928
10490
|
}
|
|
9929
10491
|
});
|
|
9930
|
-
runsCmd.command("create").argument("<app>", "App prefix or id").requiredOption("--cases <numbers>", "Comma-separated case numbers, e.g. 1,2,5").option("--device <id>", "Device id (defaults to active session)").option("--platform <platform>", "ios | android (defaults to active session)").option("--build-id <id>", "Registered build id to install before run").option("--build-path <path>", "Absolute build path to register + install").option("--mode <mode>", "auto | script | agent (default: auto)").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("--wait", "Wait until the run finishes (passed / errored / cancelled)").option("--timeout <seconds>", "Wait timeout in seconds (default: 1800)", String(DEFAULT_RUN_WAIT_TIMEOUT_SECONDS)).option("--github-output", "Write run_id and status to $GITHUB_OUTPUT").option("--no-github-output", "Do not write GitHub step outputs").action(async (appArg, options) => {
|
|
10492
|
+
runsCmd.command("create").argument("<app>", "App prefix or id").requiredOption("--cases <numbers>", "Comma-separated case numbers, e.g. 1,2,5").option("--device <id>", "Device id (defaults to active session)").option("--platform <platform>", "ios | android (defaults to active session)").option("--build-id <id>", "Registered build id to install before run").option("--build-path <path>", "Absolute build path to register + install").option("--mode <mode>", "auto | script | agent (default: auto)").option("--screen-mode <mode>", "vision | tree — what the agent sees: screenshot only, or also the accessibility tree (default: vision)").option("--lane <lane>", "appium | direct | auto — Android / iOS sim auto → Direct").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("--wait", "Wait until the run finishes (passed / errored / cancelled)").option("--timeout <seconds>", "Wait timeout in seconds (default: 1800)", String(DEFAULT_RUN_WAIT_TIMEOUT_SECONDS)).option("--github-output", "Write run_id and status to $GITHUB_OUTPUT").option("--no-github-output", "Do not write GitHub step outputs").action(async (appArg, options) => {
|
|
9931
10493
|
try {
|
|
9932
10494
|
const c = client(options.baseUrl);
|
|
9933
10495
|
const resolved = await resolveAppId(c, appArg);
|
|
@@ -9959,6 +10521,14 @@ runsCmd.command("create").argument("<app>", "App prefix or id").requiredOption("
|
|
|
9959
10521
|
if (options.mode && !executionMode) {
|
|
9960
10522
|
throw new Error("--mode must be auto, script, or agent");
|
|
9961
10523
|
}
|
|
10524
|
+
const screenMode = options.screenMode === "vision" || options.screenMode === "tree" ? options.screenMode : undefined;
|
|
10525
|
+
if (options.screenMode && !screenMode) {
|
|
10526
|
+
throw new Error("--screen-mode must be vision or tree");
|
|
10527
|
+
}
|
|
10528
|
+
const lane = options.lane === "appium" || options.lane === "direct" || options.lane === "auto" ? options.lane : undefined;
|
|
10529
|
+
if (options.lane && !lane) {
|
|
10530
|
+
throw new Error("--lane must be appium, direct, or auto");
|
|
10531
|
+
}
|
|
9962
10532
|
let run = await c.createRun({
|
|
9963
10533
|
appId: resolved.id,
|
|
9964
10534
|
caseIds,
|
|
@@ -9966,7 +10536,9 @@ runsCmd.command("create").argument("<app>", "App prefix or id").requiredOption("
|
|
|
9966
10536
|
platform,
|
|
9967
10537
|
buildId: options.buildId,
|
|
9968
10538
|
buildPath: options.buildPath,
|
|
9969
|
-
executionMode
|
|
10539
|
+
executionMode,
|
|
10540
|
+
screenMode,
|
|
10541
|
+
lane
|
|
9970
10542
|
});
|
|
9971
10543
|
if (options.wait) {
|
|
9972
10544
|
try {
|
|
@@ -10145,6 +10717,58 @@ scriptCmd.command("run").argument("<file>", "Path to a .yoqa.json / CaseScript J
|
|
|
10145
10717
|
fail("script run", error);
|
|
10146
10718
|
}
|
|
10147
10719
|
});
|
|
10720
|
+
program2.command("benchmark").description("Compare Yoqa (and Argent, when installed) tap-to-result latency on one device").requiredOption("--device <id>", "Device UDID / serial").requiredOption("--platform <platform>", "ios | android").option("--repeats <n>", "Tap-to-result repeats (default: suite repeats)", (v) => Number(v)).option("--suite <path>", "Versioned suite JSON (default: built-in v1)").option("--tools <list>", "Comma-separated: yoqa,argent (default: yoqa)", "yoqa").option("--lane <lane>", "Yoqa Lane: appium | direct | auto", "auto").option("--arms <list>", "Yoqa Screen arms: vision,tree (default: suite arms)").option("--app <id>", "Catalog App id for Case arms (required when the suite has cases)").option("--out <path>", "Write the JSON report to this path").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print JSON instead of the table").action(async (options) => {
|
|
10721
|
+
try {
|
|
10722
|
+
const platform = options.platform;
|
|
10723
|
+
if (platform !== "ios" && platform !== "android") {
|
|
10724
|
+
throw new Error("--platform must be ios or android");
|
|
10725
|
+
}
|
|
10726
|
+
const lane = options.lane === "appium" || options.lane === "direct" || options.lane === "auto" ? options.lane : undefined;
|
|
10727
|
+
if (options.lane && !lane)
|
|
10728
|
+
throw new Error("--lane must be appium, direct, or auto");
|
|
10729
|
+
const suite = options.suite ? parseBenchmarkSuite(JSON.parse(await readFile2(options.suite, "utf8"))) : DEFAULT_BENCHMARK_SUITE;
|
|
10730
|
+
const arms = options.arms ? parseBenchmarkArms(options.arms.split(",").map((item) => item.trim())) : suite.arms;
|
|
10731
|
+
const repeats = options.repeats && options.repeats > 0 ? options.repeats : suite.repeats;
|
|
10732
|
+
const tools = options.tools.split(",").map((t) => t.trim());
|
|
10733
|
+
const c = client(options.baseUrl);
|
|
10734
|
+
const runs = [];
|
|
10735
|
+
if (tools.includes("yoqa")) {
|
|
10736
|
+
for (const screenMode of arms) {
|
|
10737
|
+
runs.push(await runLatencyScenario(yoqaBenchmarkDriver(c, {
|
|
10738
|
+
deviceId: options.device,
|
|
10739
|
+
platform,
|
|
10740
|
+
lane,
|
|
10741
|
+
screenMode,
|
|
10742
|
+
appId: options.app
|
|
10743
|
+
}), { repeats, platform, suite, screenMode }));
|
|
10744
|
+
}
|
|
10745
|
+
}
|
|
10746
|
+
if (tools.includes("argent")) {
|
|
10747
|
+
console.error("Argent driver: install the argent CLI and re-run; skipping for now.");
|
|
10748
|
+
}
|
|
10749
|
+
const report = summarizeLatency({
|
|
10750
|
+
recordedAt: new Date().toISOString(),
|
|
10751
|
+
repeats,
|
|
10752
|
+
suiteVersion: suite.version,
|
|
10753
|
+
runs
|
|
10754
|
+
});
|
|
10755
|
+
const recommendation = recommendScreenDefault(report.results.find((row) => row.screenMode === "vision"), report.results.find((row) => row.screenMode === "tree"));
|
|
10756
|
+
if (options.out) {
|
|
10757
|
+
await writeFile3(options.out, `${JSON.stringify({ ...report, recommendation }, null, 2)}
|
|
10758
|
+
`);
|
|
10759
|
+
}
|
|
10760
|
+
if (options.json)
|
|
10761
|
+
console.log(JSON.stringify({ ...report, recommendation }, null, 2));
|
|
10762
|
+
else {
|
|
10763
|
+
console.log(formatLatencyTable(report));
|
|
10764
|
+
if (arms.includes("tree")) {
|
|
10765
|
+
console.log(`default ${recommendation.screenMode} — ${recommendation.reason}`);
|
|
10766
|
+
}
|
|
10767
|
+
}
|
|
10768
|
+
} catch (error) {
|
|
10769
|
+
fail("benchmark", error);
|
|
10770
|
+
}
|
|
10771
|
+
});
|
|
10148
10772
|
function buildProgram() {
|
|
10149
10773
|
return program2;
|
|
10150
10774
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yoqa/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.29",
|
|
4
4
|
"description": "Yoqa CLI — drive local mobile QA from the terminal or CI (talks to the Yoqa runner over HTTP)",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"url": "git+https://github.com/Aris-ngoy/qa-agent.git",
|
|
19
19
|
"directory": "packages/cli"
|
|
20
20
|
},
|
|
21
|
-
"homepage": "https://yoqa.
|
|
21
|
+
"homepage": "https://yoqa.arisn.dev/docs/cli",
|
|
22
22
|
"bugs": {
|
|
23
23
|
"url": "https://github.com/Aris-ngoy/qa-agent/issues"
|
|
24
24
|
},
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"health": "bun run src/main.ts health"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@yoqa/runner": "0.3.
|
|
41
|
+
"@yoqa/runner": "0.3.29"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/node": "^22.15.30",
|