@quikit/quikpilot-runner 8.2.7 → 9.0.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 +7 -0
- package/dist/index.js +472 -48
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -73,12 +73,14 @@ async function describeQuikTrackFailure(res) {
|
|
|
73
73
|
|
|
74
74
|
// src/callback.ts
|
|
75
75
|
async function reportBack(params) {
|
|
76
|
-
const { callbackUrl, runToken, status, branch, warnings, targeting, baselineFindings } = params;
|
|
76
|
+
const { callbackUrl, runToken, status, branch, warnings, targeting, baselineFindings, vanishedTests, unresolved } = 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 (unresolved && unresolved.length > 0) body.unresolved = unresolved;
|
|
80
81
|
if (targeting) body.targeting = targeting;
|
|
81
82
|
if (baselineFindings) body.baselineFindings = baselineFindings;
|
|
83
|
+
if (vanishedTests) body.vanishedTests = vanishedTests;
|
|
82
84
|
const res = await fetch(callbackUrl, {
|
|
83
85
|
method: "POST",
|
|
84
86
|
headers: { Authorization: `Bearer ${runToken}`, "Content-Type": "application/json" },
|
|
@@ -110,20 +112,20 @@ function runTestCommand(command) {
|
|
|
110
112
|
}
|
|
111
113
|
function runTestCommandAsync(command) {
|
|
112
114
|
if (!command) return Promise.resolve("success");
|
|
113
|
-
return new Promise((
|
|
115
|
+
return new Promise((resolve5) => {
|
|
114
116
|
const child = (0, import_node_child_process.spawn)(command, { shell: "/bin/bash", stdio: "inherit" });
|
|
115
|
-
child.on("exit", (code) =>
|
|
116
|
-
child.on("error", () =>
|
|
117
|
+
child.on("exit", (code) => resolve5(code === 0 ? "success" : "failure"));
|
|
118
|
+
child.on("error", () => resolve5("failure"));
|
|
117
119
|
});
|
|
118
120
|
}
|
|
119
121
|
function runCommandWithTimeout(command, timeoutMs) {
|
|
120
|
-
return new Promise((
|
|
122
|
+
return new Promise((resolve5) => {
|
|
121
123
|
let settled = false;
|
|
122
124
|
const finish = (outcome) => {
|
|
123
125
|
if (settled) return;
|
|
124
126
|
settled = true;
|
|
125
127
|
clearTimeout(timer);
|
|
126
|
-
|
|
128
|
+
resolve5(outcome);
|
|
127
129
|
};
|
|
128
130
|
const child = (0, import_node_child_process.spawn)(command, { shell: "/bin/bash", stdio: "inherit", detached: true });
|
|
129
131
|
const timer = setTimeout(() => {
|
|
@@ -250,6 +252,106 @@ function findDeniedContextPath(contextPaths, patterns) {
|
|
|
250
252
|
return null;
|
|
251
253
|
}
|
|
252
254
|
|
|
255
|
+
// src/editsApply.ts
|
|
256
|
+
function detectLineEnding(text) {
|
|
257
|
+
const crlf = text.split("\r\n").length - 1;
|
|
258
|
+
const bareLf = text.split("\n").length - 1 - crlf;
|
|
259
|
+
if (crlf > 0 && bareLf > 0) return "mixed";
|
|
260
|
+
return crlf > 0 ? "CRLF" : "LF";
|
|
261
|
+
}
|
|
262
|
+
function toLf(text) {
|
|
263
|
+
return text.replace(/\r\n/g, "\n");
|
|
264
|
+
}
|
|
265
|
+
function fromLf(text, ending) {
|
|
266
|
+
return ending === "CRLF" ? text.replace(/\n/g, "\r\n") : text;
|
|
267
|
+
}
|
|
268
|
+
function occurrences(haystack, needle) {
|
|
269
|
+
const found = [];
|
|
270
|
+
let index = haystack.indexOf(needle);
|
|
271
|
+
while (index !== -1) {
|
|
272
|
+
found.push(index);
|
|
273
|
+
index = haystack.indexOf(needle, index + 1);
|
|
274
|
+
}
|
|
275
|
+
return found;
|
|
276
|
+
}
|
|
277
|
+
function applyEdits(originalLf, edits) {
|
|
278
|
+
if (!Array.isArray(edits) || edits.length === 0) {
|
|
279
|
+
return {
|
|
280
|
+
ok: false,
|
|
281
|
+
editCount: 0,
|
|
282
|
+
refusals: [{ kind: "no_edits", edits: [], matchCount: null, message: "the response carried no edits" }]
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const refusals = [];
|
|
286
|
+
const ranges = [];
|
|
287
|
+
edits.forEach((edit, index) => {
|
|
288
|
+
const number = index + 1;
|
|
289
|
+
const record = edit !== null && typeof edit === "object" ? edit : null;
|
|
290
|
+
const find = record?.find;
|
|
291
|
+
const replace = record?.replace;
|
|
292
|
+
if (typeof find !== "string" || typeof replace !== "string") {
|
|
293
|
+
refusals.push({
|
|
294
|
+
kind: "empty",
|
|
295
|
+
edits: [number],
|
|
296
|
+
matchCount: null,
|
|
297
|
+
message: `edit ${number} is missing a string find or replace`
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (find === "") {
|
|
302
|
+
refusals.push({ kind: "empty", edits: [number], matchCount: null, message: `edit ${number} has an empty anchor` });
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const starts = occurrences(originalLf, find);
|
|
306
|
+
if (starts.length === 0) {
|
|
307
|
+
refusals.push({ kind: "no_match", edits: [number], matchCount: 0, message: `edit ${number}: the anchor matched nothing` });
|
|
308
|
+
} else if (starts.length > 1) {
|
|
309
|
+
refusals.push({
|
|
310
|
+
kind: "ambiguous",
|
|
311
|
+
edits: [number],
|
|
312
|
+
matchCount: starts.length,
|
|
313
|
+
message: `edit ${number}: the anchor matched ${starts.length} times`
|
|
314
|
+
});
|
|
315
|
+
} else {
|
|
316
|
+
ranges.push({ start: starts[0], end: starts[0] + find.length, number, replace });
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
320
|
+
for (let j = i + 1; j < ranges.length; j++) {
|
|
321
|
+
const a = ranges[i];
|
|
322
|
+
const b = ranges[j];
|
|
323
|
+
if (a.start < b.end && b.start < a.end) {
|
|
324
|
+
const [first, second] = a.number < b.number ? [a.number, b.number] : [b.number, a.number];
|
|
325
|
+
refusals.push({
|
|
326
|
+
kind: "overlap",
|
|
327
|
+
edits: [first, second],
|
|
328
|
+
matchCount: null,
|
|
329
|
+
message: `edits ${first} and ${second} overlap`
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (refusals.length > 0) return { ok: false, refusals, editCount: edits.length };
|
|
335
|
+
const pieces = [];
|
|
336
|
+
let cursor = 0;
|
|
337
|
+
for (const range of [...ranges].sort((x, y) => x.start - y.start)) {
|
|
338
|
+
pieces.push(originalLf.slice(cursor, range.start), range.replace);
|
|
339
|
+
cursor = range.end;
|
|
340
|
+
}
|
|
341
|
+
pieces.push(originalLf.slice(cursor));
|
|
342
|
+
return { ok: true, content: pieces.join(""), editCount: edits.length };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/runtimeReplyError.ts
|
|
346
|
+
var RuntimeReplyError = class extends Error {
|
|
347
|
+
summary;
|
|
348
|
+
constructor(message, summary) {
|
|
349
|
+
super(message);
|
|
350
|
+
this.name = "RuntimeReplyError";
|
|
351
|
+
this.summary = summary;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
253
355
|
// src/envelope.ts
|
|
254
356
|
var HARD_FAILURE_CODES = /* @__PURE__ */ new Set(["SEMANTIC_MISMATCH", "CONTRACT_REJECTED"]);
|
|
255
357
|
function checkHardFailure(envelope, useCase, target) {
|
|
@@ -257,17 +359,25 @@ function checkHardFailure(envelope, useCase, target) {
|
|
|
257
359
|
if (typeof code === "string" && HARD_FAILURE_CODES.has(code)) {
|
|
258
360
|
const detail = envelope.error ?? envelope.normalizedText ?? "no detail";
|
|
259
361
|
const where = target ? ` for ${target}` : "";
|
|
260
|
-
throw new
|
|
362
|
+
throw new RuntimeReplyError(
|
|
363
|
+
`${useCase} failed${where} (${code}, not retried): ${detail}`,
|
|
364
|
+
`${useCase} failed${where} (${code}, not retried)`
|
|
365
|
+
);
|
|
261
366
|
}
|
|
262
367
|
}
|
|
263
368
|
function unwrapExecute(envelope, useCase) {
|
|
264
369
|
if (envelope?.success === false) {
|
|
265
|
-
|
|
266
|
-
|
|
370
|
+
const code = envelope.errorCode ?? "UNKNOWN";
|
|
371
|
+
throw new RuntimeReplyError(
|
|
372
|
+
`${useCase} failed: ${code} \u2014 ${envelope.error ?? envelope.normalizedText ?? "no message"}`,
|
|
373
|
+
`${useCase} failed: ${code}`
|
|
267
374
|
);
|
|
268
375
|
}
|
|
269
376
|
if (!envelope?.structuredJson || typeof envelope.structuredJson !== "object") {
|
|
270
|
-
throw new
|
|
377
|
+
throw new RuntimeReplyError(
|
|
378
|
+
`${useCase} returned no structuredJson: ${JSON.stringify(envelope)}`,
|
|
379
|
+
`${useCase} returned no structuredJson`
|
|
380
|
+
);
|
|
271
381
|
}
|
|
272
382
|
return envelope.structuredJson;
|
|
273
383
|
}
|
|
@@ -394,6 +504,58 @@ function looksLikeTruncatedRewrite(target, currentContent, newContent) {
|
|
|
394
504
|
return null;
|
|
395
505
|
}
|
|
396
506
|
var CONTRACT_VERSION = "test-targets-1";
|
|
507
|
+
var CONTRACT_VERSION_EDITS = "edits-1";
|
|
508
|
+
var ANCHOR_REFUSED_CODE = "CODEGEN_ANCHOR_REFUSED";
|
|
509
|
+
function summariseLocalRefusals(refusals) {
|
|
510
|
+
const parts = [];
|
|
511
|
+
const matchCounts = [];
|
|
512
|
+
for (const r of refusals) {
|
|
513
|
+
if (r.kind === "ambiguous") {
|
|
514
|
+
parts.push(`an anchor matched ${r.matchCount} times`);
|
|
515
|
+
if (r.matchCount !== null) matchCounts.push(r.matchCount);
|
|
516
|
+
} else if (r.kind === "no_match") {
|
|
517
|
+
parts.push("an anchor matched nothing");
|
|
518
|
+
} else if (r.kind === "overlap") {
|
|
519
|
+
parts.push(`edits ${r.edits.join(" and ")} overlap`);
|
|
520
|
+
} else if (r.kind === "no_edits") {
|
|
521
|
+
parts.push("the reply had no edits");
|
|
522
|
+
} else {
|
|
523
|
+
parts.push("an edit was missing its anchor or replacement");
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return { text: [...new Set(parts)].join("; "), matchCounts };
|
|
527
|
+
}
|
|
528
|
+
function summariseRuntimeRefusal(error) {
|
|
529
|
+
const text = typeof error === "string" ? error : "";
|
|
530
|
+
const matchCounts = [...text.matchAll(/anchor matched (\d+) times/g)].map((m) => Number(m[1]));
|
|
531
|
+
const parts = matchCounts.map((n) => `an anchor matched ${n} times`);
|
|
532
|
+
if (/no match/i.test(text)) parts.push("an anchor matched nothing");
|
|
533
|
+
if (/overlap/i.test(text)) parts.push("edits overlapped");
|
|
534
|
+
if (/empty anchor|missing a string/i.test(text)) parts.push("an edit was missing its anchor or replacement");
|
|
535
|
+
return { text: parts.length > 0 ? [...new Set(parts)].join("; ") : "the runtime could not place the edits", matchCounts };
|
|
536
|
+
}
|
|
537
|
+
function throwOnTerminalRefusal(envelope, targetPath) {
|
|
538
|
+
if (typeof envelope?.errorCode === "string" && NOT_RETRIED_CODES.has(envelope.errorCode)) {
|
|
539
|
+
const detail = envelope.error ? `: ${envelope.error}` : "";
|
|
540
|
+
throw new RuntimeReplyError(
|
|
541
|
+
`Codegen refused ${targetPath}: ${envelope.errorCode} (not retried)${detail}`,
|
|
542
|
+
`Codegen refused ${targetPath}: ${envelope.errorCode} (not retried)`
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
if (envelope?.errorCode === JUDGED_BY_TOO_LARGE_CODE) {
|
|
546
|
+
const detail = envelope.error ? `: ${envelope.error}` : "";
|
|
547
|
+
throw new RuntimeReplyError(
|
|
548
|
+
`Codegen refused ${targetPath}: ${envelope.errorCode} (after narrowing)${detail}`,
|
|
549
|
+
`Codegen refused ${targetPath}: ${envelope.errorCode} (after narrowing)`
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function codegenRefusedError(targetPath, body) {
|
|
554
|
+
return new RuntimeReplyError(
|
|
555
|
+
`Codegen refused ${targetPath}${body.refusalReason ? `: ${body.refusalReason}` : ""}`,
|
|
556
|
+
`Codegen refused ${targetPath}`
|
|
557
|
+
);
|
|
558
|
+
}
|
|
397
559
|
function buildJudgedByPool(requiredTests, targets, skippedCases) {
|
|
398
560
|
const claimedIds = /* @__PURE__ */ new Set();
|
|
399
561
|
for (const t of targets) {
|
|
@@ -419,12 +581,14 @@ function narrowJudgedByForTarget(pool, targets, codeTargetPath) {
|
|
|
419
581
|
function toWireJudgedBy(pool) {
|
|
420
582
|
return pool.map(({ automationId: _automationId, ...rest }) => rest);
|
|
421
583
|
}
|
|
422
|
-
async function runCodegen(gateway, targets, workspaceListing, requiredTests, deniedPaths, judgedByPool, warnings, contextLog) {
|
|
584
|
+
async function runCodegen(gateway, targets, workspaceListing, requiredTests, deniedPaths, judgedByPool, warnings, contextLog, outOfScope = []) {
|
|
423
585
|
const results = [];
|
|
424
586
|
const listing = new Set(workspaceListing);
|
|
425
587
|
for (const target of targets) {
|
|
426
588
|
const isNewFile = target.kind === "NEW_FILE";
|
|
427
589
|
const currentContent = isNewFile ? "" : (0, import_node_fs.readFileSync)(target.path, "utf8");
|
|
590
|
+
const lineEnding = isNewFile ? "LF" : detectLineEnding(currentContent);
|
|
591
|
+
const wantsEdits = !isNewFile && lineEnding !== "mixed";
|
|
428
592
|
const contextPaths = target.contextPaths ?? [];
|
|
429
593
|
const deniedContext = findDeniedContextPath(contextPaths, deniedPaths);
|
|
430
594
|
if (deniedContext) {
|
|
@@ -439,7 +603,7 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
|
|
|
439
603
|
);
|
|
440
604
|
}
|
|
441
605
|
}
|
|
442
|
-
const namedContents = contextPaths.map((
|
|
606
|
+
const namedContents = contextPaths.map((path2) => ({ path: path2, content: (0, import_node_fs.readFileSync)(path2, "utf8") }));
|
|
443
607
|
const added = collectRunContext(target, targets, new Set(contextPaths), deniedPaths);
|
|
444
608
|
const contextContents = [...namedContents, ...added.entries];
|
|
445
609
|
for (const s of added.skipped) {
|
|
@@ -455,12 +619,13 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
|
|
|
455
619
|
contextLog?.push(contextRecord);
|
|
456
620
|
const contextData = {
|
|
457
621
|
targetPath: target.path,
|
|
458
|
-
|
|
622
|
+
// Every anchor and replacement is LF, always (Doc 62 §2), so the file is sent as LF.
|
|
623
|
+
currentContent: wantsEdits ? toLf(currentContent) : currentContent,
|
|
459
624
|
planSteps: target.planStepsImplemented,
|
|
460
625
|
kind: target.kind,
|
|
461
626
|
contextContents,
|
|
462
627
|
// QUIKTR-1083 (§15.6) — required on every call on the versioned path.
|
|
463
|
-
contractVersion: CONTRACT_VERSION
|
|
628
|
+
contractVersion: wantsEdits ? CONTRACT_VERSION_EDITS : CONTRACT_VERSION
|
|
464
629
|
};
|
|
465
630
|
if (isNewFile) {
|
|
466
631
|
contextData.workspaceListing = workspaceListing;
|
|
@@ -468,6 +633,9 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
|
|
|
468
633
|
if (deniedPaths.length > 0) {
|
|
469
634
|
contextData.deniedPaths = deniedPaths;
|
|
470
635
|
}
|
|
636
|
+
if (outOfScope.length > 0) {
|
|
637
|
+
contextData.outOfScope = outOfScope;
|
|
638
|
+
}
|
|
471
639
|
if (target.automationIds && target.automationIds.length > 0) {
|
|
472
640
|
const wanted = new Set(target.automationIds);
|
|
473
641
|
contextData.requiredTests = requiredTests.filter((tc) => wanted.has(tc.automationId));
|
|
@@ -519,22 +687,91 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
|
|
|
519
687
|
envelope = await res.json();
|
|
520
688
|
checkHardFailure(envelope, "pilot.codegen", target.path);
|
|
521
689
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
690
|
+
throwOnTerminalRefusal(envelope, target.path);
|
|
691
|
+
let newContent = null;
|
|
692
|
+
let failedEdits = null;
|
|
693
|
+
let editCount;
|
|
694
|
+
let path = "whole_file";
|
|
695
|
+
let reason = isNewFile ? "new_file" : wantsEdits ? void 0 : "mixed_line_endings";
|
|
696
|
+
if (wantsEdits && envelope?.errorCode === ANCHOR_REFUSED_CODE) {
|
|
697
|
+
failedEdits = { reason: "anchor_refused", summary: summariseRuntimeRefusal(envelope.error) };
|
|
698
|
+
} else {
|
|
699
|
+
const body = unwrapExecute(envelope, "pilot.codegen");
|
|
700
|
+
if (wantsEdits) {
|
|
701
|
+
if (body.outcome === "REFUSED") throw codegenRefusedError(target.path, body);
|
|
702
|
+
if (body.outcome !== "GENERATED" || typeof body.newContent === "string") {
|
|
703
|
+
throw new RuntimeReplyError(
|
|
704
|
+
`Codegen reply for ${target.path} was not in the edits-1 shape and was not applied`,
|
|
705
|
+
`Codegen reply for ${target.path} was not in the edits-1 shape`
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
const applied = applyEdits(toLf(currentContent), body.edits);
|
|
709
|
+
if (applied.ok) {
|
|
710
|
+
newContent = fromLf(applied.content, lineEnding);
|
|
711
|
+
path = "edits";
|
|
712
|
+
editCount = applied.editCount;
|
|
713
|
+
} else if (applied.refusals.some((r) => r.kind === "no_edits")) {
|
|
714
|
+
throw new RuntimeReplyError(
|
|
715
|
+
`Codegen reply for ${target.path} carried no edits (not retried)`,
|
|
716
|
+
`Codegen reply for ${target.path} carried no edits`
|
|
717
|
+
);
|
|
718
|
+
} else {
|
|
719
|
+
failedEdits = { reason: "local_refusal", summary: summariseLocalRefusals(applied.refusals) };
|
|
720
|
+
}
|
|
721
|
+
} else {
|
|
722
|
+
if (body.outcome !== "GENERATED" || typeof body.newContent !== "string") {
|
|
723
|
+
throw codegenRefusedError(target.path, body);
|
|
724
|
+
}
|
|
725
|
+
newContent = body.newContent;
|
|
726
|
+
}
|
|
525
727
|
}
|
|
526
|
-
if (
|
|
527
|
-
|
|
528
|
-
|
|
728
|
+
if (failedEdits) {
|
|
729
|
+
warnings.push(
|
|
730
|
+
`edits for ${target.path} could not be placed (${failedEdits.summary.text}); asked again for the whole file`
|
|
731
|
+
);
|
|
732
|
+
contextData.currentContent = currentContent;
|
|
733
|
+
contextData.contractVersion = CONTRACT_VERSION;
|
|
734
|
+
try {
|
|
735
|
+
res = await callCodegen();
|
|
736
|
+
if (!res.ok) {
|
|
737
|
+
throw new Error(`Codegen call failed for ${target.path}: ${res.status} ${res.statusText}`);
|
|
738
|
+
}
|
|
739
|
+
envelope = await res.json();
|
|
740
|
+
checkHardFailure(envelope, "pilot.codegen", target.path);
|
|
741
|
+
throwOnTerminalRefusal(envelope, target.path);
|
|
742
|
+
const whole = unwrapExecute(envelope, "pilot.codegen");
|
|
743
|
+
if (whole.outcome !== "GENERATED" || typeof whole.newContent !== "string") {
|
|
744
|
+
throw codegenRefusedError(target.path, whole);
|
|
745
|
+
}
|
|
746
|
+
newContent = whole.newContent;
|
|
747
|
+
} catch (error) {
|
|
748
|
+
if (error instanceof RuntimeReplyError) {
|
|
749
|
+
throw new RuntimeReplyError(
|
|
750
|
+
`${error.message} (this was the whole-file request made after the edits for ${target.path} were refused: ${failedEdits.summary.text})`,
|
|
751
|
+
`${error.summary} (the whole-file request, made after the edits were refused)`
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
throw error;
|
|
755
|
+
}
|
|
756
|
+
path = "fallback";
|
|
757
|
+
reason = failedEdits.reason;
|
|
529
758
|
}
|
|
530
|
-
|
|
531
|
-
if (
|
|
532
|
-
|
|
533
|
-
|
|
759
|
+
if (newContent === null) throw new Error(`Codegen produced no content for ${target.path}`);
|
|
760
|
+
if (!isNewFile) {
|
|
761
|
+
console.log(
|
|
762
|
+
JSON.stringify({
|
|
763
|
+
event: "codegen.edits_path",
|
|
764
|
+
targetPath: target.path,
|
|
765
|
+
path,
|
|
766
|
+
...reason ? { reason } : {},
|
|
767
|
+
...editCount !== void 0 ? { editCount } : {},
|
|
768
|
+
...failedEdits && failedEdits.summary.matchCounts.length > 0 ? { matchCounts: failedEdits.summary.matchCounts } : {},
|
|
769
|
+
lineEnding
|
|
770
|
+
})
|
|
534
771
|
);
|
|
535
772
|
}
|
|
536
773
|
if (!isNewFile) {
|
|
537
|
-
const suspectReason = looksLikeTruncatedRewrite(target, currentContent,
|
|
774
|
+
const suspectReason = looksLikeTruncatedRewrite(target, currentContent, newContent);
|
|
538
775
|
if (suspectReason) {
|
|
539
776
|
throw new Error(
|
|
540
777
|
`Codegen output for ${target.path} looks like a truncated/abbreviated rewrite (${suspectReason}) \u2014 refusing to apply it. Interim guard per \xA716.7; the AI Runtime's own output-cap check is not live yet.`
|
|
@@ -547,11 +784,11 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
|
|
|
547
784
|
targetPath: target.path,
|
|
548
785
|
kind: target.kind,
|
|
549
786
|
inputLength: currentContent.length,
|
|
550
|
-
outputLength:
|
|
551
|
-
removedLines: isNewFile ? null : countRemovedLines(currentContent,
|
|
787
|
+
outputLength: newContent.length,
|
|
788
|
+
removedLines: isNewFile ? null : countRemovedLines(currentContent, newContent)
|
|
552
789
|
})
|
|
553
790
|
);
|
|
554
|
-
results.push({ targetPath: target.path, newContent
|
|
791
|
+
results.push({ targetPath: target.path, newContent });
|
|
555
792
|
}
|
|
556
793
|
return results;
|
|
557
794
|
}
|
|
@@ -961,6 +1198,14 @@ function firstXmlIn(dir, namePattern) {
|
|
|
961
1198
|
const match = entries.find((e) => namePattern.test(e));
|
|
962
1199
|
return match ? (0, import_node_path3.join)(dir, match) : null;
|
|
963
1200
|
}
|
|
1201
|
+
function readTestReportBytes(cwd = process.cwd()) {
|
|
1202
|
+
try {
|
|
1203
|
+
const report = findJUnitReport(cwd);
|
|
1204
|
+
return report ? (0, import_node_fs4.readFileSync)(report) : null;
|
|
1205
|
+
} catch {
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
964
1209
|
function findJUnitReport(cwd = process.cwd()) {
|
|
965
1210
|
return walkForJUnitByName(cwd) ?? firstXmlIn((0, import_node_path3.join)(cwd, "test-results"), /\.xml$/) ?? firstXmlIn((0, import_node_path3.join)(cwd, "reports"), /^junit.*\.xml$/) ?? firstXmlIn((0, import_node_path3.join)(cwd, "target", "surefire-reports"), /\.xml$/);
|
|
966
1211
|
}
|
|
@@ -986,8 +1231,8 @@ async function reportToQuikTest(params) {
|
|
|
986
1231
|
try {
|
|
987
1232
|
console.log(`reporting ${report}`);
|
|
988
1233
|
const ciUrl = buildCiUrl(githubServerUrl, githubRepository, githubRunId);
|
|
989
|
-
const { readFileSync:
|
|
990
|
-
const body =
|
|
1234
|
+
const { readFileSync: readFileSync7 } = await import("node:fs");
|
|
1235
|
+
const body = readFileSync7(report);
|
|
991
1236
|
let url = `${resultsUrl}?ciUrl=${encodeURIComponent(ciUrl)}&automationIdFormat=bracket`;
|
|
992
1237
|
if (generatedFiles) {
|
|
993
1238
|
url += `&generatedFiles=${encodeURIComponent(generatedFiles.join(","))}`;
|
|
@@ -1077,9 +1322,11 @@ function parseJUnitOutcomes(xml) {
|
|
|
1077
1322
|
TESTCASE_RE.lastIndex = 0;
|
|
1078
1323
|
while (match = TESTCASE_RE.exec(xml)) {
|
|
1079
1324
|
const [, attrs, body = ""] = match;
|
|
1080
|
-
const
|
|
1081
|
-
if (!
|
|
1082
|
-
const
|
|
1325
|
+
const rawName = NAME_ATTR_RE.exec(attrs)?.[1];
|
|
1326
|
+
if (!rawName) continue;
|
|
1327
|
+
const name = unescapeXmlEntities(rawName);
|
|
1328
|
+
const rawClassname = CLASSNAME_ATTR_RE.exec(attrs)?.[1];
|
|
1329
|
+
const classname = rawClassname === void 0 ? void 0 : unescapeXmlEntities(rawClassname);
|
|
1083
1330
|
const id = classname ? `${classname}::${name}` : name;
|
|
1084
1331
|
const failed = FAILURE_CHILD_RE.test(body);
|
|
1085
1332
|
outcomes.set(id, !failed);
|
|
@@ -1360,8 +1607,110 @@ async function runBaselineCheck(params) {
|
|
|
1360
1607
|
}
|
|
1361
1608
|
}
|
|
1362
1609
|
|
|
1363
|
-
// src/
|
|
1610
|
+
// src/failureArtifact.ts
|
|
1364
1611
|
var import_node_fs7 = require("node:fs");
|
|
1612
|
+
var import_node_path5 = require("node:path");
|
|
1613
|
+
|
|
1614
|
+
// src/failureReason.ts
|
|
1615
|
+
var MAX_FAILURE_REASON = 1e3;
|
|
1616
|
+
var MIN_SECRET_LENGTH = 8;
|
|
1617
|
+
var CREDENTIAL_PATTERNS = [
|
|
1618
|
+
/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}/g,
|
|
1619
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}/g,
|
|
1620
|
+
/x-access-token:[^@\s'"]+/gi,
|
|
1621
|
+
// Only the header form ("Authorization: basic <token>"), or a bearer value that contains a digit: a bare
|
|
1622
|
+
// "basic authentication" or "bearer of the news" is prose, not a credential.
|
|
1623
|
+
/\bauthorization:\s*(?:basic|bearer|token)\s+[^\s'"]{8,}/gi,
|
|
1624
|
+
/\bbearer\s+(?=[A-Za-z0-9._~+/=-]*\d)[A-Za-z0-9._~+/=-]{12,}/gi,
|
|
1625
|
+
// A signed JWT is three dot-separated segments; QuikPilot's own run token is an encrypted JWT (alg dir) of five, the
|
|
1626
|
+
// second one empty. The run token is also matched by exact value, but one that has expired or rotated is not the
|
|
1627
|
+
// current value, so its shape has to catch it.
|
|
1628
|
+
/\beyJ[A-Za-z0-9_-]{8,}(?:\.[A-Za-z0-9_-]*){2,4}/g
|
|
1629
|
+
];
|
|
1630
|
+
function failureSummary(err) {
|
|
1631
|
+
if (err instanceof RuntimeReplyError) return err.summary;
|
|
1632
|
+
return err instanceof Error ? err.message : String(err);
|
|
1633
|
+
}
|
|
1634
|
+
function scrubSecrets(message, secrets = []) {
|
|
1635
|
+
let text = message;
|
|
1636
|
+
for (const secret of secrets) {
|
|
1637
|
+
if (secret.length >= MIN_SECRET_LENGTH) text = text.split(secret).join("***");
|
|
1638
|
+
}
|
|
1639
|
+
for (const pattern of CREDENTIAL_PATTERNS) text = text.replace(pattern, "***");
|
|
1640
|
+
return text;
|
|
1641
|
+
}
|
|
1642
|
+
function describeFailureReason(message, secrets = []) {
|
|
1643
|
+
let text = scrubSecrets(message, secrets);
|
|
1644
|
+
text = text.replace(/\s*\n\s*/g, " | ").trim();
|
|
1645
|
+
return text.length > MAX_FAILURE_REASON ? `${text.slice(0, MAX_FAILURE_REASON)}\u2026` : text;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// src/failureArtifact.ts
|
|
1649
|
+
var FAILURE_ARTIFACT_DIR = "quikpilot-failure";
|
|
1650
|
+
var REPORT_COPY = "final-test-report.xml";
|
|
1651
|
+
var MAX_REPORT_BYTES = 5 * 1024 * 1024;
|
|
1652
|
+
var MAX_FILES = 200;
|
|
1653
|
+
var RUNNER_VERSION = true ? "9.0.0" : "dev";
|
|
1654
|
+
function safeDestination(base, repoPath) {
|
|
1655
|
+
if (!repoPath || (0, import_node_path5.isAbsolute)(repoPath)) return null;
|
|
1656
|
+
const destination = (0, import_node_path5.resolve)(base, repoPath);
|
|
1657
|
+
const rel = (0, import_node_path5.relative)(base, destination);
|
|
1658
|
+
if (!rel || rel.startsWith("..") || (0, import_node_path5.isAbsolute)(rel)) return null;
|
|
1659
|
+
return destination;
|
|
1660
|
+
}
|
|
1661
|
+
function writeFailureArtifact(input) {
|
|
1662
|
+
if (input.results.length === 0) return { written: false };
|
|
1663
|
+
try {
|
|
1664
|
+
const base = (0, import_node_path5.resolve)(input.cwd, FAILURE_ARTIFACT_DIR);
|
|
1665
|
+
(0, import_node_fs7.rmSync)(base, { recursive: true, force: true });
|
|
1666
|
+
(0, import_node_fs7.mkdirSync)(base, { recursive: true });
|
|
1667
|
+
const files = [];
|
|
1668
|
+
const skipped = [];
|
|
1669
|
+
for (const result of input.results) {
|
|
1670
|
+
const destination = files.length < MAX_FILES ? safeDestination(base, result.targetPath) : null;
|
|
1671
|
+
if (!destination) {
|
|
1672
|
+
skipped.push(result.targetPath);
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path5.dirname)(destination), { recursive: true });
|
|
1676
|
+
(0, import_node_fs7.writeFileSync)(destination, result.newContent);
|
|
1677
|
+
files.push({ path: result.targetPath, bytes: Buffer.byteLength(result.newContent) });
|
|
1678
|
+
}
|
|
1679
|
+
let testReport = "none";
|
|
1680
|
+
if (input.testReport) {
|
|
1681
|
+
if (input.testReport.byteLength > MAX_REPORT_BYTES) {
|
|
1682
|
+
testReport = "too-large";
|
|
1683
|
+
} else {
|
|
1684
|
+
(0, import_node_fs7.writeFileSync)((0, import_node_path5.join)(base, REPORT_COPY), input.testReport);
|
|
1685
|
+
testReport = "included";
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
const manifest = {
|
|
1689
|
+
runId: input.runId,
|
|
1690
|
+
issueKey: input.issueKey,
|
|
1691
|
+
runnerVersion: RUNNER_VERSION,
|
|
1692
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1693
|
+
targets: input.targets.map((t) => ({
|
|
1694
|
+
path: t.path,
|
|
1695
|
+
kind: t.kind,
|
|
1696
|
+
...t.automationIds && t.automationIds.length > 0 ? { automationIds: t.automationIds } : {}
|
|
1697
|
+
})),
|
|
1698
|
+
files,
|
|
1699
|
+
skipped,
|
|
1700
|
+
testReport,
|
|
1701
|
+
failureReasons: input.failureReasons
|
|
1702
|
+
};
|
|
1703
|
+
const text = JSON.stringify(manifest, (_key, value) => typeof value === "string" ? scrubSecrets(value, input.secrets) : value, 2);
|
|
1704
|
+
(0, import_node_fs7.writeFileSync)((0, import_node_path5.join)(base, "manifest.json"), text);
|
|
1705
|
+
return { written: (0, import_node_fs7.existsSync)((0, import_node_path5.join)(base, "manifest.json")) };
|
|
1706
|
+
} catch (err) {
|
|
1707
|
+
console.warn(`::warning::Could not keep the failed run's files: ${err instanceof Error ? err.message : String(err)}`);
|
|
1708
|
+
return { written: false };
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// src/scopeCheck.ts
|
|
1713
|
+
var import_node_fs8 = require("node:fs");
|
|
1365
1714
|
var ROUTE_METHOD_RE = /\b(GET|POST|PUT|PATCH|DELETE)\b/i;
|
|
1366
1715
|
var ROUTE_PATH_RE = /\/[A-Za-z0-9_\-{}[\]]+(?:\/[A-Za-z0-9_\-:{}[\]]+)+/;
|
|
1367
1716
|
var ROUTE_EXPORT_RE = /\bexport\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\s*\(/;
|
|
@@ -1418,7 +1767,7 @@ function checkApiScope(results, targets, apiChanges) {
|
|
|
1418
1767
|
} else {
|
|
1419
1768
|
let currentContent;
|
|
1420
1769
|
try {
|
|
1421
|
-
currentContent = (0,
|
|
1770
|
+
currentContent = (0, import_node_fs8.readFileSync)(targetPath, "utf8");
|
|
1422
1771
|
} catch {
|
|
1423
1772
|
currentContent = "";
|
|
1424
1773
|
}
|
|
@@ -1437,7 +1786,7 @@ function describeScopeViolations(check) {
|
|
|
1437
1786
|
}
|
|
1438
1787
|
|
|
1439
1788
|
// src/targeting.ts
|
|
1440
|
-
var
|
|
1789
|
+
var import_node_path6 = require("node:path");
|
|
1441
1790
|
var CONTRACT_VERSION2 = "test-targets-1";
|
|
1442
1791
|
function listWorkspaceFiles(workspaceRoot) {
|
|
1443
1792
|
return listFiles(workspaceRoot ?? ".");
|
|
@@ -1466,15 +1815,15 @@ function validateTarget(target, workspaceRoot, workspaceListing, deniedPaths) {
|
|
|
1466
1815
|
}
|
|
1467
1816
|
return;
|
|
1468
1817
|
}
|
|
1469
|
-
const root = (0,
|
|
1470
|
-
const rel = (0,
|
|
1471
|
-
if (rel.startsWith("..") || (0,
|
|
1818
|
+
const root = (0, import_node_path6.resolve)(workspaceRoot ?? ".");
|
|
1819
|
+
const rel = (0, import_node_path6.relative)(root, (0, import_node_path6.resolve)(target.path));
|
|
1820
|
+
if (rel.startsWith("..") || (0, import_node_path6.isAbsolute)(rel)) {
|
|
1472
1821
|
throw new Error(
|
|
1473
1822
|
`pilot.targeting named a NEW_FILE path that escapes the workspace root: ${target.path}`
|
|
1474
1823
|
);
|
|
1475
1824
|
}
|
|
1476
1825
|
}
|
|
1477
|
-
async function runTargeting(gateway, design, workspaceRoot, workspaceListing, requiredTests, deniedPaths, testDiscovery) {
|
|
1826
|
+
async function runTargeting(gateway, design, workspaceRoot, workspaceListing, requiredTests, deniedPaths, testDiscovery, outOfScope = []) {
|
|
1478
1827
|
const contextData = {
|
|
1479
1828
|
// NESTED, NOT FLATTENED — a second vocabulary for the design shape is exactly what
|
|
1480
1829
|
// QUIKTR-1034 cost.
|
|
@@ -1489,6 +1838,9 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, re
|
|
|
1489
1838
|
if (deniedPaths.length > 0) {
|
|
1490
1839
|
contextData.deniedPaths = deniedPaths;
|
|
1491
1840
|
}
|
|
1841
|
+
if (outOfScope.length > 0) {
|
|
1842
|
+
contextData.outOfScope = outOfScope;
|
|
1843
|
+
}
|
|
1492
1844
|
contextData.testDiscovery = testDiscovery;
|
|
1493
1845
|
const res = await fetchWithRetry(`${gateway.baseUrl}/ai/execute`, {
|
|
1494
1846
|
method: "POST",
|
|
@@ -1511,16 +1863,28 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, re
|
|
|
1511
1863
|
return { outcome: "NO_TARGET", reason: body.noTargetReason ?? null };
|
|
1512
1864
|
}
|
|
1513
1865
|
if (body.outcome !== "TARGETED" || !Array.isArray(body.targets)) {
|
|
1514
|
-
throw new
|
|
1866
|
+
throw new RuntimeReplyError(
|
|
1867
|
+
`pilot.targeting returned an unexpected shape: ${JSON.stringify(body)}`,
|
|
1868
|
+
"pilot.targeting returned an unexpected shape"
|
|
1869
|
+
);
|
|
1515
1870
|
}
|
|
1516
1871
|
const listing = new Set(workspaceListing);
|
|
1517
1872
|
for (const target of body.targets) {
|
|
1518
1873
|
validateTarget(target, workspaceRoot, listing, deniedPaths);
|
|
1519
1874
|
}
|
|
1875
|
+
const correctedKinds = [];
|
|
1876
|
+
const targets = body.targets.map((target) => {
|
|
1877
|
+
if (target.kind === "NEW_FILE" && listing.has(target.path)) {
|
|
1878
|
+
correctedKinds.push(target.path);
|
|
1879
|
+
return { ...target, kind: "EXISTING" };
|
|
1880
|
+
}
|
|
1881
|
+
return target;
|
|
1882
|
+
});
|
|
1520
1883
|
return {
|
|
1521
1884
|
outcome: "TARGETED",
|
|
1522
|
-
targets
|
|
1523
|
-
skippedCases: body.skippedCases ?? []
|
|
1885
|
+
targets,
|
|
1886
|
+
skippedCases: body.skippedCases ?? [],
|
|
1887
|
+
...correctedKinds.length > 0 ? { correctedKinds } : {}
|
|
1524
1888
|
};
|
|
1525
1889
|
}
|
|
1526
1890
|
|
|
@@ -1683,6 +2047,19 @@ async function exchangeForRunToken(tokenUrl, oidcToken) {
|
|
|
1683
2047
|
return body.data.runToken;
|
|
1684
2048
|
}
|
|
1685
2049
|
|
|
2050
|
+
// src/vanishedTests.ts
|
|
2051
|
+
var MAX_VANISHED_NAMES = 50;
|
|
2052
|
+
function findVanishedTests(baseline, after) {
|
|
2053
|
+
if (!baseline.results || !after.results) return null;
|
|
2054
|
+
const missing = [...baseline.results.keys()].filter((id) => !after.results.has(id));
|
|
2055
|
+
if (missing.length === 0) return null;
|
|
2056
|
+
return { names: missing.slice(0, MAX_VANISHED_NAMES), total: missing.length };
|
|
2057
|
+
}
|
|
2058
|
+
function describeVanishedTests(vanished) {
|
|
2059
|
+
const noun = vanished.total === 1 ? "1 test" : `${vanished.total} tests`;
|
|
2060
|
+
return `${noun} that ran before this change ${vanished.total === 1 ? "is" : "are"} missing from the final test report; the pull request lists ${vanished.total === 1 ? "it" : "them"}.`;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
1686
2063
|
// src/run.ts
|
|
1687
2064
|
async function run(env2) {
|
|
1688
2065
|
let hardFailure = false;
|
|
@@ -1690,16 +2067,27 @@ async function run(env2) {
|
|
|
1690
2067
|
let branch;
|
|
1691
2068
|
let runToken;
|
|
1692
2069
|
const warnings = [];
|
|
2070
|
+
const failureReasons = [];
|
|
2071
|
+
const recordFailure = (err) => {
|
|
2072
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2073
|
+
console.error(message);
|
|
2074
|
+
const reason = describeFailureReason(failureSummary(err), runToken ? [runToken] : []);
|
|
2075
|
+
if (reason) failureReasons.push(reason);
|
|
2076
|
+
};
|
|
1693
2077
|
let targeted = false;
|
|
1694
2078
|
let generatedFilePaths = [];
|
|
2079
|
+
let generatedResults = [];
|
|
2080
|
+
let finalTestReport = null;
|
|
1695
2081
|
let targetingResult;
|
|
1696
2082
|
let baselineFindings;
|
|
2083
|
+
let vanishedTests;
|
|
1697
2084
|
const contextLog = [];
|
|
1698
2085
|
try {
|
|
1699
2086
|
runToken = await exchangeForRunToken(env2.tokenUrl, await fetchGithubOidcToken());
|
|
1700
2087
|
packet = await fetchPacket(env2.packetUrl, runToken, env2.githubRunId);
|
|
1701
2088
|
checkoutTargetBranch(packet.targetBranch);
|
|
1702
2089
|
const workspaceListing = listWorkspaceFiles(packet.workspaceRoot);
|
|
2090
|
+
const outOfScope = (packet.outOfScope ?? []).map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
1703
2091
|
const targeting = await runTargeting(
|
|
1704
2092
|
packet.gateway,
|
|
1705
2093
|
packet.design,
|
|
@@ -1707,9 +2095,18 @@ async function run(env2) {
|
|
|
1707
2095
|
workspaceListing,
|
|
1708
2096
|
packet.requiredTests,
|
|
1709
2097
|
packet.guardrails.deniedPaths,
|
|
1710
|
-
packet.testDiscovery
|
|
2098
|
+
packet.testDiscovery,
|
|
2099
|
+
outOfScope
|
|
1711
2100
|
);
|
|
1712
2101
|
targetingResult = targeting;
|
|
2102
|
+
if (targeting.outcome === "TARGETED") {
|
|
2103
|
+
for (const path of targeting.correctedKinds ?? []) {
|
|
2104
|
+
const w = `QuikPilot named ${path} as a new file, but it already exists in the repository. It was treated as an existing file so its current content was kept in view.`;
|
|
2105
|
+
warnings.push(w);
|
|
2106
|
+
console.warn(`QuikPilot targeting warning (not blocking): ${w}`);
|
|
2107
|
+
console.log(JSON.stringify({ event: "targeting.kind_corrected", path }));
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
1713
2110
|
if (targeting.outcome === "NO_TARGET") {
|
|
1714
2111
|
writePhase0Marker(env2.runId);
|
|
1715
2112
|
runCommand(packet.commands.setup, "Setup");
|
|
@@ -1755,9 +2152,11 @@ async function run(env2) {
|
|
|
1755
2152
|
packet.guardrails.deniedPaths,
|
|
1756
2153
|
judgedByPool,
|
|
1757
2154
|
warnings,
|
|
1758
|
-
contextLog
|
|
2155
|
+
contextLog,
|
|
2156
|
+
outOfScope
|
|
1759
2157
|
)
|
|
1760
2158
|
]);
|
|
2159
|
+
generatedResults = results;
|
|
1761
2160
|
const depCheck = checkImportDependencies(
|
|
1762
2161
|
results,
|
|
1763
2162
|
readDeclaredDependencies(results, packet.workspaceRoot),
|
|
@@ -1803,6 +2202,13 @@ async function run(env2) {
|
|
|
1803
2202
|
}
|
|
1804
2203
|
runCommand(packet.commands.lint, "Lint");
|
|
1805
2204
|
const after = captureTestSnapshot(packet.commands.test);
|
|
2205
|
+
finalTestReport = readTestReportBytes();
|
|
2206
|
+
vanishedTests = findVanishedTests(baseline, after) ?? void 0;
|
|
2207
|
+
if (vanishedTests) {
|
|
2208
|
+
const w = describeVanishedTests(vanishedTests);
|
|
2209
|
+
warnings.push(w);
|
|
2210
|
+
console.warn(`QuikPilot test warning (not blocking): ${w}`);
|
|
2211
|
+
}
|
|
1806
2212
|
const check = checkForRegressions(baseline, after);
|
|
1807
2213
|
if (check.blocked) {
|
|
1808
2214
|
throw new Error(
|
|
@@ -1818,7 +2224,7 @@ async function run(env2) {
|
|
|
1818
2224
|
}
|
|
1819
2225
|
} catch (err) {
|
|
1820
2226
|
hardFailure = true;
|
|
1821
|
-
|
|
2227
|
+
recordFailure(err);
|
|
1822
2228
|
}
|
|
1823
2229
|
if (!runToken) {
|
|
1824
2230
|
console.error("No run token \u2014 the OIDC exchange failed before QuikTrack could be reached.");
|
|
@@ -1855,7 +2261,23 @@ async function run(env2) {
|
|
|
1855
2261
|
branch = result.branch;
|
|
1856
2262
|
} catch (err) {
|
|
1857
2263
|
hardFailure = true;
|
|
1858
|
-
|
|
2264
|
+
recordFailure(err);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
if (hardFailure && generatedResults.length > 0) {
|
|
2268
|
+
try {
|
|
2269
|
+
writeFailureArtifact({
|
|
2270
|
+
cwd: process.cwd(),
|
|
2271
|
+
runId: env2.runId,
|
|
2272
|
+
issueKey: packet?.issueKey ?? null,
|
|
2273
|
+
targets: targetingResult?.outcome === "TARGETED" ? targetingResult.targets : [],
|
|
2274
|
+
results: generatedResults,
|
|
2275
|
+
testReport: finalTestReport,
|
|
2276
|
+
failureReasons,
|
|
2277
|
+
secrets: runToken ? [runToken] : []
|
|
2278
|
+
});
|
|
2279
|
+
} catch (err) {
|
|
2280
|
+
console.warn(`::warning::Could not keep the failed run's files: ${err instanceof Error ? err.message : String(err)}`);
|
|
1859
2281
|
}
|
|
1860
2282
|
}
|
|
1861
2283
|
await reportBack({
|
|
@@ -1866,7 +2288,9 @@ async function run(env2) {
|
|
|
1866
2288
|
warnings,
|
|
1867
2289
|
// Omitted when targeting itself never produced a result (nothing exists to record).
|
|
1868
2290
|
targeting: targetingResult ? buildTargetingRecord(targetingResult, contextLog) : void 0,
|
|
1869
|
-
baselineFindings
|
|
2291
|
+
baselineFindings,
|
|
2292
|
+
vanishedTests,
|
|
2293
|
+
unresolved: failureReasons.length > 0 ? failureReasons : void 0
|
|
1870
2294
|
});
|
|
1871
2295
|
return { hardFailure };
|
|
1872
2296
|
}
|