@glubean/cli 0.8.2 โ 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/dry-run.d.ts +100 -0
- package/dist/commands/dry-run.d.ts.map +1 -0
- package/dist/commands/dry-run.js +237 -0
- package/dist/commands/dry-run.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +23 -9
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/run.d.ts +18 -0
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +135 -19
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/sync.d.ts +21 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +297 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/lib/only-selectors.d.ts +61 -0
- package/dist/lib/only-selectors.d.ts.map +1 -0
- package/dist/lib/only-selectors.js +79 -0
- package/dist/lib/only-selectors.js.map +1 -0
- package/dist/lib/upload.d.ts +53 -0
- package/dist/lib/upload.d.ts.map +1 -1
- package/dist/lib/upload.js +196 -9
- package/dist/lib/upload.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +69 -0
- package/dist/main.js.map +1 -1
- package/package.json +6 -6
- package/dist/lib/env.d.ts +0 -29
- package/dist/lib/env.d.ts.map +0 -1
- package/dist/lib/env.js +0 -59
- package/dist/lib/env.js.map +0 -1
package/dist/commands/run.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { bootstrap, evaluateThresholds, MetricCollector, ProjectRunner, buildRunContext, } from "@glubean/runner";
|
|
2
|
+
import { buildOnlySelectorsFromFlags, deriveRerunSelectors } from "../lib/only-selectors.js";
|
|
2
3
|
import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
4
5
|
import { stat, readdir, readFile, writeFile, mkdir, rm } from "node:fs/promises";
|
|
@@ -519,8 +520,8 @@ export async function runCommand(target, options = {}) {
|
|
|
519
520
|
const interactive = capabilityProfile.browser;
|
|
520
521
|
const traceCollector = [];
|
|
521
522
|
console.log(`\n${colors.bold}${colors.blue}๐งช Glubean Test Runner${colors.reset}\n`);
|
|
522
|
-
|
|
523
|
-
|
|
523
|
+
let testFiles = await resolveTestFiles(target);
|
|
524
|
+
let isMultiFile = testFiles.length > 1;
|
|
524
525
|
// Single string view of target for serialization / display paths
|
|
525
526
|
// (result.json, junit, traces). Multi-suite passes an array; join with
|
|
526
527
|
// ", " so downstream consumers still see a printable target field.
|
|
@@ -544,6 +545,59 @@ export async function runCommand(target, options = {}) {
|
|
|
544
545
|
}
|
|
545
546
|
const startDir = testFiles[0].substring(0, testFiles[0].lastIndexOf("/"));
|
|
546
547
|
const { rootDir } = await findProjectConfig(startDir);
|
|
548
|
+
// โโ B2 M3 โ `{id, rowIndex}` "only" selectors โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
549
|
+
// Validate the --only-id / --row / --rerun-failed combo up front (single
|
|
550
|
+
// gate), then resolve the active selector set. `--rerun-failed` reads the
|
|
551
|
+
// previous run and narrows discovery to the files that failed; `--only-id` /
|
|
552
|
+
// `--row` narrow `testsToRun` below by template-matching concrete ids against
|
|
553
|
+
// static `.each` template ids. Either way the precise per-row filter runs in
|
|
554
|
+
// the harness subprocess via the GLUBEAN_RUNNER_ONLY_SELECTORS env channel.
|
|
555
|
+
let onlySelectors = [];
|
|
556
|
+
const selectorFlags = buildOnlySelectorsFromFlags({
|
|
557
|
+
onlyId: options.onlyId,
|
|
558
|
+
row: options.row,
|
|
559
|
+
rerunFailed: options.rerunFailed,
|
|
560
|
+
});
|
|
561
|
+
if (!selectorFlags.ok) {
|
|
562
|
+
console.error(`\n${colors.red}โ ${selectorFlags.error}${colors.reset}\n`);
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
if (options.rerunFailed) {
|
|
566
|
+
const lastRunPath = resolve(rootDir, ".glubean", "last-run.result.json");
|
|
567
|
+
let lastRun;
|
|
568
|
+
try {
|
|
569
|
+
lastRun = JSON.parse(await readFile(lastRunPath, "utf-8"));
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
console.error(`\n${colors.red}โ No previous run found. Run \`glubean run\` first.${colors.reset}\n` +
|
|
573
|
+
`${colors.dim}--rerun-failed reads ${lastRunPath}.${colors.reset}\n`);
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
const { selectors, files } = deriveRerunSelectors({ tests: lastRun.tests ?? [] });
|
|
577
|
+
if (selectors.length === 0) {
|
|
578
|
+
console.log(`\n${colors.green}โ Last run had no failures โ nothing to rerun.${colors.reset}\n`);
|
|
579
|
+
process.exit(0);
|
|
580
|
+
}
|
|
581
|
+
// Narrow discovery to the files that contained a failure. `files` were
|
|
582
|
+
// written relative to rootDir (resultPayload.tests.filePath) โ rootDir is the
|
|
583
|
+
// stable basis (the dir holding .glubean/), so rerun resolves correctly even
|
|
584
|
+
// when invoked from a different cwd than the original run. Resolve both sides
|
|
585
|
+
// to absolute for an exact match against the discovered testFiles.
|
|
586
|
+
const failedFilesAbs = new Set(files.map((f) => resolve(rootDir, f)));
|
|
587
|
+
testFiles = testFiles.filter((f) => failedFilesAbs.has(resolve(f)));
|
|
588
|
+
isMultiFile = testFiles.length > 1;
|
|
589
|
+
if (testFiles.length === 0) {
|
|
590
|
+
console.error(`\n${colors.red}โ --rerun-failed: none of the ${failedFilesAbs.size} failed file(s) ` +
|
|
591
|
+
`from the last run are in the current target.${colors.reset}\n`);
|
|
592
|
+
process.exit(1);
|
|
593
|
+
}
|
|
594
|
+
onlySelectors = selectors;
|
|
595
|
+
console.log(`${colors.dim}--rerun-failed: ${selectors.length} failed test(s) across ` +
|
|
596
|
+
`${testFiles.length} file(s)${colors.reset}\n`);
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
onlySelectors = selectorFlags.selectors;
|
|
600
|
+
}
|
|
547
601
|
// Config consolidation (docs/06 P2): the legacy package.json `glubean`
|
|
548
602
|
// flat-shape is no longer read. Profile runs get run/redaction/thresholds
|
|
549
603
|
// from the resolved plan (threaded via `options`); non-profile target runs
|
|
@@ -797,7 +851,7 @@ export async function runCommand(target, options = {}) {
|
|
|
797
851
|
}
|
|
798
852
|
const hasTags = options.tags && options.tags.length > 0;
|
|
799
853
|
const hasExcludeTags = options.excludeTags && options.excludeTags.length > 0;
|
|
800
|
-
|
|
854
|
+
let testsToRun = allFileTests.filter((ft) => {
|
|
801
855
|
const tc = ft.test;
|
|
802
856
|
if (tc.meta.skip)
|
|
803
857
|
return false;
|
|
@@ -811,6 +865,26 @@ export async function runCommand(target, options = {}) {
|
|
|
811
865
|
return false;
|
|
812
866
|
return true;
|
|
813
867
|
});
|
|
868
|
+
// B2 M3 โ narrow testsToRun for --only-id / --row. Selector ids are CONCRETE
|
|
869
|
+
// (e.g. `user-0`); a `.each` export appears in discovery under its TEMPLATE id
|
|
870
|
+
// (e.g. `user-$index`). Template-match each selector id against the discovered
|
|
871
|
+
// template ids so concrete selectors reach the right export โ the harness then
|
|
872
|
+
// applies the precise per-row filter at runtime. (Rerun narrows by file above.)
|
|
873
|
+
if (onlySelectors.length > 0 && !options.rerunFailed) {
|
|
874
|
+
const selectorIds = Array.from(new Set(onlySelectors.map((s) => (typeof s === "string" ? s : s.id))));
|
|
875
|
+
const indexed = testsToRun.map((ft) => ({ id: ft.test.meta.id, ft }));
|
|
876
|
+
const kept = new Set();
|
|
877
|
+
for (const selId of selectorIds) {
|
|
878
|
+
const match = findTemplateMatch(indexed, selId);
|
|
879
|
+
if (match)
|
|
880
|
+
kept.add(match.ft);
|
|
881
|
+
}
|
|
882
|
+
testsToRun = testsToRun.filter((ft) => kept.has(ft));
|
|
883
|
+
if (testsToRun.length === 0) {
|
|
884
|
+
console.error(`\n${colors.red}โ No tests match --only-id ${selectorIds.join(", ")}${colors.reset}\n`);
|
|
885
|
+
process.exit(1);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
814
888
|
if (testsToRun.length === 0) {
|
|
815
889
|
if (options.filter || hasTags) {
|
|
816
890
|
const parts = [];
|
|
@@ -952,6 +1026,15 @@ export async function runCommand(target, options = {}) {
|
|
|
952
1026
|
delete process.env["GLUBEAN_RUNNER_BOOTSTRAP_INPUT_MAP"];
|
|
953
1027
|
delete process.env["GLUBEAN_RUNNER_FORCE_STANDALONE_IDS"];
|
|
954
1028
|
}
|
|
1029
|
+
// B2 M3 โ hand the resolved selector set to the harness subprocess (it applies
|
|
1030
|
+
// the precise per-row filter at runtime). Clear it otherwise so a stale value
|
|
1031
|
+
// never leaks across in-process invocations (parity with the input maps above).
|
|
1032
|
+
if (onlySelectors.length > 0) {
|
|
1033
|
+
process.env["GLUBEAN_RUNNER_ONLY_SELECTORS"] = JSON.stringify(onlySelectors);
|
|
1034
|
+
}
|
|
1035
|
+
else {
|
|
1036
|
+
delete process.env["GLUBEAN_RUNNER_ONLY_SELECTORS"];
|
|
1037
|
+
}
|
|
955
1038
|
if (options.pick) {
|
|
956
1039
|
process.env.GLUBEAN_PICK = options.pick;
|
|
957
1040
|
console.log(`${colors.dim} pick: ${options.pick}${colors.reset}`);
|
|
@@ -1024,6 +1107,7 @@ export async function runCommand(target, options = {}) {
|
|
|
1024
1107
|
let currentTestItems;
|
|
1025
1108
|
let testId = "";
|
|
1026
1109
|
let testName = "";
|
|
1110
|
+
let testRowIndex = undefined;
|
|
1027
1111
|
let testItem = null;
|
|
1028
1112
|
let startTime = Date.now();
|
|
1029
1113
|
let testEvents = [];
|
|
@@ -1089,6 +1173,7 @@ export async function runCommand(target, options = {}) {
|
|
|
1089
1173
|
success: skippedClean ? true : finalSuccess,
|
|
1090
1174
|
durationMs: duration,
|
|
1091
1175
|
groupId: testItem?.meta.groupId,
|
|
1176
|
+
rowIndex: testRowIndex,
|
|
1092
1177
|
});
|
|
1093
1178
|
addLogEntry("result", skippedClean ? "SKIPPED" : finalSuccess ? "PASSED" : "FAILED", {
|
|
1094
1179
|
duration,
|
|
@@ -1406,6 +1491,10 @@ export async function runCommand(target, options = {}) {
|
|
|
1406
1491
|
(currentTestItems ? findFileTestByRuntimeId(currentTestItems, event.id) : undefined);
|
|
1407
1492
|
testId = event.id;
|
|
1408
1493
|
testName = entry?.test.meta.name || event.name || event.id;
|
|
1494
|
+
// rowIndex (B2 M3): the runtime start event is authoritative for the
|
|
1495
|
+
// per-row `.each` index (static discovery only sees the template id,
|
|
1496
|
+
// so it carries no rowIndex). undefined for non-each tests.
|
|
1497
|
+
testRowIndex = event.rowIndex;
|
|
1409
1498
|
testItem = entry?.test || null;
|
|
1410
1499
|
startTime = Date.now();
|
|
1411
1500
|
testEvents = [];
|
|
@@ -1871,6 +1960,12 @@ export async function runCommand(target, options = {}) {
|
|
|
1871
1960
|
success: r.success,
|
|
1872
1961
|
durationMs: r.durationMs,
|
|
1873
1962
|
events: r.events,
|
|
1963
|
+
// B2 M3 โ persist rowIndex + filePath so `--rerun-failed` can reconstruct
|
|
1964
|
+
// the failed `{id, rowIndex}` selector set and narrow to the failed files.
|
|
1965
|
+
// filePath is relative to rootDir (the stable .glubean/ basis), NOT cwd, so
|
|
1966
|
+
// rerun resolves correctly when run from a different working directory.
|
|
1967
|
+
...(r.rowIndex !== undefined && { rowIndex: r.rowIndex }),
|
|
1968
|
+
filePath: relative(rootDir, r.filePath),
|
|
1874
1969
|
})),
|
|
1875
1970
|
...(thresholdSummary && { thresholds: thresholdSummary }),
|
|
1876
1971
|
...(options.meta && Object.keys(options.meta).length > 0 && { customMetadata: options.meta }),
|
|
@@ -1924,29 +2019,30 @@ export async function runCommand(target, options = {}) {
|
|
|
1924
2019
|
}
|
|
1925
2020
|
}
|
|
1926
2021
|
// โโ Screenshot paths โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
2022
|
+
// This run's exact screenshot files, pulled from the `browser:screenshot`
|
|
2023
|
+
// event stream. Doubles as the upload whitelist below so `--upload` attaches
|
|
2024
|
+
// only THIS run's screenshots, not every file in the shared dir (ART1).
|
|
2025
|
+
const screenshotPaths = [];
|
|
2026
|
+
for (const run of collectedRuns) {
|
|
2027
|
+
for (const event of run.events) {
|
|
2028
|
+
if (event.type !== "event")
|
|
2029
|
+
continue;
|
|
2030
|
+
const ev = event.data;
|
|
2031
|
+
if (ev.type === "browser:screenshot" && typeof ev.data?.path === "string") {
|
|
2032
|
+
screenshotPaths.push(resolve(rootDir, ev.data.path));
|
|
1937
2033
|
}
|
|
1938
2034
|
}
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
}
|
|
1943
|
-
console.log();
|
|
2035
|
+
}
|
|
2036
|
+
if (screenshotPaths.length > 0) {
|
|
2037
|
+
for (const p of screenshotPaths) {
|
|
2038
|
+
console.log(`${colors.dim}Screenshot: ${colors.reset}${p}`);
|
|
1944
2039
|
}
|
|
2040
|
+
console.log();
|
|
1945
2041
|
}
|
|
1946
2042
|
// โโ Cloud upload โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1947
2043
|
if (options.upload) {
|
|
1948
2044
|
const { resolveToken, resolveProjectId, resolveApiUrl } = await import("../lib/auth.js");
|
|
1949
|
-
const { uploadToCloud } = await import("../lib/upload.js");
|
|
2045
|
+
const { uploadToCloud, removeUploadedScreenshots } = await import("../lib/upload.js");
|
|
1950
2046
|
const authOpts = {
|
|
1951
2047
|
token: options.token,
|
|
1952
2048
|
project: options.project,
|
|
@@ -2096,7 +2192,27 @@ export async function runCommand(target, options = {}) {
|
|
|
2096
2192
|
targetId,
|
|
2097
2193
|
envFile: effectiveRun.envFile,
|
|
2098
2194
|
rootDir,
|
|
2195
|
+
// Upload only THIS run's screenshots (whitelist), not the whole
|
|
2196
|
+
// shared `.glubean/screenshots` dir which accumulates prior runs.
|
|
2197
|
+
screenshotPaths,
|
|
2099
2198
|
});
|
|
2199
|
+
// ART1-B โ the shared screenshots dir only ever grows, so once the
|
|
2200
|
+
// Cloud confirmed it received this run's screenshots, delete the local
|
|
2201
|
+
// copies. Deletes ONLY uploadedFiles โฉ screenshotPaths (both server-
|
|
2202
|
+
// confirmed and provably this run's), realpath-contained to
|
|
2203
|
+
// `.glubean/screenshots`, and only while the on-disk file still has
|
|
2204
|
+
// its upload-time stat identity (re-checked just before each unlink)
|
|
2205
|
+
// โ a failed/partial upload keeps its files, and a concurrently
|
|
2206
|
+
// recreated path is kept.
|
|
2207
|
+
if (!options.keepLocal &&
|
|
2208
|
+
uploadReceipt.artifactUpload.status === "uploaded" &&
|
|
2209
|
+
uploadReceipt.artifactUpload.uploadedFiles?.length &&
|
|
2210
|
+
screenshotPaths.length > 0) {
|
|
2211
|
+
const { removed } = await removeUploadedScreenshots(rootDir, screenshotPaths, uploadReceipt.artifactUpload.uploadedFiles);
|
|
2212
|
+
if (removed > 0) {
|
|
2213
|
+
console.log(`${colors.dim}Cleaned up ${removed} uploaded screenshot(s) from .glubean/screenshots (use --keep-local to keep them)${colors.reset}`);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2100
2216
|
if (options.uploadReceiptJson) {
|
|
2101
2217
|
const receiptPath = resolveOutputPath(options.uploadReceiptJson, process.cwd());
|
|
2102
2218
|
await mkdir(dirname(receiptPath), { recursive: true });
|