@piwitests/reporter 0.41.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cli/index.js +1275 -469
- package/dist/global-setup-module.js +68 -24
- package/dist/index.d.ts +18 -5
- package/dist/index.js +418 -103
- package/dist/internal/capture/capture-fixtures.js +50 -7
- package/dist/internal/capture/locator-healing.d.ts +1 -7
- package/dist/internal/capture/locator-healing.js +0 -2
- package/dist/internal/capture/pick-on-failure.js +3 -3
- package/package.json +1 -1
- package/templates/skills/investigate-failure/SKILL.md +1 -1
- package/templates/skills/setup-piwi/SKILL.md +1 -1
- package/templates/skills/stabilize-flaky-tests/SKILL.md +30 -15
package/dist/cli/index.js
CHANGED
|
@@ -1924,43 +1924,43 @@ var Checker = class {
|
|
|
1924
1924
|
constructor() {
|
|
1925
1925
|
this.errors = [];
|
|
1926
1926
|
}
|
|
1927
|
-
fail(
|
|
1928
|
-
if (this.errors.length < 20) this.errors.push(`${
|
|
1927
|
+
fail(path19, message) {
|
|
1928
|
+
if (this.errors.length < 20) this.errors.push(`${path19}: ${message}`);
|
|
1929
1929
|
}
|
|
1930
|
-
text(v,
|
|
1930
|
+
text(v, path19, max, nullable) {
|
|
1931
1931
|
if (v == null) {
|
|
1932
|
-
if (!nullable) this.fail(
|
|
1932
|
+
if (!nullable) this.fail(path19, "is required");
|
|
1933
1933
|
return null;
|
|
1934
1934
|
}
|
|
1935
1935
|
if (typeof v !== "string") {
|
|
1936
|
-
this.fail(
|
|
1936
|
+
this.fail(path19, "must be a string");
|
|
1937
1937
|
return null;
|
|
1938
1938
|
}
|
|
1939
|
-
if (v.length > max) this.fail(
|
|
1939
|
+
if (v.length > max) this.fail(path19, `is longer than ${max} characters`);
|
|
1940
1940
|
return v;
|
|
1941
1941
|
}
|
|
1942
|
-
number(v,
|
|
1942
|
+
number(v, path19) {
|
|
1943
1943
|
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
1944
|
-
this.fail(
|
|
1944
|
+
this.fail(path19, "must be a number");
|
|
1945
1945
|
return 0;
|
|
1946
1946
|
}
|
|
1947
1947
|
return v;
|
|
1948
1948
|
}
|
|
1949
1949
|
};
|
|
1950
|
-
function checkTarget(c, v,
|
|
1950
|
+
function checkTarget(c, v, path19) {
|
|
1951
1951
|
if (v == null) return null;
|
|
1952
1952
|
if (!isObject(v)) {
|
|
1953
|
-
c.fail(
|
|
1953
|
+
c.fail(path19, "must be an object");
|
|
1954
1954
|
return null;
|
|
1955
1955
|
}
|
|
1956
1956
|
const alternativesIn = v.alternatives ?? [];
|
|
1957
1957
|
const alternatives = [];
|
|
1958
|
-
if (!Array.isArray(alternativesIn)) c.fail(`${
|
|
1958
|
+
if (!Array.isArray(alternativesIn)) c.fail(`${path19}.alternatives`, "must be a list");
|
|
1959
1959
|
else if (alternativesIn.length > STEPS_LIMITS.alternatives)
|
|
1960
|
-
c.fail(`${
|
|
1960
|
+
c.fail(`${path19}.alternatives`, `holds more than ${STEPS_LIMITS.alternatives}`);
|
|
1961
1961
|
else {
|
|
1962
1962
|
alternativesIn.forEach((alt, i) => {
|
|
1963
|
-
const at = `${
|
|
1963
|
+
const at = `${path19}.alternatives[${i}]`;
|
|
1964
1964
|
if (!isObject(alt)) return c.fail(at, "must be an object");
|
|
1965
1965
|
const locator = c.text(alt.locator, `${at}.locator`, STEPS_LIMITS.textLength, false);
|
|
1966
1966
|
const safe = locator ? safeLocator(locator) : null;
|
|
@@ -1974,67 +1974,67 @@ function checkTarget(c, v, path16) {
|
|
|
1974
1974
|
});
|
|
1975
1975
|
}
|
|
1976
1976
|
return {
|
|
1977
|
-
tagName: c.text(v.tagName, `${
|
|
1978
|
-
role: c.text(v.role, `${
|
|
1979
|
-
accessibleName: c.text(v.accessibleName, `${
|
|
1980
|
-
testId: c.text(v.testId, `${
|
|
1981
|
-
text: c.text(v.text, `${
|
|
1977
|
+
tagName: c.text(v.tagName, `${path19}.tagName`, 64, false) ?? "",
|
|
1978
|
+
role: c.text(v.role, `${path19}.role`, 64, true),
|
|
1979
|
+
accessibleName: c.text(v.accessibleName, `${path19}.accessibleName`, STEPS_LIMITS.textLength, true),
|
|
1980
|
+
testId: c.text(v.testId, `${path19}.testId`, STEPS_LIMITS.textLength, true),
|
|
1981
|
+
text: c.text(v.text, `${path19}.text`, STEPS_LIMITS.textLength, true),
|
|
1982
1982
|
alternatives
|
|
1983
1983
|
};
|
|
1984
1984
|
}
|
|
1985
|
-
function checkAssertion(c, v,
|
|
1985
|
+
function checkAssertion(c, v, path19) {
|
|
1986
1986
|
if (!isObject(v)) {
|
|
1987
|
-
c.fail(
|
|
1987
|
+
c.fail(path19, "is required on an assert step");
|
|
1988
1988
|
return void 0;
|
|
1989
1989
|
}
|
|
1990
1990
|
const matcher = v.matcher;
|
|
1991
1991
|
if (!ASSERTION_MATCHERS.includes(matcher)) {
|
|
1992
|
-
c.fail(`${
|
|
1992
|
+
c.fail(`${path19}.matcher`, `must be one of ${ASSERTION_MATCHERS.join(", ")}`);
|
|
1993
1993
|
return void 0;
|
|
1994
1994
|
}
|
|
1995
|
-
const expected = c.text(v.expected, `${
|
|
1995
|
+
const expected = c.text(v.expected, `${path19}.expected`, STEPS_LIMITS.valueLength, !VALUE_MATCHERS.has(matcher));
|
|
1996
1996
|
return {
|
|
1997
1997
|
matcher,
|
|
1998
1998
|
expected: VALUE_MATCHERS.has(matcher) ? expected : null,
|
|
1999
|
-
actual: c.text(v.actual, `${
|
|
1999
|
+
actual: c.text(v.actual, `${path19}.actual`, STEPS_LIMITS.valueLength, true),
|
|
2000
2000
|
negated: v.negated === true,
|
|
2001
|
-
note: c.text(v.note, `${
|
|
2001
|
+
note: c.text(v.note, `${path19}.note`, STEPS_LIMITS.valueLength, true)
|
|
2002
2002
|
};
|
|
2003
2003
|
}
|
|
2004
|
-
function checkStep(c, v,
|
|
2004
|
+
function checkStep(c, v, path19) {
|
|
2005
2005
|
if (!isObject(v)) {
|
|
2006
|
-
c.fail(
|
|
2006
|
+
c.fail(path19, "must be an object");
|
|
2007
2007
|
return null;
|
|
2008
2008
|
}
|
|
2009
2009
|
const action = v.action;
|
|
2010
2010
|
if (!ACTIONS.has(action)) {
|
|
2011
|
-
c.fail(`${
|
|
2011
|
+
c.fail(`${path19}.action`, "is not a known action");
|
|
2012
2012
|
return null;
|
|
2013
2013
|
}
|
|
2014
2014
|
const step = {
|
|
2015
2015
|
action: action === "assertVisible" ? "assert" : action,
|
|
2016
|
-
target: checkTarget(c, v.target, `${
|
|
2017
|
-
value: c.text(v.value, `${
|
|
2016
|
+
target: checkTarget(c, v.target, `${path19}.target`),
|
|
2017
|
+
value: c.text(v.value, `${path19}.value`, STEPS_LIMITS.valueLength, true),
|
|
2018
2018
|
redacted: v.redacted === true,
|
|
2019
|
-
pageUrl: c.text(v.pageUrl, `${
|
|
2020
|
-
timestamp: v.timestamp == null ? 0 : c.number(v.timestamp, `${
|
|
2019
|
+
pageUrl: c.text(v.pageUrl, `${path19}.pageUrl`, STEPS_LIMITS.valueLength, false) ?? "",
|
|
2020
|
+
timestamp: v.timestamp == null ? 0 : c.number(v.timestamp, `${path19}.timestamp`)
|
|
2021
2021
|
};
|
|
2022
2022
|
if (step.redacted) step.value = null;
|
|
2023
2023
|
if (action === "hover" || action === "dblclick") {
|
|
2024
2024
|
step.value = null;
|
|
2025
|
-
if (!step.target) c.fail(`${
|
|
2025
|
+
if (!step.target) c.fail(`${path19}.target`, `is required on a ${action} step`);
|
|
2026
2026
|
}
|
|
2027
|
-
if (action === "setInputFiles" && !step.target) c.fail(`${
|
|
2027
|
+
if (action === "setInputFiles" && !step.target) c.fail(`${path19}.target`, "is required on a setInputFiles step");
|
|
2028
2028
|
if (action === "dragTo") {
|
|
2029
2029
|
step.value = null;
|
|
2030
|
-
if (!step.target) c.fail(`${
|
|
2031
|
-
step.dropTarget = checkTarget(c, v.dropTarget, `${
|
|
2032
|
-
if (!step.dropTarget) c.fail(`${
|
|
2030
|
+
if (!step.target) c.fail(`${path19}.target`, "is required on a dragTo step");
|
|
2031
|
+
step.dropTarget = checkTarget(c, v.dropTarget, `${path19}.dropTarget`);
|
|
2032
|
+
if (!step.dropTarget) c.fail(`${path19}.dropTarget`, "is required on a dragTo step");
|
|
2033
2033
|
}
|
|
2034
2034
|
if (action === "assertVisible") {
|
|
2035
2035
|
step.assertion = { matcher: "toBeVisible", expected: null, actual: null, negated: false, note: null };
|
|
2036
2036
|
} else if (action === "assert") {
|
|
2037
|
-
step.assertion = checkAssertion(c, v.assertion, `${
|
|
2037
|
+
step.assertion = checkAssertion(c, v.assertion, `${path19}.assertion`);
|
|
2038
2038
|
}
|
|
2039
2039
|
return step;
|
|
2040
2040
|
}
|
|
@@ -2378,8 +2378,1019 @@ ${USAGE3}`);
|
|
|
2378
2378
|
return EXIT_OK3;
|
|
2379
2379
|
}
|
|
2380
2380
|
|
|
2381
|
-
// src/cli/
|
|
2381
|
+
// src/cli/flake.ts
|
|
2382
|
+
var import_node_child_process4 = require("child_process");
|
|
2383
|
+
var fs8 = __toESM(require("fs"));
|
|
2384
|
+
var os2 = __toESM(require("os"));
|
|
2385
|
+
var path11 = __toESM(require("path"));
|
|
2386
|
+
|
|
2387
|
+
// ../core/src/error-parse.ts
|
|
2388
|
+
var ANSI_RE = new RegExp("\\u001B\\[[0-9;]*m", "g");
|
|
2389
|
+
var CHAIN_LINK_METHODS = /* @__PURE__ */ new Set([
|
|
2390
|
+
...LOCATOR_BUILDER_METHODS,
|
|
2391
|
+
"frameLocator",
|
|
2392
|
+
"contentFrame",
|
|
2393
|
+
"filter",
|
|
2394
|
+
"first",
|
|
2395
|
+
"last",
|
|
2396
|
+
"nth",
|
|
2397
|
+
"and",
|
|
2398
|
+
"or",
|
|
2399
|
+
"describe"
|
|
2400
|
+
]);
|
|
2401
|
+
var LEAF_METHODS = new Set(LOCATOR_BUILDER_METHODS);
|
|
2402
|
+
var SUBJECTS = "locator|page|frame|frameLocator|elementHandle|mouse|keyboard|touchscreen|browserContext|browser|apiRequestContext|request|response|route|download|dialog|fileChooser|expect|worker|jsHandle|clock|tracing|video";
|
|
2403
|
+
var CALL_RE = new RegExp(`\\b(${SUBJECTS})\\.(\\w+): `);
|
|
2404
|
+
|
|
2405
|
+
// ../core/src/flake-plan.ts
|
|
2406
|
+
var FLAKE_PLAN_VERSION = 1;
|
|
2407
|
+
var FLAKE_BATCH_RUNS = 5;
|
|
2408
|
+
var FLAKE_SPAWN_OVERHEAD_MS = 3e3;
|
|
2409
|
+
function estimateFlakeSessionMs(arms, medianDurationMs) {
|
|
2410
|
+
if (medianDurationMs == null) return null;
|
|
2411
|
+
let total = 0;
|
|
2412
|
+
for (const arm of arms) {
|
|
2413
|
+
const twoTests = arm.conditions.some((c) => c.kind === "alongside" || c.kind === "after");
|
|
2414
|
+
const batches = arm.stopAt == null ? 1 : Math.ceil(arm.runs / FLAKE_BATCH_RUNS);
|
|
2415
|
+
total += arm.runs * medianDurationMs * (twoTests ? 2 : 1) + batches * FLAKE_SPAWN_OVERHEAD_MS;
|
|
2416
|
+
}
|
|
2417
|
+
return total;
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
// src/internal/config/desktop.ts
|
|
2382
2421
|
var fs5 = __toESM(require("fs"));
|
|
2422
|
+
var os = __toESM(require("os"));
|
|
2423
|
+
var path8 = __toESM(require("path"));
|
|
2424
|
+
function defaultDesktopConfigPath() {
|
|
2425
|
+
return path8.join(os.homedir(), ".piwi", "desktop.json");
|
|
2426
|
+
}
|
|
2427
|
+
function readDesktopConfig(filePath) {
|
|
2428
|
+
try {
|
|
2429
|
+
const parsed = JSON.parse(fs5.readFileSync(filePath, "utf8"));
|
|
2430
|
+
const url = parsed?.url;
|
|
2431
|
+
const token = parsed?.token;
|
|
2432
|
+
if (typeof url !== "string" || !url) return null;
|
|
2433
|
+
if (typeof token !== "string" || !token) return null;
|
|
2434
|
+
return { url, token };
|
|
2435
|
+
} catch {
|
|
2436
|
+
return null;
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// src/internal/config/env.ts
|
|
2441
|
+
var DEFAULTS = {
|
|
2442
|
+
projectName: "default-project",
|
|
2443
|
+
uploadTraces: true,
|
|
2444
|
+
uploadReport: true,
|
|
2445
|
+
liveFileUploads: true,
|
|
2446
|
+
collectScmInfo: true,
|
|
2447
|
+
collectCiInfo: true,
|
|
2448
|
+
collectPerformanceMetrics: true,
|
|
2449
|
+
captureLocators: true,
|
|
2450
|
+
capturePageState: true,
|
|
2451
|
+
// Off by default: the page inventory reads control and link names from every
|
|
2452
|
+
// visited page, so it stays opt-in until a project turns it on.
|
|
2453
|
+
capturePageInventory: false,
|
|
2454
|
+
// Off by default: JavaScript coverage slows the page, so a scheduled job opts in.
|
|
2455
|
+
captureCodeReach: false,
|
|
2456
|
+
captureServerTraces: true,
|
|
2457
|
+
sampleAriaOnPass: true,
|
|
2458
|
+
uploadManifest: true,
|
|
2459
|
+
defaultCapture: true,
|
|
2460
|
+
streaming: true,
|
|
2461
|
+
streamingBatchSize: 5,
|
|
2462
|
+
streamingBatchDelay: 2e3,
|
|
2463
|
+
maxStreamBufferBytes: 100 * 1024 * 1024,
|
|
2464
|
+
submitTimeout: 15 * 60 * 1e3,
|
|
2465
|
+
failOnFlakyTests: false,
|
|
2466
|
+
username: null,
|
|
2467
|
+
password: null,
|
|
2468
|
+
apiKey: null,
|
|
2469
|
+
verbose: false
|
|
2470
|
+
};
|
|
2471
|
+
var PIWI_ENV_KEYS = {
|
|
2472
|
+
serverUrl: "PIWI_DASHBOARD_URL",
|
|
2473
|
+
projectName: "PIWI_PROJECT_NAME",
|
|
2474
|
+
verbose: "PIWI_VERBOSE",
|
|
2475
|
+
apiKey: "PIWI_API_KEY",
|
|
2476
|
+
username: "PIWI_USERNAME",
|
|
2477
|
+
password: "PIWI_PASSWORD",
|
|
2478
|
+
environment: "PIWI_ENVIRONMENT",
|
|
2479
|
+
label: "PIWI_LABEL",
|
|
2480
|
+
runLabel: "PIWI_RUN_LABEL",
|
|
2481
|
+
keep: "PIWI_KEEP",
|
|
2482
|
+
streaming: "PIWI_STREAMING",
|
|
2483
|
+
streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
|
|
2484
|
+
streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
|
|
2485
|
+
maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
|
|
2486
|
+
submitTimeout: "PIWI_SUBMIT_TIMEOUT",
|
|
2487
|
+
liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
|
|
2488
|
+
failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
|
|
2489
|
+
uploadTraces: "PIWI_UPLOAD_TRACES",
|
|
2490
|
+
uploadReport: "PIWI_UPLOAD_REPORT",
|
|
2491
|
+
captureLocators: "PIWI_CAPTURE_LOCATORS",
|
|
2492
|
+
capturePageState: "PIWI_CAPTURE_PAGE_STATE",
|
|
2493
|
+
capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
|
|
2494
|
+
captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
|
|
2495
|
+
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
2496
|
+
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
2497
|
+
sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
|
|
2498
|
+
uploadManifest: "PIWI_UPLOAD_MANIFEST",
|
|
2499
|
+
defaultCapture: "PIWI_DEFAULT_CAPTURE",
|
|
2500
|
+
inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
|
|
2501
|
+
pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
|
|
2502
|
+
outputFile: "PIWI_OUTPUT_FILE",
|
|
2503
|
+
aiMode: "PIWI_AI",
|
|
2504
|
+
aiDir: "PIWI_AI_DIR",
|
|
2505
|
+
aiOnMiss: "PIWI_AI_ON_MISS",
|
|
2506
|
+
aiMaxSteps: "PIWI_AI_MAX_FLOW_STEPS",
|
|
2507
|
+
aiMaxSnapshotChars: "PIWI_AI_MAX_SNAPSHOT_CHARS",
|
|
2508
|
+
aiOptionalProbeTimeout: "PIWI_AI_OPTIONAL_PROBE_TIMEOUT",
|
|
2509
|
+
aiResponseWaitTimeout: "PIWI_AI_RESPONSE_WAIT_TIMEOUT",
|
|
2510
|
+
aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
|
|
2511
|
+
};
|
|
2512
|
+
var PIWI_DESKTOP_CONFIG_ENV = "PIWI_DESKTOP_CONFIG";
|
|
2513
|
+
var PIWI_SHARD_ENV = "PIWI_SHARD";
|
|
2514
|
+
var PIWI_PROBE_ENV = {
|
|
2515
|
+
flag: "PIWI_PROBE",
|
|
2516
|
+
plan: "PIWI_PROBE_PLAN",
|
|
2517
|
+
results: "PIWI_PROBE_RESULTS",
|
|
2518
|
+
secret: "PIWI_PROBE_SECRET"
|
|
2519
|
+
};
|
|
2520
|
+
var PIWI_FLAKE_ENV = {
|
|
2521
|
+
plan: "PIWI_FLAKE_PLAN",
|
|
2522
|
+
results: "PIWI_FLAKE_RESULTS"
|
|
2523
|
+
};
|
|
2524
|
+
var ENV_FALLBACK_SPECS = [
|
|
2525
|
+
{ option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
|
|
2526
|
+
{ option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
|
|
2527
|
+
{ option: "apiKey", env: PIWI_ENV_KEYS.apiKey, kind: "string" },
|
|
2528
|
+
{ option: "username", env: PIWI_ENV_KEYS.username, kind: "string" },
|
|
2529
|
+
{ option: "password", env: PIWI_ENV_KEYS.password, kind: "string" },
|
|
2530
|
+
{ option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
|
|
2531
|
+
{ option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
|
|
2532
|
+
{ option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
|
|
2533
|
+
{ option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
|
|
2534
|
+
{ option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
|
|
2535
|
+
{ option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
|
|
2536
|
+
{ option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
|
|
2537
|
+
{ option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
|
|
2538
|
+
{ option: "submitTimeout", env: PIWI_ENV_KEYS.submitTimeout, kind: "number" },
|
|
2539
|
+
{ option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
|
|
2540
|
+
{ option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
|
|
2541
|
+
{ option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
|
|
2542
|
+
{ option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
|
|
2543
|
+
{ option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
|
|
2544
|
+
{ option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
|
|
2545
|
+
{ option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
|
|
2546
|
+
{ option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
|
|
2547
|
+
{ option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
|
|
2548
|
+
{ option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
|
|
2549
|
+
{ option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
|
|
2550
|
+
{ option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
|
|
2551
|
+
{ option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
|
|
2552
|
+
{ option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
|
|
2553
|
+
{ option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
|
|
2554
|
+
];
|
|
2555
|
+
|
|
2556
|
+
// ../core/src/flake-verdict.ts
|
|
2557
|
+
var FLAKE_SIGNIFICANCE = 0.05;
|
|
2558
|
+
var FLAKE_REPRODUCED_RATE = 0.5;
|
|
2559
|
+
var FLAKE_VERIFY_MIN_RUNS = 5;
|
|
2560
|
+
function logFactorial(n) {
|
|
2561
|
+
let sum = 0;
|
|
2562
|
+
for (let i = 2; i <= n; i++) sum += Math.log(i);
|
|
2563
|
+
return sum;
|
|
2564
|
+
}
|
|
2565
|
+
function logChoose(n, k) {
|
|
2566
|
+
return logFactorial(n) - logFactorial(k) - logFactorial(n - k);
|
|
2567
|
+
}
|
|
2568
|
+
function assertCount(value, name) {
|
|
2569
|
+
if (!Number.isInteger(value) || value < 0) throw new RangeError(`${name} must be a whole number from 0`);
|
|
2570
|
+
}
|
|
2571
|
+
function fisherExactGreater(armFailures, armRuns, controlFailures, controlRuns) {
|
|
2572
|
+
assertCount(armFailures, "armFailures");
|
|
2573
|
+
assertCount(armRuns, "armRuns");
|
|
2574
|
+
assertCount(controlFailures, "controlFailures");
|
|
2575
|
+
assertCount(controlRuns, "controlRuns");
|
|
2576
|
+
if (armFailures > armRuns || controlFailures > controlRuns) throw new RangeError("failures cannot exceed runs");
|
|
2577
|
+
const failures = armFailures + controlFailures;
|
|
2578
|
+
const total = armRuns + controlRuns;
|
|
2579
|
+
const denominator = logChoose(total, failures);
|
|
2580
|
+
const highest = Math.min(armRuns, failures);
|
|
2581
|
+
let p = 0;
|
|
2582
|
+
for (let k = armFailures; k <= highest; k++) {
|
|
2583
|
+
if (failures - k > controlRuns) continue;
|
|
2584
|
+
p += Math.exp(logChoose(armRuns, k) + logChoose(controlRuns, failures - k) - denominator);
|
|
2585
|
+
}
|
|
2586
|
+
return Math.min(1, p);
|
|
2587
|
+
}
|
|
2588
|
+
function flakeArmVerdict(arm, control) {
|
|
2589
|
+
const pValue = fisherExactGreater(arm.matchingFailures, arm.runs, control.matchingFailures, control.runs);
|
|
2590
|
+
const rate = arm.runs > 0 ? arm.matchingFailures / arm.runs : 0;
|
|
2591
|
+
const significant = pValue < FLAKE_SIGNIFICANCE;
|
|
2592
|
+
const verdict = significant && rate >= FLAKE_REPRODUCED_RATE ? "reproduced" : significant ? "amplified" : "not-reproduced";
|
|
2593
|
+
return { verdict, rate, pValue };
|
|
2594
|
+
}
|
|
2595
|
+
function flakeVerifyRuns(rate) {
|
|
2596
|
+
if (!Number.isFinite(rate) || rate <= 0 || rate > 1) throw new RangeError("rate must be above 0 and at most 1");
|
|
2597
|
+
if (rate === 1) return FLAKE_VERIFY_MIN_RUNS;
|
|
2598
|
+
const exact = Math.log(FLAKE_SIGNIFICANCE) / Math.log(1 - rate);
|
|
2599
|
+
return Math.max(FLAKE_VERIFY_MIN_RUNS, Math.ceil(Number(exact.toFixed(9))));
|
|
2600
|
+
}
|
|
2601
|
+
function flakeFixVerified(verify, reproducedRate) {
|
|
2602
|
+
return verify.matchingFailures === 0 && verify.runs >= flakeVerifyRuns(reproducedRate);
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
// src/internal/flake/lab.ts
|
|
2606
|
+
var FAILED = /* @__PURE__ */ new Set(["failed", "timedOut"]);
|
|
2607
|
+
function overlaps(a, b) {
|
|
2608
|
+
return a.startedAt < b.startedAt + b.duration && b.startedAt < a.startedAt + a.duration;
|
|
2609
|
+
}
|
|
2610
|
+
function workerCondition(conditions) {
|
|
2611
|
+
for (const c of conditions) if (c.kind === "alongside" || c.kind === "after") return c;
|
|
2612
|
+
return null;
|
|
2613
|
+
}
|
|
2614
|
+
function roundKept(line, all, conditions) {
|
|
2615
|
+
const condition = workerCondition(conditions);
|
|
2616
|
+
if (!condition) return true;
|
|
2617
|
+
if (condition.kind === "alongside") return all.some((l) => l.role === "companion" && overlaps(l, line));
|
|
2618
|
+
const before = all.filter((l) => l !== line && l.startedAt < line.startedAt).sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
2619
|
+
return before?.role === "companion";
|
|
2620
|
+
}
|
|
2621
|
+
function countArm(batches, arm) {
|
|
2622
|
+
let seen = 0;
|
|
2623
|
+
const kept = [];
|
|
2624
|
+
for (const lines of batches) {
|
|
2625
|
+
const targets = lines.filter((l) => l.role === "target" && l.status !== "skipped" && l.status !== "interrupted").sort((a, b) => a.startedAt - b.startedAt || a.repeatEachIndex - b.repeatEachIndex);
|
|
2626
|
+
seen += targets.length;
|
|
2627
|
+
kept.push(...targets.filter((l) => roundKept(l, lines, arm.conditions)));
|
|
2628
|
+
}
|
|
2629
|
+
const count = {
|
|
2630
|
+
runs: 0,
|
|
2631
|
+
matchingFailures: 0,
|
|
2632
|
+
otherFailures: 0,
|
|
2633
|
+
discardedRounds: seen - kept.length,
|
|
2634
|
+
stoppedEarly: false,
|
|
2635
|
+
otherSignatures: []
|
|
2636
|
+
};
|
|
2637
|
+
const others = /* @__PURE__ */ new Map();
|
|
2638
|
+
for (const [i, line] of kept.entries()) {
|
|
2639
|
+
count.runs++;
|
|
2640
|
+
if (line.matchesHistory) count.matchingFailures++;
|
|
2641
|
+
else if (FAILED.has(line.status)) {
|
|
2642
|
+
count.otherFailures++;
|
|
2643
|
+
const signature = line.errorSignature ?? line.status;
|
|
2644
|
+
others.set(signature, (others.get(signature) ?? 0) + 1);
|
|
2645
|
+
}
|
|
2646
|
+
if (arm.stopAt != null && count.matchingFailures >= arm.stopAt) {
|
|
2647
|
+
count.stoppedEarly = i < kept.length - 1;
|
|
2648
|
+
break;
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
count.otherSignatures = [...others.entries()].sort((a, b) => b[1] - a[1]).map(([s]) => s);
|
|
2652
|
+
return count;
|
|
2653
|
+
}
|
|
2654
|
+
function armDone(count, arm) {
|
|
2655
|
+
return arm.stopAt != null && count.matchingFailures >= arm.stopAt || count.runs >= arm.runs;
|
|
2656
|
+
}
|
|
2657
|
+
function nextBatch(count, arm) {
|
|
2658
|
+
const left = Math.max(0, arm.runs - count.runs);
|
|
2659
|
+
return arm.stopAt == null ? left : Math.min(left, FLAKE_BATCH_RUNS);
|
|
2660
|
+
}
|
|
2661
|
+
function escapeRegex(s) {
|
|
2662
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2663
|
+
}
|
|
2664
|
+
function grepFor(tests) {
|
|
2665
|
+
return tests.map((t) => `(^| )${escapeRegex([...t.suite, t.title].join(" "))}( @\\S+)*$`).join("|");
|
|
2666
|
+
}
|
|
2667
|
+
function playwrightArgs(test, arm, repeat) {
|
|
2668
|
+
const companion = workerCondition(arm.conditions);
|
|
2669
|
+
const tests = companion ? [test, companion.test] : [test];
|
|
2670
|
+
const files = [...new Set(tests.map((t) => t.file))];
|
|
2671
|
+
const args = [...files, "--grep", grepFor(tests), `--repeat-each=${repeat}`, "--retries=0"];
|
|
2672
|
+
const alongside = companion?.kind === "alongside";
|
|
2673
|
+
args.push(`--workers=${alongside ? 2 : 1}`);
|
|
2674
|
+
if (alongside && files.length === 1) args.push("--fully-parallel");
|
|
2675
|
+
const project = arm.conditions.find((c) => c.kind === "project");
|
|
2676
|
+
const name = project?.kind === "project" ? project.name : test.project;
|
|
2677
|
+
if (name) args.push(`--project=${name}`);
|
|
2678
|
+
return args;
|
|
2679
|
+
}
|
|
2680
|
+
function armVerdict(arm, control) {
|
|
2681
|
+
return flakeArmVerdict(arm, control);
|
|
2682
|
+
}
|
|
2683
|
+
function verifyVerdict(arm, reproducedRate) {
|
|
2684
|
+
if (arm.matchingFailures > 0) return "still-fails";
|
|
2685
|
+
return flakeFixVerified(arm, reproducedRate) ? "verified" : "inconclusive";
|
|
2686
|
+
}
|
|
2687
|
+
var EXIT_REPRODUCED = 0;
|
|
2688
|
+
var EXIT_NOT_REPRODUCED = 1;
|
|
2689
|
+
var EXIT_ERROR4 = 2;
|
|
2690
|
+
var EXIT_BISECT_SKIP = 125;
|
|
2691
|
+
function exitCodeFor(verdict) {
|
|
2692
|
+
return verdict === "reproduced" || verdict === "verified" ? EXIT_REPRODUCED : EXIT_NOT_REPRODUCED;
|
|
2693
|
+
}
|
|
2694
|
+
function bisectExitCode(verdict) {
|
|
2695
|
+
if (verdict === "verified") return EXIT_REPRODUCED;
|
|
2696
|
+
if (verdict === "still-fails") return EXIT_NOT_REPRODUCED;
|
|
2697
|
+
return EXIT_BISECT_SKIP;
|
|
2698
|
+
}
|
|
2699
|
+
function parseDuration(text) {
|
|
2700
|
+
const trimmed = text.trim().toLowerCase();
|
|
2701
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) return Math.round(Number(trimmed) * 6e4);
|
|
2702
|
+
const re = /(\d+(?:\.\d+)?)\s*(h|m|s)/g;
|
|
2703
|
+
let total = 0;
|
|
2704
|
+
let consumed = "";
|
|
2705
|
+
for (const m of trimmed.matchAll(re)) {
|
|
2706
|
+
total += Number(m[1]) * (m[2] === "h" ? 36e5 : m[2] === "m" ? 6e4 : 1e3);
|
|
2707
|
+
consumed += m[0];
|
|
2708
|
+
}
|
|
2709
|
+
return consumed.replace(/\s/g, "") === trimmed.replace(/\s/g, "") && total > 0 ? Math.round(total) : null;
|
|
2710
|
+
}
|
|
2711
|
+
function formatDuration(ms) {
|
|
2712
|
+
if (ms < 6e4) return `${Math.max(1, Math.round(ms / 1e3))} s`;
|
|
2713
|
+
const minutes = Math.round(ms / 6e4);
|
|
2714
|
+
if (minutes < 60) return `${minutes} min`;
|
|
2715
|
+
return `${Math.floor(minutes / 60)} h ${minutes % 60} min`;
|
|
2716
|
+
}
|
|
2717
|
+
function withinBudget(startedAt, now, budgetMs) {
|
|
2718
|
+
return now - startedAt < budgetMs;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
// src/internal/support/connection.ts
|
|
2722
|
+
var fs6 = __toESM(require("fs"));
|
|
2723
|
+
var path9 = __toESM(require("path"));
|
|
2724
|
+
|
|
2725
|
+
// ../core/src/dotenv.ts
|
|
2726
|
+
function parseDotEnv(text) {
|
|
2727
|
+
const out = {};
|
|
2728
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
2729
|
+
const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(raw);
|
|
2730
|
+
if (!m) continue;
|
|
2731
|
+
let value = m[2];
|
|
2732
|
+
const quote4 = value[0];
|
|
2733
|
+
if ((quote4 === '"' || quote4 === "'") && value.length > 1 && value.endsWith(quote4)) {
|
|
2734
|
+
value = value.slice(1, -1);
|
|
2735
|
+
if (quote4 === '"') value = value.replace(/\\n/g, "\n").replace(/\\"/g, '"');
|
|
2736
|
+
} else {
|
|
2737
|
+
value = value.replace(/\s+#.*$/, "");
|
|
2738
|
+
}
|
|
2739
|
+
out[m[1]] = value;
|
|
2740
|
+
}
|
|
2741
|
+
return out;
|
|
2742
|
+
}
|
|
2743
|
+
function resolvePiwiConnection(sources) {
|
|
2744
|
+
const own = {
|
|
2745
|
+
serverUrl: sources.flags?.serverUrl || sources.env?.PIWI_DASHBOARD_URL,
|
|
2746
|
+
apiKey: sources.flags?.apiKey || sources.env?.PIWI_API_KEY
|
|
2747
|
+
};
|
|
2748
|
+
const workspace = { serverUrl: sources.dotEnv?.PIWI_DASHBOARD_URL, apiKey: sources.dotEnv?.PIWI_API_KEY };
|
|
2749
|
+
const project = sources.flags?.project || sources.env?.PIWI_PROJECT_NAME || sources.dotEnv?.PIWI_PROJECT_NAME || "";
|
|
2750
|
+
const desktop = sources.desktop ?? null;
|
|
2751
|
+
const trim = (url) => url.replace(/\/+$/, "");
|
|
2752
|
+
const layer = own.serverUrl ? own : workspace.serverUrl ? workspace : null;
|
|
2753
|
+
const serverUrl = layer ? trim(layer.serverUrl) : desktop ? trim(desktop.url) : "";
|
|
2754
|
+
if (!serverUrl) return null;
|
|
2755
|
+
const desktopKey = desktop && trim(desktop.url) === serverUrl ? desktop.token : null;
|
|
2756
|
+
return { serverUrl, apiKey: layer?.apiKey || desktopKey || null, project };
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
// src/internal/support/connection.ts
|
|
2760
|
+
function resolveCliConnection(flags, env, dirs) {
|
|
2761
|
+
let dotEnv = {};
|
|
2762
|
+
for (const dir of dirs) {
|
|
2763
|
+
try {
|
|
2764
|
+
dotEnv = { ...parseDotEnv(fs6.readFileSync(path9.join(dir, ".env"), "utf-8")), ...dotEnv };
|
|
2765
|
+
} catch {
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
return resolvePiwiConnection({
|
|
2769
|
+
flags,
|
|
2770
|
+
env,
|
|
2771
|
+
dotEnv,
|
|
2772
|
+
desktop: readDesktopConfig(env[PIWI_DESKTOP_CONFIG_ENV] || defaultDesktopConfigPath())
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
// src/internal/support/playwright-spawn.ts
|
|
2777
|
+
var import_node_child_process3 = require("child_process");
|
|
2778
|
+
var fs7 = __toESM(require("fs"));
|
|
2779
|
+
var import_node_module2 = require("module");
|
|
2780
|
+
var path10 = __toESM(require("path"));
|
|
2781
|
+
function resolvePlaywrightCli2(cwd = process.cwd()) {
|
|
2782
|
+
const require2 = (0, import_node_module2.createRequire)(path10.join(cwd, "noop.js"));
|
|
2783
|
+
for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
|
|
2784
|
+
try {
|
|
2785
|
+
return require2.resolve(id);
|
|
2786
|
+
} catch {
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
return null;
|
|
2790
|
+
}
|
|
2791
|
+
function spawnPlaywright(playwrightArgs2, env, options = {}) {
|
|
2792
|
+
const output = options.output ?? "inherit";
|
|
2793
|
+
let fd = output === "inherit" ? null : fs7.openSync(output.file, "a");
|
|
2794
|
+
const stdio = fd === null ? "inherit" : ["ignore", fd, fd];
|
|
2795
|
+
const cli = resolvePlaywrightCli2();
|
|
2796
|
+
const child = cli ? (0, import_node_child_process3.spawn)(process.execPath, [cli, "test", ...playwrightArgs2], { stdio, env }) : (0, import_node_child_process3.spawn)(process.platform === "win32" ? "npx.cmd" : "npx", ["playwright", "test", ...playwrightArgs2], {
|
|
2797
|
+
stdio,
|
|
2798
|
+
env
|
|
2799
|
+
});
|
|
2800
|
+
return new Promise((resolve8) => {
|
|
2801
|
+
const done = (code) => {
|
|
2802
|
+
if (fd !== null) fs7.closeSync(fd);
|
|
2803
|
+
fd = null;
|
|
2804
|
+
resolve8(code);
|
|
2805
|
+
};
|
|
2806
|
+
child.on("error", (err) => {
|
|
2807
|
+
console.error(`${options.command ?? "piwi"}: could not start Playwright \u2014 ${err.message}`);
|
|
2808
|
+
done(2);
|
|
2809
|
+
});
|
|
2810
|
+
child.on("exit", (code) => done(code ?? 2));
|
|
2811
|
+
});
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
// src/cli/flake.ts
|
|
2815
|
+
var USAGE4 = `
|
|
2816
|
+
piwi flake \u2014 make a flaky test fail on demand, then prove the fix
|
|
2817
|
+
|
|
2818
|
+
Usage:
|
|
2819
|
+
npx @piwitests/reporter flake <test> [options]
|
|
2820
|
+
npx @piwitests/reporter flake verify <test> [--runs <n>] [--bisect] [--json]
|
|
2821
|
+
|
|
2822
|
+
<test> is a test case id (the number in its dashboard URL) or a spec file and
|
|
2823
|
+
line, tests/checkout.spec.ts:42, looked up in the project on the dashboard.
|
|
2824
|
+
|
|
2825
|
+
It fetches the test's plan from the dashboard: a control arm and one arm per
|
|
2826
|
+
suspect of its flake profile, most likely first. It runs the control (10 runs),
|
|
2827
|
+
then each arm (up to 10 runs, stopping at 3 failures with the same error as in
|
|
2828
|
+
CI), one test at a time with retries off, in the checkout it runs in. It prints
|
|
2829
|
+
each arm against the control with its verdict and saves the experiment on the
|
|
2830
|
+
test's Flakiness tab. The capture fixtures must be in use (piwiFixtures).
|
|
2831
|
+
|
|
2832
|
+
verify reruns the arm that last reproduced the test, and its control, until a
|
|
2833
|
+
matching failure appears or enough runs pass to say the fix holds.
|
|
2834
|
+
|
|
2835
|
+
verify --bisect runs that arm alone at this checkout and saves nothing, for
|
|
2836
|
+
git bisect run: exit 0 (good) when enough runs pass, 1 (bad) on a failure with
|
|
2837
|
+
the same error as in CI, 125 (skip) when this commit cannot tell.
|
|
2838
|
+
|
|
2839
|
+
Options:
|
|
2840
|
+
--suspect <n> Run only the arm of suspect n (its rank on the Flakiness tab)
|
|
2841
|
+
--all Also run every condition at once when none reproduces alone
|
|
2842
|
+
--runs <n> Runs of the control and of each arm (default 10; verify:
|
|
2843
|
+
the number that proves the fix at the rate it reproduced)
|
|
2844
|
+
--budget <duration> Start no new arm after this long: 15m (default), 90s, 1h
|
|
2845
|
+
--no-upload Keep the results off the dashboard; it still needs the plan
|
|
2846
|
+
from the dashboard, or from --plan when it cannot be reached
|
|
2847
|
+
--plan <file> Read the plan from a file (a saved flake-plan response, or
|
|
2848
|
+
the plan the plan_flake_experiment MCP tool returns) and run
|
|
2849
|
+
it without the dashboard; implies --no-upload
|
|
2850
|
+
--bisect verify only: one bisect step, as above; implies --no-upload
|
|
2851
|
+
--source <where> Where the lab runs, recorded on the experiment: cli, ci or
|
|
2852
|
+
desktop (default: ci when CI is set, else cli)
|
|
2853
|
+
--json Print the results as JSON instead of text
|
|
2854
|
+
--server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
|
|
2855
|
+
--api-key <key> Reporter API key (env PIWI_API_KEY, .env)
|
|
2856
|
+
--project <name|id> The Piwi project, for a file:line test (env PIWI_PROJECT_NAME)
|
|
2857
|
+
|
|
2858
|
+
Exit codes: 0 an arm reproduced the failure (verify: the fix held),
|
|
2859
|
+
1 nothing reproduced (verify: it still fails, or too few runs),
|
|
2860
|
+
2 error; with --bisect, 0 good, 1 bad, 125 skip, 2 error.
|
|
2861
|
+
`.trim();
|
|
2862
|
+
var DEFAULT_BUDGET_MS = 15 * 6e4;
|
|
2863
|
+
var SOURCES = ["cli", "ci", "desktop"];
|
|
2864
|
+
var MAX_BATCHES = 12;
|
|
2865
|
+
var LOG_TAIL_LINES = 40;
|
|
2866
|
+
function parseFlakeArgs(argv) {
|
|
2867
|
+
const args = {
|
|
2868
|
+
verify: argv[0] === "verify",
|
|
2869
|
+
test: null,
|
|
2870
|
+
suspect: null,
|
|
2871
|
+
all: false,
|
|
2872
|
+
runs: null,
|
|
2873
|
+
budgetMs: DEFAULT_BUDGET_MS,
|
|
2874
|
+
upload: true,
|
|
2875
|
+
planFile: null,
|
|
2876
|
+
bisect: false,
|
|
2877
|
+
source: null,
|
|
2878
|
+
json: false,
|
|
2879
|
+
help: false
|
|
2880
|
+
};
|
|
2881
|
+
const rest = args.verify ? argv.slice(1) : argv;
|
|
2882
|
+
const value = (i, name) => {
|
|
2883
|
+
const v = rest[i + 1];
|
|
2884
|
+
if (v === void 0 || v.startsWith("--")) throw new Error(`${name} needs a value`);
|
|
2885
|
+
return v;
|
|
2886
|
+
};
|
|
2887
|
+
const whole = (text, name, min, max) => {
|
|
2888
|
+
const n = Number(text);
|
|
2889
|
+
if (!Number.isInteger(n) || n < min || n > max)
|
|
2890
|
+
throw new Error(`${name} must be a whole number from ${min} to ${max}`);
|
|
2891
|
+
return n;
|
|
2892
|
+
};
|
|
2893
|
+
for (let i = 0; i < rest.length; i++) {
|
|
2894
|
+
const arg = rest[i];
|
|
2895
|
+
switch (arg) {
|
|
2896
|
+
case "-h":
|
|
2897
|
+
case "--help":
|
|
2898
|
+
args.help = true;
|
|
2899
|
+
break;
|
|
2900
|
+
case "--suspect":
|
|
2901
|
+
args.suspect = whole(value(i++, arg), arg, 1, 50);
|
|
2902
|
+
break;
|
|
2903
|
+
case "--all":
|
|
2904
|
+
args.all = true;
|
|
2905
|
+
break;
|
|
2906
|
+
case "--runs":
|
|
2907
|
+
args.runs = whole(value(i++, arg), arg, 1, 100);
|
|
2908
|
+
break;
|
|
2909
|
+
case "--budget": {
|
|
2910
|
+
const ms = parseDuration(value(i++, arg));
|
|
2911
|
+
if (ms == null) throw new Error("--budget must be a duration like 15m, 90s or 1h");
|
|
2912
|
+
args.budgetMs = ms;
|
|
2913
|
+
break;
|
|
2914
|
+
}
|
|
2915
|
+
case "--no-upload":
|
|
2916
|
+
args.upload = false;
|
|
2917
|
+
break;
|
|
2918
|
+
case "--plan":
|
|
2919
|
+
args.planFile = value(i++, arg);
|
|
2920
|
+
args.upload = false;
|
|
2921
|
+
break;
|
|
2922
|
+
case "--bisect":
|
|
2923
|
+
args.bisect = true;
|
|
2924
|
+
args.upload = false;
|
|
2925
|
+
break;
|
|
2926
|
+
case "--source": {
|
|
2927
|
+
const where = value(i++, arg);
|
|
2928
|
+
if (!SOURCES.includes(where))
|
|
2929
|
+
throw new Error(`--source must be one of ${SOURCES.join(", ")}`);
|
|
2930
|
+
args.source = where;
|
|
2931
|
+
break;
|
|
2932
|
+
}
|
|
2933
|
+
case "--json":
|
|
2934
|
+
args.json = true;
|
|
2935
|
+
break;
|
|
2936
|
+
case "--server-url":
|
|
2937
|
+
args.serverUrl = value(i++, arg);
|
|
2938
|
+
break;
|
|
2939
|
+
case "--api-key":
|
|
2940
|
+
args.apiKey = value(i++, arg);
|
|
2941
|
+
break;
|
|
2942
|
+
case "--project":
|
|
2943
|
+
args.project = value(i++, arg);
|
|
2944
|
+
break;
|
|
2945
|
+
default:
|
|
2946
|
+
if (arg.startsWith("-")) throw new Error(`unknown option ${arg}`);
|
|
2947
|
+
if (args.test !== null) throw new Error(`one test at a time (got "${args.test}" and "${arg}")`);
|
|
2948
|
+
args.test = arg;
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
if (args.suspect !== null && args.all) throw new Error("--suspect and --all cannot be used together");
|
|
2952
|
+
if (args.verify && (args.suspect !== null || args.all))
|
|
2953
|
+
throw new Error("verify reruns one arm: drop --suspect and --all");
|
|
2954
|
+
if (args.bisect && !args.verify) throw new Error("--bisect is a verify option: flake verify <test> --bisect");
|
|
2955
|
+
return args;
|
|
2956
|
+
}
|
|
2957
|
+
function gitHead() {
|
|
2958
|
+
try {
|
|
2959
|
+
return (0, import_node_child_process4.execFileSync)("git", ["rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
2960
|
+
} catch {
|
|
2961
|
+
return null;
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
function commitsDiffer(a, b) {
|
|
2965
|
+
if (!a || !b) return false;
|
|
2966
|
+
const [x, y] = [a.toLowerCase(), b.toLowerCase()];
|
|
2967
|
+
return !x.startsWith(y) && !y.startsWith(x);
|
|
2968
|
+
}
|
|
2969
|
+
function short(sha) {
|
|
2970
|
+
return sha ? sha.slice(0, 7) : "unknown";
|
|
2971
|
+
}
|
|
2972
|
+
function fraction(count) {
|
|
2973
|
+
return `${count.matchingFailures}/${count.runs}`;
|
|
2974
|
+
}
|
|
2975
|
+
function formatP(p) {
|
|
2976
|
+
return p < 1e-3 ? "< 0.001" : `= ${p.toFixed(3)}`;
|
|
2977
|
+
}
|
|
2978
|
+
var FlakeError = class extends Error {
|
|
2979
|
+
};
|
|
2980
|
+
async function getJson(http, url) {
|
|
2981
|
+
const res = await fetch(`${http.serverUrl}${url}`, { headers: http.headers });
|
|
2982
|
+
if (!res.ok) {
|
|
2983
|
+
const body = await res.json().catch(() => ({}));
|
|
2984
|
+
throw new FlakeError(body.message || `the dashboard returned ${res.status} for ${url.split("?")[0]}`);
|
|
2985
|
+
}
|
|
2986
|
+
return await res.json();
|
|
2987
|
+
}
|
|
2988
|
+
async function resolveTestCase(http, test, project, apiKey) {
|
|
2989
|
+
if (/^\d+$/.test(test)) return Number(test);
|
|
2990
|
+
const m = /^(.+):(\d+)$/.exec(test);
|
|
2991
|
+
if (!m) throw new FlakeError(`"${test}" is neither a test case id nor file:line`);
|
|
2992
|
+
const projectId = await resolveProjectId({ serverUrl: http.serverUrl, apiKey, project, key: "" });
|
|
2993
|
+
const found = await getJson(
|
|
2994
|
+
http,
|
|
2995
|
+
`/api/projects/${projectId}/flake-lab/test?location=${encodeURIComponent(`${m[1].replace(/\\/g, "/")}:${m[2]}`)}`
|
|
2996
|
+
);
|
|
2997
|
+
return found.testCaseId;
|
|
2998
|
+
}
|
|
2999
|
+
function playwrightRunner(dir, log) {
|
|
3000
|
+
return {
|
|
3001
|
+
now: () => Date.now(),
|
|
3002
|
+
async runBatch(plan, arm, repeat, batch) {
|
|
3003
|
+
const planFile = path11.join(dir, `${arm.id}.plan.json`);
|
|
3004
|
+
if (!fs8.existsSync(planFile)) {
|
|
3005
|
+
fs8.writeFileSync(
|
|
3006
|
+
planFile,
|
|
3007
|
+
JSON.stringify({
|
|
3008
|
+
version: FLAKE_PLAN_VERSION,
|
|
3009
|
+
experimentId: plan.experimentId ?? `local-${process.pid}`,
|
|
3010
|
+
test: plan.test,
|
|
3011
|
+
arm: { id: arm.id, conditions: arm.conditions },
|
|
3012
|
+
errorSignatures: plan.errorSignatures
|
|
3013
|
+
})
|
|
3014
|
+
);
|
|
3015
|
+
}
|
|
3016
|
+
const resultsFile = path11.join(dir, `${arm.id}.${batch}.jsonl`);
|
|
3017
|
+
fs8.writeFileSync(resultsFile, "");
|
|
3018
|
+
const env = {
|
|
3019
|
+
...process.env,
|
|
3020
|
+
[PIWI_FLAKE_ENV.plan]: planFile,
|
|
3021
|
+
[PIWI_FLAKE_ENV.results]: resultsFile
|
|
3022
|
+
};
|
|
3023
|
+
delete env[PIWI_PROBE_ENV.flag];
|
|
3024
|
+
fs8.appendFileSync(log, `
|
|
3025
|
+
\u2500\u2500 ${arm.id} batch ${batch} \u2500\u2500
|
|
3026
|
+
`);
|
|
3027
|
+
await spawnPlaywright(playwrightArgs(plan.test, arm, repeat), env, {
|
|
3028
|
+
output: { file: log },
|
|
3029
|
+
command: "piwi flake"
|
|
3030
|
+
});
|
|
3031
|
+
return readResults(resultsFile);
|
|
3032
|
+
}
|
|
3033
|
+
};
|
|
3034
|
+
}
|
|
3035
|
+
function readResults(file) {
|
|
3036
|
+
const lines = [];
|
|
3037
|
+
for (const line of fs8.readFileSync(file, "utf8").split("\n")) {
|
|
3038
|
+
if (!line.trim()) continue;
|
|
3039
|
+
try {
|
|
3040
|
+
lines.push(JSON.parse(line));
|
|
3041
|
+
} catch {
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
return lines;
|
|
3045
|
+
}
|
|
3046
|
+
async function runArm(runner, plan, arm) {
|
|
3047
|
+
const batches = [];
|
|
3048
|
+
let count = countArm(batches, arm);
|
|
3049
|
+
for (let batch = 1; batch <= MAX_BATCHES && !armDone(count, arm); batch++) {
|
|
3050
|
+
const repeat = nextBatch(count, arm);
|
|
3051
|
+
if (repeat <= 0) break;
|
|
3052
|
+
const lines = await runner.runBatch(plan, arm, repeat, batch);
|
|
3053
|
+
if (!lines.some((l) => l.role === "target")) {
|
|
3054
|
+
throw new FlakeError(
|
|
3055
|
+
`the ${arm.id} arm recorded no attempt of "${plan.displayTitle}" in ${plan.test.file}. Is the test in this checkout, and does its spec use the Piwi capture fixtures?`
|
|
3056
|
+
);
|
|
3057
|
+
}
|
|
3058
|
+
batches.push(lines);
|
|
3059
|
+
count = countArm(batches, arm);
|
|
3060
|
+
}
|
|
3061
|
+
return count;
|
|
3062
|
+
}
|
|
3063
|
+
async function runSession(runner, plan, args, onArm = () => {
|
|
3064
|
+
}) {
|
|
3065
|
+
const started = runner.now();
|
|
3066
|
+
const control = args.bisect ? { arm: plan.control, count: countArm([], plan.control), verdict: null, pValue: null, skipped: "bisect step" } : {
|
|
3067
|
+
arm: plan.control,
|
|
3068
|
+
count: await runArm(runner, plan, plan.control),
|
|
3069
|
+
verdict: null,
|
|
3070
|
+
pValue: null,
|
|
3071
|
+
skipped: null
|
|
3072
|
+
};
|
|
3073
|
+
onArm(control, null);
|
|
3074
|
+
const arms = args.suspect != null ? plan.arms.filter((a) => a.rank === args.suspect) : plan.arms;
|
|
3075
|
+
const results = [];
|
|
3076
|
+
const run = async (arm) => {
|
|
3077
|
+
if (!withinBudget(started, runner.now(), args.budgetMs)) {
|
|
3078
|
+
const result2 = { arm, count: countArm([], arm), verdict: null, pValue: null, skipped: "budget spent" };
|
|
3079
|
+
results.push(result2);
|
|
3080
|
+
onArm(result2, control.count);
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
3083
|
+
const count = await runArm(runner, plan, arm);
|
|
3084
|
+
let verdict;
|
|
3085
|
+
let pValue = null;
|
|
3086
|
+
if (plan.kind === "verify") {
|
|
3087
|
+
verdict = verifyVerdict(count, plan.verifies?.rate ?? 1);
|
|
3088
|
+
} else {
|
|
3089
|
+
const v = armVerdict(count, control.count);
|
|
3090
|
+
verdict = v.verdict;
|
|
3091
|
+
pValue = v.pValue;
|
|
3092
|
+
}
|
|
3093
|
+
const result = { arm, count, verdict, pValue, skipped: null };
|
|
3094
|
+
results.push(result);
|
|
3095
|
+
onArm(result, control.count);
|
|
3096
|
+
};
|
|
3097
|
+
for (const arm of arms) await run(arm);
|
|
3098
|
+
if (args.all && plan.combined && !results.some((r) => r.verdict === "reproduced")) await run(plan.combined);
|
|
3099
|
+
return { control, arms: results };
|
|
3100
|
+
}
|
|
3101
|
+
function sessionVerdict(kind, arms) {
|
|
3102
|
+
if (kind === "verify") {
|
|
3103
|
+
const v = arms.find((a) => a.arm.id === "verify");
|
|
3104
|
+
return {
|
|
3105
|
+
verdict: v?.verdict ?? "inconclusive",
|
|
3106
|
+
reproducingArm: v ? "verify" : null
|
|
3107
|
+
};
|
|
3108
|
+
}
|
|
3109
|
+
const reproduced = arms.find((a) => a.verdict === "reproduced");
|
|
3110
|
+
if (reproduced) return { verdict: "reproduced", reproducingArm: reproduced.arm.id };
|
|
3111
|
+
return {
|
|
3112
|
+
verdict: arms.some((a) => a.verdict === "amplified") ? "amplified" : "not-reproduced",
|
|
3113
|
+
reproducingArm: null
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
function pad(text, width) {
|
|
3117
|
+
return text.length >= width ? `${text} ` : text + " ".repeat(width - text.length);
|
|
3118
|
+
}
|
|
3119
|
+
function clip(text, width) {
|
|
3120
|
+
return text.length > width ? `${text.slice(0, width - 1)}\u2026` : text;
|
|
3121
|
+
}
|
|
3122
|
+
function armLine(result, stopAt) {
|
|
3123
|
+
const { arm, count } = result;
|
|
3124
|
+
const name = arm.id === "control" ? "control" : `${arm.rank != null ? `${arm.rank} ` : " "}${arm.label}`;
|
|
3125
|
+
if (result.skipped) return ` ${pad(clip(name, 34), 35)}${result.skipped}`;
|
|
3126
|
+
const notes = [];
|
|
3127
|
+
if (count.stoppedEarly && stopAt != null) notes.push(`stopped at ${stopAt}`);
|
|
3128
|
+
if (count.matchingFailures > 0) notes.push("same error as in CI");
|
|
3129
|
+
if (count.otherFailures > 0)
|
|
3130
|
+
notes.push(`${count.otherFailures} different error${count.otherFailures === 1 ? "" : "s"} (counted apart)`);
|
|
3131
|
+
if (count.discardedRounds > 0) {
|
|
3132
|
+
notes.push(`${count.discardedRounds} round${count.discardedRounds === 1 ? "" : "s"} left out`);
|
|
3133
|
+
}
|
|
3134
|
+
const verdict = result.verdict ? result.verdict.replace("-", " ") : "";
|
|
3135
|
+
return ` ${pad(clip(name, 34), 35)}${pad(fraction(count), 7)}${pad(notes.join(" \xB7 "), 44)}${verdict}`.trimEnd();
|
|
3136
|
+
}
|
|
3137
|
+
function verdictSentence(report, arms, control) {
|
|
3138
|
+
const c = fraction(control.count);
|
|
3139
|
+
const against = control.skipped ? "" : ` (control ${c})`;
|
|
3140
|
+
if (report.kind === "verify") {
|
|
3141
|
+
const v = arms.find((a) => a.arm.id === "verify");
|
|
3142
|
+
if (!v) return "Verdict: the verify arm did not run.";
|
|
3143
|
+
const label = v.arm.label;
|
|
3144
|
+
if (report.verdict === "verified") {
|
|
3145
|
+
return `Verdict: fix verified: 0 matching failures in ${v.count.runs} runs under ${label}${against}`;
|
|
3146
|
+
}
|
|
3147
|
+
if (report.verdict === "still-fails") {
|
|
3148
|
+
return `Verdict: still fails: ${fraction(v.count)} under ${label} failed as in CI${against}`;
|
|
3149
|
+
}
|
|
3150
|
+
return `Verdict: inconclusive: ${v.count.runs} clean runs under ${label}, too few to verify the fix`;
|
|
3151
|
+
}
|
|
3152
|
+
const r = arms.find((a) => a.arm.id === report.reproducingArm);
|
|
3153
|
+
if (r) {
|
|
3154
|
+
return `Verdict: reproduced by ${r.arm.label} (${fraction(r.count)} against ${c}, p ${formatP(r.pValue ?? 1)})`;
|
|
3155
|
+
}
|
|
3156
|
+
const amplified = arms.find((a) => a.verdict === "amplified");
|
|
3157
|
+
if (amplified) {
|
|
3158
|
+
return `Verdict: not reproduced; ${amplified.arm.label} made it fail more often (${fraction(amplified.count)} against ${c}, p ${formatP(amplified.pValue ?? 1)})`;
|
|
3159
|
+
}
|
|
3160
|
+
return `Verdict: not reproduced (control ${c})`;
|
|
3161
|
+
}
|
|
3162
|
+
function bisectStepWord(code) {
|
|
3163
|
+
if (code === 0) return "good (the arm held at this commit)";
|
|
3164
|
+
if (code === 1) return "bad (a failure with the same error as in CI)";
|
|
3165
|
+
return "skip (too few clean runs to tell)";
|
|
3166
|
+
}
|
|
3167
|
+
function printHeader(plan, commit, estimate, budgetMs) {
|
|
3168
|
+
const out = [];
|
|
3169
|
+
out.push(
|
|
3170
|
+
`piwi flake${plan.kind === "verify" ? " verify" : ""} \xB7 ${plan.displayTitle} \xB7 ${plan.failures} failures in ${plan.windowDays} days`
|
|
3171
|
+
);
|
|
3172
|
+
out.push("");
|
|
3173
|
+
if (plan.kind === "verify" && plan.verifies) {
|
|
3174
|
+
out.push(
|
|
3175
|
+
`Rerunning ${plan.verifies.label}: it reproduced ${Math.round(plan.verifies.rate * 100)}% of runs on ${short(plan.verifies.commit)}`
|
|
3176
|
+
);
|
|
3177
|
+
} else if (plan.suspects.length === 0) {
|
|
3178
|
+
out.push("Suspects from history: none. Nothing in its history separates its failures from its passes.");
|
|
3179
|
+
} else {
|
|
3180
|
+
out.push(`${pad("Suspects from history", 57)}condition`);
|
|
3181
|
+
for (const s of plan.suspects) {
|
|
3182
|
+
const counts = `${s.counts.failuresWith}/${s.counts.failures} failures \xB7 ${s.counts.passesWith}/${s.counts.passes} passes`;
|
|
3183
|
+
out.push(
|
|
3184
|
+
` ${s.rank} ${pad(clip(s.label, 40), 41)}${pad(counts, 30)}${s.skipped ? `none: ${s.skipped}` : s.conditionLabel}`
|
|
3185
|
+
);
|
|
3186
|
+
if (s.sharedRoutes?.length) out.push(` both write ${s.sharedRoutes.join(", ")}`);
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
out.push("");
|
|
3190
|
+
const estimateText = estimate != null ? `up to ${formatDuration(estimate)}` : "unknown (no passing run on record)";
|
|
3191
|
+
out.push(`Estimate: ${estimateText} \xB7 budget ${formatDuration(budgetMs)}`);
|
|
3192
|
+
out.push(`Lab \xB7 ${os2.hostname()} \xB7 HEAD ${short(commit)} \xB7 ${plan.test.project ?? "default project"} \xB7 retries off`);
|
|
3193
|
+
if (commitsDiffer(commit, plan.failureCommit)) {
|
|
3194
|
+
out.push(`warning: this checkout is ${short(commit)}; the latest failure ran on ${short(plan.failureCommit)}.`);
|
|
3195
|
+
}
|
|
3196
|
+
console.log(out.join("\n"));
|
|
3197
|
+
}
|
|
3198
|
+
async function loadPlan(args, commit) {
|
|
3199
|
+
if (args.planFile) {
|
|
3200
|
+
try {
|
|
3201
|
+
const raw = JSON.parse(fs8.readFileSync(args.planFile, "utf8"));
|
|
3202
|
+
const plan = "plan" in raw && raw.plan ? raw.plan : raw;
|
|
3203
|
+
if (plan.version !== 1 || !plan.control || !Array.isArray(plan.arms)) throw new Error("not a flake-lab plan");
|
|
3204
|
+
if (args.verify !== (plan.kind === "verify")) {
|
|
3205
|
+
throw new Error(`it is a ${plan.kind} plan; run ${plan.kind === "verify" ? "flake verify" : "flake"} with it`);
|
|
3206
|
+
}
|
|
3207
|
+
return { plan: args.runs ? withRuns(plan, args.runs) : plan, http: null };
|
|
3208
|
+
} catch (error) {
|
|
3209
|
+
throw new FlakeError(`cannot read the plan ${args.planFile}: ${error.message}`);
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
if (!args.test) throw new FlakeError("name the test: a test case id or file:line");
|
|
3213
|
+
const connection = resolveCliConnection(
|
|
3214
|
+
{ serverUrl: args.serverUrl, apiKey: args.apiKey, project: args.project },
|
|
3215
|
+
process.env,
|
|
3216
|
+
[process.cwd()]
|
|
3217
|
+
);
|
|
3218
|
+
if (!connection) {
|
|
3219
|
+
throw new FlakeError(
|
|
3220
|
+
"no dashboard to read the plan from: pass --server-url or set PIWI_DASHBOARD_URL (or run it offline with --plan <file>)"
|
|
3221
|
+
);
|
|
3222
|
+
}
|
|
3223
|
+
const http = {
|
|
3224
|
+
serverUrl: connection.serverUrl,
|
|
3225
|
+
headers: connection.apiKey ? { "X-API-Key": connection.apiKey } : {}
|
|
3226
|
+
};
|
|
3227
|
+
try {
|
|
3228
|
+
const testCaseId = await resolveTestCase(http, args.test, connection.project, connection.apiKey);
|
|
3229
|
+
const params = new URLSearchParams({
|
|
3230
|
+
kind: args.verify ? "verify" : "reproduce",
|
|
3231
|
+
source: args.source ?? (process.env.CI ? "ci" : "cli"),
|
|
3232
|
+
machine: os2.hostname(),
|
|
3233
|
+
record: String(args.upload)
|
|
3234
|
+
});
|
|
3235
|
+
if (commit) params.set("commit", commit);
|
|
3236
|
+
if (args.runs) params.set("runs", String(args.runs));
|
|
3237
|
+
return { plan: await getJson(http, `/api/test-cases/${testCaseId}/flake-plan?${params}`), http };
|
|
3238
|
+
} catch (error) {
|
|
3239
|
+
if (error instanceof FlakeError) throw error;
|
|
3240
|
+
throw new FlakeError(
|
|
3241
|
+
`could not reach the dashboard at ${connection.serverUrl} (${error.message}). The plan comes from the dashboard; to run offline, pass --plan <file> with a plan saved earlier.`
|
|
3242
|
+
);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
function withRuns(plan, runs) {
|
|
3246
|
+
const set = (arm) => ({ ...arm, runs });
|
|
3247
|
+
return {
|
|
3248
|
+
...plan,
|
|
3249
|
+
control: set(plan.control),
|
|
3250
|
+
arms: plan.arms.map(set),
|
|
3251
|
+
combined: plan.combined && set(plan.combined)
|
|
3252
|
+
};
|
|
3253
|
+
}
|
|
3254
|
+
async function runFlake(argv, runner) {
|
|
3255
|
+
let args;
|
|
3256
|
+
try {
|
|
3257
|
+
args = parseFlakeArgs(argv);
|
|
3258
|
+
} catch (error) {
|
|
3259
|
+
console.error(`piwi flake: ${error.message}
|
|
3260
|
+
`);
|
|
3261
|
+
console.error(USAGE4);
|
|
3262
|
+
return EXIT_ERROR4;
|
|
3263
|
+
}
|
|
3264
|
+
if (args.help) {
|
|
3265
|
+
console.log(USAGE4);
|
|
3266
|
+
return 0;
|
|
3267
|
+
}
|
|
3268
|
+
const commit = gitHead();
|
|
3269
|
+
let plan;
|
|
3270
|
+
let http;
|
|
3271
|
+
try {
|
|
3272
|
+
({ plan, http } = await loadPlan(args, commit));
|
|
3273
|
+
} catch (error) {
|
|
3274
|
+
console.error(`piwi flake: ${error.message}`);
|
|
3275
|
+
return EXIT_ERROR4;
|
|
3276
|
+
}
|
|
3277
|
+
if (args.suspect != null && !plan.arms.some((a) => a.rank === args.suspect)) {
|
|
3278
|
+
console.error(`piwi flake: suspect ${args.suspect} has no arm (${plan.arms.length} arm(s) in the plan).`);
|
|
3279
|
+
return EXIT_ERROR4;
|
|
3280
|
+
}
|
|
3281
|
+
if (plan.kind === "reproduce" && plan.arms.length === 0) {
|
|
3282
|
+
if (!args.json) printHeader(plan, commit, null, args.budgetMs);
|
|
3283
|
+
console.error("piwi flake: no suspect with a condition to test, so there is nothing to run.");
|
|
3284
|
+
return 1;
|
|
3285
|
+
}
|
|
3286
|
+
const planned = [
|
|
3287
|
+
...args.bisect ? [] : [plan.control],
|
|
3288
|
+
...args.suspect != null ? plan.arms.filter((a) => a.rank === args.suspect) : plan.arms
|
|
3289
|
+
];
|
|
3290
|
+
if (args.all && plan.combined) planned.push(plan.combined);
|
|
3291
|
+
const estimate = estimateFlakeSessionMs(planned, plan.medianDurationMs);
|
|
3292
|
+
if (!args.json) {
|
|
3293
|
+
printHeader(plan, commit, estimate, args.budgetMs);
|
|
3294
|
+
}
|
|
3295
|
+
const dir = fs8.mkdtempSync(path11.join(os2.tmpdir(), "piwi-flake-"));
|
|
3296
|
+
const log = path11.join(dir, "playwright.log");
|
|
3297
|
+
let session;
|
|
3298
|
+
try {
|
|
3299
|
+
session = await runSession(runner ?? playwrightRunner(dir, log), plan, args, (result) => {
|
|
3300
|
+
if (!args.json && !result.skipped?.startsWith("bisect")) console.log(armLine(result, result.arm.stopAt));
|
|
3301
|
+
});
|
|
3302
|
+
} catch (error) {
|
|
3303
|
+
console.error(`piwi flake: ${error.message}`);
|
|
3304
|
+
if (fs8.existsSync(log)) {
|
|
3305
|
+
const tail = fs8.readFileSync(log, "utf8").trimEnd().split("\n").slice(-LOG_TAIL_LINES).join("\n");
|
|
3306
|
+
console.error(`
|
|
3307
|
+
Playwright's output (${log}):
|
|
3308
|
+
${tail}`);
|
|
3309
|
+
}
|
|
3310
|
+
if (args.bisect) {
|
|
3311
|
+
console.log("Bisect step: skip (this commit could not run the arm)");
|
|
3312
|
+
return EXIT_BISECT_SKIP;
|
|
3313
|
+
}
|
|
3314
|
+
return EXIT_ERROR4;
|
|
3315
|
+
}
|
|
3316
|
+
const { verdict, reproducingArm } = sessionVerdict(plan.kind, session.arms);
|
|
3317
|
+
const ran = [session.control, ...session.arms.filter((a) => !a.skipped)];
|
|
3318
|
+
let uploaded = false;
|
|
3319
|
+
if (args.upload && http && plan.experimentId) {
|
|
3320
|
+
try {
|
|
3321
|
+
const res = await fetch(`${http.serverUrl}/api/projects/${plan.projectId}/flake-lab/results`, {
|
|
3322
|
+
method: "POST",
|
|
3323
|
+
headers: { "Content-Type": "application/json", ...http.headers },
|
|
3324
|
+
body: JSON.stringify({
|
|
3325
|
+
experimentId: plan.experimentId,
|
|
3326
|
+
commit,
|
|
3327
|
+
playwrightProject: plan.test.project,
|
|
3328
|
+
arms: ran.map((r) => ({
|
|
3329
|
+
id: r.arm.id,
|
|
3330
|
+
label: r.arm.label,
|
|
3331
|
+
suspectId: r.arm.suspectId,
|
|
3332
|
+
conditions: r.arm.conditions,
|
|
3333
|
+
runs: r.count.runs,
|
|
3334
|
+
matchingFailures: r.count.matchingFailures,
|
|
3335
|
+
otherFailures: r.count.otherFailures,
|
|
3336
|
+
discardedRounds: r.count.discardedRounds,
|
|
3337
|
+
stoppedEarly: r.count.stoppedEarly
|
|
3338
|
+
}))
|
|
3339
|
+
})
|
|
3340
|
+
});
|
|
3341
|
+
if (!res.ok) {
|
|
3342
|
+
const body = await res.json().catch(() => ({}));
|
|
3343
|
+
throw new Error(body.message || `the dashboard returned ${res.status}`);
|
|
3344
|
+
}
|
|
3345
|
+
uploaded = true;
|
|
3346
|
+
} catch (error) {
|
|
3347
|
+
console.error(`piwi flake: could not save the results \u2014 ${error.message}`);
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
const code = args.bisect ? bisectExitCode(verdict) : exitCodeFor(verdict);
|
|
3351
|
+
const verifyCommand = `npx @piwitests/reporter flake verify ${plan.testCaseId}`;
|
|
3352
|
+
const report = {
|
|
3353
|
+
kind: plan.kind,
|
|
3354
|
+
testCaseId: plan.testCaseId,
|
|
3355
|
+
test: plan.displayTitle,
|
|
3356
|
+
experimentId: plan.experimentId,
|
|
3357
|
+
commit,
|
|
3358
|
+
failureCommit: plan.failureCommit,
|
|
3359
|
+
commitsDiffer: commitsDiffer(commit, plan.failureCommit),
|
|
3360
|
+
playwrightProject: plan.test.project,
|
|
3361
|
+
estimateMs: estimate,
|
|
3362
|
+
arms: [session.control, ...session.arms].map((r) => ({
|
|
3363
|
+
id: r.arm.id,
|
|
3364
|
+
label: r.arm.label,
|
|
3365
|
+
suspectId: r.arm.suspectId,
|
|
3366
|
+
rank: r.arm.rank,
|
|
3367
|
+
...r.count,
|
|
3368
|
+
verdict: r.verdict,
|
|
3369
|
+
pValue: r.pValue,
|
|
3370
|
+
skipped: r.skipped
|
|
3371
|
+
})),
|
|
3372
|
+
verdict,
|
|
3373
|
+
reproducingArm,
|
|
3374
|
+
uploaded,
|
|
3375
|
+
verifyCommand: plan.kind === "reproduce" && verdict === "reproduced" ? verifyCommand : null,
|
|
3376
|
+
exitCode: code
|
|
3377
|
+
};
|
|
3378
|
+
fs8.rmSync(dir, { recursive: true, force: true });
|
|
3379
|
+
if (args.json) {
|
|
3380
|
+
console.log(JSON.stringify(report, null, 2));
|
|
3381
|
+
return code;
|
|
3382
|
+
}
|
|
3383
|
+
const lines = [verdictSentence(report, session.arms, session.control)];
|
|
3384
|
+
if (args.bisect) lines.push(`Bisect step: ${bisectStepWord(code)}`);
|
|
3385
|
+
if (uploaded)
|
|
3386
|
+
lines.push(`Saved to the test's Flakiness tab.${report.verifyCommand ? ` After your fix: ${verifyCommand}` : ""}`);
|
|
3387
|
+
else if (report.verifyCommand) lines.push(`Not saved (--no-upload). After your fix: ${verifyCommand}`);
|
|
3388
|
+
console.log(lines.join("\n"));
|
|
3389
|
+
return code;
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
// src/cli/gate.ts
|
|
3393
|
+
var fs9 = __toESM(require("fs"));
|
|
2383
3394
|
|
|
2384
3395
|
// ../core/src/gate.ts
|
|
2385
3396
|
function formatGateResult(result) {
|
|
@@ -2402,8 +3413,8 @@ function formatGateResult(result) {
|
|
|
2402
3413
|
// src/cli/gate.ts
|
|
2403
3414
|
var EXIT_OK4 = 0;
|
|
2404
3415
|
var EXIT_VIOLATED = 1;
|
|
2405
|
-
var
|
|
2406
|
-
var
|
|
3416
|
+
var EXIT_ERROR5 = 2;
|
|
3417
|
+
var USAGE5 = `
|
|
2407
3418
|
piwi gate \u2014 fail a CI job on the dashboard's analysis of a run
|
|
2408
3419
|
|
|
2409
3420
|
Usage:
|
|
@@ -2453,9 +3464,9 @@ function readCount(argv, name) {
|
|
|
2453
3464
|
if (!Number.isFinite(n) || n < 0) throw new Error(`${name} expects a non-negative number, got "${raw}"`);
|
|
2454
3465
|
return Math.floor(n);
|
|
2455
3466
|
}
|
|
2456
|
-
function readRunIdFromFile(
|
|
3467
|
+
function readRunIdFromFile(path19) {
|
|
2457
3468
|
try {
|
|
2458
|
-
const parsed = JSON.parse(
|
|
3469
|
+
const parsed = JSON.parse(fs9.readFileSync(path19, "utf-8"));
|
|
2459
3470
|
const runId = Number(parsed.runId);
|
|
2460
3471
|
return Number.isFinite(runId) && runId > 0 ? runId : null;
|
|
2461
3472
|
} catch {
|
|
@@ -2510,7 +3521,7 @@ async function requestGate(args) {
|
|
|
2510
3521
|
}
|
|
2511
3522
|
async function runGate(argv, env = process.env) {
|
|
2512
3523
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
2513
|
-
console.log(
|
|
3524
|
+
console.log(USAGE5);
|
|
2514
3525
|
return EXIT_OK4;
|
|
2515
3526
|
}
|
|
2516
3527
|
let args;
|
|
@@ -2519,15 +3530,15 @@ async function runGate(argv, env = process.env) {
|
|
|
2519
3530
|
} catch (e) {
|
|
2520
3531
|
console.error(`piwi gate: ${e.message}
|
|
2521
3532
|
`);
|
|
2522
|
-
console.error(
|
|
2523
|
-
return
|
|
3533
|
+
console.error(USAGE5);
|
|
3534
|
+
return EXIT_ERROR5;
|
|
2524
3535
|
}
|
|
2525
3536
|
let result;
|
|
2526
3537
|
try {
|
|
2527
3538
|
result = await requestGate(args);
|
|
2528
3539
|
} catch (e) {
|
|
2529
3540
|
console.error(`piwi gate: ${e.message}`);
|
|
2530
|
-
return
|
|
3541
|
+
return EXIT_ERROR5;
|
|
2531
3542
|
}
|
|
2532
3543
|
if (argv.includes("--json")) {
|
|
2533
3544
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -2538,9 +3549,9 @@ async function runGate(argv, env = process.env) {
|
|
|
2538
3549
|
}
|
|
2539
3550
|
|
|
2540
3551
|
// src/cli/init.ts
|
|
2541
|
-
var
|
|
2542
|
-
var
|
|
2543
|
-
var
|
|
3552
|
+
var fs12 = __toESM(require("fs"));
|
|
3553
|
+
var path14 = __toESM(require("path"));
|
|
3554
|
+
var import_node_child_process5 = require("child_process");
|
|
2544
3555
|
|
|
2545
3556
|
// src/cli/report.ts
|
|
2546
3557
|
var STATUS_MARK = {
|
|
@@ -2557,8 +3568,8 @@ function formatStep(result) {
|
|
|
2557
3568
|
}
|
|
2558
3569
|
|
|
2559
3570
|
// src/cli/detect.ts
|
|
2560
|
-
var
|
|
2561
|
-
var
|
|
3571
|
+
var fs10 = __toESM(require("fs"));
|
|
3572
|
+
var path12 = __toESM(require("path"));
|
|
2562
3573
|
var CONFIG_NAMES = [
|
|
2563
3574
|
"playwright.config.ts",
|
|
2564
3575
|
"playwright.config.mts",
|
|
@@ -2576,9 +3587,9 @@ var LOCKFILES = [
|
|
|
2576
3587
|
];
|
|
2577
3588
|
var REPORTER_PACKAGE = "@piwitests/reporter";
|
|
2578
3589
|
function readPackageJson(root) {
|
|
2579
|
-
const pkgPath =
|
|
3590
|
+
const pkgPath = path12.join(root, "package.json");
|
|
2580
3591
|
try {
|
|
2581
|
-
return { pkg: JSON.parse(
|
|
3592
|
+
return { pkg: JSON.parse(fs10.readFileSync(pkgPath, "utf-8")), pkgPath };
|
|
2582
3593
|
} catch {
|
|
2583
3594
|
return { pkg: null, pkgPath: null };
|
|
2584
3595
|
}
|
|
@@ -2587,14 +3598,14 @@ function detectPackageManager(root, pkg) {
|
|
|
2587
3598
|
const declared = pkg?.packageManager?.split("@")[0];
|
|
2588
3599
|
if (declared === "pnpm" || declared === "yarn" || declared === "bun" || declared === "npm") return declared;
|
|
2589
3600
|
for (const [file, manager] of LOCKFILES) {
|
|
2590
|
-
if (
|
|
3601
|
+
if (fs10.existsSync(path12.join(root, file))) return manager;
|
|
2591
3602
|
}
|
|
2592
3603
|
return "npm";
|
|
2593
3604
|
}
|
|
2594
3605
|
function findConfig(root) {
|
|
2595
3606
|
for (const name of CONFIG_NAMES) {
|
|
2596
|
-
const candidate =
|
|
2597
|
-
if (
|
|
3607
|
+
const candidate = path12.join(root, name);
|
|
3608
|
+
if (fs10.existsSync(candidate)) return candidate;
|
|
2598
3609
|
}
|
|
2599
3610
|
return null;
|
|
2600
3611
|
}
|
|
@@ -2604,14 +3615,14 @@ function langOf(configPath) {
|
|
|
2604
3615
|
}
|
|
2605
3616
|
function suggestProjectName(root, pkg) {
|
|
2606
3617
|
const fromPkg = pkg?.name?.replace(/^@[^/]+\//, "").trim();
|
|
2607
|
-
return fromPkg ||
|
|
3618
|
+
return fromPkg || path12.basename(root) || "default-project";
|
|
2608
3619
|
}
|
|
2609
3620
|
function hasReporter(pkg) {
|
|
2610
3621
|
if (!pkg) return false;
|
|
2611
3622
|
return Boolean(pkg.dependencies?.[REPORTER_PACKAGE] || pkg.devDependencies?.[REPORTER_PACKAGE]);
|
|
2612
3623
|
}
|
|
2613
3624
|
function detectProject(root) {
|
|
2614
|
-
const absRoot =
|
|
3625
|
+
const absRoot = path12.resolve(root);
|
|
2615
3626
|
const { pkg, pkgPath } = readPackageJson(absRoot);
|
|
2616
3627
|
const configPath = findConfig(absRoot);
|
|
2617
3628
|
return {
|
|
@@ -2753,8 +3764,8 @@ function ensureGitignoreEntry(existing, entry = ".env") {
|
|
|
2753
3764
|
}
|
|
2754
3765
|
|
|
2755
3766
|
// src/cli/skills.ts
|
|
2756
|
-
var
|
|
2757
|
-
var
|
|
3767
|
+
var fs11 = __toESM(require("fs"));
|
|
3768
|
+
var path13 = __toESM(require("path"));
|
|
2758
3769
|
var SETUP_SKILL = "setup-piwi";
|
|
2759
3770
|
var WORKFLOW_SKILLS = [
|
|
2760
3771
|
"investigate-failure",
|
|
@@ -2765,16 +3776,16 @@ var WORKFLOW_SKILLS = [
|
|
|
2765
3776
|
"fix-a-reported-bug"
|
|
2766
3777
|
];
|
|
2767
3778
|
var ALL_SKILLS = [SETUP_SKILL, ...WORKFLOW_SKILLS];
|
|
2768
|
-
var DEFAULT_SKILLS_DIR =
|
|
3779
|
+
var DEFAULT_SKILLS_DIR = path13.join(".claude", "skills");
|
|
2769
3780
|
function findTemplatesDir(fromDir) {
|
|
2770
3781
|
let dir = fromDir;
|
|
2771
3782
|
for (let i = 0; i < 6; i++) {
|
|
2772
|
-
if (
|
|
2773
|
-
const parent =
|
|
3783
|
+
if (fs11.existsSync(path13.join(dir, "templates", "skills"))) return path13.join(dir, "templates");
|
|
3784
|
+
const parent = path13.dirname(dir);
|
|
2774
3785
|
if (parent === dir) break;
|
|
2775
3786
|
dir = parent;
|
|
2776
3787
|
}
|
|
2777
|
-
return
|
|
3788
|
+
return path13.resolve(fromDir, "..", "..", "templates");
|
|
2778
3789
|
}
|
|
2779
3790
|
function readFrontMatter(markdown) {
|
|
2780
3791
|
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
|
|
@@ -2787,14 +3798,14 @@ function readFrontMatter(markdown) {
|
|
|
2787
3798
|
return out;
|
|
2788
3799
|
}
|
|
2789
3800
|
function templatePath(templatesDir, slug) {
|
|
2790
|
-
return
|
|
3801
|
+
return path13.join(templatesDir, "skills", slug, "SKILL.md");
|
|
2791
3802
|
}
|
|
2792
3803
|
function listSkills(templatesDir, slugs = ALL_SKILLS) {
|
|
2793
3804
|
const infos = [];
|
|
2794
3805
|
for (const slug of slugs) {
|
|
2795
3806
|
const file = templatePath(templatesDir, slug);
|
|
2796
|
-
if (!
|
|
2797
|
-
const front = readFrontMatter(
|
|
3807
|
+
if (!fs11.existsSync(file)) continue;
|
|
3808
|
+
const front = readFrontMatter(fs11.readFileSync(file, "utf-8"));
|
|
2798
3809
|
infos.push({ slug, name: front.name ?? slug, description: front.description ?? "" });
|
|
2799
3810
|
}
|
|
2800
3811
|
return infos;
|
|
@@ -2804,16 +3815,16 @@ function installSkills(opts) {
|
|
|
2804
3815
|
for (const slug of opts.slugs) {
|
|
2805
3816
|
const step = `skill:${slug}`;
|
|
2806
3817
|
const source = templatePath(opts.templatesDir, slug);
|
|
2807
|
-
const relDest =
|
|
2808
|
-
const dest =
|
|
2809
|
-
if (!
|
|
3818
|
+
const relDest = path13.join(opts.skillsDir, slug, "SKILL.md");
|
|
3819
|
+
const dest = path13.join(opts.root, relDest);
|
|
3820
|
+
if (!fs11.existsSync(source)) {
|
|
2810
3821
|
results.push({ step, status: "error", detail: `no template found for "${slug}"` });
|
|
2811
3822
|
continue;
|
|
2812
3823
|
}
|
|
2813
|
-
const contents =
|
|
2814
|
-
const exists =
|
|
3824
|
+
const contents = fs11.readFileSync(source, "utf-8");
|
|
3825
|
+
const exists = fs11.existsSync(dest);
|
|
2815
3826
|
if (exists && !opts.force) {
|
|
2816
|
-
const identical =
|
|
3827
|
+
const identical = fs11.readFileSync(dest, "utf-8") === contents;
|
|
2817
3828
|
results.push({
|
|
2818
3829
|
step,
|
|
2819
3830
|
file: relDest,
|
|
@@ -2823,14 +3834,14 @@ function installSkills(opts) {
|
|
|
2823
3834
|
continue;
|
|
2824
3835
|
}
|
|
2825
3836
|
if (!opts.dryRun) {
|
|
2826
|
-
|
|
2827
|
-
|
|
3837
|
+
fs11.mkdirSync(path13.dirname(dest), { recursive: true });
|
|
3838
|
+
fs11.writeFileSync(dest, contents);
|
|
2828
3839
|
}
|
|
2829
3840
|
results.push({ step, file: relDest, status: exists ? "updated" : "created", detail: "installed skill" });
|
|
2830
3841
|
}
|
|
2831
3842
|
return results;
|
|
2832
3843
|
}
|
|
2833
|
-
var
|
|
3844
|
+
var USAGE6 = `
|
|
2834
3845
|
piwi skills \u2014 install the Piwi agent skills into this project
|
|
2835
3846
|
|
|
2836
3847
|
Usage:
|
|
@@ -2869,7 +3880,7 @@ function positionalSlugs(argv) {
|
|
|
2869
3880
|
function runSkills(argv, templatesDir, cwd = process.cwd()) {
|
|
2870
3881
|
const [sub, ...rest] = argv;
|
|
2871
3882
|
if (sub === void 0 || sub === "-h" || sub === "--help") {
|
|
2872
|
-
console.log(
|
|
3883
|
+
console.log(USAGE6);
|
|
2873
3884
|
return 0;
|
|
2874
3885
|
}
|
|
2875
3886
|
if (sub === "list") {
|
|
@@ -2890,12 +3901,12 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
|
|
|
2890
3901
|
if (unknown.length) {
|
|
2891
3902
|
console.error(`piwi skills: unknown skill(s): ${unknown.join(", ")}
|
|
2892
3903
|
`);
|
|
2893
|
-
console.error(
|
|
3904
|
+
console.error(USAGE6);
|
|
2894
3905
|
return 2;
|
|
2895
3906
|
}
|
|
2896
3907
|
const results = installSkills({
|
|
2897
3908
|
templatesDir,
|
|
2898
|
-
root:
|
|
3909
|
+
root: path13.resolve(readOption5(rest, "--cwd") ?? cwd),
|
|
2899
3910
|
skillsDir: readOption5(rest, "--dir") ?? DEFAULT_SKILLS_DIR,
|
|
2900
3911
|
slugs: requested.length ? requested : ALL_SKILLS,
|
|
2901
3912
|
force: rest.includes("--force"),
|
|
@@ -2911,13 +3922,13 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
|
|
|
2911
3922
|
}
|
|
2912
3923
|
console.error(`piwi skills: unknown command "${sub}"
|
|
2913
3924
|
`);
|
|
2914
|
-
console.error(
|
|
3925
|
+
console.error(USAGE6);
|
|
2915
3926
|
return 2;
|
|
2916
3927
|
}
|
|
2917
3928
|
|
|
2918
3929
|
// src/cli/init.ts
|
|
2919
3930
|
var DEFAULT_SERVER_URL = "http://localhost:3000";
|
|
2920
|
-
var
|
|
3931
|
+
var USAGE7 = `
|
|
2921
3932
|
piwi init \u2014 wire a Playwright project up to a Piwi Dashboard
|
|
2922
3933
|
|
|
2923
3934
|
Usage:
|
|
@@ -2966,7 +3977,7 @@ function resolveSkillSlugs(raw) {
|
|
|
2966
3977
|
return raw.split(",").map((slug) => slug.trim()).filter(Boolean);
|
|
2967
3978
|
}
|
|
2968
3979
|
function parseInitArgs(argv, env, cwd) {
|
|
2969
|
-
const root =
|
|
3980
|
+
const root = path14.resolve(readOption6(argv, "--cwd") ?? cwd);
|
|
2970
3981
|
const detected = detectProject(root);
|
|
2971
3982
|
return {
|
|
2972
3983
|
root,
|
|
@@ -2985,7 +3996,7 @@ function parseInitArgs(argv, env, cwd) {
|
|
|
2985
3996
|
}
|
|
2986
3997
|
function readFileOr(file, fallback) {
|
|
2987
3998
|
try {
|
|
2988
|
-
return
|
|
3999
|
+
return fs12.readFileSync(file, "utf-8");
|
|
2989
4000
|
} catch {
|
|
2990
4001
|
return fallback;
|
|
2991
4002
|
}
|
|
@@ -3015,7 +4026,7 @@ function stepInstallReporter(project, opts) {
|
|
|
3015
4026
|
if (opts.dryRun || !opts.install)
|
|
3016
4027
|
return { step, status: "manual", detail: `Run \`${installCommand(project.packageManager)}\`` };
|
|
3017
4028
|
const [command, args] = installArgv(project.packageManager);
|
|
3018
|
-
const result = (0,
|
|
4029
|
+
const result = (0, import_node_child_process5.spawnSync)(command, args, { cwd: opts.root, stdio: "inherit" });
|
|
3019
4030
|
if (result.status === 0) return { step, status: "updated", detail: `Installed ${REPORTER_PACKAGE}` };
|
|
3020
4031
|
return {
|
|
3021
4032
|
step,
|
|
@@ -3031,19 +4042,19 @@ function stepConfig(project, opts) {
|
|
|
3031
4042
|
status: "manual",
|
|
3032
4043
|
detail: "No playwright.config found \u2014 create one, then re-run `npx @piwitests/reporter init`"
|
|
3033
4044
|
};
|
|
3034
|
-
const rel =
|
|
4045
|
+
const rel = path14.relative(project.root, project.configPath) || path14.basename(project.configPath);
|
|
3035
4046
|
const edit = wrapPlaywrightConfig(readFileOr(project.configPath, ""), {
|
|
3036
4047
|
serverUrl: opts.serverUrl,
|
|
3037
4048
|
projectName: opts.projectName
|
|
3038
4049
|
});
|
|
3039
|
-
if (edit.status === "updated" && !opts.dryRun)
|
|
4050
|
+
if (edit.status === "updated" && !opts.dryRun) fs12.writeFileSync(project.configPath, edit.text);
|
|
3040
4051
|
return { step, file: rel, status: edit.status, detail: edit.detail };
|
|
3041
4052
|
}
|
|
3042
4053
|
function stepFixtures(project, opts) {
|
|
3043
4054
|
const step = "fixtures";
|
|
3044
|
-
const rel =
|
|
3045
|
-
const dest =
|
|
3046
|
-
if (
|
|
4055
|
+
const rel = path14.join("tests", project.configLang === "js" ? "fixtures.js" : "fixtures.ts");
|
|
4056
|
+
const dest = path14.join(project.root, rel);
|
|
4057
|
+
if (fs12.existsSync(dest)) {
|
|
3047
4058
|
const current = readFileOr(dest, "");
|
|
3048
4059
|
if (/piwiFixtures/.test(current))
|
|
3049
4060
|
return { step, file: rel, status: "already", detail: "capture fixtures already set up" };
|
|
@@ -3055,8 +4066,8 @@ function stepFixtures(project, opts) {
|
|
|
3055
4066
|
};
|
|
3056
4067
|
}
|
|
3057
4068
|
if (!opts.dryRun) {
|
|
3058
|
-
|
|
3059
|
-
|
|
4069
|
+
fs12.mkdirSync(path14.dirname(dest), { recursive: true });
|
|
4070
|
+
fs12.writeFileSync(dest, fixturesContents());
|
|
3060
4071
|
}
|
|
3061
4072
|
return {
|
|
3062
4073
|
step,
|
|
@@ -3067,12 +4078,12 @@ function stepFixtures(project, opts) {
|
|
|
3067
4078
|
}
|
|
3068
4079
|
function stepEnv(project, opts) {
|
|
3069
4080
|
const results = [];
|
|
3070
|
-
const examplePath =
|
|
4081
|
+
const examplePath = path14.join(project.root, ".env.example");
|
|
3071
4082
|
const example = upsertEnvKeys(readFileOr(examplePath, ""), [
|
|
3072
4083
|
["PIWI_DASHBOARD_URL", opts.serverUrl],
|
|
3073
4084
|
["PIWI_API_KEY", ""]
|
|
3074
4085
|
]);
|
|
3075
|
-
if (example.added.length && !opts.dryRun)
|
|
4086
|
+
if (example.added.length && !opts.dryRun) fs12.writeFileSync(examplePath, example.text);
|
|
3076
4087
|
results.push({
|
|
3077
4088
|
step: "env",
|
|
3078
4089
|
file: ".env.example",
|
|
@@ -3080,21 +4091,21 @@ function stepEnv(project, opts) {
|
|
|
3080
4091
|
detail: example.added.length ? `Recorded ${example.added.join(", ")}` : "connection template already present"
|
|
3081
4092
|
});
|
|
3082
4093
|
if (opts.apiKey) {
|
|
3083
|
-
const envPath =
|
|
4094
|
+
const envPath = path14.join(project.root, ".env");
|
|
3084
4095
|
const env = upsertEnvKeys(readFileOr(envPath, ""), [
|
|
3085
4096
|
["PIWI_DASHBOARD_URL", opts.serverUrl],
|
|
3086
4097
|
["PIWI_API_KEY", opts.apiKey]
|
|
3087
4098
|
]);
|
|
3088
|
-
if (env.added.length && !opts.dryRun)
|
|
4099
|
+
if (env.added.length && !opts.dryRun) fs12.writeFileSync(envPath, env.text);
|
|
3089
4100
|
results.push({
|
|
3090
4101
|
step: "env",
|
|
3091
4102
|
file: ".env",
|
|
3092
4103
|
status: env.added.length ? "updated" : "already",
|
|
3093
4104
|
detail: env.added.length ? `Wrote ${env.added.join(", ")} (keep .env out of git)` : "already set"
|
|
3094
4105
|
});
|
|
3095
|
-
const gitignorePath =
|
|
4106
|
+
const gitignorePath = path14.join(project.root, ".gitignore");
|
|
3096
4107
|
const gitignore = ensureGitignoreEntry(readFileOr(gitignorePath, ""));
|
|
3097
|
-
if (gitignore.added && !opts.dryRun)
|
|
4108
|
+
if (gitignore.added && !opts.dryRun) fs12.writeFileSync(gitignorePath, gitignore.text);
|
|
3098
4109
|
results.push({
|
|
3099
4110
|
step: "gitignore",
|
|
3100
4111
|
file: ".gitignore",
|
|
@@ -3108,7 +4119,7 @@ function nextSteps(opts, steps) {
|
|
|
3108
4119
|
const out = [];
|
|
3109
4120
|
if (!opts.apiKey) {
|
|
3110
4121
|
out.push(
|
|
3111
|
-
"If your dashboard has authentication on, create an API key (Settings \u2192
|
|
4122
|
+
"If your dashboard has authentication on, create an API key (Settings \u2192 Account \u2192 API keys), put it in .env as PIWI_API_KEY, and keep .env out of git."
|
|
3112
4123
|
);
|
|
3113
4124
|
}
|
|
3114
4125
|
if (steps.some((s) => s.status === "manual")) {
|
|
@@ -3123,7 +4134,7 @@ function nextSteps(opts, steps) {
|
|
|
3123
4134
|
}
|
|
3124
4135
|
async function runInit(argv, env = process.env, cwd = process.cwd()) {
|
|
3125
4136
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
3126
|
-
console.log(
|
|
4137
|
+
console.log(USAGE7);
|
|
3127
4138
|
return 0;
|
|
3128
4139
|
}
|
|
3129
4140
|
const opts = parseInitArgs(argv, env, cwd);
|
|
@@ -3156,7 +4167,7 @@ async function runInit(argv, env = process.env, cwd = process.cwd()) {
|
|
|
3156
4167
|
project: {
|
|
3157
4168
|
root: project.root,
|
|
3158
4169
|
packageManager: project.packageManager,
|
|
3159
|
-
configPath: project.configPath ?
|
|
4170
|
+
configPath: project.configPath ? path14.relative(project.root, project.configPath) : null,
|
|
3160
4171
|
projectName: opts.projectName,
|
|
3161
4172
|
serverUrl: opts.serverUrl
|
|
3162
4173
|
},
|
|
@@ -3180,15 +4191,15 @@ Piwi setup${opts.dryRun ? " (dry run \u2014 nothing written)" : ""} for "${opts.
|
|
|
3180
4191
|
}
|
|
3181
4192
|
|
|
3182
4193
|
// src/cli/select.ts
|
|
3183
|
-
var
|
|
3184
|
-
var
|
|
3185
|
-
var
|
|
3186
|
-
var
|
|
4194
|
+
var fs14 = __toESM(require("fs"));
|
|
4195
|
+
var path16 = __toESM(require("path"));
|
|
4196
|
+
var import_node_child_process6 = require("child_process");
|
|
4197
|
+
var import_node_module4 = require("module");
|
|
3187
4198
|
|
|
3188
4199
|
// src/cli/add-reporter.ts
|
|
3189
|
-
var
|
|
3190
|
-
var
|
|
3191
|
-
var
|
|
4200
|
+
var fs13 = __toESM(require("fs"));
|
|
4201
|
+
var path15 = __toESM(require("path"));
|
|
4202
|
+
var import_node_module3 = require("module");
|
|
3192
4203
|
var REPORTER_PACKAGE2 = "@piwitests/reporter";
|
|
3193
4204
|
var ADD_REPORTER_FLAG = "--add-reporter";
|
|
3194
4205
|
var ADD_REPORTER_MIN = { major: 1, minor: 63 };
|
|
@@ -3215,14 +4226,14 @@ function decideAddReporter(configSource, playwrightVersion) {
|
|
|
3215
4226
|
log: `piwi run: the Playwright config has no Piwi reporter and Playwright ${playwrightVersion ?? "unknown"} predates ${ADD_REPORTER_FLAG} (1.63) \u2014 results will not reach the dashboard`
|
|
3216
4227
|
};
|
|
3217
4228
|
}
|
|
3218
|
-
function resolveConfigPath(cwd,
|
|
3219
|
-
for (let i = 0; i <
|
|
3220
|
-
const arg =
|
|
4229
|
+
function resolveConfigPath(cwd, playwrightArgs2) {
|
|
4230
|
+
for (let i = 0; i < playwrightArgs2.length; i++) {
|
|
4231
|
+
const arg = playwrightArgs2[i];
|
|
3221
4232
|
if (arg === "--config" || arg === "-c") {
|
|
3222
|
-
const next =
|
|
3223
|
-
if (next) return
|
|
4233
|
+
const next = playwrightArgs2[i + 1];
|
|
4234
|
+
if (next) return path15.resolve(cwd, next);
|
|
3224
4235
|
} else if (arg.startsWith("--config=")) {
|
|
3225
|
-
return
|
|
4236
|
+
return path15.resolve(cwd, arg.slice("--config=".length));
|
|
3226
4237
|
}
|
|
3227
4238
|
}
|
|
3228
4239
|
return detectProject(cwd).configPath;
|
|
@@ -3230,14 +4241,14 @@ function resolveConfigPath(cwd, playwrightArgs) {
|
|
|
3230
4241
|
function readConfigSource(configPath) {
|
|
3231
4242
|
if (!configPath) return null;
|
|
3232
4243
|
try {
|
|
3233
|
-
return
|
|
4244
|
+
return fs13.readFileSync(configPath, "utf-8");
|
|
3234
4245
|
} catch {
|
|
3235
4246
|
return null;
|
|
3236
4247
|
}
|
|
3237
4248
|
}
|
|
3238
4249
|
function readInstalledPlaywrightVersion(cwd) {
|
|
3239
4250
|
try {
|
|
3240
|
-
const require2 = (0,
|
|
4251
|
+
const require2 = (0, import_node_module3.createRequire)(path15.join(cwd, "noop.js"));
|
|
3241
4252
|
for (const id of ["@playwright/test/package.json", "playwright/package.json"]) {
|
|
3242
4253
|
try {
|
|
3243
4254
|
return require2(id).version ?? null;
|
|
@@ -3248,15 +4259,15 @@ function readInstalledPlaywrightVersion(cwd) {
|
|
|
3248
4259
|
}
|
|
3249
4260
|
return null;
|
|
3250
4261
|
}
|
|
3251
|
-
function computeAddReporterArgs(cwd,
|
|
3252
|
-
const source = readConfigSource(resolveConfigPath(cwd,
|
|
4262
|
+
function computeAddReporterArgs(cwd, playwrightArgs2) {
|
|
4263
|
+
const source = readConfigSource(resolveConfigPath(cwd, playwrightArgs2));
|
|
3253
4264
|
return decideAddReporter(source, readInstalledPlaywrightVersion(cwd));
|
|
3254
4265
|
}
|
|
3255
4266
|
|
|
3256
4267
|
// src/cli/select.ts
|
|
3257
4268
|
var EXIT_OK5 = 0;
|
|
3258
|
-
var
|
|
3259
|
-
var
|
|
4269
|
+
var EXIT_ERROR6 = 2;
|
|
4270
|
+
var USAGE8 = `
|
|
3260
4271
|
piwi select / piwi run \u2014 run a saved selection of tests
|
|
3261
4272
|
|
|
3262
4273
|
Usage:
|
|
@@ -3314,7 +4325,7 @@ function findKey(argv) {
|
|
|
3314
4325
|
}
|
|
3315
4326
|
return void 0;
|
|
3316
4327
|
}
|
|
3317
|
-
function
|
|
4328
|
+
function parseDuration2(raw) {
|
|
3318
4329
|
const match = raw.trim().match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/);
|
|
3319
4330
|
if (!match) return null;
|
|
3320
4331
|
const value = Number(match[1]);
|
|
@@ -3334,7 +4345,7 @@ function parseSelectArgs(argv, env) {
|
|
|
3334
4345
|
const budgetRaw = readOption7(own, "--budget");
|
|
3335
4346
|
let budgetMs = null;
|
|
3336
4347
|
if (budgetRaw !== void 0) {
|
|
3337
|
-
budgetMs =
|
|
4348
|
+
budgetMs = parseDuration2(budgetRaw);
|
|
3338
4349
|
if (budgetMs === null) throw new Error(`--budget expects a duration like 5m or 90s, got "${budgetRaw}"`);
|
|
3339
4350
|
}
|
|
3340
4351
|
const shard = readOption7(own, "--shard") ?? null;
|
|
@@ -3357,51 +4368,52 @@ function parseSelectArgs(argv, env) {
|
|
|
3357
4368
|
extra
|
|
3358
4369
|
};
|
|
3359
4370
|
}
|
|
3360
|
-
var CACHE_FILE =
|
|
3361
|
-
function cacheKey(
|
|
3362
|
-
return `${
|
|
4371
|
+
var CACHE_FILE = path16.join(".piwi", "selection-cache.json");
|
|
4372
|
+
function cacheKey(args) {
|
|
4373
|
+
return `${args.project.toLowerCase()}:${args.key}:${args.format}:${args.budgetMs ?? 0}:${args.shard ?? ""}:${args.order ?? ""}`;
|
|
3363
4374
|
}
|
|
3364
|
-
function readCache(
|
|
4375
|
+
function readCache(args) {
|
|
3365
4376
|
try {
|
|
3366
|
-
const store = JSON.parse(
|
|
3367
|
-
return store[cacheKey(
|
|
4377
|
+
const store = JSON.parse(fs14.readFileSync(CACHE_FILE, "utf-8"));
|
|
4378
|
+
return store[cacheKey(args)] ?? null;
|
|
3368
4379
|
} catch {
|
|
3369
4380
|
return null;
|
|
3370
4381
|
}
|
|
3371
4382
|
}
|
|
3372
|
-
function writeCache(
|
|
4383
|
+
function writeCache(args, resolution) {
|
|
3373
4384
|
try {
|
|
3374
4385
|
let store = {};
|
|
3375
4386
|
try {
|
|
3376
|
-
store = JSON.parse(
|
|
4387
|
+
store = JSON.parse(fs14.readFileSync(CACHE_FILE, "utf-8"));
|
|
3377
4388
|
} catch {
|
|
3378
4389
|
}
|
|
3379
|
-
store[cacheKey(
|
|
3380
|
-
|
|
3381
|
-
|
|
4390
|
+
store[cacheKey(args)] = resolution;
|
|
4391
|
+
fs14.mkdirSync(path16.dirname(CACHE_FILE), { recursive: true });
|
|
4392
|
+
fs14.writeFileSync(CACHE_FILE, JSON.stringify(store, null, 2));
|
|
3382
4393
|
} catch {
|
|
3383
4394
|
}
|
|
3384
4395
|
}
|
|
3385
|
-
async function resolveWithCache(args
|
|
4396
|
+
async function resolveWithCache(args) {
|
|
3386
4397
|
try {
|
|
4398
|
+
const projectId = await resolveProjectId(args);
|
|
3387
4399
|
const resolution = await fetchResolution(args, projectId);
|
|
3388
|
-
writeCache(
|
|
4400
|
+
writeCache(args, resolution);
|
|
3389
4401
|
return { resolution, fromCache: false };
|
|
3390
4402
|
} catch (e) {
|
|
3391
4403
|
if (args.strict) throw e;
|
|
3392
|
-
const cached = readCache(
|
|
4404
|
+
const cached = readCache(args);
|
|
3393
4405
|
if (cached) {
|
|
3394
4406
|
console.error(`piwi: dashboard unreachable, using cached resolution \u2014 ${e.message}`);
|
|
3395
4407
|
return { resolution: cached, fromCache: true };
|
|
3396
4408
|
}
|
|
3397
|
-
return
|
|
4409
|
+
return { error: e };
|
|
3398
4410
|
}
|
|
3399
4411
|
}
|
|
3400
4412
|
function printWarnings(resolution) {
|
|
3401
4413
|
for (const w of resolution.warnings) console.error(`piwi: warning [${w.code}] ${w.message}`);
|
|
3402
4414
|
}
|
|
3403
4415
|
function gitChangedFiles(base) {
|
|
3404
|
-
const out = (0,
|
|
4416
|
+
const out = (0, import_node_child_process6.execFileSync)("git", ["diff", "--name-only", base], {
|
|
3405
4417
|
encoding: "utf-8",
|
|
3406
4418
|
stdio: ["ignore", "pipe", "ignore"]
|
|
3407
4419
|
});
|
|
@@ -3422,7 +4434,7 @@ async function runSelectImpact(args) {
|
|
|
3422
4434
|
impact = await loadImpact(args, projectId);
|
|
3423
4435
|
} catch (e) {
|
|
3424
4436
|
console.error(`piwi select: ${e.message}`);
|
|
3425
|
-
return
|
|
4437
|
+
return EXIT_ERROR6;
|
|
3426
4438
|
}
|
|
3427
4439
|
printWarnings(impact);
|
|
3428
4440
|
console.error(
|
|
@@ -3437,7 +4449,7 @@ async function runSelectImpact(args) {
|
|
|
3437
4449
|
}
|
|
3438
4450
|
async function runSelect(argv, env = process.env) {
|
|
3439
4451
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
3440
|
-
console.log(
|
|
4452
|
+
console.log(USAGE8);
|
|
3441
4453
|
return EXIT_OK5;
|
|
3442
4454
|
}
|
|
3443
4455
|
let args;
|
|
@@ -3446,13 +4458,13 @@ async function runSelect(argv, env = process.env) {
|
|
|
3446
4458
|
} catch (e) {
|
|
3447
4459
|
console.error(`piwi select: ${e.message}
|
|
3448
4460
|
`);
|
|
3449
|
-
console.error(
|
|
3450
|
-
return
|
|
4461
|
+
console.error(USAGE8);
|
|
4462
|
+
return EXIT_ERROR6;
|
|
3451
4463
|
}
|
|
3452
4464
|
if (args.key === IMPACT_KEY) {
|
|
3453
4465
|
if (!args.base) {
|
|
3454
4466
|
console.error("piwi select: impact needs a base ref \u2014 pass --base <ref> (e.g. --base origin/main)");
|
|
3455
|
-
return
|
|
4467
|
+
return EXIT_ERROR6;
|
|
3456
4468
|
}
|
|
3457
4469
|
return runSelectImpact(args);
|
|
3458
4470
|
}
|
|
@@ -3462,19 +4474,19 @@ async function runSelect(argv, env = process.env) {
|
|
|
3462
4474
|
resolution = await fetchResolution(args, projectId);
|
|
3463
4475
|
} catch (e) {
|
|
3464
4476
|
console.error(`piwi select: ${e.message}`);
|
|
3465
|
-
return
|
|
4477
|
+
return EXIT_ERROR6;
|
|
3466
4478
|
}
|
|
3467
4479
|
printWarnings(resolution);
|
|
3468
4480
|
if (resolution.estimate.count === 0) {
|
|
3469
4481
|
console.error(`piwi select: "${args.key}" resolved to 0 tests`);
|
|
3470
|
-
return
|
|
4482
|
+
return EXIT_ERROR6;
|
|
3471
4483
|
}
|
|
3472
4484
|
if (args.json) console.log(JSON.stringify(resolution, null, 2));
|
|
3473
4485
|
else console.log(resolution.materialization.args.join(" "));
|
|
3474
4486
|
return EXIT_OK5;
|
|
3475
4487
|
}
|
|
3476
|
-
function
|
|
3477
|
-
const require2 = (0,
|
|
4488
|
+
function resolvePlaywrightCli3() {
|
|
4489
|
+
const require2 = (0, import_node_module4.createRequire)(path16.join(process.cwd(), "noop.js"));
|
|
3478
4490
|
for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
|
|
3479
4491
|
try {
|
|
3480
4492
|
return require2.resolve(id);
|
|
@@ -3483,24 +4495,27 @@ function resolvePlaywrightCli2() {
|
|
|
3483
4495
|
}
|
|
3484
4496
|
return null;
|
|
3485
4497
|
}
|
|
3486
|
-
function
|
|
3487
|
-
const cli =
|
|
3488
|
-
const child = cli ? (0,
|
|
4498
|
+
function spawnPlaywright2(pkgRunner, playwrightArgs2, env) {
|
|
4499
|
+
const cli = resolvePlaywrightCli3();
|
|
4500
|
+
const child = cli ? (0, import_node_child_process6.spawn)(process.execPath, [cli, "test", ...playwrightArgs2], { stdio: "inherit", env }) : (0, import_node_child_process6.spawn)(process.platform === "win32" ? `${pkgRunner}.cmd` : pkgRunner, ["playwright", "test", ...playwrightArgs2], {
|
|
3489
4501
|
stdio: "inherit",
|
|
3490
4502
|
env
|
|
3491
4503
|
});
|
|
3492
4504
|
return new Promise((resolve8) => {
|
|
3493
4505
|
child.on("error", (err) => {
|
|
3494
4506
|
console.error(`piwi run: could not start Playwright \u2014 ${err.message}`);
|
|
3495
|
-
resolve8(
|
|
4507
|
+
resolve8(EXIT_ERROR6);
|
|
3496
4508
|
});
|
|
3497
|
-
child.on("exit", (code) => resolve8(code ??
|
|
4509
|
+
child.on("exit", (code) => resolve8(code ?? EXIT_ERROR6));
|
|
3498
4510
|
});
|
|
3499
4511
|
}
|
|
3500
|
-
function spawnPlaywrightForRun(pkgRunner,
|
|
3501
|
-
const decision = computeAddReporterArgs(process.cwd(),
|
|
4512
|
+
function spawnPlaywrightForRun(pkgRunner, playwrightArgs2, env) {
|
|
4513
|
+
const decision = computeAddReporterArgs(process.cwd(), playwrightArgs2);
|
|
3502
4514
|
if (decision.log) console.error(decision.log);
|
|
3503
|
-
return
|
|
4515
|
+
return spawnPlaywright2(pkgRunner, [...decision.args, ...playwrightArgs2], env);
|
|
4516
|
+
}
|
|
4517
|
+
function shardEnv(args) {
|
|
4518
|
+
return args.shard ? { [PIWI_SHARD_ENV]: args.shard } : {};
|
|
3504
4519
|
}
|
|
3505
4520
|
async function runRunImpact(args, env) {
|
|
3506
4521
|
let projectId;
|
|
@@ -3509,7 +4524,7 @@ async function runRunImpact(args, env) {
|
|
|
3509
4524
|
} catch (e) {
|
|
3510
4525
|
if (args.strict) {
|
|
3511
4526
|
console.error(`piwi run: ${e.message}`);
|
|
3512
|
-
return
|
|
4527
|
+
return EXIT_ERROR6;
|
|
3513
4528
|
}
|
|
3514
4529
|
console.error(`piwi run: ${e.message} \u2014 running the full suite`);
|
|
3515
4530
|
return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
|
|
@@ -3520,7 +4535,7 @@ async function runRunImpact(args, env) {
|
|
|
3520
4535
|
} catch (e) {
|
|
3521
4536
|
if (args.strict) {
|
|
3522
4537
|
console.error(`piwi run: ${e.message}`);
|
|
3523
|
-
return
|
|
4538
|
+
return EXIT_ERROR6;
|
|
3524
4539
|
}
|
|
3525
4540
|
console.error(`piwi run: ${e.message} \u2014 running the full suite`);
|
|
3526
4541
|
return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
|
|
@@ -3541,14 +4556,15 @@ async function runRunImpact(args, env) {
|
|
|
3541
4556
|
PIWI_SELECTION: IMPACT_KEY,
|
|
3542
4557
|
PIWI_SELECTION_VERSION: "0",
|
|
3543
4558
|
PIWI_SELECTION_HASH: impact.resolvedHash,
|
|
3544
|
-
PIWI_SELECTION_COUNT: String(impact.estimate.count)
|
|
4559
|
+
PIWI_SELECTION_COUNT: String(impact.estimate.count),
|
|
4560
|
+
...shardEnv(args)
|
|
3545
4561
|
};
|
|
3546
4562
|
console.error(`piwi run: impact \u2192 ${impact.estimate.count} test(s)`);
|
|
3547
4563
|
return spawnPlaywrightForRun(args.pkgRunner, [...impact.materialization.args, ...args.extra], runEnv);
|
|
3548
4564
|
}
|
|
3549
4565
|
async function runRun(argv, env = process.env) {
|
|
3550
4566
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
3551
|
-
console.log(
|
|
4567
|
+
console.log(USAGE8);
|
|
3552
4568
|
return EXIT_OK5;
|
|
3553
4569
|
}
|
|
3554
4570
|
let args;
|
|
@@ -3557,54 +4573,44 @@ async function runRun(argv, env = process.env) {
|
|
|
3557
4573
|
} catch (e) {
|
|
3558
4574
|
console.error(`piwi run: ${e.message}
|
|
3559
4575
|
`);
|
|
3560
|
-
console.error(
|
|
3561
|
-
return
|
|
4576
|
+
console.error(USAGE8);
|
|
4577
|
+
return EXIT_ERROR6;
|
|
3562
4578
|
}
|
|
3563
4579
|
if (args.format === "json") {
|
|
3564
4580
|
console.error("piwi run: --format json cannot be run; use args, grep or files");
|
|
3565
|
-
return
|
|
4581
|
+
return EXIT_ERROR6;
|
|
3566
4582
|
}
|
|
3567
4583
|
if (args.key === IMPACT_KEY) {
|
|
3568
4584
|
if (!args.base) {
|
|
3569
4585
|
console.error("piwi run: impact needs a base ref \u2014 pass --base <ref> (e.g. --base origin/main)");
|
|
3570
|
-
return
|
|
4586
|
+
return EXIT_ERROR6;
|
|
3571
4587
|
}
|
|
3572
4588
|
return runRunImpact(args, env);
|
|
3573
4589
|
}
|
|
3574
|
-
let projectId;
|
|
3575
|
-
try {
|
|
3576
|
-
projectId = await resolveProjectId(args);
|
|
3577
|
-
} catch (e) {
|
|
3578
|
-
if (args.strict) {
|
|
3579
|
-
console.error(`piwi run: ${e.message}`);
|
|
3580
|
-
return EXIT_ERROR5;
|
|
3581
|
-
}
|
|
3582
|
-
console.error(`piwi run: ${e.message} \u2014 running the full suite`);
|
|
3583
|
-
return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
|
|
3584
|
-
}
|
|
3585
4590
|
let outcome;
|
|
3586
4591
|
try {
|
|
3587
|
-
outcome = await resolveWithCache(args
|
|
4592
|
+
outcome = await resolveWithCache(args);
|
|
3588
4593
|
} catch (e) {
|
|
3589
4594
|
console.error(`piwi run: ${e.message}`);
|
|
3590
|
-
return
|
|
4595
|
+
return EXIT_ERROR6;
|
|
3591
4596
|
}
|
|
3592
|
-
if (
|
|
3593
|
-
console.error(
|
|
4597
|
+
if ("error" in outcome) {
|
|
4598
|
+
console.error(`piwi run: ${outcome.error.message} \u2014 no cached resolution, running the full suite`);
|
|
3594
4599
|
return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
|
|
3595
4600
|
}
|
|
3596
4601
|
const { resolution } = outcome;
|
|
3597
4602
|
printWarnings(resolution);
|
|
3598
4603
|
if (resolution.estimate.count === 0) {
|
|
3599
4604
|
console.error(`piwi run: "${args.key}" resolved to 0 tests`);
|
|
3600
|
-
return
|
|
4605
|
+
return EXIT_ERROR6;
|
|
3601
4606
|
}
|
|
3602
4607
|
const runEnv = {
|
|
3603
4608
|
...env,
|
|
3604
4609
|
PIWI_SELECTION: resolution.key ?? args.key,
|
|
3605
4610
|
PIWI_SELECTION_VERSION: String(resolution.version ?? 0),
|
|
3606
4611
|
PIWI_SELECTION_HASH: resolution.resolvedHash,
|
|
3607
|
-
PIWI_SELECTION_COUNT: String(resolution.estimate.count)
|
|
4612
|
+
PIWI_SELECTION_COUNT: String(resolution.estimate.count),
|
|
4613
|
+
...shardEnv(args)
|
|
3608
4614
|
};
|
|
3609
4615
|
console.error(
|
|
3610
4616
|
`piwi run: ${args.key} \u2192 ${resolution.estimate.count} tests${resolution.materialization.format !== args.format ? ` (materialized as ${resolution.materialization.format})` : ""}`
|
|
@@ -3613,16 +4619,16 @@ async function runRun(argv, env = process.env) {
|
|
|
3613
4619
|
}
|
|
3614
4620
|
|
|
3615
4621
|
// src/cli/preflight.ts
|
|
3616
|
-
var
|
|
3617
|
-
var
|
|
3618
|
-
var
|
|
4622
|
+
var fs15 = __toESM(require("fs"));
|
|
4623
|
+
var path17 = __toESM(require("path"));
|
|
4624
|
+
var import_node_child_process7 = require("child_process");
|
|
3619
4625
|
|
|
3620
4626
|
// ../core/src/diff-anchors.ts
|
|
3621
4627
|
var RENAME_FROM = `rename from `;
|
|
3622
4628
|
var RENAME_TO = "rename to ";
|
|
3623
4629
|
var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
3624
|
-
function stripPrefix(
|
|
3625
|
-
const unquoted =
|
|
4630
|
+
function stripPrefix(path19) {
|
|
4631
|
+
const unquoted = path19.startsWith('"') && path19.endsWith('"') ? path19.slice(1, -1) : path19;
|
|
3626
4632
|
return unquoted.replace(/^[ab]\//, "");
|
|
3627
4633
|
}
|
|
3628
4634
|
function parseUnifiedDiff(text) {
|
|
@@ -3690,10 +4696,7 @@ function parseUnifiedDiff(text) {
|
|
|
3690
4696
|
}
|
|
3691
4697
|
return files.filter((f) => f.path);
|
|
3692
4698
|
}
|
|
3693
|
-
var DEFAULT_TRANSLATION_GLOBS = [
|
|
3694
|
-
"**/{locales,locale,i18n,lang,translations}/**",
|
|
3695
|
-
"**/*.resx"
|
|
3696
|
-
];
|
|
4699
|
+
var DEFAULT_TRANSLATION_GLOBS = ["**/{locales,locale,i18n,lang,translations}/**", "**/*.resx"];
|
|
3697
4700
|
var ANCHOR_ATTRIBUTES = [
|
|
3698
4701
|
"id",
|
|
3699
4702
|
"name",
|
|
@@ -3735,8 +4738,8 @@ function pathGlobToRegExp(glob) {
|
|
|
3735
4738
|
});
|
|
3736
4739
|
return new RegExp(`^(?:${parts.join("|")})$`);
|
|
3737
4740
|
}
|
|
3738
|
-
function isTranslationFile(
|
|
3739
|
-
const posix =
|
|
4741
|
+
function isTranslationFile(path19, globs = DEFAULT_TRANSLATION_GLOBS) {
|
|
4742
|
+
const posix = path19.replace(/\\/g, "/");
|
|
3740
4743
|
if (!TRANSLATION_EXTENSIONS.test(posix)) return false;
|
|
3741
4744
|
return globs.some((g) => pathGlobToRegExp(g).test(posix));
|
|
3742
4745
|
}
|
|
@@ -3876,13 +4879,13 @@ function unquoteYaml(raw) {
|
|
|
3876
4879
|
function decodeXml(text) {
|
|
3877
4880
|
return text.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
3878
4881
|
}
|
|
3879
|
-
function parseTranslationFile(
|
|
4882
|
+
function parseTranslationFile(path19, content) {
|
|
3880
4883
|
const lines = content.split(/\r?\n/);
|
|
3881
4884
|
const out = [];
|
|
3882
|
-
const ext = /\.([a-z]+)$/i.exec(
|
|
4885
|
+
const ext = /\.([a-z]+)$/i.exec(path19)?.[1]?.toLowerCase();
|
|
3883
4886
|
if (ext === "json") {
|
|
3884
4887
|
const stack = [];
|
|
3885
|
-
const
|
|
4888
|
+
const path20 = (key) => [...stack.map((f) => f.key), key].join(".");
|
|
3886
4889
|
lines.forEach((text, i) => {
|
|
3887
4890
|
const t = text.trim();
|
|
3888
4891
|
const top = stack[stack.length - 1];
|
|
@@ -3891,14 +4894,14 @@ function parseTranslationFile(path16, content) {
|
|
|
3891
4894
|
const key = JSON.parse(`"${pair[1]}"`);
|
|
3892
4895
|
const rest = pair[2];
|
|
3893
4896
|
const str = /^"((?:[^"\\]|\\.)*)"/.exec(rest);
|
|
3894
|
-
if (str) out.push({ key:
|
|
4897
|
+
if (str) out.push({ key: path20(key), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
|
|
3895
4898
|
else if (/^[[{]\s*$/.test(rest)) stack.push({ key, array: rest.startsWith("["), next: 0 });
|
|
3896
4899
|
return;
|
|
3897
4900
|
}
|
|
3898
4901
|
if (top?.array) {
|
|
3899
4902
|
const str = /^"((?:[^"\\]|\\.)*)"\s*,?$/.exec(t);
|
|
3900
4903
|
if (str) {
|
|
3901
|
-
out.push({ key:
|
|
4904
|
+
out.push({ key: path20(top.next++), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
|
|
3902
4905
|
return;
|
|
3903
4906
|
}
|
|
3904
4907
|
if (/^[[{]\s*$/.test(t)) {
|
|
@@ -3962,8 +4965,8 @@ function parseTranslationFile(path16, content) {
|
|
|
3962
4965
|
}
|
|
3963
4966
|
return out;
|
|
3964
4967
|
}
|
|
3965
|
-
function translationLineToken(
|
|
3966
|
-
const ext = /\.([a-z]+)$/i.exec(
|
|
4968
|
+
function translationLineToken(path19, text, line) {
|
|
4969
|
+
const ext = /\.([a-z]+)$/i.exec(path19)?.[1]?.toLowerCase();
|
|
3967
4970
|
let key;
|
|
3968
4971
|
let value;
|
|
3969
4972
|
if (ext === "json") {
|
|
@@ -4000,9 +5003,9 @@ function translationLineToken(path16, text, line) {
|
|
|
4000
5003
|
if (value === void 0 || !keepable(value)) return null;
|
|
4001
5004
|
return { kind: "translation", group: `translation:${key ?? ""}`, value, key, line };
|
|
4002
5005
|
}
|
|
4003
|
-
function lineTokens(
|
|
5006
|
+
function lineTokens(path19, text, line, side, attributes, options) {
|
|
4004
5007
|
const spans = [];
|
|
4005
|
-
const markup = MARKUP_FILE.test(
|
|
5008
|
+
const markup = MARKUP_FILE.test(path19);
|
|
4006
5009
|
return [
|
|
4007
5010
|
...attributeTokens(text, attributes, line, spans),
|
|
4008
5011
|
...translationRefTokens(text, line, side, options.translations, spans),
|
|
@@ -4010,10 +5013,10 @@ function lineTokens(path16, text, line, side, attributes, options) {
|
|
|
4010
5013
|
...textTokens(text, line, markup)
|
|
4011
5014
|
];
|
|
4012
5015
|
}
|
|
4013
|
-
function keysByLine(
|
|
4014
|
-
const content = options.readFile?.(
|
|
5016
|
+
function keysByLine(path19, side, options) {
|
|
5017
|
+
const content = options.readFile?.(path19, side);
|
|
4015
5018
|
if (content === void 0) return null;
|
|
4016
|
-
return new Map(parseTranslationFile(
|
|
5019
|
+
return new Map(parseTranslationFile(path19, content).map((e) => [e.line, e.key]));
|
|
4017
5020
|
}
|
|
4018
5021
|
function tokenId(t) {
|
|
4019
5022
|
return `${t.group}\0${t.value}`;
|
|
@@ -4088,40 +5091,6 @@ function extractDiffAnchors(files, options = {}) {
|
|
|
4088
5091
|
return anchors;
|
|
4089
5092
|
}
|
|
4090
5093
|
|
|
4091
|
-
// ../core/src/dotenv.ts
|
|
4092
|
-
function parseDotEnv(text) {
|
|
4093
|
-
const out = {};
|
|
4094
|
-
for (const raw of text.split(/\r?\n/)) {
|
|
4095
|
-
const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(raw);
|
|
4096
|
-
if (!m) continue;
|
|
4097
|
-
let value = m[2];
|
|
4098
|
-
const quote4 = value[0];
|
|
4099
|
-
if ((quote4 === '"' || quote4 === "'") && value.length > 1 && value.endsWith(quote4)) {
|
|
4100
|
-
value = value.slice(1, -1);
|
|
4101
|
-
if (quote4 === '"') value = value.replace(/\\n/g, "\n").replace(/\\"/g, '"');
|
|
4102
|
-
} else {
|
|
4103
|
-
value = value.replace(/\s+#.*$/, "");
|
|
4104
|
-
}
|
|
4105
|
-
out[m[1]] = value;
|
|
4106
|
-
}
|
|
4107
|
-
return out;
|
|
4108
|
-
}
|
|
4109
|
-
function resolvePiwiConnection(sources) {
|
|
4110
|
-
const own = {
|
|
4111
|
-
serverUrl: sources.flags?.serverUrl || sources.env?.PIWI_DASHBOARD_URL,
|
|
4112
|
-
apiKey: sources.flags?.apiKey || sources.env?.PIWI_API_KEY
|
|
4113
|
-
};
|
|
4114
|
-
const workspace = { serverUrl: sources.dotEnv?.PIWI_DASHBOARD_URL, apiKey: sources.dotEnv?.PIWI_API_KEY };
|
|
4115
|
-
const project = sources.flags?.project || sources.env?.PIWI_PROJECT_NAME || sources.dotEnv?.PIWI_PROJECT_NAME || "";
|
|
4116
|
-
const desktop = sources.desktop ?? null;
|
|
4117
|
-
const trim = (url) => url.replace(/\/+$/, "");
|
|
4118
|
-
const layer = own.serverUrl ? own : workspace.serverUrl ? workspace : null;
|
|
4119
|
-
const serverUrl = layer ? trim(layer.serverUrl) : desktop ? trim(desktop.url) : "";
|
|
4120
|
-
if (!serverUrl) return null;
|
|
4121
|
-
const desktopKey = desktop && trim(desktop.url) === serverUrl ? desktop.token : null;
|
|
4122
|
-
return { serverUrl, apiKey: layer?.apiKey || desktopKey || null, project };
|
|
4123
|
-
}
|
|
4124
|
-
|
|
4125
5094
|
// ../core/src/locator-text-match.ts
|
|
4126
5095
|
function normalizeWhiteSpace(text) {
|
|
4127
5096
|
return text.replace(/[]/g, "").trim().replace(/\s+/g, " ");
|
|
@@ -4477,140 +5446,12 @@ function buildLiteralEdit(sourceLine, oldValue, newValue) {
|
|
|
4477
5446
|
return null;
|
|
4478
5447
|
}
|
|
4479
5448
|
|
|
4480
|
-
// src/internal/config/desktop.ts
|
|
4481
|
-
var fs11 = __toESM(require("fs"));
|
|
4482
|
-
var os = __toESM(require("os"));
|
|
4483
|
-
var path13 = __toESM(require("path"));
|
|
4484
|
-
function defaultDesktopConfigPath() {
|
|
4485
|
-
return path13.join(os.homedir(), ".piwi", "desktop.json");
|
|
4486
|
-
}
|
|
4487
|
-
function readDesktopConfig(filePath) {
|
|
4488
|
-
try {
|
|
4489
|
-
const parsed = JSON.parse(fs11.readFileSync(filePath, "utf8"));
|
|
4490
|
-
const url = parsed?.url;
|
|
4491
|
-
const token = parsed?.token;
|
|
4492
|
-
if (typeof url !== "string" || !url) return null;
|
|
4493
|
-
if (typeof token !== "string" || !token) return null;
|
|
4494
|
-
return { url, token };
|
|
4495
|
-
} catch {
|
|
4496
|
-
return null;
|
|
4497
|
-
}
|
|
4498
|
-
}
|
|
4499
|
-
|
|
4500
|
-
// src/internal/config/env.ts
|
|
4501
|
-
var DEFAULTS = {
|
|
4502
|
-
projectName: "default-project",
|
|
4503
|
-
uploadTraces: true,
|
|
4504
|
-
uploadReport: true,
|
|
4505
|
-
liveFileUploads: true,
|
|
4506
|
-
collectScmInfo: true,
|
|
4507
|
-
collectCiInfo: true,
|
|
4508
|
-
collectPerformanceMetrics: true,
|
|
4509
|
-
captureLocators: true,
|
|
4510
|
-
capturePageState: true,
|
|
4511
|
-
// Off by default: the page inventory reads control and link names from every
|
|
4512
|
-
// visited page, so it stays opt-in until a project turns it on.
|
|
4513
|
-
capturePageInventory: false,
|
|
4514
|
-
// Off by default: JavaScript coverage slows the page, so a scheduled job opts in.
|
|
4515
|
-
captureCodeReach: false,
|
|
4516
|
-
captureServerTraces: true,
|
|
4517
|
-
sampleAriaOnPass: true,
|
|
4518
|
-
uploadManifest: true,
|
|
4519
|
-
defaultCapture: true,
|
|
4520
|
-
streaming: true,
|
|
4521
|
-
streamingBatchSize: 5,
|
|
4522
|
-
streamingBatchDelay: 2e3,
|
|
4523
|
-
maxStreamBufferBytes: 100 * 1024 * 1024,
|
|
4524
|
-
failOnFlakyTests: false,
|
|
4525
|
-
username: null,
|
|
4526
|
-
password: null,
|
|
4527
|
-
apiKey: null,
|
|
4528
|
-
verbose: false
|
|
4529
|
-
};
|
|
4530
|
-
var PIWI_ENV_KEYS = {
|
|
4531
|
-
serverUrl: "PIWI_DASHBOARD_URL",
|
|
4532
|
-
projectName: "PIWI_PROJECT_NAME",
|
|
4533
|
-
verbose: "PIWI_VERBOSE",
|
|
4534
|
-
apiKey: "PIWI_API_KEY",
|
|
4535
|
-
username: "PIWI_USERNAME",
|
|
4536
|
-
password: "PIWI_PASSWORD",
|
|
4537
|
-
environment: "PIWI_ENVIRONMENT",
|
|
4538
|
-
label: "PIWI_LABEL",
|
|
4539
|
-
runLabel: "PIWI_RUN_LABEL",
|
|
4540
|
-
keep: "PIWI_KEEP",
|
|
4541
|
-
streaming: "PIWI_STREAMING",
|
|
4542
|
-
streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
|
|
4543
|
-
streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
|
|
4544
|
-
maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
|
|
4545
|
-
liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
|
|
4546
|
-
failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
|
|
4547
|
-
uploadTraces: "PIWI_UPLOAD_TRACES",
|
|
4548
|
-
uploadReport: "PIWI_UPLOAD_REPORT",
|
|
4549
|
-
captureLocators: "PIWI_CAPTURE_LOCATORS",
|
|
4550
|
-
capturePageState: "PIWI_CAPTURE_PAGE_STATE",
|
|
4551
|
-
capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
|
|
4552
|
-
captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
|
|
4553
|
-
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
4554
|
-
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
4555
|
-
sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
|
|
4556
|
-
uploadManifest: "PIWI_UPLOAD_MANIFEST",
|
|
4557
|
-
defaultCapture: "PIWI_DEFAULT_CAPTURE",
|
|
4558
|
-
inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
|
|
4559
|
-
pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
|
|
4560
|
-
outputFile: "PIWI_OUTPUT_FILE",
|
|
4561
|
-
aiMode: "PIWI_AI",
|
|
4562
|
-
aiDir: "PIWI_AI_DIR",
|
|
4563
|
-
aiOnMiss: "PIWI_AI_ON_MISS",
|
|
4564
|
-
aiMaxSteps: "PIWI_AI_MAX_FLOW_STEPS",
|
|
4565
|
-
aiMaxSnapshotChars: "PIWI_AI_MAX_SNAPSHOT_CHARS",
|
|
4566
|
-
aiOptionalProbeTimeout: "PIWI_AI_OPTIONAL_PROBE_TIMEOUT",
|
|
4567
|
-
aiResponseWaitTimeout: "PIWI_AI_RESPONSE_WAIT_TIMEOUT",
|
|
4568
|
-
aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
|
|
4569
|
-
};
|
|
4570
|
-
var PIWI_DESKTOP_CONFIG_ENV = "PIWI_DESKTOP_CONFIG";
|
|
4571
|
-
var PIWI_PROBE_ENV = {
|
|
4572
|
-
flag: "PIWI_PROBE",
|
|
4573
|
-
plan: "PIWI_PROBE_PLAN",
|
|
4574
|
-
results: "PIWI_PROBE_RESULTS",
|
|
4575
|
-
secret: "PIWI_PROBE_SECRET"
|
|
4576
|
-
};
|
|
4577
|
-
var ENV_FALLBACK_SPECS = [
|
|
4578
|
-
{ option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
|
|
4579
|
-
{ option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
|
|
4580
|
-
{ option: "apiKey", env: PIWI_ENV_KEYS.apiKey, kind: "string" },
|
|
4581
|
-
{ option: "username", env: PIWI_ENV_KEYS.username, kind: "string" },
|
|
4582
|
-
{ option: "password", env: PIWI_ENV_KEYS.password, kind: "string" },
|
|
4583
|
-
{ option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
|
|
4584
|
-
{ option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
|
|
4585
|
-
{ option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
|
|
4586
|
-
{ option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
|
|
4587
|
-
{ option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
|
|
4588
|
-
{ option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
|
|
4589
|
-
{ option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
|
|
4590
|
-
{ option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
|
|
4591
|
-
{ option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
|
|
4592
|
-
{ option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
|
|
4593
|
-
{ option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
|
|
4594
|
-
{ option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
|
|
4595
|
-
{ option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
|
|
4596
|
-
{ option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
|
|
4597
|
-
{ option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
|
|
4598
|
-
{ option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
|
|
4599
|
-
{ option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
|
|
4600
|
-
{ option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
|
|
4601
|
-
{ option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
|
|
4602
|
-
{ option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
|
|
4603
|
-
{ option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
|
|
4604
|
-
{ option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
|
|
4605
|
-
{ option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
|
|
4606
|
-
];
|
|
4607
|
-
|
|
4608
5449
|
// src/cli/preflight.ts
|
|
4609
5450
|
var EXIT_OK6 = 0;
|
|
4610
5451
|
var EXIT_BREAKS = 1;
|
|
4611
|
-
var
|
|
5452
|
+
var EXIT_ERROR7 = 2;
|
|
4612
5453
|
var MAX_LISTED = 20;
|
|
4613
|
-
var
|
|
5454
|
+
var USAGE9 = `
|
|
4614
5455
|
piwi preflight \u2014 the test locators a change breaks, before it runs
|
|
4615
5456
|
|
|
4616
5457
|
Usage:
|
|
@@ -4689,16 +5530,16 @@ function parsePreflightArgs(argv) {
|
|
|
4689
5530
|
}
|
|
4690
5531
|
var CONFIG_NAMES2 = ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs", "playwright.config.cjs"];
|
|
4691
5532
|
function findPlaywrightRoot(from) {
|
|
4692
|
-
let dir =
|
|
5533
|
+
let dir = path17.resolve(from);
|
|
4693
5534
|
for (; ; ) {
|
|
4694
|
-
if (CONFIG_NAMES2.some((name) =>
|
|
4695
|
-
const parent =
|
|
5535
|
+
if (CONFIG_NAMES2.some((name) => fs15.existsSync(path17.join(dir, name)))) return dir;
|
|
5536
|
+
const parent = path17.dirname(dir);
|
|
4696
5537
|
if (parent === dir) return null;
|
|
4697
5538
|
dir = parent;
|
|
4698
5539
|
}
|
|
4699
5540
|
}
|
|
4700
5541
|
function git(cwd, args) {
|
|
4701
|
-
return (0,
|
|
5542
|
+
return (0, import_node_child_process7.execFileSync)("git", args, {
|
|
4702
5543
|
cwd,
|
|
4703
5544
|
encoding: "utf-8",
|
|
4704
5545
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -4714,19 +5555,7 @@ function tryGit(cwd, args) {
|
|
|
4714
5555
|
}
|
|
4715
5556
|
var TEST_FILE = /(?:^|\/)[^/]+\.(?:spec|test)\.[cm]?[jt]sx?$/;
|
|
4716
5557
|
function resolveConnection(args, env, dirs) {
|
|
4717
|
-
|
|
4718
|
-
for (const dir of dirs) {
|
|
4719
|
-
try {
|
|
4720
|
-
dotEnv = { ...parseDotEnv(fs12.readFileSync(path14.join(dir, ".env"), "utf-8")), ...dotEnv };
|
|
4721
|
-
} catch {
|
|
4722
|
-
}
|
|
4723
|
-
}
|
|
4724
|
-
return resolvePiwiConnection({
|
|
4725
|
-
flags: { serverUrl: args.serverUrl, apiKey: args.apiKey, project: args.project },
|
|
4726
|
-
env,
|
|
4727
|
-
dotEnv,
|
|
4728
|
-
desktop: readDesktopConfig(env[PIWI_DESKTOP_CONFIG_ENV] || defaultDesktopConfigPath())
|
|
4729
|
-
});
|
|
5558
|
+
return resolveCliConnection({ serverUrl: args.serverUrl, apiKey: args.apiKey, project: args.project }, env, dirs);
|
|
4730
5559
|
}
|
|
4731
5560
|
function translationLookup(repoRoot, base, locale) {
|
|
4732
5561
|
const files = (tryGit(repoRoot, ["ls-files"]) ?? "").split("\n").filter((f) => f && isTranslationFile(f)).sort((a, b) => Number(prefersLocale(b, locale)) - Number(prefersLocale(a, locale)) || a.localeCompare(b));
|
|
@@ -4734,7 +5563,7 @@ function translationLookup(repoRoot, base, locale) {
|
|
|
4734
5563
|
const build = (side) => {
|
|
4735
5564
|
const map = /* @__PURE__ */ new Map();
|
|
4736
5565
|
for (const file of files) {
|
|
4737
|
-
const content = side === "new" ? readText(
|
|
5566
|
+
const content = side === "new" ? readText(path17.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${base}:${file}`]);
|
|
4738
5567
|
if (content === void 0) continue;
|
|
4739
5568
|
for (const entry of parseTranslationFile(file, content)) {
|
|
4740
5569
|
if (!map.has(entry.key)) map.set(entry.key, entry.value);
|
|
@@ -4753,34 +5582,34 @@ function prefersLocale(file, locale) {
|
|
|
4753
5582
|
}
|
|
4754
5583
|
function readText(file) {
|
|
4755
5584
|
try {
|
|
4756
|
-
return
|
|
5585
|
+
return fs15.readFileSync(file, "utf-8");
|
|
4757
5586
|
} catch {
|
|
4758
5587
|
return void 0;
|
|
4759
5588
|
}
|
|
4760
5589
|
}
|
|
4761
|
-
var CACHE_FILE2 =
|
|
5590
|
+
var CACHE_FILE2 = path17.join(".piwi", "locator-index.json");
|
|
4762
5591
|
function cacheKey2(connection, branch) {
|
|
4763
5592
|
return `${connection.serverUrl}|${connection.project}|${branch ?? ""}`;
|
|
4764
5593
|
}
|
|
4765
5594
|
function readCache2(root, key) {
|
|
4766
5595
|
try {
|
|
4767
|
-
const store = JSON.parse(
|
|
5596
|
+
const store = JSON.parse(fs15.readFileSync(path17.join(root, CACHE_FILE2), "utf-8"));
|
|
4768
5597
|
return store[key] ?? null;
|
|
4769
5598
|
} catch {
|
|
4770
5599
|
return null;
|
|
4771
5600
|
}
|
|
4772
5601
|
}
|
|
4773
5602
|
function writeCache2(root, key, index) {
|
|
4774
|
-
const file =
|
|
5603
|
+
const file = path17.join(root, CACHE_FILE2);
|
|
4775
5604
|
try {
|
|
4776
5605
|
let store = {};
|
|
4777
5606
|
try {
|
|
4778
|
-
store = JSON.parse(
|
|
5607
|
+
store = JSON.parse(fs15.readFileSync(file, "utf-8"));
|
|
4779
5608
|
} catch {
|
|
4780
5609
|
}
|
|
4781
5610
|
store[key] = { savedAt: (/* @__PURE__ */ new Date()).toISOString(), index };
|
|
4782
|
-
|
|
4783
|
-
|
|
5611
|
+
fs15.mkdirSync(path17.dirname(file), { recursive: true });
|
|
5612
|
+
fs15.writeFileSync(file, JSON.stringify(store));
|
|
4784
5613
|
} catch {
|
|
4785
5614
|
}
|
|
4786
5615
|
}
|
|
@@ -4796,7 +5625,7 @@ function planSites(b, testRoot) {
|
|
|
4796
5625
|
const sites = [...new Set(b.uses.flatMap((u) => u.callSites))];
|
|
4797
5626
|
return sites.map((callSite) => {
|
|
4798
5627
|
if (!b.rewrite || !b.replacements?.length) return { callSite, action: "none" };
|
|
4799
|
-
const file =
|
|
5628
|
+
const file = path17.resolve(testRoot, callSiteFile(callSite));
|
|
4800
5629
|
const lineNo = callSiteLine(callSite);
|
|
4801
5630
|
const text = readText(file);
|
|
4802
5631
|
if (text === void 0 || lineNo === null) return { callSite, action: "elsewhere" };
|
|
@@ -4839,7 +5668,7 @@ function applyEdits(breaks, replacementsOf) {
|
|
|
4839
5668
|
}
|
|
4840
5669
|
const edited = [];
|
|
4841
5670
|
for (const [file, edits] of byFile) {
|
|
4842
|
-
const text =
|
|
5671
|
+
const text = fs15.readFileSync(file, "utf-8");
|
|
4843
5672
|
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
4844
5673
|
const lines = text.split(/\r?\n/);
|
|
4845
5674
|
for (const e of edits) {
|
|
@@ -4849,7 +5678,7 @@ function applyEdits(breaks, replacementsOf) {
|
|
|
4849
5678
|
}
|
|
4850
5679
|
const next = lines.join(eol);
|
|
4851
5680
|
if (next !== text) {
|
|
4852
|
-
|
|
5681
|
+
fs15.writeFileSync(file, next);
|
|
4853
5682
|
edited.push(file);
|
|
4854
5683
|
}
|
|
4855
5684
|
}
|
|
@@ -4863,7 +5692,7 @@ function describeAnchor(a) {
|
|
|
4863
5692
|
const extra = a.key ? ` (key ${a.key})` : a.attribute ? ` (${a.attribute})` : "";
|
|
4864
5693
|
return `${what}${extra}`;
|
|
4865
5694
|
}
|
|
4866
|
-
function
|
|
5695
|
+
function pad2(text, width) {
|
|
4867
5696
|
return text.length >= width ? `${text} ` : text + " ".repeat(width - text.length);
|
|
4868
5697
|
}
|
|
4869
5698
|
function renderPreflight(result, cwdToRoot = (f) => f) {
|
|
@@ -4884,14 +5713,14 @@ function renderPreflight(result, cwdToRoot = (f) => f) {
|
|
|
4884
5713
|
let shown = 0;
|
|
4885
5714
|
for (const [anchor, group] of byAnchor) {
|
|
4886
5715
|
if (shown >= budget) break;
|
|
4887
|
-
out.push(` ${
|
|
5716
|
+
out.push(` ${pad2(`${anchor.file}:${anchor.line}`, 38)} ${describeAnchor(anchor)}`);
|
|
4888
5717
|
for (const b of group) {
|
|
4889
5718
|
if (shown >= budget) break;
|
|
4890
5719
|
shown++;
|
|
4891
5720
|
const meta = `${b.actions.slice(0, 3).join(", ") || "used"} \xB7 ${plural(b.tests.length, "test")} \xB7 ${b.confidence}`;
|
|
4892
|
-
out.push(` ${
|
|
5721
|
+
out.push(` ${pad2(b.locator, 48)} ${meta}`);
|
|
4893
5722
|
for (const site of b.sites.slice(0, 5)) {
|
|
4894
|
-
const where =
|
|
5723
|
+
const where = pad2(cwdToRoot(site.callSite.replace(/:\d+$/, "")), 34);
|
|
4895
5724
|
if (site.action === "edit") out.push(` ${where} \u2192 ${b.rewrite}`);
|
|
4896
5725
|
else if (site.action === "applied") out.push(` ${where} already rewritten`);
|
|
4897
5726
|
else if (site.action === "none") {
|
|
@@ -4920,7 +5749,7 @@ function renderPreflight(result, cwdToRoot = (f) => f) {
|
|
|
4920
5749
|
out.push(`${plural(new Set(possible.map((b) => b.locator)).size, "locator")} it may break (a bare string matched)`);
|
|
4921
5750
|
for (const b of possible.slice(0, Math.max(0, MAX_LISTED - likely.length))) {
|
|
4922
5751
|
out.push(
|
|
4923
|
-
` ${
|
|
5752
|
+
` ${pad2(b.locator, 48)} ${plural(b.tests.length, "test")} \xB7 ${b.anchor.file}:${b.anchor.line} ${describeAnchor(b.anchor)}`
|
|
4924
5753
|
);
|
|
4925
5754
|
if (b.rewrite) out.push(` if it does, edit by hand: ${b.rewrite}`);
|
|
4926
5755
|
}
|
|
@@ -4943,7 +5772,7 @@ function renderPreflight(result, cwdToRoot = (f) => f) {
|
|
|
4943
5772
|
}
|
|
4944
5773
|
async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
4945
5774
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
4946
|
-
console.log(
|
|
5775
|
+
console.log(USAGE9);
|
|
4947
5776
|
return EXIT_OK6;
|
|
4948
5777
|
}
|
|
4949
5778
|
let args;
|
|
@@ -4952,26 +5781,26 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
4952
5781
|
} catch (e) {
|
|
4953
5782
|
console.error(`piwi preflight: ${e.message}
|
|
4954
5783
|
`);
|
|
4955
|
-
console.error(
|
|
4956
|
-
return
|
|
5784
|
+
console.error(USAGE9);
|
|
5785
|
+
return EXIT_ERROR7;
|
|
4957
5786
|
}
|
|
4958
5787
|
const repoRoot = tryGit(cwd, ["rev-parse", "--show-toplevel"])?.trim();
|
|
4959
5788
|
if (!repoRoot) {
|
|
4960
5789
|
console.error("piwi preflight: not inside a git repository");
|
|
4961
|
-
return
|
|
5790
|
+
return EXIT_ERROR7;
|
|
4962
5791
|
}
|
|
4963
|
-
const testRoot =
|
|
5792
|
+
const testRoot = path17.resolve(cwd, args.testRoot ?? findPlaywrightRoot(cwd) ?? cwd);
|
|
4964
5793
|
let files;
|
|
4965
5794
|
try {
|
|
4966
5795
|
files = parseUnifiedDiff(git(repoRoot, ["diff", "--unified=0", "--no-color", "--no-ext-diff", args.base, "--"]));
|
|
4967
5796
|
} catch (e) {
|
|
4968
5797
|
console.error(`piwi preflight: could not diff against ${args.base} \u2014 ${e.message.split("\n")[0]}`);
|
|
4969
|
-
return
|
|
5798
|
+
return EXIT_ERROR7;
|
|
4970
5799
|
}
|
|
4971
5800
|
const connection = resolveConnection(args, env, [.../* @__PURE__ */ new Set([testRoot, cwd, repoRoot])]);
|
|
4972
5801
|
if (!connection) {
|
|
4973
5802
|
console.error("piwi preflight: no dashboard URL \u2014 pass --server-url, set PIWI_DASHBOARD_URL, or run piwi init");
|
|
4974
|
-
return
|
|
5803
|
+
return EXIT_ERROR7;
|
|
4975
5804
|
}
|
|
4976
5805
|
const key = cacheKey2(connection, args.branch);
|
|
4977
5806
|
let index;
|
|
@@ -4985,7 +5814,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
4985
5814
|
const cached = readCache2(testRoot, key);
|
|
4986
5815
|
if (!cached) {
|
|
4987
5816
|
console.error(`piwi preflight: ${e.message}, and there is no cached index`);
|
|
4988
|
-
return
|
|
5817
|
+
return EXIT_ERROR7;
|
|
4989
5818
|
}
|
|
4990
5819
|
console.error(
|
|
4991
5820
|
`piwi preflight: dashboard unreachable, using the index cached ${cached.savedAt} \u2014 ${e.message}`
|
|
@@ -4997,7 +5826,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
4997
5826
|
testIdAttributes: index.testIdAttributes ?? void 0,
|
|
4998
5827
|
isTestFile: (file) => TEST_FILE.test(file),
|
|
4999
5828
|
translations: translationLookup(repoRoot, args.base, args.locale),
|
|
5000
|
-
readFile: (file, side) => side === "new" ? readText(
|
|
5829
|
+
readFile: (file, side) => side === "new" ? readText(path17.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${args.base}:${file}`])
|
|
5001
5830
|
});
|
|
5002
5831
|
const found = predictLocatorBreaks(anchors, index, { reach: await loadReach(connection, projectId, args.branch) });
|
|
5003
5832
|
const replacementsOf = /* @__PURE__ */ new Map();
|
|
@@ -5042,8 +5871,8 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
5042
5871
|
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
5043
5872
|
else {
|
|
5044
5873
|
const rel = (file) => {
|
|
5045
|
-
const abs =
|
|
5046
|
-
const r =
|
|
5874
|
+
const abs = path17.isAbsolute(file) ? file : path17.resolve(testRoot, file);
|
|
5875
|
+
const r = path17.relative(cwd, abs).split(path17.sep).join("/");
|
|
5047
5876
|
return r.startsWith("..") ? file : r || file;
|
|
5048
5877
|
};
|
|
5049
5878
|
console.log(renderPreflight(result, rel));
|
|
@@ -5070,14 +5899,12 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
5070
5899
|
}
|
|
5071
5900
|
|
|
5072
5901
|
// src/cli/probe.ts
|
|
5073
|
-
var
|
|
5074
|
-
var
|
|
5075
|
-
var
|
|
5076
|
-
var os2 = __toESM(require("os"));
|
|
5077
|
-
var path15 = __toESM(require("path"));
|
|
5902
|
+
var fs16 = __toESM(require("fs"));
|
|
5903
|
+
var os3 = __toESM(require("os"));
|
|
5904
|
+
var path18 = __toESM(require("path"));
|
|
5078
5905
|
var EXIT_OK7 = 0;
|
|
5079
|
-
var
|
|
5080
|
-
var
|
|
5906
|
+
var EXIT_ERROR8 = 2;
|
|
5907
|
+
var USAGE10 = `
|
|
5081
5908
|
piwi probe \u2014 run the dashboard's probe plan and record what the suite noticed
|
|
5082
5909
|
|
|
5083
5910
|
Usage:
|
|
@@ -5095,46 +5922,22 @@ function readOption9(argv, name) {
|
|
|
5095
5922
|
const i = argv.indexOf(name);
|
|
5096
5923
|
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : void 0;
|
|
5097
5924
|
}
|
|
5098
|
-
function
|
|
5925
|
+
function escapeRegex2(s) {
|
|
5099
5926
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5100
5927
|
}
|
|
5101
|
-
function resolvePlaywrightCli3() {
|
|
5102
|
-
const require2 = (0, import_node_module4.createRequire)(path15.join(process.cwd(), "noop.js"));
|
|
5103
|
-
for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
|
|
5104
|
-
try {
|
|
5105
|
-
return require2.resolve(id);
|
|
5106
|
-
} catch {
|
|
5107
|
-
}
|
|
5108
|
-
}
|
|
5109
|
-
return null;
|
|
5110
|
-
}
|
|
5111
|
-
function spawnPlaywright2(playwrightArgs, env) {
|
|
5112
|
-
const cli = resolvePlaywrightCli3();
|
|
5113
|
-
const child = cli ? (0, import_node_child_process6.spawn)(process.execPath, [cli, "test", ...playwrightArgs], { stdio: "inherit", env }) : (0, import_node_child_process6.spawn)(process.platform === "win32" ? "npx.cmd" : "npx", ["playwright", "test", ...playwrightArgs], {
|
|
5114
|
-
stdio: "inherit",
|
|
5115
|
-
env
|
|
5116
|
-
});
|
|
5117
|
-
return new Promise((resolve8) => {
|
|
5118
|
-
child.on("error", (err) => {
|
|
5119
|
-
console.error(`piwi probe: could not start Playwright \u2014 ${err.message}`);
|
|
5120
|
-
resolve8(EXIT_ERROR7);
|
|
5121
|
-
});
|
|
5122
|
-
child.on("exit", (code) => resolve8(code ?? EXIT_ERROR7));
|
|
5123
|
-
});
|
|
5124
|
-
}
|
|
5125
5928
|
async function runProbe(argv) {
|
|
5126
5929
|
const sep5 = argv.indexOf("--");
|
|
5127
5930
|
const own = sep5 < 0 ? argv : argv.slice(0, sep5);
|
|
5128
|
-
const
|
|
5931
|
+
const playwrightArgs2 = sep5 < 0 ? [] : argv.slice(sep5 + 1);
|
|
5129
5932
|
if (own.includes("-h") || own.includes("--help")) {
|
|
5130
|
-
console.log(
|
|
5933
|
+
console.log(USAGE10);
|
|
5131
5934
|
return EXIT_OK7;
|
|
5132
5935
|
}
|
|
5133
5936
|
const env = process.env;
|
|
5134
5937
|
const serverUrl = (readOption9(own, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
|
|
5135
5938
|
if (!serverUrl) {
|
|
5136
5939
|
console.error("piwi probe: no dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
|
|
5137
|
-
return
|
|
5940
|
+
return EXIT_ERROR8;
|
|
5138
5941
|
}
|
|
5139
5942
|
const apiKey = readOption9(own, "--api-key") ?? env.PIWI_API_KEY ?? null;
|
|
5140
5943
|
const project = readOption9(own, "--project") ?? env.PIWI_PROJECT_NAME ?? "";
|
|
@@ -5144,7 +5947,7 @@ async function runProbe(argv) {
|
|
|
5144
5947
|
projectId = await resolveProjectId({ serverUrl, apiKey, project, key: "" });
|
|
5145
5948
|
} catch (e) {
|
|
5146
5949
|
console.error(`piwi probe: ${e.message}`);
|
|
5147
|
-
return
|
|
5950
|
+
return EXIT_ERROR8;
|
|
5148
5951
|
}
|
|
5149
5952
|
const authHeaders3 = apiKey ? { "X-API-Key": apiKey } : {};
|
|
5150
5953
|
const planUrl = `${serverUrl}/api/projects/${projectId}/probes/plan${budget ? `?budget=${encodeURIComponent(budget)}` : ""}`;
|
|
@@ -5155,22 +5958,22 @@ async function runProbe(argv) {
|
|
|
5155
5958
|
plan = await res.json();
|
|
5156
5959
|
} catch (e) {
|
|
5157
5960
|
console.error(`piwi probe: could not fetch the probe plan \u2014 ${e.message}`);
|
|
5158
|
-
return
|
|
5961
|
+
return EXIT_ERROR8;
|
|
5159
5962
|
}
|
|
5160
5963
|
if (!plan.items || plan.items.length === 0) {
|
|
5161
5964
|
console.error("piwi probe: nothing to probe \u2014 the plan is empty.");
|
|
5162
5965
|
return EXIT_OK7;
|
|
5163
5966
|
}
|
|
5164
5967
|
console.error(`piwi probe: ${plan.items.length} pair(s) to probe (budget ${plan.budget}).`);
|
|
5165
|
-
const dir =
|
|
5166
|
-
const planFile =
|
|
5167
|
-
const resultsFile =
|
|
5168
|
-
const runOutputFile =
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
const grep = plan.items.map((i) => i.testTitle).filter(Boolean).map(
|
|
5172
|
-
const pwArgs = [...
|
|
5173
|
-
if (grep && !
|
|
5968
|
+
const dir = fs16.mkdtempSync(path18.join(os3.tmpdir(), "piwi-probe-"));
|
|
5969
|
+
const planFile = path18.join(dir, "plan.json");
|
|
5970
|
+
const resultsFile = path18.join(dir, "results.jsonl");
|
|
5971
|
+
const runOutputFile = path18.join(dir, "run.json");
|
|
5972
|
+
fs16.writeFileSync(planFile, JSON.stringify(plan));
|
|
5973
|
+
fs16.writeFileSync(resultsFile, "");
|
|
5974
|
+
const grep = plan.items.map((i) => i.testTitle).filter(Boolean).map(escapeRegex2).join("|");
|
|
5975
|
+
const pwArgs = [...playwrightArgs2];
|
|
5976
|
+
if (grep && !playwrightArgs2.includes("--grep") && !playwrightArgs2.includes("-g")) pwArgs.push("--grep", grep);
|
|
5174
5977
|
const childEnv = {
|
|
5175
5978
|
...env,
|
|
5176
5979
|
[PIWI_PROBE_ENV.flag]: "1",
|
|
@@ -5180,8 +5983,8 @@ async function runProbe(argv) {
|
|
|
5180
5983
|
// it write the run output here so the results POST can name the run.
|
|
5181
5984
|
[PIWI_ENV_KEYS.outputFile]: runOutputFile
|
|
5182
5985
|
};
|
|
5183
|
-
await
|
|
5184
|
-
const lines =
|
|
5986
|
+
await spawnPlaywright(pwArgs, childEnv, { command: "piwi probe" });
|
|
5987
|
+
const lines = fs16.readFileSync(resultsFile, "utf8").split("\n").filter(Boolean);
|
|
5185
5988
|
const results = [];
|
|
5186
5989
|
for (const line of lines) {
|
|
5187
5990
|
try {
|
|
@@ -5195,7 +5998,7 @@ async function runProbe(argv) {
|
|
|
5195
5998
|
}
|
|
5196
5999
|
let runId = null;
|
|
5197
6000
|
try {
|
|
5198
|
-
const out = JSON.parse(
|
|
6001
|
+
const out = JSON.parse(fs16.readFileSync(runOutputFile, "utf8"));
|
|
5199
6002
|
if (typeof out.runId === "number") runId = out.runId;
|
|
5200
6003
|
} catch {
|
|
5201
6004
|
}
|
|
@@ -5214,19 +6017,19 @@ async function runProbe(argv) {
|
|
|
5214
6017
|
);
|
|
5215
6018
|
} catch (e) {
|
|
5216
6019
|
console.error(`piwi probe: could not post probe results \u2014 ${e.message}`);
|
|
5217
|
-
return
|
|
6020
|
+
return EXIT_ERROR8;
|
|
5218
6021
|
}
|
|
5219
6022
|
return EXIT_OK7;
|
|
5220
6023
|
}
|
|
5221
6024
|
|
|
5222
6025
|
// src/cli/quality-report.ts
|
|
5223
|
-
var
|
|
6026
|
+
var fs17 = __toESM(require("fs"));
|
|
5224
6027
|
var EXIT_OK8 = 0;
|
|
5225
6028
|
var EXIT_VERDICT = 1;
|
|
5226
|
-
var
|
|
6029
|
+
var EXIT_ERROR9 = 2;
|
|
5227
6030
|
var FORMATS = ["md", "json", "html", "pdf", "csv"];
|
|
5228
6031
|
var DASHBOARDS = ["executive", "engineering", "overview"];
|
|
5229
|
-
var
|
|
6032
|
+
var USAGE11 = `
|
|
5230
6033
|
piwi report \u2014 print a quality report from the dashboard
|
|
5231
6034
|
|
|
5232
6035
|
Usage:
|
|
@@ -5329,7 +6132,7 @@ async function fetchReport(args, query) {
|
|
|
5329
6132
|
}
|
|
5330
6133
|
async function runQualityReport(argv) {
|
|
5331
6134
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
5332
|
-
console.log(
|
|
6135
|
+
console.log(USAGE11);
|
|
5333
6136
|
return EXIT_OK8;
|
|
5334
6137
|
}
|
|
5335
6138
|
try {
|
|
@@ -5340,7 +6143,7 @@ async function runQualityReport(argv) {
|
|
|
5340
6143
|
}
|
|
5341
6144
|
const res = await fetchReport(args, reportQuery(args, projectIds, args.format));
|
|
5342
6145
|
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
5343
|
-
if (args.output)
|
|
6146
|
+
if (args.output) fs17.writeFileSync(args.output, bytes);
|
|
5344
6147
|
else process.stdout.write(bytes);
|
|
5345
6148
|
if (!args.failOn) return EXIT_OK8;
|
|
5346
6149
|
const bundle = args.format === "json" ? JSON.parse(new TextDecoder().decode(bytes)) : await (await fetchReport(args, reportQuery(args, projectIds, "json"))).json();
|
|
@@ -5351,12 +6154,12 @@ async function runQualityReport(argv) {
|
|
|
5351
6154
|
return EXIT_OK8;
|
|
5352
6155
|
} catch (e) {
|
|
5353
6156
|
console.error(`piwi report: ${e.message}`);
|
|
5354
|
-
return
|
|
6157
|
+
return EXIT_ERROR9;
|
|
5355
6158
|
}
|
|
5356
6159
|
}
|
|
5357
6160
|
|
|
5358
6161
|
// src/cli/index.ts
|
|
5359
|
-
var
|
|
6162
|
+
var USAGE12 = `
|
|
5360
6163
|
piwi \u2014 companion commands for the Piwi Dashboard reporter
|
|
5361
6164
|
|
|
5362
6165
|
Usage:
|
|
@@ -5370,6 +6173,7 @@ Commands:
|
|
|
5370
6173
|
select Print the Playwright args for a saved test selection
|
|
5371
6174
|
run Run a saved test selection with playwright test
|
|
5372
6175
|
probe Run the dashboard's probe plan and record what the suite noticed
|
|
6176
|
+
flake Make a flaky test fail on demand under its suspects' conditions, then verify the fix
|
|
5373
6177
|
ai Manage committed natural-language AI-step artifacts
|
|
5374
6178
|
codegen Turn a steps file (a Piwi Picker recording) into a Playwright spec
|
|
5375
6179
|
preflight List the test locators your uncommitted change breaks, and fix them
|
|
@@ -5396,6 +6200,8 @@ async function main() {
|
|
|
5396
6200
|
return runRun(rest);
|
|
5397
6201
|
case "probe":
|
|
5398
6202
|
return runProbe(rest);
|
|
6203
|
+
case "flake":
|
|
6204
|
+
return runFlake(rest);
|
|
5399
6205
|
case "ai":
|
|
5400
6206
|
return runAi(rest);
|
|
5401
6207
|
case "codegen":
|
|
@@ -5407,12 +6213,12 @@ async function main() {
|
|
|
5407
6213
|
case void 0:
|
|
5408
6214
|
case "-h":
|
|
5409
6215
|
case "--help":
|
|
5410
|
-
console.log(
|
|
6216
|
+
console.log(USAGE12);
|
|
5411
6217
|
return 0;
|
|
5412
6218
|
default:
|
|
5413
6219
|
console.error(`piwi: unknown command "${command}"
|
|
5414
6220
|
`);
|
|
5415
|
-
console.error(
|
|
6221
|
+
console.error(USAGE12);
|
|
5416
6222
|
return 2;
|
|
5417
6223
|
}
|
|
5418
6224
|
}
|