@quikit/quikpilot-runner 8.2.4 → 8.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +396 -29
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -73,10 +73,12 @@ async function describeQuikTrackFailure(res) {
|
|
|
73
73
|
|
|
74
74
|
// src/callback.ts
|
|
75
75
|
async function reportBack(params) {
|
|
76
|
-
const { callbackUrl, runToken, status, branch, warnings } = params;
|
|
76
|
+
const { callbackUrl, runToken, status, branch, warnings, targeting, baselineFindings } = params;
|
|
77
77
|
const body = { status };
|
|
78
78
|
if (branch) body.branch = branch;
|
|
79
79
|
if (warnings && warnings.length > 0) body.warnings = warnings;
|
|
80
|
+
if (targeting) body.targeting = targeting;
|
|
81
|
+
if (baselineFindings) body.baselineFindings = baselineFindings;
|
|
80
82
|
const res = await fetch(callbackUrl, {
|
|
81
83
|
method: "POST",
|
|
82
84
|
headers: { Authorization: `Bearer ${runToken}`, "Content-Type": "application/json" },
|
|
@@ -108,10 +110,36 @@ function runTestCommand(command) {
|
|
|
108
110
|
}
|
|
109
111
|
function runTestCommandAsync(command) {
|
|
110
112
|
if (!command) return Promise.resolve("success");
|
|
111
|
-
return new Promise((
|
|
113
|
+
return new Promise((resolve4) => {
|
|
112
114
|
const child = (0, import_node_child_process.spawn)(command, { shell: "/bin/bash", stdio: "inherit" });
|
|
113
|
-
child.on("exit", (code) =>
|
|
114
|
-
child.on("error", () =>
|
|
115
|
+
child.on("exit", (code) => resolve4(code === 0 ? "success" : "failure"));
|
|
116
|
+
child.on("error", () => resolve4("failure"));
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function runCommandWithTimeout(command, timeoutMs) {
|
|
120
|
+
return new Promise((resolve4) => {
|
|
121
|
+
let settled = false;
|
|
122
|
+
const finish = (outcome) => {
|
|
123
|
+
if (settled) return;
|
|
124
|
+
settled = true;
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
resolve4(outcome);
|
|
127
|
+
};
|
|
128
|
+
const child = (0, import_node_child_process.spawn)(command, { shell: "/bin/bash", stdio: "inherit", detached: true });
|
|
129
|
+
const timer = setTimeout(() => {
|
|
130
|
+
try {
|
|
131
|
+
if (child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
|
|
132
|
+
else child.kill("SIGKILL");
|
|
133
|
+
} catch {
|
|
134
|
+
try {
|
|
135
|
+
child.kill("SIGKILL");
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
finish("timeout");
|
|
140
|
+
}, timeoutMs);
|
|
141
|
+
child.on("exit", () => finish("exited"));
|
|
142
|
+
child.on("error", () => finish("error"));
|
|
115
143
|
});
|
|
116
144
|
}
|
|
117
145
|
|
|
@@ -214,6 +242,13 @@ function matchesDeniedPath(path, patterns) {
|
|
|
214
242
|
}
|
|
215
243
|
return null;
|
|
216
244
|
}
|
|
245
|
+
function findDeniedContextPath(contextPaths, patterns) {
|
|
246
|
+
for (const path of contextPaths) {
|
|
247
|
+
const pattern = matchesDeniedPath(path, patterns);
|
|
248
|
+
if (pattern) return { path, pattern };
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
217
252
|
|
|
218
253
|
// src/envelope.ts
|
|
219
254
|
var HARD_FAILURE_CODES = /* @__PURE__ */ new Set(["SEMANTIC_MISMATCH", "CONTRACT_REJECTED"]);
|
|
@@ -384,13 +419,19 @@ function narrowJudgedByForTarget(pool, targets, codeTargetPath) {
|
|
|
384
419
|
function toWireJudgedBy(pool) {
|
|
385
420
|
return pool.map(({ automationId: _automationId, ...rest }) => rest);
|
|
386
421
|
}
|
|
387
|
-
async function runCodegen(gateway, targets, workspaceListing, requiredTests, deniedPaths, judgedByPool, warnings) {
|
|
422
|
+
async function runCodegen(gateway, targets, workspaceListing, requiredTests, deniedPaths, judgedByPool, warnings, contextLog) {
|
|
388
423
|
const results = [];
|
|
389
424
|
const listing = new Set(workspaceListing);
|
|
390
425
|
for (const target of targets) {
|
|
391
426
|
const isNewFile = target.kind === "NEW_FILE";
|
|
392
427
|
const currentContent = isNewFile ? "" : (0, import_node_fs.readFileSync)(target.path, "utf8");
|
|
393
428
|
const contextPaths = target.contextPaths ?? [];
|
|
429
|
+
const deniedContext = findDeniedContextPath(contextPaths, deniedPaths);
|
|
430
|
+
if (deniedContext) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`pilot.codegen contextPaths cited a denied path (${deniedContext.pattern}): ${deniedContext.path} (target ${target.path})`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
394
435
|
for (const contextPath of contextPaths) {
|
|
395
436
|
if (!listing.has(contextPath)) {
|
|
396
437
|
throw new Error(
|
|
@@ -404,15 +445,14 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
|
|
|
404
445
|
for (const s of added.skipped) {
|
|
405
446
|
warnings.push(`not shown to the test for ${target.path}: ${s.path} (${s.reason})`);
|
|
406
447
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
);
|
|
448
|
+
const contextRecord = {
|
|
449
|
+
targetPath: target.path,
|
|
450
|
+
contextPaths,
|
|
451
|
+
addedPaths: added.entries.map((e) => e.path),
|
|
452
|
+
skipped: added.skipped
|
|
453
|
+
};
|
|
454
|
+
console.log(JSON.stringify({ event: "codegen.context", ...contextRecord }));
|
|
455
|
+
contextLog?.push(contextRecord);
|
|
416
456
|
const contextData = {
|
|
417
457
|
targetPath: target.path,
|
|
418
458
|
currentContent,
|
|
@@ -470,6 +510,8 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
|
|
|
470
510
|
if (envelope?.errorCode === CONTEXT_TOO_LARGE_CODE && added.entries.length > 0) {
|
|
471
511
|
contextData.contextContents = namedContents;
|
|
472
512
|
warnings.push(`context for ${target.path} was too large with the run's other files; retried without them`);
|
|
513
|
+
contextRecord.addedPaths = [];
|
|
514
|
+
contextRecord.retriedWithoutAdded = true;
|
|
473
515
|
res = await callCodegen();
|
|
474
516
|
if (!res.ok) {
|
|
475
517
|
throw new Error(`Codegen call failed for ${target.path}: ${res.status} ${res.statusText}`);
|
|
@@ -944,8 +986,8 @@ async function reportToQuikTest(params) {
|
|
|
944
986
|
try {
|
|
945
987
|
console.log(`reporting ${report}`);
|
|
946
988
|
const ciUrl = buildCiUrl(githubServerUrl, githubRepository, githubRunId);
|
|
947
|
-
const { readFileSync:
|
|
948
|
-
const body =
|
|
989
|
+
const { readFileSync: readFileSync6 } = await import("node:fs");
|
|
990
|
+
const body = readFileSync6(report);
|
|
949
991
|
let url = `${resultsUrl}?ciUrl=${encodeURIComponent(ciUrl)}&automationIdFormat=bracket`;
|
|
950
992
|
if (generatedFiles) {
|
|
951
993
|
url += `&generatedFiles=${encodeURIComponent(generatedFiles.join(","))}`;
|
|
@@ -972,12 +1014,18 @@ async function reportToQuikTest(params) {
|
|
|
972
1014
|
}
|
|
973
1015
|
}
|
|
974
1016
|
|
|
1017
|
+
// src/baselineCheck.ts
|
|
1018
|
+
var import_node_child_process3 = require("node:child_process");
|
|
1019
|
+
var import_node_fs6 = require("node:fs");
|
|
1020
|
+
var import_node_path4 = require("node:path");
|
|
1021
|
+
|
|
975
1022
|
// src/regressionCheck.ts
|
|
976
1023
|
var import_node_fs5 = require("node:fs");
|
|
977
1024
|
var TESTCASE_RE = /<testcase\b([^>]*?)(?:\/>|>([\s\S]*?)<\/testcase>)/g;
|
|
978
1025
|
var NAME_ATTR_RE = /\bname="([^"]*)"/;
|
|
979
1026
|
var CLASSNAME_ATTR_RE = /\bclassname="([^"]*)"/;
|
|
980
1027
|
var FAILURE_CHILD_RE = /<failure\b|<error\b/;
|
|
1028
|
+
var SKIPPED_CHILD_RE = /<skipped\b/;
|
|
981
1029
|
var TESTSUITE_OPEN_RE = /<testsuite\b([^>]*?)>/g;
|
|
982
1030
|
var FILE_ATTR_RE = /\bfile="([^"]*)"/;
|
|
983
1031
|
var QP_TOKEN_BRACKET_RE = /\[(QP-[A-Z][A-Z0-9]*-\d+-\d+)\]/;
|
|
@@ -999,8 +1047,10 @@ function extractFailureText(testcaseBody) {
|
|
|
999
1047
|
const text = unescapeXmlEntities(raw);
|
|
1000
1048
|
return text.length > MAX_FAILURE_TEXT ? `${text.slice(0, MAX_FAILURE_TEXT)}\u2026` : text;
|
|
1001
1049
|
}
|
|
1050
|
+
var RESULT_RANK = { pass: 0, skipped: 1, fail: 2 };
|
|
1002
1051
|
function parseJUnitOutcomes(xml) {
|
|
1003
1052
|
const outcomes = /* @__PURE__ */ new Map();
|
|
1053
|
+
const automationIdResults = /* @__PURE__ */ new Map();
|
|
1004
1054
|
const failureText = /* @__PURE__ */ new Map();
|
|
1005
1055
|
const discoveredFiles = /* @__PURE__ */ new Set();
|
|
1006
1056
|
const discoveredAutomationIds = /* @__PURE__ */ new Set();
|
|
@@ -1028,9 +1078,14 @@ function parseJUnitOutcomes(xml) {
|
|
|
1028
1078
|
if (file) discoveredFiles.add(normalizeSlashes(file));
|
|
1029
1079
|
if (classname) discoveredFiles.add(normalizeSlashes(classname));
|
|
1030
1080
|
const bracket = QP_TOKEN_BRACKET_RE.exec(name)?.[1];
|
|
1031
|
-
if (bracket)
|
|
1081
|
+
if (bracket) {
|
|
1082
|
+
discoveredAutomationIds.add(bracket);
|
|
1083
|
+
const result = failed ? "fail" : SKIPPED_CHILD_RE.test(body) ? "skipped" : "pass";
|
|
1084
|
+
const seen = automationIdResults.get(bracket);
|
|
1085
|
+
if (!seen || RESULT_RANK[result] > RESULT_RANK[seen]) automationIdResults.set(bracket, result);
|
|
1086
|
+
}
|
|
1032
1087
|
}
|
|
1033
|
-
return { outcomes, failureText, discoveredFiles, discoveredAutomationIds };
|
|
1088
|
+
return { outcomes, automationIdResults, failureText, discoveredFiles, discoveredAutomationIds };
|
|
1034
1089
|
}
|
|
1035
1090
|
function readJUnitOutcomes(cwd) {
|
|
1036
1091
|
const report = findJUnitReport(cwd);
|
|
@@ -1041,6 +1096,9 @@ function readJUnitOutcomes(cwd) {
|
|
|
1041
1096
|
return null;
|
|
1042
1097
|
}
|
|
1043
1098
|
}
|
|
1099
|
+
function readAutomationIdResults(cwd = process.cwd()) {
|
|
1100
|
+
return readJUnitOutcomes(cwd)?.automationIdResults ?? null;
|
|
1101
|
+
}
|
|
1044
1102
|
function captureTestSnapshot(command, cwd = process.cwd()) {
|
|
1045
1103
|
const outcome = runTestCommand(command);
|
|
1046
1104
|
const parsed = readJUnitOutcomes(cwd);
|
|
@@ -1111,8 +1169,186 @@ function describeRegressions(check) {
|
|
|
1111
1169
|
return parts.join("; ");
|
|
1112
1170
|
}
|
|
1113
1171
|
|
|
1172
|
+
// src/baselineCheck.ts
|
|
1173
|
+
var BASELINE_CHECK_TIMEOUT_MS = 12e4;
|
|
1174
|
+
function shellQuote(value) {
|
|
1175
|
+
return `'${value.split("'").join("'\\''")}'`;
|
|
1176
|
+
}
|
|
1177
|
+
function buildSingleFileCommand(command, path) {
|
|
1178
|
+
const quoted = shellQuote(path);
|
|
1179
|
+
return command.includes("{file}") ? command.split("{file}").join(quoted) : `${command} ${quoted}`;
|
|
1180
|
+
}
|
|
1181
|
+
var isTestTarget2 = (t) => (t.automationIds?.length ?? 0) > 0;
|
|
1182
|
+
function checkableIds(ids, types) {
|
|
1183
|
+
if (!types) return [];
|
|
1184
|
+
return ids.filter((id) => {
|
|
1185
|
+
if (!Object.prototype.hasOwnProperty.call(types, id)) return false;
|
|
1186
|
+
const type = types[id];
|
|
1187
|
+
return typeof type === "string" && type.trim() !== "" && type.trim().toUpperCase() !== "REGRESSION";
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
function insideCheckout(cwd, path) {
|
|
1191
|
+
const abs = (0, import_node_path4.resolve)(cwd, path);
|
|
1192
|
+
const rel = (0, import_node_path4.relative)(cwd, abs);
|
|
1193
|
+
return rel === "" || rel.startsWith("..") || (0, import_node_path4.isAbsolute)(rel) ? null : abs;
|
|
1194
|
+
}
|
|
1195
|
+
function writeForCheck(abs, content) {
|
|
1196
|
+
const original = (0, import_node_fs6.existsSync)(abs) ? (0, import_node_fs6.readFileSync)(abs) : null;
|
|
1197
|
+
const createdDirs = [];
|
|
1198
|
+
if (original === null) {
|
|
1199
|
+
const missing = [];
|
|
1200
|
+
for (let d = (0, import_node_path4.dirname)(abs); !(0, import_node_fs6.existsSync)(d); d = (0, import_node_path4.dirname)(d)) missing.push(d);
|
|
1201
|
+
for (const d of missing.reverse()) {
|
|
1202
|
+
(0, import_node_fs6.mkdirSync)(d);
|
|
1203
|
+
createdDirs.push(d);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
(0, import_node_fs6.writeFileSync)(abs, content);
|
|
1207
|
+
return { abs, original, createdDirs };
|
|
1208
|
+
}
|
|
1209
|
+
function restore(written) {
|
|
1210
|
+
for (const file of [...written].reverse()) {
|
|
1211
|
+
try {
|
|
1212
|
+
if (file.original !== null) (0, import_node_fs6.writeFileSync)(file.abs, file.original);
|
|
1213
|
+
else (0, import_node_fs6.rmSync)(file.abs, { force: true });
|
|
1214
|
+
for (const d of [...file.createdDirs].reverse()) {
|
|
1215
|
+
try {
|
|
1216
|
+
(0, import_node_fs6.rmdirSync)(d);
|
|
1217
|
+
} catch {
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
} catch (err) {
|
|
1221
|
+
console.error(`Baseline check could not restore ${file.abs}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
function untrackedFiles(cwd) {
|
|
1226
|
+
try {
|
|
1227
|
+
const out = (0, import_node_child_process3.execFileSync)("git", ["ls-files", "--others", "--exclude-standard", "-z"], {
|
|
1228
|
+
cwd,
|
|
1229
|
+
encoding: "utf8",
|
|
1230
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1231
|
+
maxBuffer: 64 * 1024 * 1024
|
|
1232
|
+
});
|
|
1233
|
+
return new Set(out.split("\0").filter(Boolean));
|
|
1234
|
+
} catch {
|
|
1235
|
+
return null;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
var mtimeOf = (path) => {
|
|
1239
|
+
try {
|
|
1240
|
+
return (0, import_node_fs6.statSync)(path).mtimeMs;
|
|
1241
|
+
} catch {
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
function decideCause(targets, results, cwd) {
|
|
1246
|
+
const codeTargets = targets.filter((t) => !isTestTarget2(t));
|
|
1247
|
+
if (codeTargets.length === 0) return "ALREADY_TRUE";
|
|
1248
|
+
for (const t of codeTargets) {
|
|
1249
|
+
if (t.kind === "NEW_FILE") return "ALREADY_TRUE";
|
|
1250
|
+
const next = results.get(t.path);
|
|
1251
|
+
const abs = insideCheckout(cwd, t.path);
|
|
1252
|
+
if (next === void 0 || abs === null) return "ALREADY_TRUE";
|
|
1253
|
+
try {
|
|
1254
|
+
if ((0, import_node_fs6.readFileSync)(abs, "utf8") !== next) return "ALREADY_TRUE";
|
|
1255
|
+
} catch {
|
|
1256
|
+
return "ALREADY_TRUE";
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return "NO_OP";
|
|
1260
|
+
}
|
|
1261
|
+
async function runBaselineCheck(params) {
|
|
1262
|
+
const { targets, results, testFileCmd, requiredTestTypes } = params;
|
|
1263
|
+
const cwd = params.cwd ?? process.cwd();
|
|
1264
|
+
const runFile = params.runFile ?? runCommandWithTimeout;
|
|
1265
|
+
const nothing = { findings: null, warnings: [] };
|
|
1266
|
+
if (!testFileCmd || testFileCmd.trim() === "") return nothing;
|
|
1267
|
+
const testTargets = targets.filter(isTestTarget2);
|
|
1268
|
+
const toCheck = testTargets.map((target) => ({ target, ids: checkableIds(target.automationIds ?? [], requiredTestTypes) })).filter((entry) => entry.ids.length > 0);
|
|
1269
|
+
if (toCheck.length === 0) {
|
|
1270
|
+
console.log("Baseline check: no case with a known, non-Regression label \u2014 nothing to check.");
|
|
1271
|
+
return nothing;
|
|
1272
|
+
}
|
|
1273
|
+
const warnings = [];
|
|
1274
|
+
const flagged = [];
|
|
1275
|
+
const written = [];
|
|
1276
|
+
const unsafe = /* @__PURE__ */ new Set();
|
|
1277
|
+
const skip = (path, why) => {
|
|
1278
|
+
const message = `Baseline check skipped for ${path}: ${why}`;
|
|
1279
|
+
console.warn(`::warning::${message}`);
|
|
1280
|
+
warnings.push(message);
|
|
1281
|
+
};
|
|
1282
|
+
try {
|
|
1283
|
+
const contentByPath = new Map(results.map((r) => [r.targetPath, r.newContent]));
|
|
1284
|
+
const cause = decideCause(targets, contentByPath, cwd);
|
|
1285
|
+
for (const target of testTargets) {
|
|
1286
|
+
const content = contentByPath.get(target.path);
|
|
1287
|
+
if (content === void 0) continue;
|
|
1288
|
+
const abs = insideCheckout(cwd, target.path);
|
|
1289
|
+
if (abs === null) {
|
|
1290
|
+
unsafe.add(target.path);
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
written.push(writeForCheck(abs, content));
|
|
1294
|
+
}
|
|
1295
|
+
for (const { target, ids } of toCheck) {
|
|
1296
|
+
if (unsafe.has(target.path)) {
|
|
1297
|
+
skip(target.path, "the path is outside the checkout, so the file was not written or run.");
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
if (!contentByPath.has(target.path)) continue;
|
|
1301
|
+
const reportBefore = findJUnitReport(cwd);
|
|
1302
|
+
const mtimeBefore = reportBefore ? mtimeOf(reportBefore) : null;
|
|
1303
|
+
const untrackedBefore = untrackedFiles(cwd);
|
|
1304
|
+
const outcome = await runFile(buildSingleFileCommand(testFileCmd, target.path), BASELINE_CHECK_TIMEOUT_MS);
|
|
1305
|
+
if (outcome === "timeout") {
|
|
1306
|
+
skip(
|
|
1307
|
+
target.path,
|
|
1308
|
+
`the single-file command did not finish within ${BASELINE_CHECK_TIMEOUT_MS / 1e3} seconds and was stopped. No test case from this file was flagged.`
|
|
1309
|
+
);
|
|
1310
|
+
} else if (outcome === "error") {
|
|
1311
|
+
skip(target.path, "the single-file command could not be started. No test case from this file was flagged.");
|
|
1312
|
+
} else {
|
|
1313
|
+
const reportAfter = findJUnitReport(cwd);
|
|
1314
|
+
const fresh = reportAfter !== null && (reportAfter !== reportBefore || mtimeOf(reportAfter) !== mtimeBefore);
|
|
1315
|
+
if (!fresh) {
|
|
1316
|
+
skip(
|
|
1317
|
+
target.path,
|
|
1318
|
+
"the single-file command wrote no new JUnit report (it must write the same JUnit report as the test command), so nothing could be checked."
|
|
1319
|
+
);
|
|
1320
|
+
} else {
|
|
1321
|
+
const byId = readAutomationIdResults(cwd);
|
|
1322
|
+
for (const id of ids) if (byId?.get(id) === "pass") flagged.push(id);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
const untrackedAfter = untrackedFiles(cwd);
|
|
1326
|
+
if (untrackedBefore && untrackedAfter) {
|
|
1327
|
+
for (const file of untrackedAfter) {
|
|
1328
|
+
if (untrackedBefore.has(file)) continue;
|
|
1329
|
+
const abs = insideCheckout(cwd, file);
|
|
1330
|
+
if (abs) (0, import_node_fs6.rmSync)(abs, { force: true });
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
if (flagged.length === 0) return { findings: null, warnings };
|
|
1335
|
+
const automationIds = [...new Set(flagged)].slice(0, 200);
|
|
1336
|
+
const list = automationIds.join(", ");
|
|
1337
|
+
warnings.push(
|
|
1338
|
+
cause === "NO_OP" ? `Baseline check: ${automationIds.length} test case(s) passed against the unmodified code, and every file the generated change edited came out unchanged, so the change itself may have been a no-op: ${list}.` : `Baseline check: ${automationIds.length} test case(s) passed against the unmodified code, so they may assert something already true: ${list}. This check cannot tell that from a change that did nothing.`
|
|
1339
|
+
);
|
|
1340
|
+
return { findings: { cause, automationIds }, warnings };
|
|
1341
|
+
} catch (err) {
|
|
1342
|
+
const message = `Baseline check could not run: ${err instanceof Error ? err.message : String(err)}`;
|
|
1343
|
+
console.warn(`::warning::${message}`);
|
|
1344
|
+
return { findings: null, warnings: [...warnings, message] };
|
|
1345
|
+
} finally {
|
|
1346
|
+
restore(written);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1114
1350
|
// src/scopeCheck.ts
|
|
1115
|
-
var
|
|
1351
|
+
var import_node_fs7 = require("node:fs");
|
|
1116
1352
|
var ROUTE_METHOD_RE = /\b(GET|POST|PUT|PATCH|DELETE)\b/i;
|
|
1117
1353
|
var ROUTE_PATH_RE = /\/[A-Za-z0-9_\-{}[\]]+(?:\/[A-Za-z0-9_\-:{}[\]]+)+/;
|
|
1118
1354
|
var ROUTE_EXPORT_RE = /\bexport\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\s*\(/;
|
|
@@ -1169,7 +1405,7 @@ function checkApiScope(results, targets, apiChanges) {
|
|
|
1169
1405
|
} else {
|
|
1170
1406
|
let currentContent;
|
|
1171
1407
|
try {
|
|
1172
|
-
currentContent = (0,
|
|
1408
|
+
currentContent = (0, import_node_fs7.readFileSync)(targetPath, "utf8");
|
|
1173
1409
|
} catch {
|
|
1174
1410
|
currentContent = "";
|
|
1175
1411
|
}
|
|
@@ -1188,7 +1424,7 @@ function describeScopeViolations(check) {
|
|
|
1188
1424
|
}
|
|
1189
1425
|
|
|
1190
1426
|
// src/targeting.ts
|
|
1191
|
-
var
|
|
1427
|
+
var import_node_path5 = require("node:path");
|
|
1192
1428
|
var CONTRACT_VERSION2 = "test-targets-1";
|
|
1193
1429
|
function listWorkspaceFiles(workspaceRoot) {
|
|
1194
1430
|
return listFiles(workspaceRoot ?? ".");
|
|
@@ -1200,6 +1436,12 @@ function validateTarget(target, workspaceRoot, workspaceListing, deniedPaths) {
|
|
|
1200
1436
|
`pilot.targeting proposed a target matching a denied path (${denied}): ${target.path}`
|
|
1201
1437
|
);
|
|
1202
1438
|
}
|
|
1439
|
+
const deniedContext = findDeniedContextPath(target.contextPaths ?? [], deniedPaths);
|
|
1440
|
+
if (deniedContext) {
|
|
1441
|
+
throw new Error(
|
|
1442
|
+
`pilot.targeting cited a denied path as context (${deniedContext.pattern}): ${deniedContext.path} (target ${target.path})`
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1203
1445
|
if (target.kind !== "EXISTING" && target.kind !== "NEW_FILE") {
|
|
1204
1446
|
throw new Error(`pilot.targeting returned an unknown kind "${target.kind}" for ${target.path}`);
|
|
1205
1447
|
}
|
|
@@ -1211,9 +1453,9 @@ function validateTarget(target, workspaceRoot, workspaceListing, deniedPaths) {
|
|
|
1211
1453
|
}
|
|
1212
1454
|
return;
|
|
1213
1455
|
}
|
|
1214
|
-
const root = (0,
|
|
1215
|
-
const rel = (0,
|
|
1216
|
-
if (rel.startsWith("..") || (0,
|
|
1456
|
+
const root = (0, import_node_path5.resolve)(workspaceRoot ?? ".");
|
|
1457
|
+
const rel = (0, import_node_path5.relative)(root, (0, import_node_path5.resolve)(target.path));
|
|
1458
|
+
if (rel.startsWith("..") || (0, import_node_path5.isAbsolute)(rel)) {
|
|
1217
1459
|
throw new Error(
|
|
1218
1460
|
`pilot.targeting named a NEW_FILE path that escapes the workspace root: ${target.path}`
|
|
1219
1461
|
);
|
|
@@ -1269,6 +1511,108 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, re
|
|
|
1269
1511
|
};
|
|
1270
1512
|
}
|
|
1271
1513
|
|
|
1514
|
+
// src/targetingRecord.ts
|
|
1515
|
+
var MAX_TARGETS = 25;
|
|
1516
|
+
var MAX_IDS_PER_TARGET = 50;
|
|
1517
|
+
var MAX_CONTEXT_PATHS = 20;
|
|
1518
|
+
var MAX_ADDED_PATHS = 20;
|
|
1519
|
+
var MAX_SKIPPED_CONTEXT = 20;
|
|
1520
|
+
var MAX_SKIPPED_CASES = 200;
|
|
1521
|
+
var MAX_CONTEXT_RECORDS = 25;
|
|
1522
|
+
var MAX_PATH_CHARS = 300;
|
|
1523
|
+
var MAX_ID_CHARS = 100;
|
|
1524
|
+
var MAX_REASON_CHARS = 300;
|
|
1525
|
+
function clampList(values, max, maxChars) {
|
|
1526
|
+
let cut = false;
|
|
1527
|
+
const kept = [];
|
|
1528
|
+
for (const raw of values ?? []) {
|
|
1529
|
+
const v = typeof raw === "string" ? raw.trim() : "";
|
|
1530
|
+
if (v.length === 0) continue;
|
|
1531
|
+
if (v.length > maxChars || kept.length >= max) {
|
|
1532
|
+
cut = true;
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
kept.push(v);
|
|
1536
|
+
}
|
|
1537
|
+
return { kept, cut };
|
|
1538
|
+
}
|
|
1539
|
+
function buildTargetingRecord(targeting, contextLog) {
|
|
1540
|
+
let truncated = false;
|
|
1541
|
+
const note = (cut) => {
|
|
1542
|
+
if (cut) truncated = true;
|
|
1543
|
+
};
|
|
1544
|
+
if (targeting.outcome === "NO_TARGET") {
|
|
1545
|
+
const record2 = { outcome: "NO_TARGET", targets: [], skippedCaseIds: [], codegenContext: [] };
|
|
1546
|
+
const reason = (targeting.reason ?? "").trim();
|
|
1547
|
+
if (reason.length > 0) {
|
|
1548
|
+
record2.noTargetReason = reason.slice(0, MAX_REASON_CHARS);
|
|
1549
|
+
if (reason.length > MAX_REASON_CHARS) record2.noTargetReasonTruncated = true;
|
|
1550
|
+
}
|
|
1551
|
+
return record2;
|
|
1552
|
+
}
|
|
1553
|
+
const targets = [];
|
|
1554
|
+
for (const t of targeting.targets) {
|
|
1555
|
+
if (targets.length >= MAX_TARGETS) {
|
|
1556
|
+
truncated = true;
|
|
1557
|
+
break;
|
|
1558
|
+
}
|
|
1559
|
+
const path = typeof t.path === "string" ? t.path.trim() : "";
|
|
1560
|
+
if (path.length === 0 || path.length > MAX_PATH_CHARS) {
|
|
1561
|
+
truncated = true;
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
const contextPaths = clampList(t.contextPaths, MAX_CONTEXT_PATHS, MAX_PATH_CHARS);
|
|
1565
|
+
const automationIds = clampList(t.automationIds, MAX_IDS_PER_TARGET, MAX_ID_CHARS);
|
|
1566
|
+
note(contextPaths.cut);
|
|
1567
|
+
note(automationIds.cut);
|
|
1568
|
+
targets.push({ path, kind: t.kind, contextPaths: contextPaths.kept, automationIds: automationIds.kept });
|
|
1569
|
+
}
|
|
1570
|
+
const skipped = clampList(
|
|
1571
|
+
(targeting.skippedCases ?? []).map((c) => c.automationId),
|
|
1572
|
+
MAX_SKIPPED_CASES,
|
|
1573
|
+
MAX_ID_CHARS
|
|
1574
|
+
);
|
|
1575
|
+
note(skipped.cut);
|
|
1576
|
+
const codegenContext = [];
|
|
1577
|
+
for (const entry of contextLog) {
|
|
1578
|
+
if (codegenContext.length >= MAX_CONTEXT_RECORDS) {
|
|
1579
|
+
truncated = true;
|
|
1580
|
+
break;
|
|
1581
|
+
}
|
|
1582
|
+
const targetPath = typeof entry.targetPath === "string" ? entry.targetPath.trim() : "";
|
|
1583
|
+
if (targetPath.length === 0 || targetPath.length > MAX_PATH_CHARS) {
|
|
1584
|
+
truncated = true;
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
const contextPaths = clampList(entry.contextPaths, MAX_CONTEXT_PATHS, MAX_PATH_CHARS);
|
|
1588
|
+
const addedPaths = clampList(entry.addedPaths, MAX_ADDED_PATHS, MAX_PATH_CHARS);
|
|
1589
|
+
const skippedPaths = clampList(
|
|
1590
|
+
(entry.skipped ?? []).map((s) => s.path),
|
|
1591
|
+
MAX_SKIPPED_CONTEXT,
|
|
1592
|
+
MAX_PATH_CHARS
|
|
1593
|
+
);
|
|
1594
|
+
note(contextPaths.cut);
|
|
1595
|
+
note(addedPaths.cut);
|
|
1596
|
+
note(skippedPaths.cut);
|
|
1597
|
+
const reasonByPath = new Map((entry.skipped ?? []).map((s) => [s.path.trim(), s.reason]));
|
|
1598
|
+
codegenContext.push({
|
|
1599
|
+
targetPath,
|
|
1600
|
+
contextPaths: contextPaths.kept,
|
|
1601
|
+
addedPaths: addedPaths.kept,
|
|
1602
|
+
skipped: skippedPaths.kept.map((path) => ({ path, reason: String(reasonByPath.get(path) ?? "skipped").slice(0, 40) })),
|
|
1603
|
+
...entry.retriedWithoutAdded ? { retriedWithoutAdded: true } : {}
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
const record = {
|
|
1607
|
+
outcome: "TARGETED",
|
|
1608
|
+
targets,
|
|
1609
|
+
skippedCaseIds: skipped.kept,
|
|
1610
|
+
codegenContext
|
|
1611
|
+
};
|
|
1612
|
+
if (truncated) record.truncated = true;
|
|
1613
|
+
return record;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1272
1616
|
// src/testPlacementCheck.ts
|
|
1273
1617
|
function isDiscovered(targetPath, discoveredFiles) {
|
|
1274
1618
|
const normalized = targetPath.replace(/\\/g, "/");
|
|
@@ -1332,9 +1676,12 @@ async function run(env2) {
|
|
|
1332
1676
|
let packet;
|
|
1333
1677
|
let branch;
|
|
1334
1678
|
let runToken;
|
|
1335
|
-
|
|
1679
|
+
const warnings = [];
|
|
1336
1680
|
let targeted = false;
|
|
1337
1681
|
let generatedFilePaths = [];
|
|
1682
|
+
let targetingResult;
|
|
1683
|
+
let baselineFindings;
|
|
1684
|
+
const contextLog = [];
|
|
1338
1685
|
try {
|
|
1339
1686
|
runToken = await exchangeForRunToken(env2.tokenUrl, await fetchGithubOidcToken());
|
|
1340
1687
|
packet = await fetchPacket(env2.packetUrl, runToken, env2.githubRunId);
|
|
@@ -1349,6 +1696,7 @@ async function run(env2) {
|
|
|
1349
1696
|
packet.guardrails.deniedPaths,
|
|
1350
1697
|
packet.testDiscovery
|
|
1351
1698
|
);
|
|
1699
|
+
targetingResult = targeting;
|
|
1352
1700
|
if (targeting.outcome === "NO_TARGET") {
|
|
1353
1701
|
writePhase0Marker(env2.runId);
|
|
1354
1702
|
runCommand(packet.commands.setup, "Setup");
|
|
@@ -1367,8 +1715,9 @@ async function run(env2) {
|
|
|
1367
1715
|
const blockingOverlaps = overlaps.filter((o) => o.severity === "identical");
|
|
1368
1716
|
const softOverlaps = overlaps.filter((o) => o.severity === "similar");
|
|
1369
1717
|
if (softOverlaps.length > 0) {
|
|
1370
|
-
|
|
1371
|
-
|
|
1718
|
+
const overlapWarnings = describeOverlapWarnings(softOverlaps);
|
|
1719
|
+
warnings.push(...overlapWarnings);
|
|
1720
|
+
for (const w of overlapWarnings) {
|
|
1372
1721
|
console.warn(`QuikPilot targeting warning (not blocking): ${w}`);
|
|
1373
1722
|
}
|
|
1374
1723
|
}
|
|
@@ -1392,7 +1741,8 @@ async function run(env2) {
|
|
|
1392
1741
|
packet.requiredTests,
|
|
1393
1742
|
packet.guardrails.deniedPaths,
|
|
1394
1743
|
judgedByPool,
|
|
1395
|
-
warnings
|
|
1744
|
+
warnings,
|
|
1745
|
+
contextLog
|
|
1396
1746
|
)
|
|
1397
1747
|
]);
|
|
1398
1748
|
const depCheck = checkImportDependencies(
|
|
@@ -1417,6 +1767,20 @@ async function run(env2) {
|
|
|
1417
1767
|
`QuikPilot's generated tests are not where the test runner will find them \u2014 ${describeTestPlacementViolations(placementCheck)}`
|
|
1418
1768
|
);
|
|
1419
1769
|
}
|
|
1770
|
+
try {
|
|
1771
|
+
const baselineCheck = await runBaselineCheck({
|
|
1772
|
+
targets: targeting.targets,
|
|
1773
|
+
results,
|
|
1774
|
+
testFileCmd: packet.commands.testFile,
|
|
1775
|
+
requiredTestTypes: packet.requiredTestTypes
|
|
1776
|
+
});
|
|
1777
|
+
warnings.push(...baselineCheck.warnings);
|
|
1778
|
+
baselineFindings = baselineCheck.findings ?? void 0;
|
|
1779
|
+
} catch (err) {
|
|
1780
|
+
const message = `Baseline check could not run: ${err instanceof Error ? err.message : String(err)}`;
|
|
1781
|
+
console.error(message);
|
|
1782
|
+
warnings.push(message);
|
|
1783
|
+
}
|
|
1420
1784
|
generatedFilePaths = results.map((r) => r.targetPath);
|
|
1421
1785
|
flushCodegenResults(results);
|
|
1422
1786
|
try {
|
|
@@ -1486,7 +1850,10 @@ async function run(env2) {
|
|
|
1486
1850
|
runToken,
|
|
1487
1851
|
status: hardFailure ? "failed" : "completed",
|
|
1488
1852
|
branch,
|
|
1489
|
-
warnings
|
|
1853
|
+
warnings,
|
|
1854
|
+
// Omitted when targeting itself never produced a result (nothing exists to record).
|
|
1855
|
+
targeting: targetingResult ? buildTargetingRecord(targetingResult, contextLog) : void 0,
|
|
1856
|
+
baselineFindings
|
|
1490
1857
|
});
|
|
1491
1858
|
return { hardFailure };
|
|
1492
1859
|
}
|