@keepsnappy/agent 0.1.0-alpha.16 → 0.1.0-alpha.17
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 +3 -2
- package/dist/appium-driver.js +71 -6
- package/dist/keepsnappy.js +716 -194
- package/examples/farm/workflows/keepsnappy-measure.yml +3 -1
- package/examples/github-actions.yml +3 -1
- package/package.json +1 -1
package/dist/keepsnappy.js
CHANGED
|
@@ -212,6 +212,15 @@ class ConfigError extends Error {
|
|
|
212
212
|
this.name = "ConfigError";
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
|
+
|
|
216
|
+
class UsageError extends ConfigError {
|
|
217
|
+
constructor(lines) {
|
|
218
|
+
super(lines);
|
|
219
|
+
this.message = lines.join(`
|
|
220
|
+
`);
|
|
221
|
+
this.name = "UsageError";
|
|
222
|
+
}
|
|
223
|
+
}
|
|
215
224
|
var TITLE_MAX = 200;
|
|
216
225
|
var DESCRIPTION_MAX = 2000;
|
|
217
226
|
var TOP = ["project", "app", "platform", "device", "repetitions", "flows", "steps", "output", "timeout_seconds"];
|
|
@@ -5171,7 +5180,7 @@ async function call(f, base, path, headers, body, method = "POST") {
|
|
|
5171
5180
|
try {
|
|
5172
5181
|
return text ? JSON.parse(text) : {};
|
|
5173
5182
|
} catch {
|
|
5174
|
-
|
|
5183
|
+
throw new UploadError(`${method} ${path} -> ${res.status}, the answer is not JSON (is the URL the KeepSnappy API?): ${text.slice(0, 200)}`);
|
|
5175
5184
|
}
|
|
5176
5185
|
}
|
|
5177
5186
|
async function uploadRuns(deps, outDir, runs, opts) {
|
|
@@ -5276,6 +5285,7 @@ var STATE_WORDS = {
|
|
|
5276
5285
|
var INFO_WORDS = { heat: "Phone heated up" };
|
|
5277
5286
|
var isWorseState = (state) => state === "worse";
|
|
5278
5287
|
var NOT_BLOCKING = "not blocking";
|
|
5288
|
+
var badgedWord = (word, blocking) => word === STATE_WORDS.worse && blocking === false ? `${word} (${NOT_BLOCKING})` : word;
|
|
5279
5289
|
|
|
5280
5290
|
// packages/present/dist/src/values.js
|
|
5281
5291
|
var NO_VALUE = "–";
|
|
@@ -5348,6 +5358,39 @@ var JOURNEY_ROW_WORDS = {
|
|
|
5348
5358
|
notInRun: upperFirst(GONE_TAILS.journey.notInRun),
|
|
5349
5359
|
removed: upperFirst(GONE_TAILS.journey.removed)
|
|
5350
5360
|
};
|
|
5361
|
+
var NEW_IN_PR_CHIP = "New in this PR";
|
|
5362
|
+
var placeNamesChangedTitle = (sdk) => `Place names changed with SDK ${sdk}`;
|
|
5363
|
+
var restartRowText = (sdk, mainSessions, needed) => `${placeNamesChangedTitle(sdk)}: ${restartContextText(mainSessions, needed)}`;
|
|
5364
|
+
var restartContextText = (mainSessions, needed) => `main has ${mainSessions} of ${needed} sessions on the new names`;
|
|
5365
|
+
function removedRowsText(n, page) {
|
|
5366
|
+
return { lead: `${plural(n, "component", "components")} ${GONE_TAILS[page].removed}:`, rest: " not counted." };
|
|
5367
|
+
}
|
|
5368
|
+
function notInRunRowsText(c, page) {
|
|
5369
|
+
const what = [c.screens ? plural(c.screens, "screen", "screens") : "", c.places ? `${c.places} ${c.places === 1 ? "tap or scroll" : "taps and scrolls"}` : ""].filter(Boolean).join(" and ");
|
|
5370
|
+
const them = c.screens + c.places === 1 ? "it" : "them";
|
|
5371
|
+
return { lead: `${what} ${GONE_TAILS[page].notInRun}:`, rest: ` the ${page === "journey" ? "newest session" : "journey"} did not reach ${them}; not counted.` };
|
|
5372
|
+
}
|
|
5373
|
+
var OUTSIDE_APP_NAME = "Outside app components";
|
|
5374
|
+
var OTHER_RENDER_WORK_NAME = "Other render work";
|
|
5375
|
+
var plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
5376
|
+
var rolledUpText = (rolledUpMs, totalMs) => `From libraries: ${valueText("self_ms", rolledUpMs)} of ${valueText("self_ms", totalMs)}`;
|
|
5377
|
+
var libraryLineText = (l) => `${l.name} · ${l.renders} rendered · ${valueText("self_ms", l.ms)}`;
|
|
5378
|
+
var rolledUpRestText = (rest) => `${plural(rest.n, "other library", "other libraries")} · ${valueText("self_ms", rest.ms)}`;
|
|
5379
|
+
var measureWords = (measure) => isMeasureId(measure) ? MEASURES[measure].label : `component ${BREAKDOWN_MEASURES[measure].label}`;
|
|
5380
|
+
var TIER_STATE = { worse: "worse", slight: "warning", better: "better", same: "no-change" };
|
|
5381
|
+
function leadBody(subject, measure, main, pr) {
|
|
5382
|
+
const unit = shownUnit(isMeasureId(measure) ? MEASURES[measure].unit : BREAKDOWN_MEASURES[measure].unit).unit;
|
|
5383
|
+
const to = valueText(measure, pr);
|
|
5384
|
+
const from = valueText(measure, main);
|
|
5385
|
+
const bare = unit && from.endsWith(` ${unit}`) && to.endsWith(` ${unit}`) ? from.slice(0, from.length - unit.length - 1) : from;
|
|
5386
|
+
return `${subject}, ${measureWords(measure)} ${bare} → ${to}`;
|
|
5387
|
+
}
|
|
5388
|
+
var screenLeadText = (tier, subject, measure, main, pr) => `${STATE_WORDS[TIER_STATE[tier]]}: ${leadBody(subject, measure, main, pr)}`;
|
|
5389
|
+
var FOR_INFORMATION = "For information";
|
|
5390
|
+
var informationLeadText = (subject, measure, main, pr) => `${FOR_INFORMATION}: ${leadBody(subject, measure, main, pr)}`;
|
|
5391
|
+
function foldedText(n, rows, page) {
|
|
5392
|
+
return `${n} ${GONE_TAILS[page][rows === "components" ? "removed" : "notInRun"]}`;
|
|
5393
|
+
}
|
|
5351
5394
|
function placeShownName(p, o = {}) {
|
|
5352
5395
|
const target = p.target ? componentShownName(p.target.name, p.target.file) : libraryViewName(p.kind, p.host ?? "");
|
|
5353
5396
|
const named = p.target && p.parent ? `${target} in ${componentShownName(p.parent.name, p.parent.file)}${p.k ? `, ${ordinal(p.k)}` : ""}` : target;
|
|
@@ -5355,7 +5398,7 @@ function placeShownName(p, o = {}) {
|
|
|
5355
5398
|
}
|
|
5356
5399
|
var libraryViewName = (kind, hostType) => `${kind === "drag" ? "Scroll" : "Tap"} outside app components (${hostType})`;
|
|
5357
5400
|
function componentShownName(name, file) {
|
|
5358
|
-
if (name !== "default")
|
|
5401
|
+
if (name !== "default" || !file)
|
|
5359
5402
|
return name;
|
|
5360
5403
|
const path = file.replace(/\.[^.\/]+$/, "");
|
|
5361
5404
|
const route = /^(?:src\/)?app\/(.*)$/.exec(path)?.[1];
|
|
@@ -5394,7 +5437,20 @@ var BACKGROUND_SOURCES = {
|
|
|
5394
5437
|
touch: "work of a tap outside its window",
|
|
5395
5438
|
input: "work of typing outside its window"
|
|
5396
5439
|
};
|
|
5440
|
+
var OPEN_WORDS = { first: "first time", repeat: "again" };
|
|
5397
5441
|
var OPEN_MARK = /#(1|repeat)$/;
|
|
5442
|
+
var isInputKind = (kind) => kind === "press" || kind === "drag";
|
|
5443
|
+
function placeOpen(id) {
|
|
5444
|
+
const ref = placeRef(id);
|
|
5445
|
+
if (!isInputKind(ref.kind))
|
|
5446
|
+
return null;
|
|
5447
|
+
const m = OPEN_MARK.exec(ref.stem);
|
|
5448
|
+
return m ? m[1] === "repeat" ? "repeat" : "first" : null;
|
|
5449
|
+
}
|
|
5450
|
+
var placeOpenWord = (id) => {
|
|
5451
|
+
const open = placeOpen(id);
|
|
5452
|
+
return open ? OPEN_WORDS[open] : null;
|
|
5453
|
+
};
|
|
5398
5454
|
function placeIdShownName(id, o = {}) {
|
|
5399
5455
|
const p = placeParts(id);
|
|
5400
5456
|
if (!p)
|
|
@@ -5438,6 +5494,14 @@ function placeKindWord(id) {
|
|
|
5438
5494
|
const { kind } = placeRef(id);
|
|
5439
5495
|
return kind === "other" ? "Window" : KIND_WORD[kind];
|
|
5440
5496
|
}
|
|
5497
|
+
function windowQualified(id) {
|
|
5498
|
+
const word = placeOpenWord(id);
|
|
5499
|
+
const label = placeLabel(id);
|
|
5500
|
+
if (!word)
|
|
5501
|
+
return label;
|
|
5502
|
+
const after = placeRef(id).segment === "after3s" ? ` · ${PLACE_KIND_WORDS.after3s}` : "";
|
|
5503
|
+
return `${after ? label.slice(0, -after.length) : label} (${word})${after}`;
|
|
5504
|
+
}
|
|
5441
5505
|
var ELEMENT_SEPARATORS = /(?<!\d)\.|\.(?!\d)|[_-]+/g;
|
|
5442
5506
|
var elementWords = (testId) => testId.replace(ELEMENT_SEPARATORS, " ").replace(/\s+/g, " ").trim();
|
|
5443
5507
|
function placeLabel(id, screen) {
|
|
@@ -5456,27 +5520,33 @@ function placeLabel(id, screen) {
|
|
|
5456
5520
|
const colon = name.indexOf(": ");
|
|
5457
5521
|
return `${colon > 0 && ref.kind !== "other" ? `${name.slice(0, colon)} · ${name.slice(colon + 2)}` : name}${after}`;
|
|
5458
5522
|
}
|
|
5459
|
-
// packages/present/dist/src/
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
}
|
|
5471
|
-
|
|
5523
|
+
// packages/present/dist/src/placeNames.js
|
|
5524
|
+
function shortestPaths(files) {
|
|
5525
|
+
const out = new Map;
|
|
5526
|
+
for (const file of files) {
|
|
5527
|
+
const parts = file.split("/");
|
|
5528
|
+
let shown = file;
|
|
5529
|
+
for (let k = 1;k <= parts.length; k++) {
|
|
5530
|
+
const tail = parts.slice(-k).join("/");
|
|
5531
|
+
if (!files.some((f) => f !== file && f.split("/").slice(-k).join("/") === tail)) {
|
|
5532
|
+
shown = tail;
|
|
5533
|
+
break;
|
|
5534
|
+
}
|
|
5535
|
+
}
|
|
5536
|
+
out.set(file, shown);
|
|
5537
|
+
}
|
|
5538
|
+
return out;
|
|
5539
|
+
}
|
|
5472
5540
|
// packages/present/dist/src/stability.js
|
|
5473
5541
|
var UNSTABLE_WORDS = {
|
|
5474
5542
|
short: "main varies too much here",
|
|
5475
5543
|
label: `${STATE_WORDS["cant-tell"]} · main varies too much here`,
|
|
5476
5544
|
line: "Main varies too much here to tell",
|
|
5477
5545
|
ratioTip: "1.0 = main’s run-to-run noise alone would pass the warning step about once in 100 comparisons.",
|
|
5546
|
+
note: "This place varies a lot between runs. Worth checking why: it may be in the code.",
|
|
5478
5547
|
what: "Main’s own runs vary too much here to tell, so a comparison there would mostly measure noise. It is shown with its values, never judged, never blocks and is never counted. It is judged again once main settles."
|
|
5479
5548
|
};
|
|
5549
|
+
|
|
5480
5550
|
// packages/present/dist/src/present.js
|
|
5481
5551
|
var ASPECT_WORDS = {
|
|
5482
5552
|
renderer: "React renderer",
|
|
@@ -5493,6 +5563,127 @@ var ASPECT_WORDS = {
|
|
|
5493
5563
|
endGc: "memory clean-up at the journey end",
|
|
5494
5564
|
cpuProfiling: "CPU profiling"
|
|
5495
5565
|
};
|
|
5566
|
+
var measuredDifferentlyText = (aspects) => `measured differently (${aspects.map((a) => ASPECT_WORDS[a] ?? a).join(", ")})`;
|
|
5567
|
+
|
|
5568
|
+
// packages/present/dist/src/readWords.js
|
|
5569
|
+
function componentReadName(c) {
|
|
5570
|
+
const ref = (c.key ? parseComponentKey(c.key) : null) ?? parseComponentKey(c.name);
|
|
5571
|
+
return componentShownName(ref?.name ?? c.name, c.file ?? ref?.file ?? "");
|
|
5572
|
+
}
|
|
5573
|
+
function componentReadNames(cs) {
|
|
5574
|
+
const names = cs.map(componentReadName);
|
|
5575
|
+
const rawNames = cs.map((c) => (c.key ? parseComponentKey(c.key)?.name : undefined) ?? parseComponentKey(c.name)?.name ?? c.name);
|
|
5576
|
+
return cs.map((c, i) => {
|
|
5577
|
+
const equals = cs.filter((_, k) => rawNames[k] === rawNames[i]);
|
|
5578
|
+
const file = c.file ?? (c.key ? parseComponentKey(c.key)?.file : undefined);
|
|
5579
|
+
const files = equals.flatMap((x) => x.file ?? (x.key ? parseComponentKey(x.key)?.file ?? [] : []));
|
|
5580
|
+
return new Set(files).size > 1 && file ? `${names[i]} · ${shortestPaths(files).get(file)}` : names[i];
|
|
5581
|
+
});
|
|
5582
|
+
}
|
|
5583
|
+
function storedRefWords(ref, peers = []) {
|
|
5584
|
+
if (!ref.startsWith("b:component:"))
|
|
5585
|
+
return ref;
|
|
5586
|
+
const cut = ref.lastIndexOf(":");
|
|
5587
|
+
const key = ref.slice("b:component:".length, cut);
|
|
5588
|
+
const components = [ref, ...peers].filter((r) => r.startsWith("b:component:")).map((r) => {
|
|
5589
|
+
const componentKey = r.slice("b:component:".length, r.lastIndexOf(":"));
|
|
5590
|
+
return { key: componentKey, name: componentKey };
|
|
5591
|
+
});
|
|
5592
|
+
const name = componentReadNames(components)[0] ?? componentReadName({ key, name: key });
|
|
5593
|
+
return `${name}, ${measureWords(ref.slice(cut + 1))}`;
|
|
5594
|
+
}
|
|
5595
|
+
function resultContextLines(c, verbose = false, short = false) {
|
|
5596
|
+
if (!c)
|
|
5597
|
+
return [];
|
|
5598
|
+
const out = [];
|
|
5599
|
+
if (c.namesChanged)
|
|
5600
|
+
out.push(restartRowText(c.namesChanged.sdk, c.namesChanged.mainSessions, c.namesChanged.needed));
|
|
5601
|
+
for (const w of c.windows) {
|
|
5602
|
+
const shownPlace = `${windowQualified(w.place)}${verbose ? ` (${w.place})` : ""}`;
|
|
5603
|
+
if (w.slowestCommit)
|
|
5604
|
+
out.push(`${shownPlace}: ${MEASURES.commit_max_ms.label}: ${valueText("commit_max_ms", w.slowestCommit.main)} → ${valueText("commit_max_ms", w.slowestCommit.pr)}`);
|
|
5605
|
+
if (!w.cause)
|
|
5606
|
+
continue;
|
|
5607
|
+
const s = w.cause.subject;
|
|
5608
|
+
const subject = s.kind === "component" ? `${componentShownName(s.ref.name, s.ref.file)}${verbose ? ` (${s.ref.file})` : ""}` : placeShownName({ kind: placeParts(s.place)?.kind === "drag" ? "drag" : "press", target: s.target, parent: s.parent, k: s.k, ...s.host ? { host: s.host } : {} });
|
|
5609
|
+
out.push(`${shownPlace}: ${w.information || !w.tier ? informationLeadText(subject, w.cause.measure, w.cause.main, w.cause.pr) : screenLeadText(w.tier, subject, w.cause.measure, w.cause.main, w.cause.pr)}${w.cause.measuredDifferently?.length ? `; ${measuredDifferentlyText(w.cause.measuredDifferently)}` : ""}`);
|
|
5610
|
+
}
|
|
5611
|
+
const screens = new Map;
|
|
5612
|
+
for (const p of c.notInThisRun) {
|
|
5613
|
+
const parts = placeParts(p.place);
|
|
5614
|
+
const key = parts ? componentShownName(parts.screen.name, parts.screen.file ?? "") : "";
|
|
5615
|
+
screens.set(key, [...screens.get(key) ?? [], p]);
|
|
5616
|
+
}
|
|
5617
|
+
for (const [screen, rows] of screens) {
|
|
5618
|
+
const line = notInRunRowsText({ screens: rows.filter((p) => p.kind === "screen").length, places: rows.filter((p) => p.kind !== "screen").length }, c.page);
|
|
5619
|
+
out.push(`${screen ? `${screen}: ` : ""}${line.lead}${line.rest}`);
|
|
5620
|
+
if (rows.length > 3)
|
|
5621
|
+
out.push(` ${foldedText(rows.length, "places", c.page)}`);
|
|
5622
|
+
if (!short || rows.length <= 3)
|
|
5623
|
+
out.push(...rows.map((p) => ` ${windowQualified(p.place)}`));
|
|
5624
|
+
}
|
|
5625
|
+
const removed = new Map;
|
|
5626
|
+
for (const r of c.removed)
|
|
5627
|
+
removed.set(r.place, [...removed.get(r.place) ?? [], r]);
|
|
5628
|
+
for (const [place, rows] of removed) {
|
|
5629
|
+
const line = removedRowsText(rows.length, c.page);
|
|
5630
|
+
out.push(`${windowQualified(place)}: ${line.lead}${line.rest}`);
|
|
5631
|
+
if (rows.length > 3)
|
|
5632
|
+
out.push(` ${foldedText(rows.length, "components", c.page)}`);
|
|
5633
|
+
if (!short || rows.length <= 3)
|
|
5634
|
+
out.push(...rows.map((r) => ` ${r.ref.kind === "breakdown" ? componentReadName({ key: r.ref.key, name: r.ref.name }) : measureWords(r.ref.measure)}`));
|
|
5635
|
+
}
|
|
5636
|
+
return out;
|
|
5637
|
+
}
|
|
5638
|
+
function laneContextLines(context, journey, phone, verbose = false, short = false) {
|
|
5639
|
+
const lines = resultContextLines(context, verbose, short);
|
|
5640
|
+
return lines.length ? [`${journey} on ${phone}:`, ...lines.map((line) => ` ${line}`)] : [];
|
|
5641
|
+
}
|
|
5642
|
+
var exactComponentMs = (ms) => `${ms} ms`;
|
|
5643
|
+
var componentPageText = (page, count) => `Component rows: ${count ? `${page.offset + 1}–${page.offset + count}` : "0"} of ${page.total} stored${page.next_offset !== null ? `; next --page ${Math.floor(page.next_offset / page.limit) + 1} (MCP component_offset ${page.next_offset})` : ""}.`;
|
|
5644
|
+
var omissionText = (o) => `${o.run_id}: SDK omitted ${o.sdkOmitted ?? "unknown"}; Derivation left out ${o.omitted ?? "unknown"}${o.selfMs === null ? "" : ` · ${exactComponentMs(o.selfMs)}`}. Paging leaves recorded omissions unchanged.`;
|
|
5645
|
+
var READ_WORDS = {
|
|
5646
|
+
componentTime: "component render time",
|
|
5647
|
+
ownTime: "own time",
|
|
5648
|
+
detailUnavailable: "Only render time recorded; renders, parent and library detail unavailable.",
|
|
5649
|
+
parent: "Placed by",
|
|
5650
|
+
slowest: MEASURES.commit_max_ms.label,
|
|
5651
|
+
runA: "Run A",
|
|
5652
|
+
runB: "Run B",
|
|
5653
|
+
notMeasured: "Not measured",
|
|
5654
|
+
onlyA: "Only in run A",
|
|
5655
|
+
onlyB: "Only in run B"
|
|
5656
|
+
};
|
|
5657
|
+
var READ_USAGE = {
|
|
5658
|
+
listFindings: `What got ${STATE_WORDS.worse.toLowerCase()} (${STATE_WORDS.worse} / ${STATE_WORDS.warning}) or ${STATE_WORDS.better.toLowerCase()} in the app, worst first, as KeepSnappy measured it on real phones. The answer starts with whether and when the branch was measured and how its newest run set read: an empty list on a branch that was never measured, or whose newest run set was not judged, is not a clean result. Filter by a PR branch or main, journey, phone, and state (present by default; not-judged = the newest run set did not judge it, so it may still be there). Each finding says what changed, where, how sure, and has an id for get_finding. Page with offset.`,
|
|
5659
|
+
components: " --page N | --all (places or compare: a component page, or every stored component and place; --top-components sets the bounded page size)",
|
|
5660
|
+
verbose: " --verbose (include source files and raw identities in text; JSON is unchanged)",
|
|
5661
|
+
page: "--page must be a whole number from 1; use it only with places or compare.",
|
|
5662
|
+
all: "--all is for places or compare; do not combine it with --page, --offset or --short.",
|
|
5663
|
+
detailFlag: " --detail (places: include recorded run detail; also included by --verbose or component paging)",
|
|
5664
|
+
detail: "Include recorded per-run component and window detail.",
|
|
5665
|
+
compareTop: "--top-components must be a whole number from 1 to 200 for compare.",
|
|
5666
|
+
top: "--top-components must be a whole number from 0 to 20, or from 1 to 200 with --page or --all.",
|
|
5667
|
+
paging: "The API did not advance the component page; stopped without claiming the list is complete.",
|
|
5668
|
+
componentOffset: "Skip this many stored components per place.",
|
|
5669
|
+
componentLimit: "Component page size; the response gives stored totals and next offsets."
|
|
5670
|
+
};
|
|
5671
|
+
var componentListsText = (totals, shown) => `Stored component rows: ${shown.rows} of ${totals.rows} compared; ${shown.added} of ${totals.added} ${READ_WORDS.onlyB.toLowerCase()}; ${shown.removed} of ${totals.removed} ${READ_WORDS.onlyA.toLowerCase()}.`;
|
|
5672
|
+
var notMeasuredText = (side) => `${READ_WORDS.notMeasured} in run ${side.toUpperCase()}`;
|
|
5673
|
+
|
|
5674
|
+
// packages/present/dist/src/words.js
|
|
5675
|
+
var REASON_WORDS = {
|
|
5676
|
+
"no-baseline": "there are no main runs on this phone and journey yet",
|
|
5677
|
+
"too-few-runs": "there are too few valid runs on a side",
|
|
5678
|
+
"judged-measures-missing": "the judged measures are missing on a side",
|
|
5679
|
+
"measured-differently": "the sides were measured differently",
|
|
5680
|
+
"runs-unknown": "a change that would count has no value in some runs, so it cannot be confirmed in every run",
|
|
5681
|
+
"facts-version-mismatch": "the sides were derived with different versions",
|
|
5682
|
+
"not-judged-on-simulators": "timing is not judged on simulators",
|
|
5683
|
+
"platforms-differ": "the sides ran on different platforms (iOS and Android are never compared)",
|
|
5684
|
+
"not-enough-main": "main does not have enough sessions on this phone and journey yet"
|
|
5685
|
+
};
|
|
5686
|
+
var waitingWords = (runs) => `${runs} main ${runs === 1 ? "run waits" : "runs wait"} for a reprocess`;
|
|
5496
5687
|
// packages/present/dist/src/main-history.js
|
|
5497
5688
|
var MAIN_TIER_WORDS = {
|
|
5498
5689
|
same: "In norm",
|
|
@@ -5501,6 +5692,13 @@ var MAIN_TIER_WORDS = {
|
|
|
5501
5692
|
slight: STATE_WORDS.warning,
|
|
5502
5693
|
worse: STATE_WORDS.worse
|
|
5503
5694
|
};
|
|
5695
|
+
var MAIN_TIER_RESULT_WORDS = {
|
|
5696
|
+
same: STATE_WORDS["no-change"],
|
|
5697
|
+
better: STATE_WORDS.better,
|
|
5698
|
+
cant: STATE_WORDS["cant-tell"],
|
|
5699
|
+
slight: STATE_WORDS.warning,
|
|
5700
|
+
worse: STATE_WORDS.worse
|
|
5701
|
+
};
|
|
5504
5702
|
var MAIN_TIER_RANK = { worse: 0, slight: 1, cant: 2, better: 3, same: 4 };
|
|
5505
5703
|
function mainCantWords(reason, config) {
|
|
5506
5704
|
if (!reason)
|
|
@@ -5526,7 +5724,6 @@ function mainPlaceReasonWords(reason, config) {
|
|
|
5526
5724
|
}
|
|
5527
5725
|
}
|
|
5528
5726
|
var MAIN_SUMMARY_PENDING = "Runs received; the summary is not computed yet.";
|
|
5529
|
-
var MAIN_NEWEST_SUMMARY_PENDING = `Newest: ${MAIN_SUMMARY_PENDING}`;
|
|
5530
5727
|
var SHORT_ASPECT_WORDS = {
|
|
5531
5728
|
...ASPECT_WORDS,
|
|
5532
5729
|
sdk: "SDK",
|
|
@@ -5563,6 +5760,7 @@ function mainCutWords(v) {
|
|
|
5563
5760
|
}
|
|
5564
5761
|
return parts.length ? parts.join(" ") : "Measures alike; no baseline cut.";
|
|
5565
5762
|
}
|
|
5763
|
+
var BY_RANK = Object.keys(MAIN_TIER_RANK).sort((a, b) => MAIN_TIER_RANK[a] - MAIN_TIER_RANK[b]);
|
|
5566
5764
|
// apps/agent/src/summary.ts
|
|
5567
5765
|
var fieldWord = (field) => measureDef(field)?.short ?? field;
|
|
5568
5766
|
function plainMetric(metric) {
|
|
@@ -5654,10 +5852,16 @@ var setupLine = (h) => {
|
|
|
5654
5852
|
var keyHeader = (t) => ({ "x-keepsnappy-key": t.key });
|
|
5655
5853
|
async function createRunSet(t, body) {
|
|
5656
5854
|
const r = await call(t.fetch ?? fetch, t.url, "/v1/run-sets", keyHeader(t), body);
|
|
5657
|
-
|
|
5855
|
+
if (typeof r.run_set_id !== "string" || !r.run_set_id)
|
|
5856
|
+
throw new UploadError("POST /v1/run-sets: response has no run_set_id (is the URL the KeepSnappy API?)");
|
|
5857
|
+
return { run_set_id: r.run_set_id, dashboard_url: String(r.dashboard_url ?? "") };
|
|
5658
5858
|
}
|
|
5659
5859
|
async function completeRunSet(t, id, verdicts) {
|
|
5660
|
-
|
|
5860
|
+
const path = `/v1/run-sets/${encodeURIComponent(id)}/complete`;
|
|
5861
|
+
const r = await call(t.fetch ?? fetch, t.url, path, keyHeader(t), { verdicts });
|
|
5862
|
+
if (!r.run_set_id)
|
|
5863
|
+
throw new UploadError(`POST ${path}: response has no run_set_id`);
|
|
5864
|
+
return r;
|
|
5661
5865
|
}
|
|
5662
5866
|
async function createIngestRun(t, body) {
|
|
5663
5867
|
const r = await call(t.fetch ?? fetch, t.url, "/v1/runs", keyHeader(t), body);
|
|
@@ -5672,13 +5876,20 @@ async function finishIngestRun(t, runId, invalidReason, farmSession) {
|
|
|
5672
5876
|
});
|
|
5673
5877
|
}
|
|
5674
5878
|
async function getRunSetRuns(t, id) {
|
|
5675
|
-
|
|
5879
|
+
const path = `/v1/run-sets/${encodeURIComponent(id)}/runs`;
|
|
5880
|
+
const r = await call(t.fetch ?? fetch, t.url, path, keyHeader(t), undefined, "GET");
|
|
5881
|
+
if (!Array.isArray(r.runs))
|
|
5882
|
+
throw new UploadError(`GET ${path}: response has no runs`);
|
|
5883
|
+
return r;
|
|
5676
5884
|
}
|
|
5677
5885
|
var FEW_RUNS = "Few runs — result may be noisy";
|
|
5678
|
-
var word = (s, server,
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5886
|
+
var word = (s, server, blocking) => badgedWord(server || stateWord(s ?? "inconclusive"), blocking);
|
|
5887
|
+
var setWord = (v) => word(v?.state ?? null, v?.word, v?.blocking ?? v?.headline?.blocking);
|
|
5888
|
+
var scenarioWord = (sc) => word(sc.state, sc.word, sc.headline?.blocking);
|
|
5889
|
+
var notComputed = (s) => !s.verdicts || s.verdicts.state === null && !s.verdicts.blocking;
|
|
5890
|
+
var resultWord = (s) => setWord(s.verdicts);
|
|
5891
|
+
var UNCHECKED_GATE = "Verdict not computed yet (computed on demand): --fail-on-worse checked nothing";
|
|
5892
|
+
var uncheckedGate = (s) => notComputed(s) ? UNCHECKED_GATE : null;
|
|
5682
5893
|
var countsLine = (c) => countsWords(c);
|
|
5683
5894
|
var noCounts = (c) => !c.worse && !c.warnings && !c.better && !c.noChange && !c.inconclusive;
|
|
5684
5895
|
var change = (metric, a, b) => {
|
|
@@ -5691,7 +5902,7 @@ function formatRunSet(s, top = 5) {
|
|
|
5691
5902
|
const lines = [];
|
|
5692
5903
|
const where = `${s.branch}${s.commit_sha ? ` @ ${s.commit_sha.slice(0, 7)}` : ""}`;
|
|
5693
5904
|
const v = s.verdicts;
|
|
5694
|
-
lines.push(`KeepSnappy result for ${where}: ${
|
|
5905
|
+
lines.push(`KeepSnappy result for ${where}: ${setWord(v)}`);
|
|
5695
5906
|
if (setupLine(v?.headline))
|
|
5696
5907
|
lines.push(` ${setupLine(v?.headline)}`);
|
|
5697
5908
|
if (v?.low_data)
|
|
@@ -5705,13 +5916,14 @@ function formatRunSet(s, top = 5) {
|
|
|
5705
5916
|
const dev = sc.device ? `${sc.device.model}${sc.device.os ? ` (${sc.device.os})` : ""}` : "no device";
|
|
5706
5917
|
const what = sc.role === "comparison" ? "compared with the baseline branch" : sc.role === "history" ? "latest runs compared with earlier ones" : "not judged (no branch or flow name)";
|
|
5707
5918
|
lines.push("");
|
|
5708
|
-
lines.push(`${sc.scenario ?? "(no flow name)"} on ${dev}, ${what}: ${
|
|
5919
|
+
lines.push(`${sc.scenario ?? "(no flow name)"} on ${dev}, ${what}: ${scenarioWord(sc)}`);
|
|
5709
5920
|
if (setupLine(sc.headline) && (v?.scenarios.length ?? 0) > 1)
|
|
5710
5921
|
lines.push(` ${setupLine(sc.headline)}`);
|
|
5711
5922
|
if (sc.low_data && (v?.scenarios.length ?? 0) > 1)
|
|
5712
5923
|
lines.push(` ${FEW_RUNS}`);
|
|
5713
5924
|
if (sc.role !== "adhoc" && !noCounts(movedCounts(sc)))
|
|
5714
5925
|
lines.push(` ${countsLine(movedCounts(sc))}`);
|
|
5926
|
+
lines.push(...resultContextLines(sc.context).map((line) => ` ${line}`));
|
|
5715
5927
|
if (heatNote(sc))
|
|
5716
5928
|
lines.push(` ${heatNote(sc)}`);
|
|
5717
5929
|
const allMoved = movedItems(v, sc).sort((a, b) => Number(b.state === "worse") - Number(a.state === "worse") || Number(a.grade === "warn") - Number(b.grade === "warn"));
|
|
@@ -5743,12 +5955,13 @@ Runs uploaded for \`${fallback.branch}\`. The result appears on the dashboard: [
|
|
|
5743
5955
|
}
|
|
5744
5956
|
const v = s.verdicts;
|
|
5745
5957
|
const heated = v.scenarios.flatMap((sc) => heatNote(sc) ?? []);
|
|
5958
|
+
const contextLines = v.scenarios.flatMap((sc) => laneContextLines(sc.context, sc.scenario ?? "-", sc.device?.model ?? "-"));
|
|
5746
5959
|
const rows = v.scenarios.map((sc) => {
|
|
5747
5960
|
const c = movedCounts(sc);
|
|
5748
|
-
return `| ${sc.scenario ?? "-"} | ${sc.device ? sc.device.model : "-"} | ${
|
|
5961
|
+
return `| ${sc.scenario ?? "-"} | ${sc.device ? sc.device.model : "-"} | ${scenarioWord(sc)}${sc.low_data ? ` (${FEW_RUNS})` : ""} | ${noCounts(c) ? "–" : `${c.worse} / ${c.warnings} / ${c.better} / ${c.noChange} / ${c.inconclusive}`} | ${sc.dashboard_url ? `[open](${sc.dashboard_url})` : "-"} |`;
|
|
5749
5962
|
});
|
|
5750
5963
|
return [
|
|
5751
|
-
`### KeepSnappy: ${
|
|
5964
|
+
`### KeepSnappy: ${setWord(v)}`,
|
|
5752
5965
|
"",
|
|
5753
5966
|
...setupLine(v.headline) ? [`${setupLine(v.headline)}.`, ""] : [],
|
|
5754
5967
|
`\`${s.branch}\`${s.commit_sha ? ` @ \`${s.commit_sha.slice(0, 7)}\`` : ""}, ${s.runs.valid}/${s.runs.total} runs used.${v.low_data ? ` ${FEW_RUNS}.` : ""}`,
|
|
@@ -5756,6 +5969,7 @@ Runs uploaded for \`${fallback.branch}\`. The result appears on the dashboard: [
|
|
|
5756
5969
|
`| Journey | Device | Result | ${STATE_WORDS.worse} / ${STATE_WORDS.warning} / ${STATE_WORDS.better} / ${STATE_WORDS["no-change"]} / ${STATE_WORDS["cant-tell"]} | Details |`,
|
|
5757
5970
|
"|---|---|---|---|---|",
|
|
5758
5971
|
...rows,
|
|
5972
|
+
...contextLines.length ? ["", ...contextLines] : [],
|
|
5759
5973
|
...heated.length ? ["", ...heated.map((note) => `${note}.`)] : [],
|
|
5760
5974
|
"",
|
|
5761
5975
|
`[Open the dashboard](${s.dashboard_url})`,
|
|
@@ -9591,6 +9805,14 @@ ${outcome.summary}`);
|
|
|
9591
9805
|
...api.fetch ? { fetch: api.fetch } : {}
|
|
9592
9806
|
}).catch(infra(`results are saved in ${out}, but the upload failed`));
|
|
9593
9807
|
rt.print(`uploaded ${up.uploaded} runs to ${api.url}${up.skipped.length ? ` (${up.skipped.length} skipped: ${up.skipped.map((x) => `${x.run_id} ${x.why}`).join(", ")})` : ""}`);
|
|
9808
|
+
if (up.uploaded === 0) {
|
|
9809
|
+
if (runSet)
|
|
9810
|
+
await completeRunSet(api, runSet.run_set_id, "on-read").catch(infra("no run was uploaded, and the run set could not be closed"));
|
|
9811
|
+
rt.print("Blocked: no run sent any data. Check that the build carries the SDK and that the app reaches the API.");
|
|
9812
|
+
if (runSet)
|
|
9813
|
+
await writeResult(deps, out, rt, runSet, null, git.branch, true);
|
|
9814
|
+
return EXIT.infra;
|
|
9815
|
+
}
|
|
9594
9816
|
if (!runSet)
|
|
9595
9817
|
return EXIT.ok;
|
|
9596
9818
|
let summary = null;
|
|
@@ -9607,6 +9829,9 @@ ${formatRunSet(summary)}`);
|
|
|
9607
9829
|
rt.print(`--fail-on-worse: a journey is ${STATE_WORDS.worse.toLowerCase()} than its baseline and blocks (exit 3)`);
|
|
9608
9830
|
return EXIT.worse;
|
|
9609
9831
|
}
|
|
9832
|
+
const unchecked = o["fail-on-worse"] && summary ? uncheckedGate(summary) : null;
|
|
9833
|
+
if (unchecked)
|
|
9834
|
+
rt.print(unchecked);
|
|
9610
9835
|
return EXIT.ok;
|
|
9611
9836
|
}
|
|
9612
9837
|
async function replayFlow(config, scenario) {
|
|
@@ -9623,13 +9848,16 @@ var infra = (what) => (e) => {
|
|
|
9623
9848
|
throw new Error(`${what}: ${e.message}`);
|
|
9624
9849
|
throw e;
|
|
9625
9850
|
};
|
|
9626
|
-
async function writeResult(deps, out, rt, runSet, summary, branch) {
|
|
9851
|
+
async function writeResult(deps, out, rt, runSet, summary, branch, blocked = false) {
|
|
9627
9852
|
const url = summary?.dashboard_url ?? runSet.dashboard_url;
|
|
9628
|
-
await deps.fs.writeFile(join9(out, "keepsnappy-result.json"), `${JSON.stringify({ run_set_id: runSet.run_set_id, dashboard_url: url, state: summary?.verdicts?.state ?? null, summary }, null, 2)}
|
|
9853
|
+
await deps.fs.writeFile(join9(out, "keepsnappy-result.json"), `${JSON.stringify({ run_set_id: runSet.run_set_id, dashboard_url: url, state: blocked ? "blocked" : summary?.verdicts?.state ?? null, summary }, null, 2)}
|
|
9629
9854
|
`);
|
|
9630
9855
|
const step = rt.env.GITHUB_STEP_SUMMARY;
|
|
9631
9856
|
if (step)
|
|
9632
|
-
await deps.fs.appendFile(step,
|
|
9857
|
+
await deps.fs.appendFile(step, blocked ? `### KeepSnappy: Blocked
|
|
9858
|
+
|
|
9859
|
+
No run sent any data. [Open the dashboard](${url})
|
|
9860
|
+
` : markdownRunSet(summary, { dashboardUrl: url, branch }));
|
|
9633
9861
|
}
|
|
9634
9862
|
|
|
9635
9863
|
// apps/agent/src/doctor.ts
|
|
@@ -10091,7 +10319,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
10091
10319
|
import { dirname as dirname2, join as join11, relative as relative2, resolve as resolve4 } from "node:path";
|
|
10092
10320
|
import { fileURLToPath } from "node:url";
|
|
10093
10321
|
function driverVersion() {
|
|
10094
|
-
const built = "0.1.0-alpha.
|
|
10322
|
+
const built = "0.1.0-alpha.17";
|
|
10095
10323
|
if (built)
|
|
10096
10324
|
return built;
|
|
10097
10325
|
let dir = dirname2(fileURLToPath(import.meta.url));
|
|
@@ -10146,8 +10374,6 @@ var SDK_FLUSH_WORST_CASE_MS = 315000;
|
|
|
10146
10374
|
var FLUSH_MARGIN_MS = 30000;
|
|
10147
10375
|
var FLUSH_WAIT_MS = SDK_FLUSH_WORST_CASE_MS + FLUSH_MARGIN_MS;
|
|
10148
10376
|
var FLUSH_STATUSES = new Set(["ok", "error", "off"]);
|
|
10149
|
-
// packages/appium-driver/dist/src/gestures.js
|
|
10150
|
-
var densities = new WeakMap;
|
|
10151
10377
|
// apps/agent/src/device-model.ts
|
|
10152
10378
|
import { tmpdir } from "node:os";
|
|
10153
10379
|
import { join as join12 } from "node:path";
|
|
@@ -10317,6 +10543,7 @@ function slackMessage(s, ctx) {
|
|
|
10317
10543
|
const better = items.filter(isBetter).sort(bySize);
|
|
10318
10544
|
const commit = commitLine(ctx);
|
|
10319
10545
|
const heated = v.scenarios.filter((sc) => heatNote(sc));
|
|
10546
|
+
const contextLines = v.scenarios.flatMap((sc) => laneContextLines(sc.context, sc.scenario ?? "-", phone(sc))).map(context);
|
|
10320
10547
|
const heat = heated.map((sc) => context(heatNote(sc)));
|
|
10321
10548
|
const errorRows = worse.filter((i) => i.grade !== "warn");
|
|
10322
10549
|
const warningRows = worse.filter((i) => i.grade === "warn");
|
|
@@ -10330,15 +10557,15 @@ function slackMessage(s, ctx) {
|
|
|
10330
10557
|
if (!worse.length) {
|
|
10331
10558
|
if (!heat.length)
|
|
10332
10559
|
return null;
|
|
10333
|
-
const head = `⚪ ${ctx.branch}: ${INFO_WORDS.heat}`;
|
|
10334
|
-
const blocks = [section(`*${head}*`), ...heat, ...commit ? [context(commit)] : [], section(`\uD83D\uDC49 <${heated[0]
|
|
10560
|
+
const head = `⚪ ${ctx.branch}: ${heat.length ? INFO_WORDS.heat : v.word ?? STATE_WORDS["no-change"]}`;
|
|
10561
|
+
const blocks = [section(`*${head}*`), ...heat, ...contextLines, ...commit ? [context(commit)] : [], section(`\uD83D\uDC49 <${heated[0]?.dashboard_url ?? s.dashboard_url}|View the comparison>`)];
|
|
10335
10562
|
return { text: head, attachments: [{ color: COLOR.none, blocks }] };
|
|
10336
10563
|
}
|
|
10337
10564
|
const graded = worse.some((i) => i.grade);
|
|
10338
10565
|
const places = (word, n) => `${word.toLowerCase()} in ${n} place${n > 1 ? "s" : ""}`;
|
|
10339
10566
|
const counts = graded ? [errors ? `${places(STATE_WORDS.worse, errors)}${notBlocking ? ` (${NOT_BLOCKING})` : ""}` : "", warns ? places(STATE_WORDS.warning, warns) : ""].filter(Boolean).join(", ") : `${places(STATE_WORDS.worse, placeCount(worse))}${notBlocking ? ` (${NOT_BLOCKING})` : ""}`;
|
|
10340
10567
|
const head = `${worseMark} ${ctx.branch}: ${counts}`;
|
|
10341
|
-
const blocks = [section(`*${head}*`)];
|
|
10568
|
+
const blocks = [section(`*${head}*`), ...contextLines];
|
|
10342
10569
|
const groups = new Map;
|
|
10343
10570
|
for (const i of worse)
|
|
10344
10571
|
groups.set(scenarioOf(i), [...groups.get(scenarioOf(i)) ?? [], i]);
|
|
@@ -10373,7 +10600,7 @@ ${prTable(rows, sc)}`));
|
|
|
10373
10600
|
const head = `${worse.length ? worseMark : improved ? "\uD83D\uDFE2" : v.state === "inconclusive" && (ctx.always || !heat.length) ? "❔" : "⚪"} ${status}`;
|
|
10374
10601
|
const journeys = new Set(v.scenarios.map((sc) => `${sc.scenario}|${sc.device?.id}`)).size;
|
|
10375
10602
|
const blocks = [section(`*${head}*
|
|
10376
|
-
${journeys} journey${journeys === 1 ? "" : "s"} checked`)];
|
|
10603
|
+
${journeys} journey${journeys === 1 ? "" : "s"} checked`), ...contextLines];
|
|
10377
10604
|
let shown = 0;
|
|
10378
10605
|
for (const [title, list] of [[STATE_WORDS.worse, errorRows], [STATE_WORDS.warning, warningRows], [STATE_WORDS.better, better]]) {
|
|
10379
10606
|
for (const i of list) {
|
|
@@ -10409,17 +10636,23 @@ var LINKS_USAGE = `keepsnappy activation-link --scheme <app://> --token <run tok
|
|
|
10409
10636
|
flush-link --scheme <app://>
|
|
10410
10637
|
gc-link --scheme <app://> [--n 1]`;
|
|
10411
10638
|
function linksCommand(cmd, argv, rt) {
|
|
10412
|
-
const { values: o } =
|
|
10413
|
-
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
|
|
10420
|
-
|
|
10639
|
+
const { values: o } = (() => {
|
|
10640
|
+
try {
|
|
10641
|
+
return parseArgs({
|
|
10642
|
+
args: [...argv],
|
|
10643
|
+
options: {
|
|
10644
|
+
scheme: { type: "string" },
|
|
10645
|
+
token: { type: "string" },
|
|
10646
|
+
n: { type: "string" },
|
|
10647
|
+
"no-diag": { type: "boolean" },
|
|
10648
|
+
"no-profile": { type: "boolean" },
|
|
10649
|
+
set: { type: "string", multiple: true }
|
|
10650
|
+
}
|
|
10651
|
+
});
|
|
10652
|
+
} catch (e) {
|
|
10653
|
+
throw new UsageError([e.message, LINKS_USAGE]);
|
|
10421
10654
|
}
|
|
10422
|
-
});
|
|
10655
|
+
})();
|
|
10423
10656
|
if (!o.scheme)
|
|
10424
10657
|
throw new ConfigError([`--scheme is required (the app's URL scheme, e.g. myapp://)`]);
|
|
10425
10658
|
if (cmd === "flush-link") {
|
|
@@ -10865,9 +11098,9 @@ ${tail.join(" ")}
|
|
|
10865
11098
|
...blocked ? { blocked: { reason: `No iteration sent any data${session.known ? ` (device session ${session.status})` : ""}.` } } : {},
|
|
10866
11099
|
always: envValue(rt.env, "SLACK_ALWAYS") === "1"
|
|
10867
11100
|
});
|
|
10868
|
-
if (!msg)
|
|
10869
|
-
rt.print(f.wait ?
|
|
10870
|
-
else {
|
|
11101
|
+
if (!msg) {
|
|
11102
|
+
rt.print(!f.wait ? "Slack: skipped, the message needs --wait" : notComputed(summary) ? "Slack: nothing to report, the verdict is not computed yet" : `Slack: nothing to report (${resultWord(summary).toLowerCase()})`);
|
|
11103
|
+
} else {
|
|
10871
11104
|
const r = await postToSlack(hook, msg, rt.fetch ?? fetch);
|
|
10872
11105
|
rt.print(r.ok ? "Slack: posted" : `Slack: not posted (${r.error}); the job is not affected`);
|
|
10873
11106
|
}
|
|
@@ -10878,6 +11111,9 @@ ${tail.join(" ")}
|
|
|
10878
11111
|
rt.print(`--fail-on-worse: a journey is ${STATE_WORDS.worse.toLowerCase()} than its baseline and blocks (exit 3)`);
|
|
10879
11112
|
return EXIT.worse;
|
|
10880
11113
|
}
|
|
11114
|
+
const unchecked = f["fail-on-worse"] ? uncheckedGate(summary) : null;
|
|
11115
|
+
if (unchecked)
|
|
11116
|
+
rt.print(unchecked);
|
|
10881
11117
|
return EXIT.ok;
|
|
10882
11118
|
}
|
|
10883
11119
|
|
|
@@ -11297,7 +11533,7 @@ function printPlan(p, f, rt) {
|
|
|
11297
11533
|
line(` env ${k}=${substitute(v, {})}`);
|
|
11298
11534
|
line(` 6. farm close${f.wait ? " --wait" : ""}${f["fail-on-worse"] ? " --fail-on-worse" : ""} with ${join16(p.out, `browserstack.${p.platform}.json`)}`);
|
|
11299
11535
|
line(" 7. always: stop the tunnel, put the empty stub back over the tokens file");
|
|
11300
|
-
line(" exit: 0 any verdict, 1 infrastructure / driver failed / Blocked, 2 configuration, 3 --fail-on-worse");
|
|
11536
|
+
line(" exit: 0 any verdict, 1 infrastructure / driver failed / Blocked, 2 configuration, 3 --fail-on-worse (a computed blocking Worse only; no computed verdict: nothing checked)");
|
|
11301
11537
|
}
|
|
11302
11538
|
var ANNOTATION = /^::(error|warning|notice)( [^:]*)?::/;
|
|
11303
11539
|
async function runDriver2(deps, rt, f, p, ports, ctx) {
|
|
@@ -11729,7 +11965,8 @@ var SHORT_FORM_NOTES = {
|
|
|
11729
11965
|
main_vs_previous: `the movers after the first ${SHORT_PLACES} (every new, gone or multi-device place stays)`,
|
|
11730
11966
|
place_trend: `the sessions older than the newest ${SHORT_SESSIONS} and the history restarts among them (unknown restarts always stay)`,
|
|
11731
11967
|
window_history: `the runs older than the newest ${SHORT_POINTS}`,
|
|
11732
|
-
pr_status: "the per-journey dashboard links and the heat note"
|
|
11968
|
+
pr_status: "the per-journey dashboard links and the heat note",
|
|
11969
|
+
list_problems: `the work and component lines, the dashboard links and the problems after the first ${SHORT_PLACES}`
|
|
11733
11970
|
};
|
|
11734
11971
|
var andList = (xs) => xs.length < 2 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
|
|
11735
11972
|
|
|
@@ -11748,6 +11985,13 @@ class Rows {
|
|
|
11748
11985
|
full(...texts) {
|
|
11749
11986
|
return this.add(false, ...texts);
|
|
11750
11987
|
}
|
|
11988
|
+
detail(...texts) {
|
|
11989
|
+
for (const text of texts)
|
|
11990
|
+
for (const line of text.split(`
|
|
11991
|
+
`))
|
|
11992
|
+
this.rows.push({ text: line, short: false, notice: false });
|
|
11993
|
+
return this;
|
|
11994
|
+
}
|
|
11751
11995
|
render(form, left = []) {
|
|
11752
11996
|
if (form !== "short")
|
|
11753
11997
|
return this.rows.map((r) => r.text).join(`
|
|
@@ -11762,7 +12006,7 @@ class Rows {
|
|
|
11762
12006
|
}
|
|
11763
12007
|
if (kept[kept.length - 1] === "")
|
|
11764
12008
|
kept.pop();
|
|
11765
|
-
if (this.rows.some((r) => !r.short && r.text.trim() !== ""))
|
|
12009
|
+
if (this.rows.some((r) => !r.short && r.notice !== false && r.text.trim() !== ""))
|
|
11766
12010
|
kept.push(`Short form: ${andList(left.length ? left : ["other lines"])} left out; the full form shows them.`);
|
|
11767
12011
|
return kept.join(`
|
|
11768
12012
|
`);
|
|
@@ -11770,19 +12014,22 @@ class Rows {
|
|
|
11770
12014
|
}
|
|
11771
12015
|
var LABEL_W = 14;
|
|
11772
12016
|
var line = (label, text) => ` ${label.padEnd(LABEL_W)}${text}`;
|
|
11773
|
-
var
|
|
12017
|
+
var plural2 = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
|
|
11774
12018
|
var promptText = (f) => f.what_to_do.prompt ?? "No prompt: this finding is an improvement.";
|
|
11775
12019
|
function formatFinding(f, opts = {}) {
|
|
11776
12020
|
const r = new Rows;
|
|
11777
12021
|
r.both(f.answer.headline, line("What changed", f.answer.what_changed), line("Where", f.answer.where), line("How sure", f.answer.how_sure));
|
|
12022
|
+
if (f.lane)
|
|
12023
|
+
r.both(...laneContextLines(f.lane.context, f.lane.journey, f.lane.phone_name, opts.verbose, opts.form === "short"));
|
|
11778
12024
|
r.full(line("Why", f.answer.why));
|
|
11779
12025
|
r.both(line("What to do", f.answer.what_to_do));
|
|
11780
12026
|
r.full(line("Seen", f.answer.lifecycle), line("Layer", `${f.layer.id} · ${f.layer.name} (${f.certificate ? "certified" : "uncertified"})${f.limits.length ? `; limits: ${f.limits.join("; ")}` : ""}`));
|
|
11781
12027
|
r.both(line("Id", f.id));
|
|
11782
12028
|
if (f.why.components.length) {
|
|
11783
12029
|
r.full(line("Components", ""));
|
|
11784
|
-
|
|
11785
|
-
|
|
12030
|
+
const names = componentReadNames(f.why.components.map((c) => ({ key: c.key, name: c.component, ...c.file ? { file: c.file } : {} })));
|
|
12031
|
+
for (const [i, c] of f.why.components.entries()) {
|
|
12032
|
+
r.full(` ${names[i]}${opts.verbose && c.file ? ` (${c.file})` : ""}: ${c.self_ms_before ?? "–"} → ${c.self_ms_after} ms ${c.key ? READ_WORDS.componentTime : READ_WORDS.ownTime}, renders ${c.renders_before ?? "–"} → ${c.renders_after ?? "–"}${c.cause ? `; ${c.cause}` : ""}${c.flagged ? "" : " (not certain)"}`);
|
|
11786
12033
|
}
|
|
11787
12034
|
}
|
|
11788
12035
|
const memory = f.why.memory ?? [];
|
|
@@ -11797,15 +12044,15 @@ function formatFinding(f, opts = {}) {
|
|
|
11797
12044
|
r.full("", promptText(f));
|
|
11798
12045
|
return r.render(opts.form, [
|
|
11799
12046
|
"the why, seen and layer lines",
|
|
11800
|
-
...f.why.components.length ? [
|
|
11801
|
-
...memory.some((m) => m.metric !== f.what.metric) ? [
|
|
12047
|
+
...f.why.components.length ? [plural2(f.why.components.length, "component line")] : [],
|
|
12048
|
+
...memory.some((m) => m.metric !== f.what.metric) ? [plural2(memory.filter((m) => m.metric !== f.what.metric).length, "memory evidence line")] : [],
|
|
11802
12049
|
...opts.prompt ? ["the prompt"] : []
|
|
11803
12050
|
]);
|
|
11804
12051
|
}
|
|
11805
12052
|
var when = (iso) => iso.slice(0, 16).replace("T", " ");
|
|
11806
12053
|
var LANES_SHOWN = 8;
|
|
11807
12054
|
var STATE_WORDS2 = { present: "still present", resolved: "resolved", "not-judged": "not judged in the newest run set" };
|
|
11808
|
-
function measuredLines(l, r) {
|
|
12055
|
+
function measuredLines(l, r, opts) {
|
|
11809
12056
|
const f = l.filters;
|
|
11810
12057
|
const scope = [f.branch ?? "all branches", f.journey, f.phone].filter(Boolean).join(" · ");
|
|
11811
12058
|
const m = l.measured;
|
|
@@ -11813,7 +12060,7 @@ function measuredLines(l, r) {
|
|
|
11813
12060
|
if (!n) {
|
|
11814
12061
|
const on = f.branch ? f.branch === l.baseline_branch ? f.branch : `branch ${f.branch}` : "any branch";
|
|
11815
12062
|
const narrow = [f.journey, f.phone ? `phone “${f.phone}”` : null].filter(Boolean).join(", ");
|
|
11816
|
-
r.both(m.run_sets ? `${scope}: ${
|
|
12063
|
+
r.both(m.run_sets ? `${scope}: ${plural2(m.run_sets, "run set")} measured, but no verdict yet: nothing is known about its performance.` : `No KeepSnappy measurement on ${on}${narrow ? ` for ${narrow}` : ""}: nothing is known about its performance.`);
|
|
11817
12064
|
return [];
|
|
11818
12065
|
}
|
|
11819
12066
|
const lane = (x) => {
|
|
@@ -11821,22 +12068,29 @@ function measuredLines(l, r) {
|
|
|
11821
12068
|
return `${x.commit?.sha ? `commit ${x.commit.sha.slice(0, 7)}` : "commit not recorded"} (${when(x.at)}): ${x.word}${runs}${x.waiting_for_reprocess ? `; ${waitingWords(x.waiting_for_reprocess)}` : ""}`;
|
|
11822
12069
|
};
|
|
11823
12070
|
if (m.lanes.length === 1) {
|
|
11824
|
-
r.both(`${scope}: newest measured ${f.branch ? "" : `on ${n.branch}, `}${lane(n)}; ${
|
|
12071
|
+
r.both(`${scope}: newest measured ${f.branch ? "" : `on ${n.branch}, `}${lane(n)}; ${plural2(m.run_sets, "run set")} measured.`);
|
|
12072
|
+
r.both(...laneContextLines(n.context, n.journey, n.phone_name, opts.verbose, opts.form === "short"));
|
|
11825
12073
|
return [];
|
|
11826
12074
|
}
|
|
11827
12075
|
const shown = m.lanes.slice(0, LANES_SHOWN);
|
|
11828
|
-
|
|
11829
|
-
|
|
11830
|
-
|
|
11831
|
-
r.add(
|
|
12076
|
+
r.add(shown.some((x) => !x.judged || !!x.context), `${scope}: ${plural2(m.run_sets, "run set")} measured; the newest of each journey × phone:`);
|
|
12077
|
+
for (const x of shown) {
|
|
12078
|
+
const keep = !x.judged || !!x.context;
|
|
12079
|
+
r.add(keep, ` ${x.journey} on ${x.phone_name}${f.branch ? "" : ` (${x.branch})`}: ${lane(x)}`);
|
|
12080
|
+
r.add(keep, ...resultContextLines(x.context, opts.verbose, opts.form === "short").map((line) => ` ${line}`));
|
|
12081
|
+
}
|
|
11832
12082
|
if (m.lanes.length > shown.length)
|
|
11833
12083
|
r.full(` … ${m.lanes.length - shown.length} more (--journey / --phone narrow it)`);
|
|
11834
|
-
|
|
12084
|
+
const judgedLeft = shown.filter((x) => x.judged && !x.context).length;
|
|
12085
|
+
return [
|
|
12086
|
+
...judgedLeft ? [plural2(judgedLeft, "judged lane")] : [],
|
|
12087
|
+
...m.lanes.length > shown.length ? [plural2(m.lanes.length - shown.length, "lane") + " beyond the eighth"] : []
|
|
12088
|
+
];
|
|
11835
12089
|
}
|
|
11836
12090
|
function countsLine2(l) {
|
|
11837
12091
|
const c = l.counts;
|
|
11838
12092
|
const parts = [
|
|
11839
|
-
c.worse ? `${c.worse} ${STATE_WORDS.worse.toLowerCase()}${c.not_blocking ? ` (${c.not_blocking}
|
|
12093
|
+
c.worse ? `${c.worse} ${STATE_WORDS.worse.toLowerCase()}${c.not_blocking ? ` (${c.not_blocking} ${NOT_BLOCKING})` : ""}` : "",
|
|
11840
12094
|
c.warn ? `${c.warn} ${STATE_WORDS.warning.toLowerCase()}` : "",
|
|
11841
12095
|
c.better ? `${c.better} ${STATE_WORDS.better.toLowerCase()}` : ""
|
|
11842
12096
|
].filter(Boolean);
|
|
@@ -11845,7 +12099,7 @@ function countsLine2(l) {
|
|
|
11845
12099
|
const head = parts.length ? `${parts.join(", ")}${state === "all" ? "" : `, ${STATE_WORDS2[state] ?? state}`}` : state !== "present" && state !== "all" ? `No finding ${STATE_WORDS2[state] ?? state}` : unjudged.length === l.measured.lanes.length ? `No finding, but nothing was compared yet (${[...new Set(unjudged.map((x) => x.word))].join(", ")}): measure again before calling it clean` : `Nothing ${STATE_WORDS.worse.toLowerCase()}, nothing ${STATE_WORDS.warning.toLowerCase()}, nothing ${STATE_WORDS.better.toLowerCase()}`;
|
|
11846
12100
|
const heated = c.heat_runs ?? 0;
|
|
11847
12101
|
const more = [
|
|
11848
|
-
heated ? `${INFO_WORDS.heat.toLowerCase()} in ${
|
|
12102
|
+
heated ? `${INFO_WORDS.heat.toLowerCase()} in ${plural2(heated, "run")} (run health, layer 3)` : "",
|
|
11849
12103
|
state === "present" && l.states.not_judged ? `${l.states.not_judged} more not judged in the newest run set (state not-judged)` : ""
|
|
11850
12104
|
].filter(Boolean);
|
|
11851
12105
|
const line = `${head}${more.length ? `; ${more.join("; ")}` : ""}.`;
|
|
@@ -11853,7 +12107,7 @@ function countsLine2(l) {
|
|
|
11853
12107
|
}
|
|
11854
12108
|
function formatFindingList(l, opts = {}) {
|
|
11855
12109
|
const r = new Rows;
|
|
11856
|
-
const lanesLeft = measuredLines(l, r);
|
|
12110
|
+
const lanesLeft = measuredLines(l, r, opts);
|
|
11857
12111
|
if (l.measured.newest || l.total)
|
|
11858
12112
|
r.both(countsLine2(l));
|
|
11859
12113
|
l.findings.forEach((f, i) => {
|
|
@@ -11870,11 +12124,7 @@ function formatFindingList(l, opts = {}) {
|
|
|
11870
12124
|
if (l.findings.length)
|
|
11871
12125
|
r.both("", "One finding with its evidence and a prompt: keepsnappy finding <id> --prompt");
|
|
11872
12126
|
const notWorse = l.findings.filter((f) => !isWorseState(f.state)).length;
|
|
11873
|
-
return r.render(opts.form, [
|
|
11874
|
-
...l.findings.length ? [plural(l.findings.length, "how-sure line")] : [],
|
|
11875
|
-
...notWorse ? [plural(notWorse, "place line")] : [],
|
|
11876
|
-
...lanesLeft
|
|
11877
|
-
]);
|
|
12127
|
+
return r.render(opts.form, [...l.findings.length ? [plural2(l.findings.length, "how-sure line")] : [], ...notWorse ? [plural2(notWorse, "place line")] : [], ...lanesLeft]);
|
|
11878
12128
|
}
|
|
11879
12129
|
var STATUS_LEAD = {
|
|
11880
12130
|
ok: "ok",
|
|
@@ -11890,9 +12140,10 @@ function formatBranchStatus(s, opts = {}) {
|
|
|
11890
12140
|
if (s.lanes.length)
|
|
11891
12141
|
r.both("");
|
|
11892
12142
|
for (const l of s.lanes) {
|
|
11893
|
-
const
|
|
12143
|
+
const shown = l.result === "blocks" ? `${l.word} (blocks)` : badgedWord(l.word, l.blocking);
|
|
11894
12144
|
const sha = l.commit?.sha?.slice(0, 7);
|
|
11895
|
-
r.both(` ${l.journey_title ?? l.journey} on ${l.phone_name}: ${
|
|
12145
|
+
r.both(` ${l.journey_title ?? l.journey} on ${l.phone_name}: ${shown} · ${sha ? `commit ${sha}` : "commit not recorded"} (${when(l.at)})${l.in_newest_run_set ? "" : " · older run set"}${l.waiting_for_reprocess ? `; ${waitingWords(l.waiting_for_reprocess)}` : ""}`);
|
|
12146
|
+
r.both(...resultContextLines(l.context, opts.verbose, opts.form === "short"));
|
|
11896
12147
|
for (const f of l.worst)
|
|
11897
12148
|
r.both(` ${f.id} ${f.headline}`);
|
|
11898
12149
|
if (l.present_findings > l.worst.length)
|
|
@@ -11903,8 +12154,13 @@ function formatBranchStatus(s, opts = {}) {
|
|
|
11903
12154
|
const links = s.lanes.filter((l) => l.link).length;
|
|
11904
12155
|
const heated = s.counts.heat_runs ?? 0;
|
|
11905
12156
|
const more = [
|
|
11906
|
-
[false, heated ? `${INFO_WORDS.heat.toLowerCase()} in ${
|
|
11907
|
-
[
|
|
12157
|
+
[false, heated ? `${INFO_WORDS.heat.toLowerCase()} in ${plural2(heated, "run")} (information, never blocks)` : ""],
|
|
12158
|
+
[
|
|
12159
|
+
true,
|
|
12160
|
+
s.not_judged_findings ? `${plural2(s.not_judged_findings, "finding")} not judged in the newest run set; they may still be there: keepsnappy findings --branch ${s.branch} --state not-judged` : ""
|
|
12161
|
+
],
|
|
12162
|
+
[true, statusProblemsLine(s)],
|
|
12163
|
+
[true, s.problems?.unread_lanes ? `${unreadWords(s.problems.unread_lanes)} for problems: ${staleResult(s.problems.unread_lanes)}` : ""],
|
|
11908
12164
|
[true, s.links.overview ? `Dashboard: ${s.links.overview}` : ""],
|
|
11909
12165
|
[true, s.worst.length ? "One finding with its evidence and a prompt: keepsnappy finding <id> --prompt" : ""]
|
|
11910
12166
|
];
|
|
@@ -11913,10 +12169,65 @@ function formatBranchStatus(s, opts = {}) {
|
|
|
11913
12169
|
r.both("");
|
|
11914
12170
|
for (const [short, text] of shownMore)
|
|
11915
12171
|
r.add(short, text);
|
|
11916
|
-
return r.render(opts.form, [...links ? [
|
|
12172
|
+
return r.render(opts.form, [...links ? [plural2(links, "dashboard link")] : [], ...heated ? ["the heat note"] : []]);
|
|
12173
|
+
}
|
|
12174
|
+
function statusProblemsLine(s) {
|
|
12175
|
+
const p = s.problems;
|
|
12176
|
+
if (!p?.count)
|
|
12177
|
+
return "";
|
|
12178
|
+
return `${plural2(p.places, "place")} over budget in this branch’s own runs (${p.far_count ? `${p.far_count} far over budget; ` : ""}not a regression, never blocks): list_problems, or keepsnappy problems --branch ${s.branch}`;
|
|
12179
|
+
}
|
|
12180
|
+
var unreadWords = (n) => `${n} journey × phone not read`;
|
|
12181
|
+
var staleResult = (n) => `${n === 1 ? "its stored result is" : "their stored results are"} not current`;
|
|
12182
|
+
function formatProblemList(l, opts = {}) {
|
|
12183
|
+
const r = new Rows;
|
|
12184
|
+
const b = l.filters.branch;
|
|
12185
|
+
const c = l.counts;
|
|
12186
|
+
const unread = c.unread_lanes ?? 0;
|
|
12187
|
+
const read = l.lanes.length - unread;
|
|
12188
|
+
if (!l.measured.run_sets) {
|
|
12189
|
+
r.both(`No KeepSnappy measurement on ${b === l.baseline_branch ? b : `branch ${b}`}: nothing is known about its problems.`);
|
|
12190
|
+
return r.render(opts.form);
|
|
12191
|
+
}
|
|
12192
|
+
if (!l.lanes.length) {
|
|
12193
|
+
r.both(`${b}: ${plural2(l.measured.run_sets, "run set")} measured, but no judgement yet to read problems from.`);
|
|
12194
|
+
return r.render(opts.form);
|
|
12195
|
+
}
|
|
12196
|
+
if (c.count)
|
|
12197
|
+
r.both(`${b}: ${plural2(c.places, "place")} over budget in ${b === l.baseline_branch ? "main’s" : "this branch’s"} own runs${c.far_count ? ` (${c.far_count} far over budget)` : ""}`);
|
|
12198
|
+
else if (read)
|
|
12199
|
+
r.both(`${b}: no problem over budget in the newest run set (${read} journey × phone read)`);
|
|
12200
|
+
else
|
|
12201
|
+
r.both(`${b}: no problem could be read: ${unread} journey × phone has a stored result that is not current, so nothing is known about its problems.`);
|
|
12202
|
+
r.both(l.note);
|
|
12203
|
+
if (read && unread)
|
|
12204
|
+
r.both(`${unreadWords(unread)}: ${staleResult(unread)}, so nothing is known about ${unread === 1 ? "its" : "their"} problems.`);
|
|
12205
|
+
l.problems.forEach((p, i) => {
|
|
12206
|
+
const keep = i < SHORT_PLACES;
|
|
12207
|
+
r.add(keep, "", `${l.offset + i + 1}. ${p.text}: ${p.place_name}${opts.verbose ? ` (${p.place})` : ""}${p.intermittent ? ` — ${p.note}, not counted` : ""}`);
|
|
12208
|
+
r.add(keep, ` ${p.journey_title ?? p.journey} on ${p.phone_name}, ${p.runs_text}${p.tag_word ? `, ${p.tag_word}` : ""}`);
|
|
12209
|
+
const top = p.top_components[0];
|
|
12210
|
+
const topName = componentReadNames(p.top_components)[0];
|
|
12211
|
+
const detail = [p.work, top ? `heaviest ${topName}${opts.verbose && top.key ? ` (${top.key})` : ""} ${top.self_text}` : null, p.new_component ? NEW_IN_PR_CHIP : null, p.range_text ? `${p.range_text} between runs` : null].filter(Boolean);
|
|
12212
|
+
if (detail.length)
|
|
12213
|
+
r.full(` ${detail.join("; ")}`);
|
|
12214
|
+
if (p.url)
|
|
12215
|
+
r.full(` ${p.url}`);
|
|
12216
|
+
});
|
|
12217
|
+
const shown = l.problems.length ? `Showing ${l.offset + 1}–${l.offset + l.problems.length} of ${l.total}` : "";
|
|
12218
|
+
if (l.next_offset !== null)
|
|
12219
|
+
r.both("", `${shown || `Showing none of ${l.total}`}; the rest from offset ${l.next_offset} (--offset ${l.next_offset}).`);
|
|
12220
|
+
else if (l.offset > 0)
|
|
12221
|
+
r.both("", shown ? `${shown}.` : `Offset ${l.offset} is past the last of ${l.total}.`);
|
|
12222
|
+
const detailLines = l.problems.some((p) => p.work || p.top_components.length || p.new_component || p.range_text);
|
|
12223
|
+
return r.render(opts.form, [
|
|
12224
|
+
...detailLines ? ["the work and component lines"] : [],
|
|
12225
|
+
...l.problems.some((p) => p.url) ? ["the dashboard links"] : [],
|
|
12226
|
+
...l.problems.length > SHORT_PLACES ? [`${plural2(l.problems.length - SHORT_PLACES, "problem")} beyond the fifth`] : []
|
|
12227
|
+
]);
|
|
11917
12228
|
}
|
|
11918
12229
|
var commitText = (c) => !c ? "commit not recorded" : c.title ? `“${c.title}”${c.author ? ` by ${c.author}` : ""}${c.sha ? ` (${c.sha.slice(0, 7)})` : ""}` : c.sha ? c.sha.slice(0, 7) : "commit not recorded";
|
|
11919
|
-
function formatCompare(r) {
|
|
12230
|
+
function formatCompare(r, opts = {}) {
|
|
11920
12231
|
const out = [
|
|
11921
12232
|
`Run A: ${r.a.external_id} on ${r.a.branch ?? "?"}, ${commitText(r.a.commit ? { sha: r.a.commit_sha, ...r.a.commit } : { sha: r.a.commit_sha, title: null, author: null })}`,
|
|
11922
12233
|
`Run B: ${r.b.external_id} on ${r.b.branch ?? "?"}, ${commitText(r.b.commit ? { sha: r.b.commit_sha, ...r.b.commit } : { sha: r.b.commit_sha, title: null, author: null })}`,
|
|
@@ -11926,9 +12237,42 @@ function formatCompare(r) {
|
|
|
11926
12237
|
out.push(`Measured differently: ${r.setup_differs.map((d) => `${d.field} ${JSON.stringify(d.a)} → ${JSON.stringify(d.b)}`).join(", ")}`);
|
|
11927
12238
|
for (const w of r.windows) {
|
|
11928
12239
|
const ms = w.metrics.filter((m) => m.a !== null || m.b !== null);
|
|
11929
|
-
|
|
12240
|
+
const c = w.component_page ?? w.components;
|
|
12241
|
+
if (!ms.length && !c)
|
|
11930
12242
|
continue;
|
|
11931
|
-
out.push("", w.window_name);
|
|
12243
|
+
out.push("", `${w.window_name}${opts.verbose ? ` (${w.window})` : ""}`);
|
|
12244
|
+
const singles = [...c?.added ?? [], ...c?.removed ?? [], ...w.component_page?.uncompared?.a ?? [], ...w.component_page?.uncompared?.b ?? []];
|
|
12245
|
+
const components = [...c?.rows ?? [], ...singles];
|
|
12246
|
+
const names = componentReadNames(components.map((row) => ({ key: row.key, name: row.component })));
|
|
12247
|
+
const shown = new Map(components.map((row, i) => [row.key, names[i]]));
|
|
12248
|
+
for (const row of c?.rows ?? [])
|
|
12249
|
+
out.push(` ${shown.get(row.key)}: ${row.self_ms_a === null ? "–" : exactComponentMs(row.self_ms_a)} → ${row.self_ms_b === null ? "–" : exactComponentMs(row.self_ms_b)}${opts.verbose ? ` (${row.key})` : ""}`);
|
|
12250
|
+
for (const [label, rows] of [
|
|
12251
|
+
[READ_WORDS.onlyB, c?.added ?? []],
|
|
12252
|
+
[READ_WORDS.onlyA, c?.removed ?? []]
|
|
12253
|
+
]) {
|
|
12254
|
+
for (const row of rows)
|
|
12255
|
+
out.push(` ${shown.get(row.key)}: ${label}; ${row.self_ms === null ? "–" : exactComponentMs(row.self_ms)}${opts.verbose ? ` (${row.key})` : ""}`);
|
|
12256
|
+
}
|
|
12257
|
+
const page = w.component_page;
|
|
12258
|
+
if (page) {
|
|
12259
|
+
for (const side of ["a", "b"]) {
|
|
12260
|
+
if (!page.measured[side])
|
|
12261
|
+
out.push(` ${notMeasuredText(side)}`);
|
|
12262
|
+
for (const row of page.uncompared?.[side] ?? [])
|
|
12263
|
+
out.push(` ${shown.get(row.key)}: ${side === "a" ? READ_WORDS.runA : READ_WORDS.runB} ${row.self_ms === null ? "–" : exactComponentMs(row.self_ms)}${opts.verbose ? ` (${row.key})` : ""}`);
|
|
12264
|
+
}
|
|
12265
|
+
out.push(` ${componentPageText(page.page, components.length)}`);
|
|
12266
|
+
} else if (c?.totals)
|
|
12267
|
+
out.push(` ${componentListsText(c.totals, { rows: c.rows.length, added: c.added?.length ?? 0, removed: c.removed?.length ?? 0 })}`);
|
|
12268
|
+
if (c?.omissions)
|
|
12269
|
+
for (const side of ["a", "b"]) {
|
|
12270
|
+
if (page && !page.measured[side])
|
|
12271
|
+
continue;
|
|
12272
|
+
const o = c.omissions[side];
|
|
12273
|
+
if (o.sdkOmitted === null || o.omitted === null || o.sdkOmitted > 0 || o.omitted > 0)
|
|
12274
|
+
out.push(` ${omissionText({ run_id: side === "a" ? READ_WORDS.runA : READ_WORDS.runB, ...o })}`);
|
|
12275
|
+
}
|
|
11932
12276
|
for (const m of ms)
|
|
11933
12277
|
out.push(` ${m.label.padEnd(28)}${m.a_text.padStart(10)} → ${m.b_text}`);
|
|
11934
12278
|
}
|
|
@@ -11937,7 +12281,7 @@ function formatCompare(r) {
|
|
|
11937
12281
|
}
|
|
11938
12282
|
function formatHistory(h, opts = {}) {
|
|
11939
12283
|
const r = new Rows;
|
|
11940
|
-
r.both(`${h.window_name} in ${h.journey_title ?? h.journey} on ${h.branch}: ${h.points.length ? `${
|
|
12284
|
+
r.both(`${h.window_name}${opts.verbose ? ` (${h.window})` : ""} in ${h.journey_title ?? h.journey} on ${h.branch}: ${h.points.length ? `${plural2(h.points.length, "run")}, oldest first` : h.note ?? "no runs"}`);
|
|
11941
12285
|
const ms = h.measures.slice(0, 4);
|
|
11942
12286
|
if (h.points.length) {
|
|
11943
12287
|
r.both(` ${"when".padEnd(17)}${ms.map((m) => (m.label.length > 22 ? `${m.label.slice(0, 21)}…` : m.label).padStart(24)).join("")} commit`);
|
|
@@ -11948,11 +12292,11 @@ function formatHistory(h, opts = {}) {
|
|
|
11948
12292
|
if (h.findings.length)
|
|
11949
12293
|
r.both("", "Findings on this window:", ...h.findings.map((f) => ` ${f.answer.headline} — ${f.where.branch} (${f.id})`));
|
|
11950
12294
|
const older = Math.max(0, h.points.length - SHORT_POINTS);
|
|
11951
|
-
return r.render(opts.form, older ? [
|
|
12295
|
+
return r.render(opts.form, older ? [plural2(older, "older run")] : []);
|
|
11952
12296
|
}
|
|
11953
12297
|
function formatCommits(c) {
|
|
11954
12298
|
return [
|
|
11955
|
-
`${
|
|
12299
|
+
`${plural2(c.commits.length, "measured commit")} (${plural2(c.sessions, "session")}) on ${c.branch}, oldest first:`,
|
|
11956
12300
|
...c.commits.map((x) => ` ${when(x.first_measured_at)} ${commitText(x)}${x.pull_request?.number ? `, PR #${x.pull_request.number}` : ""}${x.sessions > 1 ? `; ${x.sessions} sessions, the last ${when(x.last_measured_at)}` : ""}`),
|
|
11957
12301
|
c.note
|
|
11958
12302
|
].join(`
|
|
@@ -11994,8 +12338,39 @@ function formatSessionPlaces(p, opts = {}) {
|
|
|
11994
12338
|
p.places.forEach((place, i) => {
|
|
11995
12339
|
const m = place.measures.find((x) => x.measure === by);
|
|
11996
12340
|
const value = m ? `${fieldValueText(by, m.median)} (p90 ${fieldValueText(by, m.p90)}, n ${m.n})` : `no ${by}`;
|
|
11997
|
-
const
|
|
11998
|
-
|
|
12341
|
+
const names = componentReadNames(place.components);
|
|
12342
|
+
const comps = place.components.map((c, i) => `${names[i]}${opts.verbose && c.file ? ` (${c.file})` : ""} ${valueText("self_ms", c.self_ms.median)} (p90 ${valueText("self_ms", c.self_ms.p90)}${c.renders_median !== null ? `, ${plural2(c.renders_median, "render")}` : ""})`);
|
|
12343
|
+
r.both(`${p.offset + i + 1}. ${place.window_name}${opts.verbose ? ` (${place.window})` : ""}: ${value}`);
|
|
12344
|
+
if (place.components_page)
|
|
12345
|
+
r.detail(` ${componentPageText(place.components_page, place.components.length)}`);
|
|
12346
|
+
for (const o of (place.omissions ?? []).filter((o) => o.sdkOmitted === null || o.omitted === null || o.sdkOmitted > 0 || o.omitted > 0))
|
|
12347
|
+
r.detail(` ${omissionText(o)}`);
|
|
12348
|
+
const slowest = place.measures.find((m) => m.measure === "commit_max_ms");
|
|
12349
|
+
if (by !== "commit_max_ms" && slowest)
|
|
12350
|
+
r.detail(` ${READ_WORDS.slowest}: ${fieldValueText("commit_max_ms", slowest.median)}`);
|
|
12351
|
+
for (const detail of place.details ?? []) {
|
|
12352
|
+
if (detail.slowestCommit)
|
|
12353
|
+
r.detail(` ${detail.run_id}: ${READ_WORDS.slowest}: ${exactComponentMs(detail.slowestCommit.ms)}`);
|
|
12354
|
+
r.detail(` ${detail.run_id}: ${OUTSIDE_APP_NAME}: ${detail.outside ? exactComponentMs(detail.outside.selfMs) : "–"}; ${OTHER_RENDER_WORK_NAME}: ${detail.remainderMs === null ? "–" : exactComponentMs(detail.remainderMs)}`);
|
|
12355
|
+
for (const l of detail.outside?.rolledUp ?? [])
|
|
12356
|
+
r.detail(` ${libraryLineText(l)}`);
|
|
12357
|
+
if (detail.outside?.rolledUpRest)
|
|
12358
|
+
r.detail(` ${rolledUpRestText(detail.outside.rolledUpRest)}`);
|
|
12359
|
+
}
|
|
12360
|
+
for (const [ci, c] of place.components.entries()) {
|
|
12361
|
+
if (c.detail === "ms-only" && !c.runs?.length)
|
|
12362
|
+
r.detail(` ${names[ci]}: ${READ_WORDS.detailUnavailable}`);
|
|
12363
|
+
for (const d of c.runs ?? [])
|
|
12364
|
+
if (d.detail === "ms-only")
|
|
12365
|
+
r.detail(` ${names[ci]} · ${d.run_id}: ${d.selfMs === null ? "–" : exactComponentMs(d.selfMs)}; ${READ_WORDS.detailUnavailable}`);
|
|
12366
|
+
for (const d of c.runs ?? [])
|
|
12367
|
+
if (d.detail === "full") {
|
|
12368
|
+
if (d.parent)
|
|
12369
|
+
r.detail(` ${names[ci]} · ${d.run_id}: ${READ_WORDS.parent} ${componentReadName({ key: d.parent, name: d.parent })}`);
|
|
12370
|
+
if (d.rolledUpMs !== undefined && d.selfMs !== null)
|
|
12371
|
+
r.detail(` ${names[ci]} · ${d.run_id}: ${rolledUpText(d.rolledUpMs, d.selfMs)}`, ...(d.rolledUp ?? []).map((l) => ` ${libraryLineText(l)}`), ...d.rolledUpRest ? [` ${rolledUpRestText(d.rolledUpRest)}`] : []);
|
|
12372
|
+
}
|
|
12373
|
+
}
|
|
11999
12374
|
if (comps.length) {
|
|
12000
12375
|
r.full(` Components: ${list(comps)}`);
|
|
12001
12376
|
componentLines++;
|
|
@@ -12005,7 +12380,7 @@ function formatSessionPlaces(p, opts = {}) {
|
|
|
12005
12380
|
r.both(p.total ? `Offset ${p.offset} is past the last of ${p.total} places.` : "No place with stored statistics in this session matches.");
|
|
12006
12381
|
else if (p.next_offset !== null)
|
|
12007
12382
|
r.both("", `Showing ${p.offset + 1}–${p.offset + p.places.length} of ${p.total}; the rest from offset ${p.next_offset} (--offset ${p.next_offset}).`);
|
|
12008
|
-
return r.render(opts.form, componentLines ? [
|
|
12383
|
+
return r.render(opts.form, componentLines ? [plural2(componentLines, "component line")] : []);
|
|
12009
12384
|
}
|
|
12010
12385
|
function formatLayers(l) {
|
|
12011
12386
|
return [
|
|
@@ -12027,7 +12402,7 @@ function formatMainSessions(h, opts = {}) {
|
|
|
12027
12402
|
let otherChanges = 0;
|
|
12028
12403
|
for (const s of h.sessions) {
|
|
12029
12404
|
const k = recent.has(s);
|
|
12030
|
-
r.add(k, `${when(s.at)} ${s.run_set_id} ${commitText(s.commit ? { ...s.commit, author: null } : null)} (${
|
|
12405
|
+
r.add(k, `${when(s.at)} ${s.run_set_id} ${commitText(s.commit ? { ...s.commit, author: null } : null)} (${plural2(s.runs, "run")})`);
|
|
12031
12406
|
if (!s.places)
|
|
12032
12407
|
r.add(k, ` ${MAIN_SUMMARY_PENDING}${s.summary_reason ? ` (${s.summary_reason})` : ""}`);
|
|
12033
12408
|
else
|
|
@@ -12039,13 +12414,16 @@ function formatMainSessions(h, opts = {}) {
|
|
|
12039
12414
|
for (const v of h.versions.filter((v) => v.from_run_set_id === s.run_set_id))
|
|
12040
12415
|
r.add(k, ` Version: ${v.label}; ${mainCutWords(v)}`);
|
|
12041
12416
|
for (const c of h.changes.filter((c) => c.from_run_set_id === s.run_set_id)) {
|
|
12042
|
-
r.add(s === latest, ` Main change: ${c.place} ${c.ref}, ${c.before_median ?? "–"} → ${c.after_median ?? "–"}`);
|
|
12417
|
+
r.add(s === latest, ` Main change: ${windowQualified(c.place)}${opts.verbose ? ` (${c.place}; ${c.ref})` : ""} ${storedRefWords(c.ref, h.changes.filter((x) => x.place === c.place && x.from_run_set_id === c.from_run_set_id).map((x) => x.ref))}, ${c.before_median ?? "–"} → ${c.after_median ?? "–"}`);
|
|
12043
12418
|
if (k && s !== latest)
|
|
12044
12419
|
otherChanges++;
|
|
12045
12420
|
}
|
|
12046
12421
|
}
|
|
12047
12422
|
const older = h.sessions.length - recent.size;
|
|
12048
|
-
return r.render(opts.form, [
|
|
12423
|
+
return r.render(opts.form, [
|
|
12424
|
+
...older ? [plural2(older, "older session")] : [],
|
|
12425
|
+
...otherChanges ? [plural2(otherChanges, "main change") + " of the other recent sessions"] : []
|
|
12426
|
+
]);
|
|
12049
12427
|
}
|
|
12050
12428
|
function formatMainSessionPlaces(p, opts = {}) {
|
|
12051
12429
|
const r = new Rows;
|
|
@@ -12054,12 +12432,12 @@ function formatMainSessionPlaces(p, opts = {}) {
|
|
|
12054
12432
|
const baselineSessions = p.baseline_sessions;
|
|
12055
12433
|
r.both(`Main session places: tiers are verdicts vs ${p.baseline_sessions === undefined ? "earlier sessions" : `the ${p.baseline_sessions} sessions before`}.`);
|
|
12056
12434
|
for (const x of p.places)
|
|
12057
|
-
r.add(x.tier !== "same", ` ${x.place} ${x.ref}: ${MAIN_TIER_WORDS[x.tier]}${x.reason ? `; ${mainPlaceReasonWords(x.reason, { baseline: { sessions: baselineSessions } })}` : ""}`);
|
|
12435
|
+
r.add(x.tier !== "same", ` ${windowQualified(x.place)}${opts.verbose ? ` (${x.place}; ${x.ref})` : ""} ${storedRefWords(x.ref, p.places.filter((row) => row.place === x.place).map((row) => row.ref))}: ${MAIN_TIER_WORDS[x.tier]}${x.reason ? `; ${mainPlaceReasonWords(x.reason, { baseline: { sessions: baselineSessions } })}` : ""}`);
|
|
12058
12436
|
const inNorm = p.places.filter((x) => x.tier === "same").length;
|
|
12059
|
-
return r.render(opts.form, inNorm ? [
|
|
12437
|
+
return r.render(opts.form, inNorm ? [plural2(inNorm, "place") + " in norm"] : []);
|
|
12060
12438
|
}
|
|
12061
12439
|
var signed = (n) => `${n > 0 ? "+" : ""}${Number(n.toFixed(3))}`;
|
|
12062
|
-
var previousLine = (p) => ` ${p.place} ${p.ref}${p.devices ? ` [devices ${p.devices.before.join(", ") || "–"} → ${p.devices.after.join(", ") || "–"}]` : ""}: ${p.status === "compared" ? "" : `${p.status}; `}${p.before ?? "–"} → ${p.after ?? "–"}; delta ${p.delta === null ? "–" : signed(p.delta)}; ${p.percent_change === null ? "change undefined" : `${signed(p.percent_change)}%`}; n ${p.n.before ?? "–"} → ${p.n.after ?? "–"}`;
|
|
12440
|
+
var previousLine = (p, verbose = false, peers = []) => ` ${windowQualified(p.place)}${verbose ? ` (${p.place}; ${p.ref})` : ""} ${storedRefWords(p.ref, peers)}${p.devices ? ` [devices ${p.devices.before.join(", ") || "–"} → ${p.devices.after.join(", ") || "–"}]` : ""}: ${p.status === "compared" ? "" : `${p.status === "gone" ? p.ref.startsWith("b:component:") ? JOURNEY_ROW_WORDS.removed : JOURNEY_ROW_WORDS.notInRun : p.status === "new" ? JOURNEY_ROW_WORDS.new : p.status}; `}${p.before ?? "–"} → ${p.after ?? "–"}; delta ${p.delta === null ? "–" : signed(p.delta)}; ${p.percent_change === null ? "change undefined" : `${signed(p.percent_change)}%`}; n ${p.n.before ?? "–"} → ${p.n.after ?? "–"}`;
|
|
12063
12441
|
function formatMainVsPrevious(m, opts = {}) {
|
|
12064
12442
|
const r = new Rows().both(`Information only: main session ${m.run_set_id} vs ${m.previous_run_set_id ?? "no previous session"}.`, `Measure: ${m.measure}; ${m.status}.`);
|
|
12065
12443
|
if (m.cut === "unknown")
|
|
@@ -12071,12 +12449,12 @@ function formatMainVsPrevious(m, opts = {}) {
|
|
|
12071
12449
|
if (m.stale.length)
|
|
12072
12450
|
r.both(`stale: ${m.stale.join(", ")}`);
|
|
12073
12451
|
if (m.journey)
|
|
12074
|
-
r.both("Whole journey:", previousLine(m.journey));
|
|
12075
|
-
m.places.forEach((p, i) => r.add(i < SHORT_PLACES || p.status !== "compared", previousLine(p)));
|
|
12452
|
+
r.both("Whole journey:", previousLine(m.journey, opts.verbose));
|
|
12453
|
+
m.places.forEach((p, i) => r.add(i < SHORT_PLACES || p.status !== "compared", previousLine(p, opts.verbose, m.places.filter((row) => row.place === p.place).map((row) => row.ref))));
|
|
12076
12454
|
if (m.next_offset !== null)
|
|
12077
12455
|
r.both(`Showing ${m.offset + 1}–${m.offset + m.places.length} of ${m.total}; next offset ${m.next_offset}.`);
|
|
12078
12456
|
const smaller = m.places.filter((p, i) => i >= SHORT_PLACES && p.status === "compared").length;
|
|
12079
|
-
return r.render(opts.form, smaller ? [
|
|
12457
|
+
return r.render(opts.form, smaller ? [plural2(smaller, "smaller mover")] : []);
|
|
12080
12458
|
}
|
|
12081
12459
|
function formatPlaceTrend(t, opts = {}) {
|
|
12082
12460
|
const r = new Rows().both(`${t.journey} · ${t.phone.model} · ${t.baseline_branch}: stored per-session statistics`);
|
|
@@ -12099,7 +12477,7 @@ function formatPlaceTrend(t, opts = {}) {
|
|
|
12099
12477
|
const unit = place.unit ? ` ${place.unit}` : "";
|
|
12100
12478
|
if (!k)
|
|
12101
12479
|
olderSessions.add(session.run_set_id);
|
|
12102
|
-
r.add(k, `${session.completed_at?.slice(0, 10) ?? "date unknown"} ${session.commit.sha?.slice(0, 7) ?? "commit unknown"} · ${place.window} · ${place.label} (${place.ref}): median ${value.median}${unit}, MAD ${value.mad}${unit}, n=${value.n}`);
|
|
12480
|
+
r.add(k, `${session.completed_at?.slice(0, 10) ?? "date unknown"} ${session.commit.sha?.slice(0, 7) ?? "commit unknown"} · ${windowQualified(place.window)}${opts.verbose ? ` (${place.window}; ${place.ref})` : ""} · ${place.label} (${storedRefWords(place.ref, t.places.filter((row) => row.window === place.window).map((row) => row.ref))}): median ${value.median}${unit}, MAD ${value.mad}${unit}, n=${value.n}`);
|
|
12103
12481
|
}
|
|
12104
12482
|
}
|
|
12105
12483
|
if (!t.sessions.length)
|
|
@@ -12108,7 +12486,10 @@ function formatPlaceTrend(t, opts = {}) {
|
|
|
12108
12486
|
r.both(`Not computed: ${t.not_computed.join(", ")}`);
|
|
12109
12487
|
if (t.stale.length)
|
|
12110
12488
|
r.both(`Stale: ${t.stale.join(", ")}`);
|
|
12111
|
-
return r.render(opts.form, [
|
|
12489
|
+
return r.render(opts.form, [
|
|
12490
|
+
...olderSessions.size ? [plural2(olderSessions.size, "older session")] : [],
|
|
12491
|
+
...olderRestarts ? [plural2(olderRestarts, "older restart marker")] : []
|
|
12492
|
+
]);
|
|
12112
12493
|
}
|
|
12113
12494
|
|
|
12114
12495
|
// packages/mcp/dist/src/client.js
|
|
@@ -12133,7 +12514,13 @@ async function get(t, path, query = {}) {
|
|
|
12133
12514
|
}
|
|
12134
12515
|
const r = await (t.fetch ?? fetch)(u, { headers: { "x-keepsnappy-key": t.key, accept: "application/json" } });
|
|
12135
12516
|
const text = await r.text();
|
|
12136
|
-
|
|
12517
|
+
let body;
|
|
12518
|
+
try {
|
|
12519
|
+
body = text ? JSON.parse(text) : {};
|
|
12520
|
+
} catch {
|
|
12521
|
+
const page = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
12522
|
+
throw new ApiError(r.status, "not_json", `the API answered ${r.status} with a body that is not JSON${page ? `: ${page}` : ""}`);
|
|
12523
|
+
}
|
|
12137
12524
|
if (!r.ok) {
|
|
12138
12525
|
const code = typeof body.error === "string" ? body.error : String(r.status);
|
|
12139
12526
|
const message = typeof body.message === "string" ? body.message : code;
|
|
@@ -12144,8 +12531,9 @@ async function get(t, path, query = {}) {
|
|
|
12144
12531
|
}
|
|
12145
12532
|
var listFindings = (t, f = {}) => get(t, "v1/findings", { branch: f.branch, main: f.main ? "true" : undefined, journey: f.journey, phone: f.phone, state: f.state, limit: f.limit, offset: f.offset || undefined });
|
|
12146
12533
|
var branchStatus = (t, branch) => get(t, "v1/branches/status", { branch });
|
|
12534
|
+
var listProblems = (t, f = {}) => get(t, "v1/problems", { branch: f.branch, main: f.main ? "true" : undefined, journey: f.journey, phone: f.phone, limit: f.limit, offset: f.offset || undefined });
|
|
12147
12535
|
var getFinding = (t, id) => get(t, `v1/findings/${encodeURIComponent(id)}`);
|
|
12148
|
-
var compareRuns = (t, a, b) => get(t, "v1/compare", { a, b });
|
|
12536
|
+
var compareRuns = (t, a, b, page = {}) => get(t, "v1/compare", { a, b, ...page });
|
|
12149
12537
|
var windowHistory = (t, q) => get(t, "v1/history", q);
|
|
12150
12538
|
var commitsBetween = (t, q) => get(t, "v1/commits", q);
|
|
12151
12539
|
var layers = (t) => get(t, "v1/layers");
|
|
@@ -12159,7 +12547,10 @@ var sessionPlaces = (t, f) => get(t, "v1/sessions/places", {
|
|
|
12159
12547
|
window: f.window,
|
|
12160
12548
|
limit: f.limit,
|
|
12161
12549
|
offset: f.offset || undefined,
|
|
12162
|
-
top_components: f.top_components
|
|
12550
|
+
top_components: f.top_components,
|
|
12551
|
+
detail: f.detail,
|
|
12552
|
+
component_offset: f.component_offset,
|
|
12553
|
+
component_limit: f.component_limit
|
|
12163
12554
|
});
|
|
12164
12555
|
var SESSION_SORTS = ["median", "p90", "mad"];
|
|
12165
12556
|
var mainSessions = (t, q) => get(t, "v1/main/sessions", { ...q });
|
|
@@ -12173,6 +12564,8 @@ var placeTrend = (t, q) => get(t, "v1/places/trend", { ...q });
|
|
|
12173
12564
|
// apps/agent/src/findings.ts
|
|
12174
12565
|
var FINDINGS_USAGE = `keepsnappy findings [--branch NAME | --main] [--journey KEY] [--phone MODEL] [--state present|not-judged|resolved|all] [--limit N] [--offset N] [--short] [--json]
|
|
12175
12566
|
status --branch NAME [--short] [--json] (is my PR ok? one answer: blocked / nothing blocks / ok, per journey × phone the word, the ids of the worst findings, dashboard links; never measured and not judged are not ok)
|
|
12567
|
+
problems --branch NAME | --main [--journey KEY] [--phone MODEL] [--limit N] [--offset N] [--short] [--json]
|
|
12568
|
+
(the single problems "Over budget" / "Far over budget" in the branch's own runs, whatever main does: not a regression, never blocking)
|
|
12176
12569
|
finding <id> [--prompt] [--short] [--json] (--prompt: only the hand-off for a coding agent; with --json the whole finding)
|
|
12177
12570
|
compare <runA> <runB> [--json] (run ids or external ids; one run against one, no statistics)
|
|
12178
12571
|
history <journey> <window> [--phone MODEL] [--short] [--json] (exact window id, e.g. "press:live.chat.send")
|
|
@@ -12183,20 +12576,14 @@ var FINDINGS_USAGE = `keepsnappy findings [--branch NAME | --main] [--journey KE
|
|
|
12183
12576
|
layers [--json]
|
|
12184
12577
|
trend <journey> <window words...> --phone MODEL [--window PLACE ...] [--ref REF ...] [--sessions N] [--short] [--json]
|
|
12185
12578
|
(the words after the journey are one window name; more windows with repeated --window)
|
|
12186
|
-
|
|
12579
|
+
${READ_USAGE.components}
|
|
12580
|
+
${READ_USAGE.verbose}
|
|
12581
|
+
${READ_USAGE.detailFlag}
|
|
12582
|
+
--short (findings, status, problems, finding, history, places, main, trend: what decides, without the evidence detail; the same as the MCP tools' form short; --json is unchanged)
|
|
12187
12583
|
(key: KEEPSNAPPY_KEY or --key-env NAME; API: --api URL or KEEPSNAPPY_API)`;
|
|
12188
|
-
|
|
12189
|
-
class UsageError extends ConfigError {
|
|
12190
|
-
constructor(lines) {
|
|
12191
|
-
super(lines);
|
|
12192
|
-
this.message = lines.join(`
|
|
12193
|
-
`);
|
|
12194
|
-
this.name = "UsageError";
|
|
12195
|
-
}
|
|
12196
|
-
}
|
|
12197
12584
|
var STATES = ["present", "not-judged", "resolved", "all"];
|
|
12198
|
-
var SHORT_COMMANDS = new Set(["findings", "status", "finding", "history", "places", "main", "trend"]);
|
|
12199
|
-
var FINDINGS_COMMANDS = new Set(["findings", "status", "finding", "compare", "history", "commits", "places", "layers", "main", "trend"]);
|
|
12585
|
+
var SHORT_COMMANDS = new Set(["findings", "status", "problems", "finding", "history", "places", "main", "trend"]);
|
|
12586
|
+
var FINDINGS_COMMANDS = new Set(["findings", "status", "problems", "finding", "compare", "history", "commits", "places", "layers", "main", "trend"]);
|
|
12200
12587
|
async function findingsCommand(cmd, argv, rt) {
|
|
12201
12588
|
const args = (() => {
|
|
12202
12589
|
try {
|
|
@@ -12204,6 +12591,10 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
12204
12591
|
args: argv,
|
|
12205
12592
|
allowPositionals: true,
|
|
12206
12593
|
options: {
|
|
12594
|
+
verbose: { type: "boolean" },
|
|
12595
|
+
detail: { type: "boolean" },
|
|
12596
|
+
page: { type: "string" },
|
|
12597
|
+
all: { type: "boolean" },
|
|
12207
12598
|
branch: { type: "string" },
|
|
12208
12599
|
main: { type: "boolean" },
|
|
12209
12600
|
journey: { type: "string" },
|
|
@@ -12237,7 +12628,7 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
12237
12628
|
if (!key)
|
|
12238
12629
|
throw new ConfigError([`a project key with read scope is needed: set the environment variable ${keyEnv}`]);
|
|
12239
12630
|
const t = { url: o.api ?? envValue(rt.env, "API") ?? DEFAULT_API_URL, key, ...rt.fetch ? { fetch: rt.fetch } : {} };
|
|
12240
|
-
const answerForm = { form: o.short ? "short" : "full" };
|
|
12631
|
+
const answerForm = { form: o.short ? "short" : "full", verbose: o.verbose };
|
|
12241
12632
|
const out = (v, text) => {
|
|
12242
12633
|
rt.print(o.json ? JSON.stringify(v, null, 2) : text(v, answerForm));
|
|
12243
12634
|
return 0;
|
|
@@ -12254,6 +12645,11 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
12254
12645
|
const offset = o.offset === undefined ? undefined : Number(o.offset);
|
|
12255
12646
|
if (offset !== undefined && (!Number.isInteger(offset) || offset < 0))
|
|
12256
12647
|
throw new UsageError(["--offset must be a whole number >= 0"]);
|
|
12648
|
+
const page = o.page === undefined ? undefined : Number(o.page);
|
|
12649
|
+
if (page !== undefined && (!["places", "compare"].includes(cmd) || !Number.isInteger(page) || page < 1))
|
|
12650
|
+
throw new UsageError([READ_USAGE.page]);
|
|
12651
|
+
if (o.all && (!["places", "compare"].includes(cmd) || page !== undefined || o.offset !== undefined || o.short))
|
|
12652
|
+
throw new UsageError([READ_USAGE.all]);
|
|
12257
12653
|
switch (cmd) {
|
|
12258
12654
|
case "main": {
|
|
12259
12655
|
if (!o.journey)
|
|
@@ -12292,6 +12688,19 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
12292
12688
|
if (!o.branch)
|
|
12293
12689
|
throw new UsageError(["keepsnappy status needs --branch NAME", FINDINGS_USAGE]);
|
|
12294
12690
|
return out(await branchStatus(t, o.branch), formatBranchStatus);
|
|
12691
|
+
case "problems":
|
|
12692
|
+
if (!o.branch && !o.main)
|
|
12693
|
+
throw new UsageError(["keepsnappy problems needs --branch NAME or --main", FINDINGS_USAGE]);
|
|
12694
|
+
if (o.branch && o.main)
|
|
12695
|
+
throw new UsageError(["give --branch or --main, not both"]);
|
|
12696
|
+
return out(await listProblems(t, {
|
|
12697
|
+
...o.branch ? { branch: o.branch } : {},
|
|
12698
|
+
...o.main ? { main: true } : {},
|
|
12699
|
+
...o.journey ? { journey: o.journey } : {},
|
|
12700
|
+
...o.phone ? { phone: o.phone } : {},
|
|
12701
|
+
...limit ? { limit } : {},
|
|
12702
|
+
...offset ? { offset } : {}
|
|
12703
|
+
}), formatProblemList);
|
|
12295
12704
|
case "finding": {
|
|
12296
12705
|
need(1, "<id>");
|
|
12297
12706
|
const f = await getFinding(t, p[0]);
|
|
@@ -12301,9 +12710,15 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
12301
12710
|
}
|
|
12302
12711
|
return out(f, formatFinding);
|
|
12303
12712
|
}
|
|
12304
|
-
case "compare":
|
|
12713
|
+
case "compare": {
|
|
12305
12714
|
need(2, "<runA> <runB>");
|
|
12306
|
-
|
|
12715
|
+
const size = o["top-components"] === undefined ? 12 : Number(o["top-components"]);
|
|
12716
|
+
if (!Number.isInteger(size) || size < 1 || size > 200)
|
|
12717
|
+
throw new UsageError([READ_USAGE.compareTop]);
|
|
12718
|
+
const query = page || o.all || o["top-components"] !== undefined ? { component_offset: ((page ?? 1) - 1) * size, component_limit: size } : {};
|
|
12719
|
+
const compared = await compareRuns(t, p[0], p[1], query);
|
|
12720
|
+
return out(o.all ? await allCompare(t, compared, size) : compared, formatCompare);
|
|
12721
|
+
}
|
|
12307
12722
|
case "history":
|
|
12308
12723
|
need(2, "<journey> <window>");
|
|
12309
12724
|
return out(await windowHistory(t, { journey: p[0], window: p.slice(1).join(" "), ...o.phone ? { phone: o.phone } : {}, ...limit ? { limit } : {} }), formatHistory);
|
|
@@ -12315,9 +12730,12 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
12315
12730
|
if (sort && !SESSION_SORTS.includes(sort))
|
|
12316
12731
|
throw new UsageError([`--sort is ${SESSION_SORTS.join(", ")}`]);
|
|
12317
12732
|
const top = o["top-components"] === undefined ? undefined : Number(o["top-components"]);
|
|
12318
|
-
if (top !== undefined && (!Number.isInteger(top) || top < 0 || top > 20))
|
|
12319
|
-
throw new UsageError([
|
|
12320
|
-
|
|
12733
|
+
if (top !== undefined && (!Number.isInteger(top) || top < (page || o.all ? 1 : 0) || top > (page || o.all ? 200 : 20)))
|
|
12734
|
+
throw new UsageError([READ_USAGE.top]);
|
|
12735
|
+
if ((o.window?.length ?? 0) > 1)
|
|
12736
|
+
throw new UsageError(["keepsnappy places takes one --window (more windows: keepsnappy trend)", FINDINGS_USAGE]);
|
|
12737
|
+
const query = {
|
|
12738
|
+
...o.verbose || o.detail ? { detail: true } : {},
|
|
12321
12739
|
...o["run-set"] ? { run_set: o["run-set"] } : {},
|
|
12322
12740
|
...o.journey ? { journey: o.journey } : {},
|
|
12323
12741
|
...o.phone ? { phone: o.phone } : {},
|
|
@@ -12327,8 +12745,9 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
12327
12745
|
...o.window?.length ? { window: o.window[0] } : {},
|
|
12328
12746
|
...limit ? { limit } : {},
|
|
12329
12747
|
...offset ? { offset } : {},
|
|
12330
|
-
...top !== undefined ? { top_components: top } : {}
|
|
12331
|
-
}
|
|
12748
|
+
...page || o.all ? { component_offset: ((page ?? 1) - 1) * (top ?? 5), component_limit: top ?? 5 } : top !== undefined ? { top_components: top } : {}
|
|
12749
|
+
};
|
|
12750
|
+
return out(o.all ? await allSessionPlaces(t, query) : await sessionPlaces(t, query), formatSessionPlaces);
|
|
12332
12751
|
}
|
|
12333
12752
|
case "layers":
|
|
12334
12753
|
return out(await layers(t), formatLayers);
|
|
@@ -12347,6 +12766,100 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
12347
12766
|
throw new UsageError([FINDINGS_USAGE]);
|
|
12348
12767
|
}
|
|
12349
12768
|
}
|
|
12769
|
+
async function allSessionPlaces(target, query) {
|
|
12770
|
+
let first = await sessionPlaces(target, query);
|
|
12771
|
+
if (first.places.some((p) => !p.components_page))
|
|
12772
|
+
throw new UsageError([READ_USAGE.paging]);
|
|
12773
|
+
if (first.session.not_computed || first.session.stale)
|
|
12774
|
+
return first;
|
|
12775
|
+
const result = { ...first, places: [], offset: 0, next_offset: null };
|
|
12776
|
+
const pinned = { ...query, run_set: first.session.run_set_id, branch: undefined };
|
|
12777
|
+
const visited = new Set;
|
|
12778
|
+
for (;; ) {
|
|
12779
|
+
const places = first.places.map((p) => ({ ...p, components: [...p.components] }));
|
|
12780
|
+
let response = first;
|
|
12781
|
+
for (;; ) {
|
|
12782
|
+
const next = response.places.flatMap((p) => p.components_page?.next_offset ?? []);
|
|
12783
|
+
if (!next.length)
|
|
12784
|
+
break;
|
|
12785
|
+
const component_offset = Math.min(...next);
|
|
12786
|
+
const mark = `${first.offset}:${component_offset}`;
|
|
12787
|
+
if (visited.has(mark))
|
|
12788
|
+
throw new UsageError([READ_USAGE.paging]);
|
|
12789
|
+
visited.add(mark);
|
|
12790
|
+
response = await sessionPlaces(target, { ...pinned, offset: first.offset, component_offset });
|
|
12791
|
+
for (const p of places) {
|
|
12792
|
+
const more = response.places.find((x) => x.window === p.window);
|
|
12793
|
+
if (!more?.components_page)
|
|
12794
|
+
throw new UsageError([READ_USAGE.paging]);
|
|
12795
|
+
const keys = new Set(p.components.map((c) => c.key));
|
|
12796
|
+
p.components.push(...more.components.filter((c) => !keys.has(c.key)));
|
|
12797
|
+
p.components_page = more.components_page;
|
|
12798
|
+
}
|
|
12799
|
+
}
|
|
12800
|
+
result.places.push(...places.map((p) => ({ ...p, ...p.components_page ? { components_page: { ...p.components_page, offset: 0, limit: p.components.length, next_offset: null } } : {} })));
|
|
12801
|
+
if (places.some((p) => p.components_page && p.components.length !== p.components_page.total))
|
|
12802
|
+
throw new UsageError([READ_USAGE.paging]);
|
|
12803
|
+
if (first.next_offset === null) {
|
|
12804
|
+
if (result.places.length !== result.total)
|
|
12805
|
+
throw new UsageError([READ_USAGE.paging]);
|
|
12806
|
+
return result;
|
|
12807
|
+
}
|
|
12808
|
+
const mark = `places:${first.next_offset}`;
|
|
12809
|
+
if (visited.has(mark))
|
|
12810
|
+
throw new UsageError([READ_USAGE.paging]);
|
|
12811
|
+
visited.add(mark);
|
|
12812
|
+
first = await sessionPlaces(target, { ...pinned, offset: first.next_offset, component_offset: 0 });
|
|
12813
|
+
}
|
|
12814
|
+
}
|
|
12815
|
+
async function allCompare(target, result, size) {
|
|
12816
|
+
let response = result;
|
|
12817
|
+
const visited = new Set;
|
|
12818
|
+
for (;; ) {
|
|
12819
|
+
if (response.windows.some((w) => w.components && !w.component_page))
|
|
12820
|
+
throw new UsageError([READ_USAGE.paging]);
|
|
12821
|
+
const offsets = response.windows.flatMap((w) => w.component_page?.page.next_offset ?? []);
|
|
12822
|
+
if (!offsets.length)
|
|
12823
|
+
break;
|
|
12824
|
+
const offset = Math.min(...offsets);
|
|
12825
|
+
if (visited.has(offset))
|
|
12826
|
+
throw new UsageError([READ_USAGE.paging]);
|
|
12827
|
+
visited.add(offset);
|
|
12828
|
+
response = await compareRuns(target, result.a.id, result.b.id, { component_offset: offset, component_limit: size });
|
|
12829
|
+
for (const w of result.windows) {
|
|
12830
|
+
const c = w.component_page;
|
|
12831
|
+
if (!c && !w.components)
|
|
12832
|
+
continue;
|
|
12833
|
+
const more = response.windows.find((x) => x.window === w.window)?.component_page;
|
|
12834
|
+
if (!c || !more)
|
|
12835
|
+
throw new UsageError([READ_USAGE.paging]);
|
|
12836
|
+
const keys = new Set(c.rows.map((row) => row.key));
|
|
12837
|
+
c.rows.push(...more.rows.filter((row) => !keys.has(row.key)));
|
|
12838
|
+
for (const field of ["added", "removed"]) {
|
|
12839
|
+
const existing = c[field] ?? [];
|
|
12840
|
+
const known = new Set(existing.map((row) => row.key));
|
|
12841
|
+
c[field] = [...existing, ...(more[field] ?? []).filter((row) => !known.has(row.key))];
|
|
12842
|
+
}
|
|
12843
|
+
for (const side of ["a", "b"]) {
|
|
12844
|
+
if (!c.uncompared || !more.uncompared)
|
|
12845
|
+
continue;
|
|
12846
|
+
const known = new Set(c.uncompared[side].map((row) => row.key));
|
|
12847
|
+
c.uncompared[side].push(...more.uncompared[side].filter((row) => !known.has(row.key)));
|
|
12848
|
+
}
|
|
12849
|
+
c.page = more.page;
|
|
12850
|
+
}
|
|
12851
|
+
}
|
|
12852
|
+
for (const w of result.windows) {
|
|
12853
|
+
const c = w.component_page;
|
|
12854
|
+
if (!c)
|
|
12855
|
+
continue;
|
|
12856
|
+
const count = c.rows.length + (c.added?.length ?? 0) + (c.removed?.length ?? 0) + (c.uncompared?.a.length ?? 0) + (c.uncompared?.b.length ?? 0);
|
|
12857
|
+
if (count !== c.page.total)
|
|
12858
|
+
throw new UsageError([READ_USAGE.paging]);
|
|
12859
|
+
c.page = { ...c.page, offset: 0, limit: count, next_offset: null };
|
|
12860
|
+
}
|
|
12861
|
+
return result;
|
|
12862
|
+
}
|
|
12350
12863
|
|
|
12351
12864
|
// apps/agent/src/cli.ts
|
|
12352
12865
|
var USAGE = `keepsnappy <command>
|
|
@@ -12354,6 +12867,8 @@ var USAGE = `keepsnappy <command>
|
|
|
12354
12867
|
[--upload [--wait] [--fail-on-worse] [--ingest-url URL] [--commit SHA] [--branch NAME] [--base-commit SHA]]
|
|
12355
12868
|
[--device-model NAME --device-os VERSION] [--scenario NAME] [--replay <out dir>[:ARM] ...]
|
|
12356
12869
|
[--no-markers] [-- <e2e command...>]
|
|
12870
|
+
(--fail-on-worse: exit 3 only when the verdict computed on that run set is Worse and blocking; closing never computes one,
|
|
12871
|
+
so with no computed verdict it checks nothing: "Verdict not computed yet (computed on demand): --fail-on-worse checked nothing", exit 0)
|
|
12357
12872
|
measure --repeat N [--arms A=<app|bundle>,B=<app|bundle>] [--arm-env 'A:KEY=VAL' ...] [--bundle-id ID] [--platform ios-sim|ios-device]
|
|
12358
12873
|
[--device UDID] [--out DIR] [--timeout SEC] [--ri-capture observations.ndjson] [--mem-window NAME]
|
|
12359
12874
|
[--clock-offset-ms N] [--no-markers]
|
|
@@ -12366,6 +12881,7 @@ var USAGE = `keepsnappy <command>
|
|
|
12366
12881
|
[--driver appium --journey-module FILE] [--driver-version V] [--app-build-id ID] [--harness-version V] [--journey-version V] [--flag-set LABEL] (setup fingerprint, plan 51)
|
|
12367
12882
|
[--sdk-args k=v&...] (over the run defaults profile=1 diag=1; tokens.js carries them to the flows' activation link)
|
|
12368
12883
|
farm close --state <file> [--wait [--fail-on-worse]] [--bs-build browserstack.ios.json] [--settle-seconds 60] [--max-wait-seconds 300] [--out DIR]
|
|
12884
|
+
(--fail-on-worse as for run: with no verdict computed on the run set it checks nothing)
|
|
12369
12885
|
farm run --app <eas:ID|file.ipa|file.apk|bs://ID|custom_id:ID> --journeys a,b [--repeat 5] --device "iPhone 15 Pro-17" [--platform ios|android] [--driver maestro|appium]
|
|
12370
12886
|
[--tunnel-for localhost:PORT ...] [--env KEY=VALUE ...] (values may use {tunnel:PORT} {run_id} {out} {tokens_js} {run_set_id})
|
|
12371
12887
|
maestro: --suite-dir DIR --execute FLOW [--tokens-path flows/keepsnappy/tokens.js] [--maestro-version V]
|
|
@@ -12408,74 +12924,80 @@ async function main2(argv) {
|
|
|
12408
12924
|
if (cmd && LINK_COMMANDS.has(cmd))
|
|
12409
12925
|
return linksCommand(cmd, rest, { print: console.log });
|
|
12410
12926
|
const deps = nodeDeps();
|
|
12411
|
-
const { values: o, positionals } =
|
|
12412
|
-
|
|
12413
|
-
|
|
12414
|
-
|
|
12415
|
-
|
|
12416
|
-
|
|
12417
|
-
|
|
12418
|
-
|
|
12419
|
-
|
|
12420
|
-
|
|
12421
|
-
|
|
12422
|
-
|
|
12423
|
-
|
|
12424
|
-
|
|
12425
|
-
|
|
12426
|
-
|
|
12427
|
-
|
|
12428
|
-
|
|
12429
|
-
|
|
12430
|
-
|
|
12431
|
-
|
|
12432
|
-
|
|
12433
|
-
|
|
12434
|
-
|
|
12435
|
-
|
|
12436
|
-
|
|
12437
|
-
|
|
12438
|
-
|
|
12439
|
-
|
|
12440
|
-
|
|
12441
|
-
|
|
12442
|
-
|
|
12443
|
-
|
|
12444
|
-
|
|
12445
|
-
|
|
12446
|
-
|
|
12447
|
-
|
|
12448
|
-
|
|
12449
|
-
|
|
12450
|
-
|
|
12451
|
-
|
|
12452
|
-
|
|
12453
|
-
|
|
12454
|
-
|
|
12455
|
-
|
|
12456
|
-
|
|
12457
|
-
|
|
12458
|
-
|
|
12459
|
-
|
|
12460
|
-
|
|
12461
|
-
|
|
12462
|
-
|
|
12463
|
-
|
|
12464
|
-
|
|
12465
|
-
|
|
12466
|
-
|
|
12467
|
-
|
|
12468
|
-
|
|
12469
|
-
|
|
12470
|
-
|
|
12471
|
-
|
|
12472
|
-
|
|
12473
|
-
|
|
12474
|
-
|
|
12475
|
-
|
|
12476
|
-
|
|
12927
|
+
const { values: o, positionals } = (() => {
|
|
12928
|
+
try {
|
|
12929
|
+
return parseArgs4({
|
|
12930
|
+
args: rest,
|
|
12931
|
+
allowPositionals: true,
|
|
12932
|
+
options: {
|
|
12933
|
+
repeat: { type: "string" },
|
|
12934
|
+
arms: { type: "string" },
|
|
12935
|
+
"arm-env": { type: "string", multiple: true },
|
|
12936
|
+
platform: { type: "string" },
|
|
12937
|
+
device: { type: "string" },
|
|
12938
|
+
out: { type: "string" },
|
|
12939
|
+
"bundle-id": { type: "string" },
|
|
12940
|
+
timeout: { type: "string" },
|
|
12941
|
+
reps: { type: "string" },
|
|
12942
|
+
boot: { type: "string" },
|
|
12943
|
+
"ri-capture": { type: "string" },
|
|
12944
|
+
"mem-window": { type: "string" },
|
|
12945
|
+
"clock-offset-ms": { type: "string" },
|
|
12946
|
+
"no-markers": { type: "boolean" },
|
|
12947
|
+
config: { type: "string" },
|
|
12948
|
+
upload: { type: "boolean" },
|
|
12949
|
+
"ingest-url": { type: "string" },
|
|
12950
|
+
commit: { type: "string" },
|
|
12951
|
+
branch: { type: "string" },
|
|
12952
|
+
"base-commit": { type: "string" },
|
|
12953
|
+
wait: { type: "boolean" },
|
|
12954
|
+
"fail-on-worse": { type: "boolean" },
|
|
12955
|
+
scenario: { type: "string" },
|
|
12956
|
+
"device-model": { type: "string" },
|
|
12957
|
+
"device-os": { type: "string" },
|
|
12958
|
+
"device-id": { type: "string" },
|
|
12959
|
+
replay: { type: "string", multiple: true },
|
|
12960
|
+
api: { type: "string" },
|
|
12961
|
+
"key-env": { type: "string" },
|
|
12962
|
+
"device-provider": { type: "string" },
|
|
12963
|
+
journeys: { type: "string" },
|
|
12964
|
+
"tokens-js": { type: "string" },
|
|
12965
|
+
state: { type: "string" },
|
|
12966
|
+
"ttl-seconds": { type: "string" },
|
|
12967
|
+
"bs-build": { type: "string" },
|
|
12968
|
+
"settle-seconds": { type: "string" },
|
|
12969
|
+
"max-wait-seconds": { type: "string" },
|
|
12970
|
+
flows: { type: "string" },
|
|
12971
|
+
"completion-marker": { type: "boolean" },
|
|
12972
|
+
"driver-version": { type: "string" },
|
|
12973
|
+
"app-build-id": { type: "string" },
|
|
12974
|
+
"harness-version": { type: "string" },
|
|
12975
|
+
"journey-version": { type: "string" },
|
|
12976
|
+
"flag-set": { type: "string" },
|
|
12977
|
+
app: { type: "string" },
|
|
12978
|
+
driver: { type: "string" },
|
|
12979
|
+
"tunnel-for": { type: "string", multiple: true },
|
|
12980
|
+
env: { type: "string", multiple: true },
|
|
12981
|
+
"driver-script": { type: "string" },
|
|
12982
|
+
"journey-module": { type: "string" },
|
|
12983
|
+
"driver-arg": { type: "string", multiple: true },
|
|
12984
|
+
"suite-dir": { type: "string" },
|
|
12985
|
+
execute: { type: "string" },
|
|
12986
|
+
"tokens-path": { type: "string" },
|
|
12987
|
+
"maestro-version": { type: "string" },
|
|
12988
|
+
"timeout-minutes": { type: "string" },
|
|
12989
|
+
"kill-after-seconds": { type: "string" },
|
|
12990
|
+
"poll-seconds": { type: "string" },
|
|
12991
|
+
"custom-id": { type: "string" },
|
|
12992
|
+
"run-id": { type: "string" },
|
|
12993
|
+
"dry-run": { type: "boolean" },
|
|
12994
|
+
"sdk-args": { type: "string" }
|
|
12995
|
+
}
|
|
12996
|
+
});
|
|
12997
|
+
} catch (e) {
|
|
12998
|
+
throw new UsageError([e.message, "keepsnappy --help lists the commands and their flags"]);
|
|
12477
12999
|
}
|
|
12478
|
-
});
|
|
13000
|
+
})();
|
|
12479
13001
|
if (cmd === "doctor") {
|
|
12480
13002
|
console.log(formatChecks(await doctor(deps.spawner, process.version)));
|
|
12481
13003
|
return 0;
|
|
@@ -12495,12 +13017,12 @@ async function main2(argv) {
|
|
|
12495
13017
|
if (cmd === "measure") {
|
|
12496
13018
|
const repeat = Number(o.repeat);
|
|
12497
13019
|
if (!Number.isInteger(repeat) || repeat < 1)
|
|
12498
|
-
throw new
|
|
13020
|
+
throw new ConfigError(["--repeat N (>= 1) is required"]);
|
|
12499
13021
|
const platform = o.platform ?? "ios-sim";
|
|
12500
13022
|
if (platform !== "ios-sim" && platform !== "ios-device")
|
|
12501
|
-
throw new
|
|
13023
|
+
throw new ConfigError([`unknown --platform ${platform}`]);
|
|
12502
13024
|
if (platform === "ios-device" && !o.device)
|
|
12503
|
-
throw new
|
|
13025
|
+
throw new ConfigError(["--device <udid> is required for ios-device"]);
|
|
12504
13026
|
const arms = parseArmEnv(o["arm-env"], parseArms(o.arms));
|
|
12505
13027
|
const out = resolve8(o.out ?? `keepsnappy-${new Date().toISOString().replace(/[:.]/g, "-")}`);
|
|
12506
13028
|
const udid = o.device ?? "booted";
|
|
@@ -12534,7 +13056,7 @@ async function main2(argv) {
|
|
|
12534
13056
|
if (cmd === "extract") {
|
|
12535
13057
|
const dir = positionals[0];
|
|
12536
13058
|
if (!dir)
|
|
12537
|
-
throw new
|
|
13059
|
+
throw new ConfigError(["extract needs <out-dir>"]);
|
|
12538
13060
|
const ri = o["ri-capture"] ? new FileRiSource(deps, resolve8(o["ri-capture"])) : undefined;
|
|
12539
13061
|
const rows = await extract(deps, resolve8(dir), ri, o["mem-window"], o["clock-offset-ms"] ? Number(o["clock-offset-ms"]) : undefined);
|
|
12540
13062
|
console.log(`${rows.length} rows -> ${resolve8(dir, "metrics.csv")}`);
|
|
@@ -12543,7 +13065,7 @@ async function main2(argv) {
|
|
|
12543
13065
|
if (cmd === "aa") {
|
|
12544
13066
|
const dir = positionals[0];
|
|
12545
13067
|
if (!dir)
|
|
12546
|
-
throw new
|
|
13068
|
+
throw new ConfigError(["aa needs <out-dir>"]);
|
|
12547
13069
|
const config = { ...o.reps ? { reps: Number(o.reps) } : {}, ...o.boot ? { nBoot: Number(o.boot) } : {} };
|
|
12548
13070
|
console.log((await aa(deps, resolve8(dir), { config })).table);
|
|
12549
13071
|
return 0;
|