@piwitests/reporter 0.41.0 → 0.42.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 +1271 -465
- package/dist/global-setup-module.js +18 -5
- package/dist/index.js +152 -35
- package/dist/internal/capture/capture-fixtures.js +5 -1
- 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,1016 @@ ${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
|
+
failOnFlakyTests: false,
|
|
2465
|
+
username: null,
|
|
2466
|
+
password: null,
|
|
2467
|
+
apiKey: null,
|
|
2468
|
+
verbose: false
|
|
2469
|
+
};
|
|
2470
|
+
var PIWI_ENV_KEYS = {
|
|
2471
|
+
serverUrl: "PIWI_DASHBOARD_URL",
|
|
2472
|
+
projectName: "PIWI_PROJECT_NAME",
|
|
2473
|
+
verbose: "PIWI_VERBOSE",
|
|
2474
|
+
apiKey: "PIWI_API_KEY",
|
|
2475
|
+
username: "PIWI_USERNAME",
|
|
2476
|
+
password: "PIWI_PASSWORD",
|
|
2477
|
+
environment: "PIWI_ENVIRONMENT",
|
|
2478
|
+
label: "PIWI_LABEL",
|
|
2479
|
+
runLabel: "PIWI_RUN_LABEL",
|
|
2480
|
+
keep: "PIWI_KEEP",
|
|
2481
|
+
streaming: "PIWI_STREAMING",
|
|
2482
|
+
streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
|
|
2483
|
+
streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
|
|
2484
|
+
maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
|
|
2485
|
+
liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
|
|
2486
|
+
failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
|
|
2487
|
+
uploadTraces: "PIWI_UPLOAD_TRACES",
|
|
2488
|
+
uploadReport: "PIWI_UPLOAD_REPORT",
|
|
2489
|
+
captureLocators: "PIWI_CAPTURE_LOCATORS",
|
|
2490
|
+
capturePageState: "PIWI_CAPTURE_PAGE_STATE",
|
|
2491
|
+
capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
|
|
2492
|
+
captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
|
|
2493
|
+
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
2494
|
+
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
2495
|
+
sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
|
|
2496
|
+
uploadManifest: "PIWI_UPLOAD_MANIFEST",
|
|
2497
|
+
defaultCapture: "PIWI_DEFAULT_CAPTURE",
|
|
2498
|
+
inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
|
|
2499
|
+
pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
|
|
2500
|
+
outputFile: "PIWI_OUTPUT_FILE",
|
|
2501
|
+
aiMode: "PIWI_AI",
|
|
2502
|
+
aiDir: "PIWI_AI_DIR",
|
|
2503
|
+
aiOnMiss: "PIWI_AI_ON_MISS",
|
|
2504
|
+
aiMaxSteps: "PIWI_AI_MAX_FLOW_STEPS",
|
|
2505
|
+
aiMaxSnapshotChars: "PIWI_AI_MAX_SNAPSHOT_CHARS",
|
|
2506
|
+
aiOptionalProbeTimeout: "PIWI_AI_OPTIONAL_PROBE_TIMEOUT",
|
|
2507
|
+
aiResponseWaitTimeout: "PIWI_AI_RESPONSE_WAIT_TIMEOUT",
|
|
2508
|
+
aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
|
|
2509
|
+
};
|
|
2510
|
+
var PIWI_DESKTOP_CONFIG_ENV = "PIWI_DESKTOP_CONFIG";
|
|
2511
|
+
var PIWI_SHARD_ENV = "PIWI_SHARD";
|
|
2512
|
+
var PIWI_PROBE_ENV = {
|
|
2513
|
+
flag: "PIWI_PROBE",
|
|
2514
|
+
plan: "PIWI_PROBE_PLAN",
|
|
2515
|
+
results: "PIWI_PROBE_RESULTS",
|
|
2516
|
+
secret: "PIWI_PROBE_SECRET"
|
|
2517
|
+
};
|
|
2518
|
+
var PIWI_FLAKE_ENV = {
|
|
2519
|
+
plan: "PIWI_FLAKE_PLAN",
|
|
2520
|
+
results: "PIWI_FLAKE_RESULTS"
|
|
2521
|
+
};
|
|
2522
|
+
var ENV_FALLBACK_SPECS = [
|
|
2523
|
+
{ option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
|
|
2524
|
+
{ option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
|
|
2525
|
+
{ option: "apiKey", env: PIWI_ENV_KEYS.apiKey, kind: "string" },
|
|
2526
|
+
{ option: "username", env: PIWI_ENV_KEYS.username, kind: "string" },
|
|
2527
|
+
{ option: "password", env: PIWI_ENV_KEYS.password, kind: "string" },
|
|
2528
|
+
{ option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
|
|
2529
|
+
{ option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
|
|
2530
|
+
{ option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
|
|
2531
|
+
{ option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
|
|
2532
|
+
{ option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
|
|
2533
|
+
{ option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
|
|
2534
|
+
{ option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
|
|
2535
|
+
{ option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
|
|
2536
|
+
{ option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
|
|
2537
|
+
{ option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
|
|
2538
|
+
{ option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
|
|
2539
|
+
{ option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
|
|
2540
|
+
{ option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
|
|
2541
|
+
{ option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
|
|
2542
|
+
{ option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
|
|
2543
|
+
{ option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
|
|
2544
|
+
{ option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
|
|
2545
|
+
{ option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
|
|
2546
|
+
{ option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
|
|
2547
|
+
{ option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
|
|
2548
|
+
{ option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
|
|
2549
|
+
{ option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
|
|
2550
|
+
{ option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
|
|
2551
|
+
];
|
|
2552
|
+
|
|
2553
|
+
// ../core/src/flake-verdict.ts
|
|
2554
|
+
var FLAKE_SIGNIFICANCE = 0.05;
|
|
2555
|
+
var FLAKE_REPRODUCED_RATE = 0.5;
|
|
2556
|
+
var FLAKE_VERIFY_MIN_RUNS = 5;
|
|
2557
|
+
function logFactorial(n) {
|
|
2558
|
+
let sum = 0;
|
|
2559
|
+
for (let i = 2; i <= n; i++) sum += Math.log(i);
|
|
2560
|
+
return sum;
|
|
2561
|
+
}
|
|
2562
|
+
function logChoose(n, k) {
|
|
2563
|
+
return logFactorial(n) - logFactorial(k) - logFactorial(n - k);
|
|
2564
|
+
}
|
|
2565
|
+
function assertCount(value, name) {
|
|
2566
|
+
if (!Number.isInteger(value) || value < 0) throw new RangeError(`${name} must be a whole number from 0`);
|
|
2567
|
+
}
|
|
2568
|
+
function fisherExactGreater(armFailures, armRuns, controlFailures, controlRuns) {
|
|
2569
|
+
assertCount(armFailures, "armFailures");
|
|
2570
|
+
assertCount(armRuns, "armRuns");
|
|
2571
|
+
assertCount(controlFailures, "controlFailures");
|
|
2572
|
+
assertCount(controlRuns, "controlRuns");
|
|
2573
|
+
if (armFailures > armRuns || controlFailures > controlRuns) throw new RangeError("failures cannot exceed runs");
|
|
2574
|
+
const failures = armFailures + controlFailures;
|
|
2575
|
+
const total = armRuns + controlRuns;
|
|
2576
|
+
const denominator = logChoose(total, failures);
|
|
2577
|
+
const highest = Math.min(armRuns, failures);
|
|
2578
|
+
let p = 0;
|
|
2579
|
+
for (let k = armFailures; k <= highest; k++) {
|
|
2580
|
+
if (failures - k > controlRuns) continue;
|
|
2581
|
+
p += Math.exp(logChoose(armRuns, k) + logChoose(controlRuns, failures - k) - denominator);
|
|
2582
|
+
}
|
|
2583
|
+
return Math.min(1, p);
|
|
2584
|
+
}
|
|
2585
|
+
function flakeArmVerdict(arm, control) {
|
|
2586
|
+
const pValue = fisherExactGreater(arm.matchingFailures, arm.runs, control.matchingFailures, control.runs);
|
|
2587
|
+
const rate = arm.runs > 0 ? arm.matchingFailures / arm.runs : 0;
|
|
2588
|
+
const significant = pValue < FLAKE_SIGNIFICANCE;
|
|
2589
|
+
const verdict = significant && rate >= FLAKE_REPRODUCED_RATE ? "reproduced" : significant ? "amplified" : "not-reproduced";
|
|
2590
|
+
return { verdict, rate, pValue };
|
|
2591
|
+
}
|
|
2592
|
+
function flakeVerifyRuns(rate) {
|
|
2593
|
+
if (!Number.isFinite(rate) || rate <= 0 || rate > 1) throw new RangeError("rate must be above 0 and at most 1");
|
|
2594
|
+
if (rate === 1) return FLAKE_VERIFY_MIN_RUNS;
|
|
2595
|
+
const exact = Math.log(FLAKE_SIGNIFICANCE) / Math.log(1 - rate);
|
|
2596
|
+
return Math.max(FLAKE_VERIFY_MIN_RUNS, Math.ceil(Number(exact.toFixed(9))));
|
|
2597
|
+
}
|
|
2598
|
+
function flakeFixVerified(verify, reproducedRate) {
|
|
2599
|
+
return verify.matchingFailures === 0 && verify.runs >= flakeVerifyRuns(reproducedRate);
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
// src/internal/flake/lab.ts
|
|
2603
|
+
var FAILED = /* @__PURE__ */ new Set(["failed", "timedOut"]);
|
|
2604
|
+
function overlaps(a, b) {
|
|
2605
|
+
return a.startedAt < b.startedAt + b.duration && b.startedAt < a.startedAt + a.duration;
|
|
2606
|
+
}
|
|
2607
|
+
function workerCondition(conditions) {
|
|
2608
|
+
for (const c of conditions) if (c.kind === "alongside" || c.kind === "after") return c;
|
|
2609
|
+
return null;
|
|
2610
|
+
}
|
|
2611
|
+
function roundKept(line, all, conditions) {
|
|
2612
|
+
const condition = workerCondition(conditions);
|
|
2613
|
+
if (!condition) return true;
|
|
2614
|
+
if (condition.kind === "alongside") return all.some((l) => l.role === "companion" && overlaps(l, line));
|
|
2615
|
+
const before = all.filter((l) => l !== line && l.startedAt < line.startedAt).sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
2616
|
+
return before?.role === "companion";
|
|
2617
|
+
}
|
|
2618
|
+
function countArm(batches, arm) {
|
|
2619
|
+
let seen = 0;
|
|
2620
|
+
const kept = [];
|
|
2621
|
+
for (const lines of batches) {
|
|
2622
|
+
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);
|
|
2623
|
+
seen += targets.length;
|
|
2624
|
+
kept.push(...targets.filter((l) => roundKept(l, lines, arm.conditions)));
|
|
2625
|
+
}
|
|
2626
|
+
const count = {
|
|
2627
|
+
runs: 0,
|
|
2628
|
+
matchingFailures: 0,
|
|
2629
|
+
otherFailures: 0,
|
|
2630
|
+
discardedRounds: seen - kept.length,
|
|
2631
|
+
stoppedEarly: false,
|
|
2632
|
+
otherSignatures: []
|
|
2633
|
+
};
|
|
2634
|
+
const others = /* @__PURE__ */ new Map();
|
|
2635
|
+
for (const [i, line] of kept.entries()) {
|
|
2636
|
+
count.runs++;
|
|
2637
|
+
if (line.matchesHistory) count.matchingFailures++;
|
|
2638
|
+
else if (FAILED.has(line.status)) {
|
|
2639
|
+
count.otherFailures++;
|
|
2640
|
+
const signature = line.errorSignature ?? line.status;
|
|
2641
|
+
others.set(signature, (others.get(signature) ?? 0) + 1);
|
|
2642
|
+
}
|
|
2643
|
+
if (arm.stopAt != null && count.matchingFailures >= arm.stopAt) {
|
|
2644
|
+
count.stoppedEarly = i < kept.length - 1;
|
|
2645
|
+
break;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
count.otherSignatures = [...others.entries()].sort((a, b) => b[1] - a[1]).map(([s]) => s);
|
|
2649
|
+
return count;
|
|
2650
|
+
}
|
|
2651
|
+
function armDone(count, arm) {
|
|
2652
|
+
return arm.stopAt != null && count.matchingFailures >= arm.stopAt || count.runs >= arm.runs;
|
|
2653
|
+
}
|
|
2654
|
+
function nextBatch(count, arm) {
|
|
2655
|
+
const left = Math.max(0, arm.runs - count.runs);
|
|
2656
|
+
return arm.stopAt == null ? left : Math.min(left, FLAKE_BATCH_RUNS);
|
|
2657
|
+
}
|
|
2658
|
+
function escapeRegex(s) {
|
|
2659
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2660
|
+
}
|
|
2661
|
+
function grepFor(tests) {
|
|
2662
|
+
return tests.map((t) => `(^| )${escapeRegex([...t.suite, t.title].join(" "))}( @\\S+)*$`).join("|");
|
|
2663
|
+
}
|
|
2664
|
+
function playwrightArgs(test, arm, repeat) {
|
|
2665
|
+
const companion = workerCondition(arm.conditions);
|
|
2666
|
+
const tests = companion ? [test, companion.test] : [test];
|
|
2667
|
+
const files = [...new Set(tests.map((t) => t.file))];
|
|
2668
|
+
const args = [...files, "--grep", grepFor(tests), `--repeat-each=${repeat}`, "--retries=0"];
|
|
2669
|
+
const alongside = companion?.kind === "alongside";
|
|
2670
|
+
args.push(`--workers=${alongside ? 2 : 1}`);
|
|
2671
|
+
if (alongside && files.length === 1) args.push("--fully-parallel");
|
|
2672
|
+
const project = arm.conditions.find((c) => c.kind === "project");
|
|
2673
|
+
const name = project?.kind === "project" ? project.name : test.project;
|
|
2674
|
+
if (name) args.push(`--project=${name}`);
|
|
2675
|
+
return args;
|
|
2676
|
+
}
|
|
2677
|
+
function armVerdict(arm, control) {
|
|
2678
|
+
return flakeArmVerdict(arm, control);
|
|
2679
|
+
}
|
|
2680
|
+
function verifyVerdict(arm, reproducedRate) {
|
|
2681
|
+
if (arm.matchingFailures > 0) return "still-fails";
|
|
2682
|
+
return flakeFixVerified(arm, reproducedRate) ? "verified" : "inconclusive";
|
|
2683
|
+
}
|
|
2684
|
+
var EXIT_REPRODUCED = 0;
|
|
2685
|
+
var EXIT_NOT_REPRODUCED = 1;
|
|
2686
|
+
var EXIT_ERROR4 = 2;
|
|
2687
|
+
var EXIT_BISECT_SKIP = 125;
|
|
2688
|
+
function exitCodeFor(verdict) {
|
|
2689
|
+
return verdict === "reproduced" || verdict === "verified" ? EXIT_REPRODUCED : EXIT_NOT_REPRODUCED;
|
|
2690
|
+
}
|
|
2691
|
+
function bisectExitCode(verdict) {
|
|
2692
|
+
if (verdict === "verified") return EXIT_REPRODUCED;
|
|
2693
|
+
if (verdict === "still-fails") return EXIT_NOT_REPRODUCED;
|
|
2694
|
+
return EXIT_BISECT_SKIP;
|
|
2695
|
+
}
|
|
2696
|
+
function parseDuration(text) {
|
|
2697
|
+
const trimmed = text.trim().toLowerCase();
|
|
2698
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) return Math.round(Number(trimmed) * 6e4);
|
|
2699
|
+
const re = /(\d+(?:\.\d+)?)\s*(h|m|s)/g;
|
|
2700
|
+
let total = 0;
|
|
2701
|
+
let consumed = "";
|
|
2702
|
+
for (const m of trimmed.matchAll(re)) {
|
|
2703
|
+
total += Number(m[1]) * (m[2] === "h" ? 36e5 : m[2] === "m" ? 6e4 : 1e3);
|
|
2704
|
+
consumed += m[0];
|
|
2705
|
+
}
|
|
2706
|
+
return consumed.replace(/\s/g, "") === trimmed.replace(/\s/g, "") && total > 0 ? Math.round(total) : null;
|
|
2707
|
+
}
|
|
2708
|
+
function formatDuration(ms) {
|
|
2709
|
+
if (ms < 6e4) return `${Math.max(1, Math.round(ms / 1e3))} s`;
|
|
2710
|
+
const minutes = Math.round(ms / 6e4);
|
|
2711
|
+
if (minutes < 60) return `${minutes} min`;
|
|
2712
|
+
return `${Math.floor(minutes / 60)} h ${minutes % 60} min`;
|
|
2713
|
+
}
|
|
2714
|
+
function withinBudget(startedAt, now, budgetMs) {
|
|
2715
|
+
return now - startedAt < budgetMs;
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
// src/internal/support/connection.ts
|
|
2719
|
+
var fs6 = __toESM(require("fs"));
|
|
2720
|
+
var path9 = __toESM(require("path"));
|
|
2721
|
+
|
|
2722
|
+
// ../core/src/dotenv.ts
|
|
2723
|
+
function parseDotEnv(text) {
|
|
2724
|
+
const out = {};
|
|
2725
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
2726
|
+
const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(raw);
|
|
2727
|
+
if (!m) continue;
|
|
2728
|
+
let value = m[2];
|
|
2729
|
+
const quote4 = value[0];
|
|
2730
|
+
if ((quote4 === '"' || quote4 === "'") && value.length > 1 && value.endsWith(quote4)) {
|
|
2731
|
+
value = value.slice(1, -1);
|
|
2732
|
+
if (quote4 === '"') value = value.replace(/\\n/g, "\n").replace(/\\"/g, '"');
|
|
2733
|
+
} else {
|
|
2734
|
+
value = value.replace(/\s+#.*$/, "");
|
|
2735
|
+
}
|
|
2736
|
+
out[m[1]] = value;
|
|
2737
|
+
}
|
|
2738
|
+
return out;
|
|
2739
|
+
}
|
|
2740
|
+
function resolvePiwiConnection(sources) {
|
|
2741
|
+
const own = {
|
|
2742
|
+
serverUrl: sources.flags?.serverUrl || sources.env?.PIWI_DASHBOARD_URL,
|
|
2743
|
+
apiKey: sources.flags?.apiKey || sources.env?.PIWI_API_KEY
|
|
2744
|
+
};
|
|
2745
|
+
const workspace = { serverUrl: sources.dotEnv?.PIWI_DASHBOARD_URL, apiKey: sources.dotEnv?.PIWI_API_KEY };
|
|
2746
|
+
const project = sources.flags?.project || sources.env?.PIWI_PROJECT_NAME || sources.dotEnv?.PIWI_PROJECT_NAME || "";
|
|
2747
|
+
const desktop = sources.desktop ?? null;
|
|
2748
|
+
const trim = (url) => url.replace(/\/+$/, "");
|
|
2749
|
+
const layer = own.serverUrl ? own : workspace.serverUrl ? workspace : null;
|
|
2750
|
+
const serverUrl = layer ? trim(layer.serverUrl) : desktop ? trim(desktop.url) : "";
|
|
2751
|
+
if (!serverUrl) return null;
|
|
2752
|
+
const desktopKey = desktop && trim(desktop.url) === serverUrl ? desktop.token : null;
|
|
2753
|
+
return { serverUrl, apiKey: layer?.apiKey || desktopKey || null, project };
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
// src/internal/support/connection.ts
|
|
2757
|
+
function resolveCliConnection(flags, env, dirs) {
|
|
2758
|
+
let dotEnv = {};
|
|
2759
|
+
for (const dir of dirs) {
|
|
2760
|
+
try {
|
|
2761
|
+
dotEnv = { ...parseDotEnv(fs6.readFileSync(path9.join(dir, ".env"), "utf-8")), ...dotEnv };
|
|
2762
|
+
} catch {
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
return resolvePiwiConnection({
|
|
2766
|
+
flags,
|
|
2767
|
+
env,
|
|
2768
|
+
dotEnv,
|
|
2769
|
+
desktop: readDesktopConfig(env[PIWI_DESKTOP_CONFIG_ENV] || defaultDesktopConfigPath())
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
// src/internal/support/playwright-spawn.ts
|
|
2774
|
+
var import_node_child_process3 = require("child_process");
|
|
2775
|
+
var fs7 = __toESM(require("fs"));
|
|
2776
|
+
var import_node_module2 = require("module");
|
|
2777
|
+
var path10 = __toESM(require("path"));
|
|
2778
|
+
function resolvePlaywrightCli2(cwd = process.cwd()) {
|
|
2779
|
+
const require2 = (0, import_node_module2.createRequire)(path10.join(cwd, "noop.js"));
|
|
2780
|
+
for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
|
|
2781
|
+
try {
|
|
2782
|
+
return require2.resolve(id);
|
|
2783
|
+
} catch {
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
return null;
|
|
2787
|
+
}
|
|
2788
|
+
function spawnPlaywright(playwrightArgs2, env, options = {}) {
|
|
2789
|
+
const output = options.output ?? "inherit";
|
|
2790
|
+
let fd = output === "inherit" ? null : fs7.openSync(output.file, "a");
|
|
2791
|
+
const stdio = fd === null ? "inherit" : ["ignore", fd, fd];
|
|
2792
|
+
const cli = resolvePlaywrightCli2();
|
|
2793
|
+
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], {
|
|
2794
|
+
stdio,
|
|
2795
|
+
env
|
|
2796
|
+
});
|
|
2797
|
+
return new Promise((resolve8) => {
|
|
2798
|
+
const done = (code) => {
|
|
2799
|
+
if (fd !== null) fs7.closeSync(fd);
|
|
2800
|
+
fd = null;
|
|
2801
|
+
resolve8(code);
|
|
2802
|
+
};
|
|
2803
|
+
child.on("error", (err) => {
|
|
2804
|
+
console.error(`${options.command ?? "piwi"}: could not start Playwright \u2014 ${err.message}`);
|
|
2805
|
+
done(2);
|
|
2806
|
+
});
|
|
2807
|
+
child.on("exit", (code) => done(code ?? 2));
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
// src/cli/flake.ts
|
|
2812
|
+
var USAGE4 = `
|
|
2813
|
+
piwi flake \u2014 make a flaky test fail on demand, then prove the fix
|
|
2814
|
+
|
|
2815
|
+
Usage:
|
|
2816
|
+
npx @piwitests/reporter flake <test> [options]
|
|
2817
|
+
npx @piwitests/reporter flake verify <test> [--runs <n>] [--bisect] [--json]
|
|
2818
|
+
|
|
2819
|
+
<test> is a test case id (the number in its dashboard URL) or a spec file and
|
|
2820
|
+
line, tests/checkout.spec.ts:42, looked up in the project on the dashboard.
|
|
2821
|
+
|
|
2822
|
+
It fetches the test's plan from the dashboard: a control arm and one arm per
|
|
2823
|
+
suspect of its flake profile, most likely first. It runs the control (10 runs),
|
|
2824
|
+
then each arm (up to 10 runs, stopping at 3 failures with the same error as in
|
|
2825
|
+
CI), one test at a time with retries off, in the checkout it runs in. It prints
|
|
2826
|
+
each arm against the control with its verdict and saves the experiment on the
|
|
2827
|
+
test's Flakiness tab. The capture fixtures must be in use (piwiFixtures).
|
|
2828
|
+
|
|
2829
|
+
verify reruns the arm that last reproduced the test, and its control, until a
|
|
2830
|
+
matching failure appears or enough runs pass to say the fix holds.
|
|
2831
|
+
|
|
2832
|
+
verify --bisect runs that arm alone at this checkout and saves nothing, for
|
|
2833
|
+
git bisect run: exit 0 (good) when enough runs pass, 1 (bad) on a failure with
|
|
2834
|
+
the same error as in CI, 125 (skip) when this commit cannot tell.
|
|
2835
|
+
|
|
2836
|
+
Options:
|
|
2837
|
+
--suspect <n> Run only the arm of suspect n (its rank on the Flakiness tab)
|
|
2838
|
+
--all Also run every condition at once when none reproduces alone
|
|
2839
|
+
--runs <n> Runs of the control and of each arm (default 10; verify:
|
|
2840
|
+
the number that proves the fix at the rate it reproduced)
|
|
2841
|
+
--budget <duration> Start no new arm after this long: 15m (default), 90s, 1h
|
|
2842
|
+
--no-upload Keep the results off the dashboard; it still needs the plan
|
|
2843
|
+
from the dashboard, or from --plan when it cannot be reached
|
|
2844
|
+
--plan <file> Read the plan from a file (a saved flake-plan response, or
|
|
2845
|
+
the plan the plan_flake_experiment MCP tool returns) and run
|
|
2846
|
+
it without the dashboard; implies --no-upload
|
|
2847
|
+
--bisect verify only: one bisect step, as above; implies --no-upload
|
|
2848
|
+
--source <where> Where the lab runs, recorded on the experiment: cli, ci or
|
|
2849
|
+
desktop (default: ci when CI is set, else cli)
|
|
2850
|
+
--json Print the results as JSON instead of text
|
|
2851
|
+
--server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
|
|
2852
|
+
--api-key <key> Reporter API key (env PIWI_API_KEY, .env)
|
|
2853
|
+
--project <name|id> The Piwi project, for a file:line test (env PIWI_PROJECT_NAME)
|
|
2854
|
+
|
|
2855
|
+
Exit codes: 0 an arm reproduced the failure (verify: the fix held),
|
|
2856
|
+
1 nothing reproduced (verify: it still fails, or too few runs),
|
|
2857
|
+
2 error; with --bisect, 0 good, 1 bad, 125 skip, 2 error.
|
|
2858
|
+
`.trim();
|
|
2859
|
+
var DEFAULT_BUDGET_MS = 15 * 6e4;
|
|
2860
|
+
var SOURCES = ["cli", "ci", "desktop"];
|
|
2861
|
+
var MAX_BATCHES = 12;
|
|
2862
|
+
var LOG_TAIL_LINES = 40;
|
|
2863
|
+
function parseFlakeArgs(argv) {
|
|
2864
|
+
const args = {
|
|
2865
|
+
verify: argv[0] === "verify",
|
|
2866
|
+
test: null,
|
|
2867
|
+
suspect: null,
|
|
2868
|
+
all: false,
|
|
2869
|
+
runs: null,
|
|
2870
|
+
budgetMs: DEFAULT_BUDGET_MS,
|
|
2871
|
+
upload: true,
|
|
2872
|
+
planFile: null,
|
|
2873
|
+
bisect: false,
|
|
2874
|
+
source: null,
|
|
2875
|
+
json: false,
|
|
2876
|
+
help: false
|
|
2877
|
+
};
|
|
2878
|
+
const rest = args.verify ? argv.slice(1) : argv;
|
|
2879
|
+
const value = (i, name) => {
|
|
2880
|
+
const v = rest[i + 1];
|
|
2881
|
+
if (v === void 0 || v.startsWith("--")) throw new Error(`${name} needs a value`);
|
|
2882
|
+
return v;
|
|
2883
|
+
};
|
|
2884
|
+
const whole = (text, name, min, max) => {
|
|
2885
|
+
const n = Number(text);
|
|
2886
|
+
if (!Number.isInteger(n) || n < min || n > max)
|
|
2887
|
+
throw new Error(`${name} must be a whole number from ${min} to ${max}`);
|
|
2888
|
+
return n;
|
|
2889
|
+
};
|
|
2890
|
+
for (let i = 0; i < rest.length; i++) {
|
|
2891
|
+
const arg = rest[i];
|
|
2892
|
+
switch (arg) {
|
|
2893
|
+
case "-h":
|
|
2894
|
+
case "--help":
|
|
2895
|
+
args.help = true;
|
|
2896
|
+
break;
|
|
2897
|
+
case "--suspect":
|
|
2898
|
+
args.suspect = whole(value(i++, arg), arg, 1, 50);
|
|
2899
|
+
break;
|
|
2900
|
+
case "--all":
|
|
2901
|
+
args.all = true;
|
|
2902
|
+
break;
|
|
2903
|
+
case "--runs":
|
|
2904
|
+
args.runs = whole(value(i++, arg), arg, 1, 100);
|
|
2905
|
+
break;
|
|
2906
|
+
case "--budget": {
|
|
2907
|
+
const ms = parseDuration(value(i++, arg));
|
|
2908
|
+
if (ms == null) throw new Error("--budget must be a duration like 15m, 90s or 1h");
|
|
2909
|
+
args.budgetMs = ms;
|
|
2910
|
+
break;
|
|
2911
|
+
}
|
|
2912
|
+
case "--no-upload":
|
|
2913
|
+
args.upload = false;
|
|
2914
|
+
break;
|
|
2915
|
+
case "--plan":
|
|
2916
|
+
args.planFile = value(i++, arg);
|
|
2917
|
+
args.upload = false;
|
|
2918
|
+
break;
|
|
2919
|
+
case "--bisect":
|
|
2920
|
+
args.bisect = true;
|
|
2921
|
+
args.upload = false;
|
|
2922
|
+
break;
|
|
2923
|
+
case "--source": {
|
|
2924
|
+
const where = value(i++, arg);
|
|
2925
|
+
if (!SOURCES.includes(where))
|
|
2926
|
+
throw new Error(`--source must be one of ${SOURCES.join(", ")}`);
|
|
2927
|
+
args.source = where;
|
|
2928
|
+
break;
|
|
2929
|
+
}
|
|
2930
|
+
case "--json":
|
|
2931
|
+
args.json = true;
|
|
2932
|
+
break;
|
|
2933
|
+
case "--server-url":
|
|
2934
|
+
args.serverUrl = value(i++, arg);
|
|
2935
|
+
break;
|
|
2936
|
+
case "--api-key":
|
|
2937
|
+
args.apiKey = value(i++, arg);
|
|
2938
|
+
break;
|
|
2939
|
+
case "--project":
|
|
2940
|
+
args.project = value(i++, arg);
|
|
2941
|
+
break;
|
|
2942
|
+
default:
|
|
2943
|
+
if (arg.startsWith("-")) throw new Error(`unknown option ${arg}`);
|
|
2944
|
+
if (args.test !== null) throw new Error(`one test at a time (got "${args.test}" and "${arg}")`);
|
|
2945
|
+
args.test = arg;
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
if (args.suspect !== null && args.all) throw new Error("--suspect and --all cannot be used together");
|
|
2949
|
+
if (args.verify && (args.suspect !== null || args.all))
|
|
2950
|
+
throw new Error("verify reruns one arm: drop --suspect and --all");
|
|
2951
|
+
if (args.bisect && !args.verify) throw new Error("--bisect is a verify option: flake verify <test> --bisect");
|
|
2952
|
+
return args;
|
|
2953
|
+
}
|
|
2954
|
+
function gitHead() {
|
|
2955
|
+
try {
|
|
2956
|
+
return (0, import_node_child_process4.execFileSync)("git", ["rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
2957
|
+
} catch {
|
|
2958
|
+
return null;
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
function commitsDiffer(a, b) {
|
|
2962
|
+
if (!a || !b) return false;
|
|
2963
|
+
const [x, y] = [a.toLowerCase(), b.toLowerCase()];
|
|
2964
|
+
return !x.startsWith(y) && !y.startsWith(x);
|
|
2965
|
+
}
|
|
2966
|
+
function short(sha) {
|
|
2967
|
+
return sha ? sha.slice(0, 7) : "unknown";
|
|
2968
|
+
}
|
|
2969
|
+
function fraction(count) {
|
|
2970
|
+
return `${count.matchingFailures}/${count.runs}`;
|
|
2971
|
+
}
|
|
2972
|
+
function formatP(p) {
|
|
2973
|
+
return p < 1e-3 ? "< 0.001" : `= ${p.toFixed(3)}`;
|
|
2974
|
+
}
|
|
2975
|
+
var FlakeError = class extends Error {
|
|
2976
|
+
};
|
|
2977
|
+
async function getJson(http, url) {
|
|
2978
|
+
const res = await fetch(`${http.serverUrl}${url}`, { headers: http.headers });
|
|
2979
|
+
if (!res.ok) {
|
|
2980
|
+
const body = await res.json().catch(() => ({}));
|
|
2981
|
+
throw new FlakeError(body.message || `the dashboard returned ${res.status} for ${url.split("?")[0]}`);
|
|
2982
|
+
}
|
|
2983
|
+
return await res.json();
|
|
2984
|
+
}
|
|
2985
|
+
async function resolveTestCase(http, test, project, apiKey) {
|
|
2986
|
+
if (/^\d+$/.test(test)) return Number(test);
|
|
2987
|
+
const m = /^(.+):(\d+)$/.exec(test);
|
|
2988
|
+
if (!m) throw new FlakeError(`"${test}" is neither a test case id nor file:line`);
|
|
2989
|
+
const projectId = await resolveProjectId({ serverUrl: http.serverUrl, apiKey, project, key: "" });
|
|
2990
|
+
const found = await getJson(
|
|
2991
|
+
http,
|
|
2992
|
+
`/api/projects/${projectId}/flake-lab/test?location=${encodeURIComponent(`${m[1].replace(/\\/g, "/")}:${m[2]}`)}`
|
|
2993
|
+
);
|
|
2994
|
+
return found.testCaseId;
|
|
2995
|
+
}
|
|
2996
|
+
function playwrightRunner(dir, log) {
|
|
2997
|
+
return {
|
|
2998
|
+
now: () => Date.now(),
|
|
2999
|
+
async runBatch(plan, arm, repeat, batch) {
|
|
3000
|
+
const planFile = path11.join(dir, `${arm.id}.plan.json`);
|
|
3001
|
+
if (!fs8.existsSync(planFile)) {
|
|
3002
|
+
fs8.writeFileSync(
|
|
3003
|
+
planFile,
|
|
3004
|
+
JSON.stringify({
|
|
3005
|
+
version: FLAKE_PLAN_VERSION,
|
|
3006
|
+
experimentId: plan.experimentId ?? `local-${process.pid}`,
|
|
3007
|
+
test: plan.test,
|
|
3008
|
+
arm: { id: arm.id, conditions: arm.conditions },
|
|
3009
|
+
errorSignatures: plan.errorSignatures
|
|
3010
|
+
})
|
|
3011
|
+
);
|
|
3012
|
+
}
|
|
3013
|
+
const resultsFile = path11.join(dir, `${arm.id}.${batch}.jsonl`);
|
|
3014
|
+
fs8.writeFileSync(resultsFile, "");
|
|
3015
|
+
const env = {
|
|
3016
|
+
...process.env,
|
|
3017
|
+
[PIWI_FLAKE_ENV.plan]: planFile,
|
|
3018
|
+
[PIWI_FLAKE_ENV.results]: resultsFile
|
|
3019
|
+
};
|
|
3020
|
+
delete env[PIWI_PROBE_ENV.flag];
|
|
3021
|
+
fs8.appendFileSync(log, `
|
|
3022
|
+
\u2500\u2500 ${arm.id} batch ${batch} \u2500\u2500
|
|
3023
|
+
`);
|
|
3024
|
+
await spawnPlaywright(playwrightArgs(plan.test, arm, repeat), env, {
|
|
3025
|
+
output: { file: log },
|
|
3026
|
+
command: "piwi flake"
|
|
3027
|
+
});
|
|
3028
|
+
return readResults(resultsFile);
|
|
3029
|
+
}
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
function readResults(file) {
|
|
3033
|
+
const lines = [];
|
|
3034
|
+
for (const line of fs8.readFileSync(file, "utf8").split("\n")) {
|
|
3035
|
+
if (!line.trim()) continue;
|
|
3036
|
+
try {
|
|
3037
|
+
lines.push(JSON.parse(line));
|
|
3038
|
+
} catch {
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
return lines;
|
|
3042
|
+
}
|
|
3043
|
+
async function runArm(runner, plan, arm) {
|
|
3044
|
+
const batches = [];
|
|
3045
|
+
let count = countArm(batches, arm);
|
|
3046
|
+
for (let batch = 1; batch <= MAX_BATCHES && !armDone(count, arm); batch++) {
|
|
3047
|
+
const repeat = nextBatch(count, arm);
|
|
3048
|
+
if (repeat <= 0) break;
|
|
3049
|
+
const lines = await runner.runBatch(plan, arm, repeat, batch);
|
|
3050
|
+
if (!lines.some((l) => l.role === "target")) {
|
|
3051
|
+
throw new FlakeError(
|
|
3052
|
+
`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?`
|
|
3053
|
+
);
|
|
3054
|
+
}
|
|
3055
|
+
batches.push(lines);
|
|
3056
|
+
count = countArm(batches, arm);
|
|
3057
|
+
}
|
|
3058
|
+
return count;
|
|
3059
|
+
}
|
|
3060
|
+
async function runSession(runner, plan, args, onArm = () => {
|
|
3061
|
+
}) {
|
|
3062
|
+
const started = runner.now();
|
|
3063
|
+
const control = args.bisect ? { arm: plan.control, count: countArm([], plan.control), verdict: null, pValue: null, skipped: "bisect step" } : {
|
|
3064
|
+
arm: plan.control,
|
|
3065
|
+
count: await runArm(runner, plan, plan.control),
|
|
3066
|
+
verdict: null,
|
|
3067
|
+
pValue: null,
|
|
3068
|
+
skipped: null
|
|
3069
|
+
};
|
|
3070
|
+
onArm(control, null);
|
|
3071
|
+
const arms = args.suspect != null ? plan.arms.filter((a) => a.rank === args.suspect) : plan.arms;
|
|
3072
|
+
const results = [];
|
|
3073
|
+
const run = async (arm) => {
|
|
3074
|
+
if (!withinBudget(started, runner.now(), args.budgetMs)) {
|
|
3075
|
+
const result2 = { arm, count: countArm([], arm), verdict: null, pValue: null, skipped: "budget spent" };
|
|
3076
|
+
results.push(result2);
|
|
3077
|
+
onArm(result2, control.count);
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3080
|
+
const count = await runArm(runner, plan, arm);
|
|
3081
|
+
let verdict;
|
|
3082
|
+
let pValue = null;
|
|
3083
|
+
if (plan.kind === "verify") {
|
|
3084
|
+
verdict = verifyVerdict(count, plan.verifies?.rate ?? 1);
|
|
3085
|
+
} else {
|
|
3086
|
+
const v = armVerdict(count, control.count);
|
|
3087
|
+
verdict = v.verdict;
|
|
3088
|
+
pValue = v.pValue;
|
|
3089
|
+
}
|
|
3090
|
+
const result = { arm, count, verdict, pValue, skipped: null };
|
|
3091
|
+
results.push(result);
|
|
3092
|
+
onArm(result, control.count);
|
|
3093
|
+
};
|
|
3094
|
+
for (const arm of arms) await run(arm);
|
|
3095
|
+
if (args.all && plan.combined && !results.some((r) => r.verdict === "reproduced")) await run(plan.combined);
|
|
3096
|
+
return { control, arms: results };
|
|
3097
|
+
}
|
|
3098
|
+
function sessionVerdict(kind, arms) {
|
|
3099
|
+
if (kind === "verify") {
|
|
3100
|
+
const v = arms.find((a) => a.arm.id === "verify");
|
|
3101
|
+
return {
|
|
3102
|
+
verdict: v?.verdict ?? "inconclusive",
|
|
3103
|
+
reproducingArm: v ? "verify" : null
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
const reproduced = arms.find((a) => a.verdict === "reproduced");
|
|
3107
|
+
if (reproduced) return { verdict: "reproduced", reproducingArm: reproduced.arm.id };
|
|
3108
|
+
return {
|
|
3109
|
+
verdict: arms.some((a) => a.verdict === "amplified") ? "amplified" : "not-reproduced",
|
|
3110
|
+
reproducingArm: null
|
|
3111
|
+
};
|
|
3112
|
+
}
|
|
3113
|
+
function pad(text, width) {
|
|
3114
|
+
return text.length >= width ? `${text} ` : text + " ".repeat(width - text.length);
|
|
3115
|
+
}
|
|
3116
|
+
function clip(text, width) {
|
|
3117
|
+
return text.length > width ? `${text.slice(0, width - 1)}\u2026` : text;
|
|
3118
|
+
}
|
|
3119
|
+
function armLine(result, stopAt) {
|
|
3120
|
+
const { arm, count } = result;
|
|
3121
|
+
const name = arm.id === "control" ? "control" : `${arm.rank != null ? `${arm.rank} ` : " "}${arm.label}`;
|
|
3122
|
+
if (result.skipped) return ` ${pad(clip(name, 34), 35)}${result.skipped}`;
|
|
3123
|
+
const notes = [];
|
|
3124
|
+
if (count.stoppedEarly && stopAt != null) notes.push(`stopped at ${stopAt}`);
|
|
3125
|
+
if (count.matchingFailures > 0) notes.push("same error as in CI");
|
|
3126
|
+
if (count.otherFailures > 0)
|
|
3127
|
+
notes.push(`${count.otherFailures} different error${count.otherFailures === 1 ? "" : "s"} (counted apart)`);
|
|
3128
|
+
if (count.discardedRounds > 0) {
|
|
3129
|
+
notes.push(`${count.discardedRounds} round${count.discardedRounds === 1 ? "" : "s"} left out`);
|
|
3130
|
+
}
|
|
3131
|
+
const verdict = result.verdict ? result.verdict.replace("-", " ") : "";
|
|
3132
|
+
return ` ${pad(clip(name, 34), 35)}${pad(fraction(count), 7)}${pad(notes.join(" \xB7 "), 44)}${verdict}`.trimEnd();
|
|
3133
|
+
}
|
|
3134
|
+
function verdictSentence(report, arms, control) {
|
|
3135
|
+
const c = fraction(control.count);
|
|
3136
|
+
const against = control.skipped ? "" : ` (control ${c})`;
|
|
3137
|
+
if (report.kind === "verify") {
|
|
3138
|
+
const v = arms.find((a) => a.arm.id === "verify");
|
|
3139
|
+
if (!v) return "Verdict: the verify arm did not run.";
|
|
3140
|
+
const label = v.arm.label;
|
|
3141
|
+
if (report.verdict === "verified") {
|
|
3142
|
+
return `Verdict: fix verified: 0 matching failures in ${v.count.runs} runs under ${label}${against}`;
|
|
3143
|
+
}
|
|
3144
|
+
if (report.verdict === "still-fails") {
|
|
3145
|
+
return `Verdict: still fails: ${fraction(v.count)} under ${label} failed as in CI${against}`;
|
|
3146
|
+
}
|
|
3147
|
+
return `Verdict: inconclusive: ${v.count.runs} clean runs under ${label}, too few to verify the fix`;
|
|
3148
|
+
}
|
|
3149
|
+
const r = arms.find((a) => a.arm.id === report.reproducingArm);
|
|
3150
|
+
if (r) {
|
|
3151
|
+
return `Verdict: reproduced by ${r.arm.label} (${fraction(r.count)} against ${c}, p ${formatP(r.pValue ?? 1)})`;
|
|
3152
|
+
}
|
|
3153
|
+
const amplified = arms.find((a) => a.verdict === "amplified");
|
|
3154
|
+
if (amplified) {
|
|
3155
|
+
return `Verdict: not reproduced; ${amplified.arm.label} made it fail more often (${fraction(amplified.count)} against ${c}, p ${formatP(amplified.pValue ?? 1)})`;
|
|
3156
|
+
}
|
|
3157
|
+
return `Verdict: not reproduced (control ${c})`;
|
|
3158
|
+
}
|
|
3159
|
+
function bisectStepWord(code) {
|
|
3160
|
+
if (code === 0) return "good (the arm held at this commit)";
|
|
3161
|
+
if (code === 1) return "bad (a failure with the same error as in CI)";
|
|
3162
|
+
return "skip (too few clean runs to tell)";
|
|
3163
|
+
}
|
|
3164
|
+
function printHeader(plan, commit, estimate, budgetMs) {
|
|
3165
|
+
const out = [];
|
|
3166
|
+
out.push(
|
|
3167
|
+
`piwi flake${plan.kind === "verify" ? " verify" : ""} \xB7 ${plan.displayTitle} \xB7 ${plan.failures} failures in ${plan.windowDays} days`
|
|
3168
|
+
);
|
|
3169
|
+
out.push("");
|
|
3170
|
+
if (plan.kind === "verify" && plan.verifies) {
|
|
3171
|
+
out.push(
|
|
3172
|
+
`Rerunning ${plan.verifies.label}: it reproduced ${Math.round(plan.verifies.rate * 100)}% of runs on ${short(plan.verifies.commit)}`
|
|
3173
|
+
);
|
|
3174
|
+
} else if (plan.suspects.length === 0) {
|
|
3175
|
+
out.push("Suspects from history: none. Nothing in its history separates its failures from its passes.");
|
|
3176
|
+
} else {
|
|
3177
|
+
out.push(`${pad("Suspects from history", 57)}condition`);
|
|
3178
|
+
for (const s of plan.suspects) {
|
|
3179
|
+
const counts = `${s.counts.failuresWith}/${s.counts.failures} failures \xB7 ${s.counts.passesWith}/${s.counts.passes} passes`;
|
|
3180
|
+
out.push(
|
|
3181
|
+
` ${s.rank} ${pad(clip(s.label, 40), 41)}${pad(counts, 30)}${s.skipped ? `none: ${s.skipped}` : s.conditionLabel}`
|
|
3182
|
+
);
|
|
3183
|
+
if (s.sharedRoutes?.length) out.push(` both write ${s.sharedRoutes.join(", ")}`);
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
out.push("");
|
|
3187
|
+
const estimateText = estimate != null ? `up to ${formatDuration(estimate)}` : "unknown (no passing run on record)";
|
|
3188
|
+
out.push(`Estimate: ${estimateText} \xB7 budget ${formatDuration(budgetMs)}`);
|
|
3189
|
+
out.push(`Lab \xB7 ${os2.hostname()} \xB7 HEAD ${short(commit)} \xB7 ${plan.test.project ?? "default project"} \xB7 retries off`);
|
|
3190
|
+
if (commitsDiffer(commit, plan.failureCommit)) {
|
|
3191
|
+
out.push(`warning: this checkout is ${short(commit)}; the latest failure ran on ${short(plan.failureCommit)}.`);
|
|
3192
|
+
}
|
|
3193
|
+
console.log(out.join("\n"));
|
|
3194
|
+
}
|
|
3195
|
+
async function loadPlan(args, commit) {
|
|
3196
|
+
if (args.planFile) {
|
|
3197
|
+
try {
|
|
3198
|
+
const raw = JSON.parse(fs8.readFileSync(args.planFile, "utf8"));
|
|
3199
|
+
const plan = "plan" in raw && raw.plan ? raw.plan : raw;
|
|
3200
|
+
if (plan.version !== 1 || !plan.control || !Array.isArray(plan.arms)) throw new Error("not a flake-lab plan");
|
|
3201
|
+
if (args.verify !== (plan.kind === "verify")) {
|
|
3202
|
+
throw new Error(`it is a ${plan.kind} plan; run ${plan.kind === "verify" ? "flake verify" : "flake"} with it`);
|
|
3203
|
+
}
|
|
3204
|
+
return { plan: args.runs ? withRuns(plan, args.runs) : plan, http: null };
|
|
3205
|
+
} catch (error) {
|
|
3206
|
+
throw new FlakeError(`cannot read the plan ${args.planFile}: ${error.message}`);
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
if (!args.test) throw new FlakeError("name the test: a test case id or file:line");
|
|
3210
|
+
const connection = resolveCliConnection(
|
|
3211
|
+
{ serverUrl: args.serverUrl, apiKey: args.apiKey, project: args.project },
|
|
3212
|
+
process.env,
|
|
3213
|
+
[process.cwd()]
|
|
3214
|
+
);
|
|
3215
|
+
if (!connection) {
|
|
3216
|
+
throw new FlakeError(
|
|
3217
|
+
"no dashboard to read the plan from: pass --server-url or set PIWI_DASHBOARD_URL (or run it offline with --plan <file>)"
|
|
3218
|
+
);
|
|
3219
|
+
}
|
|
3220
|
+
const http = {
|
|
3221
|
+
serverUrl: connection.serverUrl,
|
|
3222
|
+
headers: connection.apiKey ? { "X-API-Key": connection.apiKey } : {}
|
|
3223
|
+
};
|
|
3224
|
+
try {
|
|
3225
|
+
const testCaseId = await resolveTestCase(http, args.test, connection.project, connection.apiKey);
|
|
3226
|
+
const params = new URLSearchParams({
|
|
3227
|
+
kind: args.verify ? "verify" : "reproduce",
|
|
3228
|
+
source: args.source ?? (process.env.CI ? "ci" : "cli"),
|
|
3229
|
+
machine: os2.hostname(),
|
|
3230
|
+
record: String(args.upload)
|
|
3231
|
+
});
|
|
3232
|
+
if (commit) params.set("commit", commit);
|
|
3233
|
+
if (args.runs) params.set("runs", String(args.runs));
|
|
3234
|
+
return { plan: await getJson(http, `/api/test-cases/${testCaseId}/flake-plan?${params}`), http };
|
|
3235
|
+
} catch (error) {
|
|
3236
|
+
if (error instanceof FlakeError) throw error;
|
|
3237
|
+
throw new FlakeError(
|
|
3238
|
+
`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.`
|
|
3239
|
+
);
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
function withRuns(plan, runs) {
|
|
3243
|
+
const set = (arm) => ({ ...arm, runs });
|
|
3244
|
+
return {
|
|
3245
|
+
...plan,
|
|
3246
|
+
control: set(plan.control),
|
|
3247
|
+
arms: plan.arms.map(set),
|
|
3248
|
+
combined: plan.combined && set(plan.combined)
|
|
3249
|
+
};
|
|
3250
|
+
}
|
|
3251
|
+
async function runFlake(argv, runner) {
|
|
3252
|
+
let args;
|
|
3253
|
+
try {
|
|
3254
|
+
args = parseFlakeArgs(argv);
|
|
3255
|
+
} catch (error) {
|
|
3256
|
+
console.error(`piwi flake: ${error.message}
|
|
3257
|
+
`);
|
|
3258
|
+
console.error(USAGE4);
|
|
3259
|
+
return EXIT_ERROR4;
|
|
3260
|
+
}
|
|
3261
|
+
if (args.help) {
|
|
3262
|
+
console.log(USAGE4);
|
|
3263
|
+
return 0;
|
|
3264
|
+
}
|
|
3265
|
+
const commit = gitHead();
|
|
3266
|
+
let plan;
|
|
3267
|
+
let http;
|
|
3268
|
+
try {
|
|
3269
|
+
({ plan, http } = await loadPlan(args, commit));
|
|
3270
|
+
} catch (error) {
|
|
3271
|
+
console.error(`piwi flake: ${error.message}`);
|
|
3272
|
+
return EXIT_ERROR4;
|
|
3273
|
+
}
|
|
3274
|
+
if (args.suspect != null && !plan.arms.some((a) => a.rank === args.suspect)) {
|
|
3275
|
+
console.error(`piwi flake: suspect ${args.suspect} has no arm (${plan.arms.length} arm(s) in the plan).`);
|
|
3276
|
+
return EXIT_ERROR4;
|
|
3277
|
+
}
|
|
3278
|
+
if (plan.kind === "reproduce" && plan.arms.length === 0) {
|
|
3279
|
+
if (!args.json) printHeader(plan, commit, null, args.budgetMs);
|
|
3280
|
+
console.error("piwi flake: no suspect with a condition to test, so there is nothing to run.");
|
|
3281
|
+
return 1;
|
|
3282
|
+
}
|
|
3283
|
+
const planned = [
|
|
3284
|
+
...args.bisect ? [] : [plan.control],
|
|
3285
|
+
...args.suspect != null ? plan.arms.filter((a) => a.rank === args.suspect) : plan.arms
|
|
3286
|
+
];
|
|
3287
|
+
if (args.all && plan.combined) planned.push(plan.combined);
|
|
3288
|
+
const estimate = estimateFlakeSessionMs(planned, plan.medianDurationMs);
|
|
3289
|
+
if (!args.json) {
|
|
3290
|
+
printHeader(plan, commit, estimate, args.budgetMs);
|
|
3291
|
+
}
|
|
3292
|
+
const dir = fs8.mkdtempSync(path11.join(os2.tmpdir(), "piwi-flake-"));
|
|
3293
|
+
const log = path11.join(dir, "playwright.log");
|
|
3294
|
+
let session;
|
|
3295
|
+
try {
|
|
3296
|
+
session = await runSession(runner ?? playwrightRunner(dir, log), plan, args, (result) => {
|
|
3297
|
+
if (!args.json && !result.skipped?.startsWith("bisect")) console.log(armLine(result, result.arm.stopAt));
|
|
3298
|
+
});
|
|
3299
|
+
} catch (error) {
|
|
3300
|
+
console.error(`piwi flake: ${error.message}`);
|
|
3301
|
+
if (fs8.existsSync(log)) {
|
|
3302
|
+
const tail = fs8.readFileSync(log, "utf8").trimEnd().split("\n").slice(-LOG_TAIL_LINES).join("\n");
|
|
3303
|
+
console.error(`
|
|
3304
|
+
Playwright's output (${log}):
|
|
3305
|
+
${tail}`);
|
|
3306
|
+
}
|
|
3307
|
+
if (args.bisect) {
|
|
3308
|
+
console.log("Bisect step: skip (this commit could not run the arm)");
|
|
3309
|
+
return EXIT_BISECT_SKIP;
|
|
3310
|
+
}
|
|
3311
|
+
return EXIT_ERROR4;
|
|
3312
|
+
}
|
|
3313
|
+
const { verdict, reproducingArm } = sessionVerdict(plan.kind, session.arms);
|
|
3314
|
+
const ran = [session.control, ...session.arms.filter((a) => !a.skipped)];
|
|
3315
|
+
let uploaded = false;
|
|
3316
|
+
if (args.upload && http && plan.experimentId) {
|
|
3317
|
+
try {
|
|
3318
|
+
const res = await fetch(`${http.serverUrl}/api/projects/${plan.projectId}/flake-lab/results`, {
|
|
3319
|
+
method: "POST",
|
|
3320
|
+
headers: { "Content-Type": "application/json", ...http.headers },
|
|
3321
|
+
body: JSON.stringify({
|
|
3322
|
+
experimentId: plan.experimentId,
|
|
3323
|
+
commit,
|
|
3324
|
+
playwrightProject: plan.test.project,
|
|
3325
|
+
arms: ran.map((r) => ({
|
|
3326
|
+
id: r.arm.id,
|
|
3327
|
+
label: r.arm.label,
|
|
3328
|
+
suspectId: r.arm.suspectId,
|
|
3329
|
+
conditions: r.arm.conditions,
|
|
3330
|
+
runs: r.count.runs,
|
|
3331
|
+
matchingFailures: r.count.matchingFailures,
|
|
3332
|
+
otherFailures: r.count.otherFailures,
|
|
3333
|
+
discardedRounds: r.count.discardedRounds,
|
|
3334
|
+
stoppedEarly: r.count.stoppedEarly
|
|
3335
|
+
}))
|
|
3336
|
+
})
|
|
3337
|
+
});
|
|
3338
|
+
if (!res.ok) {
|
|
3339
|
+
const body = await res.json().catch(() => ({}));
|
|
3340
|
+
throw new Error(body.message || `the dashboard returned ${res.status}`);
|
|
3341
|
+
}
|
|
3342
|
+
uploaded = true;
|
|
3343
|
+
} catch (error) {
|
|
3344
|
+
console.error(`piwi flake: could not save the results \u2014 ${error.message}`);
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
const code = args.bisect ? bisectExitCode(verdict) : exitCodeFor(verdict);
|
|
3348
|
+
const verifyCommand = `npx @piwitests/reporter flake verify ${plan.testCaseId}`;
|
|
3349
|
+
const report = {
|
|
3350
|
+
kind: plan.kind,
|
|
3351
|
+
testCaseId: plan.testCaseId,
|
|
3352
|
+
test: plan.displayTitle,
|
|
3353
|
+
experimentId: plan.experimentId,
|
|
3354
|
+
commit,
|
|
3355
|
+
failureCommit: plan.failureCommit,
|
|
3356
|
+
commitsDiffer: commitsDiffer(commit, plan.failureCommit),
|
|
3357
|
+
playwrightProject: plan.test.project,
|
|
3358
|
+
estimateMs: estimate,
|
|
3359
|
+
arms: [session.control, ...session.arms].map((r) => ({
|
|
3360
|
+
id: r.arm.id,
|
|
3361
|
+
label: r.arm.label,
|
|
3362
|
+
suspectId: r.arm.suspectId,
|
|
3363
|
+
rank: r.arm.rank,
|
|
3364
|
+
...r.count,
|
|
3365
|
+
verdict: r.verdict,
|
|
3366
|
+
pValue: r.pValue,
|
|
3367
|
+
skipped: r.skipped
|
|
3368
|
+
})),
|
|
3369
|
+
verdict,
|
|
3370
|
+
reproducingArm,
|
|
3371
|
+
uploaded,
|
|
3372
|
+
verifyCommand: plan.kind === "reproduce" && verdict === "reproduced" ? verifyCommand : null,
|
|
3373
|
+
exitCode: code
|
|
3374
|
+
};
|
|
3375
|
+
fs8.rmSync(dir, { recursive: true, force: true });
|
|
3376
|
+
if (args.json) {
|
|
3377
|
+
console.log(JSON.stringify(report, null, 2));
|
|
3378
|
+
return code;
|
|
3379
|
+
}
|
|
3380
|
+
const lines = [verdictSentence(report, session.arms, session.control)];
|
|
3381
|
+
if (args.bisect) lines.push(`Bisect step: ${bisectStepWord(code)}`);
|
|
3382
|
+
if (uploaded)
|
|
3383
|
+
lines.push(`Saved to the test's Flakiness tab.${report.verifyCommand ? ` After your fix: ${verifyCommand}` : ""}`);
|
|
3384
|
+
else if (report.verifyCommand) lines.push(`Not saved (--no-upload). After your fix: ${verifyCommand}`);
|
|
3385
|
+
console.log(lines.join("\n"));
|
|
3386
|
+
return code;
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
// src/cli/gate.ts
|
|
3390
|
+
var fs9 = __toESM(require("fs"));
|
|
2383
3391
|
|
|
2384
3392
|
// ../core/src/gate.ts
|
|
2385
3393
|
function formatGateResult(result) {
|
|
@@ -2402,8 +3410,8 @@ function formatGateResult(result) {
|
|
|
2402
3410
|
// src/cli/gate.ts
|
|
2403
3411
|
var EXIT_OK4 = 0;
|
|
2404
3412
|
var EXIT_VIOLATED = 1;
|
|
2405
|
-
var
|
|
2406
|
-
var
|
|
3413
|
+
var EXIT_ERROR5 = 2;
|
|
3414
|
+
var USAGE5 = `
|
|
2407
3415
|
piwi gate \u2014 fail a CI job on the dashboard's analysis of a run
|
|
2408
3416
|
|
|
2409
3417
|
Usage:
|
|
@@ -2453,9 +3461,9 @@ function readCount(argv, name) {
|
|
|
2453
3461
|
if (!Number.isFinite(n) || n < 0) throw new Error(`${name} expects a non-negative number, got "${raw}"`);
|
|
2454
3462
|
return Math.floor(n);
|
|
2455
3463
|
}
|
|
2456
|
-
function readRunIdFromFile(
|
|
3464
|
+
function readRunIdFromFile(path19) {
|
|
2457
3465
|
try {
|
|
2458
|
-
const parsed = JSON.parse(
|
|
3466
|
+
const parsed = JSON.parse(fs9.readFileSync(path19, "utf-8"));
|
|
2459
3467
|
const runId = Number(parsed.runId);
|
|
2460
3468
|
return Number.isFinite(runId) && runId > 0 ? runId : null;
|
|
2461
3469
|
} catch {
|
|
@@ -2510,7 +3518,7 @@ async function requestGate(args) {
|
|
|
2510
3518
|
}
|
|
2511
3519
|
async function runGate(argv, env = process.env) {
|
|
2512
3520
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
2513
|
-
console.log(
|
|
3521
|
+
console.log(USAGE5);
|
|
2514
3522
|
return EXIT_OK4;
|
|
2515
3523
|
}
|
|
2516
3524
|
let args;
|
|
@@ -2519,15 +3527,15 @@ async function runGate(argv, env = process.env) {
|
|
|
2519
3527
|
} catch (e) {
|
|
2520
3528
|
console.error(`piwi gate: ${e.message}
|
|
2521
3529
|
`);
|
|
2522
|
-
console.error(
|
|
2523
|
-
return
|
|
3530
|
+
console.error(USAGE5);
|
|
3531
|
+
return EXIT_ERROR5;
|
|
2524
3532
|
}
|
|
2525
3533
|
let result;
|
|
2526
3534
|
try {
|
|
2527
3535
|
result = await requestGate(args);
|
|
2528
3536
|
} catch (e) {
|
|
2529
3537
|
console.error(`piwi gate: ${e.message}`);
|
|
2530
|
-
return
|
|
3538
|
+
return EXIT_ERROR5;
|
|
2531
3539
|
}
|
|
2532
3540
|
if (argv.includes("--json")) {
|
|
2533
3541
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -2538,9 +3546,9 @@ async function runGate(argv, env = process.env) {
|
|
|
2538
3546
|
}
|
|
2539
3547
|
|
|
2540
3548
|
// src/cli/init.ts
|
|
2541
|
-
var
|
|
2542
|
-
var
|
|
2543
|
-
var
|
|
3549
|
+
var fs12 = __toESM(require("fs"));
|
|
3550
|
+
var path14 = __toESM(require("path"));
|
|
3551
|
+
var import_node_child_process5 = require("child_process");
|
|
2544
3552
|
|
|
2545
3553
|
// src/cli/report.ts
|
|
2546
3554
|
var STATUS_MARK = {
|
|
@@ -2557,8 +3565,8 @@ function formatStep(result) {
|
|
|
2557
3565
|
}
|
|
2558
3566
|
|
|
2559
3567
|
// src/cli/detect.ts
|
|
2560
|
-
var
|
|
2561
|
-
var
|
|
3568
|
+
var fs10 = __toESM(require("fs"));
|
|
3569
|
+
var path12 = __toESM(require("path"));
|
|
2562
3570
|
var CONFIG_NAMES = [
|
|
2563
3571
|
"playwright.config.ts",
|
|
2564
3572
|
"playwright.config.mts",
|
|
@@ -2576,9 +3584,9 @@ var LOCKFILES = [
|
|
|
2576
3584
|
];
|
|
2577
3585
|
var REPORTER_PACKAGE = "@piwitests/reporter";
|
|
2578
3586
|
function readPackageJson(root) {
|
|
2579
|
-
const pkgPath =
|
|
3587
|
+
const pkgPath = path12.join(root, "package.json");
|
|
2580
3588
|
try {
|
|
2581
|
-
return { pkg: JSON.parse(
|
|
3589
|
+
return { pkg: JSON.parse(fs10.readFileSync(pkgPath, "utf-8")), pkgPath };
|
|
2582
3590
|
} catch {
|
|
2583
3591
|
return { pkg: null, pkgPath: null };
|
|
2584
3592
|
}
|
|
@@ -2587,14 +3595,14 @@ function detectPackageManager(root, pkg) {
|
|
|
2587
3595
|
const declared = pkg?.packageManager?.split("@")[0];
|
|
2588
3596
|
if (declared === "pnpm" || declared === "yarn" || declared === "bun" || declared === "npm") return declared;
|
|
2589
3597
|
for (const [file, manager] of LOCKFILES) {
|
|
2590
|
-
if (
|
|
3598
|
+
if (fs10.existsSync(path12.join(root, file))) return manager;
|
|
2591
3599
|
}
|
|
2592
3600
|
return "npm";
|
|
2593
3601
|
}
|
|
2594
3602
|
function findConfig(root) {
|
|
2595
3603
|
for (const name of CONFIG_NAMES) {
|
|
2596
|
-
const candidate =
|
|
2597
|
-
if (
|
|
3604
|
+
const candidate = path12.join(root, name);
|
|
3605
|
+
if (fs10.existsSync(candidate)) return candidate;
|
|
2598
3606
|
}
|
|
2599
3607
|
return null;
|
|
2600
3608
|
}
|
|
@@ -2604,14 +3612,14 @@ function langOf(configPath) {
|
|
|
2604
3612
|
}
|
|
2605
3613
|
function suggestProjectName(root, pkg) {
|
|
2606
3614
|
const fromPkg = pkg?.name?.replace(/^@[^/]+\//, "").trim();
|
|
2607
|
-
return fromPkg ||
|
|
3615
|
+
return fromPkg || path12.basename(root) || "default-project";
|
|
2608
3616
|
}
|
|
2609
3617
|
function hasReporter(pkg) {
|
|
2610
3618
|
if (!pkg) return false;
|
|
2611
3619
|
return Boolean(pkg.dependencies?.[REPORTER_PACKAGE] || pkg.devDependencies?.[REPORTER_PACKAGE]);
|
|
2612
3620
|
}
|
|
2613
3621
|
function detectProject(root) {
|
|
2614
|
-
const absRoot =
|
|
3622
|
+
const absRoot = path12.resolve(root);
|
|
2615
3623
|
const { pkg, pkgPath } = readPackageJson(absRoot);
|
|
2616
3624
|
const configPath = findConfig(absRoot);
|
|
2617
3625
|
return {
|
|
@@ -2753,8 +3761,8 @@ function ensureGitignoreEntry(existing, entry = ".env") {
|
|
|
2753
3761
|
}
|
|
2754
3762
|
|
|
2755
3763
|
// src/cli/skills.ts
|
|
2756
|
-
var
|
|
2757
|
-
var
|
|
3764
|
+
var fs11 = __toESM(require("fs"));
|
|
3765
|
+
var path13 = __toESM(require("path"));
|
|
2758
3766
|
var SETUP_SKILL = "setup-piwi";
|
|
2759
3767
|
var WORKFLOW_SKILLS = [
|
|
2760
3768
|
"investigate-failure",
|
|
@@ -2765,16 +3773,16 @@ var WORKFLOW_SKILLS = [
|
|
|
2765
3773
|
"fix-a-reported-bug"
|
|
2766
3774
|
];
|
|
2767
3775
|
var ALL_SKILLS = [SETUP_SKILL, ...WORKFLOW_SKILLS];
|
|
2768
|
-
var DEFAULT_SKILLS_DIR =
|
|
3776
|
+
var DEFAULT_SKILLS_DIR = path13.join(".claude", "skills");
|
|
2769
3777
|
function findTemplatesDir(fromDir) {
|
|
2770
3778
|
let dir = fromDir;
|
|
2771
3779
|
for (let i = 0; i < 6; i++) {
|
|
2772
|
-
if (
|
|
2773
|
-
const parent =
|
|
3780
|
+
if (fs11.existsSync(path13.join(dir, "templates", "skills"))) return path13.join(dir, "templates");
|
|
3781
|
+
const parent = path13.dirname(dir);
|
|
2774
3782
|
if (parent === dir) break;
|
|
2775
3783
|
dir = parent;
|
|
2776
3784
|
}
|
|
2777
|
-
return
|
|
3785
|
+
return path13.resolve(fromDir, "..", "..", "templates");
|
|
2778
3786
|
}
|
|
2779
3787
|
function readFrontMatter(markdown) {
|
|
2780
3788
|
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
|
|
@@ -2787,14 +3795,14 @@ function readFrontMatter(markdown) {
|
|
|
2787
3795
|
return out;
|
|
2788
3796
|
}
|
|
2789
3797
|
function templatePath(templatesDir, slug) {
|
|
2790
|
-
return
|
|
3798
|
+
return path13.join(templatesDir, "skills", slug, "SKILL.md");
|
|
2791
3799
|
}
|
|
2792
3800
|
function listSkills(templatesDir, slugs = ALL_SKILLS) {
|
|
2793
3801
|
const infos = [];
|
|
2794
3802
|
for (const slug of slugs) {
|
|
2795
3803
|
const file = templatePath(templatesDir, slug);
|
|
2796
|
-
if (!
|
|
2797
|
-
const front = readFrontMatter(
|
|
3804
|
+
if (!fs11.existsSync(file)) continue;
|
|
3805
|
+
const front = readFrontMatter(fs11.readFileSync(file, "utf-8"));
|
|
2798
3806
|
infos.push({ slug, name: front.name ?? slug, description: front.description ?? "" });
|
|
2799
3807
|
}
|
|
2800
3808
|
return infos;
|
|
@@ -2804,16 +3812,16 @@ function installSkills(opts) {
|
|
|
2804
3812
|
for (const slug of opts.slugs) {
|
|
2805
3813
|
const step = `skill:${slug}`;
|
|
2806
3814
|
const source = templatePath(opts.templatesDir, slug);
|
|
2807
|
-
const relDest =
|
|
2808
|
-
const dest =
|
|
2809
|
-
if (!
|
|
3815
|
+
const relDest = path13.join(opts.skillsDir, slug, "SKILL.md");
|
|
3816
|
+
const dest = path13.join(opts.root, relDest);
|
|
3817
|
+
if (!fs11.existsSync(source)) {
|
|
2810
3818
|
results.push({ step, status: "error", detail: `no template found for "${slug}"` });
|
|
2811
3819
|
continue;
|
|
2812
3820
|
}
|
|
2813
|
-
const contents =
|
|
2814
|
-
const exists =
|
|
3821
|
+
const contents = fs11.readFileSync(source, "utf-8");
|
|
3822
|
+
const exists = fs11.existsSync(dest);
|
|
2815
3823
|
if (exists && !opts.force) {
|
|
2816
|
-
const identical =
|
|
3824
|
+
const identical = fs11.readFileSync(dest, "utf-8") === contents;
|
|
2817
3825
|
results.push({
|
|
2818
3826
|
step,
|
|
2819
3827
|
file: relDest,
|
|
@@ -2823,14 +3831,14 @@ function installSkills(opts) {
|
|
|
2823
3831
|
continue;
|
|
2824
3832
|
}
|
|
2825
3833
|
if (!opts.dryRun) {
|
|
2826
|
-
|
|
2827
|
-
|
|
3834
|
+
fs11.mkdirSync(path13.dirname(dest), { recursive: true });
|
|
3835
|
+
fs11.writeFileSync(dest, contents);
|
|
2828
3836
|
}
|
|
2829
3837
|
results.push({ step, file: relDest, status: exists ? "updated" : "created", detail: "installed skill" });
|
|
2830
3838
|
}
|
|
2831
3839
|
return results;
|
|
2832
3840
|
}
|
|
2833
|
-
var
|
|
3841
|
+
var USAGE6 = `
|
|
2834
3842
|
piwi skills \u2014 install the Piwi agent skills into this project
|
|
2835
3843
|
|
|
2836
3844
|
Usage:
|
|
@@ -2869,7 +3877,7 @@ function positionalSlugs(argv) {
|
|
|
2869
3877
|
function runSkills(argv, templatesDir, cwd = process.cwd()) {
|
|
2870
3878
|
const [sub, ...rest] = argv;
|
|
2871
3879
|
if (sub === void 0 || sub === "-h" || sub === "--help") {
|
|
2872
|
-
console.log(
|
|
3880
|
+
console.log(USAGE6);
|
|
2873
3881
|
return 0;
|
|
2874
3882
|
}
|
|
2875
3883
|
if (sub === "list") {
|
|
@@ -2890,12 +3898,12 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
|
|
|
2890
3898
|
if (unknown.length) {
|
|
2891
3899
|
console.error(`piwi skills: unknown skill(s): ${unknown.join(", ")}
|
|
2892
3900
|
`);
|
|
2893
|
-
console.error(
|
|
3901
|
+
console.error(USAGE6);
|
|
2894
3902
|
return 2;
|
|
2895
3903
|
}
|
|
2896
3904
|
const results = installSkills({
|
|
2897
3905
|
templatesDir,
|
|
2898
|
-
root:
|
|
3906
|
+
root: path13.resolve(readOption5(rest, "--cwd") ?? cwd),
|
|
2899
3907
|
skillsDir: readOption5(rest, "--dir") ?? DEFAULT_SKILLS_DIR,
|
|
2900
3908
|
slugs: requested.length ? requested : ALL_SKILLS,
|
|
2901
3909
|
force: rest.includes("--force"),
|
|
@@ -2911,13 +3919,13 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
|
|
|
2911
3919
|
}
|
|
2912
3920
|
console.error(`piwi skills: unknown command "${sub}"
|
|
2913
3921
|
`);
|
|
2914
|
-
console.error(
|
|
3922
|
+
console.error(USAGE6);
|
|
2915
3923
|
return 2;
|
|
2916
3924
|
}
|
|
2917
3925
|
|
|
2918
3926
|
// src/cli/init.ts
|
|
2919
3927
|
var DEFAULT_SERVER_URL = "http://localhost:3000";
|
|
2920
|
-
var
|
|
3928
|
+
var USAGE7 = `
|
|
2921
3929
|
piwi init \u2014 wire a Playwright project up to a Piwi Dashboard
|
|
2922
3930
|
|
|
2923
3931
|
Usage:
|
|
@@ -2966,7 +3974,7 @@ function resolveSkillSlugs(raw) {
|
|
|
2966
3974
|
return raw.split(",").map((slug) => slug.trim()).filter(Boolean);
|
|
2967
3975
|
}
|
|
2968
3976
|
function parseInitArgs(argv, env, cwd) {
|
|
2969
|
-
const root =
|
|
3977
|
+
const root = path14.resolve(readOption6(argv, "--cwd") ?? cwd);
|
|
2970
3978
|
const detected = detectProject(root);
|
|
2971
3979
|
return {
|
|
2972
3980
|
root,
|
|
@@ -2985,7 +3993,7 @@ function parseInitArgs(argv, env, cwd) {
|
|
|
2985
3993
|
}
|
|
2986
3994
|
function readFileOr(file, fallback) {
|
|
2987
3995
|
try {
|
|
2988
|
-
return
|
|
3996
|
+
return fs12.readFileSync(file, "utf-8");
|
|
2989
3997
|
} catch {
|
|
2990
3998
|
return fallback;
|
|
2991
3999
|
}
|
|
@@ -3015,7 +4023,7 @@ function stepInstallReporter(project, opts) {
|
|
|
3015
4023
|
if (opts.dryRun || !opts.install)
|
|
3016
4024
|
return { step, status: "manual", detail: `Run \`${installCommand(project.packageManager)}\`` };
|
|
3017
4025
|
const [command, args] = installArgv(project.packageManager);
|
|
3018
|
-
const result = (0,
|
|
4026
|
+
const result = (0, import_node_child_process5.spawnSync)(command, args, { cwd: opts.root, stdio: "inherit" });
|
|
3019
4027
|
if (result.status === 0) return { step, status: "updated", detail: `Installed ${REPORTER_PACKAGE}` };
|
|
3020
4028
|
return {
|
|
3021
4029
|
step,
|
|
@@ -3031,19 +4039,19 @@ function stepConfig(project, opts) {
|
|
|
3031
4039
|
status: "manual",
|
|
3032
4040
|
detail: "No playwright.config found \u2014 create one, then re-run `npx @piwitests/reporter init`"
|
|
3033
4041
|
};
|
|
3034
|
-
const rel =
|
|
4042
|
+
const rel = path14.relative(project.root, project.configPath) || path14.basename(project.configPath);
|
|
3035
4043
|
const edit = wrapPlaywrightConfig(readFileOr(project.configPath, ""), {
|
|
3036
4044
|
serverUrl: opts.serverUrl,
|
|
3037
4045
|
projectName: opts.projectName
|
|
3038
4046
|
});
|
|
3039
|
-
if (edit.status === "updated" && !opts.dryRun)
|
|
4047
|
+
if (edit.status === "updated" && !opts.dryRun) fs12.writeFileSync(project.configPath, edit.text);
|
|
3040
4048
|
return { step, file: rel, status: edit.status, detail: edit.detail };
|
|
3041
4049
|
}
|
|
3042
4050
|
function stepFixtures(project, opts) {
|
|
3043
4051
|
const step = "fixtures";
|
|
3044
|
-
const rel =
|
|
3045
|
-
const dest =
|
|
3046
|
-
if (
|
|
4052
|
+
const rel = path14.join("tests", project.configLang === "js" ? "fixtures.js" : "fixtures.ts");
|
|
4053
|
+
const dest = path14.join(project.root, rel);
|
|
4054
|
+
if (fs12.existsSync(dest)) {
|
|
3047
4055
|
const current = readFileOr(dest, "");
|
|
3048
4056
|
if (/piwiFixtures/.test(current))
|
|
3049
4057
|
return { step, file: rel, status: "already", detail: "capture fixtures already set up" };
|
|
@@ -3055,8 +4063,8 @@ function stepFixtures(project, opts) {
|
|
|
3055
4063
|
};
|
|
3056
4064
|
}
|
|
3057
4065
|
if (!opts.dryRun) {
|
|
3058
|
-
|
|
3059
|
-
|
|
4066
|
+
fs12.mkdirSync(path14.dirname(dest), { recursive: true });
|
|
4067
|
+
fs12.writeFileSync(dest, fixturesContents());
|
|
3060
4068
|
}
|
|
3061
4069
|
return {
|
|
3062
4070
|
step,
|
|
@@ -3067,12 +4075,12 @@ function stepFixtures(project, opts) {
|
|
|
3067
4075
|
}
|
|
3068
4076
|
function stepEnv(project, opts) {
|
|
3069
4077
|
const results = [];
|
|
3070
|
-
const examplePath =
|
|
4078
|
+
const examplePath = path14.join(project.root, ".env.example");
|
|
3071
4079
|
const example = upsertEnvKeys(readFileOr(examplePath, ""), [
|
|
3072
4080
|
["PIWI_DASHBOARD_URL", opts.serverUrl],
|
|
3073
4081
|
["PIWI_API_KEY", ""]
|
|
3074
4082
|
]);
|
|
3075
|
-
if (example.added.length && !opts.dryRun)
|
|
4083
|
+
if (example.added.length && !opts.dryRun) fs12.writeFileSync(examplePath, example.text);
|
|
3076
4084
|
results.push({
|
|
3077
4085
|
step: "env",
|
|
3078
4086
|
file: ".env.example",
|
|
@@ -3080,21 +4088,21 @@ function stepEnv(project, opts) {
|
|
|
3080
4088
|
detail: example.added.length ? `Recorded ${example.added.join(", ")}` : "connection template already present"
|
|
3081
4089
|
});
|
|
3082
4090
|
if (opts.apiKey) {
|
|
3083
|
-
const envPath =
|
|
4091
|
+
const envPath = path14.join(project.root, ".env");
|
|
3084
4092
|
const env = upsertEnvKeys(readFileOr(envPath, ""), [
|
|
3085
4093
|
["PIWI_DASHBOARD_URL", opts.serverUrl],
|
|
3086
4094
|
["PIWI_API_KEY", opts.apiKey]
|
|
3087
4095
|
]);
|
|
3088
|
-
if (env.added.length && !opts.dryRun)
|
|
4096
|
+
if (env.added.length && !opts.dryRun) fs12.writeFileSync(envPath, env.text);
|
|
3089
4097
|
results.push({
|
|
3090
4098
|
step: "env",
|
|
3091
4099
|
file: ".env",
|
|
3092
4100
|
status: env.added.length ? "updated" : "already",
|
|
3093
4101
|
detail: env.added.length ? `Wrote ${env.added.join(", ")} (keep .env out of git)` : "already set"
|
|
3094
4102
|
});
|
|
3095
|
-
const gitignorePath =
|
|
4103
|
+
const gitignorePath = path14.join(project.root, ".gitignore");
|
|
3096
4104
|
const gitignore = ensureGitignoreEntry(readFileOr(gitignorePath, ""));
|
|
3097
|
-
if (gitignore.added && !opts.dryRun)
|
|
4105
|
+
if (gitignore.added && !opts.dryRun) fs12.writeFileSync(gitignorePath, gitignore.text);
|
|
3098
4106
|
results.push({
|
|
3099
4107
|
step: "gitignore",
|
|
3100
4108
|
file: ".gitignore",
|
|
@@ -3108,7 +4116,7 @@ function nextSteps(opts, steps) {
|
|
|
3108
4116
|
const out = [];
|
|
3109
4117
|
if (!opts.apiKey) {
|
|
3110
4118
|
out.push(
|
|
3111
|
-
"If your dashboard has authentication on, create an API key (Settings \u2192
|
|
4119
|
+
"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
4120
|
);
|
|
3113
4121
|
}
|
|
3114
4122
|
if (steps.some((s) => s.status === "manual")) {
|
|
@@ -3123,7 +4131,7 @@ function nextSteps(opts, steps) {
|
|
|
3123
4131
|
}
|
|
3124
4132
|
async function runInit(argv, env = process.env, cwd = process.cwd()) {
|
|
3125
4133
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
3126
|
-
console.log(
|
|
4134
|
+
console.log(USAGE7);
|
|
3127
4135
|
return 0;
|
|
3128
4136
|
}
|
|
3129
4137
|
const opts = parseInitArgs(argv, env, cwd);
|
|
@@ -3156,7 +4164,7 @@ async function runInit(argv, env = process.env, cwd = process.cwd()) {
|
|
|
3156
4164
|
project: {
|
|
3157
4165
|
root: project.root,
|
|
3158
4166
|
packageManager: project.packageManager,
|
|
3159
|
-
configPath: project.configPath ?
|
|
4167
|
+
configPath: project.configPath ? path14.relative(project.root, project.configPath) : null,
|
|
3160
4168
|
projectName: opts.projectName,
|
|
3161
4169
|
serverUrl: opts.serverUrl
|
|
3162
4170
|
},
|
|
@@ -3180,15 +4188,15 @@ Piwi setup${opts.dryRun ? " (dry run \u2014 nothing written)" : ""} for "${opts.
|
|
|
3180
4188
|
}
|
|
3181
4189
|
|
|
3182
4190
|
// src/cli/select.ts
|
|
3183
|
-
var
|
|
3184
|
-
var
|
|
3185
|
-
var
|
|
3186
|
-
var
|
|
4191
|
+
var fs14 = __toESM(require("fs"));
|
|
4192
|
+
var path16 = __toESM(require("path"));
|
|
4193
|
+
var import_node_child_process6 = require("child_process");
|
|
4194
|
+
var import_node_module4 = require("module");
|
|
3187
4195
|
|
|
3188
4196
|
// src/cli/add-reporter.ts
|
|
3189
|
-
var
|
|
3190
|
-
var
|
|
3191
|
-
var
|
|
4197
|
+
var fs13 = __toESM(require("fs"));
|
|
4198
|
+
var path15 = __toESM(require("path"));
|
|
4199
|
+
var import_node_module3 = require("module");
|
|
3192
4200
|
var REPORTER_PACKAGE2 = "@piwitests/reporter";
|
|
3193
4201
|
var ADD_REPORTER_FLAG = "--add-reporter";
|
|
3194
4202
|
var ADD_REPORTER_MIN = { major: 1, minor: 63 };
|
|
@@ -3215,14 +4223,14 @@ function decideAddReporter(configSource, playwrightVersion) {
|
|
|
3215
4223
|
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
4224
|
};
|
|
3217
4225
|
}
|
|
3218
|
-
function resolveConfigPath(cwd,
|
|
3219
|
-
for (let i = 0; i <
|
|
3220
|
-
const arg =
|
|
4226
|
+
function resolveConfigPath(cwd, playwrightArgs2) {
|
|
4227
|
+
for (let i = 0; i < playwrightArgs2.length; i++) {
|
|
4228
|
+
const arg = playwrightArgs2[i];
|
|
3221
4229
|
if (arg === "--config" || arg === "-c") {
|
|
3222
|
-
const next =
|
|
3223
|
-
if (next) return
|
|
4230
|
+
const next = playwrightArgs2[i + 1];
|
|
4231
|
+
if (next) return path15.resolve(cwd, next);
|
|
3224
4232
|
} else if (arg.startsWith("--config=")) {
|
|
3225
|
-
return
|
|
4233
|
+
return path15.resolve(cwd, arg.slice("--config=".length));
|
|
3226
4234
|
}
|
|
3227
4235
|
}
|
|
3228
4236
|
return detectProject(cwd).configPath;
|
|
@@ -3230,14 +4238,14 @@ function resolveConfigPath(cwd, playwrightArgs) {
|
|
|
3230
4238
|
function readConfigSource(configPath) {
|
|
3231
4239
|
if (!configPath) return null;
|
|
3232
4240
|
try {
|
|
3233
|
-
return
|
|
4241
|
+
return fs13.readFileSync(configPath, "utf-8");
|
|
3234
4242
|
} catch {
|
|
3235
4243
|
return null;
|
|
3236
4244
|
}
|
|
3237
4245
|
}
|
|
3238
4246
|
function readInstalledPlaywrightVersion(cwd) {
|
|
3239
4247
|
try {
|
|
3240
|
-
const require2 = (0,
|
|
4248
|
+
const require2 = (0, import_node_module3.createRequire)(path15.join(cwd, "noop.js"));
|
|
3241
4249
|
for (const id of ["@playwright/test/package.json", "playwright/package.json"]) {
|
|
3242
4250
|
try {
|
|
3243
4251
|
return require2(id).version ?? null;
|
|
@@ -3248,15 +4256,15 @@ function readInstalledPlaywrightVersion(cwd) {
|
|
|
3248
4256
|
}
|
|
3249
4257
|
return null;
|
|
3250
4258
|
}
|
|
3251
|
-
function computeAddReporterArgs(cwd,
|
|
3252
|
-
const source = readConfigSource(resolveConfigPath(cwd,
|
|
4259
|
+
function computeAddReporterArgs(cwd, playwrightArgs2) {
|
|
4260
|
+
const source = readConfigSource(resolveConfigPath(cwd, playwrightArgs2));
|
|
3253
4261
|
return decideAddReporter(source, readInstalledPlaywrightVersion(cwd));
|
|
3254
4262
|
}
|
|
3255
4263
|
|
|
3256
4264
|
// src/cli/select.ts
|
|
3257
4265
|
var EXIT_OK5 = 0;
|
|
3258
|
-
var
|
|
3259
|
-
var
|
|
4266
|
+
var EXIT_ERROR6 = 2;
|
|
4267
|
+
var USAGE8 = `
|
|
3260
4268
|
piwi select / piwi run \u2014 run a saved selection of tests
|
|
3261
4269
|
|
|
3262
4270
|
Usage:
|
|
@@ -3314,7 +4322,7 @@ function findKey(argv) {
|
|
|
3314
4322
|
}
|
|
3315
4323
|
return void 0;
|
|
3316
4324
|
}
|
|
3317
|
-
function
|
|
4325
|
+
function parseDuration2(raw) {
|
|
3318
4326
|
const match = raw.trim().match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/);
|
|
3319
4327
|
if (!match) return null;
|
|
3320
4328
|
const value = Number(match[1]);
|
|
@@ -3334,7 +4342,7 @@ function parseSelectArgs(argv, env) {
|
|
|
3334
4342
|
const budgetRaw = readOption7(own, "--budget");
|
|
3335
4343
|
let budgetMs = null;
|
|
3336
4344
|
if (budgetRaw !== void 0) {
|
|
3337
|
-
budgetMs =
|
|
4345
|
+
budgetMs = parseDuration2(budgetRaw);
|
|
3338
4346
|
if (budgetMs === null) throw new Error(`--budget expects a duration like 5m or 90s, got "${budgetRaw}"`);
|
|
3339
4347
|
}
|
|
3340
4348
|
const shard = readOption7(own, "--shard") ?? null;
|
|
@@ -3357,51 +4365,52 @@ function parseSelectArgs(argv, env) {
|
|
|
3357
4365
|
extra
|
|
3358
4366
|
};
|
|
3359
4367
|
}
|
|
3360
|
-
var CACHE_FILE =
|
|
3361
|
-
function cacheKey(
|
|
3362
|
-
return `${
|
|
4368
|
+
var CACHE_FILE = path16.join(".piwi", "selection-cache.json");
|
|
4369
|
+
function cacheKey(args) {
|
|
4370
|
+
return `${args.project.toLowerCase()}:${args.key}:${args.format}:${args.budgetMs ?? 0}:${args.shard ?? ""}:${args.order ?? ""}`;
|
|
3363
4371
|
}
|
|
3364
|
-
function readCache(
|
|
4372
|
+
function readCache(args) {
|
|
3365
4373
|
try {
|
|
3366
|
-
const store = JSON.parse(
|
|
3367
|
-
return store[cacheKey(
|
|
4374
|
+
const store = JSON.parse(fs14.readFileSync(CACHE_FILE, "utf-8"));
|
|
4375
|
+
return store[cacheKey(args)] ?? null;
|
|
3368
4376
|
} catch {
|
|
3369
4377
|
return null;
|
|
3370
4378
|
}
|
|
3371
4379
|
}
|
|
3372
|
-
function writeCache(
|
|
4380
|
+
function writeCache(args, resolution) {
|
|
3373
4381
|
try {
|
|
3374
4382
|
let store = {};
|
|
3375
4383
|
try {
|
|
3376
|
-
store = JSON.parse(
|
|
4384
|
+
store = JSON.parse(fs14.readFileSync(CACHE_FILE, "utf-8"));
|
|
3377
4385
|
} catch {
|
|
3378
4386
|
}
|
|
3379
|
-
store[cacheKey(
|
|
3380
|
-
|
|
3381
|
-
|
|
4387
|
+
store[cacheKey(args)] = resolution;
|
|
4388
|
+
fs14.mkdirSync(path16.dirname(CACHE_FILE), { recursive: true });
|
|
4389
|
+
fs14.writeFileSync(CACHE_FILE, JSON.stringify(store, null, 2));
|
|
3382
4390
|
} catch {
|
|
3383
4391
|
}
|
|
3384
4392
|
}
|
|
3385
|
-
async function resolveWithCache(args
|
|
4393
|
+
async function resolveWithCache(args) {
|
|
3386
4394
|
try {
|
|
4395
|
+
const projectId = await resolveProjectId(args);
|
|
3387
4396
|
const resolution = await fetchResolution(args, projectId);
|
|
3388
|
-
writeCache(
|
|
4397
|
+
writeCache(args, resolution);
|
|
3389
4398
|
return { resolution, fromCache: false };
|
|
3390
4399
|
} catch (e) {
|
|
3391
4400
|
if (args.strict) throw e;
|
|
3392
|
-
const cached = readCache(
|
|
4401
|
+
const cached = readCache(args);
|
|
3393
4402
|
if (cached) {
|
|
3394
4403
|
console.error(`piwi: dashboard unreachable, using cached resolution \u2014 ${e.message}`);
|
|
3395
4404
|
return { resolution: cached, fromCache: true };
|
|
3396
4405
|
}
|
|
3397
|
-
return
|
|
4406
|
+
return { error: e };
|
|
3398
4407
|
}
|
|
3399
4408
|
}
|
|
3400
4409
|
function printWarnings(resolution) {
|
|
3401
4410
|
for (const w of resolution.warnings) console.error(`piwi: warning [${w.code}] ${w.message}`);
|
|
3402
4411
|
}
|
|
3403
4412
|
function gitChangedFiles(base) {
|
|
3404
|
-
const out = (0,
|
|
4413
|
+
const out = (0, import_node_child_process6.execFileSync)("git", ["diff", "--name-only", base], {
|
|
3405
4414
|
encoding: "utf-8",
|
|
3406
4415
|
stdio: ["ignore", "pipe", "ignore"]
|
|
3407
4416
|
});
|
|
@@ -3422,7 +4431,7 @@ async function runSelectImpact(args) {
|
|
|
3422
4431
|
impact = await loadImpact(args, projectId);
|
|
3423
4432
|
} catch (e) {
|
|
3424
4433
|
console.error(`piwi select: ${e.message}`);
|
|
3425
|
-
return
|
|
4434
|
+
return EXIT_ERROR6;
|
|
3426
4435
|
}
|
|
3427
4436
|
printWarnings(impact);
|
|
3428
4437
|
console.error(
|
|
@@ -3437,7 +4446,7 @@ async function runSelectImpact(args) {
|
|
|
3437
4446
|
}
|
|
3438
4447
|
async function runSelect(argv, env = process.env) {
|
|
3439
4448
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
3440
|
-
console.log(
|
|
4449
|
+
console.log(USAGE8);
|
|
3441
4450
|
return EXIT_OK5;
|
|
3442
4451
|
}
|
|
3443
4452
|
let args;
|
|
@@ -3446,13 +4455,13 @@ async function runSelect(argv, env = process.env) {
|
|
|
3446
4455
|
} catch (e) {
|
|
3447
4456
|
console.error(`piwi select: ${e.message}
|
|
3448
4457
|
`);
|
|
3449
|
-
console.error(
|
|
3450
|
-
return
|
|
4458
|
+
console.error(USAGE8);
|
|
4459
|
+
return EXIT_ERROR6;
|
|
3451
4460
|
}
|
|
3452
4461
|
if (args.key === IMPACT_KEY) {
|
|
3453
4462
|
if (!args.base) {
|
|
3454
4463
|
console.error("piwi select: impact needs a base ref \u2014 pass --base <ref> (e.g. --base origin/main)");
|
|
3455
|
-
return
|
|
4464
|
+
return EXIT_ERROR6;
|
|
3456
4465
|
}
|
|
3457
4466
|
return runSelectImpact(args);
|
|
3458
4467
|
}
|
|
@@ -3462,19 +4471,19 @@ async function runSelect(argv, env = process.env) {
|
|
|
3462
4471
|
resolution = await fetchResolution(args, projectId);
|
|
3463
4472
|
} catch (e) {
|
|
3464
4473
|
console.error(`piwi select: ${e.message}`);
|
|
3465
|
-
return
|
|
4474
|
+
return EXIT_ERROR6;
|
|
3466
4475
|
}
|
|
3467
4476
|
printWarnings(resolution);
|
|
3468
4477
|
if (resolution.estimate.count === 0) {
|
|
3469
4478
|
console.error(`piwi select: "${args.key}" resolved to 0 tests`);
|
|
3470
|
-
return
|
|
4479
|
+
return EXIT_ERROR6;
|
|
3471
4480
|
}
|
|
3472
4481
|
if (args.json) console.log(JSON.stringify(resolution, null, 2));
|
|
3473
4482
|
else console.log(resolution.materialization.args.join(" "));
|
|
3474
4483
|
return EXIT_OK5;
|
|
3475
4484
|
}
|
|
3476
|
-
function
|
|
3477
|
-
const require2 = (0,
|
|
4485
|
+
function resolvePlaywrightCli3() {
|
|
4486
|
+
const require2 = (0, import_node_module4.createRequire)(path16.join(process.cwd(), "noop.js"));
|
|
3478
4487
|
for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
|
|
3479
4488
|
try {
|
|
3480
4489
|
return require2.resolve(id);
|
|
@@ -3483,24 +4492,27 @@ function resolvePlaywrightCli2() {
|
|
|
3483
4492
|
}
|
|
3484
4493
|
return null;
|
|
3485
4494
|
}
|
|
3486
|
-
function
|
|
3487
|
-
const cli =
|
|
3488
|
-
const child = cli ? (0,
|
|
4495
|
+
function spawnPlaywright2(pkgRunner, playwrightArgs2, env) {
|
|
4496
|
+
const cli = resolvePlaywrightCli3();
|
|
4497
|
+
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
4498
|
stdio: "inherit",
|
|
3490
4499
|
env
|
|
3491
4500
|
});
|
|
3492
4501
|
return new Promise((resolve8) => {
|
|
3493
4502
|
child.on("error", (err) => {
|
|
3494
4503
|
console.error(`piwi run: could not start Playwright \u2014 ${err.message}`);
|
|
3495
|
-
resolve8(
|
|
4504
|
+
resolve8(EXIT_ERROR6);
|
|
3496
4505
|
});
|
|
3497
|
-
child.on("exit", (code) => resolve8(code ??
|
|
4506
|
+
child.on("exit", (code) => resolve8(code ?? EXIT_ERROR6));
|
|
3498
4507
|
});
|
|
3499
4508
|
}
|
|
3500
|
-
function spawnPlaywrightForRun(pkgRunner,
|
|
3501
|
-
const decision = computeAddReporterArgs(process.cwd(),
|
|
4509
|
+
function spawnPlaywrightForRun(pkgRunner, playwrightArgs2, env) {
|
|
4510
|
+
const decision = computeAddReporterArgs(process.cwd(), playwrightArgs2);
|
|
3502
4511
|
if (decision.log) console.error(decision.log);
|
|
3503
|
-
return
|
|
4512
|
+
return spawnPlaywright2(pkgRunner, [...decision.args, ...playwrightArgs2], env);
|
|
4513
|
+
}
|
|
4514
|
+
function shardEnv(args) {
|
|
4515
|
+
return args.shard ? { [PIWI_SHARD_ENV]: args.shard } : {};
|
|
3504
4516
|
}
|
|
3505
4517
|
async function runRunImpact(args, env) {
|
|
3506
4518
|
let projectId;
|
|
@@ -3509,7 +4521,7 @@ async function runRunImpact(args, env) {
|
|
|
3509
4521
|
} catch (e) {
|
|
3510
4522
|
if (args.strict) {
|
|
3511
4523
|
console.error(`piwi run: ${e.message}`);
|
|
3512
|
-
return
|
|
4524
|
+
return EXIT_ERROR6;
|
|
3513
4525
|
}
|
|
3514
4526
|
console.error(`piwi run: ${e.message} \u2014 running the full suite`);
|
|
3515
4527
|
return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
|
|
@@ -3520,7 +4532,7 @@ async function runRunImpact(args, env) {
|
|
|
3520
4532
|
} catch (e) {
|
|
3521
4533
|
if (args.strict) {
|
|
3522
4534
|
console.error(`piwi run: ${e.message}`);
|
|
3523
|
-
return
|
|
4535
|
+
return EXIT_ERROR6;
|
|
3524
4536
|
}
|
|
3525
4537
|
console.error(`piwi run: ${e.message} \u2014 running the full suite`);
|
|
3526
4538
|
return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
|
|
@@ -3541,14 +4553,15 @@ async function runRunImpact(args, env) {
|
|
|
3541
4553
|
PIWI_SELECTION: IMPACT_KEY,
|
|
3542
4554
|
PIWI_SELECTION_VERSION: "0",
|
|
3543
4555
|
PIWI_SELECTION_HASH: impact.resolvedHash,
|
|
3544
|
-
PIWI_SELECTION_COUNT: String(impact.estimate.count)
|
|
4556
|
+
PIWI_SELECTION_COUNT: String(impact.estimate.count),
|
|
4557
|
+
...shardEnv(args)
|
|
3545
4558
|
};
|
|
3546
4559
|
console.error(`piwi run: impact \u2192 ${impact.estimate.count} test(s)`);
|
|
3547
4560
|
return spawnPlaywrightForRun(args.pkgRunner, [...impact.materialization.args, ...args.extra], runEnv);
|
|
3548
4561
|
}
|
|
3549
4562
|
async function runRun(argv, env = process.env) {
|
|
3550
4563
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
3551
|
-
console.log(
|
|
4564
|
+
console.log(USAGE8);
|
|
3552
4565
|
return EXIT_OK5;
|
|
3553
4566
|
}
|
|
3554
4567
|
let args;
|
|
@@ -3557,54 +4570,44 @@ async function runRun(argv, env = process.env) {
|
|
|
3557
4570
|
} catch (e) {
|
|
3558
4571
|
console.error(`piwi run: ${e.message}
|
|
3559
4572
|
`);
|
|
3560
|
-
console.error(
|
|
3561
|
-
return
|
|
4573
|
+
console.error(USAGE8);
|
|
4574
|
+
return EXIT_ERROR6;
|
|
3562
4575
|
}
|
|
3563
4576
|
if (args.format === "json") {
|
|
3564
4577
|
console.error("piwi run: --format json cannot be run; use args, grep or files");
|
|
3565
|
-
return
|
|
4578
|
+
return EXIT_ERROR6;
|
|
3566
4579
|
}
|
|
3567
4580
|
if (args.key === IMPACT_KEY) {
|
|
3568
4581
|
if (!args.base) {
|
|
3569
4582
|
console.error("piwi run: impact needs a base ref \u2014 pass --base <ref> (e.g. --base origin/main)");
|
|
3570
|
-
return
|
|
4583
|
+
return EXIT_ERROR6;
|
|
3571
4584
|
}
|
|
3572
4585
|
return runRunImpact(args, env);
|
|
3573
4586
|
}
|
|
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
4587
|
let outcome;
|
|
3586
4588
|
try {
|
|
3587
|
-
outcome = await resolveWithCache(args
|
|
4589
|
+
outcome = await resolveWithCache(args);
|
|
3588
4590
|
} catch (e) {
|
|
3589
4591
|
console.error(`piwi run: ${e.message}`);
|
|
3590
|
-
return
|
|
4592
|
+
return EXIT_ERROR6;
|
|
3591
4593
|
}
|
|
3592
|
-
if (
|
|
3593
|
-
console.error(
|
|
4594
|
+
if ("error" in outcome) {
|
|
4595
|
+
console.error(`piwi run: ${outcome.error.message} \u2014 no cached resolution, running the full suite`);
|
|
3594
4596
|
return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
|
|
3595
4597
|
}
|
|
3596
4598
|
const { resolution } = outcome;
|
|
3597
4599
|
printWarnings(resolution);
|
|
3598
4600
|
if (resolution.estimate.count === 0) {
|
|
3599
4601
|
console.error(`piwi run: "${args.key}" resolved to 0 tests`);
|
|
3600
|
-
return
|
|
4602
|
+
return EXIT_ERROR6;
|
|
3601
4603
|
}
|
|
3602
4604
|
const runEnv = {
|
|
3603
4605
|
...env,
|
|
3604
4606
|
PIWI_SELECTION: resolution.key ?? args.key,
|
|
3605
4607
|
PIWI_SELECTION_VERSION: String(resolution.version ?? 0),
|
|
3606
4608
|
PIWI_SELECTION_HASH: resolution.resolvedHash,
|
|
3607
|
-
PIWI_SELECTION_COUNT: String(resolution.estimate.count)
|
|
4609
|
+
PIWI_SELECTION_COUNT: String(resolution.estimate.count),
|
|
4610
|
+
...shardEnv(args)
|
|
3608
4611
|
};
|
|
3609
4612
|
console.error(
|
|
3610
4613
|
`piwi run: ${args.key} \u2192 ${resolution.estimate.count} tests${resolution.materialization.format !== args.format ? ` (materialized as ${resolution.materialization.format})` : ""}`
|
|
@@ -3613,16 +4616,16 @@ async function runRun(argv, env = process.env) {
|
|
|
3613
4616
|
}
|
|
3614
4617
|
|
|
3615
4618
|
// src/cli/preflight.ts
|
|
3616
|
-
var
|
|
3617
|
-
var
|
|
3618
|
-
var
|
|
4619
|
+
var fs15 = __toESM(require("fs"));
|
|
4620
|
+
var path17 = __toESM(require("path"));
|
|
4621
|
+
var import_node_child_process7 = require("child_process");
|
|
3619
4622
|
|
|
3620
4623
|
// ../core/src/diff-anchors.ts
|
|
3621
4624
|
var RENAME_FROM = `rename from `;
|
|
3622
4625
|
var RENAME_TO = "rename to ";
|
|
3623
4626
|
var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
3624
|
-
function stripPrefix(
|
|
3625
|
-
const unquoted =
|
|
4627
|
+
function stripPrefix(path19) {
|
|
4628
|
+
const unquoted = path19.startsWith('"') && path19.endsWith('"') ? path19.slice(1, -1) : path19;
|
|
3626
4629
|
return unquoted.replace(/^[ab]\//, "");
|
|
3627
4630
|
}
|
|
3628
4631
|
function parseUnifiedDiff(text) {
|
|
@@ -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
|
}
|