mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix: make auto-review and PR triage fail safely (#114745)
* fix: make auto-review and PR triage fail safely * test: cover fail-closed PR merge review gates * fix: preserve GitHub pending-check review semantics
This commit is contained in:
committed by
GitHub
parent
1c0ccdddcd
commit
6f7edb3695
@@ -294,11 +294,12 @@ function fakeGithub(options: {
|
||||
>;
|
||||
checksByRef?: Record<string, FakeCheckRun[]>;
|
||||
workflowRunsById?: Record<number, FakeWorkflowRun>;
|
||||
pullsGetByNumber?: Record<number, Record<string, unknown>>;
|
||||
pullsGetByNumber?: Record<number, Record<string, unknown> | Array<Record<string, unknown>>>;
|
||||
events?: Array<Record<string, unknown>>;
|
||||
pageSize?: number;
|
||||
}) {
|
||||
const calls: FakeCall[] = [];
|
||||
const pullsGetCallCounts = new Map<number, number>();
|
||||
const record = (method: string, args: Record<string, unknown>) => {
|
||||
calls.push({ method, args });
|
||||
};
|
||||
@@ -351,9 +352,13 @@ function fakeGithub(options: {
|
||||
list: { endpointName: "pulls.list" },
|
||||
get: (args: Record<string, unknown>) => {
|
||||
record("pulls.get", args);
|
||||
const match =
|
||||
options.pullsGetByNumber?.[args.pull_number as number] ??
|
||||
options.prs.find((entry) => entry.number === args.pull_number);
|
||||
const pullNumber = args.pull_number as number;
|
||||
const configured = options.pullsGetByNumber?.[pullNumber];
|
||||
const callIndex = pullsGetCallCounts.get(pullNumber) ?? 0;
|
||||
pullsGetCallCounts.set(pullNumber, callIndex + 1);
|
||||
const match = Array.isArray(configured)
|
||||
? configured[Math.min(callIndex, configured.length - 1)]
|
||||
: (configured ?? options.prs.find((entry) => entry.number === pullNumber));
|
||||
return Promise.resolve({ data: match });
|
||||
},
|
||||
update: (args: Record<string, unknown>) => {
|
||||
@@ -544,6 +549,54 @@ describe("runPrCiSweeper", () => {
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not spend the re-fire budget on PRs that change during revalidation", async () => {
|
||||
const dropped = Array.from({ length: 11 }, (_, index) => ({
|
||||
...pr(),
|
||||
number: 200 + index,
|
||||
state: "open",
|
||||
head: { sha: index.toString(16).padStart(2, "0").repeat(20) },
|
||||
}));
|
||||
const pullsGetByNumber = Object.fromEntries(
|
||||
dropped
|
||||
.slice(0, 10)
|
||||
.map((candidate) => [
|
||||
candidate.number,
|
||||
[candidate, { ...candidate, head: { sha: "f".repeat(40) } }],
|
||||
]),
|
||||
);
|
||||
const { github, calls } = fakeGithub({ prs: dropped, runsBySha: {}, pullsGetByNumber });
|
||||
const { core: loggedCore, logs } = recordingCore();
|
||||
|
||||
const results = await runPrCiSweeper({
|
||||
github: github as never,
|
||||
context: context as never,
|
||||
core: loggedCore as never,
|
||||
now: NOW,
|
||||
});
|
||||
|
||||
expect(results.slice(0, 10)).toEqual(
|
||||
dropped.slice(0, 10).map((candidate) => ({
|
||||
number: candidate.number,
|
||||
sha: candidate.head.sha.slice(0, 12),
|
||||
action: "skip",
|
||||
reason: "changed-during-sweep",
|
||||
})),
|
||||
);
|
||||
expect(results.at(-1)).toEqual({
|
||||
number: 210,
|
||||
sha: "0a".repeat(6),
|
||||
action: "refire",
|
||||
reason: "ci-run-missing",
|
||||
});
|
||||
expect(calls.filter((call) => call.method === "pulls.update").map((call) => call.args)).toEqual(
|
||||
[
|
||||
{ owner: "openclaw", repo: "openclaw", pull_number: 210, state: "closed" },
|
||||
{ owner: "openclaw", repo: "openclaw", pull_number: 210, state: "open" },
|
||||
],
|
||||
);
|
||||
expect(logs.at(-1)).toContain("1 re-fire");
|
||||
});
|
||||
|
||||
it("stops listing pages once creation dates cross the lookback", async () => {
|
||||
const recent = { ...pr(), number: 30, state: "open", head: { sha: "7".repeat(40) } };
|
||||
const oldA = {
|
||||
|
||||
@@ -13,10 +13,12 @@ const describePosix = process.platform === "win32" ? describe.skip : describe;
|
||||
type MergeScenario = {
|
||||
auto?: boolean;
|
||||
autoResult?: "enabled" | "inconclusive" | "unavailable";
|
||||
checks?: "fail" | "green";
|
||||
checks?: "fail" | "green" | "pending";
|
||||
existingAutoMethod?: "" | "MERGE" | "REBASE" | "SQUASH";
|
||||
mergeStateStatus?: string;
|
||||
mergeable?: string;
|
||||
recommendation?: "ready" | "needs_work";
|
||||
reviewArtifacts?: "valid" | "invalid";
|
||||
};
|
||||
|
||||
function runMerge(scenario: MergeScenario = {}) {
|
||||
@@ -59,13 +61,27 @@ function runMerge(scenario: MergeScenario = {}) {
|
||||
const checks =
|
||||
scenario.checks === "fail"
|
||||
? [{ name: "CI", bucket: "fail", state: "FAILURE" }]
|
||||
: [{ name: "CI", bucket: "pass", state: "SUCCESS" }];
|
||||
: scenario.checks === "pending"
|
||||
? [{ name: "CI", bucket: "pending", state: "IN_PROGRESS" }]
|
||||
: [{ name: "CI", bucket: "pass", state: "SUCCESS" }];
|
||||
|
||||
const shell = `
|
||||
set -euo pipefail
|
||||
source "$OPENCLAW_TEST_MERGE_SCRIPT"
|
||||
enter_worktree() { :; }
|
||||
require_artifact() { :; }
|
||||
validate_review_artifact_data() {
|
||||
if [ "$OPENCLAW_TEST_REVIEW_ARTIFACTS" != "valid" ]; then
|
||||
echo 'review artifact validation failed' >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
require_ready_review_recommendation() {
|
||||
if [ "$OPENCLAW_TEST_REVIEW_RECOMMENDATION" != "ready" ]; then
|
||||
echo 'review recommendation is not ready' >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
verify_prep_branch_matches_prepared_head() { :; }
|
||||
mark_pr_operation_side_effects_started() { :; }
|
||||
mainline_drift_requires_sync() { return 1; }
|
||||
@@ -90,7 +106,10 @@ gh() {
|
||||
case "$1 $2" in
|
||||
"pr checks")
|
||||
case " $* " in
|
||||
*" --json "*) printf '%s\\n' "$OPENCLAW_TEST_CHECKS_JSON" ;;
|
||||
*" --json "*)
|
||||
printf '%s\\n' "$OPENCLAW_TEST_CHECKS_JSON"
|
||||
return "$OPENCLAW_TEST_CHECKS_EXIT_STATUS"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
"pr view")
|
||||
@@ -161,6 +180,7 @@ merge_run 123 "$OPENCLAW_TEST_AUTO_REQUESTED"
|
||||
OPENCLAW_TEST_AUTO_REQUESTED: scenario.auto ? "true" : "false",
|
||||
OPENCLAW_TEST_AUTO_RESULT: scenario.autoResult ?? "enabled",
|
||||
OPENCLAW_TEST_AUTO_STATE: autoState,
|
||||
OPENCLAW_TEST_CHECKS_EXIT_STATUS: scenario.checks === "pending" ? "8" : "0",
|
||||
OPENCLAW_TEST_CHECKS_JSON: JSON.stringify(checks),
|
||||
OPENCLAW_TEST_DISABLED_AUTO_META: disabledAutoMeta,
|
||||
OPENCLAW_TEST_GH_CALLS: calls,
|
||||
@@ -169,6 +189,8 @@ merge_run 123 "$OPENCLAW_TEST_AUTO_REQUESTED"
|
||||
OPENCLAW_TEST_MERGE_STATE_STATUS: scenario.mergeStateStatus ?? "BEHIND",
|
||||
OPENCLAW_TEST_POST_AUTO_META: postAutoMeta,
|
||||
OPENCLAW_TEST_PRE_AUTO_META: preAutoMeta,
|
||||
OPENCLAW_TEST_REVIEW_ARTIFACTS: scenario.reviewArtifacts ?? "valid",
|
||||
OPENCLAW_TEST_REVIEW_RECOMMENDATION: scenario.recommendation ?? "ready",
|
||||
OPENCLAW_TEST_ROOT: root,
|
||||
},
|
||||
});
|
||||
@@ -179,6 +201,22 @@ merge_run 123 "$OPENCLAW_TEST_AUTO_REQUESTED"
|
||||
}
|
||||
|
||||
describePosix("scripts/pr merge-run", () => {
|
||||
it("refuses to merge when review artifact validation fails", () => {
|
||||
const result = runMerge({ reviewArtifacts: "invalid" });
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain("review artifact validation failed");
|
||||
expect(result.calls).not.toContain("pr merge");
|
||||
});
|
||||
|
||||
it("refuses to merge when the review recommendation is not ready", () => {
|
||||
const result = runMerge({ recommendation: "needs_work" });
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain("review recommendation is not ready");
|
||||
expect(result.calls).not.toContain("pr merge");
|
||||
});
|
||||
|
||||
it("does not enable auto-merge when exact-head required CI is failing", () => {
|
||||
const result = runMerge({ auto: true, checks: "fail" });
|
||||
|
||||
@@ -187,6 +225,15 @@ describePosix("scripts/pr merge-run", () => {
|
||||
expect(result.calls).not.toContain("pr merge");
|
||||
});
|
||||
|
||||
it("does not mistake pending required checks for a GitHub API failure", () => {
|
||||
const result = runMerge({ auto: true, checks: "pending" });
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toContain("Required checks are still pending.");
|
||||
expect(result.stderr).not.toContain("unable to verify the required GitHub checks");
|
||||
expect(result.calls).not.toContain("pr merge");
|
||||
});
|
||||
|
||||
it("fails a conflicting PR without attempting auto-merge", () => {
|
||||
const result = runMerge({
|
||||
auto: true,
|
||||
|
||||
@@ -535,6 +535,50 @@ describe("lease-retry gate stamp refresh", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("prepare review readiness", () => {
|
||||
it("rejects invalid review artifacts before any preparation side effects", () => {
|
||||
const repoDir = makeTempDir("openclaw-pr-prepare-invalid-review-");
|
||||
mkdirSync(join(repoDir, ".local"));
|
||||
const result = runGatesBash(
|
||||
[
|
||||
"review_validate_artifacts() { echo 'invalid review artifacts'; return 1; }",
|
||||
"require_ready_review_recommendation() { touch .local/readiness-called; }",
|
||||
"mark_pr_operation_side_effects_started() { touch .local/side-effects; }",
|
||||
"enter_worktree() { touch .local/worktree-entered; }",
|
||||
"prepare_init 4242",
|
||||
].join("\n"),
|
||||
{ cwd: repoDir, sourcePrepareCore: true },
|
||||
);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toContain("invalid review artifacts");
|
||||
expect(existsSync(join(repoDir, ".local", "readiness-called"))).toBe(false);
|
||||
expect(existsSync(join(repoDir, ".local", "side-effects"))).toBe(false);
|
||||
expect(existsSync(join(repoDir, ".local", "worktree-entered"))).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects a non-ready review before taking the operation lock past validation", () => {
|
||||
const repoDir = makeTempDir("openclaw-pr-prepare-not-ready-");
|
||||
mkdirSync(join(repoDir, ".local"));
|
||||
const result = runGatesBash(
|
||||
[
|
||||
"review_validate_artifacts() { touch .local/review-validated; }",
|
||||
"require_ready_review_recommendation() { echo 'review is not ready'; return 1; }",
|
||||
"mark_pr_operation_side_effects_started() { touch .local/side-effects; }",
|
||||
"enter_worktree() { touch .local/worktree-entered; }",
|
||||
"prepare_init 4242",
|
||||
].join("\n"),
|
||||
{ cwd: repoDir, sourcePrepareCore: true },
|
||||
);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toContain("review is not ready");
|
||||
expect(existsSync(join(repoDir, ".local", "review-validated"))).toBe(true);
|
||||
expect(existsSync(join(repoDir, ".local", "side-effects"))).toBe(false);
|
||||
expect(existsSync(join(repoDir, ".local", "worktree-entered"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("prepare sync-head transitions", () => {
|
||||
it("publishes only appended fixups when main advances", () => {
|
||||
const repoDir = makeSyncRepo({ needsRebase: true });
|
||||
|
||||
@@ -7,12 +7,19 @@ import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
const reviewScript = join(process.cwd(), "scripts/pr-lib/review.sh");
|
||||
const reviewArtifactsScript = join(process.cwd(), "scripts/pr-lib/review-artifacts.mjs");
|
||||
const mergeScript = join(process.cwd(), "scripts/pr-lib/merge.sh");
|
||||
const describePosix = process.platform === "win32" ? describe.skip : describe;
|
||||
|
||||
function validReview() {
|
||||
return {
|
||||
recommendation: "NEEDS WORK",
|
||||
findings: [],
|
||||
findings: [] as Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
area: string;
|
||||
fix: string;
|
||||
severity: "BLOCKER" | "IMPORTANT" | "NIT";
|
||||
}>,
|
||||
nitSweep: {
|
||||
performed: true,
|
||||
status: "none",
|
||||
@@ -41,7 +48,22 @@ function validReview() {
|
||||
};
|
||||
}
|
||||
|
||||
function runValidation(review: ReturnType<typeof validReview>) {
|
||||
function validReadyReview() {
|
||||
const review = validReview();
|
||||
review.recommendation = "READY FOR /prepare-pr";
|
||||
review.issueValidation.status = "valid";
|
||||
return review;
|
||||
}
|
||||
|
||||
function runValidation(
|
||||
review: ReturnType<typeof validReview>,
|
||||
options: {
|
||||
files?: string[];
|
||||
guardFailure?: boolean;
|
||||
mode?: "pr" | "main";
|
||||
orList?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
const fixtureRoot = tempDirs.make("openclaw-pr-review-validation-");
|
||||
const localDir = join(fixtureRoot, ".local");
|
||||
mkdirSync(localDir);
|
||||
@@ -51,7 +73,10 @@ function runValidation(review: ReturnType<typeof validReview>) {
|
||||
["A)", "B)", "C)", "D)", "E)", "F)", "G)", "H)", "I)", "J)"].join("\n"),
|
||||
);
|
||||
writeFileSync(join(localDir, "pr-meta.env"), "PR_URL=https://example.invalid/pr/42\n");
|
||||
writeFileSync(join(localDir, "pr-meta.json"), '{"files":[]}\n');
|
||||
writeFileSync(
|
||||
join(localDir, "pr-meta.json"),
|
||||
`${JSON.stringify({ files: (options.files ?? []).map((path) => ({ path })) })}\n`,
|
||||
);
|
||||
|
||||
return spawnSync(
|
||||
"bash",
|
||||
@@ -63,9 +88,11 @@ function runValidation(review: ReturnType<typeof validReview>) {
|
||||
'fixture_root="$2"',
|
||||
'enter_worktree() { cd "$fixture_root"; }',
|
||||
'require_artifact() { [ -s "$1" ]; }',
|
||||
"review_guard() { :; }",
|
||||
options.guardFailure
|
||||
? "review_guard() { REVIEW_MODE=pr; echo 'review head guard failed'; return 1; }"
|
||||
: `review_guard() { REVIEW_MODE=${options.mode ?? "pr"}; }`,
|
||||
"print_review_stdout_summary() { :; }",
|
||||
"review_validate_artifacts 42",
|
||||
options.orList ? "review_validate_artifacts 42 || exit 1" : "review_validate_artifacts 42",
|
||||
].join("\n"),
|
||||
"pr-review-artifact-validation",
|
||||
reviewScript,
|
||||
@@ -75,6 +102,47 @@ function runValidation(review: ReturnType<typeof validReview>) {
|
||||
);
|
||||
}
|
||||
|
||||
function runMergeVerification(checks: "api-error" | "invalid-json" | "no-required" | "pending") {
|
||||
const fixtureRoot = tempDirs.make("openclaw-pr-merge-verification-");
|
||||
const localDir = join(fixtureRoot, ".local");
|
||||
const head = "a".repeat(40);
|
||||
mkdirSync(localDir);
|
||||
writeFileSync(join(localDir, "prep.env"), `PREP_HEAD_SHA=${head}\n`);
|
||||
|
||||
const checksResponse =
|
||||
checks === "api-error"
|
||||
? "echo 'GitHub API unavailable' >&2; return 1"
|
||||
: checks === "no-required"
|
||||
? "echo \"no required checks reported on the 'review-branch' branch\" >&2; return 1"
|
||||
: checks === "pending"
|
||||
? `printf '%s\\n' '[{"name":"CI","bucket":"pending","state":"IN_PROGRESS"}]'; return 8`
|
||||
: "printf '%s\\n' 'not valid JSON'";
|
||||
|
||||
return spawnSync(
|
||||
"bash",
|
||||
[
|
||||
"-c",
|
||||
[
|
||||
"set -euo pipefail",
|
||||
'source "$1"',
|
||||
'fixture_root="$2"',
|
||||
'enter_worktree() { cd "$fixture_root"; }',
|
||||
'require_artifact() { [ -s "$1" ]; }',
|
||||
"verify_prep_branch_matches_prepared_head() { :; }",
|
||||
`pr_meta_json() { printf '%s\\n' '{"isDraft":false,"headRefOid":"${head}"}'; }`,
|
||||
"mark_pr_operation_side_effects_started() { :; }",
|
||||
"git() { :; }",
|
||||
`gh() { case "$*" in *"--json name,bucket,state"*) ${checksResponse};; *) return 0;; esac; }`,
|
||||
"merge_verify 42",
|
||||
].join("\n"),
|
||||
"pr-merge-verification",
|
||||
mergeScript,
|
||||
fixtureRoot,
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
}
|
||||
|
||||
describePosix("scripts/pr review artifact validation", () => {
|
||||
it("accepts a valid review artifact", () => {
|
||||
const result = runValidation(validReview());
|
||||
@@ -83,6 +151,146 @@ describePosix("scripts/pr review artifact validation", () => {
|
||||
expect(result.stdout).toContain("review artifacts validated");
|
||||
});
|
||||
|
||||
it("rejects validation from main-baseline mode", () => {
|
||||
const result = runValidation(validReview(), { mode: "main" });
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toContain(
|
||||
"Review artifact validation requires the reviewed PR head, not main-baseline mode.",
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves head-guard failures when preparation calls validation in an OR-list", () => {
|
||||
const result = runValidation(validReview(), { guardFailure: true, orList: true });
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toContain("review head guard failed");
|
||||
expect(result.stdout).not.toContain("review artifacts validated");
|
||||
});
|
||||
|
||||
it.each(["BLOCKER", "IMPORTANT"] as const)(
|
||||
"rejects a ready review containing a %s finding",
|
||||
(severity) => {
|
||||
const review = validReadyReview();
|
||||
review.findings.push({
|
||||
id: "review-finding",
|
||||
title: "Actionable review finding",
|
||||
area: "runtime",
|
||||
fix: "Resolve the finding before preparing the PR.",
|
||||
severity,
|
||||
});
|
||||
|
||||
const result = runValidation(review);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toContain(
|
||||
"READY FOR /prepare-pr cannot include BLOCKER or IMPORTANT findings",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps non-ready findings and failed proof valid for review triage", () => {
|
||||
const review = validReview();
|
||||
review.findings.push({
|
||||
id: "review-finding",
|
||||
title: "Actionable review finding",
|
||||
area: "runtime",
|
||||
fix: "Resolve the finding before preparing the PR.",
|
||||
severity: "IMPORTANT",
|
||||
});
|
||||
review.tests.result = "fail";
|
||||
|
||||
const result = runValidation(review);
|
||||
|
||||
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
|
||||
});
|
||||
|
||||
it("rejects a ready review with failing proof", () => {
|
||||
const review = validReadyReview();
|
||||
review.tests.result = "fail";
|
||||
|
||||
const result = runValidation(review);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toContain("READY FOR /prepare-pr cannot include failing tests");
|
||||
});
|
||||
|
||||
it("permits documentation-only ready reviews without runtime tests", () => {
|
||||
const review = validReadyReview();
|
||||
review.tests.result = "not_run";
|
||||
|
||||
const result = runValidation(review, { files: ["docs/reference/example.md"] });
|
||||
|
||||
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"packages/normalization-core/src/string-normalization.ts",
|
||||
"packages/gateway-protocol/src/schema/approvals.ts",
|
||||
"ui/src/app.ts",
|
||||
])("requires behavioral review for core runtime path %s", (path) => {
|
||||
const result = runValidation(validReview(), { files: [path] });
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toContain(
|
||||
"runtime file changes require behavioralSweep.status=pass|needs_work",
|
||||
);
|
||||
expect(result.stdout).toContain("runtime file changes require at least one branch entry");
|
||||
});
|
||||
|
||||
it("requires passing runtime proof for a ready review", () => {
|
||||
const review = validReadyReview();
|
||||
review.behavioralSweep.status = "pass";
|
||||
review.behavioralSweep.branches.push({
|
||||
path: "ui/src/app.ts",
|
||||
decision: "verified",
|
||||
outcome: "Behavior remains correct.",
|
||||
});
|
||||
review.tests.result = "not_run";
|
||||
|
||||
const result = runValidation(review, { files: ["ui/src/app.ts"] });
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toContain(
|
||||
"READY FOR /prepare-pr on runtime changes requires passing tests",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects merge verification when GitHub cannot verify required checks", () => {
|
||||
const result = runMergeVerification("api-error");
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain("unable to verify the required GitHub checks");
|
||||
expect(result.stderr).toContain("GitHub API unavailable");
|
||||
expect(result.stdout).not.toContain("merge-verify passed");
|
||||
expect(result.stdout).not.toContain("No required checks configured");
|
||||
});
|
||||
|
||||
it("preserves GitHub CLI behavior when a branch has no required checks", () => {
|
||||
const result = runMergeVerification("no-required");
|
||||
|
||||
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
|
||||
expect(result.stdout).toContain("No required checks configured for this PR.");
|
||||
expect(result.stdout).toContain("merge-verify passed for PR #42");
|
||||
});
|
||||
|
||||
it("preserves GitHub CLI pending-check evidence from exit status eight", () => {
|
||||
const result = runMergeVerification("pending");
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toContain("Required checks are still pending.");
|
||||
expect(result.stderr).not.toContain("unable to verify the required GitHub checks");
|
||||
expect(result.stdout).not.toContain("merge-verify passed");
|
||||
});
|
||||
|
||||
it("rejects merge verification when GitHub returns malformed check evidence", () => {
|
||||
const result = runMergeVerification("invalid-json");
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain("GitHub returned invalid required-check evidence");
|
||||
expect(result.stdout).not.toContain("merge-verify passed");
|
||||
});
|
||||
|
||||
it("reports the required branch entry shape without a raw jq error", () => {
|
||||
const review = validReview();
|
||||
review.behavioralSweep.branches = ["src/example.ts"];
|
||||
|
||||
Reference in New Issue
Block a user